├── tests
├── __init__.py
├── ironpython
│ ├── com.mcneel.rhinoceros.plist
│ ├── settings-Scheme__Default.xml
│ └── settings-Scheme__Default_mingbo.xml
├── togeometry.py
└── pythonpath_test.py
├── docs
├── _build
│ ├── .nojekyll
│ ├── README.md
│ └── docs
│ │ └── README.md
├── modules.rst
├── cli
│ └── index.rst
├── README.md
├── index.rst
├── _static
│ └── custom.css
└── _templates
│ └── layout.html
├── requirements.txt
├── ladybug_rhino
├── __init__.py
├── versioning
│ ├── __init__.py
│ ├── release.py
│ ├── userobject.py
│ ├── component.py
│ ├── gather.py
│ ├── diff.py
│ └── export.py
├── __main__.py
├── color.py
├── resourcepath.py
├── visset.py
├── light.py
├── ghpath.py
├── colorize.py
├── config.py
├── openstudio.py
├── download.py
├── text.py
├── planarize.py
├── fromobjects.py
├── fromhoneybee.py
├── bakedisplay.py
├── cli
│ └── __init__.py
├── bakegeometry.py
├── bakeobjects.py
├── fromgeometry.py
└── viewport.py
├── deploy.sh
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── .releaserc.json
├── dev-requirements.txt
├── setup.py
├── README.md
└── .github
└── workflows
└── ci.yaml
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/_build/.nojekyll:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/_build/README.md:
--------------------------------------------------------------------------------
1 | # documentation
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | ladybug-display>=0.5.0
2 |
--------------------------------------------------------------------------------
/docs/_build/docs/README.md:
--------------------------------------------------------------------------------
1 | # documentation
2 |
--------------------------------------------------------------------------------
/ladybug_rhino/__init__.py:
--------------------------------------------------------------------------------
1 | """Ladybug library for communication with Rhino / Grasshopper."""
2 |
--------------------------------------------------------------------------------
/ladybug_rhino/versioning/__init__.py:
--------------------------------------------------------------------------------
1 | """Subpackage for exporting components from Grasshopper."""
2 |
--------------------------------------------------------------------------------
/ladybug_rhino/__main__.py:
--------------------------------------------------------------------------------
1 | from ladybug_rhino.cli import main
2 |
3 | if __name__ == '__main__':
4 | main()
5 |
--------------------------------------------------------------------------------
/docs/modules.rst:
--------------------------------------------------------------------------------
1 | ladybug-rhino
2 | =================
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | ladybug_rhino
8 |
--------------------------------------------------------------------------------
/tests/ironpython/com.mcneel.rhinoceros.plist:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ladybug-tools/ladybug-rhino/HEAD/tests/ironpython/com.mcneel.rhinoceros.plist
--------------------------------------------------------------------------------
/docs/cli/index.rst:
--------------------------------------------------------------------------------
1 | CLI Docs
2 | ========
3 |
4 | Installation
5 | ------------
6 |
7 | To check if the command line is installed correctly use ``ladybug-rhino --help``
8 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | echo "Building distribution"
4 | python -m build
5 | echo "Pushing new version to PyPi"
6 | twine upload dist/* -u $PYPI_USERNAME -p $PYPI_PASSWORD
7 |
--------------------------------------------------------------------------------
/tests/ironpython/settings-Scheme__Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -1764,156,869,646
5 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | test.py
3 | *.gh
4 | .pytest_cache
5 | /__pycache__
6 | .coverage
7 | *.ipynb
8 | .ipynb_checkpoints
9 | .tox
10 | *.egg-info
11 | tox.ini
12 | build
13 | /.cache
14 | /.vscode
15 | .eggs
16 | honeybee.log*
17 | *.code-workspace
18 | .vs
19 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | Contributor Covenant Code of Conduct
2 | =========================================
3 |
4 | This project follows Ladybug Tools contributor covenant code of conduct. See our [contributor covenant code of conduct](https://github.com/ladybug-tools/contributing/blob/master/CODE-OF-CONDUCT.md).
5 |
--------------------------------------------------------------------------------
/tests/ironpython/settings-Scheme__Default_mingbo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Consolas
5 | 12
6 | 0
7 |
8 |
--------------------------------------------------------------------------------
/tests/togeometry.py:
--------------------------------------------------------------------------------
1 | import rhinoinside
2 | rhinoinside.load()
3 | import Rhino
4 | from ladybug_rhino.togeometry import to_point3d
5 | from ladybug_geometry.geometry3d.pointvector import Point3D
6 |
7 |
8 | def test_to_point3d():
9 | """Test the to_point3d method."""
10 | test_pt = Rhino.Geometry.Point3d(5.0, 10.0, 3.0)
11 | assert to_point3d(test_pt) == Point3D(5, 10, 3)
12 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 | ## Usage
3 | For generating the documents locally use commands below from the root folder.
4 |
5 | ```shell
6 | # install dependencies
7 | pip install Sphinx sphinxcontrib-fulltoc sphinx_bootstrap_theme
8 |
9 | # generate rst files for modules
10 | sphinx-apidoc -f -e -d 4 -o ./docs ./ladybug_rhino
11 | # build the documentation under _build/docs folder
12 | sphinx-build -b html ./docs ./docs/_build/docs
13 | ```
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing
2 | ------------
3 | We welcome contributions from anyone, even if you are new to open source we will be happy to help you to get started. Most of the Ladybug Tools developers started learning programming through developing for Ladybug Tools.
4 |
5 | ### Code contribution
6 | This project follows Ladybug Tools contributing guideline. See [contributing to Ladybug Tools projects](https://github.com/ladybug-tools/contributing/blob/master/README.md).
7 |
--------------------------------------------------------------------------------
/.releaserc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "@semantic-release/commit-analyzer",
4 | "@semantic-release/release-notes-generator",
5 | [
6 | "@semantic-release/github",
7 | {
8 | "successComment": false,
9 | "failTitle": false
10 | }
11 | ],
12 | [
13 | "@semantic-release/exec",
14 | {
15 | "publishCmd": "bash deploy.sh"
16 | }
17 | ]
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | pytest==8.3.2;python_version>='3.6'
2 | Sphinx==8.0.2;python_version>='3.6'
3 | sphinx-bootstrap-theme==0.8.1
4 | sphinxcontrib-fulltoc==1.2.0
5 | sphinxcontrib-websupport==2.0.0;python_version>='3.6'
6 | sphinx-click==6.0.0;python_version>='3.6'
7 | twine==6.1.0;python_version>='3.6'
8 | wheel==0.45.1;python_version>='3.6'
9 | setuptools==80.9.0;python_version>='3.6'
10 | build==1.3.0;python_version>='3.6'
11 | pytest==4.6.9;python_version<'3.0'
12 | Sphinx==1.8.5;python_version<'3.0'
13 | sphinxcontrib-websupport==1.1.2;python_version<'3.0'
14 | sphinx-click==4.4.0;python_version<'3.0'
15 | twine==1.13.0;python_version<'3.0'
16 | wheel==0.38.1;python_version<'3.0'
17 | setuptools==44.1.0;python_version<'3.0'
18 | build==0.1.0;python_version<'3.0'
19 | importlib-metadata==2.0.0;python_version<'3.0'
20 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Welcome to Ladybug-rhino's documentation!
2 | =========================================
3 |
4 | .. image:: http://www.ladybug.tools/assets/img/ladybug.png
5 |
6 | Ladybug-rhino is a library for communicating between Ladybug Tools core libraries and Rhinoceros CAD.
7 |
8 | This library is used by both the Grasshopper and Rhino plugins to communicate with
9 | the ladybug core Python library. Note that this library has dependencies
10 | on Rhino SDK and Grasshopper SDK. It is NOT intended to be run with cPython with
11 | the exception of running the CLI.
12 |
13 | Installation
14 | ============
15 |
16 | ``pip install -U ladybug-rhino``
17 |
18 | CLI Docs
19 | =============
20 |
21 | For command line interface documentation and API documentation see the pages below.
22 |
23 |
24 | .. toctree::
25 | :maxdepth: 2
26 |
27 | cli/index
28 |
29 |
30 | ladybug_rhino
31 | =============
32 |
33 | .. toctree::
34 | :maxdepth: 4
35 |
36 | modules
37 |
38 |
39 | Indices and tables
40 | ==================
41 |
42 | * :ref:`genindex`
43 | * :ref:`modindex`
44 | * :ref:`search`
45 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | with open('requirements.txt') as f:
7 | requirements = f.read().splitlines()
8 |
9 | setuptools.setup(
10 | name="ladybug-rhino",
11 | use_scm_version=True,
12 | setup_requires=['setuptools_scm'],
13 | author="Ladybug Tools",
14 | author_email="info@ladybug.tools",
15 | description="A library for communicating between Ladybug Tools core libraries and Rhinoceros CAD.",
16 | long_description=long_description,
17 | long_description_content_type="text/markdown",
18 | url="https://github.com/ladybug-tools/ladybug-rhino",
19 | packages=setuptools.find_packages(exclude=["tests"]),
20 | install_requires=requirements,
21 | entry_points={
22 | "console_scripts": ["ladybug-rhino = ladybug_rhino.cli:main"]
23 | },
24 | classifiers=[
25 | "Programming Language :: Python :: 2.7",
26 | "Programming Language :: Python :: Implementation :: IronPython",
27 | "Operating System :: OS Independent"
28 | ],
29 | license="AGPL-3.0"
30 | )
31 |
--------------------------------------------------------------------------------
/ladybug_rhino/color.py:
--------------------------------------------------------------------------------
1 | """Collection of methods for converting between Ladybug and .NET colors."""
2 |
3 | try:
4 | from System.Drawing import Color
5 | except ImportError as e:
6 | raise ImportError("Failed to import Windows/.NET libraries\n{}".format(e))
7 |
8 |
9 | def color_to_color(color, alpha=255):
10 | """Convert a ladybug color into .NET color.
11 |
12 | Args:
13 | alpha: Optional integer between 1 and 255 for the alpha value of the color.
14 | """
15 | try:
16 | return Color.FromArgb(alpha, color.r, color.g, color.b)
17 | except AttributeError as e:
18 | raise AttributeError('Input must be of type of Color:\n{}'.format(e))
19 |
20 |
21 | def argb_color_to_color(color):
22 | """Convert a ladybug color into .NET color, including the alpha channel."""
23 | try:
24 | return Color.FromArgb(color.a, color.r, color.g, color.b)
25 | except AttributeError as e:
26 | raise AttributeError('Input must be of type of Color:\n{}'.format(e))
27 |
28 |
29 | def gray():
30 | """Get a .NET gray color object. Useful when you need a placeholder color."""
31 | return Color.Gray
32 |
33 |
34 | def black():
35 | """Get a .NET black color object. Useful for things like default text."""
36 | return Color.Black
37 |
--------------------------------------------------------------------------------
/docs/_static/custom.css:
--------------------------------------------------------------------------------
1 | /*
2 | * bootstrap-sphinx.css
3 | * ~~~~~~~~~~~~~~~~~~~~
4 | *
5 | * Sphinx stylesheet -- Bootstrap theme.
6 | */
7 |
8 | /* Overwrite colors */
9 | div.navbar-inverse {
10 | background-color: #EB2227;
11 | border-color: #EB2227;
12 | }
13 | a {
14 | color: #EB2227;
15 | }
16 | a:visited {
17 | color: #Bf0408;
18 | }
19 | code {
20 | color: #Bf0408;
21 | }
22 | div.bs-sidenav a {
23 | color: #333333;
24 | }
25 |
26 | /* Prevent top nav from blocking docs */
27 | div.navbar-fixed-top {
28 | position: absolute;
29 | }
30 |
31 | /* Indent the side nav when in mobile mode */
32 | @media screen and (min-width: 0px) {
33 | div.bs-sidenav ul {
34 | margin-bottom: 0;
35 | padding-left: 5px;
36 | list-style: none;
37 | }
38 | }
39 |
40 | /* Widen and de-indent the side nav when space is restricted */
41 | @media screen and (min-width: 992px) {
42 | .bs-sidenav .nav > .active > ul {
43 | display: block;
44 | }
45 | div.bs-sidenav ul {
46 | margin-bottom: 0;
47 | padding-left: 0px;
48 | list-style: none;
49 | }
50 | .bs-sidenav {
51 | width: 300px;
52 | }
53 | }
54 |
55 | /* Slightly indent the side nav when space allows it */
56 | @media screen and (min-width: 1200px) {
57 | div.bs-sidenav ul {
58 | margin-bottom: 0;
59 | padding-left: 5px;
60 | list-style: none;
61 | }
62 | .bs-sidenav {
63 | width: 370px;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/pythonpath_test.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | from ladybug_rhino.pythonpath import iron_python_search_path_windows
3 |
4 | import os
5 | import io
6 | import plistlib
7 |
8 |
9 | def test_iron_python_search_path_windows():
10 | """Test the iron_python_search_path_windows method with a sample XML file."""
11 | package_dir = os.path.join('ladybug_tools', 'python', 'Lib', 'site-packages')
12 | settings_file = './tests/ironpython/settings-Scheme__Default.xml'
13 | dest_file = './tests/ironpython/new_settings.xml'
14 |
15 | edited_file = iron_python_search_path_windows(package_dir, settings_file, dest_file)
16 | assert os.path.isfile(edited_file)
17 |
18 | with io.open(edited_file, 'r', encoding='utf-8') as fp:
19 | set_data = fp.read()
20 | assert package_dir in set_data
21 | os.remove(edited_file)
22 |
23 |
24 | def test_iron_python_search_path_windows_mingbo():
25 | """Test the iron_python_search_path_windows method with Mingbo's sample XML file."""
26 | package_dir = os.path.join('ladybug_tools', 'python', 'Lib', 'site-packages')
27 | settings_file = './tests/ironpython/settings-Scheme__Default_mingbo.xml'
28 | dest_file = './tests/ironpython/new_settings_mingbo.xml'
29 |
30 | edited_file = iron_python_search_path_windows(package_dir, settings_file, dest_file)
31 | assert os.path.isfile(edited_file)
32 |
33 | with io.open(edited_file, 'r', encoding='utf-8') as fp:
34 | set_data = fp.read()
35 | assert package_dir in set_data
36 | os.remove(edited_file)
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://github.com/IronLanguages/ironpython2/releases/tag/ipy-2.7.8/)
4 |
5 | # ladybug-rhino
6 |
7 | A library for communicating between Ladybug Tools core libraries and Rhinoceros CAD.
8 |
9 | This library is used by both the Grasshopper and Rhino plugins to communicate with
10 | the ladybug core Python library. Note that this library has dependencies
11 | on Rhino SDK and Grasshopper SDK and is intended to contain all of such dependencies
12 | for the LBT-Grasshopper plugin. It is NOT intended to be run with cPython with
13 | the exceptions of running the CLI or when used with the cPython capabilities in Rhino 8.
14 |
15 | ## Installation
16 |
17 | `pip install -U ladybug-rhino`
18 |
19 | To check if Ladybug Rhino command line is installed correctly try `ladybug-rhino viz`
20 | and you should get a `viiiiiiiiiiiiizzzzzzzzz!` back in response!
21 |
22 | ## [API Documentation](http://ladybug-tools.github.io/ladybug-rhino/docs/)
23 |
24 | ## Local Development
25 |
26 | 1. Clone this repo locally
27 |
28 | ```python
29 | git clone git@github.com:ladybug-tools/ladybug-rhino
30 |
31 | # or
32 |
33 | git clone https://github.com/ladybug-tools/ladybug-rhino
34 | ```
35 |
36 | 2. Install dependencies
37 |
38 | ```console
39 | cd ladybug-rhino
40 | pip install -r dev-requirements.txt
41 | pip install -r requirements.txt
42 | pip install pythonnet
43 | pip install rhinoinside
44 | ```
45 |
46 | 3. Generate Documentation
47 |
48 | ```console
49 | sphinx-apidoc -f -e -d 4 -o ./docs ./ladybug_rhino
50 | sphinx-build -b html ./docs ./docs/_build/docs
51 | ```
52 |
--------------------------------------------------------------------------------
/ladybug_rhino/resourcepath.py:
--------------------------------------------------------------------------------
1 | """Functions for managing user resources like standards and measures."""
2 | import os
3 |
4 | try:
5 | from ladybug.futil import preparedir, nukedir
6 | except ImportError as e:
7 | raise ImportError("Failed to import ladybug.\n{}".format(e))
8 |
9 | STANDARDS_SUBFOLDERS = (
10 | 'constructions', 'constructionsets', 'schedules', 'programtypes',
11 | 'modifiers', 'modifiersets'
12 | )
13 |
14 |
15 | def setup_resource_folders(overwrite=False):
16 | """Set up user resource folders in their respective locations.
17 |
18 | Args:
19 | overwrite: Boolean to note whether the user resources should only be set
20 | up if they do not exist, in which case existing resources will be
21 | preserved, or should they be overwritten.
22 | """
23 | # first check if there's an environment variable available for APPDATA
24 | app_folder = os.getenv('APPDATA')
25 | if app_folder is not None:
26 | resource_folder = os.path.join(app_folder, 'ladybug_tools')
27 | # set up user standards
28 | lib_folder = os.path.join(resource_folder, 'standards')
29 | for sub_f in STANDARDS_SUBFOLDERS:
30 | sub_lib_folder = os.path.join(lib_folder, sub_f)
31 | if not os.path.isdir(sub_lib_folder) or overwrite:
32 | preparedir(sub_lib_folder)
33 | # set up the user weather
34 | epw_folder = os.path.join(resource_folder, 'weather')
35 | if not os.path.isdir(epw_folder) or overwrite:
36 | if os.path.isdir(epw_folder):
37 | nukedir(epw_folder, rmdir=True) # delete all sub-folders
38 | preparedir(epw_folder)
39 | # set up the user measures folder
40 | measure_folder = os.path.join(resource_folder, 'measures')
41 | if not os.path.isdir(measure_folder) or overwrite:
42 | if os.path.isdir(measure_folder):
43 | nukedir(measure_folder, rmdir=True) # delete all sub-folders
44 | preparedir(measure_folder)
45 | return resource_folder
46 |
--------------------------------------------------------------------------------
/ladybug_rhino/versioning/release.py:
--------------------------------------------------------------------------------
1 | """Functions specific to stable releases like changing component versions."""
2 |
3 |
4 | def update_component_version(components, version, year=None):
5 | """Update the version number and copyright year for a list of components.
6 |
7 | Args:
8 | components: A list of Grasshopper component objects which will have
9 | their version updated. Typically, this should be the output of the
10 | place_plugin_components method from ladybug_rhino.versioning.gather.
11 | version: Text for the version of the components to update.
12 | year: Text for the copyright year to update.
13 |
14 | Returns:
15 | A list of Ladybug Tools component objects that have had their version
16 | and copyright year updated.
17 | """
18 | new_components = []
19 | for comp_obj in components:
20 | try:
21 | # get the code from inside the component
22 | in_code = comp_obj.Code.split("\n")
23 | code_length = len(in_code)
24 | out_code = ''
25 |
26 | # loop through the lines of code and replace the version + copyright
27 | for line_count, line in enumerate(in_code):
28 | # replace the mesage and copyright lines
29 | if line.startswith('ghenv.Component.Message'):
30 | line = "ghenv.Component.Message = '{}'".format(version)
31 | elif line.startswith('# Copyright (c) ') and year is not None:
32 | line = '# Copyright (c) {}, Ladybug Tools.'.format(year)
33 |
34 | # append the code to the output lines
35 | if line_count != code_length - 1:
36 | out_code += line + "\n"
37 | else:
38 | out_code += line
39 |
40 | comp_obj.Code = out_code # replace the old code with updated code
41 | new_components.append(comp_obj)
42 | except Exception: # not a Ladybug Tools component
43 | print('Failed to update version in "{}".'.format(comp_obj.Name))
44 | return new_components
45 |
--------------------------------------------------------------------------------
/ladybug_rhino/visset.py:
--------------------------------------------------------------------------------
1 | """Class for a bake-able version of the ladybug-display VisualizationSet."""
2 | from ladybug_display.visualization import VisualizationSet
3 | from .bakeobjects import bake_visualization_set
4 |
5 | try:
6 | import System
7 | except ImportError as e:
8 | raise ImportError("Failed to import Windows/.NET libraries\n{}".format(e))
9 |
10 | try:
11 | import Grasshopper as gh
12 | except ImportError:
13 | print('Failed to import Grasshopper.Grasshopper Baking disabled.')
14 | gh = None
15 |
16 |
17 | class VisSetGoo(gh.Kernel.IGH_BakeAwareData):
18 | """A Bake-able version of the VisualizationSet for Grasshopper.
19 |
20 | Args:
21 | visualization_set: A Ladybug Display VisualizationSet object to be bake-able
22 | in the Rhino scene.
23 | """
24 |
25 | def __init__(self, visualization_set):
26 | self.vis_set = visualization_set
27 |
28 | def BakeGeometry(self, doc, att, id):
29 | try:
30 | if self.vis_set is not None:
31 | guids = bake_visualization_set(self.vis_set, True)
32 | return True, guids
33 | except Exception as e:
34 | System.Windows.Forms.MessageBox.Show(str(e), 'script error')
35 | return False, System.Guid.Empty
36 |
37 | def ToString(self):
38 | """Overwrite .NET ToString method."""
39 | return self.__repr__()
40 |
41 | def __repr__(self):
42 | return str(self.vis_set)
43 |
44 |
45 | def process_vis_set(vis_set):
46 | """Process various different types of VisualizationSet inputs.
47 |
48 | This includes VisualizationSet files, classes that have to_vis_set methods
49 | on them, and objects containing arguments for to_vis_set methods.
50 | """
51 | if isinstance(vis_set, VisualizationSet):
52 | return vis_set
53 | elif isinstance(vis_set, VisSetGoo):
54 | return vis_set.vis_set
55 | elif isinstance(vis_set, str): # assume that it's a file
56 | return VisualizationSet.from_file(vis_set)
57 | elif hasattr(vis_set, 'to_vis_set'): # an object with a method to be called
58 | return vis_set.to_vis_set()
59 | elif hasattr(vis_set, 'data'): # an object to be decoded
60 | args_list = vis_set.data
61 | if isinstance(args_list[0], (list, tuple)): # a list of VisualizationSets
62 | base_set = args_list[0][0].duplicate() \
63 | if isinstance(args_list[0][0], VisualizationSet) else \
64 | args_list[0][0].to_vis_set(*args_list[0][1:])
65 | for next_vis_args in args_list[1:]:
66 | next_set = next_vis_args[0] \
67 | if isinstance(next_vis_args[0], VisualizationSet) else \
68 | next_vis_args[0].to_vis_set(*next_vis_args[1:])
69 | for geo_obj in next_set:
70 | base_set.add_geometry(geo_obj)
71 | return base_set
72 | else: # a single list of arguments for to_vis_set
73 | return args_list[0] if isinstance(args_list[0], VisualizationSet) else \
74 | args_list[0].to_vis_set(*args_list[1:])
75 | else:
76 | msg = 'Input _vis_set was not recognized as a valid input.'
77 | raise ValueError(msg)
78 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: CI
3 |
4 | on: [push, pull_request]
5 |
6 | jobs:
7 |
8 | test:
9 | name: Unit tests
10 | strategy:
11 | matrix:
12 | python-version: ['3.10', '3.12']
13 | os: [macos-latest, ubuntu-latest, windows-latest]
14 |
15 | runs-on: ${{ matrix.os }}
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: set up Python
19 | uses: actions/setup-python@v2
20 | with:
21 | python-version: ${{ matrix.python-version }}
22 | - name: install python dependencies
23 | run: |
24 | python -m pip install --upgrade pip
25 | pip install -r requirements.txt
26 | pip install -r dev-requirements.txt
27 | - name: run tests
28 | run: python -m pytest tests/
29 |
30 | deploy:
31 | name: Deploy to GitHub and PyPI
32 | runs-on: ubuntu-latest
33 | needs: test
34 | if: github.ref == 'refs/heads/master' && github.repository_owner == 'ladybug-tools'
35 | steps:
36 | - uses: actions/checkout@v2
37 | - name: set up Python
38 | uses: actions/setup-python@v2
39 | with:
40 | python-version: '3.12'
41 | - name: set up node # we need node for for semantic release
42 | uses: actions/setup-node@v4
43 | with:
44 | node-version: 22.2.0
45 | - name: install python dependencies
46 | run: |
47 | python -m pip install --upgrade pip
48 | pip install -r requirements.txt
49 | pip install -r dev-requirements.txt
50 | - name: install semantic-release
51 | run:
52 | npm install @semantic-release/exec
53 | - name: run semantic release
54 | id: new_release
55 | run: |
56 | nextRelease="`npx semantic-release@^23.1.1 --dryRun | grep -oP 'Published release \K.*? ' || true`"
57 | npx semantic-release@^23.1.1
58 | echo "tag=$nextRelease" >> $GITHUB_OUTPUT
59 | env:
60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61 | PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
62 | PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }}
63 | - name: Update lbt-grasshopper
64 | if: contains(steps.new_release.outputs.tag, '.')
65 | env:
66 | DISPATCH_REPO: ladybug-tools/lbt-grasshopper
67 | DEPS_TOKEN: ${{ secrets.DEPS_UPDATING }}
68 | run: |
69 | curl -X POST https://api.github.com/repos/$DISPATCH_REPO/dispatches \
70 | -H "Accept: application/vnd.github.everest-preview+json" \
71 | -d '{"event_type": "ladybug_rhino_release", "client_payload": {"version": "${{ steps.new_release.outputs.tag }}"}}' \
72 | -u ladybugbot:$DEPS_TOKEN
73 |
74 | docs:
75 | name: Generate docs
76 | runs-on: ubuntu-latest
77 | needs: test
78 | if: github.ref == 'refs/heads/master' && github.repository_owner == 'ladybug-tools'
79 | steps:
80 | - uses: actions/checkout@v2
81 | - name: set up Python
82 | uses: actions/setup-python@v2
83 | with:
84 | python-version: '3.12'
85 | - name: install dependencies
86 | run: |
87 | pip install -U .
88 | pip install -r dev-requirements.txt
89 | sphinx-apidoc -f -e -d 4 -o ./docs ./ladybug_rhino
90 | sphinx-build -b html ./docs ./docs/_build/docs
91 | - name: deploy to github pages
92 | uses: peaceiris/actions-gh-pages@v3
93 | with:
94 | # this will use ladybugbot token
95 | github_token: ${{ secrets.GITHUB_TOKEN }}
96 | publish_branch: gh-pages
97 | publish_dir: docs/_build/
98 | force_orphan: true
99 | keep_files: false
100 | full_commit_message: 'deploy: update docs'
101 |
--------------------------------------------------------------------------------
/ladybug_rhino/versioning/userobject.py:
--------------------------------------------------------------------------------
1 | """Functions for creating user objects."""
2 | import os
3 |
4 | try:
5 | import Grasshopper.Folders as Folders
6 | import Grasshopper.Kernel as gh
7 | except ImportError:
8 | raise ImportError("Failed to import Grasshopper.")
9 |
10 | try:
11 | from ladybug.config import folders
12 | except ImportError as e:
13 | raise ImportError("Failed to import ladybug.\n{}".format(e))
14 |
15 | # find the location where the Grasshopper user objects are stored
16 | UO_FOLDER = Folders.UserObjectFolders[0]
17 | GHA_FOLDER = Folders.DefaultAssemblyFolder
18 | if os.name == 'nt':
19 | # search all assembly folders to see if they live in the core installation
20 | lbt_components = os.path.join(folders.ladybug_tools_folder, 'grasshopper')
21 | if os.path.isdir(lbt_components):
22 | comp_dir = 'C:\\ProgramData\\McNeel\\Rhinoceros\\packages'
23 | for a_fold in Folders.AssemblyFolders:
24 | a_fold = str(a_fold)
25 | if a_fold.startswith(comp_dir) and 'LadybugTools' in a_fold:
26 | # a special plugin loader has been added
27 | UO_FOLDER = lbt_components
28 | GHA_FOLDER = lbt_components
29 | break
30 |
31 | # map from the AdditionalHelpFromDocStrings to values for user object exposure
32 | EXPOSURE_MAP = (
33 | gh.GH_Exposure.dropdown,
34 | gh.GH_Exposure.primary,
35 | gh.GH_Exposure.secondary,
36 | gh.GH_Exposure.tertiary,
37 | gh.GH_Exposure.quarternary,
38 | gh.GH_Exposure.quinary,
39 | gh.GH_Exposure.senary,
40 | gh.GH_Exposure.septenary
41 | )
42 |
43 | # map from the component category to the plugin package folder
44 | FOLDER_MAP = {
45 | 'Ladybug': 'ladybug_grasshopper',
46 | 'Honeybee': 'honeybee_grasshopper_core',
47 | 'HB-Radiance': 'honeybee_grasshopper_radiance',
48 | 'HB-Energy': 'honeybee_grasshopper_energy',
49 | 'Dragonfly': 'dragonfly_grasshopper',
50 | 'LB-Legacy': 'LB-Legacy',
51 | 'HB-Legacy': 'HB-Legacy',
52 | 'HoneybeePlus': 'HoneybeePlus'
53 | }
54 |
55 |
56 | def create_userobject(component, move=True):
57 | """Create UserObject from a component.
58 |
59 | Args:
60 | component: A Grasshopper Python component.
61 | move: A Boolean to note whether the component should be moved to a subdirectory
62 | based on FOLDER_MAP. (Default: True).
63 | """
64 | # initiate userobject
65 | uo = gh.GH_UserObject()
66 | # set attributes
67 | uo.Icon = component.Icon_24x24
68 | try:
69 | exposure = int(component.AdditionalHelpFromDocStrings)
70 | except Exception: # no exposure category specified
71 | exposure = 1
72 | uo.Exposure = EXPOSURE_MAP[exposure]
73 | uo.BaseGuid = component.ComponentGuid
74 | uo.Description.Name = component.Name
75 | uo.Description.Description = component.Description
76 | uo.Description.Category = component.Category
77 | uo.Description.SubCategory = component.SubCategory
78 |
79 | # save the userobject to a file
80 | uo.SetDataFromObject(component)
81 | uo.SaveToFile()
82 |
83 | # move the user object file to the assigned folder
84 | if move:
85 | ufd = os.path.join(UO_FOLDER, FOLDER_MAP[component.Category], 'user_objects')
86 | ufp = os.path.join(ufd, '%s.ghuser' % component.Name)
87 | if not os.path.isdir(ufd):
88 | # create folder if it is not already created
89 | os.mkdir(ufd)
90 | elif os.path.isfile(ufp):
91 | # remove current userobject
92 | try:
93 | os.remove(ufp)
94 | except Exception:
95 | pass # access is denied to the user object location
96 | uo.Path = ufp
97 |
98 | uo.SaveToFile()
99 | return uo
100 |
--------------------------------------------------------------------------------
/docs/_templates/layout.html:
--------------------------------------------------------------------------------
1 | {% extends "basic/layout.html" %}
2 |
3 | {% if theme_bootstrap_version == "3" %}
4 | {% set bootstrap_version, navbar_version = "3.3.7", "" %}
5 | {% set bs_span_prefix = "col-md-" %}
6 | {% else %}
7 | {% set bootstrap_version, navbar_version = "2.3.2", "-2" %}
8 | {% set bs_span_prefix = "span" %}
9 | {% endif %}
10 |
11 | {%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and sidebars %}
12 |
13 | {%- set bs_content_width = render_sidebar and "8" or "12"%}
14 |
15 | {%- block doctype -%}
16 |
17 | {%- endblock %}
18 |
19 | {# Sidebar: Rework into our Bootstrap nav section. #}
20 | {% macro navBar() %}
21 | {% include "navbar" + navbar_version + ".html" %}
22 | {% endmacro %}
23 |
24 | {% if theme_bootstrap_version == "3" %}
25 | {%- macro bsidebar() %}
26 | {%- if render_sidebar %}
27 |
28 |
33 |
34 | {%- endif %}
35 | {%- endmacro %}
36 | {% else %}
37 | {%- macro bsidebar() %}
38 | {%- if render_sidebar %}
39 |
40 |
45 |
46 | {%- endif %}
47 | {%- endmacro %}
48 | {% endif %}
49 |
50 | {%- block extrahead %}
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | {% endblock %}
60 |
61 | {# Silence the sidebar's, relbar's #}
62 | {% block header %}{% endblock %}
63 | {% block relbar1 %}{% endblock %}
64 | {% block relbar2 %}{% endblock %}
65 | {% block sidebarsourcelink %}{% endblock %}
66 |
67 | {%- block content %}
68 | {{ navBar() }}
69 |
70 |
71 | {%- block sidebar1 %}{{ bsidebar() }}{% endblock %}
72 |
73 | {% block body %}{% endblock %}
74 |
75 | {% block sidebar2 %} {# possible location for sidebar #} {% endblock %}
76 |
77 |
78 | {%- endblock %}
79 |
80 | {%- block footer %}
81 |
107 | {%- endblock %}
108 |
--------------------------------------------------------------------------------
/ladybug_rhino/light.py:
--------------------------------------------------------------------------------
1 | """Functions for setting lights within the Rhino scene."""
2 | from __future__ import division
3 |
4 | from ladybug.dt import DateTime
5 | from ladybug.sunpath import Sunpath
6 |
7 | try:
8 | import System
9 | except ImportError as e: # No .NET; We are really screwed
10 | raise ImportError("Failed to import System.\n{}".format(e))
11 |
12 | try:
13 | import Rhino.Geometry as rg
14 | import Rhino.Render.Sun as sun
15 | from Rhino import RhinoDoc as rhdoc
16 | except ImportError as e:
17 | raise ImportError("Failed to import Rhino document attributes.\n{}".format(e))
18 |
19 |
20 | def set_sun(location, hoy, north=0):
21 | """Set the sun in the Rhino scene to correspond to a given location and DateTime.
22 |
23 | The resulting sun objects will have color rendering that mimics the sun at
24 | the particular hoy specified.
25 |
26 | Args:
27 | location: A Ladybug Location object to set the latitude, longitude and
28 | time zone of the Rhino sun path.
29 | hoy: A number between 0 and 8760 that represent the hour of the year at
30 | which to evaluate the sun position. Note that this does not need to
31 | be an integer and decimal values can be used to specify date times
32 | that are not on the hour mark.
33 | north: A number between -360 and 360 for the counterclockwise
34 | difference between the North and the positive Y-axis in degrees.
35 | 90 is West and 270 is East. (Default: 0).
36 |
37 | Returns:
38 | The Rhino sun object.
39 | """
40 | doc = rhdoc.ActiveDoc
41 | # process the hoy into a .NET date/time
42 | lb_dt = DateTime.from_hoy(hoy)
43 | rh_dt = System.DateTime(
44 | lb_dt.year, lb_dt.month, lb_dt.day, lb_dt.hour, lb_dt.minute, 0)
45 |
46 | # enable the sun and set its position based on the location and date/time
47 | sun_position = doc.Lights.Sun
48 | sun.Enabled.SetValue(sun_position, True)
49 | sun.TimeZone.SetValue(sun_position, location.time_zone)
50 | sun.SetPosition(sun_position, rh_dt, location.latitude, location.longitude)
51 |
52 | # set the north of the sun, ensuring the the y-axis is North
53 | sun.North.SetValue(sun_position, 90 + north)
54 | return sun
55 |
56 |
57 | def set_suns(location, hoys, north=0):
58 | """Setup multiple light objects for several sun positions.
59 |
60 | Note that the resulting lights will not have any color rendering associated
61 | with them and all lights will be white.
62 |
63 | Args:
64 | location: A Ladybug Location object to set the latitude, longitude and
65 | time zone of the Rhino sun path.
66 | hoys: A list of numbers between 0 and 8760 that represent the hours of
67 | the year at which to evaluate the sun position. Note that this does
68 | not need to be an integer and decimal values can be used to specify
69 | date times that are not on the hour mark.
70 | north: A number between -360 and 360 for the counterclockwise
71 | difference between the North and the positive Y-axis in degrees.
72 | 90 is West and 270 is East. (Default: 0).
73 |
74 | Returns:
75 | An array of lights representing sun positions.
76 | """
77 | doc_lights = rhdoc.ActiveDoc.Lights
78 |
79 | # initialize the Sunpath and get the relevant LB Suns
80 | sp = Sunpath.from_location(location, north)
81 | sun_vecs = []
82 | for hoy in hoys:
83 | lb_sun = sp.calculate_sun_from_hoy(hoy)
84 | if lb_sun.is_during_day:
85 | sun_vecs.append(lb_sun.sun_vector)
86 |
87 | # create Rhino Light objects for each sun
88 | sli = (1 / len(sun_vecs)) * 1.75
89 | sun_lights = []
90 | for sun_vec in sun_vecs:
91 | sun_light = rg.Light()
92 | sun_light.LightStyle = rg.LightStyle(7)
93 | sun_light.Direction = rg.Vector3d(sun_vec.x, sun_vec.y, sun_vec.z)
94 | sun_light.Intensity = sli
95 | sun_light.Name = 'LB_Sun'
96 | doc_lights.Add(sun_light)
97 | sun_lights.append(sun_light)
98 |
99 | return sun_lights
100 |
101 |
102 | def disable_sun():
103 | """Disable all suns in the Rhino scene so it does not interfere with other lights."""
104 | doc_lights = rhdoc.ActiveDoc.Lights
105 | doc_lights.Sun.Enabled = False
106 | for i, light in enumerate(doc_lights):
107 | if light.Name =='LB_Sun':
108 | doc_lights.Delete(i, True)
109 |
--------------------------------------------------------------------------------
/ladybug_rhino/ghpath.py:
--------------------------------------------------------------------------------
1 | """Functions for managing the copying of user objects to the Grasshopper path."""
2 | import os
3 |
4 | try:
5 | from ladybug.futil import nukedir, copy_file_tree
6 | except ImportError as e:
7 | raise ImportError("Failed to import ladybug.\n{}".format(e))
8 |
9 | from .pythonpath import RHINO_VERSIONS
10 |
11 | # core library packages, which get copied or cleaned out of the Rhino scripts folder
12 | PACKAGES = \
13 | ('ladybug_grasshopper', 'honeybee_grasshopper_core', 'honeybee_grasshopper_energy',
14 | 'honeybee_grasshopper_radiance', 'dragonfly_grasshopper')
15 | # package containing .gha files
16 | DOTNET_PACKAGES = ('ladybug_grasshopper_dotnet',)
17 | GRASSHOPPER_ID = 'b45a29b1-4343-4035-989e-044e8580d9cf'
18 |
19 |
20 | def copy_components_packages(directory):
21 | """Copy all Ladybug tools components packages to their respective locations.
22 |
23 | Args:
24 | directory: The path to a directory that contains all of the Ladybug
25 | Tools Grasshopper python packages to be copied (both user object
26 | packages and dotnet gha packages).
27 | """
28 | clean_userobjects()
29 | copy_packages_to_userobjects(directory)
30 | clean_libraries()
31 | copy_packages_to_libraries(directory)
32 |
33 |
34 | def find_grasshopper_userobjects():
35 | """Get the paths to the current user's Grasshopper user object folder.
36 |
37 | The folder(s) will be created if they do not already exist.
38 | """
39 | if os.name == 'nt': # we are on Windows
40 | appdata_roaming = os.getenv('APPDATA')
41 | uo_folder = [os.path.join(appdata_roaming, 'Grasshopper', 'UserObjects')]
42 | else: # we are on Mac
43 | home_folder = os.getenv('HOME') or os.path.expanduser('~')
44 | uo_folder = []
45 | for ver in RHINO_VERSIONS:
46 | uo_fold = os.path.join(
47 | home_folder, 'Library', 'Application Support', 'McNeel',
48 | 'Rhinoceros', ver, 'Plug-ins', 'Grasshopper ({})'.format(GRASSHOPPER_ID),
49 | 'UserObjects')
50 | uo_folder.append(uo_fold)
51 | for uo_fold in uo_folder:
52 | if not os.path.isdir(uo_fold):
53 | os.makedirs(uo_fold)
54 | return uo_folder
55 |
56 |
57 | def copy_packages_to_userobjects(directory):
58 | """Copy Ladybug Tools user object packages to the current user's userobject folder.
59 |
60 | Args:
61 | directory: The path to a directory that contains the Ladybug
62 | Tools Grasshopper python packages to be copied.
63 | """
64 | uo_folders = find_grasshopper_userobjects()
65 | for uo_folder in uo_folders:
66 | for pkg in PACKAGES:
67 | lib_folder = os.path.join(directory, pkg)
68 | dest_folder = os.path.join(uo_folder, pkg)
69 | if os.path.isdir(lib_folder):
70 | copy_file_tree(lib_folder, dest_folder, True)
71 | print('UserObjects copied to: {}'.format(dest_folder))
72 |
73 |
74 | def clean_userobjects():
75 | """Remove installed Ladybug Tools packages from the user's userobjects folder."""
76 | uo_folders = find_grasshopper_userobjects()
77 | for uo_folder in uo_folders:
78 | for pkg in PACKAGES:
79 | lib_folder = os.path.join(uo_folder, pkg)
80 | if os.path.isdir(lib_folder):
81 | nukedir(lib_folder, True)
82 | print('UserObjects removed from: {}'.format(lib_folder))
83 |
84 |
85 | def find_grasshopper_libraries():
86 | """Get the paths to the current user's Grasshopper Libraries folder.
87 |
88 | The folder(s) will be created if they do not already exist.
89 | """
90 | if os.name == 'nt': # we are on Windows
91 | appdata_roaming = os.getenv('APPDATA')
92 | lib_folder = [os.path.join(appdata_roaming, 'Grasshopper', 'Libraries')]
93 | else: # we are on Mac
94 | home_folder = os.getenv('HOME') or os.path.expanduser('~')
95 | lib_folder = []
96 | for ver in RHINO_VERSIONS:
97 | lib_fold = os.path.join(
98 | home_folder, 'Library', 'Application Support', 'McNeel',
99 | 'Rhinoceros', ver, 'Plug-ins', 'Grasshopper ({})'.format(GRASSHOPPER_ID),
100 | 'Libraries')
101 | lib_folder.append(lib_fold)
102 | for lib_fold in lib_folder:
103 | if not os.path.isdir(lib_fold):
104 | os.makedirs(lib_fold)
105 | return lib_folder
106 |
107 |
108 | def copy_packages_to_libraries(directory):
109 | """Copy Ladybug tools Libraries packages to the current user's libraries folder.
110 |
111 | Args:
112 | directory: The path to a directory that contains the Ladybug
113 | Tools Grasshopper python packages.
114 | """
115 | lib_folders = find_grasshopper_libraries()
116 | for lib_folder in lib_folders:
117 | for pkg in DOTNET_PACKAGES:
118 | src_folder = os.path.join(directory, pkg)
119 | dest_folder = os.path.join(lib_folder, pkg)
120 | if os.path.isdir(src_folder):
121 | copy_file_tree(src_folder, dest_folder, True)
122 | print('Components copied to: {}'.format(dest_folder))
123 |
124 |
125 | def clean_libraries():
126 | """Remove installed Ladybug Tools packages from the user's Libraries folder."""
127 | lib_folders = find_grasshopper_libraries()
128 | for lib_folder in lib_folders:
129 | for pkg in DOTNET_PACKAGES:
130 | lib_folder = os.path.join(lib_folder, pkg)
131 | if os.path.isdir(lib_folder):
132 | nukedir(lib_folder, True)
133 | print('Components removed from: {}'.format(lib_folder))
134 |
--------------------------------------------------------------------------------
/ladybug_rhino/colorize.py:
--------------------------------------------------------------------------------
1 | """Classes for colorized versions of various Rhino objects like points."""
2 | from .color import black
3 |
4 | try:
5 | import System.Guid as guid
6 | except ImportError as e:
7 | raise ImportError('Failed to import System.\n{}'.format(e))
8 |
9 | try:
10 | import Rhino as rh
11 | except ImportError as e:
12 | raise ImportError('Failed to import Rhino.\n{}'.format(e))
13 |
14 | try:
15 | import Grasshopper as gh
16 | except ImportError:
17 | print('Failed to import Grasshopper.\nColorized objects are not available.')
18 |
19 |
20 | class ColoredPoint(gh.Kernel.Types.GH_GeometricGoo[rh.Geometry.Point3d],
21 | gh.Kernel.IGH_BakeAwareData, gh.Kernel.IGH_PreviewData):
22 | """A Point object with a set-able color property to change its color in Grasshopper.
23 |
24 | Args:
25 | point: A Rhino Point3d object.
26 | """
27 |
28 | def __init__(self, point):
29 | """Initialize ColoredPoint."""
30 | self.point = point
31 | self.color = black()
32 |
33 | def DuplicateGeometry(self):
34 | point = rh.Geometry.Point3d(self.point.X, self.point.Y, self.point.Z)
35 | new_pt = ColoredPoint(point)
36 | new_pt.color = self.color
37 | return new_pt
38 |
39 | def get_TypeName(self):
40 | return "Colored Point"
41 |
42 | def get_TypeDescription(self):
43 | return "Colored Point"
44 |
45 | def ToString(self):
46 | return '{}, {}, {}'.format(self.color.R, self.color.G, self.color.B)
47 |
48 | def Transform(self, xform):
49 | point = rh.Geometry.Point3d(self.point.X, self.point.Y, self.point.Z)
50 | point.Transform(xform)
51 | new_pt = ColoredPoint(point)
52 | new_pt.color = self.color
53 | return new_pt
54 |
55 | def Morph(self, xmorph):
56 | return self.DuplicateGeometry()
57 |
58 | def DrawViewportWires(self, args):
59 | args.Pipeline.DrawPoint(
60 | self.point, rh.Display.PointStyle.RoundSimple, 5, self.color)
61 |
62 | def DrawViewportMeshes(self, args):
63 | # Do not draw in meshing layer.
64 | pass
65 |
66 | def BakeGeometry(self, doc, att, id):
67 | id = guid.Empty
68 | if att is None:
69 | att = doc.CreateDefaultAttributes()
70 | att.ColorSource = rh.DocObjects.ObjectColorSource.ColorFromObject
71 | att.ObjectColor = self.color
72 | id = doc.Objects.AddPoint(self.point, att)
73 | return True, id
74 |
75 |
76 | class ColoredPolyline(gh.Kernel.Types.GH_GeometricGoo[rh.Geometry.PolylineCurve],
77 | gh.Kernel.IGH_BakeAwareData, gh.Kernel.IGH_PreviewData):
78 | """A PolylineCurve object with set-able color and thickness properties.
79 |
80 | Args:
81 | polyline: A Rhino PolylineCurve object.
82 | """
83 |
84 | def __init__(self, polyline):
85 | """Initialize ColoredPolyline."""
86 | self.polyline = polyline
87 | self.color = black()
88 | self.thickness = 1
89 |
90 | def DuplicateGeometry(self):
91 | polyline = rh.Geometry.PolylineCurve(self.polyline)
92 | new_pl = ColoredPolyline(polyline)
93 | new_pl.color = self.color
94 | new_pl.thickness = self.thickness
95 | return new_pl
96 |
97 | def get_TypeName(self):
98 | return "Colored Polyline"
99 |
100 | def get_TypeDescription(self):
101 | return "Colored Polyline"
102 |
103 | def ToString(self):
104 | return 'Polyline Curve'
105 |
106 | def Transform(self, xform):
107 | polyline = rh.Geometry.PolylineCurve(self.polyline)
108 | polyline.Transform(xform)
109 | new_pl = ColoredPolyline(polyline)
110 | new_pl.color = self.color
111 | new_pl.thickness = self.thickness
112 | return new_pl
113 |
114 | def Morph(self, xmorph):
115 | return self.DuplicateGeometry()
116 |
117 | def DrawViewportWires(self, args):
118 | args.Pipeline.DrawCurve(self.polyline, self.color, self.thickness)
119 |
120 | def DrawViewportMeshes(self, args):
121 | # Do not draw in meshing layer.
122 | pass
123 |
124 | def BakeGeometry(self, doc, att, id):
125 | id = guid.Empty
126 | if att is None:
127 | att = doc.CreateDefaultAttributes()
128 | att.ColorSource = rh.DocObjects.ObjectColorSource.ColorFromObject
129 | att.ObjectColor = self.color
130 | id = doc.Objects.AddCurve(self.polyline, att)
131 | return True, id
132 |
133 |
134 | class ColoredLine(gh.Kernel.Types.GH_GeometricGoo[rh.Geometry.LineCurve],
135 | gh.Kernel.IGH_BakeAwareData, gh.Kernel.IGH_PreviewData):
136 | """A LineCurve object with set-able color and thickness properties.
137 |
138 | Args:
139 | line: A Rhino LineCurve object.
140 | """
141 |
142 | def __init__(self, line):
143 | """Initialize ColoredPolyline."""
144 | self.line = line
145 | self.color = black()
146 | self.thickness = 1
147 |
148 | def DuplicateGeometry(self):
149 | line = rh.Geometry.LineCurve(self.line)
150 | new_pl = ColoredPolyline(line)
151 | new_pl.color = self.color
152 | new_pl.thickness = self.thickness
153 | return new_pl
154 |
155 | def get_TypeName(self):
156 | return "Colored Polyline"
157 |
158 | def get_TypeDescription(self):
159 | return "Colored Polyline"
160 |
161 | def ToString(self):
162 | return 'Polyline Curve'
163 |
164 | def Transform(self, xform):
165 | line = rh.Geometry.LineCurve(self.line)
166 | line.Transform(xform)
167 | new_pl = ColoredPolyline(line)
168 | new_pl.color = self.color
169 | new_pl.thickness = self.thickness
170 | return new_pl
171 |
172 | def Morph(self, xmorph):
173 | return self.DuplicateGeometry()
174 |
175 | def DrawViewportWires(self, args):
176 | args.Pipeline.DrawCurve(self.line, self.color, self.thickness)
177 |
178 | def DrawViewportMeshes(self, args):
179 | # Do not draw in meshing layer.
180 | pass
181 |
182 | def BakeGeometry(self, doc, att, id):
183 | id = guid.Empty
184 | if att is None:
185 | att = doc.CreateDefaultAttributes()
186 | att.ColorSource = rh.DocObjects.ObjectColorSource.ColorFromObject
187 | att.ObjectColor = self.color
188 | id = doc.Objects.AddCurve(self.line, att)
189 | return True, id
190 |
--------------------------------------------------------------------------------
/ladybug_rhino/config.py:
--------------------------------------------------------------------------------
1 | """Ladybug_rhino configurations.
2 |
3 | Global variables such as tolerances, units and Rhino versions are stored here.
4 | """
5 | import os
6 |
7 | try:
8 | from ladybug.config import folders as lb_folders
9 | except ImportError as e:
10 | raise ImportError('\nFailed to import ladybug:\n\t{}'.format(e))
11 |
12 | try:
13 | import Rhino
14 | rhino_version_str = str(Rhino.RhinoApp.Version)
15 | rhino_version = tuple(int(n) for n in rhino_version_str.split('.'))
16 | except Exception: # Rhino is unavailable; just use a placeholder
17 | rhino_version = (7, 0)
18 |
19 | try: # Try to import tolerance from the active Rhino document
20 | import scriptcontext
21 | tolerance = scriptcontext.doc.ModelAbsoluteTolerance
22 | angle_tolerance = scriptcontext.doc.ModelAngleToleranceDegrees
23 | except ImportError: # No Rhino doc is available. Use Rhino's default.
24 | tolerance = 0.01
25 | angle_tolerance = 1.0 # default is 1 degree
26 |
27 | from .ghpath import find_grasshopper_userobjects, find_grasshopper_libraries
28 |
29 |
30 | def conversion_to_meters():
31 | """Get the conversion factor to meters based on the current Rhino doc units system.
32 |
33 | Returns:
34 | A number for the conversion factor, which should be multiplied by all distance
35 | units taken from Rhino geometry in order to convert them to meters.
36 | """
37 | try: # Try to import units from the active Rhino document
38 | import scriptcontext
39 | units = str(scriptcontext.doc.ModelUnitSystem).split('.')[-1]
40 | except ImportError: # No Rhino doc available. Default to the greatest of all units
41 | units = 'Meters'
42 |
43 | if units == 'Meters':
44 | return 1.0
45 | elif units == 'Millimeters':
46 | return 0.001
47 | elif units == 'Feet':
48 | return 0.3048
49 | elif units == 'Inches':
50 | return 0.0254
51 | elif units == 'Centimeters':
52 | return 0.01
53 | else:
54 | raise ValueError(
55 | "You're kidding me! What units are you using?" + units + "?\n"
56 | "Please use Meters, Millimeters, Centimeters, Feet or Inches.")
57 |
58 |
59 | def units_system():
60 | """Get text for the current Rhino doc units system. (eg. 'Meters', 'Feet')"""
61 | try: # Try to import units from the active Rhino document
62 | import scriptcontext
63 | return str(scriptcontext.doc.ModelUnitSystem).split('.')[-1]
64 | except ImportError: # No Rhino doc available. Default to the greatest of all units
65 | return 'Meters'
66 |
67 |
68 | def units_abbreviation():
69 | """Get text for the current Rhino doc units abbreviation (eg. 'm', 'ft')"""
70 | try: # Try to import units from the active Rhino document
71 | import scriptcontext
72 | units = str(scriptcontext.doc.ModelUnitSystem).split('.')[-1]
73 | except ImportError: # No Rhino doc available. Default to the greatest of all units
74 | units = 'Meters'
75 |
76 | if units == 'Meters':
77 | return 'm'
78 | elif units == 'Millimeters':
79 | return 'mm'
80 | elif units == 'Feet':
81 | return 'ft'
82 | elif units == 'Inches':
83 | return 'in'
84 | elif units == 'Centimeters':
85 | return 'cm'
86 | else:
87 | raise ValueError(
88 | "You're kidding me! What units are you using?" + units + "?\n"
89 | "Please use Meters, Millimeters, Centimeters, Feet or Inches.")
90 |
91 |
92 | class Folders(object):
93 | """Ladybug-rhino folders.
94 |
95 | Properties:
96 | * uo_folder
97 | * gha_folder
98 | * lbt_grasshopper_version
99 | * lbt_grasshopper_version_str
100 | """
101 |
102 | def __init__(self):
103 | # find the location where the Grasshopper user objects are stored
104 | self._uo_folder = find_grasshopper_userobjects()[-1]
105 | self._gha_folder = find_grasshopper_libraries()[-1]
106 | if os.name == 'nt':
107 | # test to see if components live in the core installation
108 | lbt_components = os.path.join(lb_folders.ladybug_tools_folder, 'grasshopper')
109 | if os.path.isdir(lbt_components):
110 | user_dir = os.path.join(self._uo_folder, 'ladybug_grasshopper')
111 | if not os.path.isdir(user_dir):
112 | self._uo_folder = lbt_components
113 | self._gha_folder = lbt_components
114 | self._lbt_grasshopper_version = None
115 | self._lbt_grasshopper_version_str = None
116 |
117 | @property
118 | def uo_folder(self):
119 | """Get the path to the user object folder."""
120 | return self._uo_folder
121 |
122 | @property
123 | def gha_folder(self):
124 | """Get the path to the GHA Grasshopper component folder."""
125 | return self._gha_folder
126 |
127 | @property
128 | def lbt_grasshopper_version(self):
129 | """Get a tuple for the version of lbt-grasshopper (eg. (3, 8, 2)).
130 |
131 | This will be None if the version could not be sensed.
132 | """
133 | if self._lbt_grasshopper_version is None:
134 | self._lbt_grasshopper_version_from_txt()
135 | return self._lbt_grasshopper_version
136 |
137 | @property
138 | def lbt_grasshopper_version_str(self):
139 | """Get text for the full version of python (eg."3.8.2").
140 |
141 | This will be None if the version could not be sensed.
142 | """
143 | if self._lbt_grasshopper_version_str is None:
144 | self._lbt_grasshopper_version_from_txt()
145 | return self._lbt_grasshopper_version_str
146 |
147 | def _lbt_grasshopper_version_from_txt(self):
148 | """Get the LBT-Grasshopper version from the requirements.txt file in uo_folder.
149 | """
150 | req_file = os.path.join(self._uo_folder, 'requirements.txt')
151 | if os.path.isfile(req_file):
152 | with open(req_file) as rf:
153 | for row in rf:
154 | if row.startswith('lbt-grasshopper=='):
155 | lbt_ver = row.split('==')[-1].strip()
156 | try:
157 | self._lbt_grasshopper_version = \
158 | tuple(int(i) for i in lbt_ver.split('.'))
159 | self._lbt_grasshopper_version_str = lbt_ver
160 | except Exception:
161 | pass # failed to parse the version into values
162 | break
163 |
164 |
165 | """Object possessing all key folders within the configuration."""
166 | folders = Folders()
167 |
--------------------------------------------------------------------------------
/ladybug_rhino/openstudio.py:
--------------------------------------------------------------------------------
1 | """Functions for importing OpenStudio into the Python environment."""
2 | import os
3 | import shutil
4 | import sys
5 |
6 | try:
7 | import clr
8 | except ImportError as e: # No .NET being used
9 | print('Failed to import CLR. OpenStudio SDK is unavailable.\n{}'.format(e))
10 |
11 | try:
12 | from honeybee_energy.config import folders
13 | except ImportError as e:
14 | print('Failed to import honeybee_energy. '
15 | 'OpenStudio SDK is unavailable.\n{}'.format(e))
16 |
17 |
18 | def load_osm(osm_path):
19 | """Load an OSM file to an OpenStudio SDK Model object in the Python environment.
20 |
21 | Args:
22 | osm_path: The path to an OSM file to be loaded an an OpenStudio Model.
23 |
24 | Returns:
25 | An OpenStudio Model object derived from the input osm_path.
26 |
27 | Usage:
28 |
29 | .. code-block:: python
30 |
31 | from ladybug_rhino.openstudio import load_osm
32 |
33 | # load an OpenStudio model from an OSM file
34 | osm_path = 'C:/path/to/model.osm'
35 | os_model = load_osm(osm_path)
36 |
37 | # get the space types from the model
38 | os_space_types = os_model.getSpaceTypes()
39 | for spt in os_space_types:
40 | print(spt)
41 | """
42 | # check that the file exists and OpenStudio is installed
43 | assert os.path.isfile(osm_path), 'No OSM file was found at "{}".'.format(osm_path)
44 | ops = import_openstudio()
45 |
46 | # load the model object and return it
47 | os_path = ops.OpenStudioUtilitiesCore.toPath(osm_path)
48 | osm_path_obj = ops.Path(os_path)
49 | exist_os_model = ops.Model.load(osm_path_obj)
50 | if exist_os_model.is_initialized():
51 | return exist_os_model.get()
52 | else:
53 | raise ValueError(
54 | 'The file at "{}" does not appear to be an OpenStudio model.'.format(
55 | osm_path
56 | ))
57 |
58 |
59 | def dump_osm(model, osm_path):
60 | """Dump an OpenStudio Model object to an OSM file.
61 |
62 | Args:
63 | model: An OpenStudio Model to be written to a file.
64 | osm_path: The path of the .osm file where the OpenStudio Model will be saved.
65 |
66 | Returns:
67 | The path to the .osm file as a string.
68 |
69 | Usage:
70 |
71 | .. code-block:: python
72 |
73 | from ladybug_rhino.openstudio import load_osm, dump_osm
74 |
75 | # load an OpenStudio model from an OSM file
76 | osm_path = 'C:/path/to/model.osm'
77 | model = load_osm(osm_path)
78 |
79 | # get all of the SetpointManagers and set their properties
80 | setpt_managers = model.getSetpointManagerOutdoorAirResets()
81 | for setpt in setpt_managers:
82 | setpt.setSetpointatOutdoorLowTemperature(19)
83 | setpt.setOutdoorLowTemperature(12)
84 | setpt.setSetpointatOutdoorHighTemperature(16)
85 | setpt.setOutdoorHighTemperature(22)
86 |
87 | # save the edited OSM over the original one
88 | osm = dump_osm(model, osm_path)
89 | """
90 | # check that the model is the correct object type
91 | ops = import_openstudio()
92 | assert isinstance(model, ops.Model), \
93 | 'Expected OpenStudio Model. Got {}.'.format(type(model))
94 |
95 | # load the model object and return it
96 | os_path = ops.OpenStudioUtilitiesCore.toPath(osm_path)
97 | osm_path_obj = ops.Path(os_path)
98 | model.save(osm_path_obj, True)
99 | return osm_path
100 |
101 |
102 | def import_openstudio():
103 | """Import the OpenStudio SDK into the Python environment.
104 |
105 | Returns:
106 | The OpenStudio NameSpace with all of the modules, classes and methods
107 | of the OpenStudio SDK.
108 |
109 | Usage:
110 |
111 | .. code-block:: python
112 |
113 | from ladybug_rhino.openstudio import import_openstudio, dump_osm
114 | OpenStudio = import_openstudio()
115 |
116 | # create a new OpenStudio model from scratch
117 | os_model = OpenStudio.Model()
118 | space_type = OpenStudio.SpaceType(os_model)
119 |
120 | # save the Model to an OSM
121 | osm_path = 'C:/path/to/model.osm'
122 | osm = dump_osm(os_model, osm_path)
123 | """
124 | try: # first see if OpenStudio has already been loaded
125 | import OpenStudio
126 | return OpenStudio
127 | except ImportError:
128 | # check to be sure that the OpenStudio CSharp folder has been installed
129 | compatibility_url = 'https://github.com/ladybug-tools/lbt-grasshopper/wiki/' \
130 | '1.4-Compatibility-Matrix'
131 | in_msg = 'Download and install the version of OpenStudio listed in the ' \
132 | 'Ladybug Tools compatibility matrix\n{}.'.format(compatibility_url)
133 | assert folders.openstudio_path is not None, \
134 | 'No OpenStudio installation was found on this machine.\n{}'.format(in_msg)
135 | assert folders.openstudio_csharp_path is not None, \
136 | 'No OpenStudio CSharp folder was found in the OpenStudio installation ' \
137 | 'at:\n{}'.format(os.path.dirname(folders.openstudio_path))
138 | _copy_openstudio_lib()
139 |
140 | # add the OpenStudio DLL to the Common Language Runtime (CLR)
141 | os_dll = os.path.join(folders.openstudio_csharp_path, 'OpenStudio.dll')
142 | clr.AddReferenceToFileAndPath(os_dll)
143 | if folders.openstudio_csharp_path not in sys.path:
144 | sys.path.append(folders.openstudio_csharp_path)
145 | import OpenStudio
146 | return OpenStudio
147 |
148 |
149 | def _copy_openstudio_lib():
150 | """Copy the openstudiolib.dll into the CSharp folder.
151 |
152 | This is a workaround that is necessary because the OpenStudio installer
153 | does not install the CSharp bindings correctly.
154 | """
155 | # see if the CSharp folder already has everything it needs
156 | dest_file = os.path.join(folders.openstudio_csharp_path, 'openstudiolib.dll')
157 | if os.path.isfile(dest_file):
158 | return None
159 |
160 | # if not, see if the openstudio_lib_path has the file that needs to be copied
161 | base_msg = 'The OpenStudio CSharp path at "{}" lacks the openstudiolib.dll'.format(
162 | folders.openstudio_csharp_path)
163 | assert os.path.isdir(folders.openstudio_lib_path), \
164 | '{}\nand there is no OpenStudio Lib installed.'.format(base_msg)
165 | src_file = os.path.join(folders.openstudio_lib_path, 'openstudiolib.dll')
166 | assert os.path.isfile(src_file), \
167 | '{}\nand this file was not found at "{}".'.format(base_msg, src_file)
168 |
169 | # copy the DLL if it exists
170 | shutil.copy(src_file, dest_file)
171 |
--------------------------------------------------------------------------------
/ladybug_rhino/versioning/component.py:
--------------------------------------------------------------------------------
1 | """Module for exporting components from Grasshopper with all metadata and source code.
2 | """
3 | import json
4 | import os
5 |
6 |
7 | class Component(object):
8 | """Grasshopper component wrapper used to serialize component properties to dict.
9 |
10 | Args:
11 | name: Text for the name of the component.
12 | nickname: Text for the nickname of the component.
13 | description: Text for the description of the component.
14 | code: Text for all the Python code of the component.
15 | category: Text for all Python code in the component (including import statements)
16 | subcategory: Text for the subcategory of the component.
17 | version: Text for the version of the component formatted as a 3-number
18 | semantic version.
19 | """
20 | # dictionary to map plugin-specific language to generic slugs
21 | MAPPING = {
22 | 'grasshopper': '{{plugin}}',
23 | 'Grasshopper': '{{Plugin}}',
24 | 'GH': '{{PLGN}}',
25 | 'Food4Rhino': '{{Package_Manager}}',
26 | 'rhino': '{{cad}}',
27 | 'Rhino': '{{Cad}}'
28 | }
29 |
30 | def __init__(self, name, nickname, description, code,
31 | category, subcategory, version):
32 | """Grasshopper component wrapper."""
33 | self.name = name.replace('\r\n', '\n')
34 | self.nickname = nickname.replace('\r\n', '\n')
35 | self.description = description.replace('\r\n', '\n')
36 | self.code = code.replace('\r\n', '\n')
37 | self.category = category.replace('\r\n', '\n')
38 | self.subcategory = subcategory.replace('\r\n', '\n')
39 | self.version = version
40 | self._inputs = []
41 | self._outputs = []
42 |
43 | @classmethod
44 | def from_gh_component(cls, component):
45 | """Create Component from a Grasshopper component object.
46 |
47 | Args:
48 | component: A Grasshopper component object.
49 | """
50 | comp = cls(component.Name, component.NickName, component.Description,
51 | component.Code, component.Category, component.SubCategory,
52 | component.Message)
53 |
54 | for inp in component.Params.Input:
55 | comp.add_input(Port.from_gh_port(inp))
56 |
57 | for out in component.Params.Output:
58 | if out.Name == 'out':
59 | continue
60 | comp.add_output(Port.from_gh_port(out))
61 |
62 | return comp
63 |
64 | @property
65 | def inputs(self):
66 | """Get a list of Port objects for the component Inputs."""
67 | return self._inputs
68 |
69 | @property
70 | def outputs(self):
71 | """Get a list of Port objects for the component Outputs."""
72 | return self._outputs
73 |
74 | def add_input(self, inp):
75 | """Add an input for the component.
76 |
77 | Args:
78 | inp: A Port object for the input.
79 | """
80 | assert isinstance(inp, Port)
81 | self._inputs.append(inp)
82 |
83 | def add_output(self, out):
84 | """Add an output for the component.
85 |
86 | Args:
87 | out: A Port object for the output.
88 | """
89 | assert isinstance(out, Port)
90 | self._outputs.append(out)
91 |
92 | def to_dict(self):
93 | """Get the Component instance as a dictionary."""
94 | component = {
95 | 'name': self.name,
96 | 'version': self.version,
97 | 'nickname': self.nickname,
98 | 'description': self.description,
99 | 'code': self._clean_code(),
100 | 'category': self.category,
101 | 'subcategory': self.subcategory,
102 | 'inputs': [i.to_dict() for i in self.inputs],
103 | 'outputs': [[i.to_dict() for i in self.outputs]]
104 | }
105 | return component
106 |
107 | def to_json(self, folder, name=None, indent=2):
108 | """Write the Component instance to a JSON file.
109 |
110 | Args:
111 | folder: Text for the folder into which the JSON should be written.
112 | name: Text for the file name for the JSON.
113 | indent: Integer for the number of spaces in an indent.
114 | """
115 | name = name or self.name.replace(' ', '_')
116 | if not name.lower().endswith('.json'):
117 | name = '%s.json' % name
118 | fp = os.path.join(folder, name)
119 | with open(fp, 'w') as outf:
120 | json.dump(self.to_dict(), outf, indent=indent)
121 |
122 | def _clean_code(self):
123 | """Clean up the text of the code before export.
124 |
125 | This replaces plugin-specific text like "Grasshopper" with the generic slugs
126 | in the MAPPING property of this class.
127 | """
128 | code = self.code
129 | code = code.split('\n')
130 |
131 | for count, line in enumerate(code):
132 | if line.startswith('ghenv.Component.AdditionalHelpFromDocStrings'):
133 | break
134 |
135 | gist = '\n'.join(code[count + 1:])
136 |
137 | for o, t in self.MAPPING.items():
138 | gist = gist.replace(o, t)
139 |
140 | return gist
141 |
142 |
143 | class Port(object):
144 | """Grasshopper port wrapper used to serialize inputs and outputs to dict.
145 |
146 | Args:
147 | name: Text for the name of the input or output.
148 | description: Text for the description of the input or output.
149 | default_value: Default value for the input or output.
150 | value_type: Text the type of input or output (eg. bool)
151 | access_type: Text for list vs. item access.
152 | """
153 |
154 | def __init__(self, name, description=None, default_value=None, value_type=None,
155 | access_type=None):
156 | self.name = name.replace('\r\n', '\n')
157 | self.description = description.replace('\r\n', '\n')
158 | self.default_value = default_value
159 | self.value_type = value_type
160 | self.access_type = str(access_type)
161 |
162 | @classmethod
163 | def from_gh_port(cls, port):
164 | """Create Port from a Grasshopper port object.
165 |
166 | Args:
167 | port: A Grasshopper port object, typically accessed by iterating over the
168 | component.Params.Input or component.Params.Output properties.
169 | """
170 | if hasattr(port, 'TypeHint'): # it's an input
171 | v = port.VolatileData
172 | if v.IsEmpty:
173 | value = None
174 | else:
175 | values = tuple(str(i.Value).lower() if port.TypeHint.TypeName == 'bool'
176 | else i.Value for i in v.AllData(True))
177 | try:
178 | value = tuple(v.replace('\\\\', '\\').replace('\\', '\\\\')
179 | for v in values)
180 | except AttributeError:
181 | # non string type
182 | value = values
183 |
184 | if value and str(port.Access) == 'item':
185 | value = value[0]
186 |
187 | return cls(port.Name, port.Description, value, port.TypeHint.TypeName,
188 | str(port.Access))
189 | else: # it's an output
190 | return cls(port.Name, port.Description, None, None, None)
191 |
192 | def to_dict(self):
193 | """Translate Port instance to a dictionary."""
194 | return {
195 | 'name': self.name,
196 | 'description': self.description,
197 | 'default': self.default_value,
198 | 'type': self.value_type,
199 | 'access': self.access_type
200 | }
201 |
--------------------------------------------------------------------------------
/ladybug_rhino/download.py:
--------------------------------------------------------------------------------
1 | """Collection of methods for downloading files securely using .NET libraries."""
2 | import os
3 | import json
4 |
5 | try:
6 | from ladybug.config import folders
7 | from ladybug.futil import preparedir, unzip_file
8 | from ladybug.epw import EPW
9 | from ladybug.stat import STAT
10 | from ladybug.climatezone import ashrae_climate_zone
11 | except ImportError as e:
12 | raise ImportError("Failed to import ladybug.\n{}".format(e))
13 |
14 | try:
15 | import System.Net
16 | except ImportError as e:
17 | print("Failed to import Windows/.NET libraries\n{}".format(e))
18 |
19 |
20 | def download_file_by_name(url, target_folder, file_name, mkdir=False):
21 | """Download a file to a directory.
22 |
23 | Args:
24 | url: A string to a valid URL.
25 | target_folder: Target folder for download (e.g. c:/ladybug)
26 | file_name: File name (e.g. testPts.zip).
27 | mkdir: Set to True to create the directory if doesn't exist (Default: False)
28 | """
29 | # create the target directory.
30 | if not os.path.isdir(target_folder):
31 | if mkdir:
32 | preparedir(target_folder)
33 | else:
34 | created = preparedir(target_folder, False)
35 | if not created:
36 | raise ValueError("Failed to find %s." % target_folder)
37 | file_path = os.path.join(target_folder, file_name)
38 |
39 | # set the security protocol to the most recent version
40 | try:
41 | # TLS 1.2 is needed to download over https
42 | System.Net.ServicePointManager.SecurityProtocol = \
43 | System.Net.SecurityProtocolType.Tls12
44 | except AttributeError:
45 | # TLS 1.2 is not provided by MacOS .NET in Rhino 5
46 | if url.lower().startswith('https'):
47 | print('This system lacks the necessary security'
48 | ' libraries to download over https.')
49 |
50 | # attempt to download the file
51 | client = System.Net.WebClient()
52 | try:
53 | client.DownloadFile(url, file_path)
54 | except Exception as e:
55 | raise Exception(' Download failed with the error:\n{}'.format(e))
56 |
57 |
58 | def download_file(url, file_path, mkdir=False):
59 | """Write a string of data to file.
60 |
61 | Args:
62 | url: A string to a valid URL.
63 | file_path: Full path to intended download location (e.g. c:/ladybug/testPts.pts)
64 | mkdir: Set to True to create the directory if doesn't exist (Default: False)
65 | """
66 | folder, fname = os.path.split(file_path)
67 | return download_file_by_name(url, folder, fname, mkdir)
68 |
69 |
70 | def extract_project_info(project_info_json):
71 | """Extract relevant project information from project info JSON containing URLs.
72 |
73 | Args:
74 | project_info_json: A JSON string of a ProjectInfo object, which
75 | contains at least one Weather URL. If the ProjectInfo does not
76 | contain information that resides in the weather file, this info
77 | will be extracted and put into the returned object.
78 |
79 | Returns:
80 | A tuple with two values.
81 |
82 | - project_info_json: A JSON string of project information containing
83 | information extracted from the EPW URL.
84 |
85 | - epw_path: The local file path to the downloaded EPW.
86 | """
87 | # convert the JSON into a dictionary and extract the EPW URL
88 | project_info = json.loads(project_info_json)
89 | if 'weather_urls' not in project_info or len(project_info['weather_urls']) == 0:
90 | return project_info_json
91 | weather_url = project_info['weather_urls'][0]
92 |
93 | # first check wether the url is actually a local path
94 | if not weather_url.lower().startswith('http'):
95 | assert os.path.isdir(weather_url) or os.path.isfile(weather_url), \
96 | 'Input weather URL is not a web address nor a local folder directory.'
97 | if os.path.isfile(weather_url):
98 | assert weather_url.endswith('.epw'), \
99 | 'Input weather URL is not an EPW file.'
100 | epw_path = weather_url
101 | weather_url, file_name = os.path.split(weather_url)
102 | stat_path = os.path.join(weather_url, file_name.replace('.epw', '.stat'))
103 | ddy_path = os.path.join(weather_url, file_name.replace('.epw', '.ddy'))
104 | assert os.path.isfile(stat_path), \
105 | 'No STAT file was found at: {}.'.format(stat_path)
106 | assert os.path.isfile(stat_path), \
107 | 'No DDY file was found at: {}.'.format(ddy_path)
108 | else:
109 | file_checklist = ['.epw', '.stat', '.ddy']
110 | for f in os.listdir(weather_url):
111 | for i, f_check in enumerate(file_checklist):
112 | if f.lower().endswith(f_check): # file type found
113 | file_checklist.pop(i)
114 | if f_check == '.epw':
115 | epw_path = os.path.join(weather_url, f)
116 | elif f_check == '.stat':
117 | stat_path = os.path.join(weather_url, f)
118 | break
119 | if len(file_checklist) != 0:
120 | msg = 'The following directory does not contain these files '\
121 | '({}):\n{}'.format(', '.join(file_checklist), weather_url)
122 | raise ValueError(msg)
123 | else: # download the EPW file to the user folder
124 | _def_folder = folders.default_epw_folder
125 | if weather_url.lower().endswith('.zip'): # onebuilding URL type
126 | _folder_name = weather_url.split('/')[-1][:-4]
127 | else: # dept of energy URL type
128 | _folder_name = weather_url.split('/')[-2]
129 | epw_path = os.path.join(_def_folder, _folder_name, _folder_name + '.epw')
130 | stat_path = os.path.join(_def_folder, _folder_name, _folder_name + '.stat')
131 | if not os.path.isfile(epw_path):
132 | zip_file_path = os.path.join(
133 | _def_folder, _folder_name, _folder_name + '.zip')
134 | download_file(weather_url, zip_file_path, True)
135 | unzip_file(zip_file_path)
136 |
137 | # add the location to the project_info dictionary
138 | epw_obj = None
139 | if 'location' not in project_info or project_info['location'] is None:
140 | epw_obj = EPW(epw_path)
141 | project_info['location'] = epw_obj.location.to_dict()
142 | else:
143 | loc_dict = project_info['location']
144 | loc_props = (loc_dict['latitude'], loc_dict['longitude'], loc_dict['elevation'])
145 | if all(prop == 0 for prop in loc_props):
146 | epw_obj = EPW(epw_path)
147 | project_info['location'] = epw_obj.location.to_dict()
148 | project_info['location']['time_zone'] = \
149 | int(project_info['location']['time_zone'])
150 |
151 | # add the climate zone to the project_info dictionary
152 | if 'ashrae_climate_zone' not in project_info or \
153 | project_info['ashrae_climate_zone'] is None:
154 | zone_set = False
155 | if os.path.isfile(stat_path):
156 | stat_obj = STAT(stat_path)
157 | if stat_obj.ashrae_climate_zone is not None:
158 | project_info['ashrae_climate_zone'] = stat_obj.ashrae_climate_zone
159 | zone_set = True
160 | if not zone_set: # get it from the EPW data
161 | epw_obj = EPW(epw_path) if epw_obj is None else epw_obj
162 | project_info['ashrae_climate_zone'] = \
163 | ashrae_climate_zone(epw_obj.dry_bulb_temperature)
164 |
165 | # convert the dictionary to a JSON
166 | project_info_json = json.dumps(project_info)
167 | return project_info_json, epw_path
168 |
--------------------------------------------------------------------------------
/ladybug_rhino/text.py:
--------------------------------------------------------------------------------
1 | """Functions to add text to the Rhino scene and create Grasshopper text objects."""
2 | from __future__ import division
3 | import math
4 |
5 | from .fromgeometry import from_plane
6 | from .color import black
7 |
8 | try:
9 | import System.Guid as guid
10 | except ImportError as e:
11 | print("Failed to import System\n{}".format(e))
12 |
13 | try:
14 | import Rhino as rh
15 | except ImportError as e:
16 | raise ImportError("Failed to import Rhino.\n{}".format(e))
17 |
18 | try:
19 | import Grasshopper as gh
20 | except ImportError:
21 | pass
22 |
23 |
24 | def text_objects(text, plane, height, font='Arial',
25 | horizontal_alignment=0, vertical_alignment=5):
26 | """Generate a Bake-able Grasshopper text object from a text string and ladybug Plane.
27 |
28 | Args:
29 | text: A text string to be converted to a a Grasshopper text object.
30 | plane: A Ladybug Plane object to locate and orient the text in the Rhino scene.
31 | height: A number for the height of the text in the Rhino scene.
32 | font: An optional text string for the font in which to draw the text.
33 | horizontal_alignment: An optional integer to specify the horizontal alignment
34 | of the text. Choose from: (0 = Left, 1 = Center, 2 = Right)
35 | vertical_alignment: An optional integer to specify the vertical alignment
36 | of the text. Choose from: (0 = Top, 1 = MiddleOfTop, 2 = BottomOfTop,
37 | 3 = Middle, 4 = MiddleOfBottom, 5 = Bottom, 6 = BottomOfBoundingBox)
38 | """
39 | txt = rh.Display.Text3d(text, from_plane(plane), height)
40 | txt.FontFace = font
41 | txt.HorizontalAlignment = AlignmentTypes.horizontal(horizontal_alignment)
42 | txt.VerticalAlignment = AlignmentTypes.vertical(vertical_alignment)
43 | return TextGoo(txt)
44 |
45 |
46 | """____________EXTRA HELPER OBJECTS____________"""
47 |
48 |
49 | class TextGoo(gh.Kernel.Types.GH_GeometricGoo[rh.Display.Text3d],
50 | gh.Kernel.IGH_BakeAwareData, gh.Kernel.IGH_PreviewData):
51 | """A Text object that can be baked and transformed in Grasshopper.
52 |
53 | The code for this entire class was taken from David Rutten and Giulio Piacentino's
54 | script described here:
55 | https://discourse.mcneel.com/t/creating-text-objects-and-outputting-them-as-normal-rhino-geometry/47834/7
56 |
57 | Args:
58 | text: A Rhino Text3d object.
59 | """
60 |
61 | def __init__(self, text):
62 | """Initialize Bake-able text."""
63 | self.m_value = text
64 |
65 | @staticmethod
66 | def DuplicateText3d(original):
67 | if original is None:
68 | return None
69 | text = rh.Display.Text3d(original.Text, original.TextPlane, original.Height)
70 | text.Bold = original.Bold
71 | text.Italic = original.Italic
72 | text.FontFace = original.FontFace
73 | return text
74 |
75 | def DuplicateGeometry(self):
76 | return TextGoo(TextGoo.DuplicateText3d(self.m_value))
77 |
78 | def get_TypeName(self):
79 | return "3D Text"
80 |
81 | def get_TypeDescription(self):
82 | return "3D Text"
83 |
84 | def get_Boundingbox(self):
85 | if self.m_value is None:
86 | return rh.Geometry.BoundingBox.Empty
87 | return self.m_value.BoundingBox
88 |
89 | def GetBoundingBox(self, xform):
90 | if self.m_value is None:
91 | return rh.Geometry.BoundingBox.Empty
92 | box = self.m_value.BoundingBox
93 | corners = xform.TransformList(box.GetCorners())
94 | return rh.Geometry.BoundingBox(corners)
95 |
96 | def Transform(self, xform):
97 | text = TextGoo.DuplicateText3d(self.m_value)
98 | if text is None:
99 | return TextGoo(None)
100 |
101 | plane = text.TextPlane
102 | point = plane.PointAt(1, 1)
103 |
104 | plane.Transform(xform)
105 | point.Transform(xform)
106 | dd = point.DistanceTo(plane.Origin)
107 |
108 | text.TextPlane = plane
109 | text.Height *= dd / math.sqrt(2)
110 | new_text = TextGoo(text)
111 |
112 | new_text.m_value.Bold = self.m_value.Bold
113 | new_text.m_value.Italic = self.m_value.Italic
114 | new_text.m_value.FontFace = self.m_value.FontFace
115 | return new_text
116 |
117 | def Morph(self, xmorph):
118 | return self.DuplicateGeometry()
119 |
120 | def get_ClippingBox(self):
121 | return self.get_Boundingbox()
122 |
123 | def DrawViewportWires(self, args):
124 | if self.m_value is None:
125 | return
126 | color = black() if black is not None else args.Color
127 | args.Pipeline.Draw3dText(self.m_value, color)
128 |
129 | def DrawViewportMeshes(self, args):
130 | # Do not draw in meshing layer.
131 | pass
132 |
133 | def BakeGeometry(self, doc, att, id):
134 | id = guid.Empty
135 | if self.m_value is None:
136 | return False, id
137 | if att is None:
138 | att = doc.CreateDefaultAttributes()
139 | original_plane = None
140 | d_txt = self.m_value.Text
141 | nl_count = len(d_txt.split('\n')) - 1
142 | if nl_count > 1 and str(self.m_value.VerticalAlignment) == 'Bottom':
143 | y_ax = rh.Geometry.Vector3d(self.m_value.TextPlane.YAxis)
144 | txt_h = self.m_value.Height * (3 / 2)
145 | m_vec = rh.Geometry.Vector3d.Multiply(y_ax, txt_h * -nl_count)
146 | original_plane = self.m_value.TextPlane
147 | new_plane = rh.Geometry.Plane(self.m_value.TextPlane)
148 | new_plane.Translate(m_vec)
149 | self.m_value.TextPlane = new_plane
150 | self.m_value.Height = self.m_value.Height
151 | doc.ModelSpaceAnnotationScalingEnabled = False # disable model scale
152 | id = doc.Objects.AddText(self.m_value, att)
153 | self.m_value.Height = self.m_value.Height
154 | if original_plane is not None:
155 | self.m_value.TextPlane = original_plane
156 | return True, id
157 |
158 | def ScriptVariable(self):
159 | """Overwrite Grasshopper ScriptVariable method."""
160 | return self
161 |
162 | def ToString(self):
163 | """Overwrite .NET ToString method."""
164 | return self.__repr__()
165 |
166 | def __repr__(self):
167 | if self.m_value is None:
168 | return ""
169 | return self.m_value.Text
170 |
171 |
172 | class AlignmentTypes(object):
173 | """Enumeration of text alignment types."""
174 |
175 | _HORIZONTAL = (rh.DocObjects.TextHorizontalAlignment.Left,
176 | rh.DocObjects.TextHorizontalAlignment.Center,
177 | rh.DocObjects.TextHorizontalAlignment.Right)
178 |
179 | _VERTICAL = (rh.DocObjects.TextVerticalAlignment.Top,
180 | rh.DocObjects.TextVerticalAlignment.MiddleOfTop,
181 | rh.DocObjects.TextVerticalAlignment.BottomOfTop,
182 | rh.DocObjects.TextVerticalAlignment.Middle,
183 | rh.DocObjects.TextVerticalAlignment.MiddleOfBottom,
184 | rh.DocObjects.TextVerticalAlignment.Bottom,
185 | rh.DocObjects.TextVerticalAlignment.BottomOfBoundingBox)
186 |
187 | @classmethod
188 | def horizontal(cls, field_number):
189 | """Get a Rhino horizontal alignment object by its integer field number.
190 |
191 | * 0 - Left
192 | * 1 - Center
193 | * 2 - Right
194 |
195 | """
196 | return cls._HORIZONTAL[field_number]
197 |
198 | @classmethod
199 | def vertical(cls, field_number):
200 | """Get a Rhino vertical alignment object by its integer field number.
201 |
202 | * 0 - Top
203 | * 1 - MiddleOfTop
204 | * 2 - BottomOfTop
205 | * 3 - Middle
206 | * 4 - MiddleOfBottom
207 | * 5 - Bottom
208 | * 6 - BottomOfBoundingBox
209 |
210 | """
211 | return cls._VERTICAL[field_number]
212 |
--------------------------------------------------------------------------------
/ladybug_rhino/planarize.py:
--------------------------------------------------------------------------------
1 | """Functions to convert curved Rhino geometries into planar ladybug ones."""
2 | from .config import tolerance
3 |
4 | try:
5 | from ladybug_geometry.geometry3d.pointvector import Point3D
6 | from ladybug_geometry.geometry3d.face import Face3D
7 | except ImportError as e:
8 | raise ImportError("Failed to import ladybug_geometry.\n{}".format(e))
9 |
10 | try:
11 | import Rhino.Geometry as rg
12 | except ImportError as e:
13 | raise ImportError(
14 | "Failed to import Rhino.\n{}".format(e))
15 |
16 | import sys
17 | if (sys.version_info > (3, 0)): # python 3
18 | xrange = range
19 |
20 |
21 | """____________INDIVIDUAL SURFACES TO PLANAR____________"""
22 |
23 |
24 | def planar_face_curved_edge_vertices(b_face, count, meshing_parameters):
25 | """Extract vertices from a planar brep face loop that has one or more curved edges.
26 |
27 | This method ensures vertices along the curved edge are generated in a way that
28 | they align with an extrusion of that edge. Alignment may not be possible when
29 | the adjoining curved surface is not an extrusion.
30 |
31 | Args:
32 | b_face: A brep face with the curved edge.
33 | count: An integer for the index of the loop to extract.
34 | meshing_parameters: Rhino Meshing Parameters to describe how
35 | curved edge should be converted into planar elements.
36 |
37 | Returns:
38 | A list of ladybug Point3D objects representing the input planar face.
39 | """
40 | loop_pcrv = b_face.Loops.Item[count].To3dCurve()
41 | f_norm = b_face.NormalAt(0, 0)
42 | if f_norm.Z < 0:
43 | loop_pcrv.Reverse()
44 | loop_verts = []
45 | try:
46 | loop_pcrvs = [loop_pcrv.SegmentCurve(i)
47 | for i in xrange(loop_pcrv.SegmentCount)]
48 | except Exception:
49 | try:
50 | loop_pcrvs = [loop_pcrv[0]]
51 | except Exception:
52 | loop_pcrvs = [loop_pcrv]
53 | for seg in loop_pcrvs:
54 | if seg.Degree == 1:
55 | loop_verts.append(_point3d(seg.PointAtStart))
56 | else:
57 | # perform curvature analysis to see if it is just a line
58 | if seg.Degree != 2:
59 | max_curve = seg.MaxCurvaturePoints()
60 | if max_curve is None: # it is just a line-like curve
61 | loop_verts.append(_point3d(seg.PointAtStart))
62 | continue
63 | max_par = seg.ClosestPoint(max_curve[0])[1]
64 | else: # arcs have the same curvature everywhere
65 | max_par = 0
66 | if seg.CurvatureAt(max_par).Length < 0.001: # it is a line-like curve
67 | loop_verts.append(_point3d(seg.PointAtStart))
68 | continue
69 | # ensure curve subdivisions align with adjacent curved faces
70 | seg_mesh = rg.Mesh.CreateFromSurface(
71 | rg.Surface.CreateExtrusion(seg, f_norm),
72 | meshing_parameters)
73 | for i in xrange(seg_mesh.Vertices.Count / 2 - 1):
74 | pt = seg_mesh.Vertices[i]
75 | pt = rg.Point3d(pt.X, pt.Y, pt.Z)
76 | close_pt = seg.PointAt(seg.ClosestPoint(pt)[1])
77 | if pt.DistanceTo(close_pt) < tolerance:
78 | loop_verts.append(_point3d(pt))
79 | else:
80 | break
81 |
82 | return loop_verts
83 |
84 |
85 | def curved_surface_faces(b_face, meshing_parameters):
86 | """Extract Face3D objects from a curved brep face.
87 |
88 | Args:
89 | b_face: A curved brep face.
90 | meshing_parameters: Rhino Meshing Parameters to describe how
91 | curved edge should be converted into planar elements.
92 |
93 | Returns:
94 | A list of ladybug Face3D objects that together approximate the input
95 | curved surface.
96 | """
97 | if b_face.OrientationIsReversed:
98 | b_face.Reverse(0, True)
99 | face_brep = b_face.DuplicateFace(True)
100 | meshed_brep = rg.Mesh.CreateFromBrep(face_brep, meshing_parameters)[0]
101 | return mesh_faces_to_face3d(meshed_brep)
102 |
103 |
104 | """____________SOLID BREPS TO PLANAR____________"""
105 |
106 |
107 | def curved_solid_faces(brep, meshing_parameters, ignore_sliver=True):
108 | """Extract Face3D objects from a curved solid brep.
109 |
110 | This method ensures that the resulting Face3Ds sill form a closed solid if
111 | the input brep is closed. This is accomplished by meshing the solid Brep
112 | altogether.
113 |
114 | Args:
115 | brep: A curved solid brep.
116 | meshing_parameters: Rhino Meshing Parameters to describe how curved surfaces
117 | should be converted into planar elements. If None, Rhino's Default
118 | Meshing Parameters will be used.
119 | ignore_sliver: A Boolean to note whether tiny sliver faces should simply be
120 | excluded from the output (True) or whether a None should be put in
121 | their place (False). The latter is useful when reporting to the user
122 | that certain tiny geometries interfered with the planarization
123 | process and the Rhino model tolerance should probably be lowered
124 | in order to get a better planar representation. (Default: True).
125 |
126 | Returns:
127 | A list of ladybug Face3D objects that together approximate the input brep.
128 | """
129 | # mesh the geometry as a solid
130 | mesh_par = meshing_parameters or rg.MeshingParameters.Default # default
131 | meshed_geo = rg.Mesh.CreateFromBrep(brep, mesh_par)
132 |
133 | # evaluate each mesh face to see what larger brep face it is a part of
134 | faces = []
135 | for mesh, b_face in zip(meshed_geo, brep.Faces):
136 | if b_face.IsPlanar(tolerance): # only take the naked vertices of planar faces
137 | naked_edges = mesh.GetNakedEdges()
138 | all_verts = []
139 | for loop_pline in naked_edges: # each loop_pline is a boundary/hole
140 | all_verts.append([_point3d(loop_pline.Item[i])
141 | for i in xrange(loop_pline.Count - 1)])
142 | if len(all_verts) == 1: # No holes in the shape
143 | faces.append(Face3D(all_verts[0]))
144 | else: # There's at least one hole in the shape
145 | faces.append(
146 | Face3D(boundary=all_verts[0], holes=all_verts[1:]))
147 | else:
148 | faces.extend(mesh_faces_to_face3d(mesh))
149 | # remove colinear vertices as the meshing process makes a lot of them
150 | final_faces = []
151 | for face in faces:
152 | try:
153 | final_faces.append(face.remove_colinear_vertices(tolerance))
154 | except AssertionError: # tiny sliver Face that should not be included
155 | if not ignore_sliver:
156 | final_faces.append(None)
157 | return final_faces
158 |
159 |
160 | """________________EXTRA HELPER FUNCTIONS________________"""
161 |
162 |
163 | def has_curved_face(brep):
164 | """Test if a Rhino Brep has a curved face.
165 |
166 | Args:
167 | brep: A Rhino Brep to test whether it has a curved face.
168 | """
169 | for b_face in brep.Faces:
170 | if not b_face.IsPlanar(tolerance):
171 | return True
172 | return False
173 |
174 |
175 | def mesh_faces_to_face3d(mesh):
176 | """Convert a curved Rhino mesh faces into planar ladybug_geometry Face3D.
177 |
178 | Args:
179 | mesh: A curved Rhino mesh.
180 |
181 | Returns:
182 | A list of ladybug Face3D objects derived from the input mesh faces.
183 | """
184 | faces = []
185 | for m_face in mesh.Faces:
186 | if m_face.IsQuad:
187 | lb_face = Face3D(
188 | tuple(_point3d(mesh.Vertices[i]) for i in
189 | (m_face.A, m_face.B, m_face.C, m_face.D)))
190 | if lb_face.check_planar(tolerance, False):
191 | faces.append(lb_face)
192 | else:
193 | lb_face_1 = Face3D(
194 | tuple(_point3d(mesh.Vertices[i]) for i in
195 | (m_face.A, m_face.B, m_face.C)))
196 | lb_face_2 = Face3D(
197 | tuple(_point3d(mesh.Vertices[i]) for i in
198 | (m_face.C, m_face.D, m_face.A)))
199 | faces.extend([lb_face_1, lb_face_2])
200 | else:
201 | lb_face = Face3D(
202 | tuple(_point3d(mesh.Vertices[i]) for i in
203 | (m_face.A, m_face.B, m_face.C)))
204 | faces.append(lb_face)
205 | return faces
206 |
207 |
208 | def _point3d(point):
209 | """Ladybug Point3D from Rhino Point3d."""
210 | return Point3D(point.X, point.Y, point.Z)
211 |
--------------------------------------------------------------------------------
/ladybug_rhino/fromobjects.py:
--------------------------------------------------------------------------------
1 | """Functions to translate entire Ladybug core objects to Rhino geometries.
2 |
3 | The methods here are intended to help translate groups of geometry that are commonly
4 | generated by several objects in Ladybug core (ie. legends, compasses, etc.)
5 | """
6 | import math
7 |
8 | from .fromgeometry import from_point2d, from_vector2d, from_ray2d, from_linesegment2d, \
9 | from_arc2d, from_polyline2d, from_polygon2d, from_mesh2d, \
10 | from_point3d, from_vector3d, from_ray3d, from_linesegment3d, from_arc3d, \
11 | from_plane, from_polyline3d, from_mesh3d, from_face3d, from_polyface3d, \
12 | from_sphere, from_cone, from_cylinder
13 | from .text import text_objects
14 |
15 | try:
16 | from ladybug_geometry.geometry2d import Vector2D, Point2D, Ray2D, LineSegment2D, \
17 | Arc2D, Polyline2D, Polygon2D, Mesh2D
18 | from ladybug_geometry.geometry3d import Vector3D, Point3D, Ray3D, LineSegment3D, \
19 | Arc3D, Polyline3D, Plane, Mesh3D, Face3D, Polyface3D, Sphere, Cone, Cylinder
20 | except ImportError as e:
21 | raise ImportError("Failed to import ladybug_geometry.\n{}".format(e))
22 |
23 |
24 | def legend_objects(legend):
25 | """Translate a Ladybug Legend object into Grasshopper geometry.
26 |
27 | Args:
28 | legend: A Ladybug Legend object to be converted to Rhino geometry.
29 |
30 | Returns:
31 | A list of Rhino geometries in the following order.
32 |
33 | - legend_mesh -- A colored mesh for the legend.
34 |
35 | - legend_title -- A bake-able text object for the legend title.
36 |
37 | - legend_text -- Bake-able text objects for the rest of the legend text.
38 | """
39 | _height = legend.legend_parameters.text_height
40 | _font = legend.legend_parameters.font
41 | legend_mesh = from_mesh3d(legend.segment_mesh)
42 | legend_title = text_objects(legend.title, legend.title_location, _height, _font)
43 | if legend.legend_parameters.continuous_legend is False:
44 | legend_text = [text_objects(txt, loc, _height, _font, 0, 5) for txt, loc in
45 | zip(legend.segment_text, legend.segment_text_location)]
46 | elif legend.legend_parameters.vertical is True:
47 | legend_text = [text_objects(txt, loc, _height, _font, 0, 3) for txt, loc in
48 | zip(legend.segment_text, legend.segment_text_location)]
49 | else:
50 | legend_text = [text_objects(txt, loc, _height, _font, 1, 5) for txt, loc in
51 | zip(legend.segment_text, legend.segment_text_location)]
52 | return [legend_mesh] + [legend_title] + legend_text
53 |
54 |
55 | def compass_objects(compass, z=0, custom_angles=None, projection=None, font='Arial'):
56 | """Translate a Ladybug Compass object into Grasshopper geometry.
57 |
58 | Args:
59 | compass: A Ladybug Compass object to be converted to Rhino geometry.
60 | z: A number for the Z-coordinate to be used in translation. (Default: 0)
61 | custom_angles: An array of numbers between 0 and 360 to be used to
62 | generate custom angle labels around the compass.
63 | projection: Text for the name of the projection to use from the sky
64 | dome hemisphere to the 2D plane. If None, no altitude circles o
65 | labels will be drawn (Default: None). Choose from the following:
66 |
67 | * Orthographic
68 | * Stereographic
69 |
70 | font: Optional text for the font to be used in creating the text.
71 | (Default: 'Arial')
72 |
73 | Returns:
74 | A list of Rhino geometries in the following order.
75 |
76 | - all_boundary_circles -- Three Circle objects for the compass boundary.
77 |
78 | - major_azimuth_ticks -- Line objects for the major azimuth labels.
79 |
80 | - major_azimuth_text -- Bake-able text objects for the major azimuth labels.
81 |
82 | - minor_azimuth_ticks -- Line objects for the minor azimuth labels
83 | (if applicable).
84 |
85 | - minor_azimuth_text -- Bake-able text objects for the minor azimuth
86 | labels (if applicable).
87 |
88 | - altitude_circles -- Circle objects for the altitude labels.
89 |
90 | - altitude_text -- Bake-able text objects for the altitude labels.
91 |
92 | """
93 | # set default variables based on the compass properties
94 | maj_txt = compass.radius / 20
95 | min_txt = maj_txt / 2
96 | xaxis = Vector3D(1, 0, 0).rotate_xy(math.radians(compass.north_angle))
97 |
98 | result = [] # list to hold all of the returned objects
99 | for circle in compass.all_boundary_circles:
100 | result.append(from_arc2d(circle, z))
101 |
102 | # generate the labels and tick marks for the azimuths
103 | if custom_angles is None:
104 | for line in compass.major_azimuth_ticks:
105 | result.append(from_linesegment2d(line, z))
106 | for txt, pt in zip(compass.MAJOR_TEXT, compass.major_azimuth_points):
107 | txt_pln = Plane(o=Point3D(pt.x, pt.y, z), x=xaxis)
108 | result.append(text_objects(txt, txt_pln, maj_txt, font, 1, 3))
109 | for line in compass.minor_azimuth_ticks:
110 | result.append(from_linesegment2d(line, z))
111 | for txt, pt in zip(compass.MINOR_TEXT, compass.minor_azimuth_points):
112 | txt_pln = Plane(o=Point3D(pt.x, pt.y, z), x=xaxis)
113 | result.append(text_objects(txt, txt_pln, min_txt, font, 1, 3))
114 | else:
115 | for line in compass.ticks_from_angles(custom_angles):
116 | result.append(from_linesegment2d(line, z))
117 | for txt, pt in zip(
118 | custom_angles, compass.label_points_from_angles(custom_angles)):
119 | txt_pln = Plane(o=Point3D(pt.x, pt.y, z), x=xaxis)
120 | result.append(text_objects(str(txt), txt_pln, maj_txt, font, 1, 3))
121 |
122 | # generate the labels and tick marks for the altitudes
123 | if projection is not None:
124 | if projection.title() == 'Orthographic':
125 | for circle in compass.orthographic_altitude_circles:
126 | result.append(from_arc2d(circle, z))
127 | for txt, pt in zip(compass.ALTITUDES, compass.orthographic_altitude_points):
128 | txt_pln = Plane(o=Point3D(pt.x, pt.y, z), x=xaxis)
129 | result.append(text_objects(str(txt), txt_pln, min_txt, font, 1, 0))
130 | elif projection.title() == 'Stereographic':
131 | for circle in compass.stereographic_altitude_circles:
132 | result.append(from_arc2d(circle, z))
133 | for txt, pt in zip(compass.ALTITUDES, compass.stereographic_altitude_points):
134 | txt_pln = Plane(o=Point3D(pt.x, pt.y, z), x=xaxis)
135 | result.append(text_objects(str(txt), txt_pln, min_txt, font, 1, 0))
136 |
137 | return result
138 |
139 |
140 | def from_geometry(geometry):
141 | """Generic geometry translation function that works for any ladybug-geometry object.
142 |
143 | This is only recommended for cases where an input geometry stream can contain
144 | a variety of different objects. When the geometry type is know, it can be
145 | significantly faster to use the dedicated geometry translator.
146 |
147 | Args:
148 | geometry: Any 2D or 3D ladybug-geometry object.
149 | """
150 | if isinstance(geometry, Point3D):
151 | return from_point3d(geometry)
152 | elif isinstance(geometry, Vector3D):
153 | return from_vector3d(geometry)
154 | elif isinstance(geometry, Ray3D):
155 | return from_ray3d(geometry)
156 | elif isinstance(geometry, LineSegment3D):
157 | return from_linesegment3d(geometry)
158 | elif isinstance(geometry, Plane):
159 | return from_plane(geometry)
160 | elif isinstance(geometry, Arc3D):
161 | return from_arc3d(geometry)
162 | elif isinstance(geometry, Polyline3D):
163 | return from_polyline3d(geometry)
164 | elif isinstance(geometry, Mesh3D):
165 | return from_mesh3d(geometry)
166 | elif isinstance(geometry, Face3D):
167 | return from_face3d(geometry)
168 | elif isinstance(geometry, Polyface3D):
169 | return from_polyface3d(geometry)
170 | elif isinstance(geometry, Sphere):
171 | return from_sphere(geometry)
172 | elif isinstance(geometry, Cone):
173 | return from_cone(geometry)
174 | elif isinstance(geometry, Cylinder):
175 | return from_cylinder(geometry)
176 | elif isinstance(geometry, Point2D):
177 | return from_point2d(geometry)
178 | elif isinstance(geometry, Vector2D):
179 | return from_vector2d(geometry)
180 | elif isinstance(geometry, Ray2D):
181 | return from_ray2d(geometry)
182 | elif isinstance(geometry, LineSegment2D):
183 | return from_linesegment2d(geometry)
184 | elif isinstance(geometry, Arc2D):
185 | return from_arc2d(geometry)
186 | elif isinstance(geometry, Polygon2D):
187 | return from_polygon2d(geometry)
188 | elif isinstance(geometry, Polyline2D):
189 | return from_polyline2d(geometry)
190 | elif isinstance(geometry, Mesh2D):
191 | return from_mesh2d(geometry)
192 |
--------------------------------------------------------------------------------
/ladybug_rhino/versioning/gather.py:
--------------------------------------------------------------------------------
1 | """Functions for gathering connected components or all components on a canvas."""
2 | try:
3 | import System.Drawing
4 | except ImportError:
5 | raise ImportError("Failed to import System.")
6 |
7 | try:
8 | import Grasshopper.Kernel as gh
9 | import Grasshopper.Instances as ghi
10 | except ImportError:
11 | raise ImportError("Failed to import Grasshopper.")
12 |
13 |
14 | # Master array of all identifiers of Ladybug Tools components
15 | LADYBUG_TOOLS = ('LB', 'HB', 'DF', 'BF', 'Ladybug', 'Honeybee',
16 | 'Butterfly', 'HoneybeePlus')
17 |
18 |
19 | def is_ladybug_tools(component):
20 | """Check if a component is a part of Ladybug Tools."""
21 | return component.Name.split(' ')[0] in LADYBUG_TOOLS or \
22 | component.Name.split('_')[0] in LADYBUG_TOOLS
23 |
24 |
25 | def gather_canvas_components(component):
26 | """Get all of the Ladybug Tools components on the same canvas as the input component.
27 |
28 | This will also gather any Ladybug Tools components inside of clusters.
29 |
30 | Args:
31 | component: A Grasshopper component object. Typically, this should be the
32 | exporter component object, which can be accessed through the
33 | ghenv.Component call.
34 |
35 | Returns:
36 | A list of Ladybug Tools component objects on the same canvas as the
37 | input component. The input component is excluded from this list.
38 | """
39 | components = []
40 | document = component.OnPingDocument()
41 | for comp_obj in document.Objects:
42 | if type(comp_obj) == type(component): # GHPython component
43 | if is_ladybug_tools(comp_obj): # Ladybug Tools component
44 | components.append(comp_obj)
45 | elif type(comp_obj) == gh.Special.GH_Cluster:
46 | cluster_doc = comp_obj.Document("")
47 | if not cluster_doc:
48 | continue
49 | for cluster_obj in cluster_doc.Objects:
50 | if type(cluster_obj) == type(component) and \
51 | is_ladybug_tools(cluster_obj):
52 | if cluster_obj.Locked:
53 | continue
54 | components.append(cluster_obj)
55 |
56 | # remove the exporter component from the array
57 | components = tuple(comp for comp in components if
58 | comp.InstanceGuid != component.InstanceGuid)
59 |
60 | return components
61 |
62 |
63 | def gather_connected_components(component):
64 | """Get all of the GHPython components connected to the component's first input.
65 |
66 | Args:
67 | component: A Grasshopper component object. Typically, this should be the
68 | exporter component object, which can be accessed through the
69 | ghenv.Component call.
70 |
71 | Returns:
72 | A list of Ladybug Tools component objects that are connected to the component's
73 | first input.
74 | """
75 | param = component.Params.Input[0] # components input
76 | sources = param.Sources
77 |
78 | if sources.Count == 0:
79 | # no component is connected
80 | yield []
81 |
82 | for src in sources:
83 | attr = src.Attributes
84 | if attr is None or attr.GetTopLevel is None: # not exportable
85 | continue
86 | # collect components
87 | comp_obj = attr.GetTopLevel.DocObject
88 | if comp_obj is None:
89 | continue
90 | if type(comp_obj) != type(component): # not a GHPython component
91 | continue
92 |
93 | yield comp_obj
94 |
95 |
96 | def plugin_components(plugin_name, sub_category=None):
97 | """Get all of the components of a Ladybug Tools Plugin from the component server.
98 |
99 | Args:
100 | plugin_name: Text for the name of a particular plugin (aka. insect) to
101 | place components from (eg. "Ladybug", "Honeybee", "HB-Energy").
102 | sub_category: Text for a specific plugin sub-category (aka. tab) to
103 | be exported (eg. "1 :: Analyze Data"). If None, all components in
104 | the plugin will be places on the canvas. (Default: None).
105 |
106 | Returns:
107 | A dictionary of the plugin components with the name of the component as
108 | the key and the component object as the value.
109 | """
110 | # loop through the component server and find all the components in the plugin
111 | components = {}
112 | for proxy in ghi.ComponentServer.ObjectProxies:
113 | if proxy.Obsolete:
114 | continue
115 | category = proxy.Desc.Category
116 | subcategory = proxy.Desc.SubCategory
117 | # check to see if the component is in the plugin
118 | if category.strip() == plugin_name: # if so, see if it is in the sub category
119 | if sub_category is None or subcategory.strip() == sub_category:
120 | if str(proxy.Kind) == 'CompiledObject': # it's a .gha
121 | components[proxy.Desc.Name] = proxy.CreateInstance()
122 | elif str(proxy.Kind) == 'UserObject': # it's a .ghuser
123 | components[proxy.Desc.Name] = \
124 | gh.GH_UserObject(proxy.Location).InstantiateObject()
125 | return components
126 |
127 |
128 | def place_component(component_reference, component_name, x_position=200, y_position=200,
129 | hold_solution=False):
130 | """Place a single component on the canvas.
131 |
132 | Args:
133 | component_reference: A Grasshopper component reference object to be placed
134 | on the canvas.
135 | component_name: Text for the name of the component being placed.
136 | x_position: An integer for where in the X dimension of the canvas the
137 | components will be dropped. (Default: 200).
138 | y_position: An integer for where in the Y dimension of the canvas the
139 | components will be dropped. (Default: 200).
140 | hold_solution: Boolean to note whether to hold off on the solution after
141 | the component is dropped on the canvas. (Default: False).
142 |
143 | Returns:
144 | The Component Object for the components that have been dropped
145 | onto the canvas.
146 | """
147 | document = ghi.ActiveCanvas.Document # get the Grasshopper document
148 | component_reference.Attributes.Pivot = System.Drawing.PointF(x_position, y_position)
149 | document.AddObject(component_reference, False, 0)
150 |
151 | # find the component object on the Grasshopper canvas using its name
152 | component = None
153 | for comp in document.Objects:
154 | if comp.Name == component_name:
155 | component = comp
156 | break
157 |
158 | # expire component solution so that all of the components don't run at once
159 | if hold_solution:
160 | try:
161 | component.ExpireSolution(False)
162 | except Exception:
163 | print('Failed to stop "{}" from running'.format(component_name))
164 | return component
165 |
166 |
167 | def place_plugin_components(plugin_name, sub_category=None, x_position=200,
168 | y_position=200):
169 | """Place all of the components of a specific Ladybug Tools Plugin on the canvas.
170 |
171 | Args:
172 | plugin_name: Text for the name of a particular plugin (aka. insect) to
173 | place components from (eg. "Ladybug", "Honeybee", "HB-Energy").
174 | sub_category: Text for a specific plugin sub-category (aka. tab) to
175 | be exported (eg. "1 :: Analyze Data"). If None, all components in
176 | the plugin will be places on the canvas. (Default: None).
177 | x_position: An integer for where in the X dimension of the canvas the
178 | components will be dropped. (Default: 200).
179 | y_position: An integer for where in the Y dimension of the canvas the
180 | components will be dropped. (Default: 200).
181 |
182 | Returns:
183 | A list of Component Objects for the components that have been dropped
184 | onto the canvas. These component objects can be used to update the
185 | version of the dropped component, change their category, etc.
186 | """
187 | # find all Components/UserObjects in the Grasshopper Component Server
188 | component_references = plugin_components(plugin_name, sub_category)
189 |
190 | # loop through the components and add them to the canvass
191 | components = [] # array to hold all of the dropped components
192 | for comp_name, comp_obj in component_references.items():
193 | # add object to document
194 | if comp_obj.Attributes:
195 | comp = place_component(comp_obj, comp_name, x_position, y_position, True)
196 | components.append(comp)
197 | return components
198 |
199 |
200 | def remove_component(component):
201 | """Remove a Grasshopper component from the canvas.
202 |
203 | Args:
204 | component: The Grasshopper component object to be removed.
205 | """
206 | document = ghi.ActiveCanvas.Document # get the Grasshopper document
207 | document.RemoveObject(component, False)
208 |
--------------------------------------------------------------------------------
/ladybug_rhino/fromhoneybee.py:
--------------------------------------------------------------------------------
1 | """Functions to translate from Honeybee geometries to Rhino geometries."""
2 | from __future__ import division
3 |
4 | from .config import tolerance
5 | from .fromgeometry import from_face3d, from_mesh3d, from_face3d_to_wireframe, \
6 | from_mesh3d_to_wireframe
7 |
8 | try:
9 | import Rhino.Geometry as rg
10 | except ImportError as e:
11 | raise ImportError('Failed to import Rhino Geometry.\n{}'.format(e))
12 |
13 | try: # import the core honeybee dependencies
14 | from honeybee.model import Model
15 | from honeybee.room import Room
16 | from honeybee.face import Face
17 | from honeybee.aperture import Aperture
18 | from honeybee.door import Door
19 | from honeybee.shade import Shade
20 | from honeybee.shademesh import ShadeMesh
21 | except ImportError as e:
22 | print('Failed to import honeybee. Translation from Honeybee objects '
23 | 'is unavailable.\n{}'.format(e))
24 |
25 |
26 | """________TRANSLATORS TO BREPS/MESHES________"""
27 |
28 |
29 | def from_shade(shade):
30 | """Rhino Brep from a Honeybee Shade."""
31 | return from_face3d(shade.geometry)
32 |
33 |
34 | def from_shade_mesh(shade_mesh):
35 | """Rhino Mesh from a Honeybee ShadeMesh."""
36 | return from_mesh3d(shade_mesh.geometry)
37 |
38 |
39 | def from_door(door):
40 | """Rhino Breps from a Honeybee Door (with shades).
41 |
42 | The first Brep in the returned result is the Door. All following Breps
43 | are Door-assigned Shades.
44 | """
45 | door_breps = [from_face3d(door.geometry)]
46 | door_breps.extend([from_shade(shd) for shd in door.shades])
47 | return door_breps
48 |
49 |
50 | def from_aperture(aperture):
51 | """Rhino Breps from a Honeybee Aperture (with shades).
52 |
53 | The first Brep in the returned result is the Aperture. All following Breps
54 | are Aperture-assigned Shades.
55 | """
56 | aperture_breps = [from_face3d(aperture.geometry)]
57 | aperture_breps.extend([from_shade(shd) for shd in aperture.shades])
58 | return aperture_breps
59 |
60 |
61 | def from_face(face):
62 | """Rhino Breps from a Honeybee Face (with shades).
63 |
64 | The first Brep in the returned result is the Face (with Apertures and Doors
65 | joined into it). All following Breps are assigned Shades.
66 | """
67 | face_breps = [from_face3d(face.punched_geometry)]
68 | shade_breps = [from_shade(shd) for shd in face.shades]
69 | for ap in face.apertures:
70 | aperture_breps = from_aperture(ap)
71 | face_breps.append(aperture_breps[0])
72 | shade_breps.extend(aperture_breps[1:])
73 | for dr in face.doors:
74 | door_breps = from_aperture(dr)
75 | face_breps.append(door_breps[0])
76 | shade_breps.extend(door_breps[1:])
77 | face_breps = list(rg.Brep.JoinBreps(face_breps, tolerance))
78 | return face_breps + shade_breps
79 |
80 |
81 | def from_room(room):
82 | """Rhino Breps from a Honeybee Room (with shades).
83 |
84 | The first Brep in the returned result is a joined Polyface Brep for the Room.
85 | All following Breps are assigned Shades.
86 | """
87 | room_breps = []
88 | shade_breps = [from_shade(shd) for shd in room.shades]
89 | for face in room.faces:
90 | face_breps = from_face(face)
91 | room_breps.append(face_breps[0])
92 | shade_breps.extend(face_breps[1:])
93 | room_breps = list(rg.Brep.JoinBreps(room_breps, tolerance))
94 | return room_breps + shade_breps
95 |
96 |
97 | def from_model(model):
98 | """Rhino Breps and Meshes from a Honeybee Model.
99 |
100 | The first Breps in the returned result will be joined Polyface Breps for each
101 | Room in the Model. This will be followed by Breps for orphaned objects, which
102 | will be followed by Breps for Shades assigned to any parent objects. Lastly,
103 | there will be Meshes for any ShadeMeshes in the Model.
104 | """
105 | parent_geo = []
106 | shade_geo = [from_shade(shd) for shd in model.orphaned_shades]
107 | for room in model.rooms:
108 | room_breps = from_room(room)
109 | parent_geo.append(room_breps[0])
110 | shade_geo.extend(room_breps[1:])
111 | for face in model.orphaned_faces:
112 | face_breps = from_face(face)
113 | parent_geo.append(face_breps[0])
114 | shade_geo.extend(face_breps[1:])
115 | for ap in model.orphaned_apertures:
116 | ap_breps = from_aperture(ap)
117 | parent_geo.append(ap_breps[0])
118 | shade_geo.extend(ap_breps[1:])
119 | for dr in model.orphaned_doors:
120 | dr_breps = from_door(dr)
121 | parent_geo.append(dr_breps[0])
122 | shade_geo.extend(dr_breps[1:])
123 | shade_mesh_geo = [from_shade_mesh(sm) for sm in model.shade_meshes]
124 | return parent_geo + shade_geo + shade_mesh_geo
125 |
126 |
127 | def from_hb_objects(hb_objects):
128 | """Rhino Breps and Meshes from a list of any Honeybee geometry objects.
129 |
130 | Any Honeybee geometry object may be input to this method (Model, Room, Face,
131 | Aperture, Door, Shade, ShadeMesh). The returned result will always have
132 | Breps first and Meshes last (if applicable).
133 | """
134 | brep_geo, mesh_geo = [], []
135 | for hb_obj in hb_objects:
136 | if isinstance(hb_obj, Room):
137 | brep_geo.extend(from_room(hb_obj))
138 | elif isinstance(hb_obj, Shade):
139 | brep_geo.append(from_shade(hb_obj))
140 | elif isinstance(hb_obj, Face):
141 | brep_geo.extend(from_face(hb_obj))
142 | elif isinstance(hb_obj, Aperture):
143 | brep_geo.extend(from_aperture(hb_obj))
144 | elif isinstance(hb_obj, Door):
145 | brep_geo.extend(from_door(hb_obj))
146 | elif isinstance(hb_obj, Model):
147 | model_geo = from_model(hb_obj)
148 | for geo in reversed(model_geo):
149 | if isinstance(geo, rg.Mesh):
150 | mesh_geo.append(geo)
151 | else:
152 | brep_geo.append(geo)
153 | elif isinstance(hb_obj, ShadeMesh):
154 | mesh_geo.append(from_shade_mesh(hb_obj))
155 | else:
156 | raise TypeError(
157 | 'Unrecognized honeybee object type: {}'.format(type(hb_obj)))
158 | return brep_geo + mesh_geo
159 |
160 |
161 | """________TRANSLATORS TO WIREFRAMES________"""
162 |
163 |
164 | def from_shade_to_wireframe(shade):
165 | """Rhino PolyLineCurves from a Honeybee Shade."""
166 | return from_face3d_to_wireframe(shade.geometry)
167 |
168 |
169 | def from_shade_mesh_to_wireframe(shade_mesh):
170 | """Rhino PolyLineCurves from a Honeybee ShadeMesh."""
171 | return from_mesh3d_to_wireframe(shade_mesh.geometry)
172 |
173 |
174 | def from_door_to_wireframe(door):
175 | """Rhino PolyLineCurves from a Honeybee Door (with shades)."""
176 | door_wires = from_face3d_to_wireframe(door.geometry)
177 | for shd in door.shades:
178 | door_wires.extend(from_shade_to_wireframe(shd))
179 | return door_wires
180 |
181 |
182 | def from_aperture_to_wireframe(aperture):
183 | """Rhino PolyLineCurves from a Honeybee Aperture (with shades)."""
184 | aperture_wires = from_face3d_to_wireframe(aperture.geometry)
185 | for shd in aperture.shades:
186 | aperture_wires.extend(from_shade_to_wireframe(shd))
187 | return aperture_wires
188 |
189 |
190 | def from_face_to_wireframe(face):
191 | """Rhino PolyLineCurves from a Honeybee Face (with shades)."""
192 | face_wires = from_face3d_to_wireframe(face.punched_geometry)
193 | for ap in face.apertures:
194 | face_wires.extend(from_aperture_to_wireframe(ap))
195 | for dr in face.doors:
196 | face_wires.extend(from_door_to_wireframe(dr))
197 | for shd in face.shades:
198 | face_wires.extend(from_shade_to_wireframe(shd))
199 | return face_wires
200 |
201 |
202 | def from_room_to_wireframe(room):
203 | """Rhino PolyLineCurves from a Honeybee Room (with shades)."""
204 | room_wires = []
205 | for face in room.faces:
206 | room_wires.extend(from_face_to_wireframe(face))
207 | for shd in room.shades:
208 | room_wires.extend(from_shade_to_wireframe(shd))
209 | return room_wires
210 |
211 |
212 | def from_model_to_wireframe(model):
213 | """Rhino PolyLineCurves and Meshes from a Honeybee Model."""
214 | model_wires = []
215 | for room in model.rooms:
216 | model_wires.extend(from_room_to_wireframe(room))
217 | for face in model.orphaned_faces:
218 | model_wires.extend(from_face_to_wireframe(face))
219 | for ap in model.orphaned_apertures:
220 | model_wires.extend(from_aperture_to_wireframe(ap))
221 | for dr in model.orphaned_doors:
222 | model_wires.extend(from_door_to_wireframe(dr))
223 | for shd in model.orphaned_shades:
224 | model_wires.extend(from_shade_to_wireframe(shd))
225 | for sm in model.shade_meshes:
226 | model_wires.extend(from_shade_mesh_to_wireframe(sm))
227 | return model_wires
228 |
229 |
230 | def from_hb_objects_to_wireframe(hb_objects):
231 | """Rhino PolyLineCurves and Meshes from a list of any Honeybee geometry objects."""
232 | wire_geo = []
233 | for hb_obj in hb_objects:
234 | if isinstance(hb_obj, Room):
235 | wire_geo.extend(from_room_to_wireframe(hb_obj))
236 | elif isinstance(hb_obj, Shade):
237 | wire_geo.extend(from_shade_to_wireframe(hb_obj))
238 | elif isinstance(hb_obj, Face):
239 | wire_geo.extend(from_face_to_wireframe(hb_obj))
240 | elif isinstance(hb_obj, Aperture):
241 | wire_geo.extend(from_aperture_to_wireframe(hb_obj))
242 | elif isinstance(hb_obj, Door):
243 | wire_geo.extend(from_door_to_wireframe(hb_obj))
244 | elif isinstance(hb_obj, ShadeMesh):
245 | wire_geo.extend(from_shade_mesh_to_wireframe(hb_obj))
246 | elif isinstance(hb_obj, Model):
247 | wire_geo.extend(from_model_to_wireframe(hb_obj))
248 | else:
249 | raise TypeError(
250 | 'Unrecognized honeybee object type: {}'.format(type(hb_obj)))
251 | return wire_geo
252 |
--------------------------------------------------------------------------------
/ladybug_rhino/bakedisplay.py:
--------------------------------------------------------------------------------
1 | """Functions to add text to the Rhino scene and create Grasshopper text objects."""
2 | from ladybug_display.altnumber import Default
3 |
4 | from .color import color_to_color, argb_color_to_color
5 | from .fromgeometry import from_plane
6 | from .bakegeometry import _get_attributes, bake_point2d, bake_vector2d, bake_ray2d, \
7 | bake_linesegment2d, bake_arc2d, bake_polygon2d, bake_polyline2d, bake_mesh2d, \
8 | bake_point3d, bake_vector3d, bake_ray3d, bake_linesegment3d, bake_plane, \
9 | bake_arc3d, bake_polyline3d, bake_mesh3d, bake_face3d, bake_polyface3d, \
10 | bake_sphere, bake_cone, bake_cylinder
11 |
12 | try:
13 | import Rhino.Display as rd
14 | import Rhino.DocObjects as docobj
15 | from Rhino import RhinoDoc as rhdoc
16 | except ImportError as e:
17 | raise ImportError("Failed to import Rhino document attributes.\n{}".format(e))
18 |
19 | TEXT_HORIZ = {
20 | 'Left': docobj.TextHorizontalAlignment.Left,
21 | 'Center': docobj.TextHorizontalAlignment.Center,
22 | 'Right': docobj.TextHorizontalAlignment.Right
23 | }
24 | TEXT_VERT = {
25 | 'Top': docobj.TextVerticalAlignment.Top,
26 | 'Middle': docobj.TextVerticalAlignment.Middle,
27 | 'Bottom': docobj.TextVerticalAlignment.Bottom
28 | }
29 | LINE_WIDTH_FACTOR = 3.779528 # factor to translate pixel width to millimeters
30 | LINE_TYPES = {
31 | 'Continuous': -1,
32 | 'Dashed': -1,
33 | 'Dotted': -1,
34 | 'DashDot': -1
35 | }
36 | _display_lts = ('Continuous', 'Dashed', 'Dots', 'DashDot')
37 | for i, lt in enumerate(rhdoc.ActiveDoc.Linetypes):
38 | lt_name = lt.Name
39 | for dlt in _display_lts:
40 | if lt_name == dlt:
41 | dlt = dlt if dlt != 'Dots' else 'Dotted'
42 | LINE_TYPES[dlt] = i
43 | break
44 |
45 |
46 | """____________BAKE 2D DISPLAY GEOMETRY TO THE RHINO SCENE____________"""
47 |
48 |
49 | def bake_display_vector2d(vector, z=0, layer_name=None, attributes=None):
50 | """Add DisplayVector2D to the Rhino scene as a Line with an Arrowhead."""
51 | attrib = _get_attributes(layer_name, attributes)
52 | _color_attribute(attrib, vector)
53 | return bake_vector2d(vector.geometry, z, attributes=attrib)
54 |
55 |
56 | def bake_display_point2d(point, z=0, layer_name=None, attributes=None):
57 | """Add ladybug Point2D to the Rhino scene as a Point."""
58 | attrib = _get_attributes(layer_name, attributes)
59 | _color_attribute(attrib, point)
60 | return bake_point2d(point.geometry, z, attributes=attrib)
61 |
62 |
63 | def bake_display_ray2d(ray, z=0, layer_name=None, attributes=None):
64 | """Add DisplayRay2D to the Rhino scene as a Line with an Arrowhead."""
65 | attrib = _get_attributes(layer_name, attributes)
66 | _color_attribute(attrib, ray)
67 | return bake_ray2d(ray.geometry, z, attributes=attrib)
68 |
69 |
70 | def bake_display_linesegment2d(line, z=0, layer_name=None, attributes=None):
71 | """Add DisplayLineSegment2D to the Rhino scene as a Line."""
72 | attrib = _get_attributes(layer_name, attributes)
73 | _line_like_attributes(attrib, line)
74 | return bake_linesegment2d(line.geometry, z, attributes=attrib)
75 |
76 |
77 | def bake_display_polygon2d(polygon, z=0, layer_name=None, attributes=None):
78 | """Add DisplayPolygon2D to the Rhino scene as a Polyline."""
79 | attrib = _get_attributes(layer_name, attributes)
80 | _line_like_attributes(attrib, polygon)
81 | return bake_polygon2d(polygon.geometry, z, attributes=attrib)
82 |
83 |
84 | def bake_display_arc2d(arc, z=0, layer_name=None, attributes=None):
85 | """Add DisplayArc2D to the Rhino scene as an Arc or a Circle."""
86 | attrib = _get_attributes(layer_name, attributes)
87 | _line_like_attributes(attrib, arc)
88 | return bake_arc2d(arc.geometry, z, attributes=attrib)
89 |
90 |
91 | def bake_display_polyline2d(polyline, z=0, layer_name=None, attributes=None):
92 | """Add DisplayPolyline2D to the Rhino scene as a Curve."""
93 | attrib = _get_attributes(layer_name, attributes)
94 | _line_like_attributes(attrib, polyline)
95 | return bake_polyline2d(polyline.geometry, z, attributes=attrib)
96 |
97 |
98 | def bake_display_mesh2d(mesh, z=0, layer_name=None, attributes=None):
99 | """Add DisplayMesh2D to the Rhino scene as a Mesh."""
100 | attrib = _get_attributes(layer_name, attributes)
101 | _color_attribute(attrib, mesh)
102 | return bake_mesh2d(mesh.geometry, z, attributes=attrib)
103 |
104 |
105 | """____________BAKE 3D DISPLAY GEOMETRY TO THE RHINO SCENE____________"""
106 |
107 |
108 | def bake_display_vector3d(vector, layer_name=None, attributes=None):
109 | """Add DisplayVector3D to the Rhino scene as a Line with an Arrowhead."""
110 | attrib = _get_attributes(layer_name, attributes)
111 | _color_attribute(attrib, vector)
112 | return bake_vector3d(vector.geometry, attributes=attrib)
113 |
114 |
115 | def bake_display_point3d(point, layer_name=None, attributes=None):
116 | """Add ladybug Point3D to the Rhino scene as a Point."""
117 | attrib = _get_attributes(layer_name, attributes)
118 | _color_attribute(attrib, point)
119 | return bake_point3d(point.geometry, attributes=attrib)
120 |
121 |
122 | def bake_display_ray3d(ray, layer_name=None, attributes=None):
123 | """Add DisplayRay3D to the Rhino scene as a Line with an Arrowhead."""
124 | attrib = _get_attributes(layer_name, attributes)
125 | _color_attribute(attrib, ray)
126 | return bake_ray3d(ray.geometry, attributes=attrib)
127 |
128 |
129 | def bake_display_plane(plane, layer_name=None, attributes=None):
130 | """Add DisplayPlane to the Rhino scene as a Rectangle."""
131 | attrib = _get_attributes(layer_name, attributes)
132 | _color_attribute(attrib, plane)
133 | return bake_plane(plane.geometry, attributes=attrib)
134 |
135 |
136 | def bake_display_linesegment3d(line, layer_name=None, attributes=None):
137 | """Add DisplayLineSegment3D to the Rhino scene as a Line."""
138 | attrib = _get_attributes(layer_name, attributes)
139 | _line_like_attributes(attrib, line)
140 | return bake_linesegment3d(line.geometry, attributes=attrib)
141 |
142 |
143 | def bake_display_arc3d(arc, layer_name=None, attributes=None):
144 | """Add DisplayArc3D to the Rhino scene as an Arc or a Circle."""
145 | attrib = _get_attributes(layer_name, attributes)
146 | _line_like_attributes(attrib, arc)
147 | return bake_arc3d(arc.geometry, attributes=attrib)
148 |
149 |
150 | def bake_display_polyline3d(polyline, layer_name=None, attributes=None):
151 | """Add DisplayPolyline3D to the Rhino scene as a Curve."""
152 | attrib = _get_attributes(layer_name, attributes)
153 | _line_like_attributes(attrib, polyline)
154 | return bake_polyline3d(polyline.geometry, attributes=attrib)
155 |
156 |
157 | def bake_display_mesh3d(mesh, layer_name=None, attributes=None):
158 | """Add DisplayMesh3D to the Rhino scene as a Mesh."""
159 | attrib = _get_attributes(layer_name, attributes)
160 | _color_attribute(attrib, mesh)
161 | return bake_mesh3d(mesh.geometry, attributes=attrib)
162 |
163 |
164 | def bake_display_face3d(face, layer_name=None, attributes=None):
165 | """Add DisplayFace3D to the Rhino scene as a Brep."""
166 | attrib = _get_attributes(layer_name, attributes)
167 | _color_attribute(attrib, face)
168 | return bake_face3d(face.geometry, attributes=attrib)
169 |
170 |
171 | def bake_display_polyface3d(polyface, layer_name=None, attributes=None):
172 | """Add DisplayPolyface3D to the Rhino scene as a Brep."""
173 | attrib = _get_attributes(layer_name, attributes)
174 | _color_attribute(attrib, polyface)
175 | return bake_polyface3d(polyface.geometry, attributes=attrib)
176 |
177 |
178 | def bake_display_sphere(sphere, layer_name=None, attributes=None):
179 | """Add DisplaySphere to the Rhino scene as a Brep."""
180 | attrib = _get_attributes(layer_name, attributes)
181 | _color_attribute(attrib, sphere)
182 | return bake_sphere(sphere.geometry, attributes=attrib)
183 |
184 |
185 | def bake_display_cone(cone, layer_name=None, attributes=None):
186 | """Add DisplayCone to the Rhino scene as a Brep."""
187 | attrib = _get_attributes(layer_name, attributes)
188 | _color_attribute(attrib, cone)
189 | return bake_cone(cone.geometry, attributes=attrib)
190 |
191 |
192 | def bake_display_cylinder(cylinder, layer_name=None, attributes=None):
193 | """Add DisplayCylinder to the Rhino scene as a Brep."""
194 | attrib = _get_attributes(layer_name, attributes)
195 | _color_attribute(attrib, cylinder)
196 | return bake_cylinder(cylinder.geometry, attributes=attrib)
197 |
198 |
199 | def bake_display_text3d(display_text, layer_name=None, attributes=None):
200 | """Add DisplayText3D to the Rhino scene.
201 |
202 | Args:
203 | display_text: A DisplayText3D object to be added to the Rhino scene.
204 | layer_name: Optional text string for the layer name on which to place the
205 | text. If None, text will be added to the current layer.
206 | attributes: Optional Rhino attributes for adding to the Rhino scene.
207 | """
208 | # extract properties from the text
209 | doc = rhdoc.ActiveDoc
210 | d_txt = display_text.text
211 | nl_count = len(d_txt.split('\n')) - 1
212 | if nl_count > 1 and display_text.vertical_alignment == 'Bottom':
213 | m_vec = display_text.plane.y * (nl_count * display_text.height * -1.5)
214 | t_pln = display_text.plane.move(m_vec)
215 | else:
216 | t_pln = display_text.plane
217 | txt_h = display_text.height
218 | # create the Rhino Display Text object
219 | txt = rd.Text3d(d_txt, from_plane(t_pln), txt_h)
220 | txt.FontFace = display_text.font
221 | txt.HorizontalAlignment = TEXT_HORIZ[display_text.horizontal_alignment]
222 | txt.VerticalAlignment = TEXT_VERT[display_text.vertical_alignment]
223 | attrib = _get_attributes(layer_name, attributes)
224 | attrib.ObjectColor = color_to_color(display_text.color)
225 | # add the text to the document and return the GUID
226 | doc.ModelSpaceAnnotationScalingEnabled = False # disable model scale
227 | txt_guid = doc.Objects.AddText(txt, attrib)
228 | return txt_guid
229 |
230 |
231 | """________________EXTRA HELPER FUNCTIONS________________"""
232 |
233 |
234 | def _color_attribute(attrib, display_obj):
235 | """Process the attributes of a colored display object."""
236 | attrib.ColorSource = docobj.ObjectColorSource.ColorFromObject
237 | attrib.ObjectColor = argb_color_to_color(display_obj.color)
238 |
239 |
240 | def _line_like_attributes(attrib, display_obj):
241 | """Process the attributes of a line-like display object."""
242 | _color_attribute(attrib, display_obj)
243 | if not isinstance(display_obj.line_width, Default):
244 | attrib.PlotWeightSource = docobj.ObjectPlotWeightSource.PlotWeightFromObject
245 | attrib.PlotWeight = display_obj.line_width / LINE_WIDTH_FACTOR
246 | attrib.LinetypeSource = docobj.ObjectLinetypeSource.LinetypeFromObject
247 | attrib.LinetypeIndex = LINE_TYPES[display_obj.line_type]
248 |
--------------------------------------------------------------------------------
/ladybug_rhino/cli/__init__.py:
--------------------------------------------------------------------------------
1 | """Command Line Interface (CLI) entry point for ladybug rhino.
2 |
3 | Note:
4 |
5 | Do not import this module in your code directly. For running the commands,
6 | execute them from the command line or as a subprocess
7 | (e.g. ``subprocess.call(['ladybug-rhino', 'viz'])``)
8 |
9 | Ladybug rhino is using click (https://click.palletsprojects.com/en/7.x/) for
10 | creating the CLI.
11 | """
12 |
13 | import sys
14 | import os
15 | import json
16 | import logging
17 | import click
18 |
19 | try:
20 | from ladybug.config import folders as lb_folders
21 | except ImportError as e:
22 | raise ImportError("Failed to import ladybug.\n{}".format(e))
23 |
24 | from ladybug_rhino.config import folders
25 | from ladybug_rhino.pythonpath import create_python_package_dir, \
26 | iron_python_search_path, script_editor_search_path
27 | from ladybug_rhino.ghpath import copy_components_packages, \
28 | clean_userobjects, clean_libraries
29 | from ladybug_rhino.resourcepath import setup_resource_folders
30 | from ladybug_rhino.versioning.change import change_installed_version
31 |
32 | _logger = logging.getLogger(__name__)
33 |
34 |
35 | @click.group()
36 | @click.version_option()
37 | def main():
38 | pass
39 |
40 |
41 | @main.command('viz')
42 | def viz():
43 | """Check if ladybug_rhino is flying!"""
44 | click.echo('viiiiiiiiiiiiizzzzzzzzz!')
45 |
46 |
47 | @main.command('setup-user-environment')
48 | @click.option(
49 | '--component-directory', default=None, help='The path to a directory that '
50 | 'contains all of the Ladybug Tools Grasshopper python packages to be copied '
51 | '(both user object packages and dotnet gha packages). If unspecified, this command '
52 | 'will search for the site-packages folder in the ladybug_tools folder. If '
53 | 'they are not found, no user objects will be copied.',
54 | type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True))
55 | @click.option(
56 | '--python-package-dir', help='Path to the directory with the python '
57 | 'packages, which will be added to the search path. If unspecified, this command '
58 | 'will search for the site-packages folder in the ladybug_tools folder.',
59 | type=str, default=None, show_default=True)
60 | @click.option(
61 | '--setup-resources/--overwrite-resources', ' /-o', help='Flag to note '
62 | 'whether the user resources should be overwritten or they should only '
63 | 'be set up if they do not exist, in which case existing resources will '
64 | 'be preserved.', default=True)
65 | def setup_user_environment(component_directory, python_package_dir, setup_resources):
66 | """Set up the entire environment for the current user.
67 |
68 | This includes setting the IronPython search path, copying the components to
69 | the user-specific folders, and creating any user resource folders if they
70 | do not already exist. Note that setting the IronPython search path won't
71 | work well if Rhino is open while running this command.
72 | """
73 | try:
74 | # set the ironpython search path
75 | if python_package_dir is None:
76 | python_package_dir = create_python_package_dir()
77 | if python_package_dir is not None:
78 | click.echo('Setting Rhino IronPython search path ...')
79 | new_settings = iron_python_search_path(python_package_dir, None)
80 | click.echo('Congratulations! Setting the search path in the following '
81 | 'file was successful:\n{}'.format('\n'.join(new_settings)))
82 | try:
83 | pth_files = script_editor_search_path(python_package_dir)
84 | if len(pth_files) != 0:
85 | click.echo(
86 | 'Setting the Rhino ScriptEditor path in the following '
87 | 'locations was successful:\n{}'.format('; '.join(pth_files)))
88 | except Exception:
89 | pass # no need to worry about this part failing
90 | # copy the components if they exist
91 | if component_directory is None:
92 | component_directory = \
93 | os.path.join(lb_folders.ladybug_tools_folder, 'grasshopper')
94 | if os.path.isdir(component_directory):
95 | click.echo('Copying Grasshopper Components ...')
96 | copy_components_packages(component_directory)
97 | click.echo('Congratulations! All component packages are copied!')
98 | # set the user resources
99 | click.echo('Setting user-specific resources ...')
100 | overwrite = not setup_resources
101 | resource_folder = setup_resource_folders(overwrite)
102 | click.echo('Setting up user resources in the following '
103 | 'folder was successful:\n{}'.format(resource_folder))
104 | except Exception as e:
105 | _logger.exception('Setting up the user environment failed.\n{}'.format(e))
106 | sys.exit(1)
107 | else:
108 | sys.exit(0)
109 |
110 |
111 | @main.command('set-python-search')
112 | @click.option('--python-package-dir', help='Path to the directory with the python '
113 | 'packages, which will be added to the search path. If None, this command '
114 | 'will search for the site-packages folder in the ladybug_tools folder',
115 | type=str, default=None, show_default=True)
116 | @click.option('--settings-file', help='Path to the Rhino settings file to which the '
117 | 'python-package-dir will be added. If None, this command will search '
118 | 'the current user folder for all copies of this file for the installed '
119 | 'Rhino versions. ', type=str, default=None, show_default=True)
120 | def set_python_search(python_package_dir, settings_file):
121 | """Set Rhino to search for libraries in a given directory."""
122 | try:
123 | # search for the python package directory if it is not there
124 | if python_package_dir is None:
125 | python_package_dir = create_python_package_dir()
126 |
127 | # set the search path
128 | click.echo('Setting Rhino IronPython search path ...')
129 | new_settings = iron_python_search_path(python_package_dir, settings_file)
130 | click.echo('Congratulations! Setting the search path in the following '
131 | 'file was successful:\n{}'.format('\n'.join(new_settings)))
132 | try:
133 | pth_files = script_editor_search_path(python_package_dir)
134 | if len(pth_files) != 0:
135 | click.echo(
136 | 'Setting the Rhino ScriptEditor path in the following '
137 | 'locations was successful:\n{}'.format('; '.join(pth_files)))
138 | except Exception:
139 | pass # no need to worry about this part failing
140 | except Exception as e:
141 | _logger.exception('Setting IronPython search path failed.\n{}'.format(e))
142 | sys.exit(1)
143 | else:
144 | sys.exit(0)
145 |
146 |
147 | @main.command('copy-gh-components')
148 | @click.argument('component-directory', type=click.Path(
149 | exists=True, file_okay=False, dir_okay=True, resolve_path=True))
150 | def copy_gh_components(component_directory):
151 | """Copy all component packages to the UserObjects and Libraries folder.
152 |
153 | \b
154 | Args:
155 | component_directory: The path to a directory that contains all of the Ladybug
156 | Tools Grasshopper python packages to be copied (both user object
157 | packages and dotnet gha packages).
158 | """
159 | try:
160 | # copy the grasshopper components
161 | click.echo('Copying Grasshopper Components ...')
162 | copy_components_packages(component_directory)
163 | click.echo('Congratulations! All component packages are copied!')
164 | except Exception as e:
165 | _logger.exception('Copying Grasshopper components failed.\n{}'.format(e))
166 | sys.exit(1)
167 | else:
168 | sys.exit(0)
169 |
170 |
171 | @main.command('remove-gh-components')
172 | def remove_gh_components():
173 | """Remove all component packages to the UserObjects and Libraries folder."""
174 | try:
175 | # copy the grasshopper components
176 | click.echo('Removing Grasshopper Components ...')
177 | clean_userobjects()
178 | clean_libraries()
179 | click.echo('Congratulations! All component packages are removed!')
180 | except Exception as e:
181 | _logger.exception('Removing Grasshopper components failed.\n{}'.format(e))
182 | sys.exit(1)
183 | else:
184 | sys.exit(0)
185 |
186 |
187 | @main.command('setup-resources')
188 | @click.option('--setup-only/--overwrite', ' /-o', help='Flag to note '
189 | 'whether the user resources should be overwritten or they should only '
190 | 'be set up if they do not exist, in which case existing resources will '
191 | 'be preserved.', default=True)
192 | def setup_resources(setup_only):
193 | """Set up user resource folders in their respective locations."""
194 | try:
195 | overwrite = not setup_only
196 | # setup the resource folders
197 | click.echo('Setting user-specific resources ...')
198 | resource_folder = setup_resource_folders(overwrite)
199 | click.echo('Setting up user resources in the following '
200 | 'folder was successful:\n{}'.format(resource_folder))
201 | try:
202 | pth_files = script_editor_search_path()
203 | if len(pth_files) != 0:
204 | click.echo(
205 | 'Setting the Rhino ScriptEditor path in the following '
206 | 'locations was successful:\n{}'.format('; '.join(pth_files)))
207 | except Exception:
208 | pass # no need to worry about this part failing
209 | except Exception as e:
210 | _logger.exception('Setting up resource folders failed.\n{}'.format(e))
211 | sys.exit(1)
212 | else:
213 | sys.exit(0)
214 |
215 |
216 | @main.command('change-installed-version')
217 | @click.option('--version', '-v', help=' An optional text string for the version of '
218 | 'the LBT plugin to be installed. The input should contain only integers '
219 | 'separated by two periods (eg. 1.0.0). If unspecified, the Ladybug '
220 | 'Tools plugin shall be updated to the latest available version. The '
221 | 'version specified here does not need to be newer than the current '
222 | 'installation and can be older but grasshopper plugin versions less '
223 | 'than 0.3.0 are not supported.', type=str, default=None)
224 | def run_versioner_process(version):
225 | """Change the currently installed version of Ladybug Tools.
226 |
227 | This requires an internet connection and will update all core libraries and
228 | Grasshopper components to the specified version_to_install.
229 | """
230 | try:
231 | change_installed_version(version)
232 | except Exception as e:
233 | _logger.exception('Changing the installed version failed.\n{}'.format(e))
234 | sys.exit(1)
235 | else:
236 | sys.exit(0)
237 |
238 |
239 | @main.command('config')
240 | @click.option('--output-file', help='Optional file to output the JSON string of '
241 | 'the config object. By default, it will be printed out to stdout',
242 | type=click.File('w'), default='-', show_default=True)
243 | def config_variables(output_file):
244 | """Get a JSON object with all configuration information."""
245 | try:
246 | config_dict = {
247 | 'uo_folder': folders.uo_folder,
248 | 'gha_folder': folders.gha_folder,
249 | 'lbt_grasshopper_version': folders.lbt_grasshopper_version_str
250 | }
251 | output_file.write(json.dumps(config_dict, indent=4))
252 | except Exception as e:
253 | _logger.exception('Failed to retrieve configurations.\n{}'.format(e))
254 | sys.exit(1)
255 | else:
256 | sys.exit(0)
257 |
258 |
259 | if __name__ == "__main__":
260 | main()
261 |
--------------------------------------------------------------------------------
/ladybug_rhino/bakegeometry.py:
--------------------------------------------------------------------------------
1 | """Functions to bake from Ladybug geometries into a Rhino document."""
2 | from .fromgeometry import from_point2d, from_arc2d, from_polyline2d, from_mesh2d, \
3 | from_point3d, from_plane, from_arc3d, from_polyline3d, \
4 | from_mesh3d, from_face3d, from_polyface3d, from_sphere, from_cone, from_cylinder
5 | from .color import color_to_color, gray
6 |
7 | try:
8 | from System.Drawing import Color
9 | except ImportError as e:
10 | raise ImportError("Failed to import Windows/.NET libraries\n{}".format(e))
11 |
12 | try:
13 | import Rhino.Geometry as rg
14 | from Rhino import RhinoMath
15 | import Rhino.DocObjects as docobj
16 | from Rhino import RhinoDoc as rhdoc
17 | except ImportError as e:
18 | raise ImportError("Failed to import Rhino document attributes.\n{}".format(e))
19 |
20 | """____________BAKE 2D GEOMETRY TO THE RHINO SCENE____________"""
21 |
22 |
23 | def bake_vector2d(vector, z=0, layer_name=None, attributes=None):
24 | """Add ladybug Ray2D to the Rhino scene as a Line with an Arrowhead."""
25 | doc = rhdoc.ActiveDoc
26 | seg = (rg.Point3d(0, 0, z), rg.Point3d(vector.x, vector.y, z))
27 | attrib = _get_attributes(layer_name, attributes)
28 | attrib.ObjectDecoration = docobj.ObjectDecoration.EndArrowhead
29 | return doc.Objects.AddLine(seg[0], seg[1], attrib)
30 |
31 |
32 | def bake_point2d(point, z=0, layer_name=None, attributes=None):
33 | """Add ladybug Point2D to the Rhino scene as a Point."""
34 | doc = rhdoc.ActiveDoc
35 | pt = from_point2d(point, z)
36 | return doc.Objects.AddPoint(pt, _get_attributes(layer_name, attributes))
37 |
38 |
39 | def bake_ray2d(ray, z=0, layer_name=None, attributes=None):
40 | """Add ladybug Ray2D to the Rhino scene as a Line with an Arrowhead."""
41 | doc = rhdoc.ActiveDoc
42 | seg = (from_point2d(ray.p, z), from_point2d(ray.p + ray.v, z))
43 | attrib = _get_attributes(layer_name, attributes)
44 | attrib.ObjectDecoration = docobj.ObjectDecoration.EndArrowhead
45 | return doc.Objects.AddLine(seg[0], seg[1], attrib)
46 |
47 |
48 | def bake_linesegment2d(line, z=0, layer_name=None, attributes=None):
49 | """Add ladybug LineSegment2D to the Rhino scene as a Line."""
50 | doc = rhdoc.ActiveDoc
51 | seg = (from_point2d(line.p1, z), from_point2d(line.p2, z))
52 | return doc.Objects.AddLine(seg[0], seg[1], _get_attributes(layer_name, attributes))
53 |
54 |
55 | def bake_polygon2d(polygon, z=0, layer_name=None, attributes=None):
56 | """Add ladybug Polygon2D to the Rhino scene as a Polyline."""
57 | doc = rhdoc.ActiveDoc
58 | pgon = [from_point2d(pt, z) for pt in polygon.vertices] + \
59 | [from_point2d(polygon[0], z)]
60 | return doc.Objects.AddPolyline(pgon, _get_attributes(layer_name, attributes))
61 |
62 |
63 | def bake_arc2d(arc, z=0, layer_name=None, attributes=None):
64 | """Add ladybug Arc2D to the Rhino scene as an Arc or a Circle."""
65 | doc = rhdoc.ActiveDoc
66 | rh_arc = from_arc2d(arc, z)
67 | if arc.is_circle:
68 | return doc.Objects.AddCircle(rh_arc, _get_attributes(layer_name, attributes))
69 | else:
70 | return doc.Objects.AddArc(rh_arc, _get_attributes(layer_name, attributes))
71 |
72 |
73 | def bake_polyline2d(polyline, z=0, layer_name=None, attributes=None):
74 | """Add ladybug Polyline2D to the Rhino scene as a Curve."""
75 | doc = rhdoc.ActiveDoc
76 | rh_crv = from_polyline2d(polyline, z)
77 | return doc.Objects.AddCurve(rh_crv, _get_attributes(layer_name, attributes))
78 |
79 |
80 | def bake_mesh2d(mesh, z=0, layer_name=None, attributes=None):
81 | """Add ladybug Mesh2D to the Rhino scene as a Mesh."""
82 | doc = rhdoc.ActiveDoc
83 | _mesh = from_mesh2d(mesh, z)
84 | return doc.Objects.AddMesh(_mesh, _get_attributes(layer_name, attributes))
85 |
86 |
87 | """____________BAKE 3D GEOMETRY TO THE RHINO SCENE____________"""
88 |
89 |
90 | def bake_vector3d(vector, layer_name=None, attributes=None):
91 | """Add ladybug Ray2D to the Rhino scene as a Line with an Arrowhead."""
92 | doc = rhdoc.ActiveDoc
93 | seg = (rg.Point3d(0, 0, 0), rg.Point3d(vector.x, vector.y, vector.z))
94 | attrib = _get_attributes(layer_name, attributes)
95 | attrib.ObjectDecoration = docobj.ObjectDecoration.EndArrowhead
96 | return doc.Objects.AddLine(seg[0], seg[1], attrib)
97 |
98 |
99 | def bake_point3d(point, layer_name=None, attributes=None):
100 | """Add ladybug Point3D to the Rhino scene as a Point."""
101 | doc = rhdoc.ActiveDoc
102 | pt = from_point3d(point)
103 | return doc.Objects.AddPoint(pt, _get_attributes(layer_name, attributes))
104 |
105 |
106 | def bake_ray3d(ray, layer_name=None, attributes=None):
107 | """Add ladybug Ray2D to the Rhino scene as a Line with an Arrowhead."""
108 | doc = rhdoc.ActiveDoc
109 | seg = (from_point3d(ray.p), from_point3d(ray.p + ray.v))
110 | attrib = _get_attributes(layer_name, attributes)
111 | attrib.ObjectDecoration = docobj.ObjectDecoration.EndArrowhead
112 | return doc.Objects.AddLine(seg[0], seg[1], attrib)
113 |
114 |
115 | def bake_plane(plane, layer_name=None, attributes=None):
116 | """Add ladybug Plane to the Rhino scene as a Rectangle."""
117 | doc = rhdoc.ActiveDoc
118 | rh_pln = from_plane(plane)
119 | r = 10 # default radius for a plane object in rhino model units
120 | interval = rg.Interval(-r / 2, r / 2)
121 | rect = rg.Rectangle3d(rh_pln, interval, interval)
122 | return doc.Objects.AddRectangle(rect, _get_attributes(layer_name, attributes))
123 |
124 |
125 | def bake_linesegment3d(line, layer_name=None, attributes=None):
126 | """Add ladybug LineSegment3D to the Rhino scene as a Line."""
127 | doc = rhdoc.ActiveDoc
128 | seg = (from_point3d(line.p1), from_point3d(line.p2))
129 | return doc.Objects.AddLine(seg[0], seg[1], _get_attributes(layer_name, attributes))
130 |
131 |
132 | def bake_arc3d(arc, layer_name=None, attributes=None):
133 | """Add ladybug Arc3D to the Rhino scene as an Arc or Circle."""
134 | doc = rhdoc.ActiveDoc
135 | rh_arc = from_arc3d(arc)
136 | if arc.is_circle:
137 | return doc.Objects.AddCircle(rh_arc, _get_attributes(layer_name, attributes))
138 | else:
139 | return doc.Objects.AddArc(rh_arc, _get_attributes(layer_name, attributes))
140 |
141 |
142 | def bake_polyline3d(polyline, layer_name=None, attributes=None):
143 | """Add ladybug Polyline3D to the Rhino scene as a Curve."""
144 | doc = rhdoc.ActiveDoc
145 | rh_crv = from_polyline3d(polyline)
146 | return doc.Objects.AddCurve(rh_crv, _get_attributes(layer_name, attributes))
147 |
148 |
149 | def bake_mesh3d(mesh, layer_name=None, attributes=None):
150 | """Add ladybug Mesh3D to the Rhino scene as a Mesh."""
151 | doc = rhdoc.ActiveDoc
152 | _mesh = from_mesh3d(mesh)
153 | return doc.Objects.AddMesh(_mesh, _get_attributes(layer_name, attributes))
154 |
155 |
156 | def bake_face3d(face, layer_name=None, attributes=None):
157 | """Add ladybug Face3D to the Rhino scene as a Brep."""
158 | doc = rhdoc.ActiveDoc
159 | _face = from_face3d(face)
160 | return doc.Objects.AddBrep(_face, _get_attributes(layer_name, attributes))
161 |
162 |
163 | def bake_polyface3d(polyface, layer_name=None, attributes=None):
164 | """Add ladybug Polyface3D to the Rhino scene as a Brep."""
165 | doc = rhdoc.ActiveDoc
166 | rh_polyface = from_polyface3d(polyface)
167 | return doc.Objects.AddBrep(rh_polyface, _get_attributes(layer_name, attributes))
168 |
169 |
170 | def bake_sphere(sphere, layer_name=None, attributes=None):
171 | """Add ladybug Sphere to the Rhino scene as a Brep."""
172 | doc = rhdoc.ActiveDoc
173 | rh_sphere = from_sphere(sphere).ToBrep()
174 | return doc.Objects.AddBrep(rh_sphere, _get_attributes(layer_name, attributes))
175 |
176 |
177 | def bake_cone(cone, layer_name=None, attributes=None):
178 | """Add ladybug Cone to the Rhino scene as a Brep."""
179 | doc = rhdoc.ActiveDoc
180 | rh_cone = from_cone(cone).ToBrep()
181 | return doc.Objects.AddBrep(rh_cone, _get_attributes(layer_name, attributes))
182 |
183 |
184 | def bake_cylinder(cylinder, layer_name=None, attributes=None):
185 | """Add ladybug Cylinder to the Rhino scene as a Brep."""
186 | doc = rhdoc.ActiveDoc
187 | rh_cylinder = from_cylinder(cylinder).ToBrep()
188 | return doc.Objects.AddBrep(rh_cylinder, _get_attributes(layer_name, attributes))
189 |
190 |
191 | """________ADDITIONAL 3D GEOMETRY TRANSLATORS________"""
192 |
193 |
194 | def bake_mesh3d_as_hatch(mesh, layer_name=None, attributes=None):
195 | """Add ladybug Mesh3D to the Rhino scene as a colored hatch."""
196 | doc = rhdoc.ActiveDoc
197 | # get a list of colors that align with the mesh faces
198 | if mesh.colors is not None:
199 | if mesh.is_color_by_face:
200 | colors = [color_to_color(col) for col in mesh.colors]
201 | else: # compute the average color across the vertices
202 | colors, v_cols = [], mesh.colors
203 | for face in mesh.faces:
204 | red = int(sum(v_cols[f].r for f in face) / len(face))
205 | green = int(sum(v_cols[f].g for f in face) / len(face))
206 | blue = int(sum(v_cols[f].b for f in face) / len(face))
207 | colors.append(Color.FromArgb(255, red, green, blue))
208 | else:
209 | colors = [gray()] * len(mesh.faces)
210 |
211 | # create hatches from each of the faces and get an aligned list of colors
212 | hatches, hatch_colors = [], []
213 | vertices = mesh.vertices
214 | for face, color in zip(mesh.faces, colors):
215 | f_verts = [from_point3d(vertices[f]) for f in face]
216 | f_verts.append(f_verts[0])
217 | p_line = rg.PolylineCurve(f_verts)
218 | if p_line.IsPlanar():
219 | hatches.append(rg.Hatch.Create(p_line, 0, 0, 0)[0])
220 | hatch_colors.append(color)
221 | elif len(face) == 4:
222 | p_line_1 = rg.PolylineCurve(f_verts[:3] + [f_verts[0]])
223 | p_line_2 = rg.PolylineCurve(f_verts[-3:] + [f_verts[-3]])
224 | hatches.append(rg.Hatch.Create(p_line_1, 0, 0, 0)[0])
225 | hatches.append(rg.Hatch.Create(p_line_2, 0, 0, 0)[0])
226 | hatch_colors.extend((color, color))
227 |
228 | # bake the hatches into the scene
229 | guids = []
230 | for hatch, color in zip(hatches, hatch_colors):
231 | attribs = _get_attributes(layer_name, attributes)
232 | attribs.ColorSource = docobj.ObjectColorSource.ColorFromObject
233 | attribs.ObjectColor = color
234 | guids.append(doc.Objects.AddHatch(hatch, attribs))
235 |
236 | # group the hatches so that they are easy to handle in the Rhino scene
237 | group_t = doc.Groups
238 | docobj.Tables.GroupTable.Add(group_t, guids)
239 | return guids
240 |
241 |
242 | """________________EXTRA HELPER FUNCTIONS________________"""
243 |
244 |
245 | def _get_attributes(layer_name=None, attributes=None):
246 | """Get Rhino object attributes."""
247 | doc = rhdoc.ActiveDoc
248 | attributes = doc.CreateDefaultAttributes() if attributes is None else attributes
249 | if layer_name is None:
250 | return attributes
251 | elif isinstance(layer_name, int):
252 | attributes.LayerIndex = layer_name
253 | elif layer_name is not None:
254 | attributes.LayerIndex = _get_layer(layer_name)
255 | return attributes
256 |
257 |
258 | def _get_layer(layer_name):
259 | """Get a layer index from the Rhino document from the layer name."""
260 | doc = rhdoc.ActiveDoc
261 | layer_table = doc.Layers # layer table
262 | layer_index = layer_table.FindByFullPath(layer_name, RhinoMath.UnsetIntIndex)
263 | if layer_index == RhinoMath.UnsetIntIndex:
264 | all_layers = layer_name.split('::')
265 | parent_name = all_layers[0]
266 | layer_index = layer_table.FindByFullPath(parent_name, RhinoMath.UnsetIntIndex)
267 | if layer_index == RhinoMath.UnsetIntIndex:
268 | parent_layer = docobj.Layer()
269 | parent_layer.Name = parent_name
270 | layer_index = layer_table.Add(parent_layer)
271 | for lay in all_layers[1:]:
272 | parent_name = '{}::{}'.format(parent_name, lay)
273 | parent_index = layer_index
274 | layer_index = layer_table.FindByFullPath(
275 | parent_name, RhinoMath.UnsetIntIndex)
276 | if layer_index == RhinoMath.UnsetIntIndex:
277 | parent_layer = docobj.Layer()
278 | parent_layer.Name = lay
279 | parent_layer.ParentLayerId = layer_table[parent_index].Id
280 | layer_index = layer_table.Add(parent_layer)
281 |
282 | return layer_index
283 |
--------------------------------------------------------------------------------
/ladybug_rhino/versioning/diff.py:
--------------------------------------------------------------------------------
1 | """Functions for managing components differences and syncing component versions."""
2 | import os
3 |
4 | try:
5 | import System.Drawing
6 | except ImportError:
7 | raise ImportError("Failed to import System.")
8 |
9 | try:
10 | import Grasshopper.Kernel as gh
11 | except ImportError:
12 | raise ImportError("Failed to import Grasshopper.")
13 |
14 | from ..grasshopper import give_warning
15 | from .userobject import UO_FOLDER, FOLDER_MAP
16 |
17 |
18 | # list of valid change tags for export
19 | CHANGE_TAGS = ('fix', 'release', 'feat', 'perf', 'docs', 'ignore')
20 |
21 |
22 | def validate_change_type(change_type):
23 | """Check that a change type is a valid tag."""
24 | assert change_type in CHANGE_TAGS, 'Invalid change_type "{}". Choose from ' \
25 | 'the following:\n{}'.format(change_type, ' '.join(CHANGE_TAGS))
26 | return change_type
27 |
28 |
29 | def current_userobject_version(component):
30 | """Get the current installed version of a component.
31 |
32 | Args:
33 | component: A Grasshopper Python component with the same name as an
34 | installed user object. If no matching user object is found, this
35 | method returns None.
36 | """
37 | # load component from the folder where it should be
38 | assert component.Category in FOLDER_MAP, \
39 | 'Unknown category: %s. Add category to folder_dict.' % component.Category
40 | fp = os.path.join(UO_FOLDER, FOLDER_MAP[component.Category], 'user_objects',
41 | '%s.ghuser' % component.Name)
42 |
43 | if os.path.isfile(fp): # if the component was found, parse out the version
44 | uo = gh.GH_UserObject(fp).InstantiateObject()
45 | # uo.Message returns None so we have to find it the old school way!
46 | for lc, line in enumerate(uo.Code.split('\n')):
47 | if lc > 200: # this should never happen for valid userobjects
48 | raise ValueError(
49 | 'Failed to find version from UserObject for %s' % uo.Name
50 | )
51 | if line.strip().startswith("ghenv.Component.Message"):
52 | return line.split("=")[1].strip()[1:-1]
53 | else: # there is no currently installed version of this userobject
54 | return None
55 |
56 |
57 | def parse_version(semver_str):
58 | """Parse semantic version string into (major, minor, patch) tuple.
59 |
60 | Args:
61 | semver_str: Text for a component version (eg. "1.0.1").
62 | """
63 | try:
64 | major, minor, patch = [int(d) for d in semver_str.strip().split('.')]
65 | except ValueError:
66 | raise ValueError(
67 | '\nInvalid version format: %s\nYou must follow major.minor.patch format '
68 | 'with 3 integer values' % semver_str
69 | )
70 | return major, minor, patch
71 |
72 |
73 | def validate_version(current_version, new_version, change_type):
74 | """Validate that a version tag conforms to currently-installed version difference.
75 |
76 | Args:
77 | current_version: Semantic version string for the currently installed version.
78 | new_version: Semantic version string for the new component version.
79 | change_type: Text tag for the change type (eg. "fix").
80 | """
81 | if current_version is None:
82 | # this is the first time that this component is created; give it a pass
83 | print(' New component. No change in version: %s' % current_version)
84 | return True
85 |
86 | x, y, z = parse_version(current_version)
87 | parse_version(new_version) # just make sure the semantic version format is valid
88 |
89 | msg = '\nFor a \'%s\' the component version should change to %s not %s.' \
90 | '\nFix the version or select the correct change type and try again.'
91 | if change_type == 'ignore':
92 | valid_version = new_version
93 | elif change_type == 'fix':
94 | valid_version = '.'.join(str(i) for i in (x, y, z + 1))
95 | elif change_type == 'feat' or change_type == 'perf':
96 | valid_version = '.'.join(str(i) for i in (x, y + 1, 0))
97 | elif change_type == 'release':
98 | valid_version = '.'.join(str(i) for i in (x + 1, 0, 0))
99 | elif change_type == 'docs':
100 | valid_version = '.'.join(str(i) for i in (x, y, z))
101 | else:
102 | raise ValueError('Invalid change_type: %s' % change_type)
103 |
104 | assert valid_version == new_version, msg % (change_type, valid_version, new_version)
105 |
106 | if current_version == new_version:
107 | print(' No change in version: %s' % current_version)
108 | else:
109 | print(' Version is changed from %s to %s.' % (current_version, new_version))
110 |
111 |
112 | def has_version_changed(user_object, component):
113 | """Check if the version of a component has changed from a corresponding user object.
114 |
115 | Note that this method only works for user objects that have been dropped on the
116 | canvas. For comparing the version with a user object that hasn't been loaded from
117 | the component server by dropping it on the canvas, the current_userobject_version
118 | method should be used.
119 |
120 | Args:
121 | user_object: A Grasshopper user object component instance.
122 | component: The Grasshopper component object for which the version is
123 | being checked.
124 | """
125 | return not user_object.Message == component.Message
126 |
127 |
128 | def update_port(p1, p2):
129 | """Update one port based on another.
130 |
131 | Args:
132 | p1: The first port object, which is the one used for updating.
133 | p2: The second port object, which will be updated base on p1.
134 | """
135 | if hasattr(p1, 'TypeHint'): # input
136 | if p1.Name != p2.Name:
137 | p2.NickName = p1.NickName
138 | p2.Name = p1.Name
139 | if p1.TypeHint.TypeName != p2.TypeHint.TypeName:
140 | p2.TypeHint = p1.TypeHint
141 | if str(p1.Access) != str(p2.Access):
142 | p2.Access = p1.Access
143 | return True
144 | else: # output
145 | if p1.Name != p2.Name:
146 | p2.NickName = p1.NickName
147 | p2.Name = p1.Name
148 | return True
149 |
150 |
151 | def update_ports(c1, c2):
152 | """Update all of the ports of one component based on another.
153 |
154 | Args:
155 | c1: The first component object, which is the one used for updating.
156 | c2: The second component object, which will be updated base on c1.
157 | """
158 | for i in range(c1.Params.Input.Count):
159 | if not update_port(c1.Params.Input[i], c2.Params.Input[i]):
160 | return True
161 |
162 | for i in range(c1.Params.Output.Count):
163 | if not update_port(c1.Params.Output[i], c2.Params.Output[i]):
164 | return True
165 |
166 | return False
167 |
168 |
169 | def input_output_changed(user_object, component):
170 | """Check if any of inputs or outputs have changed between two components.
171 |
172 | Args:
173 | user_object: A Grasshopper user object component instance.
174 | component: The Grasshopper component object for which the version is
175 | being checked.
176 | """
177 | if user_object.Params.Input.Count != component.Params.Input.Count:
178 | return True
179 | elif user_object.Params.Output.Count != component.Params.Output.Count:
180 | return True
181 |
182 | return update_ports(user_object, component)
183 |
184 |
185 | def insert_new_user_object(user_object, component, doc):
186 | """Insert a new user object next to an existing component in the Grasshopper doc.
187 |
188 | Args:
189 | user_object: A Grasshopper user object component instance.
190 | component: The outdated component where the userobject will be inserted
191 | next to.
192 | doc: The Grasshopper document object.
193 | """
194 | # use component to find the location
195 | x = component.Attributes.Pivot.X + 30
196 | y = component.Attributes.Pivot.Y - 20
197 |
198 | # insert the new one
199 | user_object.Attributes.Pivot = System.Drawing.PointF(x, y)
200 | doc.AddObject(user_object, False, 0)
201 |
202 |
203 | def mark_component(doc, component, note=None):
204 | """Put a circular red group around a component and label it with a note.
205 |
206 | Args:
207 | doc: The Grasshopper document object.
208 | component: A Grasshopper component object on the canvas to be circled.
209 | note: Text for the message to be displayed on the circle. If None, a
210 | default message about input/output change wil be used.
211 | """
212 | note = note or 'There is a change in the input or output! ' \
213 | 'Replace this component manually.'
214 | grp = gh.Special.GH_Group()
215 | grp.CreateAttributes()
216 | grp.Border = gh.Special.GH_GroupBorder.Blob
217 | grp.AddObject(component.InstanceGuid)
218 | grp.Colour = System.Drawing.Color.IndianRed # way to pick a racist color name, .NET
219 | grp.NickName = note
220 | doc.AddObject(grp, False)
221 | return True
222 |
223 |
224 | def sync_component(component, syncing_component):
225 | """Sync a component on the canvas with its corresponding installed version.
226 |
227 | This includes identifying if the component by name in the user object folder,
228 | injecting the code from that user object into the component, and (if the
229 | component inputs or outputs have changed) circling the component in red +
230 | dropping the new user object next to the component.
231 |
232 | Args:
233 | component: A Grasshopper component object on the canvas to be synced.
234 | syncing_component: An object for the component that is doing the syncing.
235 | This will be used to give warnings and access the Grasshopper doc.
236 | Typically, this can be accessed through the ghenv.Component call.
237 | """
238 | # identify the correct user object sub-folder to which the component belongs
239 | ghuser_file = '%s.ghuser' % component.Name
240 | if str(component.Name).startswith(('LB', 'HB', 'DF')):
241 | fp = os.path.join(UO_FOLDER, FOLDER_MAP[component.Category],
242 | 'user_objects', ghuser_file)
243 | elif str(component.Name).startswith(('Ladybug', 'Honeybee', 'HoneybeePlus')):
244 | category = str(component.Name).split('_')[0]
245 | fp = os.path.join(UO_FOLDER, category, ghuser_file)
246 | else: # unidentified plugin; see if we can find it in the root
247 | fp = os.path.join(UO_FOLDER, ghuser_file)
248 |
249 | # check to see if the user object is installed
250 | if not os.path.isfile(fp):
251 | # check if it's a component with a name change
252 | alt_fp = fp.replace('Vizualize', 'Visualize')
253 | alt_fp = fp.replace('Mofidier', 'Modifier')
254 | alt_fp = fp.replace('Abolute', 'Absolute')
255 | alt_fp = alt_fp.replace('gbXML', 'gbXML OSM IDF')
256 | if os.path.isfile(alt_fp):
257 | fp = alt_fp
258 | elif component.Category in FOLDER_MAP: # see if category has a folder
259 | fp = os.path.join(UO_FOLDER, FOLDER_MAP[component.Category], ghuser_file)
260 | if not os.path.isfile(fp): # see if the component is in the root
261 | fp = os.path.join(UO_FOLDER, ghuser_file)
262 | if not os.path.isfile(fp): # all hope is lost; component not installed
263 | warning = 'Failed to find the userobject for %s' % component.Name
264 | give_warning(syncing_component, warning)
265 | return False
266 |
267 | # the the instance of the user object from the file
268 | uo = gh.GH_UserObject(fp).InstantiateObject()
269 |
270 | # check to see if the version of the userobject has changed
271 | if not has_version_changed(uo, component):
272 | return False
273 |
274 | # the version has changed, let's update the code
275 | component.Code = uo.Code
276 | doc = syncing_component.OnPingDocument() # get the Grasshopper document
277 |
278 | # define the callback function and update the solution
279 | def call_back(document):
280 | component.ExpireSolution(False)
281 |
282 | doc.ScheduleSolution(2, gh.GH_Document.GH_ScheduleDelegate(call_back))
283 |
284 | # check if the inputs or outputs have changed
285 | if input_output_changed(uo, component):
286 | insert_new_user_object(uo, component, doc)
287 | mark_component(doc, component) # mark component with a warning to the user
288 | return 'Cannot update %s. Replace manually.' % component.Name
289 |
290 | return 'Updated %s' % component.Name
291 |
--------------------------------------------------------------------------------
/ladybug_rhino/bakeobjects.py:
--------------------------------------------------------------------------------
1 | """Functions to bake entire Ladybug objects into the Rhino scene.
2 |
3 | The methods here are intended to help translate groups of geometry that are
4 | commonly generated by several objects in Ladybug core (ie. legends, compasses,
5 | visualization sets, etc.)
6 | """
7 | import json
8 |
9 | from ladybug_geometry.geometry3d import Mesh3D
10 | from ladybug.graphic import GraphicContainer
11 | from ladybug_display.geometry3d import DisplayText3D
12 | from ladybug_display.visualization import AnalysisGeometry
13 |
14 | from .config import units_system
15 | from .color import color_to_color
16 | from .bakegeometry import bake_point2d, bake_vector2d, bake_ray2d, \
17 | bake_linesegment2d, bake_arc2d, bake_polygon2d, bake_polyline2d, bake_mesh2d, \
18 | bake_point3d, bake_vector3d, bake_ray3d, bake_linesegment3d, bake_plane, \
19 | bake_arc3d, bake_polyline3d, bake_mesh3d, bake_face3d, bake_polyface3d, \
20 | bake_sphere, bake_cone, bake_cylinder, _get_layer, _get_attributes
21 | from .bakedisplay import bake_display_point2d, bake_display_vector2d, \
22 | bake_display_ray2d, bake_display_linesegment2d, bake_display_arc2d, \
23 | bake_display_polygon2d, bake_display_polyline2d, bake_display_mesh2d, \
24 | bake_display_text3d, bake_display_vector3d, bake_display_point3d, \
25 | bake_display_ray3d, bake_display_linesegment3d, bake_display_arc3d, \
26 | bake_display_polyline3d, bake_display_plane, bake_display_mesh3d, \
27 | bake_display_face3d, bake_display_polyface3d, bake_display_sphere, \
28 | bake_display_cone, bake_display_cylinder
29 |
30 | try:
31 | import System
32 | except ImportError as e:
33 | raise ImportError("Failed to import Windows/.NET libraries\n{}".format(e))
34 |
35 | try:
36 | import Rhino.DocObjects as docobj
37 | from Rhino import RhinoDoc as rhdoc
38 | except ImportError as e:
39 | raise ImportError("Failed to import Rhino document attributes.\n{}".format(e))
40 |
41 | BAKE_MAPPER = {
42 | 'Vector2D': bake_vector2d,
43 | 'Point2D': bake_point2d,
44 | 'Ray2D': bake_ray2d,
45 | 'LineSegment2D': bake_linesegment2d,
46 | 'Arc2D': bake_arc2d,
47 | 'Polyline2D': bake_polyline2d,
48 | 'Polygon2D': bake_polygon2d,
49 | 'Mesh2D': bake_mesh2d,
50 | 'Vector3D': bake_vector3d,
51 | 'Point3D': bake_point3d,
52 | 'Ray3D': bake_ray3d,
53 | 'LineSegment3D': bake_linesegment3d,
54 | 'Arc3D': bake_arc3d,
55 | 'Polyline3D': bake_polyline3d,
56 | 'Plane': bake_plane,
57 | 'Mesh3D': bake_mesh3d,
58 | 'Face3D': bake_face3d,
59 | 'Polyface3D': bake_polyface3d,
60 | 'Sphere': bake_sphere,
61 | 'Cone': bake_cone,
62 | 'Cylinder': bake_cylinder,
63 | 'DisplayVector2D': bake_display_vector2d,
64 | 'DisplayPoint2D': bake_display_point2d,
65 | 'DisplayRay2D': bake_display_ray2d,
66 | 'DisplayLineSegment2D': bake_display_linesegment2d,
67 | 'DisplayPolyline2D': bake_display_polyline2d,
68 | 'DisplayArc2D': bake_display_arc2d,
69 | 'DisplayPolygon2D': bake_display_polygon2d,
70 | 'DisplayMesh2D': bake_display_mesh2d,
71 | 'DisplayVector3D': bake_display_vector3d,
72 | 'DisplayPoint3D': bake_display_point3d,
73 | 'DisplayRay3D': bake_display_ray3d,
74 | 'DisplayPlane': bake_display_plane,
75 | 'DisplayLineSegment3D': bake_display_linesegment3d,
76 | 'DisplayPolyline3D': bake_display_polyline3d,
77 | 'DisplayArc3D': bake_display_arc3d,
78 | 'DisplayFace3D': bake_display_face3d,
79 | 'DisplayMesh3D': bake_display_mesh3d,
80 | 'DisplayPolyface3D': bake_display_polyface3d,
81 | 'DisplaySphere': bake_display_sphere,
82 | 'DisplayCone': bake_display_cone,
83 | 'DisplayCylinder': bake_display_cylinder,
84 | 'DisplayText3D': bake_display_text3d
85 | }
86 |
87 |
88 | def bake_legend(legend, layer_name=None):
89 | """Add a Ladybug Legend object to the Rhino scene.
90 |
91 | Args:
92 | legend: A Ladybug Legend object to be added to the Rhino scene.
93 | layer_name: Optional text string for the layer name on which to place the
94 | legend. If None, text will be added to the current layer.
95 |
96 | Returns:
97 | A list of IDs that point to the objects in the Rhino scene in the following
98 | order:
99 |
100 | - legend_mesh -- A colored mesh for the legend.
101 |
102 | - legend_title -- A text object for the legend title.
103 |
104 | - legend_text -- Text objects for the rest of the legend text.
105 | """
106 | # bake the legend mesh
107 | legend_mesh = bake_mesh3d(legend.segment_mesh, layer_name)
108 | # translate the legend text
109 | _height = legend.legend_parameters.text_height
110 | _font = legend.legend_parameters.font
111 | if legend.legend_parameters.continuous_legend is False:
112 | legend_text = [
113 | DisplayText3D(txt, loc, _height, None, _font, 'Left', 'Bottom')
114 | for txt, loc in zip(legend.segment_text, legend.segment_text_location)]
115 | elif legend.legend_parameters.vertical is True:
116 | legend_text = [
117 | DisplayText3D(txt, loc, _height, None, _font, 'Left', 'Center')
118 | for txt, loc in zip(legend.segment_text, legend.segment_text_location)]
119 | else:
120 | legend_text = [
121 | DisplayText3D(txt, loc, _height, None, _font, 'Center', 'Bottom')
122 | for txt, loc in zip(legend.segment_text, legend.segment_text_location)]
123 | legend_title = DisplayText3D(
124 | legend.title, legend.title_location, _height, None, _font)
125 | legend_text.insert(0, legend_title)
126 | # bake the text objects
127 | legend_text_guids = []
128 | for txt_obj in legend_text:
129 | legend_text_guids.append(bake_display_text3d(txt_obj, layer_name))
130 | return [legend_mesh] + legend_text_guids
131 |
132 |
133 | def bake_analysis(analysis, layer_name=None, bake_3d_legend=False,
134 | min_point=None, max_point=None):
135 | """Add a Ladybug Display AnalysisGeometry object to the Rhino scene.
136 |
137 | Args:
138 | analysis: A Ladybug Display AnalysisGeometry object to be added to
139 | the Rhino scene.
140 | layer_name: Optional text string for the parent layer name on which to
141 | place the AnalysisGeometry. The actual layer of the context will
142 | always have a name that aligns with the AnalysisGeometry.display_name.
143 | bake_3d_legend: A Boolean to note whether the AnalysisGeometry should
144 | be baked with 3D legends for any AnalysisGeometries it
145 | includes. (Default: False).
146 | min_point: An optional Point3D to override the default min point
147 | that are used to generate the legend. (Default: None).
148 | max_point: An optional Point3D to override the default max point
149 | that are used to generate the legend. (Default: None).
150 |
151 | Returns:
152 | A list of IDs that point to the objects in the Rhino scene.
153 | """
154 | doc = rhdoc.ActiveDoc
155 | # get attributes corresponding to the layer
156 | layer_name = analysis.display_name if layer_name is None else \
157 | '{}::{}'.format(layer_name, analysis.display_name)
158 | min_pt = analysis.min_point if min_point is None else min_point
159 | max_pt = analysis.max_point if max_point is None else max_point
160 | # generate the colors that correspond to the values
161 | obj_ids = []
162 | for i, data in enumerate(analysis.data_sets):
163 | # get properties used for all analysis geometries
164 | objs_to_group = []
165 | graphic = GraphicContainer(
166 | data.values, min_pt, max_pt,
167 | data.legend_parameters, data.data_type, data.unit)
168 | colors = graphic.value_colors
169 | sub_layer_name = layer_name if data.data_type is None else \
170 | '{}::{}'.format(layer_name, data.data_type.name)
171 | layer_index = _get_layer(sub_layer_name)
172 | # translate the analysis geometry using the matching method
173 | if analysis.matching_method == 'faces':
174 | c_count = 0
175 | for mesh in analysis.geometry:
176 | mesh.colors = colors[c_count:c_count + len(mesh.faces)]
177 | c_count += len(mesh.faces)
178 | bake_func = bake_mesh3d if isinstance(mesh, Mesh3D) else bake_mesh2d
179 | objs_to_group.append(bake_func(mesh, layer_name=layer_index))
180 | elif analysis.matching_method == 'vertices':
181 | c_count = 0
182 | for mesh in analysis.geometry:
183 | mesh.colors = colors[c_count:c_count + len(mesh.vertices)]
184 | c_count += len(mesh.vertices)
185 | bake_func = bake_mesh3d if isinstance(mesh, Mesh3D) else bake_mesh2d
186 | objs_to_group.append(bake_func(mesh, layer_name=layer_index))
187 | else: # one color per geometry object
188 | bake_func = BAKE_MAPPER[analysis.geometry[0].__class__.__name__]
189 | for geo_obj, col in zip(analysis.geometry, colors):
190 | attrib = _get_attributes(layer_index)
191 | attrib.ColorSource = docobj.ObjectColorSource.ColorFromObject
192 | attrib.ObjectColor = color_to_color(col)
193 | objs_to_group.append(bake_func(geo_obj, attributes=attrib))
194 | # group the objects, and add JSON of values to layer user data
195 | group_table = doc.Groups # group table
196 | group_table.Add(sub_layer_name, objs_to_group)
197 | layer_table = doc.Layers # layer table
198 | layer_obj = layer_table[layer_index]
199 | layer_obj.UserDictionary.Set('vis_data', json.dumps(data.to_dict()))
200 | layer_obj.UserDictionary.Set('guids', System.Array[System.Guid](objs_to_group))
201 | if i != analysis.active_data: # hide the inactive data layer
202 | layer_obj.IsVisible = False
203 | # add geometry to the global list and bake the legend if requested
204 | obj_ids.extend(objs_to_group)
205 | if bake_3d_legend:
206 | obj_ids.extend(bake_legend(graphic.legend, layer_index))
207 | # hide the layer if it is hidden
208 | if analysis.hidden:
209 | layer_table = doc.Layers # layer table
210 | layer_index = _get_layer(layer_name)
211 | layer_obj = layer_table[layer_index]
212 | layer_obj.IsVisible = False
213 | return obj_ids
214 |
215 |
216 | def bake_context(context, layer_name=None):
217 | """Add a Ladybug Display ContextGeometry object to the Rhino scene.
218 |
219 | Args:
220 | context: A Ladybug Display ContextGeometry object to be added to
221 | the Rhino scene.
222 | layer_name: Optional text string for the parent layer name on which to
223 | place the ContextGeometry. The actual layer of the context will
224 | always have a name that aligns with the ContextGeometry.display_name.
225 |
226 | Returns:
227 | A list of IDs that point to the objects in the Rhino scene.
228 | """
229 | doc = rhdoc.ActiveDoc
230 | # get attributes corresponding to the layer
231 | layer_name = context.display_name if layer_name is None else \
232 | '{}::{}'.format(layer_name, context.display_name)
233 | layer_index = _get_layer(layer_name)
234 | # hide the layer if it is hidden
235 | if context.hidden:
236 | layer_table = doc.Layers # layer table
237 | layer_obj = layer_table[layer_index]
238 | layer_obj.IsVisible = False
239 | # loop through the objects and add them to the scene
240 | obj_ids = []
241 | for geo_obj in context.geometry:
242 | bake_func = BAKE_MAPPER[geo_obj.__class__.__name__]
243 | obj_ids.append(bake_func(geo_obj, layer_index))
244 | return obj_ids
245 |
246 |
247 | def bake_visualization_set(vis_set, bake_3d_legend=False):
248 | """Add a Ladybug Display VisualizationSet object to the Rhino scene.
249 |
250 | Args:
251 | context_geometry: A Ladybug VisualizationSet object to be added to
252 | the Rhino scene.
253 | bake_3d_legend: A Boolean to note whether the VisualizationSet should
254 | be baked with 3D legends for any AnalysisGeometries it
255 | includes. (Default: False).
256 |
257 | Returns:
258 | A list of IDs that point to the objects in the Rhino scene.
259 | """
260 | # convert the visualization set to model units if necessary
261 | units_sys = units_system()
262 | if vis_set.units is not None and units_sys is not None \
263 | and vis_set.units != units_sys:
264 | vis_set.convert_to_units(units_sys)
265 | # bake all of the geometries
266 | obj_ids = []
267 | for geo in vis_set.geometry:
268 | if isinstance(geo, AnalysisGeometry):
269 | a_objs = bake_analysis(
270 | geo, vis_set.display_name, bake_3d_legend,
271 | vis_set.min_point, vis_set.max_point)
272 | obj_ids.extend(a_objs)
273 | else: # translate it as ContextGeometry
274 | obj_ids.extend(bake_context(geo, vis_set.display_name))
275 | return obj_ids
276 |
--------------------------------------------------------------------------------
/ladybug_rhino/fromgeometry.py:
--------------------------------------------------------------------------------
1 | """Functions to translate from Ladybug geometries to Rhino geometries."""
2 | from __future__ import division
3 |
4 | from .config import tolerance
5 | from .color import color_to_color, gray
6 |
7 | try:
8 | import Rhino.Geometry as rg
9 | except ImportError as e:
10 | raise ImportError('Failed to import Rhino Geometry.\n{}'.format(e))
11 |
12 | try:
13 | from ladybug_geometry.geometry2d import LineSegment2D, Polygon2D
14 | except ImportError as e:
15 | raise ImportError('Failed to import ladybug_geometry.\n{}'.format(e))
16 |
17 |
18 | """____________2D GEOMETRY TRANSLATORS____________"""
19 |
20 |
21 | def from_vector2d(vector):
22 | """Rhino Vector3d from ladybug Vector2D."""
23 | return rg.Vector3d(vector.x, vector.y, 0)
24 |
25 |
26 | def from_point2d(point, z=0):
27 | """Rhino Point3d from ladybug Point2D."""
28 | return rg.Point3d(point.x, point.y, z)
29 |
30 |
31 | def from_ray2d(ray, z=0):
32 | """Rhino Ray3d from ladybug Ray2D."""
33 | return rg.Ray3d(from_point2d(ray.p, z), from_vector2d(ray.v))
34 |
35 |
36 | def from_linesegment2d(line, z=0):
37 | """Rhino LineCurve from ladybug LineSegment2D."""
38 | return rg.LineCurve(from_point2d(line.p1, z), from_point2d(line.p2, z))
39 |
40 |
41 | def from_arc2d(arc, z=0):
42 | """Rhino Arc from ladybug Arc2D."""
43 | if arc.is_circle:
44 | return rg.Circle(from_point2d(arc.c, z), arc.r)
45 | else:
46 | pts = (arc.p1, arc.midpoint, arc.p2)
47 | return rg.Arc(*(from_point2d(pt, z) for pt in pts))
48 |
49 |
50 | def from_polygon2d(polygon, z=0):
51 | """Rhino closed PolyLineCurve from ladybug Polygon2D."""
52 | return rg.PolylineCurve(
53 | [from_point2d(pt, z) for pt in polygon.vertices] + [from_point2d(polygon[0], z)])
54 |
55 |
56 | def from_polyline2d(polyline, z=0):
57 | """Rhino closed PolyLineCurve from ladybug Polyline2D."""
58 | rhino_pts = [from_point2d(pt, z) for pt in polyline.vertices]
59 | if polyline.interpolated:
60 | return rg.Curve.CreateInterpolatedCurve(
61 | rhino_pts, 3, rg.CurveKnotStyle.UniformPeriodic)
62 | else:
63 | return rg.PolylineCurve(rhino_pts)
64 |
65 |
66 | def from_mesh2d(mesh, z=0):
67 | """Rhino Mesh from ladybug Mesh2D."""
68 | pt_function = _get_point2d_function(z)
69 | return _translate_mesh(mesh, pt_function)
70 |
71 |
72 | def _get_point2d_function(z_val):
73 | def point2d_function(pt):
74 | return from_point2d(pt, z_val)
75 | return point2d_function
76 |
77 |
78 | """____________3D GEOMETRY TRANSLATORS____________"""
79 |
80 |
81 | def from_vector3d(vector):
82 | """Rhino Vector3d from ladybug Vector3D."""
83 | return rg.Vector3d(vector.x, vector.y, vector.z)
84 |
85 |
86 | def from_point3d(point):
87 | """Rhino Point3d from ladybug Point3D."""
88 | return rg.Point3d(point.x, point.y, point.z)
89 |
90 |
91 | def from_ray3d(ray):
92 | """Rhino Ray3d from ladybug Ray3D."""
93 | return rg.Ray3d(from_point3d(ray.p), from_vector3d(ray.v))
94 |
95 |
96 | def from_linesegment3d(line):
97 | """Rhino LineCurve from ladybug LineSegment3D."""
98 | return rg.LineCurve(from_point3d(line.p1), from_point3d(line.p2))
99 |
100 |
101 | def from_plane(pl):
102 | """Rhino Plane from ladybug Plane."""
103 | return rg.Plane(from_point3d(pl.o), from_vector3d(pl.x), from_vector3d(pl.y))
104 |
105 |
106 | def from_arc3d(arc):
107 | """Rhino Arc from ladybug Arc3D."""
108 | if arc.is_circle:
109 | return rg.Circle(from_plane(arc.plane), from_point3d(arc.c), arc.radius)
110 | else:
111 | pts = (arc.p1, arc.midpoint, arc.p2)
112 | return rg.Arc(*(from_point3d(pt) for pt in pts))
113 |
114 |
115 | def from_polyline3d(polyline):
116 | """Rhino closed PolyLineCurve from ladybug Polyline3D."""
117 | rhino_pts = [from_point3d(pt) for pt in polyline.vertices]
118 | if polyline.interpolated:
119 | return rg.Curve.CreateInterpolatedCurve(
120 | rhino_pts, 3, rg.CurveKnotStyle.UniformPeriodic)
121 | else:
122 | return rg.PolylineCurve(rhino_pts)
123 |
124 |
125 | def from_mesh3d(mesh):
126 | """Rhino Mesh from ladybug Mesh3D."""
127 | return _translate_mesh(mesh, from_point3d)
128 |
129 |
130 | def from_face3d(face):
131 | """Rhino Brep from ladybug Face3D."""
132 | segs = [from_linesegment3d(seg) for seg in face.boundary_segments]
133 | try:
134 | brep = rg.Brep.CreatePlanarBreps(segs, tolerance)[0]
135 | except TypeError: # not planar in Rhino model tolerance; maybe from another model
136 | print('Brep not planar in Rhino model tolerance. Ignoring tolerance.')
137 | try:
138 | brep = rg.Brep.CreatePlanarBreps(segs, 1e6)[0]
139 | except TypeError: # it must be a zero-area geometry
140 | if 2 < len(face) <= 4:
141 | pts = [from_point3d(pt) for pt in face.vertices] + [tolerance,]
142 | return rg.Brep.CreateFromCornerPoints(*pts)
143 | return None
144 | if face.has_holes:
145 | for hole in face.hole_segments:
146 | trim_crvs = [from_linesegment3d(seg) for seg in hole]
147 | brep.Loops.AddPlanarFaceLoop(0, rg.BrepLoopType.Inner, trim_crvs)
148 | return brep
149 |
150 |
151 | def from_polyface3d(polyface):
152 | """Rhino Brep from ladybug Polyface3D."""
153 | rh_faces = [from_face3d(face) for face in polyface.faces]
154 | brep = rg.Brep.JoinBreps(rh_faces, tolerance)
155 | if len(brep) == 1:
156 | return brep[0]
157 |
158 |
159 | def from_sphere(sphere):
160 | """Rhino Sphere from ladybug Sphere."""
161 | return rg.Sphere(from_point3d(sphere.center), sphere.radius)
162 |
163 |
164 | def from_cone(cone):
165 | """Rhino Cone from ladybug Cone."""
166 | plane = rg.Plane(from_point3d(cone.vertex), from_vector3d(cone.axis.normalize()))
167 | return rg.Cone(plane, cone.height, cone.radius)
168 |
169 |
170 | def from_cylinder(cylinder):
171 | """Rhino Cone from ladybug Cone."""
172 | return rg.Cylinder(from_arc3d(cylinder.base_bottom), cylinder.height)
173 |
174 |
175 | """________ADDITIONAL 3D GEOMETRY TRANSLATORS________"""
176 |
177 |
178 | def from_polyline2d_to_joined_polyline(polylines, z=0):
179 | """Rhino PolylineCurve made by joining list of Polyline2D.
180 |
181 | Args:
182 | polylines: An array of Ladybug Polyline2D or LineSegment2D to be joined
183 | into a polyline. This can also be an array with a single Polygon2D.
184 | z: A number for the Z-coordinate. (Default: 0).
185 |
186 | Returns:
187 | A Rhino brep constructed from the polygon and the offset distance. If offset
188 | is unsuccessful, just the polyline will be returned.
189 | """
190 | line_crv = []
191 | for pl in polylines:
192 | if isinstance(pl, Polygon2D):
193 | return from_polygon2d(pl)
194 | if isinstance(pl, LineSegment2D):
195 | line_crv.append(from_linesegment2d(pl))
196 | else:
197 | line_crv.append(from_polyline2d(pl))
198 | return rg.Curve.JoinCurves(line_crv)[0]
199 |
200 |
201 | def from_polyline2d_to_offset_brep(polylines, offset, z=0):
202 | """Rhino Brep made by offsetting a joined list of Polyline2D inward.
203 |
204 | Args:
205 | polylines: An array of Ladybug Polyline2D or LineSegment2D to be joined
206 | and translated to an offset Brep. This can also be an array with
207 | a single Polygon2D.
208 | offset: A number for the distance to offset the Polygon inward.
209 | z: A number for the Z-coordinate. (Default: 0).
210 |
211 | Returns:
212 | A Rhino brep constructed from the polygon and the offset distance. If offset
213 | is unsuccessful, just the polyline will be returned.
214 | """
215 | curve = from_polyline2d_to_joined_polyline(polylines, z)
216 | crv_style = rg.CurveOffsetCornerStyle.Sharp
217 | all_curves = [curve]
218 | off_curves = curve.Offset(rg.Plane.WorldXY, -offset, tolerance, crv_style)
219 | if off_curves is not None:
220 | all_curves.extend(off_curves)
221 | offset_brep = rg.Brep.CreatePlanarBreps(all_curves)
222 | if len(offset_brep) == 1:
223 | if offset_brep[0].Loops.Count == 2:
224 | return offset_brep[0]
225 | return curve
226 |
227 |
228 | def from_face3d_to_wireframe(face):
229 | """Rhino PolyLineCurves from ladybug Face3D.
230 |
231 | Args:
232 | face: A Ladybug Face3D object to be translated to a wireframe.
233 |
234 | Returns:
235 | A list of Rhino polyline curves for the boundary and holes in the face.
236 | """
237 | boundary = [_polyline_points(face.boundary)]
238 | if face.has_holes:
239 | return boundary + [_polyline_points(tup) for tup in face.holes]
240 | return boundary
241 |
242 |
243 | def from_polyface3d_to_wireframe(polyface):
244 | """Rhino PolyLineCurve from ladybug Polyface3D."""
245 | return [f for face in polyface.faces for f in from_face3d_to_wireframe(face)]
246 |
247 |
248 | def from_mesh3d_to_wireframe(mesh):
249 | """Rhino PolyLineCurves from ladybug Mesh3D.
250 |
251 | Args:
252 | mesh: A Ladybug Mesh3D object to be translated to a wireframe.
253 |
254 | Returns:
255 | A list of Rhino polyline curves for the mesh edges.
256 | """
257 | line_curves = []
258 | for edge in mesh.face_edges:
259 | line_curves.append(from_polyline3d(edge))
260 | return line_curves
261 |
262 |
263 | def from_face3d_to_solid(face, offset):
264 | """Rhino Solid Brep from a ladybug Face3D and an offset from the base face.
265 |
266 | Args:
267 | face: Ladybug geometry Face3D object.
268 | offset: Number for the offset distance from the base face.
269 | """
270 | srf_brep = from_face3d(face)
271 | return rg.Brep.CreateFromOffsetFace(
272 | srf_brep.Faces[0], offset, tolerance, False, True)
273 |
274 |
275 | def from_face3ds_to_joined_brep(faces):
276 | """A list of joined Breps from an array of ladybug Face3D."""
277 | return rg.Brep.JoinBreps([from_face3d(face) for face in faces], tolerance)
278 |
279 |
280 | def from_face3ds_to_colored_mesh(faces, color):
281 | """Colored Rhino mesh from an array of ladybug Face3D and ladybug Color.
282 |
283 | This is used in workflows such as coloring Model geometry with results.
284 | """
285 | joined_mesh = rg.Mesh()
286 | for face in faces:
287 | try:
288 | joined_mesh.Append(from_mesh3d(face.triangulated_mesh3d))
289 | except Exception:
290 | pass # failed to create a Rhino Mesh from the Face3D
291 | joined_mesh.VertexColors.CreateMonotoneMesh(color_to_color(color))
292 | return joined_mesh
293 |
294 |
295 | def from_mesh3ds_to_colored_mesh(meshes, color):
296 | """Colored Rhino mesh from an array of ladybug Mesh3D and ladybug Color.
297 |
298 | This is used in workflows such as coloring Model geometry with results.
299 | """
300 | joined_mesh = rg.Mesh()
301 | for mesh in meshes:
302 | try:
303 | joined_mesh.Append(from_mesh3d(mesh))
304 | except Exception:
305 | pass # failed to create a Rhino Mesh from the Mesh3d
306 | joined_mesh.VertexColors.CreateMonotoneMesh(color_to_color(color))
307 | return joined_mesh
308 |
309 |
310 | def from_mesh2d_to_outline(mesh, z=0):
311 | """Rhino Polylines from the faces of ladybug Mesh2D."""
312 | pt_function = _get_point2d_function(z)
313 | verts = [pt_function(pt) for pt in mesh.vertices]
314 | face_plines = []
315 | for face in mesh.faces:
316 | outline = [verts[f] for f in face] + [verts[face[0]]]
317 | face_plines.append(rg.PolylineCurve(outline))
318 | return face_plines
319 |
320 |
321 | def from_mesh3d_to_outline(mesh):
322 | """Rhino Mesh and Polylines from the ladybug Mesh3D.
323 |
324 | Returns:
325 | A tuple with two items - a Rhino Mesh first followed by outline curves
326 | of the Mesh.
327 | """
328 | rh_mesh = from_mesh3d(mesh)
329 | return rh_mesh, rh_mesh.GetNakedEdges()
330 |
331 |
332 | """________________EXTRA HELPER FUNCTIONS________________"""
333 |
334 |
335 | def _translate_mesh(mesh, pt_function):
336 | """Translates both 2D and 3D meshes to Rhino"""
337 | rhino_mesh = rg.Mesh()
338 | if mesh.is_color_by_face: # Mesh is constructed face-by-face
339 | _f_num = 0
340 | for face in mesh.faces:
341 | for pt in tuple(mesh[i] for i in face):
342 | rhino_mesh.Vertices.Add(pt_function(pt))
343 | if len(face) == 4:
344 | rhino_mesh.Faces.AddFace(
345 | _f_num, _f_num + 1, _f_num + 2, _f_num + 3)
346 | _f_num += 4
347 | else:
348 | rhino_mesh.Faces.AddFace(_f_num, _f_num + 1, _f_num + 2)
349 | _f_num += 3
350 | if mesh.colors is not None:
351 | rhino_mesh.VertexColors.CreateMonotoneMesh(gray())
352 | _f_num = 0
353 | for i, face in enumerate(mesh.faces):
354 | col = color_to_color(mesh.colors[i])
355 | rhino_mesh.VertexColors[_f_num] = col
356 | rhino_mesh.VertexColors[_f_num + 1] = col
357 | rhino_mesh.VertexColors[_f_num + 2] = col
358 | if len(face) == 4:
359 | rhino_mesh.VertexColors[_f_num + 3] = col
360 | _f_num += 4
361 | else:
362 | _f_num += 3
363 | else: # Mesh is constructed vertex-by-vertex
364 | for pt in mesh.vertices:
365 | rhino_mesh.Vertices.Add(pt_function(pt))
366 | for face in mesh.faces:
367 | rhino_mesh.Faces.AddFace(*face)
368 | if mesh.colors is not None:
369 | rhino_mesh.VertexColors.CreateMonotoneMesh(gray())
370 | for i, col in enumerate(mesh.colors):
371 | rhino_mesh.VertexColors[i] = color_to_color(col)
372 | return rhino_mesh
373 |
374 |
375 | def _polyline_points(tup):
376 | """Convert a tuple of Ladybug Geometry points to a Rhino Polyline."""
377 | return rg.PolylineCurve([from_point3d(pt) for pt in tup] + [from_point3d(tup[0])])
378 |
--------------------------------------------------------------------------------
/ladybug_rhino/viewport.py:
--------------------------------------------------------------------------------
1 | """Functions for getting viewport properties, creating new viewports, and editing them.
2 | """
3 | import math
4 |
5 | try:
6 | import System
7 | except ImportError as e: # No .NET; We are really screwed
8 | raise ImportError("Failed to import System.\n{}".format(e))
9 |
10 | try:
11 | import Rhino.Geometry as rg
12 | import Rhino.Display as rd
13 | from Rhino import RhinoDoc as rhdoc
14 | import Rhino.ApplicationSettings.AppearanceSettings as aps
15 | except ImportError as e: # No RhinoCommon doc is available. This module is useless.
16 | raise ImportError("Failed to import Rhino.\n{}".format(e))
17 |
18 | try:
19 | import scriptcontext as sc
20 | except ImportError as e: # No Rhino doc is available. This module is useless.
21 | raise ImportError("Failed to import Rhino scriptcontext.\n{}".format(e))
22 |
23 | try:
24 | from .text import TextGoo
25 | except Exception: # we are outside of Grasshopper
26 | TextGoo = None
27 |
28 |
29 | def camera_oriented_plane(origin):
30 | """Get a Rhino Plane that is oriented facing the camera.
31 |
32 | Args:
33 | origin: A Rhino Point for the origin of the plane.
34 | """
35 | active_view = rhdoc.ActiveDoc.Views.ActiveView.ActiveViewport
36 | camera_x = active_view.CameraX
37 | camera_y = active_view.CameraY
38 | return rg.Plane(origin, camera_x, camera_y)
39 |
40 |
41 | def orient_to_camera(geometry, position=None):
42 | """Orient an array of Rhino geometry objects to the camera of the active viewport.
43 |
44 | Args:
45 | geometry: An array of Rhino Geometry objects (or TextGoo objects) to
46 | the camera of the active Rhino viewport.
47 | position: A point to be used as the origin around which the the geometry
48 | will be oriented. If None, the lower left corner of the bounding box
49 | around the geometry will be used.
50 | """
51 |
52 | # set the default position if it is None
53 | origin = _bounding_box_origin(geometry)
54 | pt = origin if position is None else position
55 |
56 | # get a plane oriented to the camera
57 | oriented_plane = camera_oriented_plane(pt)
58 |
59 | # orient the input geometry to the plane facing the camera
60 | base_plane = rg.Plane(origin, rg.Vector3d(0, 0, 1))
61 | xform = rg.Transform.PlaneToPlane(base_plane, oriented_plane)
62 | geo = []
63 | for rh_geo in geometry:
64 | if isinstance(rh_geo, TextGoo):
65 | geo.append(rh_geo.Transform(xform))
66 | elif isinstance(rh_geo, rg.Point3d):
67 | geo.append(oriented_plane)
68 | elif isinstance(rh_geo, rg.Plane):
69 | geo.append(oriented_plane)
70 | else:
71 | new_geo = rh_geo.Duplicate()
72 | new_geo.Transform(xform)
73 | geo.append(new_geo)
74 | return geo
75 |
76 |
77 | def viewport_by_name(view_name=None):
78 | """Get a Rhino Viewport object using the name of the viewport.
79 |
80 | Args:
81 | view_name: Text for the name of the Rhino Viewport. If None, the
82 | current Rhino viewport will be used. If the view is a named view that
83 | is not currently open, it will be restored to the active view of
84 | the Rhino document.
85 | """
86 | try:
87 | return rhdoc.ActiveDoc.Views.Find(view_name, False).ActiveViewport \
88 | if view_name is not None else rhdoc.ActiveDoc.Views.ActiveView.ActiveViewport
89 | except Exception:
90 | # try to find a named view and restore it
91 | view_table = rhdoc.ActiveDoc.NamedViews
92 | for i, viewp in enumerate(view_table):
93 | if viewp.Name == view_name:
94 | active_viewp = rhdoc.ActiveDoc.Views.ActiveView.ActiveViewport
95 | view_table.Restore(i, active_viewp)
96 | return active_viewp
97 | else:
98 | raise ValueError('Viewport "{}" was not found in the Rhino '
99 | 'document.'.format(view_name))
100 |
101 |
102 | def open_viewport(view_name, width=None, height=None):
103 | """Create a new Viewport in the active Rhino document at specified dimensions.
104 |
105 | This will also set the newly-created view to be tha active Viewport.
106 |
107 | Args:
108 | view_name: Text for the name of the new Rhino Viewport that will be created.
109 | width: Optional positive integer for the width of the view in pixels. If None,
110 | the width of the currently active viewport will be used.
111 | height: Optional positive integer for the height of the view in pixels. If
112 | None, the height of the currently active viewport will be used.
113 | """
114 | # close the view if it already exists
115 | if rhdoc.ActiveDoc.Views.Find(view_name, False):
116 | rhdoc.ActiveDoc.Views.Find(view_name, False).Close()
117 |
118 | # get the width and the height if it was not specified
119 | w = rhdoc.ActiveDoc.Views.ActiveView.ActiveViewport.Size.Width if not width else width
120 | h = rhdoc.ActiveDoc.Views.ActiveView.ActiveViewport.Size.Height if not height else height
121 |
122 | # compute the X,Y screen coordinates where the new viewport will be placed
123 | x = round((System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width - w) / 2)
124 | y = round((System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height - h) / 2)
125 | rec = System.Drawing.Rectangle(System.Drawing.Point(x, y), System.Drawing.Size(w, h))
126 |
127 | # add the new view to the rhino document
128 | rhdoc.ActiveDoc.Views.Add(
129 | view_name, rd.DefinedViewportProjection.Perspective, rec, True)
130 | return viewport_by_name(view_name)
131 |
132 |
133 | def set_view_display_mode(viewport, display_mode):
134 | """Set the display mode of a Rhino Viewport.
135 |
136 | Args:
137 | viewport: A Rhino ViewPort object, which will have its display mode set.
138 | display_mode: Text for the display mode to which the Rhino viewport will be
139 | set. For example: Wireframe, Shaded, Rendered, etc.
140 | """
141 | mode_obj = rd.DisplayModeDescription.FindByName(display_mode)
142 | viewport.DisplayMode = mode_obj
143 |
144 |
145 | def set_view_direction(viewport, direction, position=None, lens_length=None):
146 | """Set a Rhino Viewport to face a specific direction.
147 |
148 | Args:
149 | viewport: A Rhino ViewPort object, which will have its direction set.
150 | direction: A Rhino vector that will be used to set the direction of the view.
151 | position: Optional Rhino point for the target of the camera. If no point
152 | is provided, the Rhino origin will be used (0, 0, 0).
153 | """
154 | position = position if position is not None else rg.Point3d.Origin
155 | target = rg.Point3d.Add(position, direction)
156 | viewport.SetCameraLocation(position, True)
157 | viewport.SetCameraDirection(direction, False)
158 | viewport.SetCameraTarget(target, False)
159 | if lens_length is not None:
160 | viewport.Camera35mmLensLength = lens_length
161 |
162 |
163 | def set_iso_view_direction(viewport, direction, center_point=None):
164 | """Set a Rhino Viewport to have an isometric view in a specific direction.
165 |
166 | Args:
167 | viewport: A Rhino ViewPort object, which will have its direction set.
168 | direction: A Rhino vector that will be used to set the direction of
169 | the isometric view.
170 | center_point: Optional Rhino point for the target of the camera. If no point
171 | is provided, the Rhino origin will be used (0, 0, 0).
172 | """
173 | viewport.ChangeToParallelProjection(True)
174 | center_point = center_point if center_point is not None else rg.Point3d.Origin
175 | viewport.SetCameraLocation(rg.Point3d.Add(center_point, direction), False)
176 | viewport.SetCameraTarget(center_point, False)
177 | viewport.SetCameraDirection(direction, False)
178 |
179 |
180 | def capture_view(viewport, file_path, width=None, height=None, display_mode=None,
181 | transparent=False):
182 | """Capture a Viewport to a PNG file path.
183 |
184 | Args:
185 | viewport: A Rhino ViewPort object, which will have its display mode set.
186 | file_path: Full path to the file where the image will be saved.
187 | width: Integer for the image width in pixels. If None, the width of the
188 | active viewport will be used. (Default: None).
189 | height: Integer for the image height in pixels. If None, the height of the
190 | active viewport will be used. (Default: None).
191 | display_mode: Text for the display mode to which the Rhino viewport will be
192 | set. For example: Wireframe, Shaded, Rendered, etc. If None, it will
193 | be the current viewport's display mode. (Default: None).
194 | transparent: Boolean to note whether the background of the image should be
195 | transparent or have the same color as the Rhino scene. (Default: False).
196 |
197 | Returns:
198 | Full path to the image file that was written.
199 | """
200 | # create the view capture object
201 | active_viewp = viewport_by_name()
202 | img_w = active_viewp.Size.Width if width is None else width
203 | img_h = active_viewp.Size.Height if height is None else height
204 | img_size = System.Drawing.Size(img_w, img_h)
205 |
206 | # capture the view
207 | if display_mode is not None:
208 | mode_obj = rd.DisplayModeDescription.FindByName(display_mode)
209 | pic = viewport.ParentView.CaptureToBitmap(img_size, mode_obj)
210 | else:
211 | pic = viewport.ParentView.CaptureToBitmap(img_size)
212 |
213 | # remove the background color if requested
214 | if transparent:
215 | back_col = aps.ViewportBackgroundColor
216 | if (display_mode is None and viewport.DisplayMode.EnglishName == 'Rendered') \
217 | or display_mode == 'Rendered':
218 | back_col = rhdoc.ActiveDoc.Views.Document.RenderSettings.BackgroundColorTop
219 | pic.MakeTransparent(back_col)
220 |
221 | # save the bitmap to a png file
222 | if not file_path.endswith('.png'):
223 | file_path = '{}.png'.format(file_path)
224 | System.Drawing.Bitmap.Save(pic, file_path)
225 | return file_path
226 |
227 |
228 | def viewport_vh_vv(viewport, view_type):
229 | """Get the horizontal angle (vh) and the vertical angle (vv) from a viewport.
230 |
231 | Args:
232 | viewport: A Rhino ViewPort object for which properties will be extracted.
233 | view_type: An integer to set the view type (-vt). Choose from the
234 | choices below.
235 |
236 | * 0 Perspective (v)
237 | * 1 Hemispherical fisheye (h)
238 | * 2 Parallel (l)
239 | * 3 Cylindrical panorama (c)
240 | * 4 Angular fisheye (a)
241 | * 5 Planisphere [stereographic] projection (s)
242 |
243 | """
244 | if view_type == 0: # perspective
245 | right_vec = viewport.GetFrustumRightPlane()[1][1]
246 | left_vec = viewport.GetFrustumLeftPlane()[1][1]
247 | h_angle = 180 - math.degrees(rg.Vector3d.VectorAngle(right_vec, left_vec))
248 | bottom_vec = viewport.GetFrustumBottomPlane()[1][1]
249 | top_vec = viewport.GetFrustumTopPlane()[1][1]
250 | v_angle = 180 - math.degrees(rg.Vector3d.VectorAngle(bottom_vec, top_vec))
251 | return h_angle, v_angle
252 | if view_type == 1 or view_type == 5:
253 | return 180, 180
254 | if view_type == 2:
255 | v_rect = viewport.GetNearRect()
256 | return int(v_rect[0].DistanceTo(v_rect[1])), int(v_rect[0].DistanceTo(v_rect[2]))
257 | if view_type == 3:
258 | return 360, 180
259 | if view_type == 4:
260 | return 60, 60
261 |
262 |
263 | def viewport_properties(viewport, view_type=None):
264 | """Get a dictionary of properties of a Rhino viewport.
265 |
266 | Args:
267 | viewport: A Rhino ViewPort object for which properties will be extracted.
268 | view_type: An integer to set the view type (-vt). Choose from the
269 | choices below or set to None to have it derived from the viewport.
270 |
271 | * 0 Perspective (v)
272 | * 1 Hemispherical fisheye (h)
273 | * 2 Parallel (l)
274 | * 3 Cylindrical panorama (c)
275 | * 4 Angular fisheye (a)
276 | * 5 Planisphere [stereographic] projection (s)
277 |
278 | Returns:
279 | A dictionary with the following keys: 'view_type', 'position', 'direction',
280 | 'up_vector', 'h_angle', 'v_angle'
281 | """
282 | # ensure that we have an integer for the view_type
283 | if view_type is None:
284 | view_type = 2 if viewport.IsParallelProjection else 0
285 |
286 | # get the position, direction and up vectors
287 | pos = viewport.CameraLocation
288 | direct = viewport.CameraDirection
289 | up_vec = viewport.CameraUp
290 | direct.Unitize()
291 | up_vec.Unitize()
292 |
293 | # get the h_angle and v_angle from the viewport
294 | h_angle, v_angle = viewport_vh_vv(viewport, view_type)
295 |
296 | return {
297 | 'view_type': view_type,
298 | 'position': (pos.X, pos.Y, pos.Z),
299 | 'direction': (direct.X, direct.Y, direct.Z),
300 | 'up_vector': (up_vec.X, up_vec.Y, up_vec.Z),
301 | 'h_angle': h_angle,
302 | 'v_angle': v_angle
303 | }
304 |
305 |
306 | def _bounding_box_origin(geometry):
307 | """Get the origin of a bounding box around a list of geometry.
308 |
309 | Args:
310 | geometry: A list of geometry for which the bounding box origin will
311 | be computed.
312 | """
313 | # get the first geometry
314 | first_geo = geometry[0]
315 | # if the geometry is a point or plane, just return the plane origin
316 | if isinstance(first_geo, rg.Point3d):
317 | return first_geo
318 | elif isinstance(first_geo, rg.Plane):
319 | return first_geo.Origin
320 | # assume that the geometry is a bunch of breps, meshes or text objects
321 | b_box = first_geo.GetBoundingBox(False) if not isinstance(first_geo, TextGoo) \
322 | else first_geo.get_Boundingbox()
323 | for geo in geometry[1:]:
324 | if isinstance(geo, TextGoo):
325 | b_box = rg.BoundingBox.Union(b_box, geo.get_Boundingbox())
326 | else:
327 | b_box = rg.BoundingBox.Union(b_box, geo.GetBoundingBox(False))
328 | return b_box.Corner(True, True, True)
329 |
--------------------------------------------------------------------------------
/ladybug_rhino/versioning/export.py:
--------------------------------------------------------------------------------
1 | """Functions for exporting all content from Grasshopper component objects."""
2 | import os
3 | import shutil
4 | import re
5 |
6 | try:
7 | import System.Drawing
8 | except ImportError:
9 | raise ImportError("Failed to import System.")
10 |
11 | try:
12 | import Grasshopper
13 | except ImportError:
14 | raise ImportError("Failed to import Grasshopper.")
15 |
16 | from .diff import current_userobject_version, validate_version
17 | from .userobject import create_userobject
18 | from .component import Component
19 |
20 | # characters that get removed and replaced when generating clean file names
21 | REMOVE_CHARACTERS = ('LB ', 'HB ', 'DF ')
22 | REPLACE_CHARACTERS = (' ', '/', '?', '|')
23 |
24 | # map from the exposure category of a component to the order in which it is displayed
25 | REVERSE_EXPOSURE_MAP = {
26 | 'obscure': 100,
27 | 'hidden': 100,
28 | 'primary': 1,
29 | 'secondary': 2,
30 | 'tertiary': 3,
31 | 'quarternary': 4,
32 | 'quinary': 5,
33 | 'senary': 6,
34 | 'septenary': 7
35 | }
36 |
37 |
38 | def export_component(folder, component, change_type='fix'):
39 | """Export a Grasshopper component object to a package folder.
40 |
41 | This method writes the following files:
42 |
43 | * A .ghuser into the user_objects subfolder
44 | * A .py file into the src subfolder
45 | * A .json into the json subfolder
46 | * A .png into the icon subfolder
47 |
48 | Args:
49 | folder: Path to a folder into which the component files will be exported.
50 | Typically, this is the package folder of a grasshopper plugin repo.
51 | (eg. ladybug-grasshopper/ladybug_grasshopper).
52 | component: The Grasshopper component object to be exported to the folder.
53 | change_type: Text for the change type of the export. Valid change types
54 | can be seen under the CHANGE_TAGS property of the userobject module.
55 | """
56 | # process the component into a user object
57 | current_version = current_userobject_version(component)
58 | validate_version(current_version, component.Message, change_type)
59 | uo = create_userobject(component)
60 |
61 | # create subfolders in the folder if they are not already created
62 | uo_folder = os.path.join(folder, 'user_objects')
63 | src_folder = os.path.join(folder, 'src')
64 | json_folder = os.path.join(folder, 'json')
65 | icon_folder = os.path.join(folder, 'icon')
66 | for f in (folder, uo_folder, src_folder, json_folder, icon_folder):
67 | if not os.path.isdir(f):
68 | os.mkdir(f)
69 |
70 | # get the paths to the where the files will be written
71 | uo_fp = os.path.join(uo_folder, '%s.ghuser' % uo.Description.Name)
72 | src_fp = os.path.join(src_folder, '%s.py' % uo.Description.Name)
73 | json_fp = os.path.join(json_folder, '%s.json' % uo.Description.Name)
74 | icon_fp = os.path.join(icon_folder, '%s.png' % uo.Description.Name)
75 |
76 | # export the userobject to the user_objects subfolder
77 | if os.path.isfile(uo_fp):
78 | os.remove(uo_fp)
79 | uo.Path = uo_fp
80 | uo.SaveToFile()
81 |
82 | # export the .py file to the src subfolder
83 | code = uo.InstantiateObject().Code
84 | try:
85 | if isinstance(code, unicode):
86 | code = code.encode('utf8', 'ignore').replace("\r", "")
87 | except Exception:
88 | pass # we are not in Python 2
89 | with open(src_fp, 'w') as outf:
90 | outf.write(code)
91 |
92 | # export the .json file to the json subfolder
93 | if os.path.isfile(json_fp):
94 | os.remove(json_fp)
95 | component_obj = Component.from_gh_component(component)
96 | component_obj.to_json(json_folder)
97 |
98 | # export the icon file
99 | icon = component.Icon_24x24
100 | icon.Save(icon_fp)
101 |
102 | print(' UserObject, source code, icon and JSON are copied to folder.')
103 |
104 |
105 | def export_component_screen_capture(folder, component, x_dim=1000, y_dim=1000):
106 | """Export a screen capture of a Grasshopper component object to a folder.
107 |
108 | The image will always be centered on the component and at a resolution where
109 | the inputs and outputs are clearly visible.
110 |
111 | Args:
112 | folder: Path to a folder into which the image file will be exported.
113 | component: The Grasshopper component object to be exported to the folder.
114 | x_dim: Integer for the X dimension of the exported image in
115 | pixels. (Default: 1000).
116 | y_dim: Integer for the X dimension of the exported image in
117 | pixels. (Default: 1000).
118 | """
119 | # Get the coordinates of the upper-left corner of the image from the component
120 | if component.Name == 'LB Image Viewer':
121 | ul_x = component.Attributes.Pivot.X - int(((x_dim / 2) - 400) / 2)
122 | ul_y = component.Attributes.Pivot.Y - int(((y_dim / 2) - 400) / 2)
123 | elif component.Name == 'LB Clothing List':
124 | ul_x = component.Attributes.Pivot.X - int(((x_dim / 2) - 200) / 2)
125 | ul_y = component.Attributes.Pivot.Y - int(((y_dim / 2) - 450) / 2)
126 | elif 'HVAC Templates' in component.Name:
127 | ul_x = component.Attributes.Pivot.X - int(((x_dim / 2) - 400) / 2)
128 | ul_y = component.Attributes.Pivot.Y - int(((y_dim / 2) - 120) / 2)
129 | else:
130 | ul_x = component.Attributes.Pivot.X - int(((x_dim / 2) - 120) / 2)
131 | ul_y = component.Attributes.Pivot.Y - int(((y_dim / 2) - 120) / 2)
132 | rect = System.Drawing.Rectangle(ul_x, ul_y, x_dim, y_dim)
133 |
134 | # set the image zoon/resolution
135 | image_settings = Grasshopper.GUI.Canvas.GH_Canvas.GH_ImageSettings()
136 | image_settings.Zoom = 1.95
137 | canvas = Grasshopper.GH_InstanceServer.ActiveCanvas
138 |
139 | # capture the image of the component
140 | images_of_canvas = canvas.GenerateHiResImage(rect, image_settings)
141 | screen_capture = images_of_canvas[0][0]
142 |
143 | # resize the image
144 | loaded_img = System.Drawing.Bitmap(screen_capture)
145 | new_img = loaded_img.Clone(System.Drawing.Rectangle(0, 0, x_dim, y_dim),
146 | loaded_img.PixelFormat)
147 |
148 | # write the image to a file
149 | file_name = clean_component_filename(component)
150 | file_path = os.path.join(folder, '{}.png'.format(file_name))
151 | new_img.Save(file_path)
152 |
153 | # delete original image
154 | loaded_img.Dispose()
155 | path = os.path.split(screen_capture)[0]
156 | shutil.rmtree(path)
157 | return file_path
158 |
159 |
160 | def export_component_icon(folder, component):
161 | """Export a Grasshopper component icon to a folder.
162 |
163 | Args:
164 | folder: Path to a folder into which the icon image file will be exported.
165 | component: The Grasshopper component object to be exported to the folder.
166 | """
167 | file_name = clean_component_filename(component)
168 | file_path = os.path.join(folder, '{}.png'.format(file_name))
169 | icon = component.Icon_24x24
170 | icon.Save(file_path)
171 |
172 |
173 | def export_component_to_markdown(folder, component, github_repo=None):
174 | """Export a Grasshopper component's description and metadata to a Markdown file.
175 |
176 | Args:
177 | folder: Path to a folder into which the MArkdown file will be exported.
178 | component: The Grasshopper component object to be exported to the folder.
179 | github_repo: Optional URL to a GitHub repo that can be used to link the
180 | Markdown page to a GitHub repository.
181 | """
182 | # get the relevant name information from the component
183 | b_name = component.Name
184 | for item in REMOVE_CHARACTERS:
185 | b_name = b_name.replace(item, '')
186 | name = clean_component_filename(component)
187 | lines = []
188 |
189 | # write the lines for the header with the image, icon and source code
190 | lines.append('## %s\n' % b_name)
191 | img_text = '\n' % name
192 | lines.append(img_text)
193 | if github_repo:
194 | source = ' - [[source code]](%s/%s.py)\n' % (
195 | name, github_repo, component.Name.replace(' ', '%20'))
196 | lines.append(source)
197 |
198 | # write the lines for the description
199 | full_desc = []
200 | for d_l in component.Description.split('\n'):
201 | if ('-' in d_l or '_' in d_l or '.' in d_l) and len(d_l) <= 2:
202 | full_desc.append('\n\n')
203 | else:
204 | full_desc.append('{} '.format(d_l.replace('\r', '')))
205 | lines.append('\n{}'.format(''.join(full_desc)))
206 |
207 | # check to see if there are any inputs and outputs to export
208 | inputs_outputs_available = True
209 | try:
210 | component.Params
211 | except Exception: # no inputs and outputs available
212 | inputs_outputs_available = False
213 |
214 | if inputs_outputs_available:
215 | # export the inputs
216 | lines.append('\n#### Inputs')
217 | for i in range(component.Params.Input.Count):
218 | i_name = component.Params.Input[i].NickName
219 | alph = ''.join(re.findall('[a-zA-Z]+', i_name))
220 | if len(alph) == 0:
221 | continue
222 |
223 | t = ''
224 | if i_name.startswith('_') and i_name.endswith('_'):
225 | i_name = i_name[1:-1]
226 | elif i_name.startswith('_'):
227 | i_name = i_name[1:]
228 | t = '[Required]'
229 | elif i_name.endswith('_'):
230 | i_name = i_name[:-1]
231 |
232 | full_desc = []
233 | for d_l in component.Params.Input[i].Description.split('\n'):
234 | if ('-' in d_l or '_' in d_l or '.' in d_l) and len(d_l) <= 2:
235 | full_desc.append('\n')
236 | elif d_l.startswith('*') or d_l.startswith('-'):
237 | full_desc.append('\n\n {}'.format(d_l.replace('\r', '')))
238 | else:
239 | full_desc.append('{} '.format(d_l.replace('\r', '')))
240 | line = '* ##### {} {}\n{}'.format(i_name, t, ''.join(full_desc))
241 | lines.append(line)
242 |
243 | # export the outputs
244 | lines.append('\n#### Outputs')
245 | for i in range(component.Params.Output.Count):
246 | o_name = component.Params.Output[i].NickName
247 | alph = ''.join(re.findall('[a-zA-Z]+', o_name))
248 | if len(alph) == 0:
249 | continue
250 | full_desc = []
251 | for d_l in component.Params.Output[i].Description.split('\n'):
252 | if ('-' in d_l or '_' in d_l or '.' in d_l) and len(d_l) <= 2:
253 | full_desc.append('\n')
254 | elif d_l.startswith('*') or d_l.startswith('-'):
255 | full_desc.append('\n\n {}'.format(d_l.replace('\r', '')))
256 | else:
257 | full_desc.append('{} '.format(d_l.replace('\r', '')))
258 | line = '* ##### {}\n{}'.format(o_name, ''.join(full_desc))
259 | lines.append(line)
260 |
261 | # write the .md file
262 | file_path = os.path.join(folder, '{}.md'.format(name))
263 | with open(file_path, 'w') as out_f:
264 | out_f.write('\n'.join(lines).encode('utf-8'))
265 | return file_path
266 |
267 |
268 | def export_plugin_to_markdown(folder, plugin_name):
269 | """Export a Grasshopper plugin and its subcategories to Markdown files.
270 |
271 | Args:
272 | folder: Path to a folder into which the icon image file will be exported.
273 | plugin_name: Text for the name of a particular plugin (aka. insect) to
274 | place components from (eg. "Ladybug", "Honeybee", "HB-Energy").
275 | """
276 | # gather all of the components of a plugin into a dictionary with subcategories
277 | components = {}
278 | for proxy in Grasshopper.Instances.ComponentServer.ObjectProxies:
279 | if proxy.Obsolete:
280 | continue
281 | category = proxy.Desc.Category
282 | subcategory = proxy.Desc.SubCategory
283 | # check to see if the component is in the plugin
284 | if category.strip() == plugin_name: # if so, organize it by sub category
285 | if subcategory not in components.keys():
286 | components[subcategory] = {}
287 | expo = REVERSE_EXPOSURE_MAP[str(proxy.Exposure).split(',')[-1].strip()]
288 | if expo not in components[subcategory].keys():
289 | components[subcategory][expo] = []
290 | name = clean_component_filename(proxy.Desc)
291 | components[subcategory][expo].append(name)
292 | sorted_sub_categories = sorted(components.keys())
293 |
294 | # create the summary file header
295 | lines = []
296 | read_md = '[%s Primer](README.md)' % plugin_name
297 | header = '# Summary\n\n* ' + read_md + '\n* [Components](text/categories/README.md)'
298 | lines.append(header)
299 |
300 | # loop through the subcategories and add them to the index
301 | for s_category in sorted_sub_categories:
302 | # write the subcategory into the summary
303 | clean_cat = ''.join(s_category.split()).replace('|', '_').replace('::', '_')
304 | line = '\t* [%s](text/categories/%s.md)' % (s_category, clean_cat)
305 | lines.append(line)
306 |
307 | # collect text lines for the subcategory page
308 | s_category_lines = []
309 | for num in sorted(components[s_category].keys()):
310 | for comp in sorted(components[s_category][num]):
311 | readable_name = comp.replace('_', ' ')
312 | line = '\t\t* [%s](text/components/%s.md)' % (readable_name, comp)
313 | icon = '* ' % comp
314 | s_category_lines.append(
315 | icon + line.replace('\t\t*', '').replace('text', '..'))
316 | lines.append(line)
317 |
318 | # write md file for the category
319 | file_path = os.path.join(folder, 'text', 'categories', '%s.md' % clean_cat)
320 | with open(file_path, 'w') as tab:
321 | tab.write('#### Component list:\n' + '\n'.join(s_category_lines))
322 |
323 | # write the summary file
324 | file_path = os.path.join(folder, 'SUMMARY.md')
325 | with open(file_path, 'w') as summary:
326 | summary.write('\n'.join(lines))
327 | return file_path
328 |
329 |
330 | def clean_component_filename(component):
331 | """Get a clean filename derived from a component's name."""
332 | file_name = component.Name
333 | for item in REMOVE_CHARACTERS:
334 | file_name = file_name.replace(item, '')
335 | for item in REPLACE_CHARACTERS:
336 | file_name = file_name.replace(item, '_')
337 | return file_name
338 |
339 |
340 | def refresh_toolbar():
341 | """Try to refresh the Grasshopper toolbar after exporting a component."""
342 | Grasshopper.Kernel.GH_ComponentServer.UpdateRibbonUI()
343 |
--------------------------------------------------------------------------------