pax_global_header00006660000000000000000000000064135447406120014520gustar00rootroot0000000000000052 comment=d50bc82db124d3d895dd37ed7a620bcf9891e762 gdspy-1.4.2/000077500000000000000000000000001354474061200126525ustar00rootroot00000000000000gdspy-1.4.2/.gitignore000066400000000000000000000013501354474061200146410ustar00rootroot00000000000000MANIFEST .*.swp .DS_Store # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg .pytest_cache/ # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ gdspy-1.4.2/.travis.yml000066400000000000000000000011461354474061200147650ustar00rootroot00000000000000language: python python: - "2.7" - "3.5" - "3.6" # From: https://docs.travis-ci.com/user/languages/python/#running-python-tests-on-multiple-operating-systems matrix: include: - python: "3.7" dist: xenial - os: osx osx_image: xcode10.2 language: shell before_install: | python3 -m venv venv source venv/bin/activate python --version install: - pip install --upgrade pip - pip install -r requirements.txt - pip install sphinx sphinx_rtd_theme pytest - python setup.py build build_sphinx - python setup.py install script: - pytest gdspy-1.4.2/LICENSE000066400000000000000000000025211354474061200136570ustar00rootroot00000000000000Boost Software License - Version 1.0 - August 17th, 2003 Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following: The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gdspy-1.4.2/MANIFEST.in000066400000000000000000000002011354474061200144010ustar00rootroot00000000000000recursive-include docs/_build/html * include *.txt include README.* include examples/* include LICENSE include gdspy/clipper.hpp gdspy-1.4.2/README.md000066400000000000000000000461601354474061200141400ustar00rootroot00000000000000# GDSPY README [![Boost Software License - Version 1.0](https://img.shields.io/github/license/heitzmann/gdspy.svg)](http://www.boost.org/LICENSE_1_0.txt) [![Documentation Status](https://readthedocs.org/projects/gdspy/badge/?version=stable)](https://gdspy.readthedocs.io/en/stable/?badge=stable) [![Travis-CI Status](https://travis-ci.org/heitzmann/gdspy.svg?branch=master)](https://travis-ci.org/heitzmann/gdspy) [![Appveyor Status](https://ci.appveyor.com/api/projects/status/pr49a6bhxvbqwocy?svg=true)](https://ci.appveyor.com/project/heitzmann/gdspy) [![Downloads](https://img.shields.io/github/downloads/heitzmann/gdspy/total.svg)](https://github.com/heitzmann/gdspy/releases) Gdspy is a Python module for creating/importing/merging GDSII stream files. It includes key libraries for creating complex CAD layouts: * Boolean operations on polygons (AND, OR, NOT, XOR) based on clipping algorithm * Polygon offset (inward and outward rescaling of polygons) * Efficient point-in-polygon solutions for large array sets Gdspy also includes a simple layout viewer. Typical applications of gdspy are in the fields of electronic chip design, planar lightwave circuit design, and mechanical engineering. ## Installation ### Dependencies: * [Python](http://www.python.org/) (tested with versions 2.7, 3.5, 3.6, and 3.7) * [Numpy](http://numpy.scipy.org/) * [Python-future](http://python-future.org/) (only for Python 2) * C compiler (needed only if built from source) * Tkinter (optional: needed for the `LayoutViewer` GUI) * [Sphinx](http://sphinx-doc.org/) (optional: to build the documentation) ### Linux / OS X Option 1: using [pip](https://docs.python.org/3/installing/): ```sh pip install gdspy ``` Option 2: download the source from [github](https://github.com/heitzmann/gdspy) and build/install with: ```sh python setup.py install ``` ### Windows The preferred option is to install pre-compiled binaries from [here](https://github.com/heitzmann/gdspy/releases). Installation via `pip` and building from source as above are also possible, but an appropriate [build environment](https://wiki.python.org/moin/WindowsCompilers) is required for compilation of the C extension modules. ## Documentation The complete documentation is available [here](http://gdspy.readthedocs.io/). The source files can be found in the `docs` directory. ## Support Help support gdspy development by [donating via PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JD2EUE2WPPBQQ) ## History of changes ### Version 1.4.2 (Oct 01, 2019) * Bug fix in `FlexPath`. ### Version 1.4.1 (Sep 20, 2019) * Bug fixes (thanks to DerekK88 and Sequencer for the patches). ### Version 1.4 (May 18, 2019) * Revised [documentation](http://gdspy.readthedocs.io/). * New `FlexPath` and `RobustPath` classes: more efficient path generation when using the original GDSII path specification. * New `Curve` class: SVG-like polygon creation. * Added `PolygonSet.mirror` (thanks to Daan Waardenburg for the contribution). * Added `Path.bezier` to create paths based on Bézier curves. * Added `Path.smooth` to create paths based on smooth interpolating curves. * Added `get_gds_units` to get units used in a GDSII file without loading. * Added `get_binary_cells` to load only the binary GDSII representation of cell from a file. * Added argument `tolerance` to `Round`, `Path.arc`, `Path.turn`, and `Path.parametric` to automatically control the number of points in the final polygons. * Added argument `binary_cells` to GDSII writing functions to support `get_binary_cells`. * Added argument `rename_template` to `GdsLibrary.read_gds` for flexible cell renaming (thanks to @yoshi74ls181 for the contribution). * Changed return value of `slice` to avoid creating empty `PolygonSet`. * Added argument `timestamp` to GDSII writing functions. * Improved `Round` to support creating ellipses. * Added support for unlimited number of points per polygon. * Added support for BGNEXTN and ENDEXTN when reading a GDSII file. * Polygon creation warnings are now controlled by `poly_warnings`. * Incorrect `anchor` in `Label` now raises an error, instead of emitting a warning. * Added correct support for radius in `PolygonSet.fillet` on a per-vertex basis. * Speed improvements in GDSII file generation (thanks to @fbeutel for the contribution) and geometry creation. * Font rendering example using [matplotlib](https://matplotlib.org/) (thanks Hernan Pastoriza for the contribution). * Expanded test suite. ### Version 1.3.2 (Mar 14, 2019) * Small fix for building on Mac OS X Mojave. ### Version 1.3.1 (Jun 29, 2018) * `PolygonSet` becomes the base class for all polygons, in particular `Polygon` and `Rectangle`. * Added `Cell.remove_polygons` and `Cell.remove_labels` functions to allow filtering a cell contents based, for example, on each element's layer. * Added `PolygonSet.scale` utility method. * Added `PolygonSet.get_bounding_box` utility method. * Added argument `timestamp` to `Cell.to_gds`, `GdsLibrary.write_gds` and `GdsWriter`. * Added `unit` and `precision` arguments to `GdsLibrary` initialization and removed from its `write_gds` method. * Changed the meaning of argument `unit` in `GdsLibrary.read_gds`. * Improved `slice` to avoid errors when slicing in multiple positions at once. * Improved `PolygonSet.fracture` to reduce number of function calls. * Removed incorrect absolute flags for magnification and rotation in `CellReference` and `CellArray`. * Minor bug fixes. * Documentation fixes. * Removed deprecated classes and functions. ### Version 1.2.1 (Dec 5, 2017) * `GdsLibrary` can be created directly from a GDSII file * Added return value to `GdsLibrary.read_gds` * Fixed return value of `GdsLibrary.add` ### Version 1.2 (Oct 21, 2017) * Added new `gdsii_hash` function. * Added `precision` parameter to `_chop`, `Polygon.fracture`, `Polygon.fillet`, `PolygonSet.fracture`, `PolygonSet.fillet`, and `slice`. * Included labels in flatten operations (added `get_labels` to `Cell`, `CellReference`, and `CellArray`). * Fixed bug in the bounding box cache of reference copies. * Fixed bug in `_chop` that affected `Polygon.fracture`, `PolygonSet.fracture`, and `slice`. * Other minor bug fixes. ### Version 1.1.2 (Mar 19, 2017) * Update clipper library to 6.4.2 to fix bugs introduced in the last update. * License change to Boost Software License v1.0. ### Version 1.1.1 (Jan 27, 2017) * Patch to fix installation issue (missing README file in zip). ### Version 1.1 (Jan 20, 2017) * Introduction of `GdsLibrary` to allow user to work with multiple library simultaneously. * Deprecated `GdsImport` in favor of `GdsLibrary`. * Renamed `gds_print` to `write_gds` and `GdsPrint` to `GdsWriter`. * Development changed to Python 3 (Python 2 supported via [python-future](http://python-future.org/)). * Added photonics example. * Added test suite. * Clipper library updated to last version. * Fixed `inside` function sometimes reversing the order of the output. * Fixed rounding error in `fast_boolean`. * Fixed argument `deep_copy` being inverted in `Cell.copy`. * Bug fixes introduced by numpy (thanks to Adam McCaughan for the contribution). ### Version 1.0 (Sep 11, 2016) * Changed to "new style" classes (thanks to Adam McCaughan for the contribution). * Added a per-point radius specification for `Polygon.fillet` (thanks to Adam McCaughan for the contribution). * Added `inside` fucntion to perform point-in-polygon tests (thanks to @okianus for the contribution). * Moved from distutils to setuptools for better Windows support. ### Version 0.9 (Jul 17, 2016) * Added option to join polygons before applying an `offset`. * Added a `translate` method to geometric entities (thanks John Bell for the commit). * Bug fixes. ### Version 0.8.1 (May 6, 2016) * New `fast_boolean` function based on the [Clipper](http://www.angusj.com/delphi/clipper.php) library with much better performance than the old `boolean`. * Changed `offset` signature to also use the [Clipper](http://www.angusj.com/delphi/clipper.php) library (this change **breaks compatibility** with previous versions). * Bug fix for error when importing some labels from GDSII files. ### Version 0.7.1 (June 26, 2015) * Rebased to GitHub. * Changed source structure and documentation. ### Version 0.7 (June 12, 2015) * New feature: `offset` function. * New `GdsPrint` class for incremental GDSII creation (thanks to Jack Sankey for the contribution). ### Version 0.6 (March 31, 2014) * Default number of points for `Round`, `Path.arc`, and `Path.turn` changed to resolution of 0.01 drawing units. * `Path.parametric` accepts callable `final_distance` and `final_width` for non-linear tapering. * Added argument `ends` to `PolyPath`. * Added (limited) support for PATHTYPE in `GdsImport`. * A warning is issued when a `Path` curve has width larger than twice its radius (self-intersecting polygon). * Added a random offset to the patterns in `LayoutViewer`. * `LayoutViewer` shows cell labels for referenced cells. * `get_polygons` returns (referenced) cell name if `depth` < 1 and `by_spec` is True. * Bug fix in `get_bounding_box` when empty cells are referenced. * Bug fixes in `GdsImport` and many speed improvements in bounding box calculations (thanks to Gene Hilton for the patch). ### Version 0.5 (October 30, 2013) - NOT COMPATIBLE WITH PREVIOUS VERSIONS * Major `LayoutViewer` improvements (not backwards compatible). * The layer argument has been repositioned in the argument list in all functions (not backwards compatible). * Renamed argument `by_layer` to `by_spec` (not backwards compatible). * Error is raised for polygons with more vertices than possible in the GDSII format. * Removed the global state variable for default datatype. * Added `get_datatypes` to `Cell`. * Added argument `single_datatype` to `Cell.flatten`. * Removed `gds_image` and dropped the optional PIL dependency. ### Version 0.4.1 (June 5, 2013) * Added argument `axis_offset` to `Path.segment` allowing creation of asymmetric tapers. * Added missing argument `x_reflection` to `Label`. * Created a global state variable to override the default datatype. * Bug fix in `CellArray.get_bounding_box` (thanks to George McLean for the fix) ### Version 0.4 (October 25, 2012) * `Cell.get_bounding_box` returns `None` for empty cells. * Added a cache for bounding boxes for faster computation, especially for references. * Added support for text elements with `Label` class. * Improved the emission of warnings. * Added a tolerance parameter to `boolean`. * Added better print descriptions to classes. * Bug fixes in boolean involving results with multiple holes. ### Version 0.3.1 (May 24, 2012) * Bug fix in the fracture method for `PolygonSet`. ### Version 0.3a (May 03, 2012) * Bug fix in the fracture method for `Polygon` and `PolygonSet`. ### Version 0.3 (April 25, 2012) * Support for Python 3.2 and 2.7 * Further improvements to the `boolean` function via caching. * Added methods `get_bounding_box` and `get_layers` to `Cell`. * Added method `top_level` to `GdsImport`. * Added support for importing GDSII path elements. * Added an argument to control the verbosity of the import function. * Layer -1 (referenced cells) sent to the bottom of the layer list by default in `LayoutViewer` * The text and background of the layer list in `LayoutViewer` now reflect the colors of the outlines and canvas backgroung. * Changed default background color in `LayoutViewer` * Thanks to Gene Hilton for the contributions! ### Version 0.2.9 (December 14, 2011) * Attribute `Cell.cell_list` changed to `Cell.cell_dict`. * Changed the signature of the operation in `boolean`. * Order of cells passed to `LayoutViewer` is now respected in the GUI. * Complete re-implementation of the boolean function as a C extension for improved performance. * Removed precision argument in `boolean`. It is fixed at 1e-13 for merging close points, otherwise machine precision is used. * `gds_image` now accepts cell names as input. * Added optional argument `depth` to `get_polygons` * Added option to convert layers and datatypes in imported GDSII cells. * Argument `exclude_layers` from `LayoutViewer` changed to `hidden_layers` and behavior changed accordingly. * Shift + Right-clicking on a layer the layer-list of `LayoutVIewer` hides/unhides all other layers. * New buttons to zoom in and out in `LayoutViewer`. * Referenced cells below a configurable depth are now represented by theirs bounding boxes in `LayoutViewer`. ### Version 0.2.8 (June 21, 2011) * GDSII file import * GDSII output automatically include required referenced cells. * `gds_print` also accepts file name as input. * Outlines are visible by default in `LayoutViewer`. * Added background color option in `LayoutViewer`. * Right-clicking on the layer list hides/unhides the target layer in `LayoutViewer`. * `Cell.cell_list` is now a dictionary indexed by name, instead of a list. * Added option to exclude created cells from the global list of cells kept in `Cell.cell_list`. * `CellReference` and `CellArray` accept name of cells as input. * Submodules lost their own `__version__`. ### Version 0.2.7 (April 2, 2011) * Bug fixed in the `boolean`, which affected the way polygons with more vertices then the maximum were fractured. * `gds_image` accepts an extra color argument for the image background. * Screenshots takes from `LayoutViewer` have the same background color as the viewer. * The functions `boolean` and `slice` now also accept `CellReference` and `CellArray` as input. * Added the method `fracture` to `Polygon` and `PolygonSet` to automatically slice polygons into parts with a predefined maximal number of vertices. * Added the method `fillet` to `Polygon` and `PolygonSet` to round corners of polygons. ### Version 0.2.6 (February 28, 2011) * When saving a GDSII file, `ValueError` is raised if cell names are duplicated. * Save screenshot from `LayoutViewer`. * `gds_image` accepts cells, instead of lists. * Outlines supported by `gds_image`. * `LayoutViewer` stores bounding box information for all visited layers to save rendering time. ### Version 0.2.5 (December 10, 2010) * Empty cells no longer break the LayoutViewer. * Removed the `gds_view` function, superseded by the LayoutViewer, along with all dependencies to matplotlib. * Fixed a bug in `boolean` which affected polygons with series of collinear vertices. * Added a function to `slice` polygons along straight lines parallel to an axis. ### Version 0.2.4 (September 04, 2010) * Added shortcut to Extents in LayoutViewer: `Home` or `a` keys. * `PolygonSet` is the new base class for `Round`, which might bring some incompatibility issues with older scripts. * `Round` elements, `PolyPath`, `L1Path`, and `Path arc`, `turn` and `parametric` sections are now automatically fractured into pieces defined by a maximal number of points. * Default value for `max_points` in boolean changed to 199. * Removed the flag to disable the warning about polygons with more than 199 vertices. The warning is shown only for `Polygon` and `PolygonSet`. * Fixed a bug impeding parallel `parametric` paths to change their distance to each other. ### Version 0.2.3 (August 09, 2010) * Added the `PolyPath` class to easily create paths with sharp corners. * Allow `None` as item in the colors parameter of `LayoutViewer` to make layers invisible. * Added color outline mode to `LayoutViewer` (change outline color with the shift key pressed) * Increased the scroll region of the `LayoutViewer` canvas * Added a fast scroll mode: control + drag 2nd mouse button * Created a new sample script ### Version 0.2.2 (July 29, 2010) * Changed the cursor inside `LayoutViewer` to standard arrow. * Fixed bugs with the windows version of `LayoutViewer` (mouse wheel and ruler tool). ### Version 0.2.1 (July 29, 2010) * Bug fix: `gds_image` displays an error message instead of crashing when `PIL` is not found. * Added class `LayoutViewer`, which uses Tkinter (included in all Python distributions) to display the GDSII layout with better controls then the `gds_view` function. This eliminates the `matplotlib` requirement for the viewer functionality. * New layer colors extending layers 0 to 63. ### Version 0.2.0 (July 19, 2010) * Fixed a bug on the `turn` method of `Path`. * Fixed a bug on the `boolean` function that would give an error when not using `Polygon` or `PolygonSet` as input objects. * Added the method `get_polygons` to `Cell`, `CellReference` and `CellArray`. * Added a copy method to `Cell`. * Added a `flatten` method to `Cell` to remove references (or array references) to other cells. * Fracture `boolean` output polygons based on the number of vertices to respect the 199 GDSII limit. ### Version 0.1.9 (June 04, 2010) * Added `L1Path` class for Manhattan geometry (L1 norm) paths. ### Version 0.1.8 (May 10, 2010) * Removed the argument `fill` from `gds_view` and added a more flexible one: `style`. * Fixed a rounding error on the `boolean` operator affecting polygons with holes. * Added a rotate method to `PolygonSet`. * Added a warning when `PolygonSet` has more than 199 points * Added a flag to disable the warning about polygons with more than 199 points. * Added a `turn` method to `Path`, which is easier to use than `arc`. * Added a direction attribute to `Path` to keep the information used by the `segment` and `turn` methods. ### Version 0.1.7 (April 12, 2010) * New visualization option: save the geometry directly to an image file (lower memory use). * New functionality added: boolean operations on polygons (polygon clipping). * All classes were adapted to work with the boolean operations. * The attribute size in the initializer of class `Text` does not have a default value any longer. * The name of the argument `format` in the function `gds_view` was changed to `fill` (to avoid confusion with the built-in function `format`). ### Version 0.1.6 (December 15, 2009) * Sample script now include comments and creates an easier to understand GDSII example. * Improved floating point to integer rounding, which fixes the unit errors at the last digit of the precision in the GDSII file. * Fixed the font for character 5. * Added a flag to `gds_view` to avoid the automatic call to `matplotlib.pyplot.show()`. * In `gds_view`, if a layer number is greater than the number of formats defined, the formats are cycled. ### Version 0.1.5a (November 15, 2009) * Class Text correctly interprets `\n` and `\t` characters. * Better documentation format, using the Sphinx engine and the numpy format. ### Version 0.1.4 (October 5, 2009) * Class `Text` re-written with a different font with no overlaps and correct size. ### Version 0.1.3a (July 29 2009) * Fixed the function `to_gds` of class `Rectangle`. ### Version 0.1.3 (July 27, 2009) * Added the datatype field to all elements of the GDSII structure. ### Version 0.1.2 (July 11, 2009) * Added the `gds_view` function to display the GDSII structure using the matplotlib module. * Fixed a rotation bug in the CellArray class. * Module published under the GNU General Public License (GPL) ### Version 0.1.1 (May 12, 2009) * Added attribute `cell_list` to class Cell to hold a list of all Cell created. * Set the default argument `cells=Cell.cell_list` in the function `gds_print`. * Added member to calculate the area for each element type. * Added member to calculate the total area of a Cell or the area by layer. * Included the possibility of creating objects in user-defined units, not only nanometers. ### Version 0.1.0 (May 1, 2009) * Initial release. gdspy-1.4.2/appveyor.yml000066400000000000000000000015771354474061200152540ustar00rootroot00000000000000environment: matrix: - PYTHON: "C:\\Python27" - PYTHON: "C:\\Python35" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python37" - PYTHON: "C:\\Python27-x64" - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python37-x64" install: - "%PYTHON%\\python.exe -m pip install --upgrade pip setuptools wheel" - "%PYTHON%\\python.exe -m pip install -r requirements.txt" build: off test_script: - "build.cmd %PYTHON%\\python.exe setup.py test" after_test: - "build.cmd %PYTHON%\\python.exe setup.py bdist_wheel" artifacts: - path: dist\*.whl deploy: provider: GitHub tag: $(appveyor_repo_tag_name) description: Release $(appveyor_repo_tag_name) auth_token: secure: "TLjVTXLw+HfQYTJczXQ3SEGTE4sxkQNJ6gDJ0GGLDQQ5RItLcpGgkzs5Qq5WvFOF" artifact: /gdspy.*\.whl/ prerelease: false draft: true on: appveyor_repo_tag: true gdspy-1.4.2/build.cmd000066400000000000000000000015011354474061200144330ustar00rootroot00000000000000@echo off :: To build extensions for 64 bit Python 3, we need to configure environment :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: :: MS Windows SDK for Windows 7 and .NET Framework 4 :: :: More details at: :: https://github.com/cython/cython/wiki/CythonExtensionsOnWindows IF "%DISTUTILS_USE_SDK%"=="1" ( ECHO Configuring environment to build with MSVC on a 64bit architecture ECHO Using Windows SDK 7.1 "C:\Program Files\Microsoft SDKs\Windows\v7.1\Setup\WindowsSdkVer.exe" -q -version:v7.1 CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 /release SET MSSdk=1 REM Need the following to allow tox to see the SDK compiler SET TOX_TESTENV_PASSENV=DISTUTILS_USE_SDK MSSdk INCLUDE LIB ) ELSE ( ECHO Using default MSVC build environment ) CALL %* gdspy-1.4.2/docs/000077500000000000000000000000001354474061200136025ustar00rootroot00000000000000gdspy-1.4.2/docs/_static/000077500000000000000000000000001354474061200152305ustar00rootroot00000000000000gdspy-1.4.2/docs/_static/boolean_operations.png000066400000000000000000000111121354474061200216140ustar00rootroot00000000000000PNG  IHDRXIDATxKd]~yz౱ H,`M6HbR$EYD6o{Dd x]u8Luo:#nW=uI$I$I$I$IPl-$Ic̶I+XB%qϓ$IQ)߻^i yф^ $I#1I. X/x$IҮyO(` X$IɌE+ I]wi.6 $I$I 3`I$f$I*̀%ITK$0$IRa,I X$I$I 3`I$f$I*̀%ITK$0$IRa,I X$I$I 3`I$f$I*̀%ITK$0$IRa,I X$Iok2vlLx }C߯yX1PCpLf{"@y톬QQꌩ/NGtLfcYW1DzoV9^T;[ Xx 톬I$MA/i]Is}:V_>!--|a|cZXǴoV9^T;[ X3*p#[nDxm|D[sb i6)}4#YW5GoV:^<[; Ֆ+}/)iؾ1Wb:cy վۚ}8}3xQ)Dy hm&p6nKW̯zdpRz w<4^?jj X}aLg9aj<,p¾ ? >2ooo>ܦ= ֋65 6qzZa }A6ɲb0̀-t5!̓Å/./{vUw+lhSp<nr@֖;?Ňknpxi7ε<~\8֦+<$I[c$I*̀%ITK$0$IRa,I X$IU},6b`XV9{+H۷f1Q}} 0`^'2R9a;@?w?Z K6#3s>m/6^{˾iSR=T Ru)En1S*R kMO2TVO_bcZ"lїsٱIo-2/jWw%ˀE@:iQ*\4_~r5p~]kIה#u5*t{q.WX/~MU]TH:S{Sk->R /K*t"w П15*?﻽nR߾أUjG|@}E8hÕT|=`5`~~}Q!jwֶIM1@?/{A~'iI53mƅm6-%vI*$I 3`I$f$I*̀%ITK$0$IRaknl7~l>!;l 0^70'9Wk.I7N#V1V{̜OH> wS4O΀5VKcr7m*ˌT i؇\I?d9^|T0-w|ERR߾:2y>^)4ϩ;`r7`-qF|0 Rս܇ ,x T+gBoVk*G^ߣΫ;<vӤ2O iv;.}w xiĀ5/>v+Q\أtO}cmt5`8t>ےذ-IqzLM*T\uir~O#W[-zL:4R~,ITK$0$IRa,I X$I$I 3`I$}7q#17l{|)a[jh>ly3tcSRhkR9g=b ][I%,܇H}|jqtx΋=8=j_y fH*.l /ԎsyaΧ ?BO_)}^驡,|I'U +s=i|Hk%V|9y@9L:W^N|*?a/|O:So#w?#R>>~;%z ,7΋w?gLw7fOOH!k\Wg~Fgܑ?>?[߿ oOHB5)mm}Ҽpxxak[SĦ̊6}@:P: Z XpG]>8P{=v1t^+XO1/CLc7&;AAgڼg. XbYܝ~s"B/uS$,I X$I$I 3`I$f$I*̀%IT>w.U^(nw7Z9>Wc[2`iERIY*T$SI Xer'^Fun[5C$]HW^ iX9~%Y,)y*}W.bxI҅ XR2*LJpIr$2K$0$IRa,I X$I$I 3`I$f$I*̀%ITX7]9%޸YQ`>oLx>Z~}Igĝ㼦.u.bqf$iBG4g55pm!iG7`vۻzsIx Y<֚IjEk5A`'m{%\mSRZM=!N~A?Iߗ\!Ux8mI.'1[|;C. XS7XUgOx͌tB fI~:~c6z!ͣBrO76`3` k$==^0SPpE1&Z{CX˼Zz?2WƏR X WQk&t-vF:<#n/燡wA/cK*oI0`I$f$I*̀%ITX~m$IH!&>oHѩ1`;Cʝ6o(IWU:JO5|t'ٙ7$I#RF!ýWn9$ZRPO+teu^(,̮ͧTf:*thӋbnbqMCdDi^;;/4OK6` 0!uKⴤMORwi=`EVܮIt:ۺ cUe> Hc":n zWoGmyR7Hf"?K1/n7nژp̧UH]3Rc&/[maS_bzԘp}ߗbwMH2;هg'$IRa,I X$I$I 3`I$f$I*̀%ITK$0$IRa,I X$I$I 3`I$f$I*̀%ITK$0$IRa,I X$I$I 3`I$f$I*l ȃ'aqއmJ6T _;k;GgoȒ$I}S8uC8m  Y$Vd&/½/x`aSm76Jv\"0[xl*&G-A$g1=EGl8`s[y{hP$} _az\ʡp8eHN$B3'\?r;$Ivǹ_m$Ic)$I$I$I$I$Av7lIENDB`gdspy-1.4.2/docs/_static/circles.png000066400000000000000000000361351354474061200173720ustar00rootroot00000000000000PNG  IHDR`U<$IDATxydWuUJ^jZh F "a_66b<#&bl;l`L0#a@j$-ݭ޻UsO[Y[fV."*2++}޹s.!B!B!+a D(^ ϵ;~Y D!_rmz~agZ̋XԂ(-!3c/Q`/(x[6/ŔF`PN:nwc"M!,v9HEVDaEpXy(`燎-g}O'x2ym8vx&G^=w7`QiXke xp&^GkO~һR;łLq:Uxx t"g!HEX#,`p&SGNPvƷ8m};6eb&:`}I@,*" -\qLpc6IV]BL`7GP/q› t/Vbb{ +x ]0/pL'01@B=`16 p-,K՘nƄpSѭ$pbE:"?$bϛ0=ySt* y&~ļW1 #L"65y}l.Er`UBHXT :^c~Kڍyt+v)l,R>ktv+$bhxonx)X\sjX1#,^7ʦƧ\ #3cqK1}:/ _;UneLT (,"pp ~yWb0X\G0X uE,B ܄-y&0L*pg} !`10 ow7e>wln40#`7IŞ~S}Ij؆Y?wLJXE"wb l` l+QJYs1-ԝ-Dx|HExka{jBVx\%A,&M/+jW)@› 2m xXjG,M[eqx[STiX[43XS['s;x!]W,%Z$EcuJ|R< -I@0vg"?[hSғр5.G)0%T( phŚ[s MEL`6f!$j))C>`G7)[\ E)m\-@V^tp(sw8Fwh&nDZQ 4qEG\o%+lk?fk7G""HkJ,. +֒n&IWhאb7ϠE71|Q*X J[7# fe<Ń"e;q4'w-΋F$/R?BǗ xI*?x.z }H|)펯d1|Gx-@į<FsYA9vypٻCK~vH25E6Q5!VJL]t'0a@9;(ݘ Cw)kZm&g1?k2Ə`cl?+#$igWbv_FLl<L{lDvu!&E{; 2k@ ?^){nOaYkxx Z;?fV砀m5ˁ„CN x~ ]؍O<#|x Xi;\3y2`Ar0#J~2L/؃yMl6p 7(/X=G$7`y8wx_Tvb-+>I9 ލ\ޯg+Lcݍ;]x|yC mD! @&7y IDO08SX6f`~,x➱b#ގ4aaCysj"MXkIr l0pWC]5Ľ#~vgwX^8Cş0sUcXi.:sI,k%mL}/L/:ӊz&LieW ,{5? Lr2{⟁[ϸu>3mZ3m뱴zK&Daqx2VR9 o'Qދ}֋^-خReu3\/ Xa"YȪI8e>{:u½{93y v.D|(6/Ž4 {olX'Mn HՈE1$6l,S?v/ ^p>l (4 , NJf=oj=ؘƨ."\`6|hF1,$PquY|0{>MD&`ShXNpq܁y u&OοWxoxp0//1[/>-8(/ǒbie'u p.NabsN?swp7!eW݃i^lݟ5  Lޱʍ@,ƀ9-XmlQ`D|" > f#ğ%bl _m%ωcmLxkGw.` ߊs !oc㞣BQP!>w z8KGw7V֏w1Fˑ%&F<~̞!>>OH_@;Ȼ%X!}ڒrG\WCΩq@}c%4"~ySo$9c}yȑڧ1Js[Ǽ%~k\gv  p5 1djľ^&JZW@}R/. 5P[$/gg6V0wcHI8kj`oDy')wOAQXcZ^@c~vr65רZ\ʴ=29n¶?8+*G y,l쏓u>n`Oۉiܖ%gkcXx p9,x| p=ث/./AY!vk6lL0Hs \`oMbb8xj̃u LP9r:ls-hD0HL]r'CqjV6Igbc>1ޣF8r2x5pU"Y9 R\`(;<ؾ[̵icrA\yj < ^HTQcY`cO4"$s'71\~ =l'w]ƽ'3)Յ 1}56ƹ:isE\m":4֣ ~q !,#llsIw6㍤ktTJslwp!JI[IمFvmWP.F b7+l<{36z`6x/WBܧ+}XaƕfT;1=k+‹}抇a5_瀇!ƶy eۋo;>Lؽ\~?nB$# q?WUN`vw [e8lKr#js~OZpC iF1fqh8 B- +3]M .]҈ n@ؘ6wcϲ]q[\'$ 6M{Lzx6ͼ5&L7E8vbcllK!`x$xct1 ,O0!جi"{1$:340ZΆ9\hr؈me*8.g3@а[0ZxZ*</w8$~}ؘG+0[n\Ȋ07izI`3}} BӦ&" )W` {ڔ~y9a0w㥤?w"(4ʻ>qw%U)eYf& >,djr11$ 1ܜEurvݖB5.^+YiFc:`vn|!Yh;&|7T'g? <[B'(c^y!zUN/ )['r/fȣr-GRC5Is|$#4p7  I:2lfLErf[ŷ}44q/nYpؽ^f9 V8[m tCl톦zw-\ٜK& 2-? ?e'0/8 n[p &vԪ|Qno9CAh$ʓ.S{K=r{k9\ΰW=x/(Q <UNbq{O&z@ۀ`^k?zV]dok@ǂܝ#b ؄yMó$ϟMvlKFv։<,'w E8WIzY㧉Mm0};p)VZJH}dS%kF4;nk#5V|s=#N"ApHw &x^v!҃4EVXXB}guLs\?x| kv5 9\Ljb1`  mĚdWs\C/W0KI7ǘrD$=Z৙(\OӘ{r{Ų"gfO7b)^;=aXeg*`[ PWCُBhkFۓf\/(,- ]bXmpNaሴJSf#F6&D1 ODٛB!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!/a HMb"()Х:]1QОw׬@EF (y^B03ԃ2R|Xҍa%>>קiKEHY6B p&RxD ,!nΈ'+6 `=@C3ÔwxbY /;Cs1NKEsSy`!&.UĻu)(N$okk}k3?D`b;"Ġ\ 8x9vl( vVgGovb pTt 3+UQ_'I#`œ>nu6}x$&~gĎq1k3f9%^ \a~;}=Pbg 0K`!A*`p%p-&gaS#_;mx}.v@7;(34TdWա(?lfaLP {%`9LJ|EDw2xD8fD*pCJRxb&~M`B>~$hg\ ōE&zE%a6lb| {O( KYpa`&`w=l"?Q>M'ܺe\[q!! x^< k;.ScF pp 8p6K=MiNaUtmr7Pg`(Y${)̖wl f=fwbyw]C >;i\x-s !%>/۰T'<`+6X4D2N% ,^Oq1vA9Z-!0}{Rlb [縵*"Bx`@)좒 `ހ1V'[""\k O|IT{f*"µ` H7שJ%cEB  <י#b+k!.ޱW-SF"oފ-l+ӡ_R~S,c.JW2I,Nj]bb?*-,/Rg.BUocM"I$GXIo3X֟5JRi[g.Wn*>I$!qx~kF4棍}UgwJ|1~ZnǚN1F:&ZgMv2Y p25,ObmzsKc7.xa"Y݉NO; % ú*ƹd#q.lwae>PPvzxCc=:1#V`/e1Ig kS'KVKM\! !# =[>@=V-Zo:CJ OKƦօUX?V\:D(lV,_v|ljWcx-qlʚNaEx\EXq͔m|\+J p]-Ed=q?ero4ŅGOz 9O TLm Un}=~NԄ/>6FK鵀?b^ o3^,bo`'ߦ pxWkqnt$7W-*#w!Ǡ2WǦM܋ehcX,נŞ^]X{C+)既7\Ӣ7J`{bc׉1sx{1* Vd /Ocoq.l1.}>c$V,i+fEX7} ŝjd ~|l\nSŵX敽 i2xMp"fwc1$p;f,WXV&<_ `pFJWjxIQ-| p,-np8CdDŽb˷b i6/2]p6ظ\^Ay#5詴r L8Dxv`GQԢ\$eb]k_8DF=Ⱦ-XAg"iD?\H&V-]92Nn6|}E? 2#Es\ϧUoŠ6b#o<P`#e?a-uIGٱq-X }njw`<_1(&ВwON߀*$8X1eoY7 Xa{ Ab:Kxہ( Kk^q_ {vϣ C k}\yZrKOaj--3o/xhɾb~t| VgxSqj3-ϭ_/WઢM;80e҃q .d_c7zmlC;&X 'uq,yXm9?(̞UNAh򂍴m-pވ!ൽPޯy;Fc`gWu6 B~E Ɯ&` ~7Y =h`(WdF`т]# "~(>J +r^hv`nழ/+2^Wby=笊Em9!y"G Xu;Ʒ*Z\mw OޯȜy1[`lNq'}8pQ*WH/؅2qb涱?Zh!Dp/xkcY*ڔY'ND0AE kwq].keub+P8ƊЦܷL%[~v#^rKPApM|\'OQGYE,Iފyu" CH^N9-Ǜl^Kb'):Bxb&a5==5YJ_qҫj3nʳ4,6قnB,DsYc5-]7YLP=jl)tEk_*lAMx *'(e=!=j#s,(s7D!za}k|,Q4 _>Jb, <Q42Y&~F#ynKJEiau@xc)V hSb.Dǟ3 .B$BngBƩh%e\4zQB q9 fѸ(rvĘA'N4yIr׀Dv> ݚhB͝`Y0} ^ o_ ]u.σjK ǻ۔3߭i$#T|8XW. *>#\f[|hSQs 'U <:Y& <~X| d ؇euHG Τ{P~Lêik -/¹sT@ YhmGpyx.p(8>\ss;wQ? 1Iy)[Ezy`?Ds5g~͡kw<-nxtFB=1֠(7uc "FM sbwǽ pva[\ԙD13Z6 #%Xl&}󀅨+mA`gaG_~uhSKТw0(Gm'^){9HGAOY+#j#j+)Z@=E%+ԣAx&p>SpIVB46pTZ wkhx 3T ^j|"RrFee WQ:uI1B#~>9OvKѩ"rFc 3؊q@[_O8l݊JL"gބx0QUos|\-5 ]7qz`/ʸ;l&4 lν?Ӕ}jp OI!b:"'vo8 A Ϸ^ݔS~;k8]| 8I k ]qx/!/X,w2471Ψ{{ /XE߈;Hv 08SLi7 v]`M~=?{E54䳏㮴o,2ޯ5:`a-, d?ZE.4Mp1PaQ/,SLk(<+'xm6tI N`Qurz{VPcAQ`mk,/XTyrJG醮; j "/XTy;p:]醮'QS"EEA(lO?z 2//XT y9HG/QT?; L /XN%MG[wGA^  y;vK4~dt]:ya۠*FY8 }Q^,Ys9-l,=c"g֭H] ]8g6!ޘT^8*êi܎gۀb|2 =,Hf{o__XG:ʠێ?nǦ8Bw 3Q|U5Uq{3XşҶbVX(B,/mZ-g`lilWo{PI}샨Xk~r!>Sz# އGc=m=ԛ ގE?&lFwqM*MY2ܿ$e!,Y^rW?nm-MZ0xa#𯃚i<:ɌI,i^r6= <&4z\ 4-l>R, Dʢ;2ע@q/XJ<R ?C+^XXqN~N6ś;A6w/Pt1M<"˽XyO0 >Aso3B1G1w%` Oc4/J>ߟ|`g,f5L0C> &`5E57sP{BFLls7ZlzoX *%`" ? ܆eGGug 9L6МUo7P9v|qT[Yrl0!M}+EcbԄpа}*+0wI}J؝PwWc[| -m.4m@AWQ چy=b-VE=<^5mbȎ@NDžrmIl6xw\lSbY臱::>.<:>#Y,=좍KD\Z\'PuIBb u,jcn+-F`Y_ ֯c7bSXCM=17(Pay {XCԧw z6 K7o{WTxb+Gة޼H]kjH$`[[]|:]/51! !»>KM@!@ۯNRb`o_H2eP*#naRL :`SU50V+1uaNE}w6= hMF9n! Ǯp'~}"|"69/HF:}˘ia `gƿَy`|q]0k[4RY+|͘o/ ρ9 $C=ٿ2k,lX#d>RtS$cCS1;<)a!]cYef!B!B!UM5FvIENDB`gdspy-1.4.2/docs/_static/curves.png000066400000000000000000000061351354474061200172520ustar00rootroot00000000000000PNG  IHDRXm9_ $IDATx?VOv("@!%T@ҧC7/אHDIJ$ DBl{)ν3sg\ő<ǓpiyõڃoO)7|;ɳNC9e|p1w/`qp{9Dw(ɟg?}8]@{$s+07~.ڵv^7`b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b (& ,b ؃S;ɴ@M~1𙓁 p؎O^IZ,#B3%^{Kc;_ }rJ%~'{meq$>7xĔiד{SK#ɟ<쒼|$M{nO>7m07= 'yJ޽?0/qJ|pʭ*XǜWq,)pٝ*XJ*XIa\% 4l[y\% خY*X6WgָJ-U"X$lbq,`|U"-WƵJ\% jq,`D`l\% JPkq,zN`% +q܅%X9  -qu ܆ܔ!܄\G\ݒNWgX1L H\ݑ.WpA\X@"J ,@\Xmj K\D`6 ,q53" `;BlZ񉫅 ,Zq,Z񈫕 ,jqF,j,蛸j~F ,蓸j ,苸~N,胸,hvN ,h ,hvA,h,XA ,X剫 ,XW!`jCq2=Iv6x Oq GQ$Yˍ)õBW|1ɫIE`n!^Oچ{4%V򃷓+&8bKIXsq0GI>!.}q /osX}nhE\}9i!s󘚒rC\m%)yzh7WK)E[` b8f }2+WWcz~>S>+,h+ ,h6+ ,h+,h6+,Xu+ ,Xu+ ,Xe+ Xq !6D`, W%` `W'@`A!q@U'#n/Xp>qG\pW$vXps Xp3 Xp=q,8M\pk WE`gX2q,nѳ+?mğIENDB`gdspy-1.4.2/docs/_static/curves_1.png000066400000000000000000000232571354474061200174760ustar00rootroot00000000000000PNG  IHDRXHX&vIDATx돦}ofƉ4A UQ<H,U-Eu"+j'1v6 &qqH1.&` h0,]vks%vglv泿~@$I$I$I$I4O@Rue-~4_o!NsIj)`-2>΄79$̓KҎUUa5^-_W1姀##&{y!E1I#$ͬL 6 Xn> lg{ ڳ!7ׁwFJ;sS!,0 X-YEp%p8 8 ,UG<bj+׈ ƞ,r@+[0 Xvx)i:"f߸9P]0w o8'dcSpf'|WR$MeȜMoq"4U5U\Az E_WKdpm q'_d/y XƪY f։Qpu`Ӹ_5:D~կ-d*+Ud\ ]Ҋ$TzerSA+dҍگA-<˕#OSӔ8%0`I:M:"3!e/W)haa^In28 {?!澞^I[^.i XN3[9cDE yq}Dp:?oo]˟_v4,I欮#0g6Q9yr}Q'{%KکUu j_`?_%s$Bajis]vfjsV{hͮa}Kg+/quK!3cmVYC}>ls].K!67&˨9וw9ϥ3`I=ziֹ#DzzX $aE\6oYiu+M"t<W1Mut,Y]Km6?V|e+N.J,cj6WsFW^́k8:8zTw\'K-a:bDljd笴jQ\甿~8)rp׉jrmWYrρ |I< Qnc,QG׉g[^'5 XR You ,EQm: )Vg%k2lym3`I a4!/_~k XRm"[o#-G"$[8 2`w9u6Ƞ ,F*6WO9 ~0`&9+m$uAk X!sV7a~1hK4fbzI}4mJɯ;fR^>AQ:ϰf/YIRVZGʛ<͚KW9:F+m$iNwz5;ZkD>JsX&xӬ3`9+IډIY32`U4k Xjm$ie<͚Kf$Y30`4kJ,56XfMɀưFZӬ1 Xjm$u<ÀF:ӬVv4ƀ1gup6YIRxKK4&YdvAnfasV{p$uѨӬSʀ9+IqY24`i\܈6g4<[ Y_7xeh\Yo#Izk_'_̻ X!6`圕$ b%'8ѵeҎ m$I%ׁgۺ XYJp.HKr鋉F|H]xЀmQoqsVIIE/%x k XڄzUsV! ϶}KYo#IZD\ #fls2`i,m$IKCn4+eڕZÀ$@"Nv6d6_&^ X$5JY hIKH8&,` I X=f$[߿௠F29d]< WzVoszIR^{V X=1azIR$b&=ejSopm$IR/ld2`u6!<{ܟ6ZǀAHz WàZ'YRosm$Iݔ]ĕJ|B+@%YYo#I|xbrʧ X-V*_6YI Ljq|])NV j.n.ck$I}Ce ]лej6W?\ I곂ؓUcZbLv%I֘y,V LQo$IАy,VYo#ILV>ej m$IczIfXF[<kŬ$iV2eZm$IZczInX%2ge$I˱y, m$IZchHM:N\Zo#I-|ˀeZ\I<6FUX<k&9+m$IZcdȜMXo#IRS ǚUkFLWYo#IREЀ'j6]$<օ h^OC1F3 m$IMW!YG<1BOVosp+1guWH>y4)ǠfoJvE<1/}ks:<&,S %l_ 7J;F<;H^`$IS%Sނٟ(fB͓JnH ";8Hd$IWׁ{Ŗ!6p:g%IRGYW6:`UYm%ׁ$Cn_y W}^ 6yJE^}`ţF$Mpx.g 6$iJElx}G;Uge$I$l7_lkĜuK  V$.焳R>7oaԪY$mvT$4kV2/6dukL!.uH̼x'XS"\ IFt^\iOZj6E7"^$Iھ8yǧ XH%8xxy k<";o$Iː&< |&d`%IV!g$I  |(MXװUh?fNzIc r{zXo#I/?IxA {pU*_6$I}5Nj.n.ckM$IAUIvI9l'S&*2$I:&˟O|(,*!&෉ڰ"I!yP-}T+x4bh.x>m$IRD|8VɁ WÔ=z*Y 6$ISDyg N=iXϺ;"dx,I%7)`ee,CF;%f.#a<`$IR[%NUhy&k-$u\ I`bħ#gO> y8HI F U[p bwkDk$IRmg0 V]%\Ukwn",IkU6qZ~|؆|$Ij U~v''a;GI$1ڤ7\0C:1QAb׉K$\|yAJV6vb$Iju X0vݑ$I͒+r-&dZ !Wq>K$-&x; OI`ӼQVVZKh?F]B1 %IҢ%$p.f*n,vG$5.bo'LUjU(vG$@A(1i{VVu(}9)b>y]Q$iq[a3i}^ݑ$IK3,)k VՐHyH&ZmFOcڝ7$Iږ3[z`֩QۭU泮"^ i$Il8x14Uj ()Su>~\KgAk$IhSg_OPLǩ*"d/nI4&pp !k$I}T],ߜv5CUIe -R7 .2`pX $\^,zA1vߖڢjAbY;$Cu :aiְڝvG9wZazUU;:kw$I `?p0ow5CUfƩvGHD:<ޙF޻Z#IR f:DYZkw$Ij?KpN;ݑ$ b[0hkj׆7^$M^p pou5Cjݑ$3r㯗pX#IR%C=:X;T۟u!p8F<|$I͑7?Nd5'H* 铃U9uXcPs6Hj28s~-ڝc 泼6$ir p_+Š\?."8zϚLR$M>wbO @Z+AjY$-殪 XK4vg$IZ]?Vs!p11$I]?V6u)i#lq>K-mABZe>l,If*VC ʵ;lje$I۳ԹV ݹkw$Iڎ]?If'vG-}ʀH4]? ;$ҹ'vGӬ|ɨݑ$4+2`;$5c ݑ$1a2`u;'W 箪 X4E:gI-'_nKp::JSjY6ϛ Cjwn ֮u$E >^/_\õ8|({Fz4Ϫ"C]aJz-j%"\} W+]0F\A׀Az9\"er4 X=Wݹ XuX>;݂ $x KDa)C:TkwZIrpu.0mejUfiݑ$P^ppGg`|Z'M\ Ipu16}}O4;% F\+։ӉwU^vG@'Wٮ!ˡz^Neb$Iۑկ| a׉:0_> +*AݑԪ XZ@n :B ^g)IڦޟZU2 jwn$ĵdZ`JU:/]?YO!"f ZZ`R#:lwkW׈ ϒj ,5ʐY_.rS|AϒUj ,5ReVv:|ݑej Xj;nIZ&Oɀƫ]IZOv֨ukw$i<Z'Y#I՜JH\gOĀVvGQOĀNvGf.1R~룧VsbRgX#IS5[)Z3j Xkw$iz_WZ3`ݑS&u"XmS:I=6m:p X)jw1PRV̀^Q-blj,kw$! XZ!b>JbZ|60X5KV۟HePY`P,ވڝw8% V gJڝr֧v,I`j TSY밖) Cjw6AxCe0XK<ͪgkwAIdj)4FmVvfjݑ3`ISRs{H}Lj+{+W`(HQXWH|~r(C׀f`Hy`}xhЕS gÀ%-6jwq>KjΪMbg{S <*OzC,iIy,9nW=7dcjwn jw\ ƨUWVzbV^CT\\\keU{R++@ƀ%Pڰ|~ڝCğu ZҼtgvĀ%5ڝ+ڵRvjkRvVij,AF\|zkw3Uy:pTwVmKjZΣ Hȧ9P%"H#BDzaXt`]33`I U[ڝ#A~y@w1OIRb]} 9=,afuBQs%pkw}5 kĵEkĀ'_VW*i€%ȐڝG ,{]"W&0 W@'T,QM] 1"XPD^aҊvG-W_0˵_^뵟ŀ%u;j3+Y Tj$~y}B{^c XRXH_oV~@5LW ൟ%u;ZT?y =aj/q^.oM q~"qhFNKĪ'SJ*Q,9LUǝJ)ω'X'x+sT`ROvmbYT]Y9dkʄW]R8G^2`I=4v*bbb>ڝJ噩1XO{T ?uZ:u XRO yYkDвv=_w^&NNF:Ò%\e>+W>w p 4As*3biP}~F b|Xs0jS)! XFu,:`k55fZ2no ">Njx;R&Z*_婔$,I ٟU9rF hSհk,|uwX,hr㱊Wʷ1mOu0Uzu^S ; =U|կF9_R S'u_ϧy?:`\pU'5K;nekl]!Pyz}-hThQ6:e9 +F W[O\U% OR ⴋTwN:(LI.z` wSU\J\J|Y/lSP3DV_~LӨӮvFyWzr- ։/O i< pRrFCNUF^-哒 DHT*Y3j[ Li5&pj-+6H$I$I$I$I$i!ȩSCIENDB`gdspy-1.4.2/docs/_static/curves_2.png000066400000000000000000000463751354474061200175050ustar00rootroot00000000000000PNG  IHDRXswLIDATxydgy"vIUZ/JB !mdf0gKw u1xӀnt{h?|@p|}}A!B!B!B!B!R ހQu/~B¾Jo7Y^`N'~fI"L'Bpgz{ !&+= !NNo9(h?^C^w0KR13v7" msBQMJ%≥CN;*lV9Lpe{ 'a*J)ہ B3mr} ^{!\*΄B ,-!?+kt "hY|iY` vZ,O` P]D!1@l:m̄N&$LT`e tauzg.G$DN$hF O瀽a`ǵ|*301m"ǰ"J}ͽTKl;7Ư0$} !D ^؆b'&20 Rru*;RU|1z3jP1Quۯ^R~.]P* : EQw_0^!>cX^ :,"yފqo(V-WbQSJ G0ayL1d׀OG/TU]DŽ;2B0VWQ0x 0aU^? rYU۴S n)0qR?1l_Z?^!V"VR7O Qqe1 qJ <Ƣ\Om^Nwq,?U{Bxk\|)- _BLo=~-1ˣ|+%X"/e֤l_qTT7TG 8.|ce !tOW99BS˥ Nn#ix{b,u( bϩ8zu݈E\OQԸ XWA H YB1"*_Ʋ%U,Gb+٫ t_H:P%`L(.XB10i?Bzz>z׶8O7b~|GVsV ;bՅ eޅ?.b?` (֋% BjO\ Q.k$p}_~,!8+ۋt[G"ĚĕģYc8B&X4ԠoZ~7`xˌ5B^%*%RGeU?]n>9Lom*>Jg7fB4Z9`bc-ow3A=C# ^12hJ !Hkb0bޫY,-x>p[[-soʿP[!D K~.9UU(5Y:mE|Wj [}=rqu5p9Tՠբ*KUB:S loƄ:l-`3EnpR[ |~?$(%T J !BˀbiV]]/-oWRzJG+!wyn"GeGN2:\m]RIϫ7ԠugpuXT$>h`9`?_f~݀y6c=neyz TCQ!ꁋX:!=Fۼ@ĄJ:,>XW`?Ў*p+4XB Q,,B1 zy 6~&+~x د`ĢWE`/@ !%md2G}IV G]mLOM^66cYB4 ;G]э6(N~kT|8V٩wr1Zd\-޴xjP1}u!M?Fl=ZuTDKChiB$QvZ\ ,j:vYt6-7~,]VŽ.\B②(| 8Zd :nl&:[FUJa|C0pK*%D}YNdp,ⵚg2F< |16-`9֟`n} ,!/`F?u!FɢVfš~:X{vY}W)yxgK+$MCrD_RtcMGW"%DY(G:+lMPf)/f8 8 Q'-é`c\FDV\fic~GL-øXa QOw4_.Lkڷ_ߪ ,OoYzƪc}e҃Bԗ2X_Rq2̓rk&#w};']Xc5LQ,!_%np*BIRcWcQYk5֒!PAqy-B ݏ҄BԑUweX`#$vdQ\]udêWb2^5xOPQ81򦣷k=aU Q?Y}+bJX:p h |1V5iQ?V b Q]<t7E1ܻh(:)ߢ\ʣW%j˱=u;v(ƣWء"[m~l,q8waMH`ubmEQ,!RJQh&*q׵B+0 ( x 1zUh!D'ʠ{`9{[8a'XZI ,Xg(U]UK9R] x |XrY|NkP#1¢XXx#h jP-03v?`' 2A|M:5pB]֤%PEU"R@ tb+7NUZ0t 6؞2JXwu WѕMÇGqu=ֆa[9$$%E+؛s E(;-Um|B4$3z ob`?"LX1s QNvU{yJ+!*Hⷺm|7U6(֟`BaӱEƫ_] Q1 C̢<7؏Pn, (m`3"YB eXo{r"X* UX(YB +DV#D39f^c!/b f̗U^:.d7'ݑ"$)b'VU;n`?ʳl1b-Zȅnj?nHI!*Bo{Xbr`?-Ïu 4VU4'5?U q%D%*A~D` ?L45pՒbrZ1ܮA:B| VۦZ(m nʪ3UF|laL\c:Í7؏ ,8X5(](r)OĈqA!ʍG3xjz7؏ ,,t dinKs Q5C?Q/q 7؏ ,, ky %pxv+!*Fbhx0vL)ߨyO1̓,!#W q%D"IuQ11'DV"'K:WBTj%;v&6ѐys "L.3WBT+0A_͎ z 2T\ f%(7qhsĕG6Tt`"ﰓܴIfs$(/~;TòR4{t>[^w-#B} 1 ܻp!p["ԡ]*31/.uWi? [gvLU`MuKfJ;2IAX%έnG13[\9-LT ot JwJ/1f~ T>!B1ǁs]xTJE\ iA'`ZA3;)r*c"ko(*KҁsZ Q "[3<ZrnrG2pL;1N,Dæ>[pb$Dɑrǚ/J#X)I4tR,9jfا>!C"6J3:YxAez: ҈GB ?y56e>Zk%DH.a#X[w|A:EXćEc'oe8:feV{<%qaynr=ZPVJ[LAKhisQ[_GBtWSMWm,5Q䀘<6~ZCCn/G*Z{sBs%qeĉ hWw*-7k3p :LY?Zb= a +yW=W*R  )q"A1 1Z\X-`~5' +ZiBy ]F`9]eXD,u;X4P,Vml(a%Dʼn璀7ˀCN`9~p$ˀct!bxj0決$3ifny5ۨUO*զaequ2x36'Z(u(KX}&V- S\jwcǁ5(J#w8/(u"#,Z+sEc ܼ+M_|<<59!Xa{ ͘"?y^"7t`5 =Z4 <.a%D=`6|W;WssUI\q)ѣu64u@7o&wb@J:Rh/ƞ#*[U=؟z#bo>̧9J ŃRS@w]VBԔ2$,pwy"R5Yj`-P 絀-1Q ʱU Qgj`0hEBH)[e?7>Ӱ䏀ox!L EӃ)_-Vi)IG vsXKG 4,u!83M휇;3ib2s~va3Ju!z] x@ZI $N n]QۋXZ-R+'T{"JOcM@wyBƫ ƶi!f@>.I:g`bӊyJH|M/MuvE~bѩ뻘z2N9Y{'ú 'F^c;amEzF "Xc$pA2V\Ec.rҊsy0NNOt|\D۾)%2S^ɳ x3+̆J<)y6^.ѩPL. Oɿ>e:^OLTZ2B/|Sk8$JFAL ќGua3^0[P;3FXZcfDٶ,uN|6N>Q-2Z.etR%cbpﴱnJ>;3gB)1ۚ(>އs*UWb#>>İ$cEaYt3<XB,Pm.L*Dow? VHlw3(-EñN-C2Ry:-~~ZbYOҐKJ.ra}q^0 ̯<>:Z%dKh r S ,!AjpKWWYyUP #(O, ,!z a580ہcTnh~W|W\_zO$J7<"#<Ā5k j2,|r_  R~-obzkE6ަ  I%"%UYQl`y1ͪ]66y_ / ,X$*Wa'oe5KQ+U#仚(UDK4Lªd: *)U仚^XKcb H`F>"aUuUGdWf &WہĢ1q!qUwRocf-je="Dk"jIfW!b *9^+U^|!a5ڑ0CZ0$Dm)v46qlɰsEr t7uDhA`3)~85qw5u|=l4JAq3;Uxi D!/F?K}DKD\|&̣8brb~{Q'e"c{iq%j__&Xt̚?8+]Iׄ|X2Z,DVA ,EXLR \YE'D;(8hYĕ|pUb-KMeaEXZ(DiHcnQg4dl/7F %JAH:"W [(#bhSH=X++Q<&0X 2W p4H`ҧ_Q&ށ*RMZZ 8=(B,$XH\܇uE$gFDv#@ XW9qH;sWOp.M߯E-:kDK_\oJ~: z17vVg%J,/ ,1R s9*u!a)^o {IsH`%5$ V*zU{"\&XbSV5(zU7 kH`ԠhDIXHWP.8Ccb H`eԠhd"pwPWSb}K@K K15xG޸' `&V)^53>8=XCEDEC JW+Ί^5}%DK RѫoK45zu{$yr_ 1(J &+ۛ\ޏ̆>kNbQ fhr* WA pth$D_ &`mY~ 31L^af+ףh`{0hƤIDUĀD)z.C+ kt7e EJ n,4H`x:$Y++G#k^WΉC p3 fXz^5͚}}r_ 8 E+- ^]@R>m / ,сR,R`ICyj`n"J 9#%(5(J; |:DqѤWvsy߫ظ<ޗvIJ >`gRpb Ȁ`yd>p|l G EM}W7Gq54qhk1ߍ(Ak ,PjP4r>”k̼6۱L Ԯa0$D|;ދRgx4*";ɰcEj8^%Yz g+Qw& 6ĴiAvIZ];4X¯Bn1!J`E`+pfhVzPtkX CU4 'T5?(6-,`x_X %]\<'4CՕ(=(]_~nR $Lf XP|HZ➫ ?mp;$ѻmiչAtcti4i I*J~v*Dp:l<;M+ wX+bŪMAižH`5VxQKоO⪃׃˱ -.+ɑ۷?@vQ/|2\Xa$bjя4 D(0CB.xpvu}RԪ:Hz_Mp؇.`X[dv=2+}ZN8Qs; 0U_ZXx@ l\CSޞR"zەU'`8:Q{DJ\ª7~dX ,&.؝>BzMAvQ'qü ?m{X'q\Fqͨ9]Oc]e*k=pz[ _`]F㈥c-fEMJ6EEqQ5-0C, p] `hhtZKؐ Ms$jLT-`"`+&i(Q ' Ie> $jb k}A-L0E)iqތE.Qہ+tV]T8etFɭ?7`"j85ylJ}u%Jѓl:Ny{ΨVsBvy!@EqoHngʫLvc !Ca4i>t%F&Z`I\MkOEJS* ?&Vw#)'_罒]N?KJ^E6q @nXEXLkAS0'MoWcT(*^8(ůP\;#iD˞َŵdZp \T`bf7Ė i_/\\ؾNN_d;~=yDi6S*@;9Xܳ]D_7wN|/C\#̼qgbAq-f07Ѹa ;p&N^@gF}#@,&ژ ^Gu(RqugZ`[v+- )g8G+1>R{.&Dbl*줫t)fHNs"ii!Ah!<|jJT'^ B>>i4TxbvVj:Uk00c#6yax`u2(A+1 2|BazW5 ]_FaIOTz?4"s4]Z!%$Lҽ}3pUK~ej]$uRjUY:/'Eĸ}bryb}5pwvA)h$5NԪ ~Z / 8Dz?iKDKӈaA]bt1?Rڀ"9*Q  yX!&,VX`lb4,&n~( HTR_mA녘.U!dlUY'ue|"EVdl DZc`rPJT[C,=(( -lęXg;K:>vc,.jάaZL7@X@1Oj{W'IT:F*^7`Z3Ĵh]"ؾd!v sVIOuDuėf<4SIr/ma^mXn 7_: 򥘥@+1Mf4bV#cI}Vgc'/_U-,ZU'LuTl\z1mRkQwr#cptY}b4u? ufrG+1}"Zj>Y@>Is3(>:g{fo.Ubdlcl')h + |he@ pkl\KM%Dt`Y|V Zʨ(C [^_w"X%C9"Wk*~*y|6>`ywWHt7MVNŎh֚"KZT;tN$5x5ĕ¯7cZEDmV i'DL fOFḸ:ֆX2?M4\PmxU VfŝE&G+ov&$\\|-gq)ĸQ-VI(5 ˁh=W9Y+!F*EJCe#lbj(n% USW#% y9NWo%ĈP :H|;r`/H f+!Ɖ*Eɰbec@kLdf kPwqL-ZV+!Ɗ*EhS4o#pXq,6vH\ 1TA(J||2j8X4C\m>%q%DP2~8x(yH`luSԻ[{Q\}2N+!&*E] ōq$#F&`_Ex'pyj8WBL U:sVKtdxp=$M z&$ u >uL[*,54mj3]`Nwӄh$ UeXKdycZP;8%|xf8(>BTU:au|#PnĕSCxސq wQ`/q$< 5=@r)P4G.&$ u=~b`b$7PԠG.@J2 BQ|?],Pᒈ,6ǪWRK\ Q:TA(L=Z[$Gn̥{<n߇bPTy`e5RCRH ^u=J\ Q*TA(7MX9XCT 55XWWBU>@ӕQNW.DP2>v v9>XkVԠW eH\ QvTA(L;zt7i<> ,swc"`=\A!**EpYP/%cU%WOQ] BQuV_6"$L`R(>լĕGc=YM<;X}vL ^P)S[OeBBQuJdJ_TN`-,K즳 SdBɵ 3Vpjc*@)@*XH g7`L!ĨH> Vu~ v 3(XIJ#J,iga#d`BL ,#!2(X1.J,4L;VYMa;BJ_ghU嘪*YJ:XOM.f7 !pF)WRӶLj%05U`ڨ(ێbQpa߂7F!X%I  " h.q2+'?!F2QE j; A%"XSN *#oԶD!T&=eM](F\P㫱TQ̇ ksTj.XSJ vUiW6*!DRK-nvN\f  bHP gk )^j vqV6jBjF)EA~>[t߭ 9lyx\7kH{$f:gk )^Wnp QB&D&_ߝނ&u yd&n|],8,ZuY.|[Pe\+|)AƅN aU?Q5C'CQ!J)'DjsV |N3x,w,::B :hL\]\\_"Y.ҫ `/y?#BT /V1HG1T|T]H^,`qP.pFEѣ~#K]QsӸ]t#NKDi3*fx[^ wp f`&gþ)ݶ .0W+9'LGߍK 1}6yv/Sğ~σ 4XEo!ֈ`0"LN0T,O!_v%xPa&j5v:47IAP["הie8HĒ$4btrz1bNlW@wڢ?mZ|da&!~'% *IomGʟIhc1cfҘhlsl}%*Iz]ӥAf 3i  $;{ۓX[i-DGy-18C܁wँf҈[[DCDm.EidfVIVj$VcYIy 06IcׂrVݾb:f&h4zl!q{]0,#L`uX,OQ90̤ &6}<2̤ LS4d4d+4z@aICfICTlF)0'ދҦqi^q6tjG駈 {+2iXIn܇=U!ȤMeI砱VFbhъLdN XG4 z[ZF0֨iD?%ifEJ [Ty,I#(Li1ܢJ9LPmuL4F~A|J4bŷc4V 3i뉭F;2[U=Ml[q#Jiys,ц?Ë1̤>d/7ƚa&rt+mb̵d3̤ nn 2%iĒ5eobӇ41 3)5ecfI~(^\zjl M 2$ ANbq|lL,B߅w2qyhO04rwx=ʎ[Wi$,p!p 0ÍZ^"*}Ydڮ4}fU٣`;VgZY {%ǨY*kn&:f\ՙV6C 3'V6Oy?#DZQ?ݍ 14rp#p9p6Sq!PID~k0ӴCo"֔Uf~pQF{704u _L-;C]P;pݙfy+vOMs%UgE0:JŅձ D ྕ204rnW0݃qt:)ۈugvN:O^Qxf*DeKi^rF4NK1:<5n!il w"LލViWg rԨUYiǿg+٨,͞gܞ+)u ]1Rr8 |(]-0&["k;SnaQ'zFl;3<qzxuVlSԎ+_N5A*0lBc@v|Wy8{B)u/!'f'4Tu?έD!Ϧdj$|jbh 1~Txk^`-N ˵\^gx火Kߟ>:Tǁ|![~o3G.ż>>, qy*& \c %Rw MIp@cjav( ʐBE/ƪl}.Qbo۽a9zd9M2Bta6n/'Yvn&<."*?nK` k o?γiP/& 3ZcRs_WCUéyMTQ΃,\N.Ək-:ĸ6q[yTڮGnlnlܑ)v cCZ/LmWCDÀWMu/{)- 1o3v9ǰnl_ƿN}s8pZ"{_󃪋> s[CLmV ֢CT_scwz.2f5^Hr$bXS9I+rEV^F|ٜAI`ACYc%3DnOpC$ڪVa7=VS+K{|6zyI3V5̮ԝ=v{VR?g%aj.>S&{_OQ&a)油I|SwX<&ڨ.G%8%*||M2LmT:\,zc={2i"fjނVeKu!b&a6j6Ru2b?fjUa V.#~N91f ^ͪ0F9Da'b`;qh2aVi"{2:VejrU{vǤgIgG}< 4=)JM 3 {.i3:8In!s;ǤV0*ҘIpxOUIHfr"yWܳLjQ?Dw %3|x'J72S5],ͳľ `$A7!ZOa6^ ]w9+Q/v"3#|.ssg9Dz0ʟ6_Ho{oScnc1&S= O/8yj{oQ5Q;7?3gݶY"K#B5U2&Xπ_$k6?)CwHp',ڻRj+ɖh\TeCW_&!lVʞޓRh5 ɖcۉٹH۬?gKpLmM3x2hmV1&$}q7#K=eV1&}n2ܫI-fjLmRkpv#[sxAykU 3I.}#֔^jaO@jFޥ=NL2&;U 3E'}n"7Жr]Z0+fz[23cWμGSawQfh/&֗0SW&]Ic[Z0ӤC 5'~:Gj%LTe9ǽnbG?XwpOD(&Y[OsB-[qz'p_1U<5JBƗbU^G[FjL*aw7Órf ?'aa6q% ƚZ&BjQ8eY| f9/fj%O{ۂ. ^\Y/X9_24C{ y8_27' -v-f eNY?F 7o'8_0Fгe^'eA 8pLSL#W:): nMlpyd<|ٗ˜jF'p)f8=l^\Y9_aiӔJ"Bl_GYdZDlqLSbC,{5=Q5R~O)yx1x yL144Έfe\Γ%/3ק}>  'Q]TS"*8_iqaVO)z2옜[$u.Lj^b {ຼ| 4qK0% Ëu6VpiYb7f 5:<;~t/FMUìdo AO<"ޞ'QV݋bY1pBmނcemr=tfcqʲ'ڥ5HJYYQHtRzjc ^[WMA'plj;>'swai z@X޶{M:xeΟB+۟M*T+pOR' /^Inuyf1AҜ 1x[_m?_',C=ܒ0ktB!v?9JFW?.>\ʬdSy P7 ;UJaɢP;gkfcu0O)SJ6}h-wKƩ >BFM“.4]/JEwm |Ra&Mq5qOct׃JS0&8aS)3̤\S;a&7הI0̤2i4ꚲpM"L_uG)VfIe𱲦̊LZa&< Lq[fxdaӇ4LX`mVfIC{%8^v0ȤUf('dIWɮnsiF)3n.A0f=]0;lZ k0FgئZܑa.]sj־-SIN\(ksn^?3̤Ѩ%Z?a1<V.'%Wv;/-ok!»K&^Z\eJ06_# <?<7Sue ϹX销Bgdcǁa&m޵dH:rT7q_@>ET3Qi'^g[{sه^ϼa&mds,]KA3OѯGT] iК;\8CV^78J 9^ KSfwB٦%UXo3u2|W]}ĉh+ɰҠkeȢ:06^#N4I ۔Ro-8_ U"Ķ?C\!ϗ/)ߪmC1/$A 3ic%¹M F#GnTa:J4 ~U͐Vn{3.KVV0̤S[k4{lHTao+L:w@a7z4W }*LmU.Lp,wwya&Oݚ*vGcHSx!piӴE>D fG 2?Vn!V 1Mzˤ+L\_1Êcb;)']/ v{ݿpVa[qј YUCgɱ/[oI 3~եÍ p-fؒk_<66ʦW{-# 5ts!|XFm~Xwiތed6]9DL`oiR;3dw$t  D QNDH:{&΃%{7~Tx=oOeI1a蔏/^JlbwKsz9'6] ѫbkȪ4*[ƨ~fft9F1s?hgxdu^ewbOoյ݇d/"֒;BK#0'整dKNsigoTu}+ԭ'Jx=n!fi9̪qz5Ȯ&Zs4~Kc4k8{#i<4֒41lƂa& f^tLFǭ8;1bI+h4|EK!LZF'eC{C:G&!LZLYVcthsWi fRy[&>ʤ1eI=% DÇA&1LZnbbsӓxHcTjw5#؆/=LQ.A&9L*Hb#cxti"fK֔ <Ë0̤\Sv ךr k$ 44Ë)S"XNE 3MzP؆kʤq Σ}Vs4pj1lQ"{KQLӬ_<;'"iE8^/fRC'\ -<^0Tj7ǃ4^jDž˴@_vlj>ʤyR 0\f:u۪ 77YaE𑲌LjFD$3clŊr[St3ƏˁpQG Leɚ 3MTo! Sr3N+-{}QIpf LSq16~lJ]cͷ.C[ /CΑ24-=^IH(o3:xޙVbg;?Ie': _T ʆ9y8)]PޞL/K,Ts&{[!6Pr 3^cǏ#!;<b"b!b!_])w9 3Mr݂ =nbɥ!( [aVK+㹚'l7q[H xѦs0Sy *uJ;< ݡm6a-/NcKz >7Ĥ1vJG{߲;CW24. 3V9DKu:bj>`;pX0medsߗ۰n4\fj:?vd{_jܮjLc0S؉5 dJ^bW ;WEL0S+Dq1,KwPW"%nq ÜAIajv:dWMpWΎyI3V5̮%áEM"LmU=^ͅ.w8F3:=/NkZ]|,A+YMpQJQ] FɸYbx.Ë&a6u!-'X]@Hҫ[ aOqdUIf.۾S}?i( 3|vw|ߪL]dMw_]2.V[xuJ'vaƦ9|Lma:AX&vԪLmb j.;Ǥg:K^w]V06x]W& &ڦ٥'뮳Į(J4mgf\}ՁcR+fj+_աFUn2+[gٟZ0Sk"/:6GqZ]maFu|aFT2a6w&McRkfjzw'GD`Mjx?qQeMr}D 3Z!Li5 wS*S>bUx-|A`ykU 3E 1~7pg'쥶 H0k 1>؃;S4Nj%xBkVv1ZC/a.nCZ, 0ӤUc%C^-fib/h[hjnecRfd*[DӇ խyH0D*M 9Isжj%vrUf8.9wp]Jf?'aI4JZjV&J ;ABTmH}[-fib4ɮ Z0ӦE9[|rcq-d땉 YM BiXe,C_ \DTb \J?H&밢'C<|,7`{LSk^H #^LS+1Cl|9Yxlq0i:a=tOsD 3D%vNmfR*CHeE_&N\'(˚.u(vuDh=X>;f Yb[ ;Ĩi0klZۨ_O@G^u֓=o!^c]sp! 6"m5YO5GQyz-SfMËǀ/ժLSa0AV=Q.l5o_H0_FNV2 Fm~GTbNK!ӝdYcUjF]ZiccJ,IzOk]2HL=Uĺ@E|?4V ܽM_qOq?4u zuc[fʓK;?idVRsG7 v|fb"9.xJ"V\giB j+)nx𢄍$+;|E0&C.~Tfkʮ>Veb4knCZ0[c!-aI5e 3iLa&'הIk`I5efi6}2iu4^fScƪLZa&87Ȥfx ǁ8>^`I;_O1g݋` 3i F9W&hAt#Ю~ x MmE,9>|9zLi Gcz=ĕS…Z=^;<`VaҔYVU </$D^=Ӏ._EJj#Z w¤}stw$aRr!*VK!ig/1k[ a/ ! ,1VWSQ$:y\@%0x! ,1Sx2j/'}ԭUMUK\] ),ڹs #CK y$(2w ZgaqZQnC! ,1"5XfWXul,OeBjH`UmFj9u+#f !Du$TRZ] |?p!fYU7EV<B ,1UxYK?waV똎(p=n4BnH`dz6ϱ0W<V֬y\.̚["K!#%Oz! b1HrXV5,f)BdH`FSZxv_ogsYM@,%%Xd+7olCAc؎`u~'Q Ⱦe|?s(%h)q "$a5^es[#,"̓BX= d.NN^%%B2XV?DB0ZOf}8r !f ,Q[wъ2R+I,V ~HeB"XjXa惘%Kª.D'% ?\ ({Xv |P13H`P d 7Xdj"c~=#YBYAKԂKuCd& O\Ge|BL;Xbq ^+_h>_7%&1d[%@L &brpIץnBYDKp ?V*K1H`VsɊ!q|e!pXH!F"5Z%8kkl娬XBC1XbDy {~x؏,1(<6s !T! )%+X:g*YLdU(*$HHV^{ߎFZYf<J | W5h6 n.R'g> 0ЃkUpVp~\R}^eѷ%1V?dJty@Vbn6Aف} 3pDe"vO K4_gRR݂Yp|lo_jL XP]a>=3?,kAT2|>(W3 @+﷣:Nӟ7a,-Su#oMݙ?;^1^`e7Y,7Š3B Npӯ* /fxj?: G3x\<Uo $ K~q3~> sWkL28 8 Qb7FfjJyDTb&K ܛ>X'+ZIx`O;J,.gmY$fUc.?}~)|)).MY1.,f?:l2c۱{oKpXf.nE| << Α;Y<za**]|N53`j&w/U@,З݊.1/*Ŕ4^`;h7ؠ,,2\@Sg" -[ 5lp4.;*zv>M!bqLviYS|q*:^-3&&ne)V}p=)lU'B|2Mn<6V\6鼎%x3sS,_Ͻz଻%1}XX3bQhq߅=;)H|b;-\'bŧ;MW,ji0x6&69;E~|)[ʱ^k)bY&`nJN9 ufK*P mWjQ wnY.3)W:n/]a"avcRDeDRÊJ`fy)Jh!&0Qxoة'+-xctwEX!cFl(ANʾH8y˱yq=}g}bThi܀׬Xz֕Ll\.&2s˭ZVK,dMf¬ *#*%^p~qVק}#o;1 r(&bJF>?^z64.܇?֬ 48 F3^) TW4dৱ`Gh^"vs,+Ei,x2v%La99y}VF>K˹4<W^<o:u18W}x&ft8 &*23ZDgմAAL':>`w]UNgiS߅ly3:{5 "'ZseʭN07ٗ0q4,xe>$k3 LYώ2~QE{._q2f @lZ!AkuQ`j=#+x564Idk [94vX{nYrW6,?bʗMr- s?*vV*LFpb1GkDV@l~ S;in.SV|0z>b,R4͒ ;^B1yLԇ5xૄ7b+O``SO;!d.VΫ(1p?`v̚u[yD`SX.&ubzq&#`Dx*_wFTuڈ=_߇qD}f,1S5*M`e5اh.KBX@k,T71S}9ah5H&?Y^V3Q*aЋn+ewc}gŢƅ_XVfʽ0L@IRv _Z J?c-awl, WX:{/ӰvCOi1ڹx9 |0*'d;RxZlsϭ1- r֏s&} k J81/Vcb\Omb_=T' ؏\cLQndu̾D@_k6Ȭycx}> 91 }C磹^<~(S^=Xɚ[U <-EZAΑӱ;5Ŋ=u 3J\qlz{!e n9.g)u[#/~%#XK6YIlT=,L\}4jA K9h˰BSBYpELlY|lW]K}U;FOL4 oUkK8)"ؾW1>v6v<k>sdI:˭Y.4kD{f` q +1e8Z=B;B9ĄƓ1~dp>*E凱Uby/[g@{oxP0ՄU(}v/ޏ%|.2ڹgmEM4+ׅXgaI {f,\Jv.OR%nc {-py uL:lz%FGޡ Oayb=C:Iru1֑*}E``=Tq2j#\bUf(V*;B*DgL<@>G>c"w01;\u%[aB,Nl%noSUaB(۴  ,G&} o~ loBXL<;;Y- AWX`bj`֩݃?O E]\]|*GlD^woybы](X@]4tlӰ|P2҅"抝ǎovAyҏ. 1Xn@?13'O𧡔zAjUd 'o<O!b$Ul`{LڌܜM<&i"c9K┮}&΁'MvvHh]U9ˬ#[m[y^ުQӗ2Ӱb b䁣'&̦]4şr4wӱsU-|f)cBh}zf ”c5 Ŋ Mi 9\<>7_f-b?MzS#>]PYS-]bЏGI{8u̥ƾVu/+xXתBij7Ӄ~aAUbp ]Y0 &oKsY'qL7a!~#}cKM}b귖Wkͫᅕ|[O0ϰtnzLm)E\v=og'燀61uhyfZJ*f0uAο!T,M郯8U-"#][CʬWaK>]U6SB>pO`[ZQ=%0Sn'a%f{'PxJ6_&<7$wIBˇ㮞x;bh7 z&zj_{0ko^n'cݬ߃J+} xw,j3nO&e.n"ppM:|ܘKS0Hq=Ԗ:XX{Ҡ)E $wOۛ(e[j|uFY|)7a E- 7u6.lo׸ =ύ64bV h>Pzcn[ŪCVa0OAբruRz=_Ǟ]yXG\&:(,ݬYy/hynw;qhUqwEqsaX0Է#|f a{E0ab!.Kބ-un.6iWSW}b]ǁ 3b5 #Q`2o ˻ l~~]utpW ۏE_]ߗ7XF<tP-\^/Z7V&<ضܚ [X]Z-0O㈭t|xYMΪ#~;3O_ %Un<`+,`V5zu^҂i7kX*ɩہۀi2KXM OĖ}%O8~}PMzD_&wW>Xi?NޗI\ `}Tt\{ OB+/w\CHޅ:ځoEs3Jه7Gȸ<&UӱA)e|"Chh P+g`ȥb: H}>nVͥhI'ct)J7,_٢qd[} ԽP1VevG+\m Ew zެ훰:gbGjE.<ǝ3M~-:݄G(bܚu;kQ(e&" CGӱ8J"VKWf&N~}Տ./v3vB?Ty gWKUȵ^l;[ EX?n^=m۱i5`El[1v/wVZjqeLM[dHwU^Mjy"\\]|2tɗN?|pӰ۱{9ìߋn6=:)0ăBEkSbnұc`W4#z6xwRcnvgKǭ^a:R*}Ly ֡ ,u:C;wbYv_.Y"#0J=J w\}1tZ\qa*!.6+1}XޥyJu(`),DU+"|8g;*e*[:] vm_i ?Z<$UV4C1J/ܦ\,/5  /Ίx3? `gE./Ps*V WShJ`hXMz jW&lszZ^I}vLb"<@U{ǯ]y~%,|1MdÄ#jl1'mwyz|>~?0^\i&MXZ_T!UJ:W]dge1Kphwan7&.MP&MNZDn,PJ}ΪF=-${q}DLS>[X5aPa_n>>DKت&S݇c>5{f5b Zb _w2׷WqK[ĬW{iXq]7>`z(b}1*XcEW5]~?:ZB&sY'ӟ@ja\;J+VFԤ .nOjI.A!F?oVx+v4 =chbi]:s=*bE[j=N֫l! w>%T}gKBaAZ_`EvƲ1m,dXX,1͌Њv>Kt, m!x[vIRY|F؂ufM%Sj7 ЌS39te|5]"b=z1Bz[/̤|<}|듅oS2Q&|K#\q]myo ;>b ^vdz:~]QB"UZG]1-ګ1wGbI6X#ꅏ^BmMwg9ޭUvbd; ce/0/J?޽Zdr:̚p]<즗 & svY0b;g^W\CJOcy&:S)xQ#D$ $wǃAWMfwwew9]֫ombi~1kS g=se](܄Y\)(} [Aٚ?am/\yn% 츞ޖ:>1KXp&u;Ga;LsVy`%#[AG!jL:?Z,`.&X|PvE poQ܌lݍ }}%q{&vV><~`+͘K?`ֿCظ3_L+hB%]$Fx[xG"/-WD܍H`C/uM YhC[e[1"PX,OD4}-7*cc{L]֫h9jrcƇʳ;p{9*x\;WcB(&N)yR?Y6Ӿܐo usU(̽{"fuԆ0+IauN/.ZW 1ˤnf'S Hk0T p_L麸!Yu}} { 6r5~1xvL=>_,t\9XMeOk:{(b6wy絡j<bp(Wtn{I~s73Fv Xz+Ĭfx>  m l45.mN1tjab`ohnj{>pp#EA#ɺBȪqs^H{[3/׃%Ll܈sx]ˮ͉iv?fI[vL}snoM:ǴIDdta5~*o FR:~ݮ~M?ϡ5N\ZL.\1,xz͆ӓV,0 ( }{,VvY>Yһu[UmߺXү>uszNF?<`ձ уP8bRc]:ǰJ7FR=dK1lsן ȫ:_Ǣ<)Yb[svsb\hy[?j p3p F37rfn+o" _/~Xew#D3y º$\ܖ~Jd._~ fj#jǰ6wl>X|>liX(%iX ?.n ô\U!, gUݘк0}|?.ZO0bu}ױ\OR Z`ް!$WTS(bQ'1*k:b!*Y$k֟bIzg;T 40VְekXI๯ܭ}:Pj2tܲJ1=>t٩ z ˃{t xmVfź޹r0K&PB=}UNJQ;J֬[G0WG&'Wwsx!:mi D Π^kSu j,|%,2wڂ%w^z*|FE+V+UM] +RVw<>C fV,O0&Lư?1TyA/zk΄8ҵDڂ/KLmk ۵ůa `i>Y'>@U6ԉ&XN`gnj!FAu6b(NjL!TZ[Oh0?=va)Sx^Op6jo[nEcфo/5wpWd bH1nͺX9X9䤲^&^4ae_sy0oE . tr%+_=%D(ŬM` !FDfmrּ$=htX1z%c7/?Ƃ=P}vxvx;^ _H8j\LC bDf`99Wxo! 6KV Gdj )DjEٛ̊+v<O+TbLc@7a7%>  DBc%䤏P$'$yiz%<&J.1(i\B N 3&L sVѬ]]bE,!TbMW/fB<9II^x,}dYi1WHjVUe#pi蝧'sy#p;:[^vB^Hi>1;Yz>{k*7a-XPӻ,XBA3O<XbE8ʽ^LV`mڱ|Α*]Zwk|.OVɊQdy d]gwկpZbZ6B4,n/nۭh6iu*',=Mn+f4<3v)vp,Nr,?G3IV$ݬX˯cJHC._2 9uŗN^+'/f܅C|0q鼶= v֤~p.ٳ['ȭXU'O'ֻҗ‚iώZ:'/܂!yD'`^AjN%~է}eo@}Nn8'qK@V&Mf~d]]\,}iq,:[a׾ onB.OWG%snvs0KS^s[9#{(vvmUxn}zDž_3>_1z9 Ere^e<"0 z'y< +=\t/Vq%[cֳ76IH+ %/NlyR<0k;Y(c籓 sW0ms/ KSM~qEIRb !ƈǢ/lr >v܇y¬=@-b+N0cX\"*]'%ʼ>2 p܉Y)C V>&Q&B :\bZ`V~=ZN1c-rVs@\VUQL{/7w>ϐQuEwDSǡf73K[p(MPmez{xAe'rl.n6 ;MoKQlݎ Uq,sމ 0 adfa]EA.=ma07~٥J&(jqI~*ff!'{iw} 5^$ WXٻ[bkj8ZM(F6`q8vwv &t)&  &aAzc'pIl7B1~zrHp젰J丵d'ء~];c9F)ʸ: d]-Bã$*=e .Ϗ}+fq;dnBOqpJ/m:OMrZX{^,!>FvT<3^{v".PԯCO~&YPBa٘aLtJыܽ.`o>WYdl ~^P)) K5ZEb7f5RY${c z,`9wYTso_ uu4E'+D+xէn5iߗExFvu!s9*{o݄_ (sV/&LAhw)/4JLY-0 "EFk"&b(5tcԩb 9yDA {@OS_{I ?Neb( q$wWLsչ #E/X迈_JtpՃ넋GGx-fZB NS,IB}>f=Md "kkފYvaVQ!~^'|}3` jas796#wG`g{EQgvp-JIoT,M-YNE,1;L?-b<n'?W܏gqLd=׀Ƭpn;j,ASԛ\M8 =AQ~U{fmf@z&{]ypy_)H:/cf*Iꊋ,1̽dڽbXڿ .n§G.ay.bv=`1N: \f4&V[,Li_ڹ % 3_=tU UDO^]&%?u ^z$|t*)V)m V*F,&k &#=L[$8~宻N+0GUO +ſVxz1WiJGBK1Yb^3f򘩝 b$%|1Hl<WT1x87_2܆'cZؚNRJ'VQq_0ŋ~gekߕ>~lF⠑ȝX\O%Dӑbxmŏ(ƝIB+{s>݃#&-ob+ZĶ\ <&`N}}ڼbA quc]{1x X?&zeYXj^tq?lnB!Hh 13eHb<[$]XNst."9b6w:f-tw ܂C_󰸳(ih:130FS;̲4hqg*MthѬLJڄ`=J|vuPUĬX)=s>pM2B|z;qGmnfz,}v>6/qR_x+ʭNU|%E:i Hh 1DJa'{0G nbI6Op3)c" oc+S`{LbV+ʕ **.%Xm?duߕ~lI^`Ɋ%f -!H6a'ΉZRR f9[3ڵ᧢aq+\UjKUS\=c"E+K-率թR z)U]I$&l;YČ#%*)M߄=~]̅cb._/j.}]dݜU:aM K8?uXv&$X%i1%QG=n)=DVh}y=Xp 0G87J37|:DT5T;ߙXe8*Z܂upbE Y]VϬX}٫ӎYnjٝA"k{.f tOF {h[h=W32נMh W.hWcu X;JPwl嘟skBL#턖;P|+=V]X1Wd쒺bFX2t/I{簔 \|`jz*XႣR_X`9i*Ę"n /ZYjUK"k;h9VYh\'=S1gLh{-fyH/MaϓZv e^bP dI6]6cOF,Yz[{OD֍Xr 1at4~'"[k 3((8V> n"qNC[6\"KHhyu?M+qq}> Zbis)_ǓZam텷53>,OU?[ZX.m$Ҿ=S\b/}5R,(L1?h5m!hUhqC&Ha E{U5*밒5.F&IM1%dtDԖ`كYZqZb]\$ձx\A E`@іzATb HhIk0բX/Rfq<YHIFaXQC;r @bZm0|zhg<_ O[k!gh V<>0fz%I9,Ek[)|V5^1gUꑢ0%1i` x;6|>Xڏ_oodjQ,SP,/̱b7Z$腄_x.`Y4^&D 6LD*Bdm^$zq[y~܈]Q$#]J?Hv-ؼ6.`Ⱥ9݌nD,!!%Vˁoa֩rKUфeX~".ĊJTJ_B+}V hO`V3K1 L+3?֏D`h7֥$wՑ9nx @MUoDŽ &c4?EPis]>V@}2Z`x>, ,̡BGQfV4> ֨{)R7Ky6[{X^! EcQй?<{ˍga"sfH`c'>2 14jp# 1 jocP:eY 0Tdy|6g#\OXV +s9teuHM5`&TBRjzb9nlڰ 6^ǒ`"SXyLbcC;"H͡p$~BN>-`v>b0ƃS~s'UEjxoAS6975X !҈Q MY%K<b/~k1wV,/Ǜ>Wss6EFAhR^+BD{vb!\ôelLӃq3;XߩHd Zj?812d<)Y7cv976\Rݘ<4jsD\F:~gVөnu sԦLbƽoc&M(]aPQ q'웰u54b,(h sgTb qcL4x?&c}6N´FmYK^+7)_R |5̄Kh :j=8FQ{4a/&N`u^(xV\+ :~;_pVƶX5uؓ~/t^kzUN?ny B`,V/Bd ~p 2.0h786!2up-6~9` TOƫk3t6\7e`?{Y؞]l~zTtkN(Ͻ.̃Nyb N!A@uE#ݽyEFX8\Ly1 f9,fŒ>f>̽:_9VHcy`ŻXL(-X()JbݻtOT`AS][1腨!Ecս^`5o~<ƻ~Wa .} ;1x&9z-E\оhi"fq!v :r*8)\;A݃~u/tda>%g"WI(? o=c!J;h6scFVI6Y`?E6yB\M$~j zp A7hcW*~'=+hV4J`9Ն{OMbDGv-B$Ft7lzE,, r|և#|K|̲q̽^VԘR?wܬϠˮy`+_x.V#3@wgF+#J4S`YȒ IGL`.? $ƁVi_ h'cBb^3g /¬W`#c%w;DOތyV}_ۻ2^|Wor\hMbdM "\Np EyMc[{F7g`.^}nrcKM`)nZ?k+< V>H9@|%hJh J<&(f^hX$ ULl-`֬\d> .w98g΄hZ,砉[]]1{`t6p~SKR}.=К8Zͣ}ϼЂ">k&#`V9 s1E")Z s܍/;}}q SJ+`)6=%Jzo{?dm C5X)ޑSđjy,R]B+#fJl|CϤT[0nī(\i@tb .ߍtwJ:[(Vq`{!=ݔWj$&N-GQ ޴q% +NY<}4)Xʽiu+o2K_Je[' Yim u|!v>$&NmGQ jbMC.=f8jM,XbY]lE[/wVUMf<V.KŜb)$&Nc2b͘wJ|B=QbF f_aw_!:Fk#7]F,YBp.oMR XnUP?JxBַa"0;. f/ tERL@jO j܃/Ui348:ֲEL #_ <$􃋢 hݶu=]"Swv[t,`kGA*nXGBk"p6c!3滉 :xJo5 캜N+̂}u7Ú\ݛb\D>[%|@`mt(K hM|p< bb^+Fkzqqw'"sEϻ{Q$/-ahJ|?\arn H Y&r l@g#\QTu,ZS[ވw{Y}`1,28eVgbK\w]s_\Eq/T=>܃th"!QN\m)2%֌aMrW/cZ9u%#),oy{$`b|OBU-T\=H` Q -1cD L\ TKhM?j,?(vΏ`v EMXvvn ݮ*݃˓l$ ,A $^heǮջN#v?-a֫NWnGԗWWI= `;;$ -1#Dl=HhMJ}|+7,jn,o֥jy,Y6z֫/svm^9Y~gXB>(݀h.>. 5uB+.:VbՅ+e"zCc4AjԮE*mm\CкKSXb;MEBkʄV&@? <x l2t lw5ص>J{b=NiEn=݃ۀ\gͦ"H0m/XjMwhO!~.•AjG axn+\ت? vV\̂7`W"GbZO^=A5RP }! C$`-ak BYhJI|\-1U<1qt6yk/~_ ]S&DOL![Hm=XB Z (K Z Z.J(ٰՅRՂ6Y2,V~U2vK{~|ǰB0{$XUr iGBB+U!)j)sᝏYs`Q\R>Wצvt!zp $>Vb֐Ъ*U1Z~7`)E:gy{98>Xu[%5!U:W d_b˸kiw֫q;lމ=tzu! ,!* a%2"6~){ - a<}Ԯ báC&Wc׫H :**H`  +!V16cM2.V?<\ \#%8Dco+R<д]{zuۡClF{u{m(`1Yԋ9L<tqy Ԥ X<s$KUoR-|'=<]Er]>\\.+ x*ִn"wVr $(Պ:1KUEK Zp4΀b6ؒ3XhebjlD;bvSre-#X: j_ jo:tp_vewquȿ֫$)j[ ,! ǰlàxIf)_G,ZXE+f9T= &?,"q&X^霗ʅ׉bz.F_]ߪ zu>'6>^K4V99PwMUi@ I,"Ջ*Bǒ,3>-3kOmjkߍShqQ/ŒvKeQʬW[خ~W'/ zXbFea%,@v@x Q#rA#U^B &֦v-/qͧqLl{ڄZ&W[c{A W?nh!z ֫aY@KVw%VtZ1bTqL[2'sۢ}b,'+NrXY֫= zXbFV3VߔQڋϣƭV0G0U/qncPćA1 ?ְLyíT7<C^r$j #7.Bp݆2aG̕ :ڵ;_:/cOfE{Oo"$AKL%V@JGBQx|mACt>KDqWu|v: T_7XbV#ZcNlUc7`H_twabs,Uvb ҿ+鏤> $jVp+,)w| \/&V0-{1qM,~9?dWcUu 3wa8 $DVb`$jQ^ |`HKpWkJTWkG]uj`wx ݋Dwگ_^h(V@J +Ŏ{WU[уtw ȹ(}Ɗ*w/o"bu_ QH`F!aU $Ȑ9"[\kyAVj. *s-x yaPE\eb~WN1 $DVbbHh@na VH}-8=/.qGUHl_`ٿZډJ, LKA}XHX +Q$**wW[jw8cQXpg/sU+©c~}b(H`Z!aU $Dm*}8`eh' =c8X_lc클^.~9o5Z [,h 9{v*&XHX +11*:gʄa/=YN`,cjcMjBXW X++&ށE֫]H`h$&(ـzܲRS bOm~.rA4YNX{1CU.SsJVumͽ}X<} fjѿ [2S]uBKLPBj\HXR >9PK;*}`zqEY{ǯ`Wm4oW7qA+̽悩ժ>{fU1ہD\]^"=Vb䖪t][{[s Y\jU}+~`W0W]s9k\MrYJ稊 | >܂..~21;&3x_V(-a^vOJX +1q2WM+_Ҏ G?g`N( Uݼ}K1auj1Yܽ."\ԆE_1i'XbDl+N8fA6&'c?p,,W7b<#qW'w#%@?hDu$D-(Y^A顈vXh-KI 3qV &73>x <{ +#u!K$jR,&qu vҿi.L8*GKCJԆXX.ވw Io5-G0DKt|)܀QUXyV0էVMUqs1%*~2:]]H` 1=HXZᮢh։)q iՋ-aIP]?;em:b? b%.گ+x>&Y)踸:Ľ;vNcEK#a%jKn ؍ *֒Z(/ǂ̿,a+b?7`  ;?RU[o D/fu(V_qhf8XB4 +V <̴ <N]<Kp.p v >X'0GXy>i^uR4V> AչEM\MDJ4a^&usW1)yL@g_E}Sھ'(ݛ/Oǡnadzycto 0bu>S+qXB4 +! .~2nor1'wp=rAUNYUl'c}&܎ZyZb+a} nA [B*Mnq"%D3MZ{S[OJ)}NE~˸ڌ[VcXZ *6/ƮM.ؐHX Q& oJ`MclsC86H0lHqXBL +!㖋X>LP<=+& } 1C\/o<;{-j CJ޴IJt">!P$uZSjo "z| ^AP T]{+MmڑWkXsX^ĦuBK#a%DD`]{_OuX4_h}x pj}XFYPU e]WayaV8BՉi;wM#H` 1J$|8ߜ"|Bڕ]CTej#Vb U|ٴ/)5Zg."`:Yo9qηg- <9oXrDTKYB ^Vhg 6H` !I&(^'͘ &`b XB!f :R\? , XB!3)\P(:,݂U$B)Zpp T$p]zH` !mKPj> +Y?W Bх | `yVAK!rR㲛n1< .Z%pB!`g]o',>~=4oA\#CK!Bk"Yg`cNY+mP} " {x(#A5$BUEh= x5p&X.tv3VZgkU=B!@ x'[ME'.~ XB!i#aK4ejXB!ȄVS$BaGB!D?H` !B  ,!B!#%B1d$B!B!ĐB!2XB!Cf BdR!B iX^{w!B3mY9 !BiX؍ k !bL !bBLB!XB!CFK!bH` !B  ,!B!#%B1d$B!B!ĐB!2 nBц0UL x/I` !sbqH` !^dc 8 p#̅1w ,!u!bpX?) GFB1,&x\`= %Bԑ, b ~ju  F;@!*Y`ao6QS !MrL1+7|_nl&̊#2=-aVZUX`P#f۱{QE- E$ 0uiA0< LY2BX^$ 0 `VѵbJI"k>A"KS-R֘BKDbL,T ,!:k,F'=Ƙ1 ,!c)̂bN%BMXD+z ,!c&a%crJ` !;ȑ57鶈5.qXB!&DTLL%9!B  ,!B!#%B1d$B!܅B |LB2jpBK!Ġ`&DݐB/ K_8:H` !臈ہSQ1isjS-جj0-xvB8<.QܓS+L8w/Y[)+a#N(T"¤V͛534Ć0A+vSl19p 0x}$ *LK$z*UY~rn-~aF3atd*D8 _4^?yx.ppV;209h[x*i(4X.ESP4U9!PyiJsAr1_w-f%X>V@ ØÜc+1Uv:`*_&ULҳ٥_S0t09HuxeY>§E!a0x\UΈp,;XwA:/Y m5U`ga1r89N>U]O J BV*Mѿ_D"KGm<2 c1iu=αÜAutb,casxECMtKr-~]z.5ymQ;V:Y@ Ø豵ky! y#3s*ۺ"֭VJFaR @ pR9vaƬ8Z0/^ 1 y͇1V@ Øehc mM"!~k4lB1K#ĭ[[T, |\gYJ$0fG|lx=۱uʰ50JxoLԟxTp hH"߅?DŽBa,a#\0 53׳[g)+a43x=t0lzW; 9IxV[g)+a9#)Mz*UY !B +Y0sC@U:KyauȈmxfXI7aų&:Ky06&^Gfb,a#\0Q7r*azuұ; 0ȶV Kg)+a0m=eƱu1a3DUi|l:ˣ26ց1$B[BEHGN֮;310ieVM%}lNFuuaLQh/ '<*)3;9`,casm0b׍ND8'Qn0 mjcDS32i,IQ#|zٰ͚AoC dxm1"45ysy[gP0J}l?rmA@vؾ h{3 è"۷q*'D G2Iޔl0*10d-~=Eِ$YΞR.-`#\0HQ(x-! 5< "غ&1F1FBh'2-ZuaTJL_f2*S9""%YV:hQŁE>Mч}a!VUgU~]b,ccoae$kjg R)V Ð&Bj}_s,᙮.L<5XjF 8 kUc;! EX}q#>UE^q7k,l0I]gs"2HhXUA|{ yʿ*A/Tu0qyRc[:@Kkɨx>U2/3RP0! ŎBqwǐI[+ŏR)>%w)k ǃ) _EP%GR4a:F`70AeVd!cg XE7~d:]&*yV!~ !ց^rhsb9   RKng0bU쑾 `Hle4V@ >۰Anؠ@s|l'B"3aVr6 u.ߏATz'Y_a%MzE'xK/Sclȉ/vYPUټ U [{2@ c2fux=qq %]zG#,u-=ns`>1(ޅ)#?.ϐݍܱ*aȣ@V[gjsbTc^dg) m|Kn߻Wf*Pb"[gI0g c1*FU.f014rNH(l`g&]9~[aaz >"-zBbf|U~m]]ҟ48Pig"[g/0^8Jh];Y`0ܱ{i`謳qlc#\Øe$B[. 6$}l%wFSje5Y5QTIrF]XI<ӯk:j K|$axk[O wpD1L4YkݼY3g&U^$1U0+R@&v,wF}s\ qUR3,ցFClas"$@c ~ ADLq7kgQ*;ڱxj8تXv*-|mnyx3Ihp;գ(βzn1RyB_m{򱭷S2 &,V@ cqrŁתU 9^׹Phrh/0?ZǶ.uD܁e4={^@AU01gsn a;-E>ixG"vr|xny2R,TֻHl aL3hlܨqwǐXu)$??}3Ib:PØ6m2ZSec;^'}lg{b28è& Ή"i usXDs-:Pè9}lM(T2>g-ZI!(BH׿hX51:Zx]2P(#%"l^ktMEvmOo1\[g_a,ܼY36 a;I1>xPs]k4U%@p(-q}M鲫"Z1Y0f^ &i-k]v^N͂<_>Av95Kt"NDsj^tØ8Z" `g ⃚vpiM > 8YE+Xؖ 9^b9baT 6.87ju*ҩDY-ӊmTtԉnЎGIAҔ'4h/0Q[kRSKޚ^@v^ Yz..fuHx=>g<gVxx`ЬU^e|د1.P/ldulVtqH|?X z1Q:8:_gٹs$uVa#\( c;H8 Z&{EC^١9UTTvl7 %C;V4 .Dsee}AQK׬[. 6I~>q7& uvmL R (Q/#xv)z s+5@٬_gu_aԊmp=\Cxա"_o{fgcZVN:$HGcr_"Ȱ*z~%z.٘&}6 Vt$K*}cʿHq9.x8T9O.nvVPØ }lf; Hd|l+ D*$~?yߞ' ශg"-Hbhuk1#Եh֯0&dkclaUVgjcǨ5 yt!dtˣYuYtV.9-/D耺B5q9lZ5 [YN_Uc_g/v K(Vo=e 6?"ۋtbQaJ\Ty38H2\v0Ƶ83cGF6RFU./0&V"T Ecm踱Tv^p/?*$*Ш qiDw˓Q`:* "TidX 늙~S ;g[[TَOٯJ:4/q ϮUaWuuIW k@c.T+BmX0qD8/ٱ_aO6 8JIgy#p>0ev婿wsH$T"HX>'}_h>DQ:KRM a1 AܴIhj▤PȊLOul-X$T*%*> O1*)Ume 16n8ǻc#Xu)$?_,Np tA[G7bP8EҤ_;EnDUI55 `ƮAuQٺ x-B*EYLOu$txpZ˅3&6GzVn40֯@s=tݻok ;4t7 Ov:Oyc[Ph+S)~PuD׹Dl#rPs@$T SyzͶu+F3vk(͈mJsvܐ&^`MOjQDXk8 Z&!"B>΢4$ ѠS>BvaX(?omOq:Kcuցu^Osc l ܛ|~RNG]'#P$K>s0=UBNLbԨ׬[. 6Lx=5ۂH(΋;L$KǺ@s߯țv@N; Xq˓Oʡfa~5f)Dbf?>|lxkvoߩ+Hhxe+ yAp0-FGeƝ՟Y5fQ:,FB|DV|S %D3 u&DBr-d#P{ hAp\5:KCU{c0fؖF2) "N&>. kC&E6HFE2?B+; /1QX5fQu?1>^))9 !RuY &E/(ޏJzo> 'o0PjˣsY:5t:Pcu]k֯g*> rna۔}lGǍth<`mQzL=s'ZyYl~R.t.2B1QY̨q'M#QfEXJ*ڳG4Yv^po F+)#;eHKu 4+] 鑞.,4Voؠ׫`Fx9:PƌCHCq+1x-*U8#O):Tp.\ڮU)RYN`!ױH(EbC=OTV1EۢQnMQ4#H/pyvE1Ww.Uy?fY5bHؖHV "ϜS}Ʉix-EB»i_Wkւ. ,KLԨ, "FëA׮ 0@^(H&B svɷ_kP |B8L"!J M0r(:.BŸE5w(R4-H?C;*^Gb" s\i6h ;ўٍ@ |%5/ |{Yt\; :FїEQ\'h<ճJWEW r8G.t'ZraY%ƣHABEG? /g"UT~iJdQ>n6:a)ϑNۀ r8iVt+M-iN~?ipIt.ե; +g\gh4oTd$}lu.ԅKicE$'+tBQnhL7q&|rKJnmjLE~:"µ" rSNF2X?ڳ/f])kӑy|OՔgJ5Fm$z Jf6 >ܺk_F׹hTۢp=p3<|< +[ުEYGz 2 Įe"dtwx10,uŒpǷi^oRҋYM9ZTY&£|5ddǍ8 gb >Ig+:(ȗ@ջ΢™^r ?ƍ>;)t*!X(CMO?gˇP]h}85:P߼Y3g&U^$1UR"䃀N({ (&yo< {1,bE `7%sZڨKĸJj`!Ѝ4':+>BW>b.G*()]G7GgH 链jW'1gJ>Xq˓OʡhjSw?65"F.5gAx>x=^g|!q CѺB# ]'Nn0 [DXgMI$Rب~~*S" ^2G!>xQA 2ۣBxI_sĜ5:ux  .lU]go ΣEB+ je@O7cN/.c| |tqNCv]h=s,޽[^DuYՍdkI\e{PF9b@k{ycr=DBd~[)*RYtٖ *xB8>J UKo/q.t.(tO1sU.r]@5ELa&\o1q?ý~!L¸6ӑ$Ӏ]g|]pg&DBU:]c^)]a:&R]&)Ax/A.wVbXgtNrNR/"Ì2|l]]Ώ AAN(h]'q^a^z?V$49ЕmuZ))@/~Y`1k-swyP~|ZKM \ޡk))n~^QK 3;=^k6#+SF\7wuAml."a*A-ݿsA*HQmpv\0~m@tykek>#=.{2DRӜ:~(5XN_)KS8l t"X(ݻGB.\Ք}@g,%DBw#NǤX)?qqe]s] tRr^\jT{U Vt]vgETh{<~O(NgaťYl;' Za7xe0&Q%D"l*"<묹H蜳}!+?wRHGg-]V_ciӂC#^Ek-(¿U(yO1QwVc>S cH+ }"/Yp /B,~{u$.N \hc CK]̋} 謥xğ8Jh22|l|'򤏒8-%3d^ An(3Qt8 Xh;z0\-'P.p;0 g?/~ Gwgg5wF7mҡHIL7d#zp&t;uv7»4Y񜐸HkqDB@h椓 ᰤV!sw84Ge'#W!ḚHձS%~׺~~mxDdeBVxwt{w =WvT8k$Z/b?:ȑ"]TX5>ŝ*>|!wq AAW:bMg9󱝄kVrjJةw1E_'HhNB00OKKG{aru#jW>< 6y::Іk!uNDRd.B|%:댖'-#"N#2/o4 H(9ԂeBtBvJ k~1V}Q0pg_.c ~k@Z&x BŸw61gRmQ *hqk9 xxp9HHwF"HYZga$x/ȠRP"1}؋WiOzҢۊ>b)&|4:g=ݞ0lD"B>΢b ȺuTc) =v!Ц׺m*oガ rQ(r $ h_ϜS}s݊g)e!~%%:9W SgmBA3/ڙxFg- n{!!xN"ڏ4S@ c } !(Oiڎ+Ciү,tD*BZ\ޙ::!x]c{iN?hP>zQדw: kI(p!`ڤP :v?,A(}24# Tk5(ނ/ 7wm+q t}y]2=ҳww&Xu@뀸x^~^4a& .tٚ1I"ڝY!$T*;m7B8uZNAhD TkbůKaw=J^m|r)Vmſ: >jtgm3Zٌv쐰H(thVEi 4,v"Tv^p/d?knIH7(*C t%^ S9gP 4 +)S&1ݭ" .`xj~%: ʐ$ɍiA]סk{7 ǜItxk pg be#=Wx;y܄]ga5 Ee0DB1Â,VtQ5DBQጳ9KT%,?|:K x.rXMc =%*rUL$:G:hf޽dm;gFBOP*M|'xB y_$_FP.Ў}g@0W76H(vVH(4!zJ%]_L %*x"~y7^=|<︾VJ>n3;xAy](pR]!q3v3%&R%:ŭtuʹ>"뒉}lAWK Ntmv /|iX튦-i/ӱ)^_<BŸ~,ͺU·ߏDgboJZmDY1nٌȊʇAXgtZ 'FTGBcI™elMv| x }F =%nxJs6 /rj^YH 眳xFkЃ넫-*^+AQ"lqg3&&RRq9 3bh97Z"x]wZJ*ڳgd=e쮳X$D5 8kF$EBL@ՄHsa 7u%>|VBF"qRmqHdu759/=t{fcΘu@k؁`דPx}9fyw0n)(+: x #?F4m~KǺ5RH%s:<}h)M0X}Z.6wlW~UDQ$YCŁް|b hM 7m-l PhBH|"%"lկq9J 5¶X$tt܅0TH P㿶BU8Xf{,BR5ϟRDFP<._Cc2Aw@p: :%YuVQ)#>XdMX!ْM$4!3 x%cM}_)&}\3cwՖOncw5~WzeЙ:)TBܨJ ~,քωSRxпf|(s{Wstt{-~x%=H$pݝ%{I9PvDJ|;J)=/J ~֟sH|MYxsRD5)"j˗Y~̏.>NDgjwBgٟi4:a',β"p@~sB1Jy6P9 zj  Nsձ8㊜2x-!B9 u4NO^pUYHtVB4cYM$0Z)# S 3JQ fs>|e(kBb.J+t:[#ϒ~vEGkt(3D"8/ݻ wku3(֭B!`)_@c,EQ8K}Q 4P(H^ 3yHϝ#ų P@bƫcPrB xŻQPĻwGԄ pP$Kp=4Mw6bh;t^~.it%"F^m}l/:uk઀<[$Bݑ52ߋB=/} w9xspP\aP$/ފB"(o.W |W' 8t{.tFD rq*ŗvGm`B-1>V Ӏ]gԳއ,B4>.o gHyBs!?+ks)jzAJj-G߫ob1x.tڝfrX&EeV;m*[UTexN$AV{<)Jٟ[K6fȼF] #"^zEB"!:9,UY74 xN ߫@ni?g#UDv?\9oʐy+8GQp&Z^cgfrF!ƍTКntae*He;:z P{yP4݀qcU izo"PSMѿ@hGg&r?FE&vE3 rEvofbF;h=zXKng2xv!0n"xPCN@NU$4@*TBx>M# A%Q^c;Z&AQ"tH|ܟ:s(6aȟ/>.1>DhY}|̢/]L[Yt+z  E+=Oɩ>Ψ[ᕵ1gcEdv>GO8kVnUǻDVQ7I筂j:5=ҳ;ްAWgiMg(|zٰ͚AoCމ/P1WI.%=NTxu -%Ȫ($8`S,pm~Wz?sJN%=HBZN!9-&>3si'i7+彏oNm_}ƿv NEEP #Ϛ`Hg1AEx 󱝌b=,_vHf~/+I$TF7s" %)^Yk$1a6~Mi]O1CbE _mA(_w]v\#=Fw/&b:ˣӱ2;bTc^dg> >c;;EBHҿZ|9RIrߏWHǓR 񿇟7joO=Dӽ2 h|ݴIp7Ҋ%z?W4(𺐞2:nb͊ o1E"e &8'<03@/ԘVJl?fs`PHޤh &^Ĵǜ1M,sb-nܨqwǐG0P)d#TjTീE"ЀqcV[ 4?`'1=[&Ԥ7`)O'\mqv'xFѥ؝hDӺ2X/׋|lip)V"!+MQX$4 S,P%0؄Ε;v_oIpF?<owO/zg)xՖZ˾xMAWyZL@pLѫ. oGAE_תn+dYf0^Npm<38]r{Rp:_piMGNB"'Fsmȹ%?[|* 19T$ 'Sg^wXS7EQ}?7Z_0t\FGE,Z!󉀠FD}:3eP!Yuٌt=Łף|l Ҹ!E(SHh5>FA9Y@H+)i|_9,D+>apJqE)pE!cƝMhYfYcoR/Ŭ&#)Z)£|uNɍlgHHYw"C t%p5^ t^+V&?K[g{īe{(?QE׊.tDdEX"ј>YT$l|lR)>%wF3@8{DBay |>Hh)rP~5H dYN;%ؾk35 %Pdit},EDH0b"!P6xN! >+D9 y/&!@, 'p zaA()Jy g /vB LQ($@{|8EjKa.Aj=A:1gx;s>x=!>#v|'}g$t87c0=sHLsiAh5;8`%sE`z!x9boۻ7cޜ&ngšH?P iu&u.CڰAnؠ CHCCSَD(s͞=ߗUA+loガ.kF"RAn5٠!**ic(DŊr"^AcNzoN |?(+[},E):!9acΒ,bG5^M|JQV4C#+A5e֊*N@(* tD [bM dN -cY>x;M:Ku>zlZm번1mPN|݄"%Ÿ%=}h9,^y|B$ģܻcX[.};4Qjc.OZ T tqk+׻ ҁʣ=wG(> >p]A$[9I 'E`1eB^?"Dy,&2JE  WM9STڲYm=~(n-t0-b}Zx-{D`v!B{Ru^$ԡ7(΀`8Ԡ"0򰽀EBE@qPRBU-7>e!dA r/j߯ ]tjR'#ZfxUSliە& |GߨNBȿv>`=FB"w3~MB$lTkC _L;!j)ގB/ƟPbg"Un)jM^u:@G]q*i휐xtsNm?Is8vC;>^>|uS$$hV Q"E1N"..9g|a "e'7)kbˁ#emPZ4"B<_{*'%zB|r;bo_ۧk@Z>E_H8`ny2?_*B~a-^oؠ@sS&"+TiV3{Ƚl8_' id?@j"C&E+9%bar\[@>:*+ ')ǁ^|~?*SM rB5ɸXLJPϛf,u43:EǶ:F(򱍅B#s0H5jJ,Zh;z0&*b۳aȭv#\jEB:L$PFm^]PZK BCu3Ab~ ]^jd9 `~'{L@p, B`EcAu-S9ttu'G-$}lQ_貍5$#4J_ <:HH  /ƫ3T&_F evè*QjnA*KmoOo6sE6x["𧅸3u5 0ũ_ڵKݶMۧօNc:fuckד^|l.zOth i{yP4Հ"|t޹Zx"1B7 B3 0@_aC)>?~`Ƃ~k8v5MƝmPb"j2 hǶ8[Osʰ!tk઀<2h|H0O/Z.>O ER)L dr`ao3je*-i^pbZԴ-M)x}X\g۔r0*,ymA\QYE/zۺ^@@RS=zj|H}QMW>]+gZ԰z qh%!"U΄<ZJUE!׍ )p葞[ɱ©u*C T9g)Fy/9HBP4 VłZoϫi/G7j-&!غuj5#B൭M2X_y/y8Ku4k"!8W$4PtTY ՋO`!~G{p VROՖdv6YZ݈4ˇ||lSU{k[O)d֥?W^p~ՁjжRGA$pg㉄Fg$G {L @YgL d4:DuKJ R$>ƈ(^NC2oEGVTLTu*Pw_f2*񋾈-KĒrDy{~ЖkrhldpFFdX%~CMKSЦ>fd>WYy_ nIx4(aBcx8wht*+/}LѶh[+ڱvߞG ǪTyULgrMXN_]xmBɂkڂ7΃yv ."!Qc@hP"p@>S/0jAt6;ЭBŸG*U9 U) 8\~,8EnoոTc@yHGRjئ\;|e4}iNOѦ a޳H$Q"Eɒxtn F#xa`Jߗ3zphC3;)AΓ*S((:^ nx*"U= N;ol^Sj6nFZs"n\ϛh)EQpkXpgvK/PR*#w_/z=&Jދ y}YaZ*}Nvx;H]G{*>Fӭ&E;_܆7^O\ʴ.ʇv$_J?ݵKvx,C*~,}*K:Μ7z`[ Z" |꣏;A) 8A!2OwCnQaU n"mHO)XaF]E.Rmgd:ٳ\zy*f% vGDX8YOA ӟؽWS;ߛ/ ? ~0iHh0t=u;#Ыwwڥv@h~&z0#1ʭx"?6 dc.^Ju~ᘤjL@(z[{5X;QIR4nbQ _"'GOn׏WYљ_~"%~H,jbа]X$; KwG@v)dsaTFl4xko'm4H>AVFS.Y(`8{ZowrߨH#l v(ゥzwuݸQ_ ¯~O*o#4xJ|:N>Qpl#}pTt0_>%-^ptB.wgRFP_8_3QE?`$b.kI-gт ODfBg?Of z$Fm%=^=)'T m^6p|DSYgQQkג͆YTLv!M>с<',֥25RH$,R"zWdn<>p|ه3;x`BQp_ |oPuJ𓣅iѧ@"t#lqd Em7;RsKDըw^m{ A œSGo}Ǒ~Y \3 "=sB%THUA `!Ø6+7}?~%`|ڏ_SJ^GB'-z"B 4LeOzTKL+6o eLj k'@Υ2r&@ӏOo\ήx"gێ{9![4HSbP$tpHwTHNmx3aG@BNAh cF_;w]Of}qd}SM?3LC!硛7`p[DV[ 0L h3+l[|) h^wA{pޔ͹y?r-uT7I9R $1EA9*7&=- ,- K]~:sʷ_*aU8 M vuqK ~d-[vTYC}wfS-yo{e[EA~Imjbڝh>"N@ 7P܏ )A(CA c9 4 TȿoJ;إx:^蓫@JS'5S4gƵH0O_ɹ~H_ WAcqH,e3nDgg1Hr߅Q59^r'VmABɦMGw/ῆʿ|*%Ӂvu**UVQ?ha Ut> zzOq@ vRS 6hE/zZS=}Ou,E (aEeQqc'Djo+֦|O@3AU2A8-`VbX' BSVDYR|]Bow[L.\}҈EB9|E5׻{2}?qpnDGBx\Һθp';B5G@jK:}KxmˇuM rPѩv 7ӻ&ckt1gu:KQTx3Q!UZ |@:z/>vN!|FrNb-bЪ'n> |:ԝ#Q@(kB1a46Uy77k8TKPhJ[w$1cY,3_̦)+r&=Բ;p3oëNKe4kq(ygB9u\4۩L p:|}3ND4a46(nbV=#IuģJ*n _"H. bYN \@: 5eUGsfG/8[? +k"ϩEWdxχ{Ҥ)~|x>z( FB cvPe?(e\z<~zia~9MY5h@Ƞ[t}"K3uQ@' N,Ͻ?[~O_tMYGtPDBAHo};n&{ݺ":g%s9 # A0fUB=<5EǛSr=ԻL>9x-!32 N|_tm?9ʮlT&oBfi{t'YܳTe PR>O hRJ@ ږʺΣA#M=T6A AY*%`g!0!4R9#v ґZj>}"UaN0*)>貉YfKJ;wJ3P'Uc4Q5Ɋ Oi|ʑ_'|0740^ t޾v.B&2 c,xm MTG-xG{)p| 8O^pKL& pX ,0R _]ùRm/R/NmZUV'1Pپ܉e E>ġHR/H5+e(׶0-iJ2̧xȦs`\ڍP8SX?"QI'(&~<3h;w[5d[dPQNh{7oy^܄џ:BA@*?߽D>fI%8.:nୂ<]_ 9q ^ڗN=9ྥv\g/}N3πg+kk1+Bb6QLz@&'e\wuU9Z^q23^$}#/Hk TlEpEKUv:Sׯe" 8%?g2ͺA^P_LP2=HvrӌV-8| |XH.e'$;dn:r)ǵ4 B g8lIq+P(V< è"ňgfNw×ßMmds{^]h!$l qcbǡTɩQ΍\WC$H?!9Ϯ3I7=vO 8|)h A&P5; A,-ٔ>ܔoT<;NXB Øja'Co^5 f\0 .*PM#MwlG ߙT8 o8<T9>c̞4 d 38/'M)\!فc.]|tF"HFVmث[knfpM9K[(@ t/zݲ9 è I?ך1Qnʑn!]a78ܬDStW@*$|Ov4{,ZZ|G/c>8U&4=5)ts)*Iq jU h*9ǚiGeݧw)r)GHaPʸ6Nc!0߳6gcd\Bg? QTxt, iZN # L> o瑹 oQ{9GA K3"$k‰Lw=🇥yY6`:PKYNN:-YviF_r?} 2w|O,XsPԡHLr ʠ" n ̮@T lBEQWʸ84|_ 7tX@ȳ)6n 09ǹ╵&2 cI/'o8~ ;'qLg_XɋF40 Lsh`%WCM_O/R; 4IQڸ6!G$+Kw r*2Ait'i9| ;wr(pqvzLV@H(.3=ͲhCwvG>>siNdH;D 1$vC,^OPN2wGIO.5 JCQ+#J>chn94g˥98$4a͟‹]Sm; t/ېh+[yw; hDOW˻?8Z@.MS&?2(A|πgba)I0(U¡AƸ6 (]enZQ Z l>-qHsys 3y͓|]3y` %(+h cUh`~ ԋ|Upi@DpSxQxpNݰjU"6ƥN ir "Bɿ Aj}wkÛq BΗևOi9{C6AZ?͟N)cdm9 =1&=43]299h$-^Dj&2ٜZRkAQ% OAc nF9^|ӅYwa|aӔ_k߀a+g"NЩzpw}7Y EY8v-|u#Xj@)\  y´u\/4H+Z5A.̧~{CW-"=sOh/65x+Zv}h@]S # ?&dhwc}3GXtZC?70 $5PVH}];ʽ≮v>P6nKhxn,OpE֭Dx ԏttPꑞ}|6$\"Hڏ&~ݚה5})u!$[rgW(;^KJv Agi7! 50DT푞Fϑa|`|9Z8Nhi3fjEYx*_D0 c.Y-w^䘬x,i+9hX牊hT< ೻vɾR'y5ESY-xʿV< Øˌq,Yxi5yuy籊(03g#-蘡"ZY)VT< 0cNK.;kRP!".Ϊ| *"NxƔVDxaTDYxBة6u}+B/_s~%O[2Sɋٺu*^&~UZ4 UypwJ3f"y+a'щN]`_|}fe &sVmE8%U^KQ z]X4 0JK~LS)$.]O0 d[  xdNxNj4Kty=(Z4 0JS.[Mʂj96F^7\%I'2|ɫ ]&Nk>=L&->W'3,KpF=mʰ gDz*}"[ݽlqm ߖȫ g|]ęfxȾe "'8~aMLK3;l`2i'|e1,uF~sGˋ>45Rm01|"mqó`yy^ܡj(Zad;҄4sb}M ΍KGT=X^Ó*jI1'{r[gQ @_PXY#nUbTQXNyWπ>HË/$I}׾|o2 nx}lzb4k 4PMrO d8_aAd-:MVnwFciFju 1sQ5GΦn 6y~. g%95#L㕾/w2̰:wd#(,#S|6+4+/z]Ξ g_!:v Ѐ, Րnjp'V'[[ֳDDzšn9aE=r6 (ߜ}̥`u<\@ar"DDbmǪ)SCtfj]:z({s6b ^KVSO cllOm,8kUJJu%/ї>36yf,~ 'eh @र ""U`^ҡ:G̰ͭܰO;⦛Cߏ-:4.P#Xw` `\_K@b(qiq`70|_JM|짋BsanX<‘NҜp{zBUhy><_O{ss1VI epq{Y9½(ֈqќi}yG!q|,8ׂ* ""]fVpuumG]*EYՇ PV/R0V: 2xw Wrb-Ovehh&_>ip8qo8'w[ {ay fXÎ\yyA""[-to'ġ x- 18!ԓ⿵ q÷rG VDDzrëXG -U{&$ ۢU‚60?@WnUkY88TX}h>C|pY;ՂmӪZaADdj= Hp .4[vhb32䁹eV=DD3PE9w5L^ *"4XWM~ x 2s2A6VD_kzUR""X17` , V?Ǡ841x|<_a[U+,U8q?j24&0x֡iIo?0Ҳbb{-d7wlUz=CaADDT ||&?vz[U ""[pO_!nZiXv i6fX"fnҪZOQXk VlPh8@Y؉K;ѳԞ}b""q 7$VSe(%%~86y6`4t<8>Ȱ_y ""f #Ηŏwd%%|u kCU(,:X 4|gQbVVeXrXb1t2ҀAg$6^Zm8C/"0$?lȾ0q|9=u~'#""ݩuɥC yrXˆ(}g5 aU5(,j#ܼ*.ba5/6U5mSk[WOPX պtKpXahgk.jV7l ),Ȇ+.&?#nĞ .0JbP9 ""iʓ91<|QV)Z\^ aq0dؒ5O?[AaADD6UQeE^C *Qbu⁩UuAaADDBHɏ0t:a G1VYuTXѲ21b!G'>V9 ""UZ6rJ > k`0@>FaADDTK`x9^ t>q%A[(,Hj*''cGX VQ=!-->6ge+%pZqP ""]ѠʕeoYbG.(,ȖP1XdD'{;nnYmߪEVJfeXaq]ebժy""RebN'=b~PmߪZaADD$0<.qC'mY*iZU ""[ؼ!r,]ZU_ ""[ZK`8Wc}ǏϑjU ""!/V3,n::'!C{?FV ""Pl qyd'dG~ 飘ZU+,H(a7. 灛u ulVXѲ53/rgwq۹U‚"0vIe 8cv*Tj9,$0Ь8i2ijI ,C{[6C11?|g6UivCdO("""͔*N.t2lrspi[ eϤ%K*pBLT{'>lΝ=תZaADDzZ˄o\Osj k,wk|Tx"""(wx(pC 4|q%_oZUoiGpD8{[mICvx:?^I=LjZ#T^G)+yl=L]5H\kU ""F,n13Vϛ7M0DuZU+,vS^`y)aj]%&ʒ{U‚l+ke8K .=OM$I5_N_2 ^kU ""μg1dz.]s4;|ف5.Ljٮʓ/Dbd~Sٹ!VRhޑBDXV ""-)E#5,ΧU*!>[3DDdji6_,:"6< qIaADD;/N}:"g^hU]y5N`,d$KTzU‚lk3xxɎW Y1!.6 [\埀 Tm/}o ՅK#>'~ik[PXm :,U]HVF߬i͞[ ˭nV """W],W}>t>\Wh@֤[‚ VR](Zz)WPҗ<8aᎹzDDDibXȘW}5]-'X u>u ""R( i`:Pk1 WۦfH1⮐r-9oAaADDd&9`{ܪ|BN23`S2aM),h.| 8GKub"s~TY'7~V#glʈµpby¾I&Gʍ6 ‚|!? o࡯)b!෽O^Fy 9'kbHڪZaADDdiL B+"{!IڪZaADDd ^% _pvCkޗR7֡ ""<+V?y _pB/߀juldk/֡ ""r(  OgemU ""wEzzVWαf3#gCx-ŷԼ'`07Qm g 6Xה‚H{r E 5c W0{X /\Wne(,g ̆/ vT|{\j9@Ÿ*/W[0fT><2Gg6P׎‚H_#ԭ;P+nda^hUY5Pͬ_ *,!ywB{Δ''k3R[fApY9r:Y%l1)xE bK9$ݛw]-BaADD3Ija2}*֬5|hY|`r-),tؘ8GO ZXTnBSBDDD:g">c2FmnXH-;ZU+,t<54Ɏ"8(4ȶײ|O7+ .E>Vc7wop|*!Q}:>sN=Zl|| ^50;6Ia$;mbR{{($ST|QFa6<0ب~2UDDd;{KӾ#KЬ9[A/\0<%}#x5& {y0zs~`ꂹJp'@hVwܺQWhsz,#O_N7R⿒[sL~ ~KKUe'޷ ;x o4a@`74v`pf/&9%KƠ|_N<_ ~'@Ű eAADDU2w<Ɓ9إ]vn2Ƿ k5L ٖ }wc}a8V~h~%eY ""c`=̖䄙v56 ݽ|rN ""tҪ G=[BUVY60; urVhWVɬꂥUK}6]'׎B{_1TI&UmUm$Otf8Fl:+4Ho1{`:95ۚto56$ ""!.l%MժȆ&}ɋ_Rea ""emH,bg>ˠ{[Uvnv- jU 5f$Э{-Y1款Jl)eZUI Un2jBl+e1 ֪z-NN:t"<(4ȶP>u8VkHʰZNGoG s}?3TlٽB5遙fy ]lN ?/c1`ܰix@!EDMP*LL`~Vk}ON#?g' * "" ތ8;8nΈ;K/"Y ""ۉtڟHt(ZUOd7^ߓ)4s`ܠV'Eak6d ""@٪_i)nUuZ۠;ZؓBl/N&{3{~$\. trThtXGxճ >]Z;);[[Uuo !΃Zr)""[ѽcYϲē-wCCN fܰPRhR5'ndjov.;iis,Zևn:UuWe ""R09f}^ePBl1=4n4{1TLWj &&wU9 ќ"Αg:fޕueJlyr$ǥ64߀ubkDDK}mOd_=x4gjg.}7B%e:0CZUwemn3[mȪ US5wY Ob*0N%U OȚk$d@v_6N]a˵.8in+aJx0>凌ՄVb]* ""JIF;-{{lRVРRTi9@%`gtcL멍+v ""2gP@[|O$ iSp&SO+K~mzj]m#_pHPq#?mx2M!uK+)rB%]rK8UDdk!ALD٪?O& ^!ig+ZU#|457o}4G ҙ`gV}_~mW󀷴`UAD2#~*ϫeg<'DMhRXآpPy csri`pe(NiB؉F'{7oM.z.aͪ'aF7~-[&l`5ӊ'n6̮'`a,@?T*] p "]ܵ.h.wpWU1@ޗ%QMnU}: F=fǗ 0?s~u=İB`:1LsمB@PLGFBe@~nwn4d#b9Wip7rɬHe^1HT|bͩvp,d5VM}o "YGC͘_ oTUkry58:3vsqUeay1ʯ/^,~ވcʻ]0,2c.iB_e3e24ݻS34ոq Mf^JFe%/;N}z.&xal%A|OϜS(ByC  $ZMmU 5fY$Y8h]N0XS,>bd? vQY+*B Xjċ&0Ɨ:i]'Adż0 | ׀QJ Yb*_ ֪CJg8VM=~ŧxeU{A؉ pY"^起B1a2s/5g ĊækSׂ*NVl"f9Obp9εeb34U4!mV Pm]z# ¡'^}e꧱cOSN?މBeUqWv_G?ttff,\V k`^HHo&ye qB7TֺsYEp(siƥ]62wtԁp[oG,s1DB/oVէY> @Hx Uba2~*<^-ogror=#`sNWӰpdžT?CV{7&{;4 "yUL{UI!XBw[ {* +HHxq1mt Y˯A\1y[N9\2g˯;Ffl[גJ#d#6KR9ǡG:;d,K"=/m3#M{ DdYB\j~'MVeEBb%!&w|rbQ~1vAsdqx~< Kf x|e/A`7srRqw a]ip O+)y,إƙ//vTh* ^\3:9NG| ɏ%ϰx79nx~Pe r!9≱^렰P8H'ĉ'7s=N\ޕrGܼ \\ȀZFW}ܰ֕F" WvƮӳ7_B\]tVUhaYQ;uZ]ؔ,,a^H'3WIhaNG'-VWZ_vNI/o:㗝/o!Vnl$$3I4f֋C6|>'y=?x䮾ShVQU(n|lvv#{4>8nȰ o^O?C50Kc-CCf)4Wh෼hhWU'٨~aN۪:#svyUe+8ۉ/IBZM*)ҤW B`Kv̯ެ(4JC#iwZYUx੖%ɒ qmy5mBQwExɄ}!Ɋ9qQb96|Jrxq>:o̾板3O~W[k_.LO옴< ^=mm9cWUȉ2?'$VM -Bˤ>%rvLi {cb#XWiɂ:&/3˗7f㝿;Vsj`R^uhBDzV9 ^?/U n@$3,jp۸͚VasLH3\Wr$k7-n]ű( !/W~?{]gYZei3B,^斱nۿr>n*+!l3̴۪:38Z>mBKY6 jB.~іcVVDdQ6ef//KeڴUD2%]t6lCPQUz;bGf;lUm@^X  vLj[ Wi/1.jHH(6x^f(rnvcὓ "R()q5\ra Ԣ*,V)٠6eRD/?".S<\/%{12x!N\'$s_\{C\]gN Y}گ67;9rO?y7#~G1bS\wΝI ޗidd_w|{Zclee˴ .b+* }v r'w>F/7|G߷SAd='r`$ָ\ޔ5;|UɰgUJOuB1L!ak0Ҡ " F\w:OI-WUHXo=OxnbHXm5!%@70lD+..\^;[/W?<|iUt83aƈʠɆ5V>L{og^iz?v }Lj|v )qV.1d,a3ąX>a @wz/,:q/+O{BȶTV.n>lLK%Ttg||Sa꧿:a$-ZJH5ljÏ^х3[>YN\Y_QiIz* "q3[)Y/~>#.\]Uvw-ZG7+ ĠB$~7 *GDzAraqû(u[䆶reLJKPQ=AA,ԉemUGixBdk**V ["A2(>euArjB,{,5 ۰V[P0T#yAK#[n>nB,UDr17l<г(=uy-Z&MRNwd,_aÿ1xc\_zBHk~7p#qگ d{ Yo-ҪzK )E|qN,) [Ɯ4 + ҝZ[ ?l W*AWa!]ߪ61b-[KqNB?3/(4t;;4v2p-͗뜱Vհ'C%8<1v @l#(gƮD6׼%?J܃|Y> g/hUuCK!6*_|nqkY"Y|DY..6yT ?AK\D6BH2j9V*ᇟ18珮nUi[^w7gvCc9tK-sg)4vs;fEPNV+5(ֻCVҪڰa?mVx\&֩UF C=b߇vOF2ق  ΋~!Ol6M *VټVbMbtV>? '2~綇jSk{mc-,LXyp q#v'ǀ*(la}\~;uYV+{/Bۈ/D&n7|1-5찄^Ijۺ%!/`9 ?h% 4?4x~ ۷# y!!;#I!a5Aa?p{!({qbDBrmޮ#n2w%vt9p5 Ѐc K[$$x=q3\YIXiHpbPgOV ɻl!ºM `d{}mq|VwpNsv7?I|!ۭ*8qܩ^w\ dg3%4ᜆnJCValbCZBa^H`^HȉX* b|7X0CaĐxm-BB$2 [urĕ5 Rk0>L9hs(z#qgb?^|B1(2 PlLf?~;+dB\bq sC+UN`{>O?+WVՁ&ho塡vadV7fۼY9O~6)4"uO^Vx%z*~ 7>.O3>7`劆8/d. _yN7{bPMh +֚[2lj}jz:Ӵq?ք""[f O 92=ȝ_Yy˾TǍ/OcCor%6 ္rnĐp;qxsV| M`űcBgom6a!%\Ll{ioO*2A^|`BUI!oYr_T[o{}~yWy6<_qX*M-q>B8sK[k=rۉÊZUеoWY| SUAmfh0*{κ'!9t CwJRHq 6MKx{wwh# Jl= ' WV97߮aİ)1mZ.\uX96Sm軼[cSVwj6U}9d,3󞎜]rpU͟v /4d\|i0ʓH[W.o,X8hC *Z?2,gM`\ZKQFV~ Kq šݭ:,|ՃGk`vHUmа"dյ y 93.]63_{kgƣoLY҄׆ۀw/hW׆LõV"gUACBapPHYaRygg  WUޠ1J|mpo۪B%%LTl5o5啶6轭76Lh8\~g8!qҐ@Ύ?μ#'It¹VMa(%x7Y^99gOqtؒ9O`n]ůX>_⾖UBܢ  |v~Hy 年VEEi[U*,Advqoa hhkG9x,3~C4}v)Z Oa%93g?o:3ol`̌ruY5;Q;1 @OG~whBn9x<˳`F?yzڅoXаCs;8v/fpV7iUqXh5Ҡ4:q7 R_1&Z* @0;w|ģjo}7<]wTq^K֨ט-CUZw,B?q s93_WJ.6UBDu96|IUYp~ ޲x|3w$B3ZcEU  z's_, b; ,'K# RTُAD=W 9|d?<::GFkpfd'CϦ7{}9VVhmMzu?2_#? 7-~7V@xץUuGa!~;k΄&BI< ~ӉO {͹[*o~ٞk^^B~Ф1Cnͪ5,M b_.}pwohXjP|"V{S=b)Mm 6Uuaa!ĠpxƵ"4sqՃlm{vw/v}g{厐FhXl#jAe-/Nglnh|n٧k5ærVmc͊e&Odtۭh Uo<YB <q@6 >U-_c OxЁq'NcIN24< iŚꋼ18;'<>xMBBRttޟmޯ 1\[P4q4ox\")]ZUw.w> 9IB{ @C"慆I<3Y[ {}|Girͫipp/o~|l`jE1r1e v3s KOWm|o\o+ڪ!ElB ys/˟yC+:3%wagX:d2c=s_z?tElXjAoV ChBDz CB懇EQℴ`gߗOg~N= e%mjw,˰fhԨYJl2bCm,"p N2igNO3S84,7ݵO6ashNc>}0r']nUܒv{e; 96 Cܿ DdQh.Va?my6{$v{k#KA6[Kj6`ؒSǃ3+ZUj7hZM?iBFw7F\ z`3 rjLJ VXVK.F@^{,\2yC|IkxB<8x Ϝ*\%fZU/슏JDd+QAWkj؄VKV<<9ﻳ-ے!9Q DD c[U/4 ,!mg%ssh "ۋ* :mU L+^E,I5l\>7CLnl =j l9VHf|EDdJުz%I'wVYX7Rh6U,d|Qd;zk9 Yi=vk[2X!!V܀'Vs@""&UN[UCX7ljx;a/),lUMkUot@ma3ZU/tmuQhMتce<7iҶBnגInЛVت:#[qE_]^KԼy3R("5zɊ[Um8yͼRٔA1UDDBCp}tX]Xً2 !""LlhE__^ ٶN^DD6BV) iFVԚZDdQhr6UUc'4fmrBK(?ɃHQh~Ԫzѿ$!. ,lu n[U_v, na_ClxjqaUٰV)ZPamxjUDDdeTi,ުlU6F<$ cZUkBDD֖'6FXYĊD[ KDD* iC[U ""KuYϰumUUl54 iURDDdShX ڪ2DDW)4F:ɋЩ kUMOZDDDsުlXj ""M4,g[U ""[* W[VeADDU.lLjUDDdkRؘV՝tEDDDv ت7_ ""UshhskUamtbCá{ǿV:TS>,;CSDDl\hߪXd冢ĵ>r]e:yY ,U[/_w{#~ !:p#|-v"""= X;`štqNN(^EeӺzX`Vu2pЪÂcw?H2vϕ7z=Gmh0ߘY;`լXͲ6.T? ""kVB 3v桅-hS&l;;8Xb* """JCQx}BC ""қ W gF} """m^hF{kNY;`3 """I1᪯A4ߩ """""""""""""""]PXĝIENDB`gdspy-1.4.2/docs/_static/fonts.gds000066400000000000000000000150461354474061200170660ustar00rootroot00000000000000X     library>A7KƧ9D/ZT    TXT  m553&{p{$5G5 6 $~&(<2)"*0T+p,V,*&)$)TH(x''B&\|%D^$rT#Ph!  Np h"&R) +z , .h 0/:/b/v-~*'& #!z t~p   & !f8"L##$p&R'V(* +z+++ *N ) ( T% "t &   X6 4.bT(!#2t$~ 31?:0 5=31=3G8G< aD4GFG= BsED?f;3 Lb=IK=IGLGNQPPGVGV=O=Mj\MPM3M$ZM!M"M)rM72MCMrMNw^NVO|^P0PQ $QmQ4R!RLTTT_SCKRQCPOZNKrM)KJPJ?J(III&II0II\Lb= twPv>t>tdttttTttAtv~t1s r5ro=qn*pnnqmulBl k KifiFf5FfGmvGlm\ynMoP[pp-qsCt,tu uuvPvwyhwP ~02 TdJ$>Hb6P|8^\TDh}||Bp|{{{h{{{~xR d R 0$Lvv~}d|zyh~xZ w w | | }P8~@~pF@~ n 8 p  T~ ~h | w w wwwXw6wx4ybzT{(}&t~ [==bG@G tzRf)TYXhEn { ^~u    ~e8F4{!|1kL KR ~ Z~nv*dlf&<hVb,t N (0 ~X"RJ jh ( v \|D*l:v6n~ R "0 $,Zj@<80^L,D44lTx2Nt~ ~x2TbTЬђb$ΆHͪ~˘|ʀ^ɤTȌh ŀpNhR Ь  Ӛ 0lԔNvҾ~\ƬX"°~ *  X Ƙ8Lj20pˎ̈ жR π θ P T ǰ X   X6>4`bPT(nt~ tP>>dTA~C 5=* -q߷B KܙܣFGF٪GGny_b[- U>bhP L-EdmTkT+Ow;k81N FIpjp" b=<'#!$7&Y){- L5=,=GqG B=r=GGtyzrR")$6Y   h  : B >E ,n    1 T z ~`De4+8OF^4!?|LC KB Lh (hv |!"n%F%fx$"( 4^  <T2v2v|N* $ x|*  !"z"> " R $X|jl^xVp>v22`HNt~> Zv&*nJ0\VnHB4\>0p@$ZPJ&Z&hRTThgdspy-1.4.2/docs/_static/fonts.py000066400000000000000000000046071354474061200167420ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### from matplotlib.font_manager import FontProperties from matplotlib.textpath import TextPath import gdspy def render_text(text, size=None, position=(0, 0), font_prop=None, tolerance=0.1): path = TextPath(position, text, size=size, prop=font_prop) polys = [] xmax = position[0] for points, code in path.iter_segments(): if code == path.MOVETO: c = gdspy.Curve(*points, tolerance=tolerance) elif code == path.LINETO: c.L(*points) elif code == path.CURVE3: c.Q(*points) elif code == path.CURVE4: c.C(*points) elif code == path.CLOSEPOLY: poly = c.get_points() if poly.size > 0: if poly[:, 0].min() < xmax: i = len(polys) - 1 while i >= 0: if gdspy.inside(poly[:1], [polys[i]], precision=0.1 * tolerance)[0]: p = polys.pop(i) poly = gdspy.boolean([p], [poly], 'xor', precision=0.1 * tolerance, max_points=0).polygons[0] break elif gdspy.inside(polys[i][:1], [poly], precision=0.1 * tolerance)[0]: p = polys.pop(i) poly = gdspy.boolean([p], [poly], 'xor', precision=0.1 * tolerance, max_points=0).polygons[0] i -= 1 xmax = max(xmax, poly[:, 0].max()) polys.append(poly) return polys if __name__ == "__main__": fp = FontProperties(family='serif', style='italic') text = gdspy.PolygonSet(render_text('Text rendering', 10, font_prop=fp), layer=1) gdspy.Cell('TXT').add(text) gdspy.write_gds('fonts.gds') gdspy.LayoutViewer() gdspy-1.4.2/docs/_static/holes.png000066400000000000000000000051021354474061200170460ustar00rootroot00000000000000PNG  IHDR6 IDATxݽnlWolsHD) t{h()zDEp%Az:!EAB x6$'#mmki{ lzaؽvqƱ0=&َ=Ȑl60$o%I`цv>$>oË'\CEI~IzLuד|ϒ29efgw/ $eXmvsuw1]*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@7c`f67F@ېDg7]F# m$=d7|aAN˜IH&c!y'ϓ|1#؆/w:{׵bT$lCVpTP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@" TP*@E@I6CrbH6cf.wo0M7uv]'ɐ;b1m|CUi.l8k'"F@u%y춱oW{"D@IN$.edx|$n[v.x<. fG9!0S¹,p $ xB $`©,p + xJ$"±p +$ K Pg &CJ>8X5zpy(! a%r?'wr;J< _'Ȏx<@eҚ"ր#1 pk xɚ"G! pKxȒ"'Ԁ-1 pK xɒ"g-! 0D<F2瀈"0D) 0!s xL"4DM9 0aS xL"30LL) 0#S x"34v@` xX# x,9" rœ# @x,)" v" x1"+r# B xc"+D<V xIKd^ru9_\$oo6$m9JXan,9?Sa`q}zp'ɛ=$o%`޶IȞmUv<~z/\&O[3{]&um_']LOc|M|x,&Ɗ{X2`kHض>x0< 85MݵSپP;NHO "`Ag߳nCv̊~mCMꅹu Tk֟7AL0яaSxXE6E~ѶϪORˀ?0heT9}q?՞@-f 롱l𞭴޳3 $l^{*&yZ̺`7Aڻ ob4n-6 u6#7z'ު=-QEm@X|bfO_dkZ6B,{7ѷDUl㻵 Fxj `ߡAeCO,m^qѯm=-x~%GmǸ!0AK gjzQcO_|bfX]Si΋dݽJhS[TDj.1|pWnc[^9W=D-p&igQZ~=-zbw_'\Xxp*r謞X,̼~-m;yC W ddwxXY89oxhچsCm &`XiSӴryã!n4ob,IVv`m̬ĩiydW^ ہ 3ߩe]>׮`;PiRӺ. 1XIS)yěkiOʞZt^v$I9U, mU(Fռ:sɮNoj0X9&$ԏjiIϬWփ6`0S46^{*&yZxU}ROK'ޜ_kg 3M11_۰]mm႙85MݵSپL5z+o9fў ~VZz] ہ []7잕+Ej0KX &6uޭ?o`5* AUyPۀY"ԣatn[x&OazzOmf S-qjw0F-Dr.\840;Y_ې|MccyC*?W=S0Ԉ3nXLچ'7ҽ>.ϩm4 S+qjZz'ު=-Q0TU׻M5+;#= 6`04vltOl鋕 ׯmȾI_0TӺ0XJ&ѷDUƀF1Z>~v"Fj0X:D[6dnFikU]󴣕Sپ&bJ1uLPڡ^9tTx4iCc=[iggӆ856Av :ݫ6 &zĬ[); LPrzK-0mჩqwOF-0m#`a*knE&kZ6B SU'1ynm5L &^4\.ap 09 o$m4`cycIdH* ?'lֱ1Iǃ-qjw^85n3ٕWBv`DMG߮t,L, =lp;LP|si's{Z6$#`ab]{ b~`TT{Y=S;%v0XHD>.>I-04UB;:ف1XH&$ԏjѯmX9W=0kvAi]wS72ӃL -&N41V#g8o>6``DIکl_ ӉL:&Fŭrz] ہچO}ԖuyUIb0A5玾]_J-04Ve{HIs 1IXD[6dnFi]w^SNj^ d 7 ,TLTİzvW1Y!&o>^9XSۀ AK?d_'{Ц U}ROK'ޜ_kg Em rzZ`k7ҽK< <0%w! Rފ;cN㎀կexbfO_A`viӭ֮vʕ" w,N2D}TWFm&  c)qju\--}{zf $,L%ob,IVv""bVo?[=g+l cINLva@_0ћ fMq]1X;lv75DAɍtϪmsv0nX+wj|ěk< ~j VwGzm!`alknōt/ _ې}~v"Fj0FX TuP}jKح )U]󴣕Sپ&b1"y?;֭&  Am 1\4wNeҎV8oà㈀ ڳJV`;Am #ejS\w& &Q_ Dm  #Am}~2xچ7??аSbadLPIGߩ 2xD䵰\P*zè Hjˆꅹoǩe o$¨0hDZmj?'lֱ1Qࡆs`x~D-"EMG߮[6{b fvKg?Ӳ.sޠsG߮F}Ta@TT{Y=S;%v0l, і g?۳QZ{P8 bzξNvAFmņ1AK gjzQoGn-lPۀ `a(a}^%e0H6`Xh ocHؕsC2j0*<0p2)Әw?s9CA@Q`a\.6fG}v*xқ^iE5Jd{;]Pԯ!k &([#fFh혐?Q_,|.n/oؽNZzEtHNFռ:N?}rg-  cjn_^IUع6WW|<zGE$JO^rCCL5|fr \aX`bL$kOwH9eFǞ72jvתL݀em$1 mФS _f*W J0Gwj76BJ^Q*R?YM@D1$߽ ԅ"`ab?kH)Ď֭t1, + [suVxEA"`aXqjor[7kVo1쎝 t`LDL^k\LA j0pP|^)\Ө|;W~XQqj Hq:W[IQ_5FkWbgdL21 , 1d!|xrvL],UBeο<ۑcr0$, MT{ϛm*-D#NjȜ-Sˀa!`ah!s|e}^n'hY$F5NWKb1L, U01ZI3<8wjXD_m&ٗϴoV]2؎a"`a_Pm&Y0w2A`$([f F}[۰JgmP5獒3 W6FnmCr0QcL;oxrj1,1,ۗץ2QEPnϛ{}B-F &D\<޺TJ^ع~-%wJf,F&5gJn݁w L8g~yJB-F &T=ZM4 Zr~.y5F_ռk~+jIi{Zq  #wN޸Dmj+>[|0:نcb,$NM.QaPˀqEX6KmPe"`alPaCd7^^iݠ㄀AmQT&^k\LZj0NX+6xZW;52`0Vmp?Qc{_o\)jㆀCmUB2sW/\B-c?`1K[Z' ~Tjs6Ηp  clr9vͪKR1X[6.4 F+2ev0Xk6{0獒3 Wg,5jkJ쬟l~IBc; c`EPn/7v$8AL& l24."s `a"P(j0X6ZL2&j;0Ƙ%~ck}AR1QX(߭m /V][k0y4,XL~mqXBaLӎl/ܾ!`axC&IydӼ;6%r[<j0XHT;gmG~e}3sRQL$&R41fΖ/Un'5j)&N_lPˀE &ƒܫƉyL~-ڍ]nRˀIEĊFb%g7\ίqN!0 R~csyp,L`bĖ_i|j?oqݔZL80&Z4K$UjRT׿}yyQ*GģlQb,uM֥D0؎i@TLPm&ٍ۾^s`; S`2iՠɹS+L  d2|yפӄAm09ƥ&,LjЯe8rl8oӆBm0 6.L3{)Cԡs1qbϽ8_yJ,L%jlr9vͪKR1XJ6MJLl+L+ x &jԯk_>7J$^aZ0mG!槙$ej0Xj6n-Cvَ2`0ըmF_˰ S`bĉ=󧛿Kp f 3{  pkn.nr[7YAL6|t& E15b[51CXĘgtw EJ:7Ny`f13Xqjor[7m*jiTOn_BRa m 5IʧZb/am[y|OOe!`aNMøgk$F ԯe_Yi̜e 3m YH*ܾoSa (P~~g%Ie,"`a&%A'ޯyܖ9jbk>_|ռkRˀYEL6K: 7Jwƥ1XYh%Ib, {ܕKPˀ̢(N4JSm_庣FmP(1rxR"jlǬ#`aQ<`b6rom_9SbGmh4jkX@D`e~ j7p 2y6H 7|Em 6.L3{ j'ܫ%17| jlr9vͪKRہ?F৙I0_i|-{ 6?.$jdyLC,GPPӎl/ܾ *ܖ7{j{#`@m~-%wJfl+XO+FM3j5e xHPwI/5/7wKSBL1\SWӯ7.Ĕx5<<Ę[J]SL-3whR.%2;@4kΔ~˽ j0 Pjs/5/ N&J\<޺ہ#`;DmI0W~c綯ל)1 x6`ZeA twvLʱyZGDm&Y!槙$4 `_pَ7Lh$IuPPj0qbTĞ{q$&a( ( $\ί]~}꒔v8,@6`D15K=4#`.V][k0y@񸩀Qۀq5ƴc* 'fA `@mUT ̭y''2A~-%wJf,c'FM3j5e 1dO6?+Cmj^o\)1  2   JT i]=ھKp qCMej0[[g^ں\w 'ܫ%17軵 .][H6f y !۲ 7ki|ܕj^AF_+>wfi,X6: Web^CF$6i̇?ŭ5m)0G`H+s|ؠ $i̇Nۈ#Qp h$&"AbD0TEE$W FBQ EUkL]lb:ႈ? Xƈ3FA5TTԥE) S3R 'v]$fȾ(;BH%F^?_Kǽ{m1? X#/\FR#sw(FRݷb4>3Wʶȯ~a0#{D`{R|_tM9IENDB`gdspy-1.4.2/docs/_static/offset_operation.png000066400000000000000000000061721354474061200213120ustar00rootroot00000000000000PNG  IHDR6 AIDATxݿ\}{v24" @]L\A\@@ )@–(dX,:3y`Xp̜9Y$7O챑)>#pFZqtYƔiZg?O~r̻֒UV|dv=Ir$ȋ# l~'9O2z`9-'9M|s^u'ٌ ғ/3>Ǘ~UP" %@P" %@P" %@P" Yܴv+e:y.{2;%S[ zZ@X[͗O::_5EL's[Ogb꣹!|5-I8}4Ngw1/% ,'9t~.~vh&%y>Q\fZryX@XTOhG6mMr-і> ;1rg {B&%@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %@P" %֟$mQ#SƮGn@$/)-9_%y22Vˏxgd|K+ ;#)'932>licd8 `֔YIΓl" h`M#cSϱ @P" %@P" %@P" %@P" l'dZ!iӜpmF{yWn.,'s[N4=Nٌ뎯%cW[Ocђ6i||iLY?7wgsZg׀$SO'}LgnI[yY8,dZ=~?>?kS{{kIy/>Gys_?$V8޽'fծbh/zl7bg =ev۷ۧG붷G\dʸh:~ͷn&ɽ98~<2vxδ2xdOOnEzG;ZhɵbrLmt;%ɸJFլ{9l=@Í5j}oͿi’\D@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(Jy>I#z^e|(ђ]h'_zkxwzoK[rg3L\Ȕ&Gſ=m?8;d$eyZN{w=C@$#ywZƸ4Zɓq/}Ϧyxr=o8^-& ,M@`d!FsruAD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(w=p=ڋǔx3jdLrcLe*=IŴ$1)hU{1Zr|7?wMϦ>%Sq16iOo~-fN?VOfsf}R_@Xܓ<ђl<ɯz9v'{LIne|MaqHk/+G;=N2] g j. 섽{ٯWŁ@(JD@(JD@(JD@(J^]N$u_@Wjicd$In5+Uҋx32>,,Ų~Z% 6şS1\ 9TG" iٺp5^JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(JD@(J]hU.^n)W'؅E2%Sv%_n>6,'2%8N?}4N(>WӒG8]DX"sƭVΗuZ`u~+cg',ck `~\)ɭծ,뱓 p9`i}n"(JD@(JD@(JD@(Jii#Ioi=IOHJ[Xeu'ivR P|>G8 -ͮgn'#?C@lIENDB`gdspy-1.4.2/docs/_static/photonics.gds000066400000000000000000003650601354474061200177470ustar00rootroot00000000000000X     library>A7KƧ9D/ZT     NRing @<0%I kz = W*8*>;:,+^ !~"M$N%'(k()M+l,a-.0.1i234x6^7>>8X9l:z;<~N=}>u{?dz@Ly4A-wBvBu0CsDnrnE-qEoFn0G>lGkKHziI hZIfJe^JcK bXKw`K_JL7]L\5LZMYMZWMUMTiMRMQENONN N N L^OLZQ=LNRL9TZLUKWsKXKZKZ\K]J_Jt`Jb$IcIFe!HfHVhGiGGjFlmFmEwoBDpDr CeshBtAvAwj@?x?cz>{D=|<};~:#9M8s765r4S3-20/.c-$+*_)J3'&%H#="!"9Mgkv|f7y5 z h S=:Ynz~@ N N LNJNIhMGMFFMDMC&MZAM@ L>L<L7;nK9Kw8dK 6J5cJ3I2lI 0Hz/G.G>,F+8E)E-(oDn'C%B$_B# A-!@L v?d1>u=<;R:z'9l8X7>64321i0..- ,a + ) D(k w' %$N;"!~ @+:>8E*H keAI$%:(T S  h Wy mN|NklM R!" " # z%H @& ' )J*+-$v.ck/g0i2q3-4S5r6789N:;< ="@>#?c$@?&(A'~A(B*6Ce+D,D.gEw/F1CF2GG4+G5HV7H8IF:I;J="Jt>J@/KAKZCBKDKFYKGLItL9KLNLLZN L^N N  m$HAe&xH_ENDCL@aނ;۲ O w Dו ? ӟVΗb2Ǩ'ƔRņ~|€1 v!# $_%$'W(o)+8,k./ 02l3i5c6j8d9;n%<>t@ (AC&DrFFEG!IhJLN N LKItGFYD4CBiA@/>7=";:M87.54+.21CL/.g,2+*6(Z'~ &($#"@~ gVKNGIQ`tʎ˭qig.kcvѝ i ֶ  @^ zڸ  x RGl#N N|m ~W "T(9Tq N OQERTi!UEWrYZ\5(]t_J`%bXce^jfhZiikKl n0okqrnsu0Wv$wy4z{}~N€|~ņƔ>Ǩ^x2bΗVlӟM(?וOM۲ނ^a,L;C*DNW_=xz&H0XY)XpTXXXXX+XVXXXXpXY-X> XX W W WWw0WB[W VVVCU2U]UTTTT: S4Sf_RR~ R!Q# P$7Po%aO$N#OT"O!bPc <PQ\QRARzSSSs-ST'TzTUkUXDUUV VB Vr V \V 6VWW"W8WKtWYMWc'WiWkWiWcWYeWK?W8W"WVV}VVVr0VB VUUUXnUGT!TzT'SSsS_R9RAQQ\PޞPcwOPOT)NڟO ڡRS S!T)LTwUUUVh#VNW,yWWX/X}%XPY {YJYYY'Z RZK}ZrZZZ)ZTZ[[[[+[V[[ZZZ-Z XZ Zr ZK Z Y/YZYYJY XX}1X/\WWW,VVh3U^UU T!T)# S$5S %_R$Qx#R"R!dS =SSTlTU@{UUV.V[VWWMWlWEXXSX X X Y ]Y; 6Y]Y|YYYtYNY'YYYYYeY>YYY|Y]Y;|YVX/XXXSXWmWFWM WVV[VU^U@7TTlSSޜS uROR'QxڡR ڣUEUVP#VNWGyWX+XX$Y]OYzZZfZ[&[HQ[|[\\9\k(\S\~\] ](]A)]WT]i]w]]]+]V]]w]i]W]A-]( X] \ \ \\k.\9Y\[[[H[0Z[ZfZYY]X1X\X+W WG!V#VP$3U%]UE$T)#T"U1!fU ?V%VWWnW}X3VX/XY8YYZmZXFZZ[ [6 [c [ ][ 6[[\ \\0t\>N\H'\N\O\N\H\>e\0>\\ [[[|[U[c.[6[ZZZXlZEYYY8XXX3]W6WnWVV%ޚUtU1MT%T)ڣUE ڤWXwX$YpOYzZWZ[*[&[Q\F{\\];]']R^ }^G^^^(_S_:~_____*_T_____+_V_____,_ W_ __ _: _^.^X^^G^ ]]/];Z\\\F[[0[*[ZZW Y!Yp#X$1Xw%\W$V#WX"W!gXN @XY1YZZd~ZW[0[o [\ \T\n\G] ]K] ] ] ^ ^^( 7^H^e^}^^u^N^'^^^^^e^>^^}^e^H^({^T].]]]K]\k\D\T\ [[o[Z[Zd4ZYY1XޙXNrWKWX$VڤW ڦZ[[&\Q\{\]X]^'^yR^|_%_t_`(`JS`}``a.a^)aTa~aabb)*b>UbOb\bebkbl+bkVbeb\bOb>b),b Wa a a aa^-a.X````J`._Y_t_%^^y^/]Z]X\ \!\#[$0[%ZZ$Y~#Y"Zw!iZ B[][\0\\]NX]1] ^E^^_o_VH_!__ `( `S `z ^` 7```aaua#Na,'a2a3a2a,a#da=a````{`zT`S-`(____Vj_D^^^E]]]NZ\3\ \0[߾[]ޗZqZwJY"Y~ڦZ ڧ]E]^9'^R_}__`N`(aSa[}aabCb)bSc~c@cvcc)dTd'dJdidd*dUddddd+dVddddd,d Vdi dJ d' dc,cWcvc@cbb-bCXaaa[a`.`NX__ _!^#^9$.]%Y]E$\$#\"]!j] C]^^^_$__Y`-2`} `aaWaoaHb !bBbs b b b _c 8c2cMcecxcucNc'cccccdc=cxcecMc2czbSb,bbsbBb ajaCaWa``}`-_Y_2_$ ^^^߽]ޖ]o]H\!\$ڧ]E ک_`b`)aGSa~bb~bc9)cTc~d2d~de )eHTeeef"fO*fxUffffg*g$Ug4g@gIgOgQ+gOUgIg@g4g$g+f Vf f f fxfO,f"VeeeeHe ,dWd~d2ccc9-bWb~b a!aG#`$-`b%W_$^#_?"_!k`! D``aTab bcZb3c cNccdpdQId"dd e eB eg _e 8eeeeeufNf'fffffde=eeeeezegSeB,eddddQidBcccNcbbcXb 1a aT`߼`ޕ`!n_G_?^ک_ ڪbccr*cTdHdeeke*fUfjfggGg*gUhh8hkhh*hUii5iSimi+iUiiiii+iUiiiii+im ViS i5 i hh+hVhkh8hgg+gGVgffjfe+ekVed dH!c#cr$,c%Vb$ah#a"bM!mb Ec ccd@dd[e<4e effVfqfJg#g6ge g g g _g 8hh5hLh_hnuhzNh'hhhhhzdhn=h_hLh5hgygRg+ggeg6gfhfAfVfeee<~dWd0d@ cc߻c ޓblbMEaahڪb ګe*ef +fvVfg?gghN+hVhi<iij+jEUj~jjkk?+kfUkkkkk+l Ull%l.l3l4+l3Ul.l%ll k+k Uk k k kfk?+kUjjj~jEj*iUii<hhhN*gUgg? f!fv#f $*e%Ue*$d#dx"d!neN Ge ffrfg!gs\g5h hShhiqiJJi#ii j j/ jS `js 9jjjjjujNj'jjjjjdj=jjjjjsyjSRj/+jiiiiJgi@hhhSh g}gsVg!/ffrf߹eޒeNkdDdxdګe* ڭgh7h,i Winij+jj,k(VkvkllHl,lVlm.m_mm+mVnn n<nUnk+n}Vnnnnn+nUnnnn}nk*nU Un< n n m m*mUm_m.lll*lHTlkkvk(j)jTj+i in!i "h$)h7%Sg$f#g"g|!og HhE!hhiVii]jF6jjkkTkrkKk$l*lV l l l `l 9mmm2mDmSum_Nmg'mlmmmlmgm_cmS<mDm2mmlylRl*llVl*kkgk@kTkjjjF|iUi.iVhh߸hEޑgjg|Cgfڭg ڮjbjk8-kXkl]lm m`-mWmnBnno,o?Wouoopp-,pSVpupppp+pVpq qqq+qUqq ppp*p Up p pu pS p-*pTooouo?o)nTn~nBmmm`(m Sl~l] k!k"k8$(j%Rjb$i=#i"j!pju Ij"k2kkl2l^l7mmUmmn snALns$nn n o o> ao] 9oxoooovoNo'oooooco<ooooxo]xo>Qo*nnnnsnAfn ?mmmUml{lTl2-kkk2߷jސjuijBii=ڮjb گlmfm.n/YnnoBoo.p4Xp~pqqFq-qWqr"rQr|r,rWrs s%s<sQ+scVsqs|sss+sUss|sqscsQ*s< Ts% s r r r)r|TrQ~r"qqq(qFSq}pp~p4o(oRoB}n n!n/"m$'mf%Ql$k#l>"l!qm Jmd#mnnhno_oM7ooppOpspLp%qqF qn q q aq :qrrr*r8vrCNrK'rPrRrPrKrCcr8<r*rrqqxqQq*qnqFqppep>pOpoooMzoSn,nhnm߶mdޏmhlAl>kگl ڰoop_/pZqqvqrrm.rYssEsss-t7Xtktttu,u?Wu`u~uuu+uVuuuuu+uUuuuuu*u Tu u~ u` u? u)tSt~ttkt7s(sRs}sEsrrm'rQq|qv q!p"p_$&o%Po$nk#n"o5!ro Ko$pIppq>q_q8rrTrrsts6Msg%ss s t t) atF :tatxtttvtNt'tttttct<tttxtatFxt)Pt)ssssgs6es>rrrTrqyqRq>+pppIߵoގogo5?nnkڰo ڱr(rr0sN[sttTtt/uqeqڱr( ڲtu u1u\v5vvw+ww0wZxxFxxx.y-Yy_yyyz-z+XzKzgzzz,zVzzzzz*zUzzzzz)z Tz ~zg zK z+ z(yRy}yy_y-x&xQx{xFxwww%w+Pvzv v5!u"u$$u %Nt$s#s"tU!tt Mu%u]uuvGvav9wwRwwwux*NxY&xx x x y by/ :yIy_yryyvyOy'yyyyycy;yyry_yIy/wyPx(xxxxYx*dw<wwwRwvxvQvG)uuu]߳uތtetU=ssڲt ڳwOwx2xh\xyycyy1zA[zz{{={t/{Y{||2|Z|-|X|||} },}.V};}E}M}Q}R*}QU}M}E};}.})} S| ~| | | |'|ZR|2||{{{t&{=P{{zzzAy$yOycyy x!xh"x$#w%MwO$v&#v"v!uw< Mw&wx5xxyayS:yyz z@zsuzNz'z{# {H {i { b{ ;{{{{|v| O|'||||| c|;{{{{{w{O{i({H{#zzzczs<z@z yyySwyPx)xx5w߳wދwMZvcOj'npnjcbZ;M>,vN'b:b:}G uNP& y߰+މ~a~:~.}ڶ~ ڷ <5`-w 3D],_1[4V/vY,W*U( R } v V&4P{_$,NxD"Lwv- !"<$%I$a#"!xb Q)GdP<+ZwP(  > [ ct <vO'b;tv[N>'aZ:+PuM&G߯ވba9aڷ ڸq 6`f@ 4^;s1 \5_/Y 7K\-kWw*Uwk\(K R7 |  &Pz_5 #Msx;!@Ku f!"$q%H$#D"!y Q7*Qd=?sxP))Os c <$2>vHON'RTRNHb>;2$uN&sO)a9s?tMQ%߯7އ`8Dڸ ڸ N7a5} 4B^~"S2\ A/`Y{-W*U( R | { ` A% PzS#"Mw~B K}u5 !"N$%H$y#" !yo R*LeK=LxwP) ( C c\ <rvO'b;r\uCN(& waL9KtL%L߮އo_ 8yڸ ڹ0 7$bpB 5_4j2\%Ns0Z1B-PW\ekoq*oTke\PB(1 R | %OsyN%"Mjw4 BJt p!$"$%G0$#V"!z S@+Pf>5hxQ)<_  d < #w,O2(6862,b#: uM&_<`9h5sKP$߭@ކ^7Vڹ0 ں \8b; 5>`xF3s] ,0IZd|-W*T' R || d I ,% OysF"Lvx>Is; !"\$ %F$#".!{z S, O fE>|=ygQ*  , dD <Yl|wO(b:|lYDu,M%g`=8|Er K#O ߭ޅz^.6ں ڻC9.cwB 6`+_ 3];`0Z(-5WAIPST*STPIA5(' Q | $O`y;!L_v+BIs w!."$%EC$#e"!{ TG,Ng?+[yR*)J i d =wO(b:tM%iJ)_8[+rJN#߬Gޅ]5e ڻC ڻh9d? 69`q 83c]02ZLcx-W*T~' Qx {c L 2 $Nxc8! Kuq9Hr? !"h$%E$#"8!| U-Pg=?r-yUR{*  d+ =@RbozwO(bz:obR@+tM%{U_-7r=qJ"P߫ބ\85 ڻ     NTaper ,<PP     Negative  !P ') / 7wm ?K G N V~ ^ e m tN {{_ + h< #" ', , F1 D6t < A G M T] }Z a h Աo 5v o~ _w  Z c?  ns P   9 [ }%   '  # *^ 1 8 o?x ;F3 LL S% <YV _T .e !j 'p ,u 2y 9 ~ ?m E L SE Z ao i pj x s . O  ~   ` ~  !IPI٠J}JWK]+LwM O ^Q S yV Y &.\ -[`O 4dd ;Hh, Bl Hq Nu U&{ [$d ` f| k p uM z] ~ |   O ?g  :ނ C/  i  Nc  @ ' /  7w ?K ] G w N V~ ^ e m tN {{ O h  # ,   F D  d ޼ } M Ա | 5 o  _   "g Z ) c 1  9/ @ H n P Xc  `@ ~  !P ]7M g>Y;?D (ss   T  \լ ۪ " '= ,= 0 4l 8qu ; ?/ BW D G(r I#0 J7 LI? McGy N.OS NW0 N q N y N g Or ; P=  QW R n T}  V X [ > ^q k a t e/ X h  m qc  u 6 z 4  D   m -   x  \ "% e %_ Œ (O * G -J /S b 1  2y 3 4^ i 4 C 5  5~  !P=`E=rM=TW\dq}ls{9q/$ cS DĬ<ϒ%Ԫ+ل2-8t?xF\QMeT[cGjrbz)Ci C  } W R +  7  ^ ] f y  a. Q[ d H $ * #C0 '7& ,=$ 1B 7$H| <M B|R HzW N\] U ` [d bXh i<l pEoO wrr? ~t 'w: yC Bz |i } u~N I~ #~ ~~  ! 'P '@ ' (b )- *G +Q -m /v% 1- 4q4 7a; :B >J AP EW JS^3 Nd Sj Xp ^4v c| ir o ud | T he L1 U   7  R  ˻ Ӆ# Y 3g  mG2$,4N=;FCiJAR1YKk`Tg8̻mt#zټޖl\Z M  } 8  "%? )R/ 0 8* ?3 G" NY Vs ^U> f)ȷ n u~  ! qP q q rR s t7u u1 w] yfw { ~ab Q     C u  " $(l - |3R z8j =D A ̝F4 XJE <N EQ rT W 'Zc \ B^ `~ a uc %Ic -#dG 5dpOdpW]d_7eg enfvhb~>jl&Yn}q!t;wKDz(~sv\t<:k=]q/2~ i # I kc 5.    '~  ! P S Z Bb j 'rU z M VW ŭ QB A {   c 3 U Ά Ԅ L  l2jJ $H%$,+5u2b9 A CHP2W^_geo9w'~PpPMy'B.I ]޲#%+)+4,0{4c8=|BVGn L&LRT+X0^5dK:-j>q=BwFa~IMP^RU IWXZ9K[S\\\٠\~  !P 2k$5 + =3 F;7BAJ"1QnkX_f#mC(#s,z51f6d<,AG\MZ*SY`}g8nu%U|R#z"ƒ>ũU)ǎ0`0=Yɝʷ q"!9=(/7 >$ޏE[KlRSX\^6dNjp<4u z+t#)Q07>F>MU)\do)l+Cs{~ NRing  NRing K! NRing % NRing ) NRing  )- NRing  s1 NRing  5 NRing 9 NTaperO NGrat ~~ ~     PGrat ,S\S ,i8S%S ڛJcJKL&GLrM5MN-NOJOxuOP;PP"Q9MQxQR RIR%RPR{S S3STSq(SSS~SSSS+SVSSSSSq.ST YS3 S R RR1RI]R QQQ9 P4P`P;OOxO N8N-cMM5 L!L&#K$;J%eJc$IN#I"J}!_K 8KLLM M|wMQNP*NOOhOP iPTCPPQ QM Q Q [Q 5QRR9RQRetRtMR'RRRRRtfRe?RQR9RQQ~QXQ1QM QPPPTpP JO#OhONNPMbM|<M LLKޡK {J}TI-INڛJc ڝM#MNJNIO[tOPVPQ; QKR wRlRSSq#SNTyTKTTT&U.QU[|UUUU(UTVVV*V0V2+V0VV*VVUU.U YU U U[ U.T0T\TTKTSSq3S^RRlR Q Q;6PaPVO O[!N# NJ$9M%cM#$L #L"M0!`M :N=NO3OPyP~RP+QBQQRCRjRDSSXS S S T& \TN 5TrTTTTtTMT'TTTTTeT?TTTTrTN~T&WS0S SSXSRoRIRC"QQQBPP~aP:OO3NN=ޠMyM0RL+L ڝM# ڟOPoPQKRvR~RSfS"T:MTxTUTUU$VCOVzVW WBWw&WQW}WXX>XY)XpTXXXXX+XVXXXXpXY-X> XX W W WWw0WB[W VVVCU2U]UTTTT: S4Sf_RR~ R!Q# P$7Po%aO$N#OT"O!bPc <PQ\QRARzSSSs-ST'TzTUkUXDUUV VB Vr V \V 6VWW"W8WKtWYMWc'WiWkWiWcWYeWK?W8W"WVV}VVVr0VB VUUUXnUGT!TzT'SSsS_R9RAQQ\PޞPcwOPOT)NڟO ڡRS S!T)LTwUUUVh#VNW,yWWX/X}%XPY {YJYYY'Z RZK}ZrZZZ)ZTZ[[[[+[V[[ZZZ-Z XZ Zr ZK Z Y/YZYYJY XX}1X/\WWW,VVh3U^UU T!T)# S$5S %_R$Qx#R"R!dS =SSTlTU@{UUV.V[VWWMWlWEXXSX X X Y ]Y; 6Y]Y|YYYtYNY'YYYYYeY>YYY|Y]Y;|YVX/XXXSXWmWFWM WVV[VU^U@7TTlSSޜS uROR'QxڡR ڣUEUVP#VNWGyWX+XX$Y]OYzZZfZ[&[HQ[|[\\9\k(\S\~\] ](]A)]WT]i]w]]]+]V]]w]i]W]A-]( X] \ \ \\k.\9Y\[[[H[0Z[ZfZYY]X1X\X+W WG!V#VP$3U%]UE$T)#T"U1!fU ?V%VWWnW}X3VX/XY8YYZmZXFZZ[ [6 [c [ ][ 6[[\ \\0t\>N\H'\N\O\N\H\>e\0>\\ [[[|[U[c.[6[ZZZXlZEYYY8XXX3]W6WnWVV%ޚUtU1MT%T)ڣUE ڤWXwX$YpOYzZWZ[*[&[Q\F{\\];]']R^ }^G^^^(_S_:~_____*_T_____+_V_____,_ W_ __ _: _^.^X^^G^ ]]/];Z\\\F[[0[*[ZZW Y!Yp#X$1Xw%\W$V#WX"W!gXN @XY1YZZd~ZW[0[o [\ \T\n\G] ]K] ] ] ^ ^^( 7^H^e^}^^u^N^'^^^^^e^>^^}^e^H^({^T].]]]K]\k\D\T\ [[o[Z[Zd4ZYY1XޙXNrWKWX$VڤW ڦZ[[&\Q\{\]X]^'^yR^|_%_t_`(`JS`}``a.a^)aTa~aabb)*b>UbOb\bebkbl+bkVbeb\bOb>b),b Wa a a aa^-a.X````J`._Y_t_%^^y^/]Z]X\ \!\#[$0[%ZZ$Y~#Y"Zw!iZ B[][\0\\]NX]1] ^E^^_o_VH_!__ `( `S `z ^` 7```aaua#Na,'a2a3a2a,a#da=a````{`zT`S-`(____Vj_D^^^E]]]NZ\3\ \0[߾[]ޗZqZwJY"Y~ڦZ ڧ]E]^9'^R_}__`N`(aSa[}aabCb)bSc~c@cvcc)dTd'dJdidd*dUddddd+dVddddd,d Vdi dJ d' dc,cWcvc@cbb-bCXaaa[a`.`NX__ _!^#^9$.]%Y]E$\$#\"]!j] C]^^^_$__Y`-2`} `aaWaoaHb !bBbs b b b _c 8c2cMcecxcucNc'cccccdc=cxcecMc2czbSb,bbsbBb ajaCaWa``}`-_Y_2_$ ^^^߽]ޖ]o]H\!\$ڧ]E ک_`b`)aGSa~bb~bc9)cTc~d2d~de )eHTeeef"fO*fxUffffg*g$Ug4g@gIgOgQ+gOUgIg@g4g$g+f Vf f f fxfO,f"VeeeeHe ,dWd~d2ccc9-bWb~b a!aG#`$-`b%W_$^#_?"_!k`! D``aTab bcZb3c cNccdpdQId"dd e eB eg _e 8eeeeeufNf'fffffde=eeeeezegSeB,eddddQidBcccNcbbcXb 1a aT`߼`ޕ`!n_G_?^ک_ ڪbccr*cTdHdeeke*fUfjfggGg*gUhh8hkhh*hUii5iSimi+iUiiiii+iUiiiii+im ViS i5 i hh+hVhkh8hgg+gGVgffjfe+ekVed dH!c#cr$,c%Vb$ah#a"bM!mb Ec ccd@dd[e<4e effVfqfJg#g6ge g g g _g 8hh5hLh_hnuhzNh'hhhhhzdhn=h_hLh5hgygRg+ggeg6gfhfAfVfeee<~dWd0d@ cc߻c ޓblbMEaahڪb ګe*ef +fvVfg?gghN+hVhi<iij+jEUj~jjkk?+kfUkkkkk+l Ull%l.l3l4+l3Ul.l%ll k+k Uk k k kfk?+kUjjj~jEj*iUii<hhhN*gUgg? f!fv#f $*e%Ue*$d#dx"d!neN Ge ffrfg!gs\g5h hShhiqiJJi#ii j j/ jS `js 9jjjjjujNj'jjjjjdj=jjjjjsyjSRj/+jiiiiJgi@hhhSh g}gsVg!/ffrf߹eޒeNkdDdxdګe* ڭgh7h,i Winij+jj,k(VkvkllHl,lVlm.m_mm+mVnn n<nUnk+n}Vnnnnn+nUnnnn}nk*nU Un< n n m m*mUm_m.lll*lHTlkkvk(j)jTj+i in!i "h$)h7%Sg$f#g"g|!og HhE!hhiVii]jF6jjkkTkrkKk$l*lV l l l `l 9mmm2mDmSum_Nmg'mlmmmlmgm_cmS<mDm2mmlylRl*llVl*kkgk@kTkjjjF|iUi.iVhh߸hEޑgjg|Cgfڭg ڮjbjk8-kXkl]lm m`-mWmnBnno,o?Wouoopp-,pSVpupppp+pVpq qqq+qUqq ppp*p Up p pu pS p-*pTooouo?o)nTn~nBmmm`(m Sl~l] k!k"k8$(j%Rjb$i=#i"j!pju Ij"k2kkl2l^l7mmUmmn snALns$nn n o o> ao] 9oxoooovoNo'oooooco<ooooxo]xo>Qo*nnnnsnAfn ?mmmUml{lTl2-kkk2߷jސjuijBii=ڮjb گlmfm.n/YnnoBoo.p4Xp~pqqFq-qWqr"rQr|r,rWrs s%s<sQ+scVsqs|sss+sUss|sqscsQ*s< Ts% s r r r)r|TrQ~r"qqq(qFSq}pp~p4o(oRoB}n n!n/"m$'mf%Ql$k#l>"l!qm Jmd#mnnhno_oM7ooppOpspLp%qqF qn q q aq :qrrr*r8vrCNrK'rPrRrPrKrCcr8<r*rrqqxqQq*qnqFqppep>pOpoooMzoSn,nhnm߶mdޏmhlAl>kگl ڰoop_/pZqqvqrrm.rYssEsss-t7Xtktttu,u?Wu`u~uuu+uVuuuuu+uUuuuuu*u Tu u~ u` u? u)tSt~ttkt7s(sRs}sEsrrm'rQq|qv q!p"p_$&o%Po$nk#n"o5!ro Ko$pIppq>q_q8rrTrrsts6Msg%ss s t t) atF :tatxtttvtNt'tttttct<tttxtatFxt)Pt)ssssgs6es>rrrTrqyqRq>+pppIߵoގogo5?nnkڰo ڱr(rr0sN[sttTtt/uqeqڱr( ڲtu u1u\v5vvw+ww0wZxxFxxx.y-Yy_yyyz-z+XzKzgzzz,zVzzzzz*zUzzzzz)z Tz ~zg zK z+ z(yRy}yy_y-x&xQx{xFxwww%w+Pvzv v5!u"u$$u %Nt$s#s"tU!tt Mu%u]uuvGvav9wwRwwwux*NxY&xx x x y by/ :yIy_yryyvyOy'yyyyycy;yyry_yIy/wyPx(xxxxYx*dw<wwwRwvxvQvG)uuu]߳uތtetU=ssڲt ڳwOwx2xh\xyycyy1zA[zz{{={t/{Y{||2|Z|-|X|||} },}.V};}E}M}Q}R*}QU}M}E};}.})} S| ~| | | |'|ZR|2||{{{t&{=P{{zzzAy$yOycyy x!xh"x$#w%MwO$v&#v"v!uw< Mw&wx5xxyayS:yyz z@zsuzNz'z{# {H {i { b{ ;{{{{|v| O|'||||| c|;{{{{{w{O{i({H{#zzzczs<z@z yyySwyPx)xx5w߳wދwMZvcOj'npnjcbZ;M>,vN'b:b:}G uNP& y߰+މ~a~:~.}ڶ~ ڷ <5`-w 3D],_1[4V/vY,W*U( R } v V&4P{_$,NxD"Lwv- !"<$%I$a#"!xb Q)GdP<+ZwP(  > [ ct <vO'b;tv[N>'aZ:+PuM&G߯ވba9aڷ ڸq 6`f@ 4^;s1 \5_/Y 7K\-kWw*Uwk\(K R7 |  &Pz_5 #Msx;!@Ku f!"$q%H$#D"!y Q7*Qd=?sxP))Os c <$2>vHON'RTRNHb>;2$uN&sO)a9s?tMQ%߯7އ`8Dڸ ڸ N7a5} 4B^~"S2\ A/`Y{-W*U( R | { ` A% PzS#"Mw~B K}u5 !"N$%H$y#" !yo R*LeK=LxwP) ( C c\ <rvO'b;r\uCN(& waL9KtL%L߮އo_ 8yڸ ڹ0 7$bpB 5_4j2\%Ns0Z1B-PW\ekoq*oTke\PB(1 R | %OsyN%"Mjw4 BJt p!$"$%G0$#V"!z S@+Pf>5hxQ)<_  d < #w,O2(6862,b#: uM&_<`9h5sKP$߭@ކ^7Vڹ0 ں \8b; 5>`xF3s] ,0IZd|-W*T' R || d I ,% OysF"Lvx>Is; !"\$ %F$#".!{z S, O fE>|=ygQ*  , dD <Yl|wO(b:|lYDu,M%g`=8|Er K#O ߭ޅz^.6ں ڻC9.cwB 6`+_ 3];`0Z(-5WAIPST*STPIA5(' Q | $O`y;!L_v+BIs w!."$%EC$#e"!{ TG,Ng?+[yR*)J i d =wO(b:tM%iJ)_8[+rJN#߬Gޅ]5e ڻC ڻh9d? 69`q 83c]02ZLcx-W*T~' Qx {c L 2 $Nxc8! Kuq9Hr? !"h$%E$#"8!| U-Pg=?r-yUR{*  d+ =@RbozwO(bz:obR@+tM%{U_-7r=qJ"P߫ބ\85 ڻ     PTaper ,شPPش ,<'LiPP<     Positive ,PP $imm]YMM8(uZX@t.G~$|X{#yIx,vuA{srb"pjon/lkuEjjhgf6dcbwaB`^]{\[[6Zt YfX^W\kV`~,Uk|T|{SzPRxQwQvMP8tOrsNr%MpMKoMLmLlhKfjJiwJIgIf{IJdHcuHjaH`gG^GT]RG[FZ7FXFRWF%UFSERbEPEO=EMELEJEHFGcF%EFRDCFBFA(G?GT>G<H;Hj9H7IJ6I5JI3J2Kf0L/-L-MK,UM*N)Or(-P8&Q%|Q$*R"S!T| NUkV`W\X^nYfDZt[\]^`aBbwcdf6gh 5j aku l n/ o Xprbs]uAv1x,y"{#|3~$b.@e Xu^A(-!MPiPi $m i i!s-A^)K he b3"1 ]^ X .   aK 5AI~Ǯ8DLnZbd` NU!D",$* %|&ּ(-׈)N* ,U-u/-02Z35w67v9;V<߻>?lA(BDC:EnGcHJLMO=PRbSUWXnZ7:[]R^l`ga߻cuVdf{vgiwwjlhZmoMpur%s tNvM׈wּxzP {,|D~,Uk`db Z6L[8{Ǯ~IAjEKj."^{ IXGt ZhK)8MYs]mm  PGRZGSGUvGWGXHZHI[H].H^I`;IlaIcAJ.dJf>KgKi2LjLlM,mMnNip_OqOs'P{tQ:uRw5RxSyT|{U^|aVG}W6~X+Y'?Z)j[1\@]T^n_`acdCe}fhiI{jPkm>noZqX rt'Vuwxuy{d|~\T^gRx 5Zv4QQ4 Eqx(tg&^~\k|{dkyxuw $u t' or qX o n ]m>*kjiIhfe}dCca`_^n]T\@[1:Z)jY'X+ W6"VG#]U^$T|%S'ER(R)Q:+SP{,O.O/Ni0M2`M,3L5HL6K8<K9J;9J.<I>?Il?IALHBHD_HIEHGvGIGJGL GMGO=GPG ovZ5 3HRYTbd\JV- Zh(P{)wC}IJ 2Rlˀj̏?͗Ι~ϕ}Њ|ay{byDxw5utֆs'Eqp_خnWmlڔj'i2۲g6f>ܲd&cAݒa`;T^ީ].[:ZwX߬WUvSRZ.P:O=>M:L .JIGvE߬D_wB:AL?ީ>?T<;9ݒ9&8<ܲ665H۲3'2`ڔ0/W.خ,+SE)ֆ('E%$D#]b"y ЊϕjΙ:͗̏ˀlR2 IJ}Cw)* ] (  h o  $-Jk\kd&btY(Hq3E oo ,I55PIPI $^Yqqa]MQ<( u^X@x.K~$|\{#yMx,v!uAsrb&pnoݯn/lkuIjnhٍgإf6׶dcbwaBӻ`ҭ^љ]\`[:ZtYfX^ʪW\oV`0UkT|ŢSTRQQQP8OrN)MMKQLLlKfJ{JIIIJHyHjHkGGTVGF;FFRF%FEfEEAEEEEFgF%FRGFF,GGTGH HjHIJIJI}J|KfzLy1LwMKvYMtNsOrr1P8pQoQn-RlSkT|jRUkiV`gW\fX^erYfdHZtc"[b\`]_^^`]aB\bw[cZdYf6XgXhW9jVekuUlTn/ToS\pRrbRsQauAPvP5x,OyO&{#N|N7~$MMf.M L@LiL$XKKuKKbKE(K1K%MK!JJP^YP^Y $q ^Y ^YJJK!K%sK1KEKb)KKKKL$hLiLM  MfMN7NO&OP5P QaRR^S\T.TUVeKW9XXAYZ[\I]~^Ǯ_`bc"8dHLerZfbgdi`jRUkDl,n- opּr1׈sNt vYwuy1z|Z}wv V߻l,G:ngAfn;:Vlk߻yVv{wlZQu) NQ׈ּT Ţ,D0Uo`ʪdbZ:L`8љҭӻǮ~I׶إٍAnIKݯn.&^! M\Kx ^hK )<Q]saqq  G^GGzGGHHIH2HI?IlIEJ.JBKK6LLM,MNicOO+P{Q:R9RŠST|!U^eVGǤW6X+Y'DZ)n[1͓\@γ]T^n_`acdCe}fhؤiIjTk!m>nܦo^qXr޸t'Zuwxuy{d|~\X^ gVx 9^z4QLLQLM4M$MIMuMMxN,NxgNO*^OO~\Po|P{dQoyQxuRwS(uSt'TsrU$qXUoVnWam>X.kYjYiIZh[f\e}]dC^c_a``a_b^nc]Td\@f[1g>Z)hnY'iX+jW6lVGmaU^nT|oSqIRrRsQ:uWP{vOxOyNizM|dM,}LLLK@KJ=J.ICIlIPHHcHIHzGGG$GGAGG oz^9 3HV YXbd\JZ-޸^hܦ(!T)ؤwC}IJ 2Rγl͓ˀn̏D͗ΙϕǤЊey!bDŠ9ֆ+EcخWڔ'6۲6Bܲ&Eݒ?Tީ2:w߬z^.:A>:$.z߬cw:PީCT=ݒ&@ܲ6L۲}'|dڔzyWxخvuWEsֆrqIonDmablyjЊiϕhnΙg>͗f̏dˀclbRa2` _^IJ]}\C[ZYwY)X.WaV(UU$hTsSS(-RQJQoP\PoOdOO*bNNxYN,MHMMu3MIM$MLLoLo ,wwPP $IXuXu1e1aM1U1@(1$10u00bX0/@/|/ ...O~$-|-`{#,y,Qx,+v+%uA*s)rb)*p(ro'n/&l&!ku%Mj$rh#g"f6!d cbwaB`^]\d[>ZtYfX^W\sV`4UkT|SXR Q Q UP8OrN-MMKULLpKfJJIIIJH}HjHoGGTZGF?FFRF%FEjEEEEE EލEFkF%FRKFֽF0GӤGTGГH Hj͉HIJʈI JIǑJKfĦL5LMK]MNOr5P8QQ2RST|VUkV`W\X^vYfLZt&[\]^`aBbwcdf6gh=jikuln/o`prbseuAv9x,y*{#|;~$j.@m(XufI(5)M%PIPI $Xu I I%)s5If)K(hm j;*9 e^`.iK=AI~Ǯ&8LLvZbd`VUD,2 ּ5׈N ]u5ĦZǑ wʈv͉ VГ߻Ӥl0ֽK:nkލ Ejn?:Zlo߻}VvwpZUu- N U׈ ּ X ,D4Us`dbZ>Ld8Ǯ~I !"#A$r%M&!K&'(r.)*)^*+%+ ,Q,-`-.O./ /| /00bh00K11$)1@1U1as1eXuXu  GbGG~G GH#HIH6HICIlIIJ.JFKK:LL"M,MNigOO/P{Q: R =R S T|%U^iVGW6X+Y'GZ)r[1\@]T^n_`acdCe} f!h"iI#j$Xk%%m>%n&o'bqX(r(t')^u)w*xu+y+{d,|,~\,-\^-.g.Z.x.//=/b/~4//Q/Q 4(Myx0|g.^~\s|{dsyxuw,ut'wr(qXonem>2kjiIhfe}dCca`_^n]T\@[1BZ)rY'X+W6!VGeU^T|SMRRQ:[P{O#OÌNiMhM,LPLKDKJAJ.IGIlITHHgHIH~G GޙG(GGEGG /o//~/b/=/3..H.Z.Y--\b,,d,+\+*J))^-(('bh&%(%%$X#)"w! C}IJ 2RlˀȑG͗ΙϕЊiy%b D  = ֆ/EgخW"ڔ':۲6Fܲ&IݒCTީ6:#w߬ ~b.:E>:(.ޙ ~߬gw:TީGTAݒ&Dܲ6P۲'hڔÌW#خ[EֆMDeb!yЊϕrΙB͗̏ˀlR2 IJ}Cw)2e((hw,-Js\sd.b|Y0Hy3M( o//o ,ggPP $9yy{i{eM{Y{D({({zuzzfXz!y@yy$.xxS~$w|wd{#vyvUx,uvu)uAtssrbs.prvoqn/plp%kuoQjnvhmglf6kdjcibwhaBg`f^e]d\cg[bBZtaYf_X^^W\]wV`\8UkZT|YSX\RW QUQTYP8ROrQNP1MNMKMYLKLJtKfHJGJIFIDIJCHAHj?H>sG<GT;^G9F8CF6FR5#F%3F2E0nE.E-IE+E*$E(E'F%oF%#FR"OF F4GGTGHHjH IJIJIJKfL 9L MK aMNOr9P8QQ5RST|ZUkV`W\X^zYfPZt*[ \]^`aBbwcdf6ghAjmkuln/odprb siuAv=x,y.{#|?~$n.@q,X߻uߎjM(9-M)ޱޱP9P9 $y 9 9ޱޱ)-s9Mj)ߎ߻K,hq n?.= i ^d.mKAAI~Ǯ *8PLzZbd`ZUD,5 ּ9׈N  a u 9Zw vV߻l4 "O:#n%o'(*$+-I.0n235#6n8C:9;^<l>s?߻AVCDvFGwHJtZKMYNuP1Q RNTY׈UּW X\ Y,ZD\8U]w`^d_baZbBLcg8defgǮh~iIjklmAnvoQp%Kpqrv.s.s^tu)u vUvwdwxSxy$y yz!zfhzzK{{(){D{Y{es{iyy  .G0fG1G3G5G6H8'HI9H;:H<I>GIl?IAMJ.BJDJKEKG>LHLJ&M,KMMNiNkOOOQ3P{RQ:SRUARVSWT|Y)U^ZmVG[W6\X+^Y'_KZ)`v[1a\@b]Tc^nd_e`gahchdCie}jfkhliImjn\ko)m>onpoqfqXrrrt'sbuswtxuuyu{dv|v~\vw`^wxgx^xxxyyAyfy4yyQyQ4,Q}x4g2^~\w|{dwyxuw0ut'{r,qXonim>6k jiIhfe}dCca`_^n]T\@[1FZ)vY'X+W6%VGiU^T|SQRRQ: _P{ O 'O NiMlM,LTLKHKJEJ.IKIlIXH H"kHI#H%G'G(G*,G+G-IG.G yoyyyfyAy3xxHx^xYww`bvvdvu\utJssb-rrqfhpo(o)n\m)lwkjiCh}hIJge d2cRblaˀ`v̏_K͗^Ι\ϕ[ЊZmyY)bWDVUASRֆQ3EONkخMWKJ&ڔH'G>۲E6DJܲB&AMݒ?>GT<ީ;:9:8'w6߬5310f..:-I>+:*,.('%#߬"kw :XީKTEݒ&Hܲ6T۲'lڔ W 'خ  _EֆQDib%yЊϕvΙF͗̏ˀlR2 IJ}Cw )6i(,h{0-Jw\wd2bY4H}3Q,oyyo , & W WP &P & $ <) } } m iM ] H( ,  u ħ jX % @ Ä (.  W~$ | h{# y Yx, v -uA s rb 2p zo n/ l )ku Uj zh g f6 d c bw aB ` ^ ] \ l[ FZt Yf X^ W\ {V` <Uk T| S `R Q Q ]P8 Or N 5M MK ]L L xKf J JI I IJ H Hj H wG GT bG F GF FR 'F% }F |E zrE xE wME uE t(E rE qF osF% mFR lSF jF i8G gGT f#G dH cHj aH `IJ ^I ]JI [J Z"Kf XL W=L UMK TeM RN QOr P=P8 NQ MQ L9R JS IT| H^Uk GV` EW\ DX^ C~Yf BTZt A.[ @\ >] =^ <` ;aB :bw 9c 8d 7f6 7g 6 h 5Ej 4qku 3l 2n/ 2 o 1hp 0rb 0s /muA .v .Ax, -y -2{# ,| ,C~$ + +r. + *@ *u *0X ) )u ) )n )R( )= )1M )- ( (P <)P <) $ }  <)  <) ( ( )- )1s )= )R )n) ) )K ) *0h *u * + +r + ,C , -2 - .A . /m 0 0^ 1h 2 . 2 3 4qK 5E 6  7A 7 8 9 :I ;~ <Ǯ = > @ A.8 BTL C~Z Db Ed G` H^U ID J, L9 M Nּ P=׈ QN R Te Uu W= X Z"Z [ ]w ^ `v a cV d߻ f# gl i8 j lS: mn os q r t( u wM x zr | } ' n G:  b l w ߻ V  v  w  xZ  ] u 5  N ]׈ ּ  ` , D <U {` d b Z FL l8    Ǯ ~ I    A z U )K   z. 2 ^  -  Y  h  W  ( Ä  % jh ħ K  ,) H ] is m } }   xG zjG {G }G G H +HI H >H I KIl I QJ. J NK K BL L *M, M Ni oO O 7P{ Q: R ER S T| -U^ qVG W6 X+ Y' PZ) z[1 \@ ]T ^n _ ` a c dC e} f h iI j `k -m> n o jqX r t' fu w xu y {d | ~\  d^  g b §x   E j Æ4 Û çQ ë * *Q * +4 +0 +U + + +x ,8 ,g , -6^ - .~\ .{| .{d /{y 0xu 0w 14u 1t' 2r 30qX 3o 4n 5mm> 6:k 7j 7iI 8h 9f :e} ;dC <c =a >` ?_ @^n A]T B\@ D [1 EJZ) FzY' GX+ HW6 J)VG KmU^ LT| NS OUR PR RQ: ScP{ TO V+O WNi YM ZpM, [L ]XL ^K `LK aJ cIJ. dI fOIl gI i\H jH loHI mH oG qG rG t0G uG wMG xG  ço Û Æ j E 3  §H b Y  db  d  \  J  f-   jh  ( - ` ) w   C } IJ   2 R l ˀ z̏ P͗ Ι ϕ Њ qy -b D  E  ֆ 7E  oخ W  *ڔ ' B۲ 6 Nܲ & Qݒ  KT ީ > : +w ߬  } { zj. x: wM> u: t0. r q o m߬ low j: i\ gީ fOT d cIݒ a& `Lܲ ^6 ]X۲ [' Zpڔ Y WW V+خ T ScE Rֆ P OU N LD Kmb J)y HЊ Gϕ FzΙ EJ͗ D ̏ Bˀ Al @R ?2 > = <IJ ;} :C 9 8 7w 7) 6: 5m 4( 3 30h 2 1 14- 0 0J /{ .\ .{ .d - -6b , ,Y ,8 +H + +3 +U +0 + * *o * ë ço , p ]G ]GP pP p $  6 6 q mM a L( 0   u  nX ) @  ,.  [~$ | l{# y ]x, v 1uA s rb 6p ~o n/ l -ku Yj ~h g f6 d c bw aB ` ^ ] \ o[ JZt Yf X^ W\ V` @Uk T| S dR Q Q aP8 Or N 9M MK aL L |Kf J ۋJI I ؏IJ H ՉHj H {G GT fG F KF ʼFR +F% ǚF E vE E QE E ,E E F wF% FR WF F <G GT 'G H Hj H IJ I JI J &Kf L AL MK iM N Or AP8 Q Q =R S T| bUk #V` W\ X^ Yf XZt 2[ \ ] ^ ` aB bw c d f6 g $h Ij ~uku }l |n/ |$o {lp zrb zs yquA xv xEx, wy w6{# v| vG~$ u uv. u t@ ty t4X s su s sr sV( sA s5M s1 r rP P  $ 6     r r s1 s5s sA sV sr) s sK s t4h ty t u uv u vG v w6 w xE x yq z z^ {l |$. | } ~uK I $ A    I ~ Ǯ    28 XL Z b d #` bU D , =  ּ A׈ N  i u A  &Z  w  v  V ߻ ' l <  W: n w   ,  Q  v  ǚ + ʼn K:  f l { ߻ ՉV  ؏v  ۋw  |Z  a u 9  N a׈ ּ  d , D @U ` d b Z JL o8    Ǯ ~ I    A ~ Y -K   ~. 6 ^  1  ]  l  [  ,   ) nh  K   0) L a ms q 6 6   G nG G NJG G ʤH /HI ͹H BH I OIl I UJ. J RK K FL ܻL .M, ߞM Ni sO O ;P{ Q: R IR S T| 1U^ uVG W6 X+ #Y' SZ) ~[1 \@ ]T ^n _ ` a c dC e} f h iI j dk 1m> n o nqX r t' ju w xu #y {d #| ~\  h^  g f x   I n 4  Q  t tQ u u4 u4 uY u u ux v< vg v w:^ w x ~\ x| x{d yy z xu zw {8u {t' |r }4qX }o ~n qm> >k j iI h f e} dC c a ` _ ^n ]T \@ $[1 NZ) ~Y' X+ W6 -VG qU^ T| S YR R Q: gP{ O /O Ni M tM, L \L K PK J MJ. I SIl I `H H sHI H G G G 4G G QG G  o   n I 3  H f Y  hb  d # \ # J  j-   nh  ( 1 d ) w   C } IJ   2 R l ˀ ~̏ S͗ #Ι ϕ Њ uy 1b D  I  ֆ ;E  sخ W ߞ .ڔ ܻ' F۲ 6 Rܲ & Uݒ  OT ީ B ͹: /w ʤ߬  NJ  n. : Q> : 4.    ߬ sw : ` ީ ST  Mݒ & Pܲ 6 \۲ ' tڔ  W /خ  gE ֆ  Y  D qb -y Њ ϕ ~Ι N͗ $̏ ˀ l R 2   IJ } C   w ) > q ~( } }4h | { {8- z z J y x\ x x d w w:b v vY v< uH u u3 uY u4 u u to t  o ,  7 7P P  $ YuYqMYeYP(Y4YXuXXrXX-W@WW0.VV_~$U|Up{#TyTax,SvS5uARsQrbQ:pPoOn/NlN1kuM]jLhKgJf6IdHcGbwFaBE`D^C]B\As[@NZt?#Yf=X^<W\;V`:DUk8T|7S6hR5Q3Q2eP81Or/N.=M,MK+eL)L(Kf' J%JI$I"IJ!HHjHGGTjGFOFFR/F%F EzE E UE E0EE F{F%FR[F F @G GT +G H Hj H IJ I JI J *Kf L EL MK mM N Or EP8 Q Q BR S ݫT| fUk 'V` W\ طX^ ׆Yf \Zt 6[ \ ] ^ ` aB bw c d f6 g (h Mj yku Ǭl n/ (o pp rb s uuA v Ix, y :{# | K~$  z.  @ } 8X  u  v Y( E 9M 5  P P  $       5 9s E Y v)  K  8h }   z  K  :  I  u  ^ p (.  Ǭ yK M ( A    I ~ Ǯ    68 \L ׆Z طb d '` fU ݫD , B  ּ E׈ N  m u E  *Z  w  v  V ߻ + l @ [:n{ 0  U z /nO:jl߻V!"v$%w' (Z)+e,u.=/ 1N2e׈3ּ56h 7,8D:DU;`<d=b?#Z@NLAs8BCDEǮF~GIHIJKALM]N1KNOP.Q:Q^RS5S TaTUpUV_VW0W WX-XrhXXKYY4)YPYeYqsYu   GrGGGGH3HIHFHISIlIYJ. J"VK#K%JL&L(2M,)M+Ni,wO-O/?P{0Q:1R3MR4S5T|75U^8yVG9W6:X+<'Y'=WZ)>[1?\@@]TA^nB_D`E aFcG dCGe}HfIhJiIKjLhkM5m>MnNoOrqXP#rPt'QnuR wRxuS'yS{dT'|T~\UUl^UVgVjVxVW!WMWrW4WWQW  Q  4 8 ]   x @ g  >^  ~\ ƒ| {d Ãy xu ġw <u t' Ƈr 8qX o ȯn um> Bk j iI h ͼf Ϋe} ϠdC Мc ўa Ҧ` ӵ_ ^n ]T \@ ([1 RZ) ڂY' ۸X+ W6 1VG uU^ T| S ]R R Q: kP{ O 3O Ni M xM, L `L K TK J QJ. I WIl I dH HwHIHGGG8G G UG G WoWWWrWMW!3VVHVjVYUUlbUTdT'S\S'RJR Qn-PP#OrhNM(M5LhK)JwIHGCG }FIJE D B2AR@l?ˀ>̏=W͗<'Ι:ϕ9Њ8yy75b5D43M10ֆ/?E-,wخ+W)(2ڔ&'%J۲#6"Vܲ &YݒSTީF:3w߬r. : U> :8.߬ww : d ީ WT  Qݒ & Tܲ 6 `۲ ' xڔ  W 3خ  kE ֆ  ]  D ub 1y Њ ۸ϕ ڂΙ R͗ (̏ ˀ l R ӵ2 Ҧ ў МIJ Ϡ} ΫC ͼ  w ) B u ȯ(  8h Ƈ  <- ġ J à \ ƒ d  >b  Y @ H  3 ] 8   o WWo ,''PP $ʉʉyuMiT(8uvX1@4.c~$|t{#yex,v9uAsrb>pon/l5kuajhgf6dcbwaB`^]\x[RZt'YfX^W\V`HUkT|SlRQ}Q|iP8{ OryNxAMvMKuiLsLrKfq JoJInIlIJkHiHjh HfGdGTcnGaF`SF^FR]3F%[FZEX~EVEUYESER4EPEOFMF%KFRJ_FHFGDGEGTD/GBHA!Hj?H>IJ<I;JI9J8.Kf6L5IL3MK2qM1 N/Or.IP8,Q+Q*FR(S'T|&jUk%+V`#W\"X^!Yf `Zt:[\]^`aBbwcdf6 g,hQj}kuln/,otprbs yuA v Mx, y >{# | O~$  ~. "@<Xuz](I=M9qqPP $ʉ  qq9=sI]z)K<h "  ~  O  >  M   y^t,.}KQ, AI~Ǯ:8 `L!Z"b#d%+`&jU'D(,*F +,ּ.I׈/N1  2q3u5I68.Z9;w<>v?A!VB߻D/ElGDHJ_:KnMOPR4SUYVX~Z[]3^n`S:acndlfh ߻iVklvnowq rZsuivuxAy { N|i׈}ּl ,DHU`db'ZRLx8Ǯ~IAa5K.>^9 etc4 1vhK8)Tiusyʉʉ  VGXvGZG[G] G^H`7HIaHcJHdIfWIlgIi]J.jJlZKmKoNLpLr6M,sMuNiv{OwOyCP{zQ:{R}QR~ST|9U^}VGW6X+,Y'[Z)[1\@]T^n_`acdCe}fhiIjlk9m>novqX'rt'ru wxu+y{d+|~\ p^"gnx%Qv4QQ  4 < a   x D g  B^  ~\ | {d yxuw@ut'r<qXonym>FkjiIhfe}dCca`_^n]T!\@",[1#VZ)$Y'%X+&W6(5VG)yU^*T|,S-aR.R0Q:1oP{2O47O5Ni7 M8|M,9L;dL<K>XK?JAUJ.BID[IlEIGhHHHJ{HILHMGO GPGR<GSGUYGVG ovQ%3Hn"Ypb d+\+J r-'vh(9l)wC}IJ 2Rlˀ̏[͗,ΙϕЊ}y9bD~}Q{zֆyCEwv{خuWsr6ڔp'oN۲m6lZܲj&i]ݒgfWTdީcJa:`7w^߬] [ZXv.V:UY>S:R<.PO ML߬J{wH:GhEީD[TBAUݒ?&>Xܲ<6;d۲9'8|ڔ7 5W47خ21oE0ֆ.-a,*D)yb(5y&Њ%ϕ$Ι#V͗",̏!ˀlR2 IJ}Cw)Fy(<h@-J  \  d  Bb  Y D H  3 a <  oo  !  ' . * 5 ID Kd Q 4X ^: Zd( Di eos t "Jy (~ - 4 :[4 @. Ge N#` U \ c!7 jZ q y . y z Q2   G~  ![[ ' /s 7$ > Fr N U_ ] du k r z  9   T$  "J *'4 ,U 1 c7: < B I wOK U $\U c i Pp x yJ '  {   iA j " 7 ݷ   :} V %  W >  !G y' - 4 :% s@ E Ky P VE "g[k '`d +e/ 0i 5n1 ;=re @vd FYz, L } R  X1 ^@ d j qbF w0 ~ HB k Q  Y x{ y{~  !TTهTUmUMV&WY7 Z l\ ^ aY c #yf )i /mS 5p ;t Ax GI| LG R W; \4 ` e j n59 r4 u y | g  B  t (   ; !s X ) Y K ' K /s o 7$ > Fr N U t ] O du q k r z z  ) T  * :  $ E c Ν * w ; $  E  P  y  ' : # * 2k  9 i A j I1 " P X `' ~  !?K?Kٹ?oS?@ARBDt uFO Hq UJ M $Pz +S 2W) 9Z @^ G4c Mg Sl: Z q$ _vE e{ kC* p u z W;  E   0 f Y: Ћ o k l  I1 J  q ' ' . 5  Z  ?  ? m ?  ?~  !;;_32dU?a5φwֿj{y *  5 ō # "{ ' , 17+ 5 95 = A E HF K9* M{ PO! Rq)[ TL0 U8~ W)@! X*G XO} YQW Yu q Yu x Y } Y ] Z 6 [  \ ^ | _  a c ' f9 h k n  r3 u ٷ y Y } % ' K D   E w D       G  Ω  " "& ۲ $ T %  '" (K )1 s ) S *9 8 *[ 9 *[~  !uu=GDHK-R X_fxm,sz^993g  '#Ŀ(Y-3w9>ټDMJQWG]Gd"jqTxi~sS89  S/  үV RW  u4  U1  G : p " )4 / 5 I< "A 'G -MC 2]R 7W =\ CaW Ie Oj Vlm ]q cu0 jxf q{Y x~ o K l + I NJ  q M ~  !++=yOEӾLvTow\cTk5/rQz޹fgZ [eiø %v} [| f&-3:%@wGN&U]dk /k QsK ,z n   m 1 U U ˆ w m M  & e   l      y    $ '# +l)I /k. 34 89; <>4 AlB FeG KL PP5 VWT4 [W a[ g^ mb sd z'g i l n 4o q r; zs! Ss 3t) tK tK~  ! 2e 2e' 2( 2 3 4u 5 7X 8 :% <,> ?)2 A9 D?` GE K#K NQ R|WG V{\ Zbc _g cl h|q muv r{9 w }g   -   7 '   D I Ȼ ϊ c Cf ( ) C~6$B7+3e:BEIyP'W^PelÂs$|y˴)3=vG  L   V #I * 1_ 9+́ @\ H  O9 W.: ^ f|a n-Ӆ uӅ~  !  Y ?  fO g  "D $$ &A+ (3F +W: .JA 1H 4O} 8V; <\ @cE EYi J o Nu T{V Ym ^; d\ jF p w l }|  W     [ 1 ;  ^  ۬ ] 5w5 xW]=_!E'n.5\À;jBǒIOi̞U[ҜagلmLs9Kx~+$LEk % 7$  }   % ,w 2i 8 ?r F L S+ ZZ a3 h n; u;~  ! |U |U |w | } ~e u 8  ׎    @ j u ` l ' k C   l# e( - 1 W5 9 =| ثA ޶Dd G 'Jb M On Q 4S U) V zW SX '3YF .Y 5YOYW3Y^Z^f[n2\u]b}U^`5beYhjn0q{uby\Å} ۖ#{hV7s,ǃ c Fd9܁O qrL v) *  \Q u 'u~  ! g  g 9 g/ g hV/ iW jj l4 nn p1 r& uG_ x:| {p} ~]   % Im ! 'x -6 ]2 8 =< B& F KL lO S~ W7 ۣZ ] ` c Ke h +i k~ Nl m %n -Mn 4oOgoVho7]Mod-p?kq%qrNxsLu`wJ~yr{Y~~_|d,)+£_a,%yKŀmkvנgIR k :   + '+~  ! E ER gY ` og Un ~uU | ̐ zn Т w ծ ؏ ۬J U @ \ [ # u Л \ՔU_{ aG\"(D.a5B;yANHvO$`U \rcj jC q# &x    #  > " BEξ%v9 #k'B+< u/t38=%B+G0kM5SH:vY6?'_SCeGl KrOycSCV6DY)a[^?`aRb<cVef‘f<gAgeهge~  !  S Z bd Fj Gq yJ $ N !  7? *\ `] = ̒ Ќ  9M j X  M    ,\d^ % ,3:gB;IQX^`>go bw= ~ W X!=!z"#$.ˆ%<'@)*n+R-I0^3?6\9=DwA E  I?M RAW  \%ia+)f}.k2q5w`9]}K<V?BEW)GI2KMyNKP PQQҸRٹR~  !55W _E fn'5-4j;NAHWN"U%[*(a5,g 0Ll4Kr8x<}UA{FLtKE?PkUA[7u`tf<lr$xA"i.rV@RZ{3aiyo "5´ď Ʊ'f.5κ<CiJ["PWeT]czidovuz[j|(f 3{$w+C2#&9$@AGz/NQV2,]e6 l tq|1UgU~  ! ~D67#+*2:.AyH 'P W<P^=ek|rtx#)-'J,817=<BHvNT [LDa>hopv}ęG+ɯ ͬ>.Љъ|B-ұGH-Z  xw,   ^2&ߙ-9>39<?E$KgQ V[`!ej oY s]w{#y)@M/+56;`AGH NU[ibhi+ov}  ~ PTaperO PGrat ~~ ~gdspy-1.4.2/docs/_static/photonics.py000066400000000000000000000160031354474061200176100ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### import numpy import gdspy def grating(period, number_of_teeth, fill_frac, width, position, direction, lda=1, sin_theta=0, focus_distance=-1, focus_width=-1, tolerance=0.001, layer=0, datatype=0): ''' Straight or focusing grating. period : grating period number_of_teeth : number of teeth in the grating fill_frac : filling fraction of the teeth (w.r.t. the period) width : width of the grating position : grating position (feed point) direction : one of {'+x', '-x', '+y', '-y'} lda : free-space wavelength sin_theta : sine of incidence angle focus_distance : focus distance (negative for straight grating) focus_width : if non-negative, the focusing area is included in the result (usually for negative resists) and this is the width of the waveguide connecting to the grating tolerance : same as in `path.parametric` layer : GDSII layer number datatype : GDSII datatype number Return `PolygonSet` ''' if focus_distance < 0: p = gdspy.L1Path((position[0] - 0.5 * width, position[1] + 0.5 * (number_of_teeth - 1 + fill_frac) * period), '+x', period * fill_frac, [width], [], number_of_teeth, period, layer=layer, datatype=datatype) else: neff = lda / float(period) + sin_theta qmin = int(focus_distance / float(period) + 0.5) p = gdspy.Path(period * fill_frac, position) c3 = neff**2 - sin_theta**2 w = 0.5 * width for q in range(qmin, qmin + number_of_teeth): c1 = q * lda * sin_theta c2 = (q * lda)**2 p.parametric(lambda t: (width * t - w, (c1 + neff * numpy.sqrt(c2 - c3 * (width * t - w)**2)) / c3), tolerance=tolerance, max_points=0, layer=layer, datatype=datatype) p.x = position[0] p.y = position[1] sz = p.polygons[0].shape[0] // 2 if focus_width == 0: p.polygons[0] = numpy.vstack((p.polygons[0][:sz, :], [position])) elif focus_width > 0: p.polygons[0] = numpy.vstack((p.polygons[0][:sz, :], [(position[0] + 0.5 * focus_width, position[1]), (position[0] - 0.5 * focus_width, position[1])])) p.fracture() if direction == '-x': return p.rotate(0.5 * numpy.pi, position) elif direction == '+x': return p.rotate(-0.5 * numpy.pi, position) elif direction == '-y': return p.rotate(numpy.pi, position) else: return p if __name__ == '__main__': # Examples # Negative resist example width = 0.45 bend_radius = 50.0 ring_radius = 20.0 taper_len = 50.0 input_gap = 150.0 io_gap = 500.0 wg_gap = 20.0 ring_gaps = [0.06 + 0.02 * i for i in range(8)] ring = gdspy.Cell('NRing') ring.add(gdspy.Round((ring_radius, 0), ring_radius, ring_radius - width, tolerance=0.001)) grat = gdspy.Cell('NGrat') grat.add(grating(0.626, 28, 0.5, 19, (0, 0), '+y', 1.55, numpy.sin(numpy.pi * 8 / 180), 21.5, width, tolerance=0.001)) taper = gdspy.Cell('NTaper') taper.add(gdspy.Path(0.12, (0, 0)).segment(taper_len, '+y', final_width=width)) c = gdspy.Cell('Negative') for i, gap in enumerate(ring_gaps): path = gdspy.FlexPath([(input_gap * i, taper_len)], width=width, corners='circular bend', bend_radius=bend_radius, gdsii_path=True) path.segment((0, 600 - wg_gap * i), relative=True) path.segment((io_gap, 0), relative=True) path.segment((0, 300 + wg_gap * i), relative=True) c.add(path) c.add(gdspy.CellReference(ring, (input_gap * i + width / 2 + gap, 300))) c.add(gdspy.CellArray(taper, len(ring_gaps), 1, (input_gap, 0), (0, 0))) c.add(gdspy.CellArray(grat, len(ring_gaps), 1, (input_gap, 0), (io_gap, 900 + taper_len))) # Positive resist example width = 0.45 ring_radius = 20.0 big_margin = 10.0 small_margin = 5.0 taper_len = 50.0 bus_len = 400.0 input_gap = 150.0 io_gap = 500.0 wg_gap = 20.0 ring_gaps = [0.06 + 0.02 * i for i in range(8)] ring_margin = gdspy.Rectangle((0, -ring_radius - big_margin), (2 * ring_radius + big_margin, ring_radius + big_margin)) ring_hole = gdspy.Round((ring_radius, 0), ring_radius, ring_radius - width, tolerance=0.001) ring_bus = gdspy.Path(small_margin, (0, taper_len), number_of_paths=2, distance=small_margin + width) ring_bus.segment(bus_len, '+y') p = gdspy.Path(small_margin, (0, 0), number_of_paths=2, distance=small_margin + width) p.segment(21.5, '+y', final_distance=small_margin + 19) grat = gdspy.Cell('PGrat').add(p) grat.add(grating(0.626, 28, 0.5, 19, (0, 0), '+y', 1.55, numpy.sin(numpy.pi * 8 / 180), 21.5, tolerance=0.001)) p = gdspy.Path(big_margin, (0, 0), number_of_paths=2, distance=big_margin + 0.12) p.segment(taper_len, '+y', final_width=small_margin, final_distance=small_margin + width) taper = gdspy.Cell('PTaper').add(p) c = gdspy.Cell('Positive') for i, gap in enumerate(ring_gaps): path = gdspy.FlexPath([(input_gap * i, taper_len + bus_len)], width=[small_margin, small_margin], offset=small_margin + width, gdsii_path=True) path.segment((0, 600 - bus_len - bend_radius - wg_gap * i), relative=True) path.turn(bend_radius, 'r') path.segment((io_gap - 2 * bend_radius, 0), relative=True) path.turn(bend_radius, 'l') path.segment((0, 300 - bend_radius + wg_gap * i), relative=True) c.add(path) dx = width / 2 + gap c.add(gdspy.boolean( gdspy.boolean(ring_bus, gdspy.copy(ring_margin, dx, 300), 'or', precision=1e-4), gdspy.copy(ring_hole, dx, 300), 'not', precision=1e-4).translate(input_gap * i, 0)) c.add(gdspy.CellArray(taper, len(ring_gaps), 1, (input_gap, 0), (0, 0))) c.add(gdspy.CellArray(grat, len(ring_gaps), 1, (input_gap, 0), (io_gap, 900 + taper_len))) # Save to a gds file and check out the output gdspy.write_gds('photonics.gds') gdspy.LayoutViewer() gdspy-1.4.2/docs/_static/polygonal-only_paths.png000066400000000000000000000306621354474061200221270ustar00rootroot00000000000000PNG  IHDRV1yIDATx{g}svWWʲl, ol.6` ImIg iC 3mC:m)iKGd:&tHBd0.U`],ɺٳ=g~H{sHqR@H_ZE͑|QS@v?S^;T#cE|"3yh;"ʋgCݚ1`_|̟ՆjT$Sxd$_^|OWPF`kp va}Ә E6O5v."N8 ( [), /s]0')G3-_:,83q < ڼf^Ӵklg} 9`o( Pȸtw_(ga0<^~|+׿lws|p pDՉcۀ79,㿟flp>o k܀[(_{ p!0U'2 dâHn+ Ijj4t?>Ll|۫¾Np ǀCՈ5SxF(6;0viP^TKo68H߯p2P~:b A1bQذk, .~+ Ί򜡬N^ mByӈTB+Lj +`8 ?hvZ({(>Y7a;g xRiUHG !kKCcEop+,dP{簛o,#x{4a7ǐCkl6ElXACQ2 И{H_@E9Cߧ56q=piJcɗÆ6c텭)@êFD<Chr kbhG|B6l&I 6MP5RW\Ʀ&xKh8vFujj5kA*yh?OfN]MYi4X#'<ūFemvI|SZix=66mSQ(CنGzEPpL>9~N,#x6~Vi&m?D2|مGKp۔(6DrS{ Oc_ 94j! lH KJͬ7Iya;9OZ!!!n2`YG zHׇ<\=ڠʇCD DZzSw$V7)*B*8DU 0l(kTQ!"]07okW2 av0  ?AS~ܭVR/CDV`AV2/CDdQd*  _~AVmC!"C>*M !Za.&6<"2B ֤ CDF]ga,AZL\x$'*8DdZ o/5Iۻӽ=rG!"yz&#ߊy!";/};T#419^5tokr8;Ȣ* ypUQVװM֡DWrE?K|{XI2qy9) |}kK_^*k<ևKsP۱A%nQ4\5*~AH&6+€.7u|&6sۂ=c2$Ng)_3]{61rxo*s5<CdxҰb؄Vz}0x +vV}:VYWEEğ,vH" 桒;\^q~6j _E+pE{}7aw [X 8bCssPنELs`?S}&+ 8 c $84\5X>Ĵ@9g}6HƪfU,o'[kuUeϯE  mcz@Pӥ|ȯicFf, Wmޏck>>qVc?*6{Ӧ0uZ6Wt ֠.^ \zN4Pu2  | )?c߳h1ZcذA?ab]l 뿬j)2 ܀߮ Ň3  #d-puN,|!: ZVE6 76G9?F 2(o]oOjƃ7PXpYu u\8U%S3?|t MHoV )YZiWcCǰ`e ΤO`2hkjvUoL:aRXP\x!1 mVF^|(Ix$/ǪP IzS_kþwJ?Lc|<@b =jC!a ~R櫛loӲ;@4V~%w)7a3 NsTns> _J}H}8F"?Cf)b8aJTB8FSXn@6UQYu(8V~Hkʡa?YU?^WuUx:R[-CYg c},P3֞Y5@!,ʚ p7"۱a cuۗl"W ԷPp$Pb9.alfVlJd _ +ʝ6wRϪC!ҁ_v}x>I֚jo:.>Y.i'WxNqصtIZӰYرl>w sIO+8DڋlOVO>H+>/)IZUmV#𝈴ƛ/a}3ыt {IZUxT6GKՑW)*Ȩ%}'?;ޯl ;bNT/ƫ۩_U{5\%ҟ>M}k>᫾BUux"}`>Vq=|،l*Al{Îr|~}v]U"C]lYt,!}**~?FEiJd> P_= bCC,vc{ԡp򂸀5b~3+ Hiccwx_[U"#oB:lLJ(['8A}[pH$[ac}sx;.ŶιS/VU"~C(@\Ķn~>i6!P!2"1@:H`+)`ϸ {^u\lJH2dyߴolc(E?Q~GkP!21@c|*55ϯȧjhWu4TuOX<| :^ت^lvaSrOS!2bלE+nGvSsx_-R?KUKwQOo #ޑDB󂪪Cd%Mt˨oLc7{J[Э~|$EX1M@UHE^綎Mt߶QU=ZUH4mt1@|ےkFY}גYê:D*&|6UH.g 9> FX} P!RA0FFFu3kxԤߑVCZXÖmjJ.CzX#R^}n-\}9;R0k+j-:{l^7>2};" `[)ձ8 ^ht 1%cV-"?|[@X ֧e&vb_/~Vo+zZA~G|sdenlź _my຅ƍ؞9T`WE$ryxq1aT|C9:^K!5.01OKxhjb[=lwd&G7<"u9Yk5P]q{[~D$3;}HC}Gն d9[dvn)"%<яxi_>4"yk9vI1ȽV a5 ^}\\aJ1ԽQ$H'ضD#9uغk~ޝ>?Is@"5\ƹ;'yǀ.Ԋc(gZiJ>|g ^lU]{#"QR}>ذU!GP|Wg]^JxxykGZYH5>-^U+El!aU~LzZǢBCjX}f'Wk5|yn ڳ֡eV~z!"+h>®y9W6lVBDd"ڐ)#~AHF#~mٞ뉃>d?$Qk}\:l&" v`|؁X*j4n`}4B0|mrDqF}|xɥ;(8Z;8e"gd 69uk7t0]hbS$зc>[1HjV}եU!>GbGpG+<湎g}(;kJ 妰xNBD֨FGz|> Ygsb ew8V""`8/kX|&m*Pq庞gYgȯnBS 8U[?`vvkp9:rV3 :1wGʪ% ɚW-soҵWxHZB=8AWzx'vB #8|zvo "8\ <ت lŽU(\ج3W`ྡྷ\$8uzaE$S-8L=KMKZ q!aʫA BGF6=LaOFQuCD**]7b?bC6ǰ~GQ0gX"RU[rS*H{ X *G79lǏ EyDPx$;:9ql Gn~_K[ 'Y 6ipS4\&"<7ִD7m&6kH{'18`c´X ^"dj=X#$ lFccXW8pMBDZDix Xxv| "V@Hg]C: W[a,l  K.x.l6Ux8`"$MSQpT7œƿ؇7|,_^:"RM/l*lu6 Su7B!"\~x>4p S lb6=CD2De%cJ|vnljb6=CDzFUo^ޏ]OZ7Tl)=B!"bhLast־?\U"Јox 6vS_c%W_ pOz:v|!%cUWɊ)\a]T!RqX*[x{_yplǂ(NK󻈏]܂U3ڻ1BcVߏH`12/V_ ƠxpLc?rd6~  9I4vxPU^эCF;.f̞cU4KGWh OJY( RV$XOQSہV (WwȧLnt/H v}l+_9. K?̒ߧmtFSQ_62_ |ǰ1,m`"y֋K.6Mu"0iw ~jz[q~)Y[5ó^t2NIXep!tR@a*=ZPxH+2|mt2T )A7NSh6!YN1_Ga)w 23K*3rLe_-?rJk;t8qௐ]y܏aM :; tro9a>kB)ͮUZPDsj.2,R̝9@~?4Pi/n Jdb+YT: W3۰̈́1Ю 4p[/ jQ9G\ޡ&h 9^ 7hB܊G~lL`fi#"qOsQ!q0=jΣ%hn\T3RA3;~Kk z%/0I>GGG.:P@o~W dκZν0p5"ԳPfGCEJuHCN9Cꎹ31ف45CDSfhj,4S9MCEY'oaJfJij#FQZPBΛ}(o4 ݐ Dwgsw,MAlth A3ȹ ynNcQG8wX{[CO"e3mHz-0X2tva6ۼw[!aa ݄G'Nryi IY-Dх^:yk|YmP77!M(6qupˠH/ 9_9pNZ(KTH\Rb!hkk;$EK6+4l6yׯsδ4 ّm?s^#h0Eo|G4/$Ro^BBZU+#b]'EBIls R(Ц J6t?9cH:|.h!%f$%nvpߓ\d {Nޤd q.{_sPz8IVsybaG2'~ׯ.xJ|8ߎ;x\BZC>OG/BSb)š}4:zz_,&hOi-gp0 y a܁ A}TL:=Ϣ$𓔵i+k 7 b_}%j@ҋ"8HjbtX/ uIOfcy3,IYtq-~&)GKL&Y:qW 6m.R@uec9h{/ FD#4$c)JL]<| G@Ԁ뀟w i?rA_p0SƸM)FsO*j팚 }16m͏ <JBh*,w>9(vN`w{}f[sQУr!˨qB~=}l}HAyv;%hQ^Ku2aJ^N{"BzO"o{i:iZ{goC᱾:=˼ Lr9.P#ǖ c#?ɔ\<ݏ L4~g i:[)kZ4~o,]sf:qjx ='^뻢"Nj"aaO~9 ,gκ~ʶB,*0^lh4 陀xsQ' Toix e >.Nj!&)&˛5{o65ϲlQ1Gi̔ %Dpe/QOcdEIGt[w)!y^9hpjЙD mR` Sש Le(&u/&/ xkJr=ZE@'Yڕpw#`ˌ?5a߫|Eh=6gY(Sσk؊H֮ FqBHbDS1Ps/ n\t=GDGYںOݮӵU6exE&FH?ѫn!^HlV8&as(ZeƦ$AU75Am~e"n;.YqWzam({S]3&e;~V(Ḍ(QҷF$}DI9oz"iD h cwYyx'0uhYQ;5]'I? !$mRC_4v8i ڦɻ]G9JYLT}(&.UݤE 05x mzOY9m9E^4Ey;X1O҉AM1^J'!h_bwvѳQ_rBTTKge/ dH({];A*ﺎbks_ $7,St&#(z&eE̵ tc! !nrXN įK|4guPN.?˓9M ?b3iL(v"FaLk*DƷ""}E8] .`o :/>2OhDJӍ@ęA+:Mz=O.:|wI:$+\ԝB{xvn8i*mF҇wJ_Ĭb }4Gt-൮B"hC)#5MqW-XJ:A/;:߿̜fMŦP5y|eYjh-\hRiBŝDq&su~Mr^lutj NL)=j:`ʩ̟ ft_ۨIXENSF?B1M\wc(z}q-h =/D;O ٚkRj.;q.Ț8e= Xgd1߅:#NR~]$[#$k1{<J^Bv4Nm3ϗnO)=)SH}>FFNdLi%mMH: ޼H⿣=.3+s?:q餧ٝW m۲Љco zSz()`3I>,Q.Ɛ/fpu [Jz&=!4uZ&{l$g16Axvo=cHzmEw o(ġ [3E)F%7)߶Q_UEH;qc$u~xMoFν F(?xss xM?4sU{2:I8MI:wo88ARt;ͤvHX;WQuJBB]j{дITeೞOR,?cxB&Pș刴]=VsȪߏeh^̟UtMXgP򢛔ŴRۀ7: K (7̸:Q$E?C -1Zۺ:v;8kN!ѩvp{"P[";~7+Yz I?Mz (t+XrӅTv2~<]$=Ӧ>VW(E I=H㫛j86:㪭剐=3u' _<AVpv~K^aam G0LV.7,#I 9ò 43ѱ'}:HG4)ėF jU7.Lzޝ^ |7R:6 m73a.4(*R>_aD#hcz1]E֕Ҽԓ^ވ,IMAť7@ pM6.4кCRzʉUm9u{_7娾@ȏ# D.145-G(K ^mp;$8B'ͅE$%ޣkc;LBh0Zo~$~; RbPlV_+s]V3c_: < OT3m oA^P Bd*n,-=*4.^v1vƯxDʝz34N3u9ḀBj)=u=eO)$Q ]y,4מ$fZ3UjG)#EF%G'}.'{ e"߷UpxoWΠ@j/':zҫ)`ڈz9:~Б=,:p:F6>DŽBU"̊}yc7)oJL؆vFӁcB4oEkЍ(\EXĤCDIIJoU7HQϑ, myaW?/ ,ySZ[iec}!1N]Y&PuoG54.IhqZTWE<l3Po~K3T=YU@r; -&8 Uݍ -8Oٰwi6,u{V,4̫?l[1xl+ \ܧͩܛ$c+G<@bD>fb:|.6\uEJ9Jv6ݕʐq:l渗8}eއvcolcy xNܲĘ+>Vw /k;b|Jhۆ`WSJ)d?L~RXU; ǂE]W־ ݧ[Γ~&۵u>BVU=˺5gڢXKY$-+ơɽ[ RoTZЉpp_Lea}##o@ٮ$͈e-LXopTTlHjQRU & Di>EМn-jG+tYwq:, v]se0R&J6݈O 9 ށ iBP|~!L U?Qj@[5 V.Vdd4WqDd)r:>Q89VZlF޻2lS}]%J\M\bSgsI/!\YVd&#~öW[bo!~<"wSC. 뉕&k1P[maypE{]/!9&'9}Է a DyM2g֡IzP+l10;ev8 ]Ak*lP_GqZM`$T;;e_accl4j>їpĸ4Ǒ;kDЕ&*6iMA䱓nǓh:Qe|6l3?r,}}NؓM)uEfx.C'wifA 7w3L4/Ac;d[q$V*4g nݴ>g$+o2.S*뻀 $JuavhءЬԲhގT$C+[# 18+mTo[0~.#D4tvFPzV&L߭\&AOQ϶7;TvҨsmOSJXnMZR/:R@jѶc>R ͗NJ8Ic ƥmH~L)k[ 5B%Adom؟&& #>J\,(%^Ҫw#U>[a#5k\o!m!%_ߋFlRrhIlmAcZe$RUmm&&ſ2{ 41U{$r}q VC1ګuM"֫[| q>Sn7_Y'Q{ B;qvt,x--i!JE0cs,r)vH)ś²xU Hx9hv=Jk8F\`qk/{-ij|">{xIhE@0&{4/]L`bWMxb YY C Y]ؑh~H kK_]8eߢAMRŴ>9cu-ɻ9hJف,&$͓q_Xa։aI ؋Ki .)!QmS(Mqd nx ыF/MvT*]NT-9~ջ}>}`sz)[Jr[{S6 lspZxo&IJĆK1F& ?߸xWUU/!=924J I=zaGədaۀbCF{ MK&C U^SgˆA4U'b=]I6GfHr>AXFOMPCؒy{ u΄ +0fĖAh EQ=E *`7~] }Fo>2bsaScqǰc}ٯw.L:#QW@uń$.~R'%YMMTTOꏃ|6p YO]&N^ڦeJc:潎42V jhRyc?t"gɧzCg9(r8On?/x.#=l^G\sS`UܞXI*yVe=Lj$d.)&FKOa}s!{E6nZdT7PٓėFD9+0yrWu_'ަqHW:9F=bEh:Iv|zrm_FGꖵ0(j>*͇ 627Z#Ea$E}nW:1hԖyrM5u(,.Ѭ@6@lڌ40~֌6m+AM? k^_vs`'wsOKr[$ʵ2sP)ͺxXT񪆈͕R&qxԑ(MUSe wom I߶[nf~xaHRbܿTL)Gl!LKC9s짻$A+Pev Hl3]pmu7>#ƳRl {e2|ĵ!K{5ۡ0"4ķg-7ksF\;E-í>(jW?vm(6RWZu@ԑ i _X/Pʍ |`/e:0k x2[X蒈!hNjn|XzNIړ+F:'^zڼO)J۫죳?"ٟpUA6T 7DKsłD{S]5Bۀ Úx1T0Z>-ڏA9=Nm۷зW6MVk~>{n,(Og-?RCq硪{]//LȆ1-LZlla=>Eg?ikX!)&' X&ϥ\"Q ?ݘ$ (INd{7F41yPJz(+ucFىs1)s,:vdpLjECn*ej{rtc+Bvj Ba4c?ޟ !3qwt .̪\h@@~Rt hս I[`3.̺U5۰P_D64#C9]T`)ݔ 1DV Y\<\z9,J@_$㩤( >m R^Y=IG^JJg}wٜM9D#u2&ؒqަؽnQrhJ zAM\6B$BchZ(\2OL?/މEۦuٝ9dQ&9$׮~1cMkmp13j:0}6v1)M0MLR:~4o~7xw9ذ~-))NҪrp[Jrj#n}ugLTTꏽMDe!IoB},( % j:%3Bw!~ݩ.Rc|>Ru{s.Uˋ!t?p:ΎHp" xe>R:(Yw0~߫;$ 󚢐ː*6G* 8&W&2,!R DІ| mP),]{Ǒ~>8|?QyZ"2LOR6f`fgPmN:5?k"_]*,JIjr(Y+8L!~#N~r~IV 4_wͺs2A`>Qb+bx|'Tʅ`VIѪтr.#1sm̈(73L]܎4N#.%9-yB]MU {g"ĉ4v[CppM]iG%t:o>@S^S9hB\?&( R} žAa1r8Շì{3Z;eZRrԅ9A9IڼKJ b@l5N;!+0 hRK뢈xt 'ٛ~r|]ʪ1kT$U#e$ ?63htsLzt U,f@{Oيf'lg Ws>|8Fb-ڸW ɴFœ/x۵.n"5mf+Eat6AdFNOu qQ*תrDIOYL Sv_DYTA7;x# <IYyOCx+_ND&,"ШXY ˼s (bl܁O/.  }d 0mCdzpwU(|]:}Iaukm,F U-/0nE ݩc]ˡp9(6","8D{jM2Ayx;3DQB&Zf5waj(,HND̺`̳|.jx^ruyݛShW:$ŋSe(oH*baOpd]QmDȻoo4 BdwDMhC|'5i 5U+mCvá6V}4' m 08Q HRO"̂޶r9WbBn{ >tBYBY],Z:JP5cozkЬ6pT8(ӥgZ~=$i AMmsZt" }6,xΰAoCƹZTDWWм殙9砜;sU''Xϴqj wD34}5o8e{:a9x5!B4+Y(?F <m B E&؆g!tI`Wʇ&,xK{@it#ڙ_9>Oy s '+),>w*J'Mdo@=<6#P4D;oD47~|dB{yx )70>J;ߵp$zJd/y\ҜH cpHk; IQ.9mroH9HxM2W I)LO +5sש'ɒ6OtM bb`C51 6{sh~ M.}1OEQ2ť"/z zlˡ(kN^lFs9S腢~'qmCW]>]t¿SeHXQ/C9u[ -H竑'thHTwR#ž*Qwjj@-VoSɡ,yrNtMNCQ|8jrjB퀤I S(T6\_UBHQ|ؕ9HIPB\+ Li!e3LzoP!zQaMZYOO|Qd&"[99eZ>C A'IBirm LPNA~ a_ KIs7}ͱvembٶECjCGPXD9B* iz@';k0]xuZ 6 DΏLzF8Cl$(,!ʂm`bM8*1QEU| RVlKS[N0I(=pSn.CKP(swסmߡlr-סdO8VW47ONj=xðJO"1˧VaΨ-(T8Wߙ1UR&1_"#}]H+Om5u*=FDwQ8NY}HSx eH\`pfcrFt!HZقJ6j &E]R."GYg=?93C&]͎=He;N asA6AY(:hssi2R=x5R,N@݄c+:c{)HuqeR̛MMLC`ΠEA5ҡI~P]tV _G5ʸCVgr.zo3!bIٜpP݃r'HJ#tW>hd2+))`sflC0璣w˽]-ʢl{%*'sJހ6~L]Jba2sus gOoGݲ;G(s&'R]T8Iq[ IhA ދYp5V*Jb]HYteCiQMQ ]_DV07 ]> I v^VI*$E~[7D(]=ϕGXu!hNQ+ %d t ڍ jK Ћc(VzCm UDPXU@ \?ZE/˝(*?,m15]Q8Bs?reCe>pա"MMGI M>`ĜEs|A; Ԅ$L'R*DO:FW9s݂B$mߥ}FCG5R}Ҩs+/ ;2j(i%9?yc?+Î:;D@}B@g_B!4&°V&PM˰5HP2D}1"Q䑜cj,nFs T`d T[_맍f j(lﰝ>t#,2&)sP/`t IUjҾk*Qs{P+#ce^ !mPw܀96{CЮ0VF>!#*SlIK 8"jK –h=N&A'r9K( j;u؇tAd,@MZ = e{(nG!BD֚{fUaO^Խ>k-JK+tvp x#[9BH5xWQTё(m{@3',L򼝠C4)yUExUXc%e/$JHAZӨ h?87TyMNىוh\A9 y{If8Y`8yrxe|xkBZ+WBQ֮ 9&$6߫B( $aV 12=DwyI.U(2A3F<è2A3 =fw Oxrj$؞Bsa''df ?RxlA8]"4ch؝!4;A܄Bs U 10Ǎ\Kx(BBCW~)df  S99jaCLЌ'L%$is7pm"4ch$"\^y -9P LЌ@%FTٿZ.)3FK&haNB=7r%a7X!fO&(|"BQ n)s 4c`(ʳHrnFg2$"QQ4:^rބ|]8e'#WhXFN .TM"vi42i|u%h Pͯ}<&> :uLЌ"uMFۡ)}C]~َ)i52A3O9pR [YHΑB&hFHL^GV-9!4CtDXxtlՒr9;u5:z!KЌ9WCHȹT@Uֆ4#: J< Б<9Y>5:"|bE19o^LЌdfD#͈B&g QG&hF-dr+"spXE&hƒ<2A3z"s8 ÃLЌ.df#s dr+rɓn&!K5+Ukk*da0k79T]s7"r62!t ۜ7h'%#d1sP# LΎ PPE!;'gPGqkq& gr)rt K1TF`7pݭ39,A|˄ drTR#b5 drY]6K!g 29,AW)r 4' p:Z/}D&*D%u{nOαL A"e =ﵑs]?3F^+ 8>"i=?,AW ,Y@^&me!9Twa@X1g M 2AW*#KD8;XZgF&#>>DD}'VR9c@^FcmrO{O95ۜÁ,AG)?:v4͜AcHr'g9 G! `kdr 2AGS(뱵ڭ(]NdYBÃLC%Pm,9v :Bc dn"52AG= cS# o=e y6 ShH :Hg CL!G`w fi| =>!G&)|)T BׇS~dތb9 tHxV_AE'Wm(Bweh!t؝r">$nShd :d#B#LهoDNgOsH>xOα]d*ː׶ImTۛBL!@^\J{xOrȒsđ :`e-׵I*%)fJ;G9p~P!.[ȒPs t#\#nETȪmFd}5"Ys(aY]#cī3UUL¤uTMC fv O.B=͗.ɪ*G&hq=*9N36 }Fjk!wǒdvDs][BNkn !HOK{ y;jƐ"cin{>=Ɛ ,uk*=7}&kX5K?;I=@&h~9Yzf]gLmM@z \ srCZ9-؋!5$@ĞFGdI8xLv+`=ps\&g"d6DQ< UNQoP(p0HXL氹6TWs(߇ 9 y!*(`J;ՂLT^zQ`"$,QȋL%x8@ked,B&h$b|^;''L_gdD&h<,IћA g䄌e #sb+sH~?ə,2Aaz18"u?3j!4s7q,Ru C=ٟs5ߗƑ 'Pi;#cYd!۟}E&hM$?όdևٟ4wL02^E\+#cYdևBd4Fra,E3j!4gZߕA% Zhʥ޷8$ی WDH\ *์Z#^D2nf&`jZcN^͈B& 8 8M 8ك7x.#62Acb{g;>3KHQ+pǙ 2lėOfN[>i32 Z Sρ[cu;UƪF&h}ނQYFd #!H=5hLmd 'c- >b`x@\FF2Ak;bmP]22!tI ;).412Aa?b$bcfكLzh5q34#0YhLa|:i_oqFF#S Y|,2AIxexƩXf4B&h=4!P=̐` Z;5m3La~ied4F&h=4Uq33Z!MCFF&hG2!I ZsKfF^@K h9= @8oʣgV zj0WA9{f<&ig1),^32 Zc2on 6|6I}m {A..;RB?o< fg+s`lN* Փw1mDLҦ,A32(`ixddE^G93##########cTEIENDB`gdspy-1.4.2/docs/_static/polygonal-only_paths_2.png000066400000000000000000000641741354474061200223550ustar00rootroot00000000000000PNG  IHDRX`( hCIDATxy%]̬UZKR]l ؖqC=404݇mp`̜sf>bXٲڪ-KR6Ւ1/xEq{Ϋ|^/"nw `0 `0 `0 `0 0)ILƖ/ 7M $Jh|m CQ4E`EQB `0Ơ2 Na&~O?CRHHS<,"Ik&ແ,0Zgt`0UKĥ,?J:?K /2qY bTtm\iӸvHlG.9 8)!Dqۋm53%e aX! f<*cDL"R4Hy{7) ZznF.!HT;A=E! CbUjaE++p$,NTJ 4*va+F/X_M^dIssϻ+ͽ( n0U~X]+Wyjb &#Xjif]u>ݟ1in7T UVjWP` IRL7 +}93 ӋU~X1" [U- &p=⮘F7ax !B+TVl^X="^d0&iTa3&~Nb7 !6+DVl^X!I>]&}wsN`tbsde5]"づbF+(1@I}e*˪R^m;o~/jy`_]GVW$ *铔Gǹ+}:07w0-RŪ[յX_5^A*[u@F+z^Qg`="PaA^Xz`uYՊzVo7vqWͽ:9->gk:z`uYՊzV٢o>]1 ne\n0Xu갺ªUUkTS6](y]Ywn0 ]Xª5 T^Wtqۚ. ss7 SU:uX]zaMUU#XU}wssڬJn0]bbX]_^A5LѷnU}>I~ \Ś+/:X]_^AaͽjAܫ+`0L+Pau5VWWP2EX^U껛{wE;`XU~X]xaMEU'!@}諛{YwE˺+`0L+Ū\ՑV.:Q-Wͽ Ԣ#k:.&jS NrLRT\[ss7 ӎ.ja9p:UJsjdwn5 XsV]anaё5IV^Xz`Yչz D$E_'qqW1m6In0VT&j VWWPnUvV>OgW7:Ne0 #сuXªfUTEXAͽn+`0]҄-{aMꁥV5Uz|OB}wE;`(j+ƢM/<\sj" F (77.WU'n0V RA49ԔN%DUځp ܣQ` aЈRѷu+z^<`XUhYŚAJlrI^XU=4ؔzI(P0^$}93 IіZWۘEXK㹅fӃQW0`MjDFܫ+>)yss7 -X)Bnv?м$X:XP4ALSjDO*Ǥ.w?%n0V#P 0ª䁕r&ꯢS|*z^Lojgss7 -XaA;^XU<ڨRr$ΔϾqqW)mV'Vss7 mXZš(+MQ蓛{]K`0LUuXmxaUj*Z )XQ}RwECgss7 MXa-0"*+i*Z QBO}ssbg0 ђU /*XM_E^aYu>>onu3`XhZŚYU."j7@.`0 XU*DU/+=jZ/ LAn\ܽ`0B*V:x`5QK F,M%5_/)uu5v7wuq_xgX EcPQ Dg\VVq_DZ[wE=@9E#~m~yL-(rr6Ru_Bnpi*x0~*kW-).!3 \%}vd Zl܅:! k|qt7٬:n4 =L BD+|L]{¨Z2i04SZlHɤGؙ%䆫sM |{`kOi5 r 2Ҝz%+MB:޾ilkWjlS\f|4\.ɦs,[MF}$oa0 ]!*!-=W!zߐe")XzNXF](=5DQϮ$ m_v:SB4Q^B|Ft/؊ j 'K+Ur_H`~65mx/BB91UR92p*V *֭9)_$\{l]`0EhB΢[|>L!})oKrJ]VEd{'5gAU p`6?L C.d0 ^pC Yܾ o #o&I^p30K w(G5m]kvQ0}z^C.d0 .^)$~zcEj0 F_s{FA?r! !#io$2 p̩^iz.Y]}aI3M=Ȳe78ra>mN $" -tW]{]B=}t#W %s"c/xp z_e{2BVWl]帥yz*KeǐANTH[v M/!&po#lf_UDSڧTRs7!e3!ԫ*Wb+AOY U>6F 26m?TFd-R~)ʈLBd>0Pv J@Э3+z_Ng}Fenx?XD$ԺF1ɥ W5(t]c1 puWKzq#B(B;AJӔ ,G|AQ(-gX)$=!CԐE.)yW9Y}xe.449$tޖ۾`0\-ҧ!R8>\=-lBćPWJp,i{ɬƠ1Yo11y 'R< ^ QQ8>Mvh~ 8c`%0d:>W{Pyfyn&Kց wUBi{JyH) r|!tSkJ $n}1رh0 !G~QBJ,v$pxL핖!Bj]5Exh=}=EfPs5zR LtF9g(^-TZVH;30CU; 7ۍ`0$B@B7H6ߍ^݆X#ToE&GES﫣p&Xi~0'4!q Bҽ 5miФXץpL2 c jBU [чa- P~ia罄Yz꽯|:"D a?0[=]P ^DT턱k^_(4)ZcRJ`0FErJ{i÷"cNbm<ڇ9Oj+ uȁX#!fy=H\\Z^7 h\]B4[pT|sאKL4ϛ-DS}6i!ƣ:T9F[}Ym|CXb7Fj*M"Ջ7TႥ ClHaŴ">V=z5<naz$YF[pǧ39S, ?Mxpsk~2klV` ixAکADJ O5}dv~(U.͓Qaa|z*e&bO C@QTri{S;O(]*c N}SS%WZL~5Gꮼ}xp(P}i%.,uhS}#쳄/v?I7QԳc+!Ii_sQ{HwSSh=,')0h\]>-r`٥yTE{_97UމgT(vA:o# iz )=-]h0ڄ*5<,29&WfEVYGJ(i*$}^5W> x =:a Mwbij#H{J8#H=;S;6`04%) D9B2!fdߕWIYg9?TjH,3:df 4%GS]=^b.U%Ia} +z x)\Ԍd Fԑ5!qx==kH*uW:Rj22vޟȒoKL&+J'ɖc MC* YX$DަF C(xUºTջp5MEYzg1~WߍXFr8!c%q> ՐX4My/4ϫ[G=\W>-ߒT*VZT\? rY*$Ȅ_J["ԙ6`ʮ~)]\},K ~.IP3tߧg?EyTz2޾V  "*ցEsOhk-r c(/ 4uSe<Xr Y: ~ǻRYzcH7) 2C~G9CxsS`M!S2ߤuAªXz/,2D:V"yx)L X􈖥 2rj._A͗4ac"rI;zzujKW{GVz `~",ZT,㹀4w cL8~f QR ?%aS U Wc'V mJB äʯzshrU%LE!/,!}UlWhdOXTx xO$ jܑ½IKMdFf4b)l2"Tڹ;u: kn0*O)'\%h"~C <^R;d~94΂ŽC!ށX| WR}lm](}8JfvG.yE/!H+)<|*9]i_pW 6$.$Rư^>JYB׈%Zx6=5 63uZ@!VYjPh7" 薐4]8-)xiBP 0[͘!]F:JxQX %P#HD~X2z%Ul~L*6HI,GtU^?o/Mك+)DDի$𷫱J4jhcOe{hD&ȓ 2 U5iCCH.ȶ\>i: <|8$|KF ݣ߮~ڽXxsq+%5_OՒYªWkS"W#/nAڇ0THIdVb R*Ѵ&vrIlp<N$Ÿ nM2ZN S;!V{2u*f~, Rg_8y%!|T8jWdMM̎$]]FH vfV<N'"#W.K]?CŐ z_iSm&UEݣ zXxE\#Rc}͖IZ mcJ_<Η r I/aScJ[(OJL:U$gWG䵝E&Iĩp|9?Ys7i܎Oz5(fx/he;^Ja\Bݕ#?k*6U$؉$XMX)Bd!{@q |CT!d7>|ZЎɷ)H6ͻ54R˓+]LDO CӁ%3+ .OB+oFj{&Oը _IgV 1xf(s-2O7d~'TΧ5}p fv mϏf)^|AWAK5ȸ <__K[Nw =djR615X@EnR0ب?_d4kyqOMB,N<w@Vu!a2 tvWv Hq ҎOsx?׻頻p)?txyukX|EnET]H:4ϰ|! D6J]~s7@旐s$ >>2M5b{b")pU6jBN6ΓF)x C&2BONi{y7tFbk֗_> \GZ-+oQ]]H64mդ$d ϔIB#m>UVwV$JM6r>Yïچċ0s ~oBZ|'[=䟿O4m@ԑH9u dJ]*)̋<A#22Ω5Qw#Z"_~Fn;!di Mn{O J~O۞^cUֻ}͏o;F-j{>nEÐ Ǿ9 #%6䟦ɕq]i< Ϧ^*FU,;Tih?5&SkJVJ; '2(?2hGӇ6O%D";ΓHZgv){9s|°sa%AwQ3hJn| d3>7!.o Ss}>K.Uoۯ;֏Sb"8rǩnݑƧo-m9E+%)rުqF*WM }ޝ@F"W2xR2Sԅ3 XMX|"XӜ*A%.lU˩Y?^$s` ": i*EգQkQ^`IE% ~az(ii+QZ~4 xLEdP5kdma ^|zmwQgs˪V3w_!kz/OnJҘlI1h`A*^u®i2U$͢m-w#QN&Dal|uUIGZingOݸfr,%Vdտ>8EBJPHJp?BT}ruMB=,KnET]"+rh*Rڥ 9߈.*|'^dfR/3 b04ݽC%VT+y=I3Wda uWւ7RsL"9Uo]H?*q&;?8}qr  Jzi3R pM $P%"G~3):R\ͺlM ? S$qȕ~'of 2r5(X ^ Ҙ.IfN#c,gͩuhk?$2S ijFy*ŻR"uGߞ ed} Y:TY@S?< @w&"W-]UZ`yCܕh2j^?BB;,K$ 1bD 㽡$G'K(>|jZͥ|arrFݕU9.C ":/a,"FG.0p?< 5mHH9ӷ} Dk` 8 1 {CK@~B=N"o7{DJm~vdLjr.B;uWc= VB6\C >\snޱў[kmVG)<ڊG9')RpMbL7TjRXeX `0%4&%W5wʯ*etNTZ7fy7D[$K 5,Uwl%B.S]"CSKva(IdIyB~=K"X_5jb,`A+Bx&"$^^Fv5ҴvvsgoBԭ5ȓy-Ru~Y]+h|)9g;wm}?R0텊 ~ p BzBjJ}WXɤ4ɕ]XUeDNB2 )濂4h$Ks%hGqDHچ,{ґlv:GoUu'l?u s4=V7ǠwQ|m;g]4!ݧL [9t|eڞW5As/㉳wI_9>伴m;3?auW 2 rnEXW&৑"ͶHVHGvH,Dˋa&\Tu&e "~-ot1ˏuA:@?q9$ !ƧFf%tylfg7"e=Y5u@384n{#R}f5%;<8:w)[^~|~qߛRw_:&dt~ ~#͒^} We fYB.Ӟq {͸nBv!.bGmDI}6~_⺑l=/\\&>s"[ҢgAKs$YAw,;\\\;ݶϓ-\uI_}YrmϏ&o{)b~_Faijj>>'MEzr^!E镼6\qg1,Iy'g,%J 7~5Y2DKIR7YIuB H|֚ ,`؀9'RP"(Ƅ!KO܋ oD:ۋngFmvu B\>̘bIX! w cuQQiJ9ksw8+A5kF2(Q0,>UfEPLDݘ$[\l7!2cT*Q;m.dUWM2߷&l{"WOrD-!BB ø9vFэ d3'>rRK:;#XJuH ҇#"Yi̧h'Xb\1p]H;\dP8!E:( -@EC?h,yC.d?PRdCKu[ħ2Y)Tב IdLFKƷ)ħJ I8m F_ہc}FADeC ŷ BDW m'^ޝ5ʕC>jr:8^}l 8vD 9o' / [s\v>wDI`pͤ !$,d)l¡>ځ /!os,CMW T"dPލ%|Gg~H= }Ȁ 4}E N$uEqE:G-Eg'|5 /#\ۂ"u!pې #-fD}G2"K*ZJDEo=I@[Oevwbm+V:lG:8OXc$W~U'ӂ dPԝzҡ} %YЎKJR2cH$3REE tMɺz؀\402XqIˈm ml i\Vnh̵HBxn_Mx̃|#ZdCS"O/;ߊdom%K/i$uK!rߠԄ{k q)$C|ATBaYi9>}zmϏiO/Ԫ{-H})DXé,lۭ]D ȧ oD&fMh-N#tD$ dA5Mv-+DL C\\vڂ.OφRcǔȕ*0@[,Mc xd%R9rN+t7Ň>/oB::u\xx*YY`0Lh~;HH(%}cmdH&"zk_@"O(6d G|o^[5ˇ*ځGfmrDV`l(0]ȥs RQD&"JY.fVJ5k5yP=!ꮂ H$]*įnr&SVzKPeKhnycΣ W:`FSg" jȻy`Gq5anz*tSru=CH6퐫fj Fx/HO4]KvK{$k}H QR|԰eR2GJΟoPf0؎Wy2zxg:w#;<#}q%BVJP X o̐)i#W-W H{Ys[?MS]ϬݐwߏլG߀L WFz,h?4#4cߠE R>ʍ19苚U_JR BSiQS--7=(elR^I95/x|K?,ҺǟDMp[Ku ! )2Ȯv$$* We5 IYIQ^ soFZF_ vs}hVRl%!cg5kJ2-s v|lGxru~Kiҧe*v#Otq?rK8/,CJ7r.^|P [RnJ}Sy­P09Jsmv!*Ea_Cm_o{[6CxR:K>S0ᱴU<>wR? Ez Q>Dz3rݔ|}9梨!s>_Q,BnADpWrt + uA_,PV=%E1Mik.#*-|wJi7)A0r)zM`gH ܉tHq,.׻2p!΢"FL !!!ͧfsYٍJZN UR'~|lD92BOoac- ځp! .dZ'S )iC&GHkw V{lŘ럻YH*!wVXY&o`}FFWvmAH[ ӌEQB[A*%F:G  <|byf 5o'pHұ~ B@E5 b zo@(ϨZC[@ B48m;'{6S" ! R]!+>CVPZ/:M}֯Ic;IFn|EmoRUj XJN(&V]U Y^OI%Ʀf!]H'r #ZyTD+VT5>a?1(iEZBVkGese IXBq`r{=| 2ZXw#W`j 4FR/ M/\ԓA"JP`]YbhaAĿO@78l?U$t_Smo ߗbIW4 +;H 0mK;o؇&ð5 %S*Zw!:Vڠ`!$~oh}N^'t([g4Xmq26`*BL5+Yv Ydk X=V)kdV{e?f&#\6Me"{Ȭq\QUJ7h|'bVߓu4{ZI HTjN8 m/?s y݆^Bl7L<30`z;Rjqhs0AJ;`*:L-Rx!jd/mYffESpw= S.ri2 m#{ß1[ |1YE'+i`J @5>qkD`:,/?Eň5`=#d ruveB@"${=.Xl&af G붽 ^Pn- *}鐙]Qq02!\=#RTEe Qt] \7&U=4 2Gm=ս||^9.6u-OhD;ZmyLa˼,vi8>uOF5I)\ 28! |DɓTT`pf`̣RюUH`dEgHzIcċl?D `!H)rHY0u/-]]$ w/"D{0OZ4ӾoYk eŶŧC/|λ9YO8$ypAYɇ.0B^⾫ʲe^PF҇c:'+g.+fDfݙ>,%cJ+HecjdUX hҕFO_ ,lDtu~|m`uj:GE1*U2lG+H|xxeH|:n܈\'% K zgT|z][SD\y/S85}9oG#݋Od͈=M̯8,>o+ڞң.B60HwR퍺7|S⪟N=<*jG'RX?O@JSd{UK {H3CoC 8(h\ O"ȱ}ߟ9O-Qܦ~문x@? x!OK$Dox_e2,;>ͥ6 j 20\> e?Qp>NdfX|HiDPR&-2(o .^Obrm+sGANXK܇r]O ~t 2mYyk`~DFt(NǒD}n+Gvߌx]dD~6A yb)aU,XpDH}X3Ҋpn}vځ :IF\nʆ^B ';jـ HD!ݐI|t䞾SE7 *dƹT!!jg\ <3<ŠcIf*RQ|2>Q#m&]|:}' N4懁Éب -T3/ZHQ"qmn)Q{'L Qbr ) v=ݏ~gBV\FT%D:SeUO頍Ï#Ouvm"O"j|$~ fP9㽥9}vzo{Z@QהADY؎({sȀp !it5ud5>~|T_R_qD`yDd ; +H^މYD'jཡd)_+jc*N֢dD<  \Ա 5`˃GZ y4K?&mtyPF4S;uǰ<|j|wۈo5=M#7d{{E@Tm!VZSxlU^+$d)HzHPVco) $ݤ >ҥ CHMWM1"-nE}kSIɺ,rO2N}u; coعS+7 F̹v!!oB,Y VS#XCY;$Z)Y}D@BIӰ!7RJC 0Ufx$kx2$硪ڎHUk`SXY``0Ć܃0¸ Ǹ t:#!؁G,%805@ "7 n 29 DF -9`0B#WLB&0}ENw]5 0@ t|QxH M>X>!:24b}!Jr _r qm/yٮTi{I|>Mgu r] HM>dFl`DÉ3tȥ w! 1{>jZa#r,Mf.$ҕ.;9oEu22Ip~r@BL:7͞z Oװ=El8E&0 ,rGCZtǷsd 契Χ>LPV ط#8Z0͓JH~߬2ۇvWQM%˿"u_' 3SIu xh.Qo[f$zz#⫲aBV!i镈:r-||tTZOo!߰u/or_'!綽bǸ m~R2_T[kr\|w'ro"L0B]CeFA}Y0:Pf*Oߍʇ"w!nCC`ń!f';wdۑr1~t2Hv7Iۻ iKڡ}.o> W9`* ,e2iĨlt l<+to 2|Ai#BET6i{$QwS ʝD*xQ>A҉ć"t#ٺmg"T9o{)Y=XI4֝Ȣ`r.T_[W~4%[Aאt}@;<]lJ22Lyێ ^=.8{c'Vy>|+`7VCȥ "DOm!t;DTyAY傡$ʤ jYW]!w" FVP9co+ R?I ~Quf+Hj.?r "3ހNv/铗`0Ď| >|)Pbe2JR"?M qO"E`)ĦZ CUL˽1,xD=IU#)X#Vh]F|TNE:Dٺ̧RPUSUa Vd(Qɥ7ulQC LS}7R;R_`x QCjAb~6 m!{7d/\o)@CgapOQ)M!~ YDr?}HjK/_׶(/#4t #X=@li q$D Yc9ҥ:2\mw0g3t30Oږ%_mcT|ݥ5EgXTE⫃!dJ]에ȒA_q$` ag#KS ׺~ʝGPn Ў_(L[̻szov/{ۀ\s}gH|P}ݼAVx/_,Ħ)*mrAsXLk|Z_m/.tۛR}k=A8wzǓZ~ *C۰)G|'JG n݅,l5 {n]3 gSnE@ "19|D}d3BXpl)}ҟCgA$2#f~Be}]w@Γ*%||N!яo;p=كd gRuɠOZtdR-H/ : k]|{zyJv <(Wķ97# w?CF WKv iп\9,Y&$7S$y+!DHŧ[U&1__,e=\c32eFVpu2\ʧqK1hkA\ۑl"#-!Go7d5rRFoEToBH&3OAxr4\/N5(w%6ނϺ{TU!恿([1~xmȶθ|evX|Z<#k=@oY)Vtf"s h.1"M5<4gwN` *>b2!Bί/#*ա"">p, 2D #X )*|w J8s n/OXQVgݏ"DVq3V"D8T}IM,駃'>#Ndӷ1v."O"łB  n@ފ TK$֋diyFH]X"xd٢t !EJpr`!is3djT|Gߦ$M>K5VNxR"8*#`r<) ^KS{udcB@(u|R43K >=28cyffLJ+DR0 >*Pr ;)ڜ_a:$M7~ϓ /a_R PQ|m{A۞Az?A4WW;> S#XBT!^w Hv#(_("E#vYլO#65t'S0^O$Jܾ'4I7 $y?cX`]2nǐ~plY~fM0^z*TP"@ExsON8.x'p\"Kqɽ|bC78V|גw|bh%ru O戮0n7_oxn#yןK>8ͯ2I"{uk_C{png!T|+YĦ6gvN6LI=RP"@ELjp@e(m9`xF[p-Z<nɖ!p[x!'kp5{x!p kp%x!0cp53;x,C8`!) r,Xcp-= x,RqOG8`!$/&W7<XcHr~I~!ဍxlmC8`cn#بgpظ&$xxpMv)Q8/e7$I!p7σ2&_/?;T'3&8moTIENDB`gdspy-1.4.2/docs/_static/references.png000066400000000000000000000565671354474061200201020ustar00rootroot00000000000000PNG  IHDR1_1"]>IDATx[$YvsG㖙u498͒9Q Q$Iؗą iԋ Pf+r(e(>Mwf8]]S]Uy#9=3**3*<23<|vbwg*</ |}"QՋkyx繂*@AM̗ҵ"*>?J)f!UY)Wch6{|>+|gD9>Ǜ*~@ Z SÛ1}sp!ffdibw2@2c"(`3,Bb'IU(CL?p_w 1TaSy2`* o$-bZ{gSC/xc ,xc ,|c&""ZztbH7eαjGB Gb%\$51Po>8%RzuZf}c#)!7u)f{D0r"bTgh뷾ǟJf1BS'?gQ DdOSJW/}E,kF18__ć g| 23z{{^Z4z.1(8ӃDh8nhҍEI3ie>'NJ5OP㥻@bbbbbbbb۱W *"*iN ynu<CLlCR^lanblCm攁]1@TUc'Ҙ$|WHx)Q_1ڈrYw^\>ҩ75=5*87vVD>5TKͫKihm+{^Q#xզACLt([?7[PLC{DP?yޟɡD6560냢ZޟOChc% 8ڦxJ$6VڻxEo0/ i HMy܄T&b)DKl]sy#yM}b&>$񦱒Fv{bTcFc&6 ATĨ1 ޷@Qclh&0er5*$FUmyV& UctP#G1 ĨDջmf'#PMx"1"ʵ RH4!$kWkkX><"9@ vdZs+j4^u/OI؉q9 TE|ħ٧キz^(fsswxx^'>2Iy"]Ng㴳MNq9=9;>bfbHr>VkĘ7Tjhusvz9;S$xJЭÝigc$/ mҧul PTD%"Zs"K}c;g'WW;@O 1*‹fpw{t#834ύ}߾horBL!ab}mh{oBZ-rQ#P輏V.6VWgۧ n]6tœ'o.dlF54+\g^]1 B`ć4k4Cg_l Qfͬguoq;`abRFxvyǠ&|#DEn+񖁞jex4"`epy օsϜ+y<:OLu!ĝ~O75|5D1wmDBf}h]pqL|pE8i{)8ڎ&jr~@?\>{*k2@OsVf{`&)"Szy`1E"[OOO_]nwMb"eC݆'j&Ƭ aOI O\O7ۗgggGgluk>8ځ7hdW,i}xsw8k-"o \{6'>tC[Wy.>zy=lf`E{ _4wiX uȯ+*d岽Ѻa<͡{n >{I_p5fL%6+|*\p}ގ.*DCrsfLgc5'E]g[6 Jzц8&drnd$"0, hC$>bQ"-U.JTc'aXz$BmB!b6ewBUztr4ys"""Zb!""Zb!""Zb!""Zb!""Zb!""Zb!""Zb!""Zb!""Zb!""Zb!""Zb!""Zb!""Zb!""Zb!""ZNթ|]"fDdqM%Ę`-Ffu h1 oXDUL6: -PTb[g'.-T%V:o~5X^R%Dxc]oo[-+AQUQM$u>qݭ^7k7fh`j&Xn $`$epwgrm;_D^$Ჴ!Vy\EըeUJO|IgQH\ &R;sWW{GY3˝w)E7?us7Zyg*2P5jLO[y#xeo D&b^"qy^]l\nn5Vy'=UTwDcׯ.~MUj8;:Z^༗ Tj5ӬڸW$+T5EzWWn6NN:;ĄFlu!.ニٙ k`aM%@T"HUoftkktk4E`6Xc}ͬwxx^'>I:;jgȢ.)/F ( q0#5񩨕eɛu@#qwtrnv֣ 'r'V&`Xsj:!fv.η_vjbY^WQ.k4??~u{tfh$0LvV^ڸ::qkF)'rQU(ODc׻'ώ WN}1xakV =Ot 0пk^hyW :zFu>M[OBvEy.U50 5?9*>|oxy\_f$5Ed8''.jlbbpggtk;>/F9J٫WY987ȡ`}kV58i>-I~>{fUŃe.Vp8x[)\*UEAupGuW8ExWӕaW5|}a8ksLh9A{ Uʁ'xp֞eףg΃6,|zfJ5H'9q7͘ʰ ]MZ5VW=g{';g… UAAY[i5%/Ҹe7l)#Nkr6@8nHtj \~_{ĽPBT'f ۩ד_MU? jv+P5IHaJ Q/ʾUhNދ$H"zLa> @իÚ[k`qy{| 6{D nWbPr^jjK4,>j@39mc=U- Ýig'-oCqwtpw|:(RѼ<|-mcܛ _Omu/|"|WoVO]$ݍ f7Qn5^v..V.k7+Y\Ҝ3+^U'P5jJTnM kE#<7+&Y@=i$g/uEAT[6X3:C(ɢ1Y}))דUeKi0&FcL1jE5"T@M1d1Qq)d 52ljg h'r٣C (͝k\C'M-hF>O{AWb65Mn\r}[>dz4q0A$5C@hwU<(Clo~ODCJ]1pUM@U]Z7rWhA`̴+ONQk+mo|]T^4l}"0p1 g+%3h i]w7^IL}j:+AL#-+ivCvOpShVb` koF4jtt{b c$qk%Fc^ODtb|\4ԧѨZD%HZot=1S'0QctR NRh[foZ3)DQ(a(h\ ""Zb!""Zb!""Zb!""Zb!""Zb!""Zb!""Zb!""Zb!""Zo^Ǯh/Ϳ:)ahr Wu;^zGU4Nef<8DAӜ Y0Z(?;)1F?4b4DbI(ѳj!F t:%=)snkwpM:yhܴvDD#l5DI3ie=񴗮zVR [x^d}b H~ܘ"Jf Fh>{"zUfqg_~0V7可X~/.?8]ښPst?0ڦ=Ĩj'?G8 842Ik_dL@Q ƌ?`L-=v4B\Ld+7A$Y8  z(6S/̗9-–{jGK-;A i6w 1DDDTSnAT ,͋[5Paպׂ\  ņ{0ļ(캯i3N4[ /3zKXe[ 4ő|`(O)18}j&- /)G uk!@} A f:UKP~4|c50 r3z}Z^p2ԯ`6cPDyf <ʉf4T5*0{(kbVfxL\yp28 5 1feq~hѽ+ X,nW(k500D ѲxW9! Q=v~~R30?g0c~rb" {t9ֶfx.X35 <>(X:V)&3qD:x?{P{chqFo jIˋQydCX&B@_c?aG@#QCѲWX|rkR=z i9Lm(oWDV`Ϝm|}"I!}Oߪ>QJϦkIFC/w?Oؔ+{ LYw/d#QH$w0y\͂Xl_m\4jpٺ{Ql2j ,[T^5g[W/ΣIה k^Z_ G8_K %\NˮDw Qj"^>ۼjׯfh:6]{<|X8[ʚHR%6\yo4z-}~48u>v@0OX+0W׿):D[|\"ø2~r֎&qgpFg|~{zAY">)QgkA Dcn֮Ͽg[ Z|jb 0PɎ$#TӼ:ygӢp 0^|lm7+kVF& LssuqP+:Nю\кYY[\_YIE^A0EXX_[loi5*2/I^v׺>Yo `Z={\Ce"5 *m&}PVZ`T$wy/hc !Bs'jk" 0s](&=*51xm-lx Ɔ ]}I^lnn7Te1z 0 a][/ AEHFstk˻\(!fqxWӭ4k4-ƾy){}hmm)F*16tts{ғ׀F9 Nc&pz 0P`q*oH7N:Χ [ɻ:i Sy!(vvzttB\/FO|zL|KkaOr9F˻›)Mm&bo?wܯ=,tbp{QDe:(PG `fv: `z/"u `A3oj7O;u1 @H$b;wj0Wd_@,tn7> u 0y~0~$'Yv3;_V_g_<[3fwݾ}8rZ5N'9|:04/*~ͼos/՘f{^ۻ>}b[Y ӭN|?JE~뤳s"`;;QTD$/ֺk~[sigFO׷!:oZSYsg]Ap7{v'9xr;`>VanSĸbid>d&b l`wEe-<:a>]KY|+5]Qlv,% 0*?+7GP"lnVb&|z,Oa?ػ\ϑ0fsLJG=,Ub3Q빰<n@|z,EjsQ#b'XfFPц; ZK`f^:;SqlR([׫jo:Nb*Br34o硨R46W/W+q\`@TmHN%;DW[Χsr1!f "q++k}'ZL1STT!j5/7TthE(VWڮHjx biښ. 6$.Y\[ 2f-,_0Y"Ie 1RKf @%n\^IrzYo̪17WWc*7WWV㛧pޚg` []#(Ǚn_^_'y#0/jFlT5 'ǯw\ITsv]os^Bp9=>I~0јg/Nӳ5Qӽcy5l8k2Q3/Zu˭nspѨPX\흜"[P 0/&u{'y398irվ,'o7e1 5{qYƔKxEyL`5G^`\$yQuc yY`nUJ43 Npy{:`nUw9;Y߆J\JO^E2IRpUj{ݼ1:eay j_^_.;+?O?`0@%ɡ6Ux./λ+s:q'9&a?ɱn>fVE.Ͳw.w*|7.ֻ3ke?Xe`B=?Y~~c?,櫨Ȱ͢w."[sw2&_USg3j㹠dyAi ˹`HԘ~|+[Q`U` eMUIiɼUb`F! gۧ.&QQQ#O53 QM|b/:g_٭U$蜝z{M:5{UU'`Y3]lwb6&yQY_jRE$ks.O#EzRQ_BaCbF?`:o+9?:uKA')*;+ l)mdy7?kf N+}/d(wYެ_^[ok'0`L }듏wޘqB 7gN.)S;{I?M1v Q,*0Ljw"[$N!f >?I  LT"**"HcbBodb" 1Ia@$K:sm8S1Lhɍ3pYS!ec^Zv 0D%8yA%딥pxQgs(?0p.?'p *hPK`hnWFsBWn2#FD$;~NW}|9D+{1y@`ؿ;8q$Hvf"ټ~ۼ~ 0^Nxf _nA̐?9˱鄎ff{9!D2@zk#1|8 5Pb"\PoߘԔ=x7+00SqoࡸN7\`ނٽZaeCTbs_ g6es#Ѵ&M&_iʱ.5i:/(~kgѻ O"f;IF~e_Cdi^j`(W_P _; 1 KTA70\ SIP : a 5@^H~?΄@ B2Wk0`E2Õ>0k)oe-{.‡zPznkhu@DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD49roW>Ѵ |}khu@DDD4QL 6ʎ苼dz<vY-\|V@^ݷƗ&g8DhCUcpCKck k#( /ߚ+2D%T 3έ1}WU4 # _8* Cf| 1T`CQZۜwA9Ϻ5CU(ǝxH[2O5@0O5@cji{R W44A^2<8!F N)1yl;ք+4Iqb4tfe0ڹkzok3`wRl^5@OZ7ևYܿfA3rEusv&QH,boek~7$-EMK!b`?7}׮تԭ_oEP7@ŊP?'/W":^@J/xk^};)DZ77σiLDԨh2i~߸^AF-l+1\clw.+0_깚J(TIL.0ur9 5Ic&1hBP j;Aq:+Ϳ'nP㥻FDDDCDDDCDDDCDDDCDDDCDDDCDDDCDDDCDDDCDDD-I07.LsKͯ Wt4kQVR]=>)LooBL#+@`΢5`RԧZX[0 @ jcBj§4raC7V/?D4 :ד?MhZ]F6/@Y4rhܸh?rk@T5[a~\q>룡9Ds}<|.א oу ;g/W_ԦH-,x':"Ѽ e͆=NzPSJX=K7ߙ%^6XʥtQ39P,)H!o'xV }L!L *DLN1D-1@"I H^j!IJk51"*1 pY&rcw;#jdÌi$5&T;5jzk'Z:9IOOs\"Y$z$ ;{;O?}Iᇿ?#$.Qk~k5^3!@+/6NDK#W[]84N4+g흃'[&e{svrw|\8eb@&oOڼiaqy}ee'n+T[ 1*L]?l=7hdY926X'U{;/V٧iPmTbnM~3}jwm=Fɝ{HT$:#˵EsvH0ՆĤ>87H\*o/j$p'ПjӵF .dw@l]_m\^YoO3{1ng󤳫Ao_JyfԘÝE{dfJ8i &nM|XX_>KFH<4|&*~z}}zt9;Gu[w^^y:yO<"J>lnmUDt踷z}c`9xRAW.WW;G_ݰ8!h?9^9גBy$ 2g3·gΫݧjF F _y¤}7϶6輟څ@<_뮭y"?tLLՈIY1z$ .")T%$o1XIxCT@fI38ZfHU,Ͳa,*b\xƬPv@M4+RxLO0`D9q, 03c G<\xj!j!j!j!j!j!j!j!j!j!j!j!j!j!j!ji*!F :P *TdAKJETTqDhsoq ~} CHT< P68gM4I*%RYnMlpNƪ`XwihxW"e#GQMU DU%j(!)<g!F?~eK4j'FR‹o|eyg+/ ZN꼳c!4PQLIIp~xgYT[||Ӿldͦ Vh4Fl޴/?'{aq'{Gݫڦw.x2Q$ E7RՓニYA]p7Uͪ1Mo操ӭbB#,mhuλjtO_D凟Xظ>k<3UF nh卬˫+m4I:_o>1EO,Ddz۝VoNqfE l\^Voq!.DXhS 1i`UƤw;oj~]6j|YUS$nZ^%ι Y`̿?)^o[^<|ܚE?(Lh ĄF)ЫZ\_]Oz*&uKQHdÝ@Ϟu;{{i@+iH&\@9x3&2?)8/;۹8~Um˃+R':`  1N1 4[1VO:oO_8JJ \X }Z4J?]~p98h *.d/S*A !rZ^Ts%fo@fƚhoK8Za^tjS-ЇqmvPZI+|Mƌo IpO"i1T|!4u0`iMoÝig;$8y0{HP} ;wwtpZڌke,/._\W' WT}!;zNCЭ|wrzٹ 8UOD{+'Fj&@/\8yggp!Ks!& :QuQd Á鲆S( 'fU#N+KcR H hc_B)?/Ijp.@dwU- oD(CLڇi׸HfK'1J@ڟP<6HԦHTM'0TS6ުj2)5Ms5zzG >7/ >Mf^h S$B齓կ׾>ht@?Ռq鬉hE]XnF1Ɨ]w(:bMj{$p1iK?A|h>/+hO A LVf)`̴O'_L6p>};ILS󛠍Z,Y]P4*-}q*@:&=ƅowhjhsi45ZF/x|ODM"5QMQec/I&JB jDj4FX4:&5*+@]#zW]Xiյ_hR"W@RC C C C C C C C vWv481_xm1Ѩh`>7 $Q4*0S40(5$E2ףCV6V5urV6VL,r4l<]i^ S>IKih4tW]{*@J}Mb@O|ӟ[AAg}==*_?5ygI 24qDG7젶F#iONrGS\Gٿ;cchyyf2&;W"dM @y7#&bkkHQHZ0\F&""ZRxJmŗFԀxOaBԁTxuZ׹C̻ +_U @Yk |5p yA$Υ?C̗ mR2 Wp NT=?rEI8XT5 Y3T[Pw 1_4kڃ~*:xpI| ~I{X 8UG*âAwKƎ12 7F6_MFX Hcb|eNLB M*]eGH(J!x_&OCP7(o[+/җbjBy{KJ|~&ip }DVkMo.7o^\!ac@E$) I*MFwdN͛E8a%E4jn[UX38lF3Z1"`AŊ1u bjAV%bN5N7DBb7 k4ƑB(D&Zk,Ĉ 58ab/F# 0ՙSHшxqKjib bF6&b}XV!&#A֛YчѨ4ǯ/Ccz(ֿz+QEz qk\f`$/&7`9?ȷX*QbxiReu 0*'ymnxqdbuӟ&t4B~fY tQ 1lwv 'p*xg5Fg&tZ]7։w][-fMke{һPblmnm%/U[[ihFQ1FeTEhv'J0Ui fq퓼j&RF0ĸbip91L,lu`wOMPLL46O|뤳*x-JYjltv4Db봳m5uHN `@p!mx'~妵zL:$No+LxDE:)\pA!f T$;;":Cl&}㝯/Yfy\O|0X - 0 Χv\Q`DI^v׺ф>`b;@LV/.ۛE_I^lnn7Tb)^[oB|>-[4k47OY"΋} 6A/#51uk!f 0l9.L vP7 `ww;Nm+<ͳ,06󮯮v֋$/u1 Z{WߨEd]C;ͼa*K Z 0TfrdZǾ%n3o%ŭFBM$b?fw0c T%8Z3Ա/`I-EIf^Se$b"@E W)6 jc zֺiNIԭ/` -~Q5ޚ)6'Y|+5R"88:He0 `*ov0Wcr~(Q;;i0_4f&?цh;/ \`fvqO:_ܱ~DE|k<: 0w73ۈI^u˧4s)q,~ (Bt޺-6io8ΎZݭy1`J63&#s5rR*6w|뤳R>r=3K`*#f:$y1`*dмim=Wcfe9L;bu/`,Iq*?f|jO1r`RĢA3ﻨHp>/ɾ3P`) (j}Q#E 6/` >$ Le3 o$9|ghʃ@#,xjWфZ^<~ f@`ObtӬјY3 :;bfթt@=~0@%ĕFgVͼ2\ެTfxK@ wCym[=s N 6sKj')0;7.}c9P{U'iA* fw0]By=f& $9c*FE\$R\ɀfo6+1 \NL\(g8X$q3]""g{'+'ә.?C('Ξè!$蜟)Q,pW6ZݺVz'NF(O\[]'T?`n70a o+;;9͛Y9*I& Sa-O`QJ][H(d^v}zicO& )6$6odY⓹(/ۮs vDcM02/?yz:) &69?:h2T`L1;Xy# Uw9;Y߆9oMNU.YzK`rERԙ/:'I&p[I%jRu}ymy 1KL}y}ݾ,D%Ir9=.Y=0@%ɡ6u{++]© ˵}DygOcf 5zwrκ.o3$OÃrڙ ϼNNN]y  6&zj{ta^`ѳ֋*"acޥ{Uy8>=ΚLCnxmsxϼC0Lۈ֜θ`\z\D 0o::Wλ4s~.fgY 0o#Hgr`Ǭ|j }A0ܣlhM5UaOo̚|`*n򕨎`eMffb``VM\ 06&_z~`P3l&s<`U`JTTWnw_tg1DaWn%.8w}z,,QHt!U'irվ,/fcQ}~r*f`CDTT]W啍 &1DIe} kv/f 1 0Qa_@HsY3]lw9yOKbɱHqT'#6N=t6*B X #S3H0L@}uxi)ئ _|zj`&PkH] 1 0ͿDUQ~CQ}3'`/oeNuOW*AII8Pir=/N껦52L1f/?>=y4O`+ih?9|Y<$C0LͿ>R*ƿVP$>IVݫ9ijV7cVAYGF9;953 G/;+W+kIh8E/ED7kW7)5hݓhc zw~`a˧DR`QF{#Mzreo|dF& `6Q5ٓ}Y*Sd~|NrؚMre 2U˫1Tȩ1IM1`@DM\PS~Pd`4hhכ[B 0[;q1D1Mb]#Οw80l13,@ cq?S<*g?,w]#G`o[ 1`C .j>q"T`$f# $Q[޵h.f,po(6$|~[i4$b`48̓SL*U4eyVqFCBb\2kcbP1(CQc4<\0΄0iFؑ"Jj"FU k뫭N[= ?A7|D,}tkl_= 0B}=moF;1͖]s7-&Q8'?K)W\`$Q'_7K+XN7Ltc79- `#M#ا Q [B2F+ 0@#Eq8_'?4hh$;>: 5x_U70 0P5Zk? <B`hq ͥLߎ4XAcq`CsX\#z8 .$y=춙u` Y-50렐XAY?›4hf"\p_q '#jRm| \YJ2,aFp 6cMX5Xv~?@yL` P5"Usxk$?~ͪ~[ /HP 8DYDoZ |g>As2P*e|?Uk61+|FMDDDs3놔`Fh|Υs%ѷ"&w * :@ADDDDDDDDDDDDDDDDRժUܷSIENDB`gdspy-1.4.2/docs/_static/robust_paths.png000066400000000000000000002677451354474061200205000ustar00rootroot00000000000000PNG  IHDRX;IDATx}w]UۦN:.( vgg "  N 2iws{L$3O`iwy׻zZ@UTQEUTQEUTQEUTQEUTQEUTQEUTQEUTQEUTQE.x#Wֿc&xLX3Z)^oYū@U` %E}@ pUҨւ՝Ǘ(]of'gʝUT TC6@ `h00 ^l8 XEc¨k8L608@>-!MVճY وnqğ%䀁S+UQX;#jсsqR$K `P Y؜x@+h ש*^-ƜnsNdL P@08Fr(ecaC _I5Ua;gӁ3@S M@;j$>VepʝU+T΂1fh?O&`@@GYB5 v Td}ĪdQ+sm| =(0 DB /)D/ Pwg1aUUſdp@.Hŀ@qks|s*vT<>08  ,Q@F+A)[%*^Št%_ЋQA%y3j-!P@ g (-Km*Dhy$3s6έ?ylp'k\Dz*vdT<8WB `& %RHnP UEGLih'V@`0dcP!Zz$c40x@*FϧۗDȿy_Ԉҹ-4W dUl a I(qxPbTN++F};MEꡞg~?UyPX;*bA-h9@'tD)=NHlSp(dS(eg!Zvama%,HV 0Hhh#PϚ@EE69pITfQ9ҐUw``/d J XFb2rTn,eu}woUٛ鵠0 +Qq6\\` [ ؁u˟RA$xȮv@Qk$P¶1xy!ꯞ7kGCEji &J,cAвBnf3Zoe`!Ȃ⚦mqwsϞ`5!YlCv ]V4 UکiEñoZUQS@(sgo"HPyPX; &JM-D HmJ>~mtxA [` `Eڟ^sښ׈M,l@?ISzUUdI[e?- 4F]^9w`v*Ji-(RH}7s:mۍ1FhL ]l) ~F3cH\YIͲ˕O8cNB,N&Tpba(z$0?<[#_l˝.VE.D;O0V@-l>Md)<{63D%Y'4={ l!np G0jUc\ [{wq(vaTfn>@ <7Z,BuL܂-L`=`;& !?z'&5j{VCe'FY A"kF'/gp#dnYsӚ`A}GmLd`J*jZB[`,GԲbx--f%UU {A(M ꮍz1J Ї/`:2-T*2g^ϾL9 `\GNONtfpqL*Q( hA`#x 6)nrobMr A/Jm3R"+<¡ ˔1zL " gecD¶N#}QUd\laOqӈu5ɿi]N28K1b~J̛ 3;|G&+#skx<'[iF鏭@50 *vmT q _a-Ch-2]p7``qJwn`AO[@, RB;` 듗'arvف 3{|Kn#{ aGG-d {[UdbWnT, 3ح\o;X*4 !q-@Hs):`aq[DI6J[$l웿;ba Wlz)} X1i7*vMDY&n:E@u:hd߽/) ƹ!ukIn%tdd(H\L 6* `Hji`Lxog[ma֊!L_w 'ϪV]UFa .߶F`=;]+v׾kB͋z&` ccX .pjMl6nEf(1sqpC|[Kwۯq Z/09Yca&VE.hy Pd.. z,l &3^hwwC?ApylDm!0Yq-=êw3'e5kPg5OnQs 5ig|~YC˷O@-cF̶}Y~[e/"kWD1Mtr$Gb,_F_w.ƒ[eRY򼬬S)3R,lHgB!e^M_;!ki77fXA-%d"2CgIf3 o[h2l®]N(Hn꾥ʝW # ݮ>Lsqj]ρ6-=θyO5gmaocZI!9A@^ HXyc5-IG3VXpL~ȪzvfD糙/lnȒqfN9aGh#aMÐ4bz2~5!i4zEw읿ỉ ;>Ȉ\bEr[Ě^x =´R?;UFn>`6!-tAos9|Q_+1YLy&zX)`0ܩO dI>9Zlⓓ3t@^QUR zJ\SxpfiD;Dfj pYFuCU`Wg78x3 HZdXK=B|:d2]'ar4K^%{;x Y#lMsb&Fqa{j d YIQToc;+FHcl0e'i[kO,;p\GfM)1K#1$MfkfpIGe"+E @̙YxA,2ZY dG۳a(rνa.kݗm"RQUd b)7Dn(p={͗:x}/D`:(B f!yak/PbԱK6M)-i8~Χ}-&}+(,aCR'pMY/n}褮`ꭇ(̤Y2{;or箅o_+A;zjGZx0̰P݇/7!\7''j3A$|p7Rr; @h<\_2qC=ԳAʯYQgg=&)T[y7ߺDEuvʒ`Q??_% [ZN ƭĕY%ܛ6yw_-b_?9T# &l"j$lfkHLVVYFg0c+.c2ZrxKjqY`o3 o]n/͝H&\vR_ܜyko#1 dp|HkRF%`Xq(6wtjW j wF\p ]~RW$6Biqn{zKUQX5t{) zor箁ZlYkFn%gWV)㯞ƚb˴bPAGocESpeLK5\0TZ%i-_|ҴVO oO9} rߤq҂B":neUW' WcIȕ7Bƻ{Vl_z{;ħx( RX L&Kߙ:8B*%p|)*?] ȷM~2qc);С%֪H0:Kp4jNbx>u{:yZ`@Z0_df#7N;nBa"T/r`=Mn(ؒ ƵEZ&[ܔ(6RŠBٔiezח}MJWX k 84?!'{de:Zݷ] ύzk;wT忁Ղ @fǤ*f55RVuQ2R$m=fG)w84F(1$REJzԦek{>a2y]픕lӖ)I/ q䰇g&n@v{ܓ"5$]%aB:L\F$&1%mpK'Y F nMUO6GA}c}-4a񃇧(D wHġIl*'y*+\z8u؝ZE." [U {8Nzn{jS{z&r:)*BVrؽ6HRC ;d<kFwzG H{SH$Z ?1-֟/-uxVSbBE7k+Z 16ZL j;V-'4A^Y=sIIbjZ=ز"+Tp>6 {;,lW>z@k_'ܤwtIoD`'ʚq .gbߗڍ.Ze'E/GpKHBd01bC;wbTށ/~ÒvE"uf+4.{,%\⃑jN"g@RiVY,dh/p+ _5ЄM|YwNf3urN$F.ʄ̋x5 8[8&}mXEA 76_ ׃Ƿv`u_^vnTH\r|XMhB@s3%Ża'!*hxXnj&b{ gf.B /`ZÎ0D G{!X5KRTfUv9Fx7,Ht%LhM VK9Kljx|B7矃m 40)hГ7cQHi]vohzp5ܬ"mvH%&铆ܝ iE|Ou℉X?~<8輱ki( HjC,W8C3WmNaeUޭzZ=;/(AOSy-%B0E Ioa `"حBV:{m! qS-ⷤۂ{î&J`oGY&Ig[_wa XԞ4V=FʲWNg,/zn} g/}tM, QSt{X% U`rp4ݵWV'f}ˠlhi0[fo`5Vb|vbnܩaoև|~'5DZ4!ՙLw/ o  )|gߍp{7y2Ik,4+Θ57hrwa􅹑@?cv0l͝Dl-wRfz>brBSM5:zP67|[˔ȗrgB<=h׽HMt38Rj?A)䍲X͵Y_wgx5iw$OUok7Sx$Z%_짾:,L%ib4W)@;wNT녊'P)ԑeIzP)v_~'홿g /\#e8Awq{+] T5^Y1-9J#MC~X-PH?ls^vޘ՞Zߟˢ|m̷3,y5 (!9*(h1cvڳN,z(ƬJdzpfeAŏ5pz@ 0s[N[gAQ2g(MQ [2YHoq2Tѡ6 !\ÓO ,Is!G Viqu:7d(ˊUcJ|7rr nVuMQw%IE QDsq^x߽9\+7T"h#uAsڥb0v1V|WvP'47|}:=(~gZ'-_ٻGvL^왿w%AFi=o٢k:pDy\UzQeU.p&RsӰUk᷅.r2 ! _oщ%ڔp5>hM} d471xC/^\غl wB`եU6YZ +XASR= +8sܾ<h'2-g,]h Nh腵@9l+4˓{-3ݕAZ[U ϻiŏ[ ֆI@Dl)#g*.f kv| <uv?v>T@_l3KH΍d" *=wFͷ~E #`8`+r$r-:={LFJhHrȡZS'&%|p \Inaddh(Y  -hC^u&Rr,$Bm|oߏDwWXzEPiӆXzѻq@>E<"O(U;7%q[x&ueA!XE蚩#IAX)s?{)R`pI\ş8m5'OtE W:1 F6 N : ^h.@Mo%NZ8@91!޺g&ݻ$ V Pl[nֽ?z@!tVhfn>>jy@`1xJ )U&[N~.ώĕP_ܚs~= B w7 ˆV<=m4; `"9V'z<@1 #t8.Zߪ9;T,o {гTf&4* @Ff$~&L.>pAM #\ &iȄ69+4CiAYU0m3=U)u^9S'xڹ!hQP#PMk|%‘u:$6 9m=/|c\@z.PlY =[/_FžKԭ][upG> "0D? H74؀b qi_}(aMB> :0' S r4S=zMG!pR:13ͬr(K6̟(o-A<|U-W#a߫؜]4=nO Պ:33yGu=RTM)-'cH;_' BXn1s唡j=lM"3xjعPUï% uc*D |eU'?fb%W9ΉCLD7Zrb6W ޢo~&W[{B0AQiJ@PO  3N} O_ZvvPp9]x'C*\jmW~Su.D1_` -@L$y FK/J[t:vQ/n&dH,g득@@#oY%zvU= 4E][M좮4$%Ƌ #g_>nBсT` u}òL+p8, \->{YCQ}ΫfYjr4LmjT\2MKc9i3++ɖv#qbOOc[ ;Шȯk&em!V+`gzC=wSwPϝ=@|"X4O^R3I,A79E;6};n %b}h.rPlQWfYloZ; 뵅Js |i{zYlޝ#rzRLHp^O: Xs^̯jyd,7)7 z 0#o@zpiPAǪp%ñex -Wxtř]ƄTcm{rЪeȶP(G>nqBuZ,4`z4Oh4)SRTYM)Xa X:ӽ2S%q%8wR`:B%=IR[6iJ ~} n K1 yz π+w' #/Erɴ LOONGFTXo  t26XF1H{GxAಂ>{?_Ը-EIԟiʹ?zoQoe<ޞu(߼Ks[.GD7ދ Qf\ʁd smjA1[Q|z); TV ha=~]dZǬq&ЂFnSP! 47J03%.|2 =a-Bb m-:ng]pq6?8: 4觑W|9d0dҫHy;ɡN3LwO4-y!|-i16qONCcQ: iz*ZQ%2i7H36ւZBpF`x́ c  {f?ۯ?O8@m\iJzUPTd(3F"f  3m7D8%1ՇgoyfQӋ^0ߺ—N\T5nQ`( y2Ȑzx|bPdOwN\@%!̿M9-!ǔ^H\G}7Ggu@&!fqz08kf/~x$(S@N~]m7DNA =a[ w!%em Gآ΍=Qz{O|WY1ɋsm=B_r~w+>H2&`X!NۏD 薚cv w2/D +^Ni9( zn>?*eRp[.?}Q-0V0ߗ/{ { JUlv 1[y̪QL@'7r T⥹`25t %i/"޺(YB!$>sQr{KC,l /AC투Ƒ ػg}fqRwZ`,p`0$ ls-Y6{T\ /?˧`Xt&, Cr)vt]H\zWv #.g ̥(r d`A waޣ85X0Hw/(9KlXlc;~t]6\paOu@l:^(, L#}PqA \`|1L/i,hg _ 헿tmk:vڃ@CS8|ʥ, [9^:_Lo VL1=[)Y+a:!&&oq'2׮ր vj⏾| yd~ơleO&eYǯF."|j(zU'C,8k@4 ġ6u؄ aQu h1ڑťi/!k9u:2Qv%m@5ow/.i7 YXi3YO_~HRY= {'ʝQ&e@|bT%HZM6迵VZ‰e!Фu;-ׯ-6x牃qBx70S1wD!!пqe?"1P[J>XЅV_6ۤǏP QWpR{_Fd$c4ꢴ*w  u}J/ Y(5|âV5>Kuy>zdvO}(h-4fv9M%qe{DVҫ t1Ja 7C_z*sqH. )[ Ó~1mM :;Y1yrccpGl-+!ωz&SgD[O4ި?Mql!CK70$΄Buay[ƻ|kC'ZT%5J%aT:yWzX0d=Nl]61mSkk֮S6z!LkB(//RIYdXqԐup@2?'-(LAP-(-q8l͝_Ivz%A3| MdQך?g =؊=e49ٳg d}wȝ7j ;R SC782ZCH6$y8vp0g ~8*TT_ȿ6%Z^w ޣ{K>T}#MwzdQ+glj6DiG@5=seh"M i+A$`}'?O6yB8f/|HhȢO~ؐ37-S謌̠eOCU% VPoHC7S(Wge>,{9̀I1E@HC 4i}0AC&\⪜)hF' &eg`c!DC/=٦P;ah^΋ڤ/U3޿M̟@$|/ .žRk qg[jd~!Tv1pDᬪ9"{E u"s 'BPY̬Cݟ,3=r)N2g%tXۮn]?G<0Y@:x >%|RGph o}] z# ձ";v,F31#h ea=vl1 /f@΁WB [`5 DrvIZظe^ן^XX{u hwY(@ob OaBBvEIMS1!"ȍs `s/,9~wo'ذN98I9yQ;ZUcrCZ y)1ދgW~9SFGw57D_PK \ס2b![L @Q;`ڔIOِr63!X3- L~ٴ\_1A/Q*濏_ Ý @EoUp"wеl> /L5|K ,Iӂ ݂JS*Z, 衞PDB6n7`nx}tNԤpc ]{Kk65_m"yr *QZTx[̠>7(=JO|_p KG۽a˅z_r.5-WPb)Cl9 ?+;- uP PJz`J Ӧ47/! {4ʆddⲦ2O4Td2Su$11h2O)s24ف}㋦+&D`C<1s* lΟQdϏ)xzoGiPY'|,+J!z ĉdQwu4mX˂L L{m%A0bwArP+8 U˿ O#Cezu[, m>̟p|9^7=)ǹ!h 1#r~V( d0DD?qg)d4[֨+CwvDlH!y~shޒx~@ÏQ>ʌ-⪌(R ^QYvoŃ~J4`kTCRrh[1,_<{~Ζ#-Ϊ]mBp0&5r[XqL!X %,"%cG ]q6kqu!pj5kk vFXNJ̨z 6/Tp-4se&W!`9 qQ[ E5rF8jۙJޗ ?`ϛNO)h>];Հ|Hcv|3~؁_\s{(-q01x?ma|QXPg"G7+CV*v; J͝~݋\9lcNHA@9f򶺳y P)wpʾo)t魠[N'BrzKקȧu gO2^>vTdY-`8bKGp\-  >;(r:|ʊgݐ4#*= RPo _1rNW\x Vwz/Ee϶%e8ݗg{JKZg@Xq1}fepCŻƬ|!zHrI2$N ?NWQ[h#U|cQ%WT F(T{N|,IӞmDI1Pr8,Wsς+ 4xCc1fK$Yms{ljp&b $Gjo 1];u[Eu6֫A걝u8B]XcOVچ8$,I''kݻ?4?~˔lf 30W.}a, lѮho>|}q lXcID?q*8:;y8!ekhZmx *qmăL-gHQ~Y3!L_tc#f2J_o`ʆ &̽ۻ9dnx.9O$X37k7E~O<n%P;rFMu6֎6mQ!d%?3^hWxk:դ KiH͹Q-w-c72cc|Wo*#Ci]I'ؔco>ٺѳKdBh59ΚS/"SaO<2BTgc"؅{-i|]sS礼Xw̉lLd<[H)^X< ǘAAKB< "[eY5oH'p@W~CɨFg%)f[0"OeZ|fG^<CTWM1U@`<qEg_Qt/I0$kc?ek[5;Ԕ|(KG> l z w]P_!+mXuX:o_ Hh$1{]'`1ZQqP}ǢP&0DTV/0p] JSPy uDAl-ICyJם&N1^' ًV7wR0e?JFK{,fH:G3P L5 $5A{((3Xowy D-2azb;{Ewy虽 ] kWw:[6ɍ)3p D,ӟY~;'*#m E;T2cYq{R'=]D|+ .MASWYGhT+2xVR!E? PGMh$KB'pzwꥩk㭌 gbvD{V1Yj78'N?m ֮JL~1O;\t1kZ={*a? NI#Rڪځ&J!US+6sL `2V6 I;Ab ,O{ ZlOg;Ėgŀ4mè"D7*="S9p[?4c6[kY-9ۭQ_P8ZMV综g FqcT_ /{MxvV շ׾c%G2Z0Kڻ?u܃V.5,I0)0"4ha}- 0eipˢs*꡿׾ciQIJ.=#\3g40T%-_yL-UK 7p$ y0*Z rs<}vhR<4NgzsҐ2b6[ѝ|q55 -]/P!6 qx#ټRTJQD|˙t޵gVy󞊛lcc3)q_pv䄴;`; ť9UAX%$Gn[ ) mnioᖃ۸fn4OBE}@S2}R}4&m헭1.\[L[sKݱ񵸥ybVaJC߆7b$+EEV6 hF۟|fyR%}$y}.$}k; f;޺ "@jͮ:e{232U58~ὭB{P!˅ɷܟvDgqԼ ak?RM(%w /?&+g@ݒK 'nrjrc3!fLsXϣLPzF^crF X!p191gJ߆I[X8u'Ѐ{K$R` gJ-wVn ܌.uo(ޝYϥp%ii\b&'948Ł0aK[ܹcEUIP蠫 !uR37[.! 4@Q '$퉚oL]K. vǹP۫:9@K#B9O9;=oxߒ 48 20WFA%}D:zN^N\8!AM*<5w=!!}6R-dM]^ y[҈ΓZ fq&asb4׸7Ta"'|#]@748LVk22Q|(y.q> t{;&;>揧 `@(|WW.^>Odz\zi6z!gGfs6VޟZLѵNmuc7;1ZQmc1Rh֓wɱl'׻WƸ7*oN+#}}҃F~JT[ c&LMtD |  ` @'P5@/P=2$jo ΌfY),ITԞi+ĭ(L6ޖ͑eT"@\Z{;̠OX85~b7{;zˊ溷O'#EƟ~s ',wDtF*6x[dH _wa! @[sdb$8[[܋z.m!K7*dz[m!Пs#2T -]oR5L8y<<LA^Hj|bF.C{@uOY1UTe] .xMwdb Ti5.ѤnF4ޛ::kV^vUg=d48%0O %p-&^gwj3kYM&oW@҅@./9jiq8j @; kTXY0TYTyW@TEuR҇`YVŽpB {@ ?P6kY [9墑l '\][Pw:9vӿn}k\e;P h< t7<]jl0@eiG=?P/3= FuXD꣡[IY߃9^Yx9'$ؘAٖzۧ#\t:;OXdBb#P$2#dO{FT ت^Ng³ZGG?`z>ٞz$qx (O'W.[rUdԯTbB XsoI6Dr螚S5 nxC(NAiq0feaz#W)1xJH >{^*_Z5N, -?.`yͫ9 %Vn=ߓk 4DJHe),腡PP 6yҞs]~{*F!$]L}*vq:wi7:-  Vx.<엇Njti-/&[O`e-|A1:ƁIJ푄6Iٵ -%n^ůj\PTn'Z@Rs#05ɟnơB\`;jNnwk|(yS)Hŀ/|~=Wޖm@ڴEY".9V{[ֻ3_55'`,! ~O2{ű~ yp *98"#sK.Gt3fDŽFSsc!X†?t@%2ty$F"K?NH(ҩ`ۦ)hZ?So$a [ngRʅ|O$iPUFŀ`].a3[5pP"``T@  sBdsBd}u2ͮo[Uͬ~``-36cGg$pFo沕Zkc6fA[#Mlw^o0V^ @ ) XlƽS5) `O9x{é}Jc kJ:fX |=~[uA5.`!b&;p[ݙ0,`=o~z@kN 7$ޖ%.,KAmoےz2괠C˺ 3 bek-:HXn2+nsD!ֱhzS(+f^3xMp'Hg%R>MvȎV䢇=.Y R_[9%k&yDl2MGl8h;\63XaEu ˢ ( *aAB BPО5m+C?{Nx~~uR? ĕ!v-Τ o$w\ZH4%nݙ6aS^GG۲=X0(@p-eFJVSæqy[D y[I) 0ÅO~?fDQ7c`uM!~_PiP@Rƿ "^,C:lBK]~>b;E:,&s.L24qgsSǓmu5gb@RB0\xt3^Opt(]zf]/pdQ~ PU屩ծB82AG9x5vm'KBdyKoyVQd]-lpf͂I%ϋ`׳Ibmo[uS$䷚n+`(N`@QF?/; ]Qlօ-hXa`"SE! rB`aO8~ Oa9OE Wj"<:f\N$vDW8R& ЊRʝ۲=,wdfM!AwfA {8.C9p `Ud8U7}"i g8=h1; \~^ OK (m>'{^wzsR'/ o.*_\鞾[k%<-<-Ylt3EoY+ݹdg,Y!{? ۮvXv"PLCe`!4(lty섣S_ J Ǖ$4i + V ?u, $ lw-P_pphۛ;^LXSAk߽0e >!XDnqDعV؅Vn]،ohoc6ª (}4vom1GL^cn9C|sŇlla )Mh"LJ(퐵i8PLp]ݻ[X66ysZB|NbBܟ3kY;.Digt00Kh`vFsK WmWd@^ug,w?\HRn@ wBKKc};_r^A!V ,X:4J;G.ߥQω.oN{+ LS;]3t My(yCd(Ƞ_S"2ʖ;ϻWZ6Tr]W)Ⱥkv,I 5g,g#va %Ci@pOqDWmT0%n77g|٬Z9嬕 PZFsXi_~4qqg˾~d~U\\*_ XM-|> j1"oDI&ZX0HC6+1us%D@iE|jXo5YQ媖ůc3z?έ@ $㯊Vaφ]Lh%I'"k~qK4sa ~D mQi@{y66r-3}9 ąH(c0!`&'[tꨦYMYQS5 s8jJ;j;yݘu⃮K |?IaNX`k-޵H)h -|AX L/~m= b fwM{&1+r (8ޫɮmX@S|P/޷ XJATs.ͫ~>H6 W͹K 74!9 u S){Y7f#+/+ *.h>4 , }53>c$lZbݏ~5U#CSüݗ~;5Tܧ73nX6!.TϺwvK[\\s^cw8t* B$YeҠ N?hd o",ZP.Rז PW?t# XW[ؐ!)<;wPL1qܵEyrlFhHp׿àJCa"w5g-H;OF Qvz]pDۣgufp[ !bH[o}oӀWtpB(? mctpZe.S*ym `Ӏ Ow.>SØEް,=B3/tݼf5F˜=/`|St)1 -3Hu}KR)Bġמ5Krur9?/w(1e(M#M#WL}A0ѐx{Bg; g?zݟv9Bh Gɢ* Yףo=}y/fwלru+A)hńt*k 0\z",/Z-Orn=5EfhgV]>70Q ,0%tuٯ|y,[lO5]_>|t ] szٹbX< ][0iP0P?}I(~peM*+v/2!zFwaҝY/1K6}.8nYKZ)yqW` (HzFמo 4cgC D׈҂fO]9/ԐpO'/ul҂zb%ӂo$2,M424˜St%S+il0⃲ם5k@w9 t@`˯1Ӳ];_NO: QsezBl/ڭ:VrgNi3!E8sA8JxrXjZimb}J䏸?{n§{KLA\0#6?ss; fjq9P;N2R4r*@w,/NJO sW5hmypڣ ﹌bPyEvfE]g,!"tw|dJiq?b)2Anq&>v]Fk&TSw~y@9Xd3#ޱ̫I`:~FX(\yvfR6 aG-_+#RO@3 2 ~mú"L9 ,:^72Y1w;( P6fuVu:3˛>'`4$ RPRl䬃nwsiy?e8 X0؈ZqE'Sjva,@l(8 QKXvc2*`2fn{r6mq PZV@ٙO[v$D &.3+|z .ȝ/il' ҅_ȥ!4D\W4~zfgjwS.5YLo6wʰa#$-rN-6##K`Rͽ/P<2,ȿ76đAp74|dƝR,LIsg|,hO XORSc)aM[ z'I;sU0Ę3h( eFF +Tca{=MʈJ 8GB~6,v"V}6bUʂH8x?` BCH&JNIk]9\vwNf p Qʗw,<8` W4~fđ{!'덎k\:eSE@*j!``#]Q:_5R2/eg6 *{xZvh. ge|GyO}m>G ؒʿ7G4JDRb*zA@t/ufdTsకՐkbW=>/ٰcz>.eXn>@'hEK+J[ӿ|=cơ94M:mq` Mi];DsnD;=nWZ: &x*qXeM_8^yuG?ht\+_q ];'N`RVl_\K^;Zg7" f+L'5XԜ[:G% 5)pk]r[MwoLDޟ<~E8ReEc%8߾g=8/-|^fέg=ka˚t{$nZ; R_Im2,wj:GaB  v6 )OVU:#^Wr8x @>p㑣YDF.ѭ_odx±LLHOӈn2[ٱ o \Ofߓ{jȺE-Röԛ4|Ǵ(ZI@q/ QXC=6Lkxt Œ N}A}H(Q.սcZwR-܇{ܦZY7ny]#KM5whl~nO1 B aO)rWp(h1]dSǕ/42rhHԬS=Ѵ_p@i:p9S PZaAisK!kb2°D̯vup pwC؀",ܟxqә]מ8 j[í%} ޺56VoCzz ϺNkIwΝ!jFc7ʰyTKGpvUy%1rd &=&(Li.w.n_Dn*Kk4kr +?Wlb1%MsxN@ T*䬄=,hTn~8j&<ʺh_ ݢPWd (`"oE {;0 4DX'7^8bF!Rm[kͰ񓇀ʁƈa\uІqEROvgmxmb;oxK X#ą& V"ơ.~Pj޼N?j}+@В?[m'valOYR8Ar I Atݩ}+ځwyUS0Kӟ ,yZډeH ڿvԨj~|S΀i&tzCrVFv Ÿ7q}|M$nG^k(7@z|gw|{88 ~e_oQƌdʌ5nqs݉NhvwoL\{ox`Eؚ#& %Fo+۵ :_0w1sJi^ƶ>e,X-\Vln[ۓ'-~V8B68 VqMC)R c;|ri8 34 J:W6|`8u~@VP-e3/ mUk ,l>:2y)~̎q5Å\>8/naG6ǂ-~)?栮Ml o0Yv&}= F ;0"%hvM}Njµ1E oÒ?Z#L$[Ԏ: ~xrv2 ߎy(Rg5hy7|Mh:Yw!n>BK`]j^CٳPV6KO%:juZ_]*70yxM3ȕO^w\PkPt؛/]N8С!b\?;UuGn +~g?П?;M++t Q*N_>Bѿ/    {+}!#XwE _tc%d8v-nwA(Ra[؝#uʢ7ԳA!>79_Ğ]wal/M0g81j+*[kmN=ނ524xUf;]x=_D}9EJTyW2dϮ<5~9L=(ЃbkAe(Uz>ۼ@Q8YGoN;aws=ƥ.@<"@`FmR◿_} } Yu);Mu9B(o吧S\Ew`>T3x QU:EHS0yĜ#aٖ1 !C m*[7]$PȖ 3z :!DJ}ʣGekdLikº+)㄂GFVBikD=5 S$C-ʡ! kh7w -w@΄֛z-7z"YTDC WV0O COg39 ILﲎKMK@F%L̵MV Ӄo6}{{a|vq$~(C u Pܽ] nR!6h+z;Mgmcˀ1$#9E jixdYp(;+Xmd{ ?تSz=ݦō;α Ld f(VD x2Ȝ̏ J$/rWv$ e dOu+=d-.1 [gï6v5ޡ1lrɿ0 TMh:lf~_PN & XܔMW=Wv!9,8[J!7^2h˱g6P;?9|d OPokcl`ɑamK`Pcq?GHM@+.N P_ؕ:'گX1.jjHp&d𐌍 /tŵ=[h@BiU(¼ol*0\A- wtVG=ENv4[˛>aVrU"6uO +mAuQbzj71r|sίsӧ*NBUw,O Q~bK{dH;@=哝9E-udIܿ &4܃<[\Ʋ$Kdb_ΙSpHǃS7t[ۊ7lQ*A~2ND_vnV1%g_Zt~cLq\E}6ZeIM>|7:z護X< $bF)U#P7Nm`'  X|7ܜ:-ܚ  XB廝-5G<@]2}J4v< H?νpj6 W[ ym>^4f6 iRn(`haܬ)o\:K-Q] KS]lU{ KzAYf+minTRUVw9;{ZJy&7lf&x? T&šZ$Is W5{{ܹWk\;(^A{b25 >:dp d֯)mᶉ\DƱA|\GAx_dЮ hhjU}A?3P{aA"]ѷG4R/&8 ,`o%c);krg ]]"ۻIOa0T?UF'No)pA2?IulwkPOQ*fFGϊƔLۜtO2ٔ>C(rZy'hC#(~ JhL@鈼y P# >g 615wԲ7nr"a [ʴJv ΁TqGzܠO. vX\ e`|d$fB2wų_Kx;|;mE~4^`%;v t pIwy_. ͠%7xK\=z\$x ?*UTO&еѡiz͞n}Ͻtk,$ Cj\qeI m.;֣y^on<#UH PPEsb:5QBŶ1}z_]|r|F5*ԧ2W0EzdS2X=ժgbz 2CM_Fg)ܪ0|Z3 `〬s5k 0-"SNik41+Tx0ɞ\o|ձ묇x, +ؘ5CDm:0R)PF ނӞstB^I3D!:VB瑾MOnQgFB_2,Y'M,߸+Onntw%Y{8vBaakJMu0:3u~QqVgrVe[ 㹭*phIEEr3t6ڰz=tg CLҦ섻ř2į}>p;jѨuF~O1~3+%iKNnN-yآԖC>:kLWn@{AUj0E($~3_Ýs ?Ԭ{'4;BҐb+nlrtOM#TC*̼:^1!k\%O^jI.BUĎ/_n )& IuM?h01ʎ)o2e7Z j?qn< U3S{щ*8{Og\ENZO]@4@[,+RޞGS9ơllr~e8^9_Y  wx.hSk SQM̙/h+1vŽm7%6ſ]8.mdb[Rumˏ/|`>Ums^^@&ж %Ա=T]qrsu$X6r$ -yYw %K A2䩣O]6&#kzykVәt80ة `Bu9 zP6al+yibm{uM 6y$ 6*/5%;4 JAS o7=lQf($,(Tdڲ&H{8HR.Â՟u@C80a#%iv";n¿cM<0;ae:  s^2Q^%V>"0 Z M ެ_AX25KA0Pbs\/W/ɺ*S vT)|$f [ l55UVI>pҜ`}aސ D a//k=7oGb*ħlάb'^)^xJ{~icO5qL@B3QldQ ]eUPV ^U! HŏHErvz48,?-4cƂJp\ֵ\Xu7A5@]Ƹ8A+ƿS+eA b-po%0Ƈv`fT4A2 Hvw)3ov?G?gt\.Z0H =[ (Zg7,_S)@uCI) YOn.^% gjv~:x_Sηذ X&@2lX YЗ{図oہ\@2a{M 5+{7[|6h8dX"$Шf#qAVҬ$i 2D)G ZN#P;JdHES#p3X|.h2y/e'Zp J8q;,ܜS =@/)ag"yC ̧ˁChr|j k_G/f!QY/ȈG70MQtQbvbE5cӂ`;!9>iYH5;c.' 7dg+ED,,((Xԧ;z|"WVx]>;lvɓBr\3o"yk"rUr@RC\U/lc7ΊpYn;CO3FfCԳ  `>8Qְv+H0`~ Ycƺы+hÁmu[oci3+hhbL68Ԛ%=,R./ ` i;AW/eWeP{_3x#1,&/Hp6ݢ3![ޟ?+)UBrSԍL&\<;JAq¾@[*8pvE+W>%b ?:KeLi=6{׏|? "9 q>Fc۟9]7Ro8J, O5w6(Uʭ7&4 =&A `a`|0 T^(z?Ro:/cGJqٛV;-ibHKN <Îd'z"TmXN:-0ƺe/Zn gY_=RBRd:qo[V<-҉!V^O}n`ORvBu \ _qݙg.kB,KA0BCs}u^f_$(dp@܌s`FElSp+& bȹ^{d{ [&\w ٦WI+| *: %E7V9v4碋. c1#rxism}Jh΂u(/>=Oܢmc75pC^;36us2bVB P󌐬7Ei8ۜo>tT+ K㨢@8+ؔDb&+Ӊ?w[JO#R!`8R4TD${"p?nXmUun|pswɺĄq%zy>!Zy_F22EdyTpqR7$Y37bFE+"8y# GnaZ*Ps Ġj?:{cg ]@@N=0/]݄C숁w^H6 Xи{hµ3.k3!m}M&BZa[~1} ' iF8gdEvr,GH]q'tT^ XZw^^9v5ڛW+凿Vv ,} JI[3-m^ ʃ֋Z3[X Z3IʞR m&)]zie8ź]DPiA U%W{97xr7Jf&udXU+ =x?Gelo_4A>cۛGm!y ~09@Qp;#u`;:NL)lLX `IចT"8DҩIi=zRka_o'ʬ8=jdzyY[:~4/eZ3VlI}'}αWr>KVazs$Fn|%ژ*ZAPkZAF:yiyIu{Y7%pT;z\!{tzc1ʂ0BmԸe 9tLeMiKdHc=LM ]`jmhmF9Qeأ3*AuƵڲla) JC8Y?<ByJ~p/ڄѣoOX M3CY}FlTZdUǸffkG*ԫNdl_iLтawm*7|OqDc`9_FŅ\EϣT%WTVrѮo=g)i Y}_`>mf<2B AڹmǯduNUe:f{:PT^|kP:.hΠ/O<8ʂB749Н9?4x?Wo VE.MO y T@w%`ҾP._vx\d^Ľ-/R_=3Ow ]3N!_ro}P{f`:gS*̂ fgmNᛉ\Gcˠ&m\tjY} !,)9νS:jX;hm OB.vZ! yk.64儎+k)+k!./<`A"~qP"VN=3PqLw^YRIJ8 ck\byµ-l3+r-ruNOP` 00߽5 d"m?Ǹ߳oN(` kݼ`A K:sgNJJ7-" '&jHC W|v#$1 ?zDžn}M iHO0hSaTSvw644v`3IҐ6oU Z=x@{8IaH&)?@Ac'#$饲#D#7.|-k0n;5q)Y㲾?~%pF:"~1jG4rZRK#P>``ZAƴi=s!"!EIߒy%98Ǿy _Am*zߖޖ6  e (`QkCxF(mOA,Lv%нMP\chЃ+?[}0  r>ԐgZх.z euTKcUVJl7l%J=@0O/?υ QnFDoVu y]+]2-@,CD̥pbozeMٱ ZBP b/guW #a|@Tv?4SL ׍xڌԋmݸ/7q*X1߷xBA˹ee@v)d#ƙ3.e>@]ް"h Y|jуBA@YnI7'zh`E%C12cTB} rh3h.}OCXG2` ~P~=ÚTߞZQ3K}4pUfd0xvŜW ] y} xC?GsKm>)]e7ƿiilzjlZuL'WӊQlK~t3|[DX9\-f!]R&T?w1`䴪A0>#3",Wj~0oi@ꕛ2F;k6Nf=#FGˏ1aS{-Mqx>bWJtBG@_ZC""y8cr%pgN'u &(* JFƻUn~<Y/CĿww޴U ,aKogk3[C 8@`/ ;cg.L X}{饣m$ ) ج78;]\$QtǮz &rJhp-?)55޴p ghz[\ (`b-\[#s7=ΡKI(:H|cEuaSU[ثQ*9`rCBT RrGgM ;> #zz[譱޴x|o 3H=iwE a9kj.z=K+hP(U#<36+4H2XfwҳC7-vN6Z8LTMF4ťWz,dZmV41V/j*W ?bXD/ Ik?;y 1l@Tz4!@ՍyWf[ý"T79S~I$DQ L#M_H~w&~íӟ ߫tی2ɲ`n@"A5\kJn}e׶BB+1pȁA F$!C ZL>jj@y*H9!๗%|Fd3}ˊ¿g)b1g GI\zaH· r37сnY>Yljr > 6S+> 熃lc@+ `#-mR.VK `:B--1ߛZQCYv{jg Wa @zХ*+hO!)KAI1OCpX9Jg5U,c1bT,]Ub!ZBZ ;}{?4&L*qfr /iG#nOf41Sb2`L+.+.܋Ըb2k~Q` +XR 2#Wu$\[t@^W\=M@LW Y 4wX߼%N"Ņ礣]IB'3$?A1T􇢍 6ߵr|@hcK Qag/&uv^Ee䰯?aם|v l B#m4Ha!*mDp[V+-l?uZX>z u@̔bNm×qd/MzpqNЮ́ܽݿɍ 7S7nYbr}V _pc_پ` ^o\ܳ,GCʱgǗ_C tږYثw{`E)fnNYئDW F ?V+l.WM ザB[;L*z$S;02&Clc`w-:^I@ bxݻ岝k +,y?=@v,g `\@|~ ;`a'0IsOgY0^1Duy?F[1 Iϩr.6u+1B`*/C)8s:'D{=R&N p,ȓ]zj>НfJFo3x0)! vJ= 9 /2! 47p01@ &wK(?h@N-C# M]tǓ YXZ!ղ B /9~*F7W[ 59rt5|PfTӣ X-|"t ,`N[_ DM|eHAla#P#TQ3Cw[J]!ME|a_uU=WٖՐrCW~8_M9@][V@la-y4<*MDZ>l3P{$_⢟2 ;Y~񒿥NbV{ʲ[so;R0* o,JmB0xS9vhEYY[j^yrӥ e@Z\:g?;; 8™sגs<Խ5mbjdAJ =X85V Y, \.8n#վvVV'bTXbhE%W/8TTZ ;"Uc.m-_d|E(G.cg Q oqHJJ1A &V 4&-3*\SO>ko0# 1KpA7o\90HS-.ɱ 7}TTvUs7 \^JA@u͟45 I\Bh rȚϭ}]5GIe- 3Nu#K=?Zj660C@*!/Ȟ!&TԲ :K9y黷6*h|Zm<ȕ 7DxNW.JJ;I@caUus'z=ߟsaʱ3ttQOiꆥ+h!}GN?pVԝ׻Yo%6 \ &.$0-v-Vp"vX@YE֙NP̓ƥ7C4〜A$|r=zIbgxf4&Y !r`{yn1Ț)QqA+ Kf%xh1'-tV~pC.bX4Jsy-U},r[JxM_(M>B(BYCzuf[-#ߺuIO}O0E!|'tV(x=?uG6RgM6|NWDQm"P76IeC`,6޶]p4b`y~Mo#ߺ Ds.Mr˽?%G@U5daz?/E:d{rmhx@[kNn9$kٿԠQ :qg邏V5 ˏʥyae1Tk_uMeg˳: v\~5K rO_HN̅ A$j/$'֧i9~W{ꕉ4E }Krtal'Ln='{PłME߻~ÐsD1R& L!r}82y( Ú ux! Q4}LnV o[XirϹ,sfp-+0^yVRѡ(Q1q3@2t O>J58fXNl̨Wݙ{,I ٳb;ܵ}tCPOR*PX6uG6VyA. %ˑٛ ܗ_=v1sq!|?kn('P/d @ YQREA) (VġWgPgό Q\T|㴱o%{+`8J_|{ rV<P{f\j9YJV͆?2i,; |&b9>0kxlLxZ tم ,%l)OY:Un3õYNG't_nڊ1$rRC}-.~i޲鴱<qh4A(F$bmm-{VPwa)g&v䵄!49=MwKG}G}<Sk^7a-̓Y H^Sn.݊JVĠJ:ER-! }Pů愄?YgM$ kNEV2]{bܘ 4`),;l]I#kGtaޙ>M$>}ρ$ fpp*f0d` K [JǤ׻O$"y.c1hwg;lXSǮ;v-ϱ4hсMKRsQH8nB"s 7{n脴Tvl}|zYdU㝧o>tT6zˊw?pn7D;.8#8\!2G4oR٥eb/+|B7DD$+ ǩ3['M^g.u:N 'z3-+>*l>**&'PA֪OZm y6_5_wxؖ3RwtvWm{шl;{S9"WE$~ojp*mPA7brVS>f񯞛\(FNN Êuaqu7͚"pĒ|`b/c#5IEb710Zbgz>pܮ GmŅ4JsBb8ǟw|ǭ(޵l@v~5 |_zB!6 ;UT>2!㳩>}{9=GxG}Dw_δR/ :fK Z[&dJ'eW[^MJ]}ɓ?o cϊWs"||2[=g?wggE5A B$nZEG̸0l!ҷ1>n>ׯZ`Z=iz2 C2.i> 6T0G.~5'vs=[HZ8η{^ҬiMdv GP7cԿ&!ܨ?Y5菱n*SBAjh6-RQvuA=zVyIIp6mIGEMlo[]^럊/>s/-{S E^Q57yBL 4@4gЦan3QAHwC ( u*udۨjL#퀰 v {Kl=jЧv {>U%zd(x37_c"[JcVp*P2{Q??~G[tpVvfڂM` ߷e۟^uHvG[x]aDǛ͆_߱~:I~+]aOR,u#;ڳ3 XqjZkjCԿ  puPS23֌/Pi`y Xi9ϧU}[PvB}/iX9(Az=8#p9Կ)cWm\o;^Ǹd!sgWр ;^#,R6yu_z# ӾH$l]YU?ZPpá|-7H#dOUr QsipM5Ũc5q@UDz=ɛtd=V6& ;2wևc3E-o8+j q9$wa9Ysy'}O}P&[<ܪ+mG8k {MENc?pk:Zu@7GO\hTyp ~ c!e!a珯^tj^il4)2 ShLI/0ʝ}\~[xD>%r 2v5ON,a_>1o:irAHRO3]~JlNW)`ѫ_(y7@"%8b Ǐ#t 1([#]?{qnLp S5'*:1@%z/qĒGLBHJ<66#Udp:kyݻ.} ?0{B1y@n^$y#*"r݄Ft;/`4%J•Wo1ؔý}$xoC=`r @2TSODE&Gz!jsqKh2* hD珎 (v v`Dc2m8r;:8nd/ _(bjv͟;bL6 $`>:m&×?G:-&iY)C!20<*z,, G[ivj 8_Ca h3(k;Sz ٘ &ld_oM{c XD0%"Wt M LjlO}eoqhSym5iif;t]!%![em Vjv!z 7 áB 13h2°p=mցS>E! .1KC;! f`]Coz]@w7UzmE\Ur|}p+cw>$5˽\{`azp`Ӏm ]*j*p$]Hتɕ) iD8\?+94vjnc#}!"PF/t5SoۋϹϘ\㿸W͎ $krd;qܢw mCxkM%t ̰3mWuu)3=CڬH }n;t!n=xM>.Hz4\َa>2fVOHdI7OOdІk Gs"Z8 Lµ>'m~ǀH-ɵɑҎi+h~WoLvej G]OJ,~CU0.<[Y)lL5)AcYZCt4N4]1%֙W9L G5ܝ|m1X;n@3b'IޕANd yd>{7p/KiIW.vH+l3r ykgՃ p 8DDs6>敐pOΘVR+2\ }bp~fcg:Qlπ'* ̚gS Qha[*,S:znYw$}auTSεyaI0PШ{GdwN},~T] Z1LQ@_ןcWd\6zI;6, $͉e>YT'6P!ٷ\yzWG0XϜ1?߶cpjy@;|!Μ? sc=nO֦` X6C]лGq.g&aaҩᣉs/ĵW|*ZCAC5jbe4SM >O3\qeaOnqZ"Vzq?`T69(LZ"87UɗJW_qz.¼˦ÊؙW0ӧu 8 Hɮs2gS t7譝efFNk#rLՃhK鴎mƓ+<剮"c, r7S7Gisy}ddZ0q@4.-jKG+ 8m؁g7mTQ7Hysك ٧,]H;_սy>V߬31*UuAr`33G{_x}Ŗ?b졑.46z4UUz># +*_+Tn(B Wld;%R ɔ Ne59٢n4}֐+7 g ~ mv܀+Ռnф;C8^Z{*?D蚊?8dELܺ4h*uBw.8UJڌVwp}mԬ}x\6^.W/=a(5/T/*`: 5c%.CȨ_ʳ%>g@lڌֵ6ކ4Bw 906t-Ai;VLى&Wd HXW>T[h614;íޗ5@kᝂpqD?& @Vc fNu4bZk$¢]wWІn0A^Yƙ5U>)g`;_@!m}8T5mF HXckW@‚[Aq Y=f`.EÁKXyyr _nuފA2\XF1LUSf{2[*L0G@l:Oh+,Ǽm?r۶8H *5RpL l"D!0!߼n *_t"wpS?V%W>6ncaOgp g(v\?w~|xe` Dia% oύL)DcIQb'\mh co1JvؔA2DLUr_ل' 6dw"|x߯287$4A77H!,IL0̐,J?3fP%@\k M,c{?`U x}k.^YBG[y +6c/[ 2mH40DWӃU@Qnd8sI3_maO%jF* @,D]>o=e[5ep}ĎLXXkzc@  l??pPX( <:cC Q.buUr_hSZB3?iJktp !ⅺ™MoǸ+N8앵})? [eKL&dg:N#Pe{ɝ@A-!Py6U7 dr] d˜pv\'<[ -߁ȐG|.bPŌ%KF1zWP7 1Js %U 9EB-h-H]}~3}8^ ;1*g1VYXWuLdȩ^bŰ&ɦ"~WYߌA0Rc,lEZ-ߌAvf+9R]Cm`f~IO<+xE l݀v61 7ӈ{>G8b_ `\Qk g3ajQQu]=՘*- iV9ns\@P"YӔV, zާijq;9WfǾS]sΝGϑhLAgX>`GmfQrg^g,S*'iJZ e`׀!h-ܲ{PZ#rnJ ;ΖZ(Κw+F_=wz%$q~h oɿﻏ+v% >0"xs3U:. Vs{nD)OtV@%l12m| !WHJyrx:hH;150N~[E;TZ=߅^D)[ @ݤ[n߮$Yژͽ:e ̧MKtNYٍ(U\&V!,OiV*Vd~- [4Zg )H"Pl`vI]c/ǹ1-=0RBޖzڶnK`L۠ <\ `pSHpQ/>n4P+~žNYچ(U+$h SzG~QSb#Fkl2kFbw/1F&+PS'tSU8$ C)y~6DCثcSQQ\J~,[lQJRuɘGGTk)7Qva-ݸ,zèIjX;]RP]#Jd`$P#!!}`υDQ7&f=YFj .̹\j$fpb7E*[D V1@q~>`1҈&$2AȰ 63k j Z .(Hv.{Uv08+ ~5ި T, EJ(Z"'SSn1˵ьhaZ$]eUF3RJp/O`AA3;u#ܠ Ȱ`"d:$`|ZǪ >aZ`1x̅;^TۘJd?j00lS &H#! A7;G\0`*PZ6ayxPe`|Wh0@%d"U8!Po |WɟoEnYQ2m 3xr:-vf%tVNAY$N[ؙ^; zBU>Ei]0@vɳ A` *%Lr W6hBR3piE&d%VFm3F g);X~  3 ʭ,+@(`*v Mj:-vGN!@uFrZ Vta;_1`/pPS ֔TgmB&Iݺ!7m~DLScҨptr&7HY?'P: `o`҈ƣ އ@l;GdRץ&G)8|CubR&f3816tT[#Mik4u|{+&hb)*CLYOx[c`"FIx)OZ_ [nU>&,lJ1! 1ۋV;ÓUA邞_@Km2ZX pI%oYk}j"%XUP:je^&C09PEX@ GW7*h/Z=[RԋdpK)l( ̆! |U8nmr@ ̠QE{ $V*>Lt|2ȘNbhŢn덢PY#!I "2d ~8ZevQzH@3V**bē3I"`一L9i A@1Xly$X Cӫǚ$Ÿg7KX(AUTQEUTQ^'Cʤf>*6ؤWVЄRwKD޵ IC\UODpKG!wvI(j**b ;:AЀ)bآ;$,xT' BjΎM_l-dʁ6 @F)u,'Аg:"**۱ VHDZIXP2KsD 6k-,E1! @Dϒ`E+'aMc ` ܏*vX$3 <Y%fUTG[ VwI;;[44vآIXþtkXaaW2LUU3'Vxƭgn.l-o Lg HJ(]RCGBGrecbNr?1YeG&,0x{{#M#:s%w|wAIWU̶e sێ)(.+i– $fVJĪxaA-rfp# v`kd!,` Ѓa(x뎅-],*[1Ev4M_O!)6 M 3$\P!!`)F Df>T !cUED=w]*1e&siƒ}?݅,b Wv@epl:WIҦ)i 5[Žxb>15۵2V%cUKY^2v0ffhd:r^!QLXG ThSdD0Rx>UT=v vs\ GnLhSҒ| ; J($WSrdZ&W@O,,س32a2$(a %F 4%$ % 0VRԑ(ͫDR6pP3wm8g/eAT,y+'enDEDamZuY=[?k]ЮĬ@QNm#fzkYITӖbYNQu9e{,$^tr쁉Ay3* l~nfD\i1bL4l#q[.{Aߊ5I;.vBČXT1 Iټe،,-]FT:TA!ghY )ѝ,;xškqaRP@^9 B ^U,`f Ni E" SM)= @@j,XƐ @F-Әo0"r~($JCo&I,}b.k K57fuR%O&çN v&M4gNL.b 8[ԦE@ ,x"DB4o=FT&6HSTA2P 䨓 ISn M9zS.[hk ݒ %,$͖V.Ԥ{!-Q$sjkEfYKJ`zю͏<ߎ#+ g6j<u!nZHz(U\SfZ鶓RhHǸbK Ixm^tcTk eZtk'b! cWTSQ=,K3>QE +4x+X  A9)Zvl#_<41i[ՄHpL?h1x3H2 -Y=D# }Q6 L?6#bHw _jZ۸'c/r2g1vh>wHvq:.p5(E׌|] Cf4gf $iR-DL DQ-oϻ7=?t5X\HAmv$[ʂ_9qw4|*~֚7/L]Ru=sUѦ屵q][ydLI p͂ ΰ| ғ}zi@BRn(K4q?2D45htWvFA,`rb/GJMjL(`%DÒ^F,kfHxS rgڍV#j>=E.JIP0/؉8e:`c@ijrpΔ}|t cLps תݗԒRvd@di6:Dl#IQb)HI M^łkԾ_'W47 D W|V{fG1Q(&J>2gJ֑/HysIP]ɭ..4{Ek6(gdD8oQ:N>7a3{G) |XDdi[jb{(#$'YҤx=/FW#ix6"@zeiX"q] LFj*UxcT#__"^EJ<ej:y `4MAvT1/t>C!Z'52,Q}Q@q-tD ;e-۽Pj!Q9z$E:(Udsh 7` qT[qjQj6_P:@eЀl*T"g#zv@ 5PB)\tA ńToIDAT)@iz6q^?&z>5Y(&C>I,2=C,XDN+bbPDO?y݇{{QZ DKon< {|x;+eN<?uCgʷ>S7< ,?# bYia\FƸ:2>ŘJ7?}L1M27+~!%H` XT)4tcHQSS-^1T5/xKA}*PHB^Mg\/ڰ0կ z19)j#U`&|ncr ?yeq$`(ys4d`N% 3XD ]|R}NchGRX" ƿz.JG9VpHb)a؃)I"KuS".l;Kuj\=-jd+_\4>>7 bx} `Ω'xⳣ[Y\x1" D KOݡdx k(==(8C=05(bF|*X'sYY^Iuc41 @K+}rm艌(+$^ݼ^oRcolm`9 ,}%ƕGg:&'D73VGtU919B7C1"L:wi_Tt@DjO*hn!R#^B@ɤ2@(A(4ITG5*0~ʙ˓ * rWp$B@uԉO荶C6|/#yg 5wPOH(Ug)}5K%°7Iv|%:5[sݫR1Z@ Ȟ5s{[a4 I6^= rM+oʝS.dlEI{`_^+FqudݕUN,T"KT%)SF, e9qgkH?ؕuCܐ \/ՊCW< KmЪv1(5+1XH۱0h6}UY$y'(l%+X?\#_+1;MRT^ۻ&ǶUAVlETW%*Dn*$L)H8u6.+d,;5Y!ȓJNJ>H!NK\/>q~F+{z2 AѦ(pqSr\6l #JbݡOS'Y2~4+Y!RMXޞcwx F侮쩵3_Y{[OP d7}+ǯKvᣗyj0իg*uJGɀ따]IօZE925?7pG[2b]˓ ]Eˣ>0eA~i<-ڱ5<5K)׆A{@hn&|$<鎖M@Hգ^fs4'JAшjW8V(jʔWтʆHT:BE8&gH8 JUya=WѰ Zt9ZT^ޣ830[^i1cj"[XMZ5 ѓ=KGyˉ#VrUӓޓj|ڇ dRAڎ3n+N3sc͎Q\zcs;b9{/ݶwUJ%/\@b$UUX̔FK>SsHn7w"`P`۰ u320GX AudzEJR>:Q_j=XPi:1UTZCJ pT70kSdtAmгdZ枙3#X-yGXݱ4H 2rt# E!x|UJ6Uj+:[t]Ē;-m~~"e0 ,F>615C' [$ih)KikrNUB[!LF]חw d[GE= ,WqX=:_*3+3m f֪]}w<$.<>&Q尫mvrtֵ>#;;j`nJ2M~< uJ \q7l"xѨP"ιbԓdg,6/sלy߸Sm-W}n^)a}6jZ3n`*JAurl+~kyz|*vh/`Țʸ>{'ϸrŒ׏m a>^97!zK2%{tO(!HR=F;]XKWw!`XJh>I? yCS :|B#zu;vB_.dh($úm^v5lXPM&ߝԏ%ƹf@}zˢ7@Y Eӕ]}_}%w(V>8+/oWcE%b$\SHfwǾO[ˠ:8WeLÕWwϸ'}oo'YuĬAolkS;'\ۨ|b깆zs^Aʸ) SU S.ǥX*F"frnK;e_al, لok#2YvnZ޹]z5"cȿ2x/7:\KDIv u3UNfF,z3b*,1K0ԋD ! ;@w,AŢ.,[mc %]]%|&w]1isi˂2GQy(jrKvr)tf?4n^E0E [=swf"#oY=hr_Fx*|XNd?&.R1qoꍛrj-=ovz큩3*wM>\Uv$/)wTIe $l0BgWɱm_waCgqCb/2Nx0[-M &3$=;d$rŬpJ)YzTH_cNnh|1lJy0.:ɽIeT ?v` CƱ ^?{*FÝF*'cAB{,ZLbD ,Zlρ:^#`Dί}Ƭjh=eKZ)oX0) t1s'_'\7wvgwGu/PȼokIf]3k uYϹ~j߆1c۪MVw׎yԾG1i(UL}+ǯ%;~$wڍxάLBF6$ɥ? pn\؉L˱} }}j8gУ]uGnh"D8aon/75/;jrx̔ 7lhfI{mB]WC|{G)]Qzt;FMxp(Hw`dfb`(0?K0j(J[YcWr|ӗD0B AJB%~˿xt> "VTdu6)uJH{ \4ԁ IE~OF;̰7i!Va_[.\s4+a fACX"j j6z.}4{7튝887Kn$eFe*憽W[.@,fKPG{o}O`@0){Cϭvr(FDl/<bw/ I4`ou{FI&WQLԾ⣉n|0y᮳*wMbmL^8hGܗg=[b<`26&X6̽R ;]%~|N^hۿe, ၱ`@j/ j)KkvҙzU*,YFh͕FP@)y[Zr ͆CufsJߛ-1`ܖӿ&|';]Td ڤ\HJV%wֆ>4?ՠ({jWC=H`J-?sգ7n˞K}HJTxśKZBAS7GuBp&w#y*Fҋ6ec%D!CZ,$}l+֯ RSQxCTHJ-=>\D8 ߔث!5w)dHxlɊL/+dZ- Q,15ٿyI{c񫧭ឨ@JLս2$O';֭>MȂ]8~슻}!X4qPd}}[;}K3npvʙ Ekr p{N޻k@cX۽rڰ;o[R|C'~x(-?yʨ+Wdr_5vmtﰜ?l_Ʈԣǡ%DoqRHڟ;XFEb{2ʚ''Y ;!k>A͒HA,Sw΋K:iGdW}/GRc9ї|?_z?^c]ˎno;ŞQ 24Pxcuiu, +.ǨEa@Ltri]x0f0S2# qOSMovҖ gG!\#AVl-| ao_757ny25Jԧ|FG\M@y~Kv"P~ycqjv)E%_5rӅ7.aj@$BLb q͔C31%΃kc c*Ëfc0ɰ8";97-ɥݧ !)vvlW?t{[|ûeTiG XpXCODMwW#CMs ;&q`+a+_=ܹT8Vv旷R|vK#! =IJlv=Vc^qLɄ1t}F\؉ڕ==o}W*Yi]DU–'ޑg$mXOPT00vEi7Qص9/nq3ޗ6wN @z0]/ ˀVo^])5[J~ux^Z'H_W 2Ȃ#8c9*_H!)$>Ӵ*8@Sg{ RI/@82_;;.w߸y[-~ѣռL]5ްwD%ixn^ -:2m lSNu \"rݟs*wOH[^ySoZ>.7ir'v .A8V{}[϶X!C[?<>/JDӺ!MO+x ( %pݻ9g}v{~ZiBx{έ哟@RCpa%/~k)qП]Ҟf{N>kLEݜc@d`0YZx!-.yԫgRǫĥ߃`MAf,K< x.S3q;8{Ewō2VF*^݊`9nn⢠iO}d!-l` CUĭc=dA |#a9f\˅RBϩoVt4KAZKׇZ?uv^JGߙz/IbXؾRs8V")YF{sj_~N @A c]X3 @ uYU =c`'.'fq6׃7%(<'DJ;_3zToI_~d]˫EvS֔E&G3,H%Rf>.wwMk/(:f CbEGD&>[BE=W cꌅKeW;֭Wܻ@pX%~dӠ76u=+S!}~~XoL^Hny㶬- ذM6rPK?^_+JAUPmA]۱ӳ?JyӉsD&mrS޾bԤټGbĆ֐$@wҿv3S(َ=6F}5]:/=Y ]bJ>xmUo8c_=`Lߙ.yo4]`]6=)uP(&mGf/6vC5kd8{stx_I<3's g&/yw|2aͻ7]R Ej:&GSt$N%d 𢁌dcQK-zle>!w5T<%h,CN| .|jeU;t7fl957+NCNFT'}=o;i}y}gQdY3Y~o.abe:s?|MT8W`K3Yk?];sWƗEpa I6y.}kߗ|Y+&0XO٪͞#2dGB0⨺PU-YFN sfA|gD+3}@#VԃeXs՟:rɖL]o}iYkMAG0_R$mlÎHʤqTWomvgM9\QBXXxmvvmɁ.ʁDbG>u.,';d>9׵uU#ٿ!=}硌oPg=8sk6{hȪ"! I{>O^97ij*wz/LXC U[⍋uod~y{Zh$4ʔ05m \ ~o{_?W7́ KI.վ r =/^" 1+ߛFnD%Amcf{uHr4W;k#<ز&u{dVb ϸd'Ψh7zuIZ"c.bY-4{N_Ƀ[\*%Q8ˁW2rokVdZނa-,a S*|iWW=?!`Vc;o[^pp@k55yz=OVXDΑU>7F*ԅ4,}TC_J9uP}_\>a_M%kȑ=lj)=#=zz5gj+ي Jk7x:ML$Y؇pL,{,'Ke1 pPQc耷{Y^'ŀ2{ӎx~97(Y~5Qr!W~ڸ!m}cq€vLI_?v7OXSe=A7RLWJ2]{]s$SDž- wkjj[E&~pUz:eRd biIYC©ة a1MuSW,zx@xN%2zT^ABW` \("?URpyC![:u{y''WK&`É5>k8;g&j!oD0YOzsD3r0>{=o?i_qp^,Y X Zb˾oOY:.׫g Sz1"/{.-~ߝT Pl㒝8ON{OM+~mDh'}]=o9y# UTL}05C1G@k~eK||uѡac6LvpS^2;mȪHeD_TWΡ U,Ss:ԌjSfrtXʯg/ 65d0a-w'bXT:ۆI_:z~鶽+5ʅ;u=?eM0{em HW:?So:iKȑ)]!oguly}g5/9VEt,v(ܛt;=m0%xF [Yͅ8A]ҩNr;1VDjurKw:[d61c4g.MfKXoL}~$|p` -%.Ad#3XMFxj`Ǟ@mk PK.bf $P:Ŭb?̹vodFEO" z6vR <`Zd!olN 9՜Ms.)dَ?s͒?M_iH]-JD(~7Knr/BҠΫbk溞^Pm\؉CݖX,]7ϓasL]}>wͫVZq+>t:lA/ rS ^AW\ӟ zن'ϖoy+;벣ޮ1 hvbB-lѫJw^u~)+^lj&/j#gDs'<_._*?9wˢ's4{{u ߕdIƶWvUֹ6<? <<0!3%Lu6ݪV1+\A=5r+̜V{d6wWмWu' z*2a4upP80D=U Ӈq4<蝹_8/ZB1+(ɕY'nbgͩ}fv”~vEdYu܊Ϳ\*a\#ZOmT^.'g8|݅}Tɐh|5sjW><Vj3 !hph\^ÅfK9i g.(OuJ(J_XFi+Ȝ-=aɛ_[gK׬Z 'oۑ(8? U1?=S=^^?wk0P ߫0xRս:H8.ŬI g"do@ "6)]'*fa8Փ vLŋ㺧E݈DR8ӗ甾΅ P~e q 6hvE:P $̘wO?9$X0 }"g<@rP:aC5zgZCO?"':o0zXFA ~ّ;S*v"KfL%aj->zBU$&L"jʼ *L"iGw}x!Xc1 ԗh7%%Pu?;;`=;l 0y_ }N剽Άߟĝa{e$K^ wnfjUDcKz–7 gyÛGk5筁u{DrI+ z^ѯ^f_bópϟ61`y5EП37a^6Gm5jK2z6@{ϴ~Ş${{[V@iXv4.#>;W=8wcF͕SPq+Rƀ"f)_e혚"0H7EE+C`cdo$0ͼOϞ[XE@0O Q{D;YA'!@\?s[}̇YEFZx8p5#` TyF GY(x0rJbt,f;A!A0׷>Ruλݝ3}\F}(ԖIWC؛ϾgC!օZJj̾%#d*]͓fO{d){83}p m5 `2[ -7ˠr4_4 ]SxLUD,r."AQI)3IN:iCkZA X"ZNyˠ/4;)R&Fd^NǐLڣ~>oA+E7ѯ&K}'|>T&I׌r7UENҠ$;o9},eм=ۧ`_+ ì W 2-QAMIS4xdɂK*ɤշOD=RA@$KPCKtLpH?|=rR }<6 =9Lck#x]} NA vRD*XQB-۽;'bM 乴%]4~Nc"!X-#מP<˶7\NJ-aJnJD>e6)ḁo?7X~8U(,çɋrkzJ$ZgkڱfuȇDr^OnJxoA4-õ1L7Q"VvʏSnW&"81{C/?tYDd̊w p>~]7l5 SL#0IJ,a${J-2{^S}[:%0 `i`*륓eDzv;f10's"~$ZuըG2%=z_w[+k#q0 w0Y)ri`y0:]@ʆ a w~6teں+lzٔ6>KB@Xh @TrP7~k5o?;#{OM#O$vg +4źP6ZIuE+"n%u~z]i|5gyZ7x}j4fwN Zp.wc }95^k[+g,3O`4Yg-,Q׳o?vˑ7+v`S |v\b% ujD.Ԧ5=׌|k>xnk߼SR{X>g.#mÁ6 Px|_b|kn^왴.Um&@Yco|V7>PU,X.e;U) 7uJV$g`lP_(ED&^gxPc(|2FB:ߋ"ihya7mEs,Age@^®t$]cke["p)fMesf*;a3Fy%aж#۪LB{I{ȹ`>kmGBƞ$13;_skJ5xRѡ<8Ћ78_Ii |`G (4;~BHn] x{ KShf%,Zc]#i3[O=p`c?xƨO蜴ӦP{W^ _Xp4̤.湚fKF6lF族|_7Wץy~5Q&{H m~eKVP2,]!9pSLrh֤p No|Hh'y( ",)ؠ|ÓW=@ܟ~ЏRosK4Ќrʛ6z/Lo?4<r`Yt_ζC~n(d]RfM tL d߹/腿ՙC~QﻷgHu`KIdV*<)ȶ1t%jp ˃TO5Vf_8 _seSro޸M⥧FE=]MӛJ;T #b#{3ҟ(kjkm^[}[;3|Cp`4fo G z{Iq'qu;%+3 R-ߝ~AooK[Ş)lk d `Tqh RR=/%0JhK6kSx*}j}w"~z|?QDшhb@~33]mݝ’ё=")."Mnwk2VxO d۾Rq5!޿}p˿)I]q wRd&~R;}`- \!#4U nzޒj툟_M^\M.c.ي,C ߣ rl޳+[+(iv' W#e%R i mŖ"M>3(2Tڷ^N6DF:@ Y@B⠞u`rm . -18k}&=4͙ 3r2!]Z(7p1=\H.M&^^29&䪽q{}*~ZFNM$@S1n'W˺}`-@B4Zθ=ȿ)Vry]Q40 aꂔN/phz Ҷbh ,X(Jt@89F*)rXw䍻y3E?tI2jd%XXn|.7ݑ(^3Xua7.D(.l"1]"X&_FqD"Z '瞨(dVYohɷy lK, z ̉DJ!dcGz±nTCr 9`4J2g9|3ٛ3S5%욅K1p^t쬵jo!$y;- Bl⼾qzt$Xk"Y\׽&_-O] +`?!?7 0$Jp=Q&q5d$rHi/$Ra/M/G҉TcnX԰BUJ\u1f{DubKթ( 2qҭ {[[Ա& ``3M/pO 7 * MH,0 J&n3.#,D-3'\&d 38XeL t#ބ0Oy˯3兔/E :^v2l}7~לּȥx'nއ?YQO9$\reC=x3&^ۭL5i[OabIWKp^fo/gmSSpig졞@Z< JFK&o ^]  "M&, >N7MQ};-Iy/̋T. ڄf<, |9WћQԵw?,BdX*~$ AҞr2"N *{-|"e:tf< ؖ '\IPenHLUxcTJ M*ոgܙ0] |8B$ fM}g DU4`@{E~'7.OH }]Y ծE,J<Ѝ2#+' YLJ`F@0pǓ-OZ[WSve_:~,=cG jh2!oǓ 6xfZɚRc&27Zhox? f-` va&WէߛdS{>Xhg;''Xݙ8#y񀷷` 5bH̬5>ؕ]>!5v{җn pneޱH/^8nf3T;ý#j'Y讀 i/y׮B bԈҘ0_ 7?虓eF AAX[DE(CKZribP7# &rT͉d() c%Pd*LE:)CDq@63at,_] E p?`e`4 `e: @*8@5D'j']] .=taNˎ"h/;;\B/P0 *_¿^/vk3:-9f{#l 76睧 L01s &̥u5TnPJV?9X >yȥ/}sJۇ&IV皑8o:OMtyHqw^?:L r{ϝ85#[S__9qO93Ǭۓ-vMAfCaNˎ@3e4SJ]rb*L &% ɾ+bGw?.\u PkX cئ$JO"toH(bf@Awv,鵓5 On<5 hIY@-Ae-Ĥ琷g(d6eU))U չdp@queX(b!ԃ34;CU<r߆}D5_30Z@\'ֱ( r@*Av#^!jxyZ@+,|yb@S)q=Qz80i :j1HP 3Qe`@EAFز:àe(3xeEtP ;<<-! !g&19y05t'XܐGW0OXC<4po` (J"ݠMժvWXfFz&f)tn $=ˍv8^~z^,-IDK,h SZ;~x6HXkiyP.ٰ^;'riC?1<͜Uwes_SSGC Á b.NR-a*nT }j@څc Clmj*ZXJҶ,7"i#Z>@Vvf6\PH bU.r\aAZHueE* fQlIOR䆊TX*&"D*j'@OJh/3xZC?D//0\87ҡ"qy;(ǹ D Jӱ]@|pVޥ;/@UFw#!_t}4N=MؚPO*hA[ cg9lb[&<ϛuNJ0!8 ,A肽\RNBͣ1pc 0e9]2@(` E@qM`WOWJ+(Z#;Nؒ+tDf d[O7K@8QhUk(==(8C=0>$(xCPy1<cTQ&2o QS/= O3Z1V m}$.A l⬾Opq\W\dIfIͻLkG.s/)|g~r.+""#7]8-{rz-MW ;t ]Etn{Т8 8lJO Y>zxx6Mzk00 (,ҏ9PP⨈ |19DQ`Wv(M Q>':Q@pa?H"XQ70u ]*]c`j4iㅶ32 ]hH@EL75X%SiNfq f 8YT p:\!%l3sD\O^,€=v"bq\ΙxʃǦE۩˖oU 47; 1Vq+fs9kU3Zï!M(ߨvߐ|$ ZvDH蚑ϟ>aWϔ\s5`!ȰOIs]$K%Mv ?^!WL!dq)D4oawfO^($))N)4ɓ$* c ǘT`HYMj=* l{Hg׷jJ`fz\رWh¹_Cў'l[T JJVH݈0yHTۘO 8PPUv52,0Y4pf֤Lp5ˁq3$,@fx(G]B/D,[શ2N|BME^©; oxpp{뭾7<`bA*g_F7O/3?-5&_y5sj"S/O_6OOS8KD{aŠ,o}o-P*GsLsX)? ӱj"nW'bŹ/i-**5׳*E#);#/o}xxv( b8 Zz|Ga6l/8)<,ͺхI<hc|P@\1k\jn U ' *KMzM7 &ڱמ%7C J®={%J# ’|F ȗ#O`@SU H XW"ݎ=s& %`?K b -rMo˽3\-S" 6#Y{K\6+4˔`+h nN䚀V,yrQں/}#- (oQ<ք)lm칥sęW`i"n3 ;c[reҮ"ހ-Uwe\#6tSMUj|&gNvOa$6lF|V "? R H{a\%MWyG~' B_4GPA*Ǩω0K^d*1P IT'9ЍDAv®c/FbHsX%XG#: ^aQCٰ7)()!OzOF~/`qkxWI9S%*ˉa {glCmB\fTfbPCWY5'hcE 2:!5Qb&W4Cv΃ޔK᥻rEv% Jię=9gr\{5I[BSԹI9 B )ǝkR2o:(7LDIh[G\;:IWrCU",{ ++ 䓦>JQf iJ j\G*,qp'DЈ7h@}9e{,D,P@r^Dȷ N~ LFC?f%l׮X%RV /5?=rA|!CXvsC;DL0ŭ4 +`ߝuSRENMVXzp.4m e @G`OTVְ*yт+oD̚{p[Lq|#(E㿄/Oo\6<\gsDVﺑ^vE( =mw;3m_fa*5Vu_0 Q< bpfe%ڄfƣ!Zd0'fivmR@@HevS9$P0T(q (yL`2 Q4N oHm5X%R0V ug1ǹR@.!#³!RAn>Mv%`!Du, &LڳIgJ5cUN]C1M2;g}3\& o(_mQǚ/Y9A6, JdL@5ABEGw hR9W~ߑ/:(BK%2'uX6"j]QJ9Oczx1x܋f2ZNZ@TmVlȟ`ń0 #Ȅĉ %OJ,@'B0_/-G50y'hb|NDV *:ʍ^` mRP $ E` .Z@s]@೨T)Q``҉lM`Ynirkh}li=*+?`*c$,QFOWߞ/8_a_ߍ((2\2H]'춄ܼ]Mc;E$KiR]6TӖbYNQ5,U䆦qW%N@+y5X>4`nlDj/魒U4JVh񷓝~bA5eȆ`YIb 1mw0J|rՍ?Qv`NM:xyQBfh nHW8$,r/oA^k/D7-?礼5~e/'QX_m蝍dgN:Ұ \xEfV =MWTBdȾa1e06ɏ.5uLunȓ1 }0^ ,b0Maj|U*ZkQk? GJ01rv'bok NR%iZK"i'dgA6%b2 L)B c4d'j 2ԦlW"QAhoy~0ryY wzz]E'E)m84@ŒJI&{x.^Q-&&1u>j'O*^V'PVX%X8X& OPP@J>_DĂHoga1Do!!PZձQ>j-?kWB+g@ Z;Q K!jЍ(N}3<4ۿppAvv"=>QOR0]E&$J7ȩPQvυB3x|5-ۉYi2X| hUBhâY@@[<5's g ̬ |f@H*Ző!`r!P@Ru* d(t ywЙYcm ag`SD-;kRkMb D"QL5I]Bp٬Wz܉IϼPËz]Ej]K\h\3;iEҼi hR۟m`@YDL6\4c*ڕ1 hܰ9:,ە:-(jo9)KA'w KADIZk㷈E M_4' 9 P)[ ~I@*("20JH- L`͖GUX%X8z=h"Kbp0M_AA@h#MG"+ܰ(W%yD;+j7/܁t3klk_PR ̳bhGH\ AC-Xl`f  a4u1Jx\N di8dX%XXюݔ/eOY9/KXhU*Z*V4,-Fan-t_U"UbX*VUbX*VUbX*VUbX*VUbX*VqPI|'IENDB`gdspy-1.4.2/docs/_static/slice_operation.png000066400000000000000000000475651354474061200211360ustar00rootroot00000000000000PNG  IHDRX2&}Oè-$1/Z"5Nkku8ϵBzڒpGzrg3V4 ᦠ׋xan *L/p,jKfmkCᵎt;6Æt"5%D*I/_#Îm ڱhxޡx#Ck=a;nk?P'͆_Ngq⋃^+k}_ttθsdɁ Gctܦ*H*\M߀ M)Lσ^;!>k۪-'sX[ڎMe׃TUis. >)w:xRWb1Q̿< vLE+!ܩjyurQxm OmPG\;&)\&[V, *oKhIzŧYp薶u!rϛαk&]Pc)xzO;►x=X'^ fAUYPMӗ;7M:|%@-?QU+W,V[Q3K.݈vNUn";~)Tkޠ>[,b&XCvvXLwWA;hr[xyWQdn^|:tЧUM132F;7`@~ήy2<&5 =e$*BQu96*|6*\Vy\۰KXZ*nDzxuoK_Dmi"Q~ŵ~2ت|=p&g>%$A+ 6`W e&l*p ^;0}V1W:LS"o+,4Q*&{nd`o\Q,Q[.bk;&3lE+iBkBցŁ"G'w#F} ‡K_UT-8@v#3jKM<]4x(MG 1 RjT8=G_ʍ؉mR9@28].OWITMVny !ZaXzznDdwڧ 1BX x689 V8@60®pO$|*Z=[v O_Tp&ܗHwe j!މUXY}Z{(|*1NlҨ0=\ly6l6VLXGߎFgzdW>opVa. HՖcl݈I3}%ƇֈQXiڦ-ؒu e )OG/`ֺVjbv>ޖ|fhHh !#a5ĮBOa%ꉻZZV" 5f$(1В-HhIX6Hh u"a%г"&a%Zг O* v$D{zZ*f$Dk -=MhLk6ہN;)L a%SZ Go*/"a%z,|2srCJ.XQG +/.V7bu q( 1'KJ :h 7_$zkXC<LXR'&Ņk0'KJe`3&QfF]lgl=MRW׾blU;WrbSpỌNL!D1af(,r-mՂ}|57=f\+!u'Gn@*5,VV{=yB1}9XV\%_VPVB!4"7`Q46SFB,i },j3p6rBr^e4b[POdsc|#k%Btf>fS\os*hJpk؀B+uxG3i˚:+W ku5pkWB!Dna,~ i˚CAY*8\+!bui,0+VQ2H\ !EI,־/B 6DZ*ՍB1|r,Ʈb1wk^Jf)B!En!V$1fdv!BO~s^ٯ~ ؋UeWB1~iXl]Oz'%_ [BJ@v='ê p.zmm%Psn{2p8DzP+4I`qk}L8?&Xm ոw c.|Tk~r9=g;  Fx E@_M|?6 +6y6% Z\/Άf-ɝ.qP Is!gxe{{9 F,vIPdU^`I\VApsQˁs ‚a꛰Buѱ scAչMrtqbj{>7w Wg2Bor؁Y{xy8M`}) 5)r&΄_ `xΑl ׆,<&X@}XsȪZ(АOxxxn*/~cV z:p`wn>u+6a0z4w  7ap8 <<_W5ڒ ]k[1ufU >2Y 53XWC \PmTI@y,yx`$_2huRF?<2%a\6Q `BiL? ')Or"+/_p_7Ni6r_h̬ DןK`[@y ,Hܖ)*VYJ^M 7C-+˱bY@_LL`5I`.^zN Lt؍ El0jdX?3MW ,ϻz?p%u :_m!Ule"Ev=&L젺@_>|++򗀵m)rf y-M5OdV+_>Ya t~W1jKR.ž'dQx2U5r+Wߋm4WK-B9JV=vЎ-5dU*z{<UEˀ;2*J RcIU#ntU/\yBN* ,wz]Bm ObS&sz'6竒5q rWEU +uТSNj$O q%+I_? ɱ&'.d_͜d򫨪 <C У~p/DQhèVF2$FNB,_ ]mNe+UhXc)45Ӂ`a$Cu%^Q ,w6cU?Vcк++FQfw a56:XQQ R+3IONL`Eի 3|p$HX{=LQau+v.dV_j̴Z~b{YXeaw2kVcк Z'Ïb MND`n~]]g1O^W'6Z?XpZpQ%&K,r uN C`"}7vl=P>=Āċr=`iw<뗰} 'Ҥ9/2 "wc"|qԉUXV{콘qЩa ,kuZi%uXpz+S\+ QMfDܰRRXI1vM 146z /tubգEp\Tϰ/؉;B+aUQp 㩞 xAVe NJa a9/OMU`ES;c Ns9!e]X)Y婞צ#< 6rtaA7vjYUq*qWd`~8ۙވYӸ2ZB{p~"iޓMX.\TgOЯ6\4ip-fM[9rfU7mE]~6ڞ&qnCp;ʔ| CH X#| S{fX2(>Qcm8 ӖZ !#X~,4e91ޖõ61ۅ Ճm؂-3xPCO39b_ب`QU}ܬ#> {97Y*q9 ,zi; ,ª7վ"p/웰< rqU=)F| 3`f)^_Lhq"@<}K{ VyVϰeՄfX [X +Z%f1rM@x<0e#,iXo"b?3زv^8eX@#V֔۱*O=ۀA9&r7W4ur_7`i1x*.|q3xߊpfAj Fc ;e,< fX[Tw0+qUo‚,;Oam n}$簶tM.슎2WJ`c;(27aʤE[Uu/.}IM~ovYHw` Ͽ^JO--\b".$?H5^TFܒ:jKfGntS5~ elu 0D{s[r~) >!7})` "cZqmcXG0 3,(~, s "5eW]YnV6(\QBGDv"k' KUsr\s`AQǂF#XSXW] qmˬG+5k ) Ӌr-DSGdyt1'CRtq'ͅ\?by-Va/굀BOQv<χ *q% gb#^\CrD+D zKXnyTX }Ej]'ޟ~(h| PmNl$^Xm;'#z`5 ĖK_%^ؾYjVJ#,_D1s/bkYӂQ~ߝX^DScQ%,@wjADэenW7>l˒;ZPJRE.`3ESu mS CKنa+ؽʩoD⪮+LVXg8BCaiM1~Vb\sV e..ZWw`cXރDߔn4#EmI gci Ǩ:96A S X@01uJCM(㠀(&rE%R>JJuzeL!q9XQZol:n!n&,ϋiQ!&qP;ېڒX-Pl㩒cZ(|Ї"ykV r ! D (^-0i5ì54ۀEl z˱ZWI\ !3zu9Y9Yۆp+j* GZ~~̸ڍSBL*;Țô˫}ِJ6 'Xĺl DJ\ !!6RR20-CGXaMUgmoUJFB15XLSo4ͺu]h[ {/&z9'ںD!ĴR*h'XlSLl+)˽rqu%ܫB!ج^YXo ,s6G[VaùJ\ !vVCLc,F>q2m.k=W ~ǽZjbQsV>!SOT_WޕBL)X\a &4%u:iW RL !GBB,}X3O4a?W݀-\"mu;'|,B!D]! [:/~ߌYf=+jB18ъGؚ:/bF$z>jdXB!&Ibz|4VuI$Eۯlԫ؅Z^jB~;Oڳ]^}$W7Jؽ?ОB!ĺ(|zX`psU`5rZ UA!*XMY\yEA 4U`E?= lˬ J3!C"*px=uӄ{5"9'- BQw>KQ+e' ^nwO,>8Ef<^ !C'ڻ^,n&xNaZHO+ Ճacʔ:B*3b탤byAO`EWہnT8!x)3i^򰺉%=_WA!byu@i"ߖ^ֵ_sPf}%B&)S^,еb6cWB!hXX v"zyr7f?O+B kPK?Ә&U`喘W]sM "vBL%}DZtz5 A & XBW+\|%K۞bIcb_bq$,t[Ik%W6cB1XWuwYy'3'Hp1!DʼnX,Dښ mVG`ug0qof4蜜A!b&o~U v.: mS=!% .{brx6ZmBj93cB1^<>F3I l%ahǼΐ1決K B!hȋ Oc9EcD=4in*rCxN,w!b؛X|sX œ4// B!|hqi+ 9Xs< 1,Bɳg;Jѹqeh)B`)W瓵+eQ2E!Ha9ՄK+7n U` < Bm-hMI`-eOu ! %t_Hזn'-rv߈B!X ,FeNF&e <B!Dˤ)[50 ऺBQWR3Y`7+䖭Jm܃*J*ӏ;N!.x,؜b4VK,<K` !c#q:e&wNR?ԫ !u#Bqoas%BTcs~h_+uJ!bC'!o\:!"%<>dhFa{ `3pDZttZ;~0ɁytHB,殟 #q%Bl>t ؞XFu"Bԇ]C x/Ė%)ŽV'%GQ\ ?aK$U;ǀ{-(ZLH$2% 1BlNu0aۀ/G5*G;S\Ԙ}4$H$+]1V7գ8ki%a%BT,'8T%w!BH` E!ZkN.eSB!FK<&BBҭ 1{EղB:u-&)°g_ }R\Y("ܗB!&;b!]ۙn9Xu"T@!Z/w iWI^ L!atg a/Lrt+Mq .zL!cuX%Sm&H2{"B&vc1:U {;tk'E+o8aȵ]B1B CLމTS2vbǰvRt ̷B!Ȋ` <=n+չјU`)BI8mE`ظ,ܗB!&H ,?owy^ɱ$DŽB1~< ͩ.sMp HlB!Dmxix)kh):b),=rf={2hh%B1^r+`1 i!ycMQdHƃXǹR!B -ִ(rkaA]~kP!/{=8+mŴ?֒^"r%cB!{_T]DݞI`/)WP$|EQEw!blDܷXA"\`GH3񪫛˳ŢB~9WH;bX:Xi<pK+B[X*i`lX[NOf6q-ks;M<BiZ,ZK4ڨm}E]dS{3a !#'H5OIst9;?`!x,\AϽDX[z纽Piw.B!R"1;cY76؃ESv}fĴˀ]6G!8{`pTai)Pī_U#BT,(Li^Q`ewbT/P\B!F'Bo|,N+G+X|DZa^(_jM !C&Fבvv0 +0U1_ܧ3n WK![vL D/jK9AW^[Ek| !Ub뱘YL/KWa[I7 \b,nO!$O gI;ήb0p+}WҶrLy"Bԕ7cvt+O-ZQQ߭r()ӄP+eu-BL4rKl`ʸ@?^{4k9Gb !Ucyʼn3< x=C0ǒ7 lB!&*Ͱ ˿:ڔ}~g#}Ss *<*B 7bހeW7SgY,,XK.֭ysB!D Vw4&LV~g2S`KSX`ۈMJvB!Ǔہ7a14u ڧ]~p`.֦ B$!ފݫ8K᱾Χo Γgb^B! 9X,=C0D N`ZW:WɁX vNZQ(Bt!Z9x K7#AdgpQ.Mx?hzc !"]ayӃ0%=,t|%BLSwEi-vgXS]>OmX7%bڨ{U&L E`E>tQY]>bv 9lШQKW9\?rsH5]("0 z&r@7qԳyr>஠]ʰ^W˓` 4j=-} p,:vhbDmvlo?}CBk*OtAStM X욆8= ;b02r)pvYO`աM/%WS<DZx{U{ $h][Dw\Q.R"7}#A,V=MUi+Ъ{Ñ v4A.Wb[wyVy,=*,`c2pp GXTcUzWMS3n#9xQ)JX9We8[lc:a –DPo |rxyX@l0زgz+:^b*ϗbkbi ?+^^b4 {4 eF6>%-Q9̙IMԟ( tOTyO# >cSBP`)kZL/ub5x i2T5`Sbᴚ`8ht"&*|5تi*t|Jz\a ,w2́8z*E存3i,'vTfj46+nb\qO$Q~lLȂ(z~/J\K3*Ou}/uYTX=EJvS&wb 7k"!^3UÞ>{"7,xrx*3Ib_DV,Q$.ߍ-/]㠅ڃǼK>*j OR[!\ZN K`9q*eCX1%sӘkXtߎn86éT%{7VVBk:=X-2gO>*b< 뱶-T=X}X[r>iVoÄVD˰)lēˣ7c-b_^5çza Ο\!QvcÛǞgtبY X"SВ 1Zk3 ă}y| s_ aQV &vRsj9e2l| +*/O\`AzVI$8b h=u] h3:G/(;qV]?bQ=ʹm[0Q}O4R`,cb@.i1|=VvJ̱:p?낋+/f{UWPa?X>VlɲlZ7=S@(ƶN,j)\R,H a~+*g5xq ,ǿ+QКڑVZѴ+0w6,aU}6,X{DU M /~x ֊ Wi3[eZ-(fscgՉIO4 -X+HVqIX*ٚd+Bs:=<@b+@VU W2xJie4M~/6M4ӷwR=_ LfT87bQ$Ov}Y'Lol%2sܼ ە"V̍r {/r5󒨺 ހʧ`\)ėwdUt*%IdS=t  a_g5@A ;hDr (6?DIUX/~mFPCr=X y̋;&ͤ Xsms[j%vbX[I/a#a՞ k_'3XTS`yc~ >)\:\6l q+ƍf^$/tIPqU¨& ,b*¤&[/-$(; s'&^p0ix]MHW^5*" XYDU{ NkGCͩZ[|ktZXV W:Hܭe_έ[#+܅bq Ӫ b*h.nCX߷3[DvBKWbmEYlr.,܎*+Ŗxo`m,{bQzc<[UlǓPb*)`M>`9!Ӽ <(F`d6tx>awtys{iZnr>6aNթSQaVL'7̑x YXEL-y[S$ ~al\|BTUFVLm~6`( 6(e5ǭ1l;SdAsZvV-t i> KTOT"bb7"Ǯ̕؈45niLmK~::%Wp3<<*ˉvosX[N=p~x[3>i'y  #t9DȰJ;, D;7(\ {LhX ˀ#ôeo ~/`9Aߝ:VLܖV}bhx!K^aI׆ppXt>TVz9=Ä*6n=>SvCksmg115Kmg)Vw{r[ +;. Yc i Xa)4V`"Akޙ.`ɞ eU5 ~ܩ*Jgp^gvQGO(s]@bnJx̅*}[LU`)bO~M3[HY,ŝx0z2q4X7I+HD`DGP$ބI`?+&f(FD1/uXe 9W4 ]9=߾E!$E`~y Lj~B@`"}pkɊ+(I j6\`ۇ jk%Wamm9V^'nKg P1Rz-s:~?go?}pqu +HH`5.1㘍[%5#/根(wNO_ԮIϵuՖ 6غW:[HJ`V*Oby`#^9YE^ %1,Ԗ&.>b~2Ε\uQlDC!BO bGCT^IN`> %BLY\}$W,!&N\A $BĩtYwB!Gډ+"yl=2!B<f j"`074WB!db&F mg*w BɐQx+HhG %!gmP}ބBȰն`DfSj`D{>m% !ēwbuWPcDL_^TA!=^*,bBl , asZa(BX̽bL ȩ "+`)Ow\YB!İ(~ >&#Za89ލ^BB!zd XfpO$iL + =~[]x}B!d]jNC2{;bLi(ϣ)C!|J+|hL/Ǧ &zpB!D*y,z24TM߁OǁlF6B! 2,Y}K dvL*?2e`;* BĮ՘9f@Hfg1VL4ex l3gf !VVS=%Xf3(+g˱Σx!G/TO Ղfe5=B1]ȵ9Xm Ig3xzpEn-!uD:wBޟpf8zU}`Ý%1֎V}Z VdB؍U=56G/KXڃm-b_"z~2,-a\\Q''!7+ 5v .bUYXguq lyV Π$Zl,O9G9?UypT߄ UH` HrXN %jF|:F#%RVM>J{[sGBKEjH` %D;>LX AVm -ъ ִ=к -QГ O !PnZ"ӌV Xgv#p{{XpyG6m[+WafA,p8}XyV6uXɉM&RLwMxo%ہ tQlYS`h3kG,斶r3w0uOZOXQ^=Jpm6 /\G۫,kڃni2IQ 籩 )[ w&K(\I5_žO:ܦ}T7$-:^cɜKcFٵj.En;WA,wXGJ/r[ځnn)Q{[jW~\] ovcֈi)=+h_%cU[J7Ns`YsT V$ˣɋ#_Ӭ`ql J;lP o` tv^Pu-.c"~&>Wq0 qߛ0W?߃o]TAphf) ,n>0,!…q!^P[;),>B<U 1=&3Lp]u wv`QzGE':'Gsb+U>y{Np~~/`&wRUgF!懁f!SP>~V11 U#Rn5Xa\Ń?Pa궅A]8dR~ /rͧ )/v$v׆aX'IbU,A5`m)Ѯk(L^ܱBʅqPxޑ[&Z<X,gF_IJI1 qvt1KPM)XSLLx=0xǠǜyM=6cxS; =ʹYjGKԵ-ttfa3>VIX-m:6Y%z易W3~a 1?{ԖDW$Cn?ꨦ%1,ԖB!B!B!BLUq4IENDB`gdspy-1.4.2/docs/_static/text.png000066400000000000000000000342311354474061200167250ustar00rootroot00000000000000PNG  IHDR!xi8`IDATxy$euW}Bc7-x (:ouqvUqe)(^ !M#4M]udTvuYYY4Uē|lLL%F g!яImԼ!FB%lccrS\$](LRF?&ht5vսfLB@"! BHDbwc^-U~6:pt:Uq\HESy\K,Ty` %Fj3icQImJ~\Ғ:l=NW3Lgm4WZ{Z8!ҡBEHcnğ~ι9 !stJp1ٷx॒j!eJZhh_}[JFq(U~H s\r2n'gtF.3l%w&cҐEf#du5VIIdI%S1Uu::Y7cIW(%JKBA|քkmob$l9@6&U#1z%KZ7GE|6 z%`Vp+F ih>Ζ@R' 0oIjt.jBH<.Um='6*:X;ymg]|`u~6+(.#j<4"rq4\XdXXШ'$A DB@" 5Sm{imMR{hltc&n20c}#f]8$+ )J2ռ7da{:kh:h/i:c$  )BP6N"U?9'@R8+#Y++i=MCIIfhf=%Z iv5UHQ8|oVsI[7xJ,!GýP{$Tu-t}=qL#I 5M]oQtRi#L}XJ5gXT@ntFG^Lg* }ݥ4 IG):mWx_!beTw)O])PcmHkI/1wJ>#ˁ*BPK)I-:88eF*@ɆFj1杒*og5-!.+9BjV<&{$p^ RcL/ۀꡮ DB@"! BHw#eRdɽn0Og'#]tYbVX4>*:JfjC]IE'j9j6:bT7宇w:K)1US@"! BH!$c3d{-;܌geS^ 3W@_$^ʿET(/)Ck%ߊhz@Sg !Yi~なHZLhǖ.W9nځ'+HگZ.BI]TY¼Bi@,ϒjKYѼ!O#* 5^(iFJS^Kڭ(OYFtY¼Bil)EYU7,q({&n4\0HF?h.`I.WuƤ%0ҏ>Khv]M*?FԲ%$rl1y|i]VRF[Rg !9؊T 0U5M=aRŖ_ga;zts0OB@"! BHDbwFU7jԸ߆qD-J~** 7KM#N7yPRcUS izf;}۟LnNm‶]-O 1!Rh$4 @5}˩1Bv#i&DJrx5!DU>'jmOQZQt)ĝ@SIH!$i;oʲps'H9z5-! Hzj<9e9ͦ^ sKʫqBHRv/IA"hGq=?C01cM=6F+uJjO3v̢$[BZ]#[O8}jzzldp3_`jFM$-!'~Ʀeh*mR0'?f.JMߵM`DWYG{*-#))&o&LP3>iZ!LQj3VAEG+k9 2V)BcZ$?ԣ CY\`ڬ1AGSBH!$A DB@"! BH!$A DB@"! /cg䅄¤0BMJB+[I%նU$.f#m7Bhn$JVBTR*I˿B-"ڇ!Iř.@ssve3wɮ{mk 2to3Gܧdcnpx=uϢtL˔Btkq.#^o!C#pd2xoz•wݴۼlW>wwd;ش|MǬߺx;:=yy! r_>9 ]S4;72}M<'1^Mdž |#c\xqgw6ƱcNz⎖S.3K(SsfuF G9UOXilz;y֔!Dy˼9jU_nO$' {+ 30H ri'ȧwdP {O9NvQ4J:o>y۽ǧaX~3_?Wn[-^:Lyg˲L̅ok<7oC-#?8;>ktׯ}_7_?䤭zǍ7oZs^eܵk/_{ůxGT}l;ֿpҁbKћNҴ_tv.Ȍ_O>S{w#__9W+Gl&u<9tkv-zqE|/JwHyg3N/f̞#Tk!R/}e==ZKrqC|׳1##7^ɣvٹ{tKz^؏OYk ֟kĻ;O|rP%e#?YG:p$u,{3ޓη__oױdpǖox#7uɶ:3kMBBIE>/|~OO˃{'W6eif$KJ g3 |/ ltWͷ[?[O/?IY#{x|.}c=JZ 'cݞGW|{ᄣ8[`]ݟK|λ\s'u$2 .xS/yu§~7g}ʿHUHҺ{ >O/P[AnCgo~ ~ŕgoďsgq|`&d[owDj)}ӢJ}olv864?? _+t'_-2#q'EI2V&nXsfji+Furv/RWSIw{r?{s|[]}]]7Yz39w}e7p~K \ w|v󂓶߿`yPmGqH}Ot?l`H]ؾ>.xiTQk+*[85O-:tk m8jy֮m326=觧]A ,fn?;v[տuɋ7\mY27I=CJc%co;\mt[ Ec e_6g[z⮧XTz$ulk(3߳*u?M[>Ad[]niGlP^ڢ>uxvOW|w-}I;8.mI|6el|߁:<"`X9nK k͞y$SyF(e|IʔF~9Zvj-/c/ͮB}U[,;a%R> Wf^ Y?s{n[%XPȺyA3&ТuřMD9:vk_ҿ//KCGj)}zْ. =uq=q׾le*_c2̄riuh.MB(Vf|{۰6,9R(w|O/Лyv\ɇK^lq>e_ˮ^o ߱`MI 2i-3q'ֆdcD5&ȧ[K!4ŔBk$-#QGam+F[ݺruU=J*x-IYe=S<:~~)cQU}I|6O)ggMy@3s$HZekl#)@(p:|-'ZHS)}qӎ$Kv>ݿ¥#ٮscOzVu߾#?к O^p]'qLE]qIԠ1eu3őg'4M$),NO҈ZKi?Ⓩ.4R~4Zy_BwNFwot?:$L2sF6S =h(Sj dFRm3Α|jowJڮ7 z%`VMB0T:,: ʓ؜jZZr~Il`:ZFZ/ϪuՁ-$cr[mUm΂ ]l[GOdY*(fqdfzGo J޺~I:E [ IVOБ2Ŝ}|_l++Ȭ<+0*ɜ-TJMȢώKM?_V' ikw?ғ 邗.=x@R[ɶ;cgn[(/7]s*j  Λٳ83Ãm^IAѤxʝ읫%cN*ص/YceMB/'' r\v߷Or&zknXr7}NZJCn;_V.n_;^_kX^0LnXt:Ux>-Ys㷜n: ,Ju^w>ͥ w`Pؔ^4s+7ݴj;Y/Ґ[Kzc{>@p.KV,>lČPMű)x x_qj{螁2#ɾ[[Βdn9U G3VmT/8;y͗=]-ΆS4 SK;,#I=8td+^ݸq9{?u =l${߮=?z\=shso4 )4=55nxVƦ oY~痯U3mxwY}nޡO_~ݭm#ISEr@o/V]o}柝ˏKޱ`MΒ/ w}ӅT[4xrYv~?"3R4OO}]ks?ITIT2\8ޙ*4jrz!eJ/Ϗ kw]?_.ϻ}sy{9qpn(ȆFƱ3֢٨~ <)@N)T<>!#Ya)MߵM/BKhB$q̅v,P ;:}辧::sH5|nqÏ>{UK:[fB/nwOܺ|^sԁY,nHV:^:{.t|+ڂEHZzpGXV,_[YxT[p}Q)m}+^V/Jnx6uG_ŭAgċvE7ph_q/( :3S̥쳽n{ ,:6Ċ3kjґJ+KzNRK-]$}Hߏ34mMh<;\ZDsn8VӴwF@|Jaڗ9O^&)Ћ]Yʤ vexk?5].Kr{sa BRQreC#dƻųqP?*ċ+SQ۞&olG\ǒZ osJ)I݃Ju狞Dl5zS~P[BVI:KQA;b7ϯTe k&U,cMs[g:W~$2$H$4qm>Ֆ/^1"_9L/F 4x;Cǜ5}c\kyׄVgS4ݺ訜$1! ƑZ1! h4DB@"! BH!$A DB@"! BH!$A DB@"! BHt 78)od! i#Vz5JV(g|HV2&!4uZ% kBQzH48ޓ!4K29}}B1`YD@QXPպ!4wdFRAs'4ٓ!40+%(q3|I+4I!d%DIJ^EĎ>E-*~7I[k!46ORBG|%I mRUڑ*?*CHJR\^S֚qBhqF lo]#8! BH!$A DB@"! BH!$A DB@"! BH!$A DB@"! [*\#-y sY0OBnF葿>5w][ &uxKo~#ɨFvmt;J\5qTcN*1`LB5- ^5aʏ?{s](;!46+$PUJ*@$Z$R:Z*~Jj2E]0溳oH1ŁIc$鍒AQK\]1t[/g%3 !Ѝ]c\V:YRID;./-@j͆ShNu:5RhW>ʔbZBh~F#2!(3ZI[qpE@s;ިhPs?l{ i4P8".D!41bf9k!413TqB@"! BH!$A DB@"! BH!$A DB@"! BH!$A DxS=Zk}co@5a`ʤ4HBR`Kz6ԣ.YVzbwuHHtH~|4L2Uh<. }`IENDB`gdspy-1.4.2/docs/_static/transformations.png000066400000000000000000000071631354474061200211760ustar00rootroot00000000000000PNG  IHDRX3}:IDATx=,YϽwu b~SLA@pEd#um헪:uޞTt @ӶwۈqX -o{$84D&"*l"~wj},͈gDc'#x7P(d52"~ح+b , a=|B (V53 -H XauLhEX@R $VcB J`VDŽQXXZ0KauLhXM* cB HJ`TVDŽFi, -`Q :&E,fX1D`!Z$ :'&Z( :%fZE :#%tBX%%4NXJh!Y*+XaUXaU4XP9aUXP)aU5XPaXP a4XP8aXP(a5XPaXPaB *# 3aB *! a B '`eŠ -(+ZP +V$ Vd$ 3 ViWfS-D`BU:ǻP>5Ђ ,IXgߍ">È_G<Y7ZǹV:=-n1^׻e^(vboD[X0b$Ո$&`$aV\-MhA" Vt@h!Ђ,8"@h\ DhVpЂV0ЂV0Ђ+V8g,' 9GV:{+Nh=E3Gh-EOhETGh EuTOhNk<~.Y,r.CDx6b9s~k٪DĿ"e gwO"}?ͷ#J͔cFlD|& "tDߋ.;yPcۈcW#ÈGRx?w  .OE"gۈSO#Ap/{haX},va^ꗛݽ7>k/fSB _V]t ZHXl44fѰ:dB \:̈́PauxلPU`Z@aV Ã.Nhe Ã'#e A$'ĊFh +*:TdX.LTtX2;\QEX TVk {Uՠ-Na5(:B DX 4Vk zMՠ-Na5:B EX tVk .jd` 4X ]@h ]@hl.k `2a5Bׁ5Zp@`Zauua5@h!a5jAk@UB  B -* V$$(@`% (H`%$@X@`@haU"@ª@+UVFB  -FVXZ *$ $aU5U0%aUaU$a U!a0U1P%aPaP$a! Pa10B`@hBXHBX!aYcB &ŠB `ahIŠ/Z!A`q:%M`q:!Xb44JX8dB h"̈́P)aErلP ajZ@X,NhVd#HFh+X$'+!XVG`:,DXQ,E6B x0b- aEuZZb -薰z -膰j-h9-hYj -y -f-(;-([f -FX=EFXB VpD`Vp[B n& E&`${B V0#B ^Vp#g-:&`&W-:"`! FZ4LXL$hDHhQ1a ,IhQa+XEL`„VD +L`AbB +(-VP+Z,HXAd"AXAd&@XA%Bhq,(‰J ,(ꚰ ,(ꊰF,j,jF ,Ъ ,ЪN,h*,h*N ,hJXA4NhJX!B+)a< 3BkQ 8I`A, H`A$ E`!V$ x@h= ,CKX,BKXX(%I -a$!TZ HJ`TZ XQxh +`U XTa%,DV@V HjV@ġ%,`U $,f&& "FZD (r!O"Vl#6ۈGn#;Xvwzk:2a0>@E]DCIENDB`gdspy-1.4.2/docs/conf.py000077500000000000000000000243271354474061200151140ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # gdspy documentation build configuration file, created by # sphinx-quickstart on Fri Jun 26 15:49:53 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import shlex import distutils.command.build from distutils.dist import Distribution # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. b = distutils.command.build.build(Distribution()) b.initialize_options() b.finalize_options() sys.path.insert(0, os.path.abspath(".." + os.sep + b.build_platlib)) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.autosectionlabel", ] # Autodoc settings autodoc_member_order = "bysource" # Napoleon settings napoleon_google_docstring = False napoleon_numpy_docstring = True napoleon_include_init_with_doc = False napoleon_include_private_with_doc = False napoleon_include_special_with_doc = False napoleon_use_admonition_for_examples = False napoleon_use_admonition_for_notes = True napoleon_use_admonition_for_references = False napoleon_use_ivar = True napoleon_use_param = True napoleon_use_rtype = True # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "gdspy" copyright = "2009-2019, Lucas H. Gabrielli" author = "Lucas H. Gabrielli" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. from gdspy import __version__ as vv version = vv # The full version, including alpha/beta/rc tags. release = vv # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { #'canonical_url': '', #'analytics_id': '', #'logo_only': False, "display_version": True, #'prev_next_buttons_location': 'bottom', #'style_external_links': False, #'vcs_pageview_mode': '', "collapse_navigation": True, "sticky_navigation": True, "navigation_depth": -1, #'includehidden': True, #'titles_only': False } # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = "gdspydoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, "gdspy.tex", "gdspy Documentation", "Lucas H. Gabrielli", "manual") ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "gdspy", "gdspy Documentation", [author], 1)] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "gdspy", "gdspy Documentation", author, "gdspy", "A Python GDSII importer/exporter.", "Scientific/Engineering", ) ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False gdspy-1.4.2/docs/gettingstarted.rst000066400000000000000000000540121354474061200173660ustar00rootroot00000000000000############### Getting Started ############### GDSII files contain a hierarchical representation of any polygonal geometry. They are mainly used in the microelectronics industry for the design of mask layouts, but are also employed in other areas. Because it is a hierarchical format, repeated structures, such as identical transistors, can be defined once and referenced multiple times in the layout, reducing the file size. There is one important limitation in the GDSII format: it only supports `weakly simple polygons `_, that is, polygons whose segments are allowed to intersect, but not cross. In particular, curves and shapes with holes are *not* directly supported. Holes can be defined, nonetheless, by connecting their boundary to the boundary of the enclosing shape. In the case of curves, they must be approximated by a polygon. The number of points in the polygonal approximation can be increased to better approximate the original curve up to some acceptable error. The original GDSII format limits the number of vertices in a polygon to 199. Most modern software disregards this limit and allows an arbitrary number of points per polygon. Gdspy follows the modern version of GDSII, but this is an important issue to keep in mind if the generated file is to be used in older systems. The units used to represent shapes in the GDSII format are defined by the user. The default unit in gdspy is 1 µm (10⁻⁶ m), but that can be easily changed by the user. *********** First GDSII *********** Let's create our first GDSII file: .. code-block:: python import gdspy # Create the geometry: a single rectangle. rect = gdspy.Rectangle((0, 0), (2, 1)) cell = gdspy.Cell('FIRST') cell.add(rect) # Save all created cells in file 'first.gds'. gdspy.write_gds('first.gds') # Optionally, display all cells using the internal viewer. gdspy.LayoutViewer() After importing the gdspy module, we create a :class:`gdspy.Rectangle` with opposing corners at positions (0, 0) and (2, 1). Then a :class:`gdspy.Cell` is created and the rectangle is added to the cell. All shapes in the GDSII format exist inside cells. A cell can be imagined as a piece of paper where the layout will be defined. Later, the cells can be used to create a hierarchy of geometries, ass we'll see in :ref:`References`. Finally, the whole structure is saved in a file called "first.gds" in the current directory. By default, all created cells are included in this operation. The GDSII file can be opened in a number of viewers and editors, such as `KLayout `_. Alternatively, gdspy includes a simple viewer that can also be used: :class:`gdspy.LayoutViewer`. ******** Polygons ******** General polygons can be defined by an ordered list of vertices. The orientation of the vertices (clockwise/counter-clockwise) is not important. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Polygons :end-before: draw .. image:: _static/polygons.* :align: center Holes ===== As mentioned in :ref:`Getting Started`, holes have to be connected to the outer boundary of the polygon, as in the following example: .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Holes :end-before: draw .. image:: _static/holes.* :align: center Circles ======= The :class:`gdspy.Round` class creates circles, ellipses, doughnuts, arcs and slices. In all cases, the arguments `tolerance` or `number_of_points` will control the number of vertices used to approximate the curved shapes. If the number of vertices in the polygon is larger than `max_points` (199 by default), it will be fractured in many smaller polygons with at most `max_points` vertices each. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Circles :end-before: draw .. image:: _static/circles.* :align: center Curves ====== Constructing complex polygons by manually listing all vertices in :class:`gdspy.Polygon` can be challenging. The class :class:`gdspy.Curve` can be used to facilitate the creation of polygons by drawing their shapes step-by-step. It uses a syntax similar to the `SVG path specification `_. A short summary of the available methods is presented below: ====== ============================= Method Primitive ====== ============================= L/l Line segments H/h Horizontal line segments V/v Vertical line segments C/c Cubic Bezier curve S/s Smooth cubic Bezier curve Q/q Quadratic Bezier curve T/t Smooth quadratic Bezier curve B/b General degree Bezier curve I/i Smooth interpolating curve arc Elliptical arc ====== ============================= The uppercase version of the methods considers that all coordinates are absolute, whereas the lowercase considers that they are relative to the current end point of the curve. Except for :meth:`gdspy.Curve.I`, :meth:`gdspy.Curve.i` and :meth:`gdspy.Curve.arc`, they accept variable numbers of arguments that are used as coordinates to construct the primitive. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Curves :end-before: draw .. image:: _static/curves.* :align: center Coordinate pairs can be given as a complex number: real and imaginary parts are used as x and y coordinates, respectively. That is useful to define points in polar coordinates. Elliptical arcs have syntax similar to :class:`gdspy.Round`, but they allow for an extra rotation of the major axis of the ellipse. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Curves 1 :end-before: draw .. image:: _static/curves_1.* :align: center Other curves can be constructed as cubic, quadratic and general-degree Bezier curves. Additionally, a smooth interpolating curve can be calculated with the methods :meth:`gdspy.Curve.I` and :meth:`gdspy.Curve.i`, which have a number of arguments to control the shape of the curve. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Curves 2 :end-before: draw .. image:: _static/curves_2.* :align: center Transformations =============== All polygons can be transformed trough :meth:`gdspy.PolygonSet.translate`, :meth:`gdspy.PolygonSet.rotate`, :meth:`gdspy.PolygonSet.scale`, and :meth:`gdspy.PolygonSet.mirror`. The transformations are applied in-place, i.e., no polygons are created. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Transformations :end-before: draw .. image:: _static/transformations.* :align: center Layer and Datatype ================== All shapes in the GDSII format are tagged with 2 properties: layer and datatype (or texttype in the case of :class:`gdspy.Label`). They are always 0 by default, but can be any integer in the range from 0 to 255. These properties have no predefined meaning. It is up to the system using the GDSII file to chose with to do with those tags. For example, in the CMOS fabrication process, each layer could represent a different lithography level. In the example below, a single file stores different fabrication masks in separate layer and datatype configurations. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Layer and Datatype :end-before: draw .. image:: _static/layer_and_datatype.* :align: center ********** References ********** References give the GDSII format its hierarchical features. They work by reusing a cell content in another cell (without actually copying the whole geometry). As a simplistic example, imagine the we are designing a simple electronic circuit that uses hundreds of transistors, but they all have the same shape. We can draw the transistor just once and reference it throughout the circuit, rotating or mirroring each instance as necessary. Besides creating single references with :class:`gdspy.CellReference`, it is possible to create full 2D arrays with a single entity using :class:`gdspy.CellArray`. Both are exemplified below. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: References :end-before: draw .. image:: _static/references.* :align: center ***** Paths ***** Besides polygons, the GDSII format defines paths, witch are `polygonal chains `_ with associated width and end caps. The width is a single number, constant throughout the path, and the end caps can be flush, round, or extended by a custom distance. There is no specification for the joins between adjacent segments, so it is up to the system using the GDSII file to specify those. Usually the joins are straight extensions of the path boundaries up to some beveling limit. Gdspy also uses this specification for the joins. It is possible to circumvent all of the above limitations within gdspy by storing paths as polygons in the GDSII file. The disadvantage of this solution is that other software will not be able to edit the geometry as paths, since that information is lost. The construction of paths (either GDSII paths or polygonal paths) in gdspy is quite rich. There are 3 classes that can be used depending on the requirements of the desired path. Polygonal-Only Paths ==================== The class :class:`gdspy.Path` is designed to allow the creation of path-like polygons in a piece-wise manner. It is the most computationally efficient class between the three because it *does not* calculate joins. That means the user is responsible for designing the joins. The paths can end up with discontinuities if care is not taken when creating them. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Polygonal-Only Paths :end-before: draw .. image:: _static/polygonal-only_paths.* :align: center Just as with :ref:`Circles`, all curved geometry is approximated by line segments. The number of segments is similarly controlled by a `tolerance` or a `number_of_points` argument. Curves also include fracturing to limit the number of points in each polygon. More complex paths can be constructed with the methods :meth:`gdspy.Path.bezier`, :meth:`gdspy.Path.smooth`, and :meth:`gdspy.Path.parametric`. The example below demonstrates a couple of possibilities. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Polygonal-Only Paths 1 :end-before: draw .. image:: _static/polygonal-only_paths_1.* :align: center The width of the path does not have to be constant. Each path component can linearly taper the width of the path by using the `final_width` argument. In the case of a parametric curve, more complex width changes can be created by setting `final_width` to a function. Finally, parallel paths can be created simultaneously with the help of arguments `number_of_paths`, `distance`, and `final_distance`. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Polygonal-Only Paths 2 :end-before: draw .. image:: _static/polygonal-only_paths_2.* :align: center Flexible Paths ============== Although very efficient, :class:`gdspy.Path` is limited in the type of path it can provide. For example, if we simply want a path going through a sequence of points, we need a class that can correctly compute the joins between segments. That's one of the advantages of class :class:`gdspy.FlexPath`. Other path construction methods are similar to those in :class:`gdspy.Path`. A few features of :class:`gdspy.FlexPath` are: - paths can be stored as proper GDSII paths; - end caps and joins can be specified by the user; - each parallel path can have a different width; - spacing between parallel paths is arbitrary; the user specifies the offset of each path individually. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Flexible Paths :end-before: draw .. image:: _static/flexible_paths.* :align: center The following example shows other features, such as width tapering, arbitrary offsets, and custom joins and end caps. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Flexible Paths 1 :end-before: draw .. image:: _static/flexible_paths_1.* :align: center The corner type 'circular bend' (together with the `bend_radius` argument) can be used to automatically curve the path. This feature is used in :ref:`Example: Integrated Photonics`. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Flexible Paths 2 :end-before: draw .. image:: _static/flexible_paths_2.* :align: center Robust Paths ============ In some situations, :class:`gdspy.FlexPath` is unable to properly calculate all the joins. This often happens when the width or offset of the path is relatively large with respect to the length of the segments being joined. Curves that join other curves or segments at sharp angles are an example of such situation. The class :class:`gdspy.RobustPath` can be used in such scenarios where robustness is more important than efficiency due to sharp corners or large offsets in the paths. The drawbacks of using :class:`gdspy.RobustPath` are the loss in computation efficiency (compared to the other 2 classes) and the impossibility of specifying corner shapes. The advantages are, as mentioned earlier, more robustness when generating the final geometry, and freedom to use custom functions to parameterize the widths or offsets of the paths in any construction method. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Robust Paths :end-before: draw .. image:: _static/robust_paths.* :align: center Note that, analogously to :class:`gdspy.FlexPath`, :class:`gdspy.RobustPath` can be stored as a GDSII path as long as its width is kept constant. **** Text **** In the context of a GDSII file, text is supported in the form of labels, which are ASCII annotations placed somewhere in the geometry of a given cell. Similar to polygons, labels are tagged with layer and texttype values (texttype is the label equivalent of the polygon datatype). They are supported by the class :class:`gdspy.Label`. Additionally, gdspy offers the possibility of creating text as polygons to be included with the geometry. The class :class:`gdspy.Text` creates polygonal text that can be used in the same way as any other polygons in gdspy. The font used to render the characters contains only horizontal and vertical edges, which is important for some laser writing systems. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: # Text :end-before: draw .. image:: _static/text.* :align: center ******************* Geometry Operations ******************* Gdspy offers a number of functions and methods to modify existing geometry. The most useful operations include :func:`gdspy.boolean`, :func:`gdspy.slice`, :func:`gdspy.offset`, and :meth:`gdspy.PolygonSet.fillet`. Boolean Operations ================== Boolean operations (:func:`gdspy.boolean`) can be performed on polygons, paths and whole cells. Four operations are defined: union ('or'), intersection ('and'), subtraction ('not'), and symmetric subtraction ('xor'). They can be computationally expensive, so it is usually advisable to avoid using boolean operations whenever possible. If they are necessary, keeping the number of vertices is all polygons as low as possible also helps. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Boolean Operations :end-before: draw .. image:: _static/boolean_operations.* :align: center Slice Operation =============== As the name indicates, a slice operation subdivides a set of polygons along horizontal or vertical cut lines. In a few cases, a boolean operation can be substituted by one or more slice operations. Because :func:`gdspy.slice` is ususally much simpler than :func:`gdspy.boolean`, it is a good idea to use the former if possible. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Slice Operation :end-before: draw .. image:: _static/slice_operation.* :align: center Offset Operation ================ The function :func:`gdspy.offset` expands or contracts polygons by a fixed amount. It can operate on individual polygons or sets of them, in which case it may make sense to use the argument `join_first` to operate on the whole geometry as if a boolean 'or' was executed beforehand. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Offset Operation :end-before: draw .. image:: _static/offset_operation.* :align: center Fillet Operation ================ The method :meth:`gdspy.PolygonSet.fillet` can be used to round polygon corners. It doesn't have a `join_first` argument as :func:`gdspy.offset`, so if it will be used on a polygon, that polygon should probably not be fractured. .. literalinclude:: makeimages.py :language: python :dedent: 4 :start-after: Fillet Operation :end-before: draw .. image:: _static/fillet_operation.* :align: center ************* GDSII Library ************* All the information used to create a GDSII file is kept within an instance of :class:`GdsLibrary`. Besides all the geometric and hierarchical information, this class also holds a name and the units for all entities. The name can be any ASCII string. It is simply stored in the GDSII file and has no other purpose in gdspy. The units require some attention because they can impact the resolution of the polygons in the library when written to a file. Units in GDSII ============== Two values are defined: `unit` and `precision`. The value of `unit` defines the unit size—in meters—for all entities in the library. For example, if ``unit = 1e-6`` (10⁻⁶ m, the default value), a vertex at (1, 2) should be interpreted as a vertex in real world position (1 × 10⁻⁶ m, 2 × 10⁻⁶ m). If `unit` changes to 0.001, then that same vertex would be located (in real world coordinates) at (0.001 m, 0.002 m), or (1 mm, 2 mm). The value of precision has to do with the type used to store coordinates in the GDSII file: signed 4-byte integers. Because of that, a finer coordinate grid than 1 `unit` is usually desired to define coordinates. That grid is defined, in meters, by `precision`, which defaults to ``1e-9`` (10⁻⁹ m). When the GDSII file is written, all vertices are snapped to the grid defined by `precision`. For example, for the default values of `unit` and `precision`, a vertex at (1.0512, 0.0001) represents real world coordinates (1.0512 × 10⁻⁶ m, 0.0001 × 10⁻⁶ m), or (1051.2 × 10⁻⁹ m, 0.1 × 10⁻⁹ m), which will be rounded to integers: (1051 × 10⁻⁹ m, 0 × 10⁻⁹ m), or (1.051 × 10⁻⁶ m, 0 × 10⁻⁶ m). The actual coordinate values written in the GDSII file will be the integers (1051, 0). By reducing the value of `precision` from 10⁻⁹ m to 10⁻¹² m, for example, the coordinates will have 3 additional decimal places of precision, so the stored values would be (1051200, 100). The downside of increasing the number of decimal places in the file is reducing the range of coordinates that can be stored (in real world units). That is because the range of coordinate values that can be written in the file are [-(2³²); 2³¹ - 1] = [-2,147,483,648; 2,147,483,647]. For the default `precsision`, this range is [-2.147483648 m; 2.147483647 m]. If `precision` is set to 10⁻¹² m, the same range is reduced by 1000 times: [-2.147483648 mm; 2.147483647 mm]. Saving a GDSII File =================== To save a GDSII file, the easiest way is to use :func:`gdspy.write_gds`, as in the :ref:`First GDSII`. That function accepts arguments `unit` and `precision` to change the default values, as explained in the section above. In reality, it calls the :meth:`gdspy.GdsLibrary.write_gds` method from a global :class:`gdspy.GdsLibrary` instance: :attr:`gdspy.current_library`. This instance automatically holds all cells created by gdspy unless specifically told not to with the argument `exclude_from_current` set to True in :class:`gdspy.Cell`. That means that after saving a file, if a new GDSII library is to be started from scratch using the global instance, it is important to reinitialize it with: .. code-block:: python gdspy.current_library = gdspy.GdsLibrary() Loading a GDSII File ==================== To load an existing GDSII file (or to work simultaneously with multiple libraries), a new instance of :class:`GdsLibrary` can be created or an existing one can be used: .. code-block:: python # Load a GDSII file into a new library gdsii = gdspy.GdsLibrary(infile='filename.gds') # Use the current global library to load the file gdspy.current_library.read_gds('filename.gds') In either case, care must be taken to merge the units from the library and the file, which is controlled by the argument `units` in :meth:`gdspy.GdsLibrary.read_gds` (keyword argument in :class:`gdspy.GdsLibrary`). Access to the cells in the loaded library is provided through the dictionary :attr:`gdspy.GdsLibrary.cell_dict` (cells indexed by name). The method :meth:`gdspy.GdsLibrary.top_level` can be used to find the top-level cells in the library (cells on the top of the hierarchy, i.e., cell that are not referenced by any other cells) and :meth:`gdspy.GdsLibrary.extract` can be used to import a given cell and all of its dependencies into :attr:`gdspy.current_library`. ******** Examples ******** Integrated Photonics ==================== This example demonstrates the use of gdspy primitives to create more complex structures. These structures are commonly used in the field of integrated photonics. :download:`photonics.py <_static/photonics.py>` :download:`photonics.gds <_static/photonics.gds>` .. literalinclude:: _static/photonics.py :language: python :linenos: Using System Fonts ================== This example uses `matplotlib `_ to render text using any typeface present in the system. The glyph paths are then transformed into polygon arrays that can be used to create `gdspy.PolygonSet` objects. :download:`fonts.py <_static/fonts.py>` :download:`fonts.gds <_static/fonts.gds>` .. literalinclude:: _static/fonts.py :language: python :linenos: gdspy-1.4.2/docs/index.rst000066400000000000000000000012001354474061200154340ustar00rootroot00000000000000##################### Gdspy's Documentation ##################### .. automodule:: gdspy For installation instructions and other information, please check out the `GitHub `_ repository or the README file included with the source. .. toctree:: :maxdepth: 3 :caption: Table of Contents gettingstarted reference ******* Support ******* Help support the development of gdspy by donating: .. image:: https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif :alt: PayPal :target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JD2EUE2WPPBQQ :align: center gdspy-1.4.2/docs/makeimages.py000066400000000000000000000363441354474061200162710ustar00rootroot00000000000000import gdspy import numpy import colorsys from PIL import Image, ImageDraw, ImageFont class ColorDict(dict): def __missing__(self, key): layer, datatype = key rgb = tuple( int(255 * c + 0.5) for c in colorsys.hsv_to_rgb( (layer % 3) / 3.0 + (layer % 6 // 3) / 6.0 + (layer // 6) / 11.0, 1 - ((layer + datatype) % 8) / 12.0, 1 - (datatype % 3) / 4.0, ) ) self[key] = rgb return rgb color = ColorDict() def draw(cell, name=None, width=600, height=400, margin=20): global color (ax, ay), (bx, by) = cell.get_bounding_box() ax = min(0, ax) ay = min(0, ay) bx = max(1, bx) by = max(1, by) sx = 3 * width - 2 * margin sy = 3 * height - 2 * margin bx -= ax by -= ay if bx * sy > by * sx: scale = sx / bx sy = int(sx * by / bx + 0.5) else: scale = sy / by sx = int(sy * bx / by + 0.5) ox = margin oy = sy + margin width = int((sx + 2 * margin) / 3 + 0.5) height = int((sy + 2 * margin) / 3 + 0.5) img = Image.new("RGBA", (3 * width, 3 * height), (0, 0, 0, 0)) for key, polys in cell.get_polygons(by_spec=True).items(): lc = color[key] fc = color[key] + (128,) for p in polys: p[:, 0] = ox + scale * (p[:, 0] - ax) p[:, 1] = oy - scale * (p[:, 1] - ay) pts = list(p.flatten()) tmp = Image.new("RGBA", img.size, (0, 0, 0, 0)) dr = ImageDraw.Draw(tmp) dr.polygon(pts, fill=fc, outline=lc) img = Image.alpha_composite(img, tmp) z = ox - scale * ax, oy + scale * ay p = ox + scale * (1 - ax), oy - scale * (1 - ay) # n = ox + scale * (-1 - ax), oy - scale * (-1 - ay) dr = ImageDraw.Draw(img) dr.line([z[0], z[1], p[0], z[1]], fill=(0, 0, 0, 255), width=3) dr.line([z[0], z[1], z[0], p[1]], fill=(0, 0, 0, 255), width=3) labels = cell.get_labels() if len(labels) > 0: font = ImageFont.truetype("/user/share/fonts/TTF/DejaVuSans.ttf", 72) for l in labels: x = ox + scale * (l.position[0] - ax) y = oy - scale * (l.position[1] - ay) dr.text((x, y), l.text, font=font, fill=color[(l.layer, l.texttype)]) img = img.resize((width, height), Image.ANTIALIAS) name = "docs/_static/" + (cell.name if name is None else name) + ".png" print("Saving", name) img.save(name) if __name__ == "__main__": # Polygons # Create a polygon from a list of vertices points = [(0, 0), (2, 2), (2, 6), (-6, 6), (-6, -6), (-4, -4), (-4, 4), (0, 4)] poly = gdspy.Polygon(points) draw(gdspy.Cell("polygons").add(poly)) # Holes # Manually connect the hole to the outer boundary cutout = gdspy.Polygon( [(0, 0), (5, 0), (5, 5), (0, 5), (0, 0), (2, 2), (2, 3), (3, 3), (3, 2), (2, 2)] ) draw(gdspy.Cell("holes").add(cutout)) # Circles # Circle centered at (0, 0), with radius 2 and tolerance 0.1 circle = gdspy.Round((0, 0), 2, tolerance=0.01) # To create an ellipse, simply pass a list with 2 radii. # Because the tolerance is small (resulting a large number of # vertices), the ellipse is fractured in 2 polygons. ellipse = gdspy.Round((4, 0), [1, 2], tolerance=1e-4) # Circular arc example arc = gdspy.Round( (2, 4), 2, inner_radius=1, initial_angle=-0.2 * numpy.pi, final_angle=1.2 * numpy.pi, tolerance=0.01, ) draw(gdspy.Cell("circles").add([circle, ellipse, arc])) # Curves # Construct a curve made of a sequence of line segments c1 = gdspy.Curve(0, 0).L(1, 0, 2, 1, 2, 2, 0, 2) p1 = gdspy.Polygon(c1.get_points()) # Construct another curve using relative coordinates c2 = gdspy.Curve(3, 1).l(1, 0, 2, 1, 2, 2, 0, 2) p2 = gdspy.Polygon(c2.get_points()) draw(gdspy.Cell("curves").add([p1, p2])) # Curves 1 # Use complex numbers to facilitate writing polar coordinates c3 = gdspy.Curve(0, 2).l(4 * numpy.exp(1j * numpy.pi / 6)) # Elliptical arcs have syntax similar to gdspy.Round c3.arc((4, 2), 0.5 * numpy.pi, -0.5 * numpy.pi) p3 = gdspy.Polygon(c3.get_points()) draw(gdspy.Cell("curves_1").add(p3)) # Curves 2 # Cubic Bezier curves can be easily created with C and c c4 = gdspy.Curve(0, 0).c(1, 0, 1, 1, 2, 1) # Smooth continuation with S or s c4.s(1, 1, 0, 1).S(numpy.exp(1j * numpy.pi / 6), 0, 0) p4 = gdspy.Polygon(c4.get_points()) # Similarly for quadratic Bezier curves c5 = gdspy.Curve(5, 3).Q(3, 2, 3, 0, 5, 0, 4.5, 1).T(5, 3) p5 = gdspy.Polygon(c5.get_points()) # Smooth interpolating curves can be built using I or i, including # closed shapes c6 = gdspy.Curve(0, 3).i([(1, 0), (2, 0), (1, -1)], cycle=True) p6 = gdspy.Polygon(c6.get_points()) draw(gdspy.Cell("curves_2").add([p4, p5, p6])) # Transformations poly = gdspy.Rectangle((-2, -2), (2, 2)) poly.rotate(numpy.pi / 4) poly.scale(1, 0.5) draw(gdspy.Cell("transformations").add(poly)) # Layer and Datatype # Layer/datatype definitions for each step in the fabrication ld_fulletch = {"layer": 1, "datatype": 3} ld_partetch = {"layer": 2, "datatype": 3} ld_liftoff = {"layer": 0, "datatype": 7} p1 = gdspy.Rectangle((-3, -3), (3, 3), **ld_fulletch) p2 = gdspy.Rectangle((-5, -3), (-3, 3), **ld_partetch) p3 = gdspy.Rectangle((5, -3), (3, 3), **ld_partetch) p4 = gdspy.Round((0, 0), 2.5, number_of_points=6, **ld_liftoff) draw(gdspy.Cell("layer_and_datatype").add([p1, p2, p3, p4])) # References # Create a cell with a component that is used repeatedly contact = gdspy.Cell("CONTACT") contact.add([p1, p2, p3, p4]) # Create a cell with the complete device device = gdspy.Cell("DEVICE") device.add(cutout) # Add 2 references to the component changing size and orientation ref1 = gdspy.CellReference(contact, (3.5, 1), magnification=0.25) ref2 = gdspy.CellReference(contact, (1, 3.5), magnification=0.25, rotation=90) device.add([ref1, ref2]) # The final layout has several repetitions of the complete device main = gdspy.Cell("MAIN") main.add(gdspy.CellArray(device, 3, 2, (6, 7))) draw(main, "references") # Polygonal-Only Paths # Start a path at (0, 0) with width 1 path1 = gdspy.Path(1, (0, 0)) # Add a segment to the path goin in the '+y' direction path1.segment(4, "+y") # Further segments or turns will folow the current path direction # to ensure continuity path1.turn(2, "r") path1.segment(1) path1.turn(3, "rr") draw(gdspy.Cell("polygonal-only_paths").add(path1)) # Polygonal-Only Paths 1 path2 = gdspy.Path(0.5, (0, 0)) # Start the path with a smooth Bezier S-curve path2.bezier([(0, 5), (5, 5), (5, 10)]) # We want to add a spiral curve to the path. The spiral is defined # as a parametric curve. We make sure spiral(0) = (0, 0) so that # the path is continuous. def spiral(u): r = 4 - 3 * u theta = 5 * u * numpy.pi x = r * numpy.cos(theta) - 4 y = r * numpy.sin(theta) return (x, y) # It is recommended to also define the derivative of the parametric # curve, otherwise this derivative must be calculated nummerically. # The derivative is used to define the side boundaries of the path, # so, in this case, to ensure continuity with the existing S-curve, # we make sure the the direction at the start of the spiral is # pointing exactly upwards, as if is radius were constant. # Additionally, the exact magnitude of the derivative is not # important; gdspy only uses its direction. def dspiral_dt(u): theta = 5 * u * numpy.pi dx_dt = -numpy.sin(theta) dy_dt = numpy.cos(theta) return (dx_dt, dy_dt) # Add the parametric spiral to the path path2.parametric(spiral, dspiral_dt) draw(gdspy.Cell("polygonal-only_paths_1").add(path2)) # Polygonal-Only Paths 2 # Start 3 parallel paths with center-to-center distance of 1.5 path3 = gdspy.Path(0.1, (-5.5, 3), number_of_paths=3, distance=1.5) # Add a segment tapering the widths up to 0.5 path3.segment(2, "-y", final_width=0.5) # Add a bezier curve decreasing the distance between paths to 0.75 path3.bezier([(0, -2), (1, -3), (3, -3)], final_distance=0.75) # Add a parametric section to modulate the width with a sinusoidal # shape. Note that the algorithm that determines the number of # evaluations of the parametric curve does not take the width into # consideration, so we have to manually increase this parameter. path3.parametric( lambda u: (5 * u, 0), lambda u: (1, 0), final_width=lambda u: 0.4 + 0.1 * numpy.cos(10 * numpy.pi * u), number_of_evaluations=256, ) # Add a circular turn and a final tapering segment. path3.turn(3, "l") path3.segment(2, final_width=1, final_distance=1.5) draw(gdspy.Cell("polygonal-only_paths_2").add(path3)) # Flexible Paths # Path defined by a sequence of points and stored as a GDSII path sp1 = gdspy.FlexPath( [(0, 0), (3, 0), (3, 2), (5, 3), (3, 4), (0, 4)], 1, gdsii_path=True ) # Other construction methods can still be used sp1.smooth([(0, 2), (2, 2), (4, 3), (5, 1)], relative=True) # Multiple parallel paths separated by 0.5 with different widths, # end caps, and joins. Because of the join specification, they # cannot be stared as GDSII paths, only as polygons. sp2 = gdspy.FlexPath( [(12, 0), (8, 0), (8, 3), (10, 2)], [0.3, 0.2, 0.4], 0.5, ends=["extended", "flush", "round"], corners=["bevel", "miter", "round"], ) sp2.arc(2, -0.5 * numpy.pi, 0.5 * numpy.pi) sp2.arc(1, 0.5 * numpy.pi, 1.5 * numpy.pi) draw(gdspy.Cell("flexible_paths").add([sp1, sp2])) # Flexible Paths 1 # Path corners and end caps can be custom functions. # This corner function creates 'broken' joins. def broken(p0, v0, p1, v1, p2, w): # Calculate intersection point p between lines defined by # p0 + u0 * v0 (for all u0) and p1 + u1 * v1 (for all u1) den = v1[1] * v0[0] - v1[0] * v0[1] lim = 1e-12 * (v0[0] ** 2 + v0[1] ** 2) * (v1[0] ** 2 + v1[1] ** 2) if den ** 2 < lim: # Lines are parallel: use mid-point u0 = u1 = 0 p = 0.5 * (p0 + p1) else: dx = p1[0] - p0[0] dy = p1[1] - p0[1] u0 = (v1[1] * dx - v1[0] * dy) / den u1 = (v0[1] * dx - v0[0] * dy) / den p = 0.5 * (p0 + v0 * u0 + p1 + v1 * u1) if u0 <= 0 and u1 >= 0: # Inner corner return [p] # Outer corner return [p0, p2, p1] # This end cap function creates pointy caps. def pointy(p0, v0, p1, v1): r = 0.5 * numpy.sqrt(numpy.sum((p0 - p1) ** 2)) v0 /= numpy.sqrt(numpy.sum(v0 ** 2)) v1 /= numpy.sqrt(numpy.sum(v1 ** 2)) return [p0, 0.5 * (p0 + p1) + 0.5 * (v0 - v1) * r, p1] # Paths with arbitrary offsets from the center and multiple layers. sp3 = gdspy.FlexPath( [(0, 0), (0, 1)], [0.1, 0.3, 0.5], offset=[-0.2, 0, 0.4], layer=[0, 1, 2], corners=broken, ends=pointy, ) sp3.segment((3, 3), offset=[-0.5, -0.1, 0.5]) sp3.segment((4, 1), width=[0.2, 0.2, 0.2], offset=[-0.2, 0, 0.2]) sp3.segment((0, -1), relative=True) draw(gdspy.Cell("flexible_paths_1").add(sp3)) # Flexible Paths 2 # Path created with automatic bends of radius 5 points = [(0, 0), (0, 10), (20, 0), (18, 15), (8, 15)] sp4 = gdspy.FlexPath( points, 0.5, corners="circular bend", bend_radius=5, gdsii_path=True ) # Same path, generated with natural corners, for comparison sp5 = gdspy.FlexPath(points, 0.5, layer=1, gdsii_path=True) draw(gdspy.Cell("flexible_paths_2").add([sp4, sp5])) # Robust Paths # Create 4 parallel paths in different layers lp = gdspy.RobustPath( (50, 0), [2, 0.5, 1, 1], [0, 0, -1, 1], ends=["extended", "round", "flush", "flush"], layer=[0, 2, 1, 1], ) lp.segment((45, 0)) lp.segment( (5, 0), width=[lambda u: 2 + 16 * u * (1 - u), 0.5, 1, 1], offset=[ 0, lambda u: 8 * u * (1 - u) * numpy.cos(12 * numpy.pi * u), lambda u: -1 - 8 * u * (1 - u), lambda u: 1 + 8 * u * (1 - u), ], ) lp.segment((0, 0)) lp.smooth( [(5, 10)], angles=[0.5 * numpy.pi, 0], width=0.5, offset=[-0.25, 0.25, -0.75, 0.75], ) lp.parametric( lambda u: numpy.array((45 * u, 4 * numpy.sin(6 * numpy.pi * u))), offset=[ lambda u: -0.25 * numpy.cos(24 * numpy.pi * u), lambda u: 0.25 * numpy.cos(24 * numpy.pi * u), -0.75, 0.75, ], ) draw(gdspy.Cell("robust_paths").add(lp)) # Boolean Operations # Create some text text = gdspy.Text("GDSPY", 4, (0, 0)) # Create a rectangle extending the text's bounding box by 1 bb = numpy.array(text.get_bounding_box()) rect = gdspy.Rectangle(bb[0] - 1, bb[1] + 1) # Subtract the text from the rectangle inv = gdspy.boolean(rect, text, "not") draw(gdspy.Cell("boolean_operations").add(inv)) # Slice Operation ring1 = gdspy.Round((-6, 0), 6, inner_radius=4) ring2 = gdspy.Round((0, 0), 6, inner_radius=4) ring3 = gdspy.Round((6, 0), 6, inner_radius=4) # Slice the first ring across x=-3, the second ring across x=-3 # and x=3, and the third ring across x=3 slices1 = gdspy.slice(ring1, -3, axis=0) slices2 = gdspy.slice(ring2, [-3, 3], axis=0) slices3 = gdspy.slice(ring3, 3, axis=0) slices = gdspy.Cell("SLICES") # Keep only the left side of slices1, the center part of slices2 # and the right side of slices3 slices.add(slices1[0]) slices.add(slices2[1]) slices.add(slices3[1]) draw(slices, "slice_operation") # Offset Operation rect1 = gdspy.Rectangle((-4, -4), (1, 1)) rect2 = gdspy.Rectangle((-1, -1), (4, 4)) # Offset both polygons # Because we join them first, a single polygon is created. outer = gdspy.offset([rect1, rect2], 0.5, join_first=True, layer=1) draw(gdspy.Cell("offset_operation").add([outer, rect1, rect2])) # Fillet Operation multi_path = gdspy.Path(2, (-3, -2)) multi_path.segment(4, "+x") multi_path.turn(2, "l").turn(2, "r") multi_path.segment(4) # Create a copy with joined polygons and no fracturing joined = gdspy.boolean(multi_path, None, "or", max_points=0) joined.translate(0, -5) # Fillet applied to each polygon in the path multi_path.fillet(0.5) # Fillet applied to the joined copy joined.fillet(0.5) draw(gdspy.Cell("fillet_operation").add([joined, multi_path])) # Text # Label anchored at (1, 3) by its north-west corner label = gdspy.Label("Sample label", (1, 3), "nw") # Horizontal text with height 2.25 htext = gdspy.Text("12345", 2.25, (0.25, 6)) # Vertical text with height 1.5 vtext = gdspy.Text("ABC", 1.5, (10.5, 4), horizontal=False) rect = gdspy.Rectangle((0, 0), (10, 6), layer=10) draw(gdspy.Cell("text").add([htext, vtext, label, rect])) gdspy-1.4.2/docs/reference.rst000066400000000000000000000051571354474061200163020ustar00rootroot00000000000000############# API Reference ############# ********************* Geometry Construction ********************* PolygonSet ========== .. autoclass:: gdspy.PolygonSet :members: :inherited-members: :show-inheritance: Polygon ======= .. autoclass:: gdspy.Polygon :members: :inherited-members: :show-inheritance: Rectangle ========= .. autoclass:: gdspy.Rectangle :members: :inherited-members: :show-inheritance: Round ===== .. autoclass:: gdspy.Round :members: :inherited-members: :show-inheritance: Text ==== .. autoclass:: gdspy.Text :members: :inherited-members: :show-inheritance: Path ==== .. autoclass:: gdspy.Path :members: :inherited-members: :show-inheritance: PolyPath ======== .. autoclass:: gdspy.PolyPath :members: :inherited-members: :show-inheritance: L1Path ====== .. autoclass:: gdspy.L1Path :members: :inherited-members: :show-inheritance: FlexPath ======== .. autoclass:: gdspy.FlexPath :members: :inherited-members: :show-inheritance: RobustPath ========== .. autoclass:: gdspy.RobustPath :members: :inherited-members: :show-inheritance: Curve ===== .. autoclass:: gdspy.Curve :members: :inherited-members: :show-inheritance: Label ===== .. autoclass:: gdspy.Label :members: :inherited-members: :show-inheritance: boolean ======= .. autofunction:: gdspy.boolean offset ====== .. autofunction:: gdspy.offset slice ===== .. autofunction:: gdspy.slice inside ====== .. autofunction:: gdspy.inside copy ==== .. autofunction:: gdspy.copy ************* GDSII Library ************* Cell ==== .. autoclass:: gdspy.Cell :members: :inherited-members: :show-inheritance: CellReference ============= .. autoclass:: gdspy.CellReference :members: :inherited-members: :show-inheritance: CellArray ========= .. autoclass:: gdspy.CellArray :members: :inherited-members: :show-inheritance: GdsLibrary ========== .. autoclass:: gdspy.GdsLibrary :members: :inherited-members: :show-inheritance: GdsWriter ========= .. autoclass:: gdspy.GdsWriter :members: :inherited-members: :show-inheritance: LayoutViewer ============ .. autoclass:: gdspy.LayoutViewer :members: :no-inherited-members: :no-show-inheritance: write_gds ========= .. autofunction:: gdspy.write_gds gdsii_hash ========== .. autofunction:: gdspy.gdsii_hash get_binary_cells ================ .. autofunction:: gdspy.get_binary_cells get_gds_units ============= .. autofunction:: gdspy.get_gds_units current_library =============== .. autodata:: gdspy.current_library gdspy-1.4.2/gdspy/000077500000000000000000000000001354474061200140005ustar00rootroot00000000000000gdspy-1.4.2/gdspy/__init__.py000066400000000000000000012317631354474061200161260ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### """ gdspy is a Python module that allows the creation of GDSII stream files. Most features of the GDSII format are implemented, including support for polygons with any number of vertices. GDSII format references: - http://boolean.klaasholwerda.nl/interface/bnf/gdsformat.html - http://www.artwork.com/gdsii/gdsii/ - http://www.buchanan1.net/stream_description.html """ from __future__ import division from __future__ import unicode_literals from __future__ import print_function from __future__ import absolute_import import sys if sys.version_info.major < 3: from builtins import zip from builtins import open from builtins import int from builtins import round from builtins import range from builtins import super from future import standard_library standard_library.install_aliases() else: # Python 3 doesn't have basestring, as unicode is type string # Python 2 doesn't equate unicode to string, but both are basestring # Now isinstance(s, basestring) will be True for any python version basestring = str import struct import datetime import warnings import itertools import numpy import copy as libcopy import hashlib from gdspy import clipper try: from gdspy.viewer import LayoutViewer except ImportError as e: warnings.warn( "[GDSPY] LayoutViewer not available: " + str(e), category=ImportWarning, stacklevel=2, ) __version__ = "1.4.2" _halfpi = 0.5 * numpy.pi _zero = numpy.array((0.0, 0.0)) _one = numpy.array((1.0, 1.0)) _mpone = numpy.array((-1.0, 1.0)) _pmone = numpy.array((1.0, -1.0)) _pmone_int = numpy.array((1, -1)) _directions_dict = {"+x": 0, "+y": 0.5, "-x": 1, "-y": -0.5} _directions_list = ["+x", "+y", "-x", "-y"] _angle_dic = {"l": _halfpi, "r": -_halfpi, "ll": numpy.pi, "rr": -numpy.pi} _bounding_boxes = {} def _record_reader(stream): """ Iterator over complete records from a GDSII stream file. Parameters ---------- stream : file GDSII stream file to be read. Returns ------- out : list Record type and data (as a numpy.array, string or None) """ while True: header = stream.read(4) if len(header) < 4: return size, rec_type = struct.unpack(">HH", header) data_type = rec_type & 0x00FF rec_type = rec_type // 256 data = None if size > 4: if data_type == 0x01: data = numpy.array( struct.unpack( ">{0}H".format((size - 4) // 2), stream.read(size - 4) ), dtype="uint", ) elif data_type == 0x02: data = numpy.array( struct.unpack( ">{0}h".format((size - 4) // 2), stream.read(size - 4) ), dtype="int", ) elif data_type == 0x03: data = numpy.array( struct.unpack( ">{0}l".format((size - 4) // 4), stream.read(size - 4) ), dtype="int", ) elif data_type == 0x05: data = numpy.array( [ _eight_byte_real_to_float(stream.read(8)) for _ in range((size - 4) // 8) ] ) else: data = stream.read(size - 4) if str is not bytes: if data[-1] == 0: data = data[:-1].decode("ascii") else: data = data.decode("ascii") elif data[-1] == "\0": data = data[:-1] yield [rec_type, data] def _raw_record_reader(stream): """ Iterator over complete records from a GDSII stream file. Parameters ---------- stream : file GDSII stream file to be read. Returns ------- out : 2-tuple Record type and binary data (including header) """ while True: header = stream.read(4) if len(header) < 4: return size, rec_type = struct.unpack(">HH", header) rec_type = rec_type // 256 yield (rec_type, header + stream.read(size - 4)) def _eight_byte_real(value): """ Convert a number into the GDSII 8 byte real format. Parameters ---------- value : number The number to be converted. Returns ------- out : string The GDSII binary string that represents `value`. """ if value == 0: return b"\x00\x00\x00\x00\x00\x00\x00\x00" if value < 0: byte1 = 0x80 value = -value else: byte1 = 0x00 fexp = numpy.log2(value) / 4 exponent = int(numpy.ceil(fexp)) if fexp == exponent: exponent += 1 mantissa = int(value * 16.0 ** (14 - exponent)) byte1 += exponent + 64 byte2 = mantissa // 281474976710656 short3 = (mantissa % 281474976710656) // 4294967296 long4 = mantissa % 4294967296 return struct.pack(">HHL", byte1 * 256 + byte2, short3, long4) def _eight_byte_real_to_float(value): """ Convert a number from GDSII 8 byte real format to float. Parameters ---------- value : string The GDSII binary string representation of the number. Returns ------- out : float The number represented by `value`. """ short1, short2, long3 = struct.unpack(">HHL", value) exponent = (short1 & 0x7F00) // 256 - 64 mantissa = ( ((short1 & 0x00FF) * 65536 + short2) * 4294967296 + long3 ) / 72057594037927936.0 if short1 & 0x8000: return -mantissa * 16.0 ** exponent return mantissa * 16.0 ** exponent def _hobby(points, angles=None, curl_start=1, curl_end=1, t_in=1, t_out=1, cycle=False): """ Calculate control points for a smooth interpolating curve. Uses the Hobby algorithm [1]_ to calculate a smooth interpolating curve made of cubic Bezier segments between each pair of points. Parameters ---------- points : Numpy array[N, 2] Vertices in the interpolating curve. angles : array-like[N] or None Tangent angles at each point (in *radians*). Any angles defined as None are automatically calculated. curl_start : number Ratio between the mock curvatures at the first point and at its neighbor. A value of 1 renders the first segment a good approximation for a circular arc. A value of 0 will better approximate a straight segment. It has no effect for closed curves or when an angle is defined for the first point. curl_end : number Ratio between the mock curvatures at the last point and at its neighbor. It has no effect for closed curves or when an angle is defined for the last point. t_in : number or array-like[N] Tension parameter when arriving at each point. One value per point or a single value used for all points. t_out : number or array-like[N] Tension parameter when leaving each point. One value per point or a single value used for all points. cycle : bool If True, calculates control points for a closed curve, with an additional segment connecting the first and last points. Returns ------- out : 2-tuple of Numpy array[M, 2] Pair of control points for each segment in the interpolating curve. For a closed curve (`cycle` True), M = N. For an open curve (`cycle` False), M = N - 1. References ---------- .. [1] Hobby, J.D. *Discrete Comput. Geom.* (1986) 1: 123. `DOI: 10.1007/BF02187690 `_ """ z = points[:, 0] + 1j * points[:, 1] n = z.size if numpy.isscalar(t_in): t_in = t_in * numpy.ones(n) else: t_in = numpy.array(t_in) if numpy.isscalar(t_out): t_out = t_out * numpy.ones(n) else: t_out = numpy.array(t_out) if angles is None: angles = [None] * n rotate = 0 if cycle and any(a is not None for a in angles): while angles[rotate] is None: rotate += 1 angles = [angles[(rotate + j) % n] for j in range(n + 1)] z = numpy.hstack((numpy.roll(z, -rotate), z[rotate : rotate + 1])) t_in = numpy.hstack((numpy.roll(t_in, -rotate), t_in[rotate : rotate + 1])) t_out = numpy.hstack((numpy.roll(t_out, -rotate), t_out[rotate : rotate + 1])) cycle = False if cycle: # Closed curve v = numpy.roll(z, -1) - z d = numpy.abs(v) delta = numpy.angle(v) psi = (delta - numpy.roll(delta, 1) + numpy.pi) % (2 * numpy.pi) - numpy.pi coef = numpy.zeros(2 * n) coef[:n] = -psi m = numpy.zeros((2 * n, 2 * n)) i = numpy.arange(n) i1 = (i + 1) % n i2 = (i + 2) % n ni = n + i m[i, i] = 1 m[i, n + (i - 1) % n] = 1 # A_i m[ni, i] = d[i1] * t_in[i2] * t_in[i1] ** 2 # B_{i+1} m[ni, i1] = -d[i] * t_out[i] * t_out[i1] ** 2 * (1 - 3 * t_in[i2]) # C_{i+1} m[ni, ni] = d[i1] * t_in[i2] * t_in[i1] ** 2 * (1 - 3 * t_out[i]) # D_{i+2} m[ni, n + i1] = -d[i] * t_out[i] * t_out[i1] ** 2 sol = numpy.linalg.solve(m, coef) theta = sol[:n] phi = sol[n:] w = numpy.exp(1j * (theta + delta)) a = 2 ** 0.5 b = 1.0 / 16 c = (3 - 5 ** 0.5) / 2 sintheta = numpy.sin(theta) costheta = numpy.cos(theta) sinphi = numpy.sin(phi) cosphi = numpy.cos(phi) alpha = ( a * (sintheta - b * sinphi) * (sinphi - b * sintheta) * (costheta - cosphi) ) cta = z + w * d * ((2 + alpha) / (1 + (1 - c) * costheta + c * cosphi)) / ( 3 * t_out ) ctb = numpy.roll(z, -1) - numpy.roll(w, -1) * d * ( (2 - alpha) / (1 + (1 - c) * cosphi + c * costheta) ) / (3 * numpy.roll(t_in, -1)) else: # Open curve(s) n = z.size - 1 v = z[1:] - z[:-1] d = numpy.abs(v) delta = numpy.angle(v) psi = (delta[1:] - delta[:-1] + numpy.pi) % (2 * numpy.pi) - numpy.pi theta = numpy.empty(n) phi = numpy.empty(n) i = 0 if angles[0] is not None: theta[0] = angles[0] - delta[0] while i < n: j = i + 1 while j < n + 1 and angles[j] is None: j += 1 if j == n + 1: j -= 1 else: phi[j - 1] = delta[j - 1] - angles[j] if j < n: theta[j] = angles[j] - delta[j] # Solve open curve z_i, ..., z_j nn = j - i coef = numpy.zeros(2 * nn) coef[1:nn] = -psi[i : j - 1] m = numpy.zeros((2 * nn, 2 * nn)) if nn > 1: ii = numpy.arange(nn - 1) # [0 .. nn-2] i0 = i + ii # [i .. j-1] i1 = 1 + i0 # [i+1 .. j] i2 = 2 + i0 # [i+2 .. j+1] ni = nn + ii # [nn .. 2*nn-2] ii1 = 1 + ii # [1 .. nn-1] m[ii1, ii1] = 1 m[ii1, ni] = 1 # A_ii m[ni, ii] = d[i1] * t_in[i2] * t_in[i1] ** 2 # B_{ii+1} m[ni, ii1] = -d[i0] * t_out[i0] * t_out[i1] ** 2 * (1 - 3 * t_in[i2]) # C_{ii+1} m[ni, ni] = d[i1] * t_in[i2] * t_in[i1] ** 2 * (1 - 3 * t_out[i0]) # D_{ii+2} m[ni, ni + 1] = -d[i0] * t_out[i0] * t_out[i1] ** 2 if angles[i] is None: to3 = t_out[0] ** 3 cti3 = curl_start * t_in[1] ** 3 # B_0 m[0, 0] = to3 * (1 - 3 * t_in[1]) - cti3 # D_1 m[0, nn] = to3 - cti3 * (1 - 3 * t_out[0]) else: coef[0] = theta[i] m[0, 0] = 1 m[0, nn] = 0 if angles[j] is None: ti3 = t_in[n] ** 3 cto3 = curl_end * t_out[n - 1] ** 3 # A_{nn-1} m[2 * nn - 1, nn - 1] = ti3 - cto3 * (1 - 3 * t_in[n]) # C_nn m[2 * nn - 1, 2 * nn - 1] = ti3 * (1 - 3 * t_out[n - 1]) - cto3 else: coef[2 * nn - 1] = phi[j - 1] m[2 * nn - 1, nn - 1] = 0 m[2 * nn - 1, 2 * nn - 1] = 1 if nn > 1 or angles[i] is None or angles[j] is None: sol = numpy.linalg.solve(m, coef) theta[i:j] = sol[:nn] phi[i:j] = sol[nn:] i = j w = numpy.hstack( (numpy.exp(1j * (delta + theta)), numpy.exp(1j * (delta[-1:] - phi[-1:]))) ) a = 2 ** 0.5 b = 1.0 / 16 c = (3 - 5 ** 0.5) / 2 sintheta = numpy.sin(theta) costheta = numpy.cos(theta) sinphi = numpy.sin(phi) cosphi = numpy.cos(phi) alpha = ( a * (sintheta - b * sinphi) * (sinphi - b * sintheta) * (costheta - cosphi) ) cta = z[:-1] + w[:-1] * d * ( (2 + alpha) / (1 + (1 - c) * costheta + c * cosphi) ) / (3 * t_out[:-1]) ctb = z[1:] - w[1:] * d * ( (2 - alpha) / (1 + (1 - c) * cosphi + c * costheta) ) / (3 * t_in[1:]) if rotate > 0: cta = numpy.roll(cta, rotate) ctb = numpy.roll(ctb, rotate) return ( numpy.vstack((cta.real, cta.imag)).transpose(), numpy.vstack((ctb.real, ctb.imag)).transpose(), ) def _func_const(c, nargs=1): if nargs == 1: return lambda u: c elif nargs == 2: return lambda u, h: c return lambda *args: c def _func_linear(c0, c1): return lambda u: c0 * (1 - u) + c1 * u def _func_multadd(f, m=None, a=None, nargs=1): if nargs == 1: if m is None: return lambda u: f(u) + a if a is None: return lambda u: f(u) * m return lambda u: f(u) * m + a elif nargs == 2: if m is None: return lambda u, h: f(u, h) + a if a is None: return lambda u, h: f(u, h) * m return lambda u, h: f(u, h) * m + a if m is None: return lambda *args: f(*args) + a if a is None: return lambda *args: f(*args) * m return lambda *args: f(*args) * m + a def _func_rotate(f, cos, sin, center=0, nargs=1): if nargs == 1: def _f(u): x = f(u) - center return x * cos + x[::-1] * sin + center elif nargs == 2: def _f(u, h): x = f(u, h) - center return x * cos + x[::-1] * sin + center return _f def _func_trafo(f, translation, rotation, scale, x_reflection, array_trans, nargs=1): if translation is None: translation = _zero if array_trans is None: array_trans = _zero if rotation is None: cos = _one sin = _zero else: cos = numpy.cos(rotation) * _one sin = numpy.sin(rotation) * _mpone if scale is not None: cos = cos * scale sin = sin * scale array_trans = array_trans / scale if x_reflection: cos[1] = -cos[1] sin[0] = -sin[0] if nargs == 1: def _f(u): x = f(u) + array_trans return x * cos + x[::-1] * sin + translation elif nargs == 2: def _f(u, h): x = f(u, h) + array_trans return x * cos + x[::-1] * sin + translation return _f def _func_bezier(ctrl, nargs=1): if nargs == 1: def _f(u): p = ctrl for _ in range(ctrl.shape[0] - 1): p = p[1:] * u + p[:-1] * (1 - u) return p[0] elif nargs == 2: def _f(u, h): p = ctrl for _ in range(ctrl.shape[0] - 1): p = p[1:] * u + p[:-1] * (1 - u) return p[0] return _f def _gather_polys(args): """ Gather polygons from different argument types into a list. Parameters ---------- args : None, `PolygonSet`, `CellReference`, `CellArray` or iterable Polygon types. If this is an iterable, each element must be a `PolygonSet`, `CellReference`, `CellArray`, or an array-like[N][2] of vertices of a polygon. Returns ------- out : list of numpy array[N][2] List of polygons. """ if args is None: return [] if isinstance(args, PolygonSet): return [p for p in args.polygons] if ( isinstance(args, RobustPath) or isinstance(args, FlexPath) or isinstance(args, CellReference) or isinstance(args, CellArray) ): return args.get_polygons() polys = [] for p in args: if isinstance(p, PolygonSet): polys.extend(p.polygons) elif ( isinstance(p, RobustPath) or isinstance(args, FlexPath) or isinstance(p, CellReference) or isinstance(p, CellArray) ): polys.extend(p.get_polygons()) else: polys.append(p) return polys class PolygonSet(object): """ Set of polygonal objects. Parameters ---------- polygons : iterable of array-like[N][2] List containing the coordinates of the vertices of each polygon. layer : integer The GDSII layer number for this element. datatype : integer The GDSII datatype for this element (between 0 and 255). Attributes ---------- polygons : list of numpy array[N][2] Coordinates of the vertices of each polygon. layers : list of integer The GDSII layer number for each element. datatypes : list of integer The GDSII datatype for each element (between 0 and 255). Notes ----- The last point should not be equal to the first (polygons are automatically closed). The original GDSII specification supports only a maximum of 199 vertices per polygon. """ __slots__ = "layers", "datatypes", "polygons" def __init__(self, polygons, layer=0, datatype=0): self.polygons = [numpy.array(p) for p in polygons] self.layers = [layer] * len(self.polygons) self.datatypes = [datatype] * len(self.polygons) def __str__(self): return ( "PolygonSet ({} polygons, {} vertices, layers {}, datatypes {})" ).format( len(self.polygons), sum([len(p) for p in self.polygons]), list(set(self.layers)), list(set(self.datatypes)), ) def get_bounding_box(self): """ Calculate the bounding box of the polygons. Returns ------- out : Numpy array[2, 2] or None Bounding box of this polygon in the form [[x_min, y_min], [x_max, y_max]], or None if the polygon is empty. """ if len(self.polygons) == 0: return None return numpy.array( ( ( min(pts[:, 0].min() for pts in self.polygons), min(pts[:, 1].min() for pts in self.polygons), ), ( max(pts[:, 0].max() for pts in self.polygons), max(pts[:, 1].max() for pts in self.polygons), ), ) ) def rotate(self, angle, center=(0, 0)): """ Rotate this object. Parameters ---------- angle : number The angle of rotation (in *radians*). center : array-like[2] Center point for the rotation. Returns ------- out : `PolygonSet` This object. """ ca = numpy.cos(angle) sa = numpy.sin(angle) * _mpone c0 = numpy.array(center) new_polys = [] for points in self.polygons: pts = points - c0 new_polys.append(pts * ca + pts[:, ::-1] * sa + c0) self.polygons = new_polys return self def scale(self, scalex, scaley=None, center=(0, 0)): """ Scale this object. Parameters ---------- scalex : number Scaling factor along the first axis. scaley : number or None Scaling factor along the second axis. If None, same as `scalex`. center : array-like[2] Center point for the scaling operation. Returns ------- out : `PolygonSet` This object. """ c0 = numpy.array(center) s = scalex if scaley is None else numpy.array((scalex, scaley)) self.polygons = [(points - c0) * s + c0 for points in self.polygons] return self def to_gds(self, multiplier): """ Convert this object to a series of GDSII elements. Parameters ---------- multiplier : number A number that multiplies all dimensions written in the GDSII elements. Returns ------- out : string The GDSII binary string that represents this object. """ data = [] for ii in range(len(self.polygons)): if len(self.polygons[ii]) > 8190: warnings.warn( "[GDSPY] Polygons with more than 8190 are not supported by the official GDSII specification. This GDSII file might not be compatible with all readers.", stacklevel=4, ) data.append( struct.pack( ">4Hh2Hh", 4, 0x0800, 6, 0x0D02, self.layers[ii], 6, 0x0E02, self.datatypes[ii], ) ) xy = numpy.empty((self.polygons[ii].shape[0] + 1, 2), dtype=">i4") xy[:-1] = numpy.round(self.polygons[ii] * multiplier) xy[-1] = xy[0] i0 = 0 while i0 < xy.shape[0]: i1 = min(i0 + 8190, xy.shape[0]) data.append(struct.pack(">2H", 4 + 8 * (i1 - i0), 0x1003)) data.append(xy[i0:i1].tostring()) i0 = i1 else: data.append( struct.pack( ">4Hh2Hh2H", 4, 0x0800, 6, 0x0D02, self.layers[ii], 6, 0x0E02, self.datatypes[ii], 12 + 8 * len(self.polygons[ii]), 0x1003, ) ) xy = numpy.round(self.polygons[ii] * multiplier).astype(">i4") data.append(xy.tostring()) data.append(xy[0].tostring()) data.append(struct.pack(">2H", 4, 0x1100)) return b"".join(data) def area(self, by_spec=False): """ Calculate the total area of this polygon set. Parameters ---------- by_spec : bool If True, the return value is a dictionary with ``{(layer, datatype): area}``. Returns ------- out : number, dictionary Area of this object. """ if by_spec: path_area = {} for poly, key in zip(self.polygons, zip(self.layers, self.datatypes)): poly_area = 0 for ii in range(1, len(poly) - 1): poly_area += (poly[0][0] - poly[ii + 1][0]) * ( poly[ii][1] - poly[0][1] ) - (poly[0][1] - poly[ii + 1][1]) * (poly[ii][0] - poly[0][0]) if key in path_area: path_area[key] += 0.5 * abs(poly_area) else: path_area[key] = 0.5 * abs(poly_area) else: path_area = 0 for points in self.polygons: poly_area = 0 for ii in range(1, len(points) - 1): poly_area += (points[0][0] - points[ii + 1][0]) * ( points[ii][1] - points[0][1] ) - (points[0][1] - points[ii + 1][1]) * ( points[ii][0] - points[0][0] ) path_area += 0.5 * abs(poly_area) return path_area def fracture(self, max_points=199, precision=1e-3): """ Slice these polygons in the horizontal and vertical directions so that each resulting piece has at most `max_points`. This operation occurs in place. Parameters ---------- max_points : integer Maximal number of points in each resulting polygon (at least 5 for the fracture to occur). precision : float Desired precision for rounding vertice coordinates. Returns ------- out : `PolygonSet` This object. """ if max_points > 4: ii = 0 while ii < len(self.polygons): if len(self.polygons[ii]) > max_points: pts0 = sorted(self.polygons[ii][:, 0]) pts1 = sorted(self.polygons[ii][:, 1]) ncuts = len(pts0) // max_points if pts0[-1] - pts0[0] > pts1[-1] - pts1[0]: # Vertical cuts cuts = [ pts0[int(i * len(pts0) / (ncuts + 1.0) + 0.5)] for i in range(1, ncuts + 1) ] chopped = clipper._chop( self.polygons[ii], cuts, 0, 1 / precision ) else: # Horizontal cuts cuts = [ pts1[int(i * len(pts1) / (ncuts + 1.0) + 0.5)] for i in range(1, ncuts + 1) ] chopped = clipper._chop( self.polygons[ii], cuts, 1, 1 / precision ) self.polygons.pop(ii) layer = self.layers.pop(ii) datatype = self.datatypes.pop(ii) self.polygons.extend( numpy.array(x) for x in itertools.chain.from_iterable(chopped) ) npols = sum(len(c) for c in chopped) self.layers.extend(layer for _ in range(npols)) self.datatypes.extend(datatype for _ in range(npols)) else: ii += 1 return self def fillet(self, radius, points_per_2pi=128, max_points=199, precision=1e-3): """ Round the corners of these polygons and fractures them into polygons with less vertices if necessary. Parameters ---------- radius : number, array-like Radius of the corners. If number: all corners filleted by that amount. If array: specify fillet radii on a per-polygon basis (length must be equal to the number of polygons in this `PolygonSet`). Each element in the array can be a number (all corners filleted by the same amount) or another array of numbers, one per polygon vertex. Alternatively, the array can be flattened to have one radius per `PolygonSet` vertex. points_per_2pi : integer Number of vertices used to approximate a full circle. The number of vertices in each corner of the polygon will be the fraction of this number corresponding to the angle encompassed by that corner with respect to 2 pi. max_points : integer Maximal number of points in each resulting polygon (at least 5, otherwise the resulting polygon is not fractured). precision : float Desired precision for rounding vertice coordinates in case of fracturing. Returns ------- out : `PolygonSet` This object. """ two_pi = 2 * numpy.pi fracture = False if numpy.isscalar(radius): radii = [[radius] * p.shape[0] for p in self.polygons] else: if len(radius) == len(self.polygons): radii = [] for r, p in zip(radius, self.polygons): if numpy.isscalar(r): radii.append([r] * p.shape[0]) else: if len(r) != p.shape[0]: raise ValueError( "[GDSPY] Wrong length in fillet radius list. Expected lengths are {} or {}; got {}.".format( len(self.polygons), total, len(radius) ) ) radii.append(r) else: total = sum(p.shape[0] for p in self.polygons) if len(radius) != total: raise ValueError( "[GDSPY] Wrong length in fillet radius list. Expected lengths are {} or {}; got {}.".format( len(self.polygons), total, len(radius) ) ) radii = [] n = 0 for p in self.polygons: radii.append(radius[n : n + p.shape[0]]) n += p.shape[0] for jj in range(len(self.polygons)): vec = self.polygons[jj].astype(float) - numpy.roll(self.polygons[jj], 1, 0) length = (vec[:, 0] ** 2 + vec[:, 1] ** 2) ** 0.5 ii = numpy.flatnonzero(length) if len(ii) < len(length): self.polygons[jj] = numpy.array(self.polygons[jj][ii]) radii[jj] = [radii[jj][i] for i in ii] vec = self.polygons[jj].astype(float) - numpy.roll( self.polygons[jj], 1, 0 ) length = (vec[:, 0] ** 2 + vec[:, 1] ** 2) ** 0.5 vec[:, 0] = vec[:, 0] / length vec[:, 1] = vec[:, 1] / length dvec = numpy.roll(vec, -1, 0) - vec norm = (dvec[:, 0] ** 2 + dvec[:, 1] ** 2) ** 0.5 ii = numpy.flatnonzero(norm) dvec[ii, 0] = dvec[ii, 0] / norm[ii] dvec[ii, 1] = dvec[ii, 1] / norm[ii] dot = numpy.roll(vec, -1, 0) * vec theta = numpy.arccos(dot[:, 0] + dot[:, 1]) ct = numpy.cos(theta * 0.5) tt = numpy.tan(theta * 0.5) new_points = [] for ii in range(-1, len(self.polygons[jj]) - 1): if theta[ii] > 1e-6: a0 = -vec[ii] * tt[ii] - dvec[ii] / ct[ii] a0 = numpy.arctan2(a0[1], a0[0]) a1 = vec[ii + 1] * tt[ii] - dvec[ii] / ct[ii] a1 = numpy.arctan2(a1[1], a1[0]) if a1 - a0 > numpy.pi: a1 -= two_pi elif a1 - a0 < -numpy.pi: a1 += two_pi n = max( int(numpy.ceil(abs(a1 - a0) / two_pi * points_per_2pi) + 0.5), 2 ) a = numpy.linspace(a0, a1, n) ll = radii[jj][ii] * tt[ii] if ll > 0.49 * length[ii]: r = 0.49 * length[ii] / tt[ii] ll = 0.49 * length[ii] else: r = radii[jj][ii] if ll > 0.49 * length[ii + 1]: r = 0.49 * length[ii + 1] / tt[ii] new_points.extend( r * dvec[ii] / ct[ii] + self.polygons[jj][ii] + numpy.vstack((r * numpy.cos(a), r * numpy.sin(a))).transpose() ) else: new_points.append(self.polygons[jj][ii]) self.polygons[jj] = numpy.array(new_points) if len(new_points) > max_points: fracture = True if fracture: self.fracture(max_points, precision) return self def translate(self, dx, dy): """ Translate this polygon. Parameters ---------- dx : number Distance to move in the x-direction. dy : number Distance to move in the y-direction. Returns ------- out : `PolygonSet` This object. """ vec = numpy.array((dx, dy)) self.polygons = [points + vec for points in self.polygons] return self def mirror(self, p1, p2=(0, 0)): """ Mirror the polygons over a line through points 1 and 2 Parameters ---------- p1 : array-like[2] first point defining the reflection line p2 : array-like[2] second point defining the reflection line Returns ------- out : `PolygonSet` This object. """ origin = numpy.array(p1) vec = numpy.array(p2) - origin vec_r = vec * (2 / numpy.inner(vec, vec)) self.polygons = [ numpy.outer(numpy.inner(points - origin, vec_r), vec) - points + 2 * origin for points in self.polygons ] return self class Polygon(PolygonSet): """ Polygonal geometric object. Parameters ---------- points : array-like[N][2] Coordinates of the vertices of the polygon. layer : integer The GDSII layer number for this element. datatype : integer The GDSII datatype for this element (between 0 and 255). Notes ----- The last point should not be equal to the first (polygons are automatically closed). The original GDSII specification supports only a maximum of 199 vertices per polygon. Examples -------- >>> triangle_pts = [(0, 40), (15, 40), (10, 50)] >>> triangle = gdspy.Polygon(triangle_pts) >>> myCell.add(triangle) """ __slots__ = "layers", "datatypes", "polygons" def __init__(self, points, layer=0, datatype=0): self.layers = [layer] self.datatypes = [datatype] self.polygons = [numpy.array(points)] def __str__(self): return "Polygon ({} vertices, layer {}, datatype {})".format( len(self.polygons[0]), self.layers[0], self.datatypes[0] ) class Rectangle(PolygonSet): """ Rectangular geometric object. Parameters ---------- point1 : array-like[2] Coordinates of a corner of the rectangle. point2 : array-like[2] Coordinates of the corner of the rectangle opposite to `point1`. layer : integer The GDSII layer number for this element. datatype : integer The GDSII datatype for this element (between 0 and 255). Examples -------- >>> rectangle = gdspy.Rectangle((0, 0), (10, 20)) >>> myCell.add(rectangle) """ __slots__ = "layers", "datatypes", "polygons" def __init__(self, point1, point2, layer=0, datatype=0): self.layers = [layer] self.datatypes = [datatype] self.polygons = [ numpy.array( [ [point1[0], point1[1]], [point1[0], point2[1]], [point2[0], point2[1]], [point2[0], point1[1]], ] ) ] def __str__(self): return ( "Rectangle (({0[0]}, {0[1]}) to ({1[0]}, {1[1]}), layer {2}, datatype {3})" ).format( self.polygons[0][0], self.polygons[0][2], self.layers[0], self.datatypes[0] ) def __repr__(self): return "Rectangle(({0[0]}, {0[1]}), ({1[0]}, {1[1]}), {2}, {3})".format( self.polygons[0][0], self.polygons[0][2], self.layers[0], self.datatypes[0] ) class Round(PolygonSet): """ Circular geometric object. Represent a circle, ellipse, ring or their sections. Parameters ---------- center : array-like[2] Coordinates of the center of the circle/ring. radius : number, array-like[2] Radius of the circle/outer radius of the ring. To build an ellipse an array of 2 numbers can be used, representing the radii in the horizontal and vertical directions. inner_radius : number, array-like[2] Inner radius of the ring. To build an elliptical hole, an array of 2 numbers can be used, representing the radii in the horizontal and vertical directions. initial_angle : number Initial angle of the circular/ring section (in *radians*). final_angle : number Final angle of the circular/ring section (in *radians*). tolerance : float Approximate curvature resolution. The number of points is automatically calculated. number_of_points : integer or None Manually define the number of vertices that form the object (polygonal approximation). Overrides `tolerance`. max_points : integer If the number of points in the element is greater than `max_points`, it will be fractured in smaller polygons with at most `max_points` each. If `max_points` is zero no fracture will occur. layer : integer The GDSII layer number for this element. datatype : integer The GDSII datatype for this element (between 0 and 255). Notes ----- The original GDSII specification supports only a maximum of 199 vertices per polygon. Examples -------- >>> circle = gdspy.Round((30, 5), 8) >>> ell_ring = gdspy.Round((50, 5), (8, 7), inner_radius=(5, 4)) >>> pie_slice = gdspy.Round((30, 25), 8, initial_angle=0, ... final_angle=-5.0*numpy.pi/6.0) >>> arc = gdspy.Round((50, 25), 8, inner_radius=5, ... initial_angle=-5.0*numpy.pi/6.0, ... final_angle=0) """ __slots__ = "layers", "datatypes", "polygons" def __init__( self, center, radius, inner_radius=0, initial_angle=0, final_angle=0, tolerance=0.01, number_of_points=None, max_points=199, layer=0, datatype=0, ): if hasattr(radius, "__iter__"): orx, ory = radius radius = max(radius) def outer_transform(a): r = a - ((a + numpy.pi) % (2 * numpy.pi) - numpy.pi) t = numpy.arctan2(orx * numpy.sin(a), ory * numpy.cos(a)) + r t[a == numpy.pi] = numpy.pi return t else: orx = ory = radius def outer_transform(a): return a if hasattr(inner_radius, "__iter__"): irx, iry = inner_radius inner_radius = max(inner_radius) def inner_transform(a): r = a - ((a + numpy.pi) % (2 * numpy.pi) - numpy.pi) t = numpy.arctan2(irx * numpy.sin(a), iry * numpy.cos(a)) + r t[a == numpy.pi] = numpy.pi return t else: irx = iry = inner_radius def inner_transform(a): return a if isinstance(number_of_points, float): warnings.warn( "[GDSPY] Use of a floating number as number_of_points is deprecated in favor of tolerance.", category=DeprecationWarning, stacklevel=2, ) tolerance = number_of_points number_of_points = None if number_of_points is None: full_angle = ( 2 * numpy.pi if final_angle == initial_angle else abs(final_angle - initial_angle) ) number_of_points = max( 3, 1 + int(0.5 * full_angle / numpy.arccos(1 - tolerance / radius) + 0.5), ) if inner_radius > 0: number_of_points *= 2 pieces = ( 1 if max_points == 0 else int(numpy.ceil(number_of_points / float(max_points))) ) number_of_points = number_of_points // pieces self.layers = [layer] * pieces self.datatypes = [datatype] * pieces self.polygons = [numpy.zeros((number_of_points, 2)) for _ in range(pieces)] if final_angle == initial_angle and pieces > 1: final_angle += 2 * numpy.pi angles = numpy.linspace(initial_angle, final_angle, pieces + 1) oang = outer_transform(angles) iang = inner_transform(angles) for ii in range(pieces): if oang[ii + 1] == oang[ii]: if inner_radius <= 0: t = ( numpy.arange(number_of_points) * 2.0 * numpy.pi / number_of_points ) self.polygons[ii][:, 0] = numpy.cos(t) * orx + center[0] self.polygons[ii][:, 1] = numpy.sin(t) * ory + center[1] else: n2 = number_of_points // 2 n1 = number_of_points - n2 t = numpy.arange(n1) * 2.0 * numpy.pi / (n1 - 1) self.polygons[ii][:n1, 0] = numpy.cos(t) * orx + center[0] self.polygons[ii][:n1, 1] = numpy.sin(t) * ory + center[1] t = numpy.arange(n2) * -2.0 * numpy.pi / (n2 - 1) self.polygons[ii][n1:, 0] = numpy.cos(t) * irx + center[0] self.polygons[ii][n1:, 1] = numpy.sin(t) * iry + center[1] else: if inner_radius <= 0: t = numpy.linspace(oang[ii], oang[ii + 1], number_of_points - 1) self.polygons[ii][1:, 0] = numpy.cos(t) * orx + center[0] self.polygons[ii][1:, 1] = numpy.sin(t) * ory + center[1] self.polygons[ii][0] += center else: n2 = number_of_points // 2 n1 = number_of_points - n2 t = numpy.linspace(oang[ii], oang[ii + 1], n1) self.polygons[ii][:n1, 0] = numpy.cos(t) * orx + center[0] self.polygons[ii][:n1, 1] = numpy.sin(t) * ory + center[1] t = numpy.linspace(iang[ii + 1], iang[ii], n2) self.polygons[ii][n1:, 0] = numpy.cos(t) * irx + center[0] self.polygons[ii][n1:, 1] = numpy.sin(t) * iry + center[1] def __str__(self): return ("Round ({} polygons, {} vertices, layers {}, datatypes {})").format( len(self.polygons), sum([len(p) for p in self.polygons]), list(set(self.layers)), list(set(self.datatypes)), ) class Text(PolygonSet): """ Polygonal text object. Each letter is formed by a series of polygons. Parameters ---------- text : string The text to be converted in geometric objects. size : number Height of the character. The width of a character and the distance between characters are this value multiplied by 5 / 9 and 8 / 9, respectively. For vertical text, the distance is multiplied by 11 / 9. position : array-like[2] Text position (lower left corner). horizontal : bool If True, the text is written from left to right; if False, from top to bottom. angle : number The angle of rotation of the text. layer : integer The GDSII layer number for these elements. datatype : integer The GDSII datatype for this element (between 0 and 255). Examples -------- >>> text = gdspy.Text('Sample text', 20, (-10, -100)) >>> myCell.add(text) """ # fmt: off _font = { '!': [[(2, 2), (3, 2), (3, 3), (2, 3)], [(2, 4), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (2, 9), (2, 8), (2, 7), (2, 6), (2, 5)]], '"': [[(1, 7), (2, 7), (2, 8), (2, 9), (1, 9), (1, 8)], [(3, 7), (4, 7), (4, 8), (4, 9), (3, 9), (3, 8)]], '#': [[(0, 3), (1, 3), (1, 2), (2, 2), (2, 3), (2, 4), (2, 5), (3, 5), (3, 4), (2, 4), (2, 3), (3, 3), (3, 2), (4, 2), (4, 3), (5, 3), (5, 4), (4, 4), (4, 5), (5, 5), (5, 6), (4, 6), (4, 7), (3, 7), (3, 6), (2, 6), (2, 7), (1, 7), (1, 6), (0, 6), (0, 5), (1, 5), (1, 4), (0, 4)]], '$': [[(0, 2), (1, 2), (2, 2), (2, 1), (3, 1), (3, 2), (4, 2), (4, 3), (3, 3), (3, 4), (4, 4), (4, 5), (3, 5), (3, 6), (3, 7), (4, 7), (5, 7), (5, 8), (4, 8), (3, 8), (3, 9), (2, 9), (2, 8), (1, 8), (1, 7), (2, 7), (2, 6), (1, 6), (1, 5), (2, 5), (2, 4), (2, 3), (1, 3), (0, 3)], [(0, 6), (1, 6), (1, 7), (0, 7)], [(4, 3), (5, 3), (5, 4), (4, 4)]], '%': [[(0, 2), (1, 2), (1, 3), (1, 4), (0, 4), (0, 3)], [(0, 7), (1, 7), (2, 7), (2, 8), (2, 9), (1, 9), (0, 9), (0, 8)], [(1, 4), (2, 4), (2, 5), (1, 5)], [(2, 5), (3, 5), (3, 6), (2, 6)], [(3, 2), (4, 2), (5, 2), (5, 3), (5, 4), (4, 4), (3, 4), (3, 3)], [(3, 6), (4, 6), (4, 7), (3, 7)], [(4, 7), (5, 7), (5, 8), (5, 9), (4, 9), (4, 8)]], '&': [[(0, 3), (1, 3), (1, 4), (1, 5), (0, 5), (0, 4)], [(0, 6), (1, 6), (1, 7), (1, 8), (0, 8), (0, 7)], [(1, 2), (2, 2), (3, 2), (3, 3), (2, 3), (1, 3)], [(1, 5), (2, 5), (3, 5), (3, 6), (3, 7), (3, 8), (2, 8), (2, 7), (2, 6), (1, 6)], [(1, 8), (2, 8), (2, 9), (1, 9)], [(3, 3), (4, 3), (4, 4), (4, 5), (3, 5), (3, 4)], [(4, 2), (5, 2), (5, 3), (4, 3)], [(4, 5), (5, 5), (5, 6), (4, 6)]], "'": [[(2, 7), (3, 7), (3, 8), (3, 9), (2, 9), (2, 8)]], '(': [[(1, 4), (2, 4), (2, 5), (2, 6), (2, 7), (1, 7), (1, 6), (1, 5)], [(2, 3), (3, 3), (3, 4), (2, 4)], [(2, 7), (3, 7), (3, 8), (2, 8)], [(3, 2), (4, 2), (4, 3), (3, 3)], [(3, 8), (4, 8), (4, 9), (3, 9)]], ')': [[(3, 4), (4, 4), (4, 5), (4, 6), (4, 7), (3, 7), (3, 6), (3, 5)], [(1, 2), (2, 2), (2, 3), (1, 3)], [(1, 8), (2, 8), (2, 9), (1, 9)], [(2, 3), (3, 3), (3, 4), (2, 4)], [(2, 7), (3, 7), (3, 8), (2, 8)]], '*': [[(0, 2), (1, 2), (1, 3), (0, 3)], [(0, 4), (1, 4), (1, 3), (2, 3), (2, 2), (3, 2), (3, 3), (4, 3), (4, 4), (5, 4), (5, 5), (4, 5), (4, 6), (3, 6), (3, 7), (2, 7), (2, 6), (1, 6), (1, 5), (0, 5)], [(0, 6), (1, 6), (1, 7), (0, 7)], [(4, 2), (5, 2), (5, 3), (4, 3)], [(4, 6), (5, 6), (5, 7), (4, 7)]], '+': [[(0, 4), (1, 4), (2, 4), (2, 3), (2, 2), (3, 2), (3, 3), (3, 4), (4, 4), (5, 4), (5, 5), (4, 5), (3, 5), (3, 6), (3, 7), (2, 7), (2, 6), (2, 5), (1, 5), (0, 5)]], ',': [[(1, 0), (2, 0), (2, 1), (1, 1)], [(2, 1), (3, 1), (3, 2), (3, 3), (2, 3), (2, 2)]], '-': [[(0, 4), (1, 4), (2, 4), (3, 4), (4, 4), (5, 4), (5, 5), (4, 5), (3, 5), (2, 5), (1, 5), (0, 5)]], '.': [[(2, 2), (3, 2), (3, 3), (2, 3)]], '/': [[(0, 2), (1, 2), (1, 3), (1, 4), (0, 4), (0, 3)], [(1, 4), (2, 4), (2, 5), (1, 5)], [(2, 5), (3, 5), (3, 6), (2, 6)], [(3, 6), (4, 6), (4, 7), (3, 7)], [(4, 7), (5, 7), (5, 8), (5, 9), (4, 9), (4, 8)]], '0': [[(0, 3), (1, 3), (1, 4), (2, 4), (2, 5), (1, 5), (1, 6), (1, 7), (1, 8), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9)], [(2, 5), (3, 5), (3, 6), (2, 6)], [(3, 6), (4, 6), (4, 5), (4, 4), (4, 3), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (4, 8), (4, 7), (3, 7)]], '1': [[(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (2, 9), (2, 8), (1, 8), (1, 7), (2, 7), (2, 6), (2, 5), (2, 4), (2, 3), (1, 3)]], '2': [[(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (5, 3), (4, 3), (3, 3), (2, 3), (1, 3), (1, 4), (0, 4), (0, 3)], [(0, 7), (1, 7), (1, 8), (0, 8)], [(1, 4), (2, 4), (3, 4), (3, 5), (2, 5), (1, 5)], [(1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9)], [(3, 5), (4, 5), (4, 6), (3, 6)], [(4, 6), (5, 6), (5, 7), (5, 8), (4, 8), (4, 7)]], '3': [[(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3), (0, 3)], [(0, 8), (1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9), (0, 9)], [(1, 5), (2, 5), (3, 5), (4, 5), (4, 6), (3, 6), (2, 6), (1, 6)], [(4, 3), (5, 3), (5, 4), (5, 5), (4, 5), (4, 4)], [(4, 6), (5, 6), (5, 7), (5, 8), (4, 8), (4, 7)]], '4': [[(0, 4), (1, 4), (2, 4), (3, 4), (3, 3), (3, 2), (4, 2), (4, 3), (4, 4), (5, 4), (5, 5), (4, 5), (4, 6), (4, 7), (4, 8), (4, 9), (3, 9), (2, 9), (2, 8), (3, 8), (3, 7), (3, 6), (3, 5), (2, 5), (1, 5), (1, 6), (0, 6), (0, 5)], [(1, 6), (2, 6), (2, 7), (2, 8), (1, 8), (1, 7)]], '5': [[(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3), (0, 3)], [(0, 5), (1, 5), (2, 5), (3, 5), (4, 5), (4, 6), (3, 6), (2, 6), (1, 6), (1, 7), (1, 8), (2, 8), (3, 8), (4, 8), (5, 8), (5, 9), (4, 9), (3, 9), (2, 9), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6)], [(4, 3), (5, 3), (5, 4), (5, 5), (4, 5), (4, 4)]], '6': [[(0, 3), (1, 3), (1, 4), (1, 5), (2, 5), (3, 5), (4, 5), (4, 6), (3, 6), (2, 6), (1, 6), (1, 7), (1, 8), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9)], [(4, 3), (5, 3), (5, 4), (5, 5), (4, 5), (4, 4)]], '7': [[(0, 8), (1, 8), (2, 8), (3, 8), (4, 8), (4, 7), (4, 6), (5, 6), (5, 7), (5, 8), (5, 9), (4, 9), (3, 9), (2, 9), (1, 9), (0, 9)], [(2, 2), (3, 2), (3, 3), (3, 4), (3, 5), (2, 5), (2, 4), (2, 3)], [(3, 5), (4, 5), (4, 6), (3, 6)]], '8': [[(0, 3), (1, 3), (1, 4), (1, 5), (0, 5), (0, 4)], [(0, 6), (1, 6), (1, 7), (1, 8), (0, 8), (0, 7)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 5), (2, 5), (3, 5), (4, 5), (4, 6), (3, 6), (2, 6), (1, 6)], [(1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9)], [(4, 3), (5, 3), (5, 4), (5, 5), (4, 5), (4, 4)], [(4, 6), (5, 6), (5, 7), (5, 8), (4, 8), (4, 7)]], '9': [[(0, 6), (1, 6), (1, 7), (1, 8), (0, 8), (0, 7)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 5), (2, 5), (3, 5), (4, 5), (4, 4), (4, 3), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (4, 8), (4, 7), (4, 6), (3, 6), (2, 6), (1, 6)], [(1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9)]], ':': [[(2, 2), (3, 2), (3, 3), (2, 3)], [(2, 5), (3, 5), (3, 6), (2, 6)]], ';': [[(1, 0), (2, 0), (2, 1), (1, 1)], [(2, 1), (3, 1), (3, 2), (3, 3), (2, 3), (2, 2)], [(2, 4), (3, 4), (3, 5), (2, 5)]], '<': [[(0, 5), (1, 5), (1, 6), (0, 6)], [(1, 4), (2, 4), (2, 5), (1, 5)], [(1, 6), (2, 6), (2, 7), (1, 7)], [(2, 3), (3, 3), (4, 3), (4, 4), (3, 4), (2, 4)], [(2, 7), (3, 7), (4, 7), (4, 8), (3, 8), (2, 8)], [(4, 2), (5, 2), (5, 3), (4, 3)], [(4, 8), (5, 8), (5, 9), (4, 9)]], '=': [[(0, 3), (1, 3), (2, 3), (3, 3), (4, 3), (5, 3), (5, 4), (4, 4), (3, 4), (2, 4), (1, 4), (0, 4)], [(0, 5), (1, 5), (2, 5), (3, 5), (4, 5), (5, 5), (5, 6), (4, 6), (3, 6), (2, 6), (1, 6), (0, 6)]], '>': [[(0, 2), (1, 2), (1, 3), (0, 3)], [(0, 8), (1, 8), (1, 9), (0, 9)], [(1, 3), (2, 3), (3, 3), (3, 4), (2, 4), (1, 4)], [(1, 7), (2, 7), (3, 7), (3, 8), (2, 8), (1, 8)], [(3, 4), (4, 4), (4, 5), (3, 5)], [(3, 6), (4, 6), (4, 7), (3, 7)], [(4, 5), (5, 5), (5, 6), (4, 6)]], '?': [[(0, 7), (1, 7), (1, 8), (0, 8)], [(1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9)], [(2, 2), (3, 2), (3, 3), (2, 3)], [(2, 4), (3, 4), (3, 5), (2, 5)], [(3, 5), (4, 5), (4, 6), (3, 6)], [(4, 6), (5, 6), (5, 7), (5, 8), (4, 8), (4, 7)]], '@': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9)], [(2, 4), (3, 4), (4, 4), (4, 5), (3, 5), (3, 6), (3, 7), (2, 7), (2, 6), (2, 5)], [(4, 5), (5, 5), (5, 6), (5, 7), (5, 8), (4, 8), (4, 7), (4, 6)]], 'A': [[(0, 2), (1, 2), (1, 3), (1, 4), (2, 4), (3, 4), (4, 4), (4, 3), (4, 2), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (4, 8), (4, 7), (4, 6), (4, 5), (3, 5), (2, 5), (1, 5), (1, 6), (1, 7), (1, 8), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9)]], 'B': [[(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3), (1, 4), (1, 5), (2, 5), (3, 5), (4, 5), (4, 6), (3, 6), (2, 6), (1, 6), (1, 7), (1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(4, 3), (5, 3), (5, 4), (5, 5), (4, 5), (4, 4)], [(4, 6), (5, 6), (5, 7), (5, 8), (4, 8), (4, 7)]], 'C': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (5, 3), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 8), (2, 8), (3, 8), (4, 8), (5, 8), (5, 9), (4, 9), (3, 9), (2, 9), (1, 9)]], 'D': [[(0, 2), (1, 2), (2, 2), (3, 2), (3, 3), (2, 3), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (2, 8), (3, 8), (3, 9), (2, 9), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(3, 3), (4, 3), (4, 4), (3, 4)], [(3, 7), (4, 7), (4, 8), (3, 8)], [(4, 4), (5, 4), (5, 5), (5, 6), (5, 7), (4, 7), (4, 6), (4, 5)]], 'E': [[(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (5, 3), (4, 3), (3, 3), (2, 3), (1, 3), (1, 4), (1, 5), (2, 5), (3, 5), (4, 5), (4, 6), (3, 6), (2, 6), (1, 6), (1, 7), (1, 8), (2, 8), (3, 8), (4, 8), (5, 8), (5, 9), (4, 9), (3, 9), (2, 9), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)]], 'F': [[(0, 2), (1, 2), (1, 3), (1, 4), (1, 5), (2, 5), (3, 5), (4, 5), (4, 6), (3, 6), (2, 6), (1, 6), (1, 7), (1, 8), (2, 8), (3, 8), (4, 8), (5, 8), (5, 9), (4, 9), (3, 9), (2, 9), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)]], 'G': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (4, 6), (3, 6), (2, 6), (2, 5), (3, 5), (4, 5), (4, 4), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 8), (2, 8), (3, 8), (4, 8), (5, 8), (5, 9), (4, 9), (3, 9), (2, 9), (1, 9)]], 'H': [[(0, 2), (1, 2), (1, 3), (1, 4), (1, 5), (2, 5), (3, 5), (4, 5), (4, 4), (4, 3), (4, 2), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (4, 9), (4, 8), (4, 7), (4, 6), (3, 6), (2, 6), (1, 6), (1, 7), (1, 8), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)]], 'I': [[(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9), (1, 8), (2, 8), (2, 7), (2, 6), (2, 5), (2, 4), (2, 3), (1, 3)]], 'J': [[(0, 3), (1, 3), (1, 4), (0, 4)], [(0, 8), (1, 8), (2, 8), (3, 8), (3, 7), (3, 6), (3, 5), (3, 4), (3, 3), (4, 3), (4, 4), (4, 5), (4, 6), (4, 7), (4, 8), (5, 8), (5, 9), (4, 9), (3, 9), (2, 9), (1, 9), (0, 9)], [(1, 2), (2, 2), (3, 2), (3, 3), (2, 3), (1, 3)]], 'K': [[(0, 2), (1, 2), (1, 3), (1, 4), (1, 5), (2, 5), (2, 6), (1, 6), (1, 7), (1, 8), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(2, 4), (3, 4), (3, 5), (2, 5)], [(2, 6), (3, 6), (3, 7), (2, 7)], [(3, 3), (4, 3), (4, 4), (3, 4)], [(3, 7), (4, 7), (4, 8), (3, 8)], [(4, 2), (5, 2), (5, 3), (4, 3)], [(4, 8), (5, 8), (5, 9), (4, 9)]], 'L': [[(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (5, 3), (4, 3), (3, 3), (2, 3), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)]], 'M': [[(0, 2), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (2, 7), (2, 8), (1, 8), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(2, 5), (3, 5), (3, 6), (3, 7), (2, 7), (2, 6)], [(3, 7), (4, 7), (4, 6), (4, 5), (4, 4), (4, 3), (4, 2), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (4, 9), (4, 8), (3, 8)]], 'N': [[(0, 2), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (2, 7), (2, 8), (1, 8), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(2, 5), (3, 5), (3, 6), (3, 7), (2, 7), (2, 6)], [(3, 4), (4, 4), (4, 3), (4, 2), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (4, 9), (4, 8), (4, 7), (4, 6), (4, 5), (3, 5)]], 'O': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9)], [(4, 3), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (4, 8), (4, 7), (4, 6), (4, 5), (4, 4)]], 'P': [[(0, 2), (1, 2), (1, 3), (1, 4), (1, 5), (2, 5), (3, 5), (4, 5), (4, 6), (3, 6), (2, 6), (1, 6), (1, 7), (1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(4, 6), (5, 6), (5, 7), (5, 8), (4, 8), (4, 7)]], 'Q': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (4, 8), (4, 7), (4, 6), (4, 5), (4, 4), (3, 4), (3, 3), (2, 3), (1, 3)], [(1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9)], [(2, 4), (3, 4), (3, 5), (2, 5)]], 'R': [[(0, 2), (1, 2), (1, 3), (1, 4), (1, 5), (2, 5), (3, 5), (3, 4), (4, 4), (4, 5), (4, 6), (3, 6), (2, 6), (1, 6), (1, 7), (1, 8), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(4, 2), (5, 2), (5, 3), (5, 4), (4, 4), (4, 3)], [(4, 6), (5, 6), (5, 7), (5, 8), (4, 8), (4, 7)]], 'S': [[(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3), (0, 3)], [(0, 6), (1, 6), (1, 7), (1, 8), (0, 8), (0, 7)], [(1, 5), (2, 5), (3, 5), (4, 5), (4, 6), (3, 6), (2, 6), (1, 6)], [(1, 8), (2, 8), (3, 8), (4, 8), (5, 8), (5, 9), (4, 9), (3, 9), (2, 9), (1, 9)], [(4, 3), (5, 3), (5, 4), (5, 5), (4, 5), (4, 4)]], 'T': [[(0, 8), (1, 8), (2, 8), (2, 7), (2, 6), (2, 5), (2, 4), (2, 3), (2, 2), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (4, 8), (5, 8), (5, 9), (4, 9), (3, 9), (2, 9), (1, 9), (0, 9)]], 'U': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3)], [(4, 3), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (4, 9), (4, 8), (4, 7), (4, 6), (4, 5), (4, 4)]], 'V': [[(0, 5), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6)], [(1, 3), (2, 3), (2, 4), (2, 5), (1, 5), (1, 4)], [(2, 2), (3, 2), (3, 3), (2, 3)], [(3, 3), (4, 3), (4, 4), (4, 5), (3, 5), (3, 4)], [(4, 5), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (4, 9), (4, 8), (4, 7), (4, 6)]], 'W': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (2, 3), (1, 3)], [(2, 3), (3, 3), (3, 4), (3, 5), (3, 6), (2, 6), (2, 5), (2, 4)], [(3, 2), (4, 2), (4, 3), (3, 3)], [(4, 3), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (4, 9), (4, 8), (4, 7), (4, 6), (4, 5), (4, 4)]], 'X': [[(0, 2), (1, 2), (1, 3), (1, 4), (0, 4), (0, 3)], [(0, 7), (1, 7), (1, 8), (1, 9), (0, 9), (0, 8)], [(1, 4), (2, 4), (2, 5), (1, 5)], [(1, 6), (2, 6), (2, 7), (1, 7)], [(2, 5), (3, 5), (3, 6), (2, 6)], [(3, 4), (4, 4), (4, 5), (3, 5)], [(3, 6), (4, 6), (4, 7), (3, 7)], [(4, 2), (5, 2), (5, 3), (5, 4), (4, 4), (4, 3)], [(4, 7), (5, 7), (5, 8), (5, 9), (4, 9), (4, 8)]], 'Y': [[(0, 7), (1, 7), (1, 8), (1, 9), (0, 9), (0, 8)], [(1, 5), (2, 5), (2, 6), (2, 7), (1, 7), (1, 6)], [(2, 2), (3, 2), (3, 3), (3, 4), (3, 5), (2, 5), (2, 4), (2, 3)], [(3, 5), (4, 5), (4, 6), (4, 7), (3, 7), (3, 6)], [(4, 7), (5, 7), (5, 8), (5, 9), (4, 9), (4, 8)]], 'Z': [[(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (5, 3), (4, 3), (3, 3), (2, 3), (1, 3), (1, 4), (0, 4), (0, 3)], [(0, 8), (1, 8), (2, 8), (3, 8), (4, 8), (4, 7), (5, 7), (5, 8), (5, 9), (4, 9), (3, 9), (2, 9), (1, 9), (0, 9)], [(1, 4), (2, 4), (2, 5), (1, 5)], [(2, 5), (3, 5), (3, 6), (2, 6)], [(3, 6), (4, 6), (4, 7), (3, 7)]], '[': [[(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (3, 8), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9), (1, 8), (1, 7), (1, 6), (1, 5), (1, 4), (1, 3)]], '\\': [[(0, 7), (1, 7), (1, 8), (1, 9), (0, 9), (0, 8)], [(1, 6), (2, 6), (2, 7), (1, 7)], [(2, 5), (3, 5), (3, 6), (2, 6)], [(3, 4), (4, 4), (4, 5), (3, 5)], [(4, 2), (5, 2), (5, 3), (5, 4), (4, 4), (4, 3)]], ']': [[(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (4, 4), (4, 5), (4, 6), (4, 7), (4, 8), (4, 9), (3, 9), (2, 9), (1, 9), (1, 8), (2, 8), (3, 8), (3, 7), (3, 6), (3, 5), (3, 4), (3, 3), (2, 3), (1, 3)]], '^': [[(0, 6), (1, 6), (1, 7), (0, 7)], [(1, 7), (2, 7), (2, 8), (1, 8)], [(2, 8), (3, 8), (3, 9), (2, 9)], [(3, 7), (4, 7), (4, 8), (3, 8)], [(4, 6), (5, 6), (5, 7), (4, 7)]], '_': [[(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (5, 3), (4, 3), (3, 3), (2, 3), (1, 3), (0, 3)]], '`': [[(1, 8), (2, 8), (2, 9), (1, 9)], [(2, 7), (3, 7), (3, 8), (2, 8)]], 'a': [[(0, 3), (1, 3), (1, 4), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (4, 6), (4, 5), (3, 5), (2, 5), (1, 5), (1, 4), (2, 4), (3, 4), (4, 4), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 6), (2, 6), (3, 6), (4, 6), (4, 7), (3, 7), (2, 7), (1, 7)]], 'b': [[(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3), (1, 4), (1, 5), (1, 6), (2, 6), (3, 6), (4, 6), (4, 7), (3, 7), (2, 7), (1, 7), (1, 8), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(4, 3), (5, 3), (5, 4), (5, 5), (5, 6), (4, 6), (4, 5), (4, 4)]], 'c': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (5, 3), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 6), (2, 6), (3, 6), (4, 6), (5, 6), (5, 7), (4, 7), (3, 7), (2, 7), (1, 7)]], 'd': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9), (4, 9), (4, 8), (4, 7), (3, 7), (2, 7), (1, 7), (1, 6), (2, 6), (3, 6), (4, 6), (4, 5), (4, 4), (4, 3), (3, 3), (2, 3), (1, 3)]], 'e': [[(0, 3), (1, 3), (1, 4), (2, 4), (3, 4), (4, 4), (5, 4), (5, 5), (5, 6), (4, 6), (4, 5), (3, 5), (2, 5), (1, 5), (1, 6), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 6), (2, 6), (3, 6), (4, 6), (4, 7), (3, 7), (2, 7), (1, 7)]], 'f': [[(0, 5), (1, 5), (1, 4), (1, 3), (1, 2), (2, 2), (2, 3), (2, 4), (2, 5), (3, 5), (4, 5), (4, 6), (3, 6), (2, 6), (2, 7), (2, 8), (1, 8), (1, 7), (1, 6), (0, 6)], [(2, 8), (3, 8), (4, 8), (5, 8), (5, 9), (4, 9), (3, 9), (2, 9)]], 'g': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (0, 6), (0, 5), (0, 4)], [(1, 0), (2, 0), (3, 0), (4, 0), (4, 1), (3, 1), (2, 1), (1, 1)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 1), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (4, 6), (4, 5), (4, 4), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 6), (2, 6), (3, 6), (4, 6), (4, 7), (3, 7), (2, 7), (1, 7)]], 'h': [[(0, 2), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (2, 6), (3, 6), (4, 6), (4, 7), (3, 7), (2, 7), (1, 7), (1, 8), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(4, 2), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (4, 6), (4, 5), (4, 4), (4, 3)]], 'i': [[(1, 6), (2, 6), (2, 5), (2, 4), (2, 3), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (2, 7), (1, 7)], [(2, 8), (3, 8), (3, 9), (2, 9)]], 'j': [[(0, 0), (1, 0), (2, 0), (2, 1), (1, 1), (0, 1)], [(1, 6), (2, 6), (2, 5), (2, 4), (2, 3), (2, 2), (2, 1), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (2, 7), (1, 7)], [(2, 8), (3, 8), (3, 9), (2, 9)]], 'k': [[(0, 2), (1, 2), (1, 3), (1, 4), (2, 4), (3, 4), (3, 5), (2, 5), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(3, 3), (4, 3), (4, 4), (3, 4)], [(3, 5), (4, 5), (4, 6), (3, 6)], [(4, 2), (5, 2), (5, 3), (4, 3)], [(4, 6), (5, 6), (5, 7), (4, 7)]], 'l': [[(1, 8), (2, 8), (2, 7), (2, 6), (2, 5), (2, 4), (2, 3), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (2, 9), (1, 9)]], 'm': [[(0, 2), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (2, 6), (2, 5), (2, 4), (2, 3), (2, 2), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6), (4, 6), (4, 7), (3, 7), (2, 7), (1, 7), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(4, 2), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (4, 6), (4, 5), (4, 4), (4, 3)]], 'n': [[(0, 2), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (2, 6), (3, 6), (4, 6), (4, 7), (3, 7), (2, 7), (1, 7), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(4, 2), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (4, 6), (4, 5), (4, 4), (4, 3)]], 'o': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 6), (2, 6), (3, 6), (4, 6), (4, 7), (3, 7), (2, 7), (1, 7)], [(4, 3), (5, 3), (5, 4), (5, 5), (5, 6), (4, 6), (4, 5), (4, 4)]], 'p': [[(0, 0), (1, 0), (1, 1), (1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3), (1, 4), (1, 5), (1, 6), (2, 6), (3, 6), (4, 6), (4, 7), (3, 7), (2, 7), (1, 7), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3), (0, 2), (0, 1)], [(4, 3), (5, 3), (5, 4), (5, 5), (5, 6), (4, 6), (4, 5), (4, 4)]], 'q': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 1), (4, 0), (5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (4, 6), (4, 5), (4, 4), (4, 3), (3, 3), (2, 3), (1, 3)], [(1, 6), (2, 6), (3, 6), (4, 6), (4, 7), (3, 7), (2, 7), (1, 7)]], 'r': [[(0, 2), (1, 2), (1, 3), (1, 4), (1, 5), (2, 5), (3, 5), (3, 6), (2, 6), (1, 6), (1, 7), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3)], [(3, 6), (4, 6), (5, 6), (5, 7), (4, 7), (3, 7)]], 's': [[(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3), (0, 3)], [(0, 5), (1, 5), (1, 6), (0, 6)], [(1, 4), (2, 4), (3, 4), (4, 4), (4, 5), (3, 5), (2, 5), (1, 5)], [(1, 6), (2, 6), (3, 6), (4, 6), (5, 6), (5, 7), (4, 7), (3, 7), (2, 7), (1, 7)], [(4, 3), (5, 3), (5, 4), (4, 4)]], 't': [[(1, 6), (2, 6), (2, 5), (2, 4), (2, 3), (3, 3), (3, 4), (3, 5), (3, 6), (4, 6), (5, 6), (5, 7), (4, 7), (3, 7), (3, 8), (3, 9), (2, 9), (2, 8), (2, 7), (1, 7)], [(3, 2), (4, 2), (5, 2), (5, 3), (4, 3), (3, 3)]], 'u': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (0, 7), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 3), (3, 3), (2, 3), (1, 3)], [(4, 3), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (4, 7), (4, 6), (4, 5), (4, 4)]], 'v': [[(0, 5), (1, 5), (1, 6), (1, 7), (0, 7), (0, 6)], [(1, 3), (2, 3), (2, 4), (2, 5), (1, 5), (1, 4)], [(2, 2), (3, 2), (3, 3), (2, 3)], [(3, 3), (4, 3), (4, 4), (4, 5), (3, 5), (3, 4)], [(4, 5), (5, 5), (5, 6), (5, 7), (4, 7), (4, 6)]], 'w': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (0, 7), (0, 6), (0, 5), (0, 4)], [(1, 2), (2, 2), (2, 3), (1, 3)], [(2, 3), (3, 3), (3, 4), (3, 5), (3, 6), (2, 6), (2, 5), (2, 4)], [(3, 2), (4, 2), (4, 3), (3, 3)], [(4, 3), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (4, 7), (4, 6), (4, 5), (4, 4)]], 'x': [[(0, 2), (1, 2), (1, 3), (0, 3)], [(0, 6), (1, 6), (1, 7), (0, 7)], [(1, 3), (2, 3), (2, 4), (1, 4)], [(1, 5), (2, 5), (2, 6), (1, 6)], [(2, 4), (3, 4), (3, 5), (2, 5)], [(3, 3), (4, 3), (4, 4), (3, 4)], [(3, 5), (4, 5), (4, 6), (3, 6)], [(4, 2), (5, 2), (5, 3), (4, 3)], [(4, 6), (5, 6), (5, 7), (4, 7)]], 'y': [[(0, 3), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (0, 7), (0, 6), (0, 5), (0, 4)], [(1, 0), (2, 0), (3, 0), (4, 0), (4, 1), (3, 1), (2, 1), (1, 1)], [(1, 2), (2, 2), (3, 2), (4, 2), (4, 1), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (4, 7), (4, 6), (4, 5), (4, 4), (4, 3), (3, 3), (2, 3), (1, 3)]], 'z': [[(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (5, 3), (4, 3), (3, 3), (2, 3), (2, 4), (1, 4), (1, 3), (0, 3)], [(0, 6), (1, 6), (2, 6), (3, 6), (3, 5), (4, 5), (4, 6), (5, 6), (5, 7), (4, 7), (3, 7), (2, 7), (1, 7), (0, 7)], [(2, 4), (3, 4), (3, 5), (2, 5)]], '{': [[(1, 5), (2, 5), (2, 4), (2, 3), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (2, 8), (2, 7), (2, 6), (1, 6)], [(3, 2), (4, 2), (5, 2), (5, 3), (4, 3), (3, 3)], [(3, 8), (4, 8), (5, 8), (5, 9), (4, 9), (3, 9)]], '|': [[(2, 2), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (2, 9), (2, 8), (2, 7), (2, 6), (2, 5), (2, 4), (2, 3)]], '}': [[(0, 2), (1, 2), (2, 2), (2, 3), (1, 3), (0, 3)], [(0, 8), (1, 8), (2, 8), (2, 9), (1, 9), (0, 9)], [(2, 3), (3, 3), (3, 4), (3, 5), (4, 5), (4, 6), (3, 6), (3, 7), (3, 8), (2, 8), (2, 7), (2, 6), (2, 5), (2, 4)]], '~': [[(0, 6), (1, 6), (1, 7), (1, 8), (0, 8), (0, 7)], [(1, 8), (2, 8), (2, 9), (1, 9)], [(2, 7), (3, 7), (3, 8), (2, 8)], [(3, 6), (4, 6), (4, 7), (3, 7)], [(4, 7), (5, 7), (5, 8), (5, 9), (4, 9), (4, 8)]], } # fmt: on __slots__ = "layers", "datatypes", "polygons" def __init__( self, text, size, position=(0, 0), horizontal=True, angle=0, layer=0, datatype=0 ): self.polygons = [] posX = 0 posY = 0 text_multiplier = size / 9.0 if angle == 0: ca = 1 sa = 0 else: ca = numpy.cos(angle) sa = numpy.sin(angle) for jj in range(len(text)): if text[jj] == "\n": if horizontal: posY -= 11 posX = 0 else: posX += 8 posY = 0 elif text[jj] == "\t": if horizontal: posX = posX + 32 - (posX + 8) % 32 else: posY = posY - 11 - (posY - 22) % 44 else: if text[jj] in Text._font: for p in Text._font[text[jj]]: polygon = p[:] for ii in range(len(polygon)): xp = text_multiplier * (posX + polygon[ii][0]) yp = text_multiplier * (posY + polygon[ii][1]) polygon[ii] = ( position[0] + xp * ca - yp * sa, position[1] + xp * sa + yp * ca, ) self.polygons.append(numpy.array(polygon)) if horizontal: posX += 8 else: posY -= 11 self.layers = [layer] * len(self.polygons) self.datatypes = [datatype] * len(self.polygons) def __str__(self): return ("Text ({} polygons, {} vertices, layers {}, datatypes {})").format( len(self.polygons), sum([len(p) for p in self.polygons]), list(set(self.layers)), list(set(self.datatypes)), ) class Path(PolygonSet): """ Series of geometric objects that form a path or a collection of parallel paths. Parameters ---------- width : number The width of each path. initial_point : array-like[2] Starting position of the path. number_of_paths : positive integer Number of parallel paths to create simultaneously. distance : number Distance between the centers of adjacent paths. Attributes ---------- x : number Current position of the path in the x direction. y : number Current position of the path in the y direction. w : number *Half*-width of each path. n : integer Number of parallel paths. direction : '+x', '-x', '+y', '-y' or number Direction or angle (in *radians*) the path points to. distance : number Distance between the centers of adjacent paths. length : number Length of the central path axis. If only one path is created, this is the real length of the path. """ __slots__ = ( "layers", "datatypes", "polygons", "x", "y", "w", "n", "direction", "distance", "length", ) def __init__(self, width, initial_point=(0, 0), number_of_paths=1, distance=0): self.x = initial_point[0] self.y = initial_point[1] self.w = width * 0.5 self.n = number_of_paths self.direction = "+x" self.distance = distance self.length = 0.0 self.polygons = [] self.layers = [] self.datatypes = [] def __str__(self): if self.n > 1: return "Path (x{}, end at ({}, {}) towards {}, length {}, width {}, {} apart, {} polygons, {} vertices, layers {}, datatypes {})".format( self.n, self.x, self.y, self.direction, self.length, self.w * 2, self.distance, len(self.polygons), sum([len(p) for p in self.polygons]), list(set(self.layers)), list(set(self.datatypes)), ) else: return "Path (end at ({}, {}) towards {}, length {}, width {}, {} polygons, {} vertices, layers {}, datatypes {})".format( self.x, self.y, self.direction, self.length, self.w * 2, len(self.polygons), sum([len(p) for p in self.polygons]), list(set(self.layers)), list(set(self.datatypes)), ) def translate(self, dx, dy): """ Translate this object. Parameters ---------- dx : number Distance to move in the x-direction. dy : number Distance to move in the y-direction. Returns ------- out : `Path` This object. """ vec = numpy.array((dx, dy)) self.polygons = [points + vec for points in self.polygons] self.x += dx self.y += dy return self def rotate(self, angle, center=(0, 0)): """ Rotate this object. Parameters ---------- angle : number The angle of rotation (in *radians*). center : array-like[2] Center point for the rotation. Returns ------- out : `Path` This object. """ ca = numpy.cos(angle) sa = numpy.sin(angle) * _mpone c0 = numpy.array(center) if isinstance(self.direction, basestring): self.direction = _directions_dict[self.direction] * numpy.pi self.direction += angle cur = numpy.array((self.x, self.y)) - c0 self.x, self.y = cur * ca + cur[::-1] * sa + c0 self.polygons = [ (points - c0) * ca + (points - c0)[:, ::-1] * sa + c0 for points in self.polygons ] return self def scale(self, scalex, scaley=None, center=(0, 0)): """ Scale this object. Parameters ---------- scalex : number Scaling factor along the first axis. scaley : number or None Scaling factor along the second axis. If None, same as `scalex`. center : array-like[2] Center point for the scaling operation. Returns ------- out : `Path` This object. Notes ----- The direction of the path is not modified by this method and its width is scaled only by `scalex`. """ c0 = numpy.array(center) s = scalex if scaley is None else numpy.array((scalex, scaley)) self.polygons = [(points - c0) * s + c0 for points in self.polygons] self.x = (self.x - c0[0]) * scalex + c0[0] self.y = (self.y - c0[1]) * (scalex if scaley is None else scaley) + c0[1] self.w *= scalex return self def mirror(self, p1, p2=(0, 0)): """ Mirror the polygons over a line through points 1 and 2 Parameters ---------- p1 : array-like[2] first point defining the reflection line p2 : array-like[2] second point defining the reflection line Returns ------- out : `Path` This object. """ origin = numpy.array(p1) vec = numpy.array(p2) - origin vec_r = vec * (2 / numpy.inner(vec, vec)) self.polygons = [ numpy.outer(numpy.inner(points - origin, vec_r), vec) - points + 2 * origin for points in self.polygons ] dot = (self.x - origin[0]) * vec_r[0] + (self.y - origin[1]) * vec_r[1] self.x = dot * vec[0] - self.x + 2 * origin[0] self.y = dot * vec[1] - self.y + 2 * origin[1] if isinstance(self.direction, basestring): self.direction = _directions_dict[self.direction] * numpy.pi self.direction = 2 * numpy.arctan2(vec[1], vec[0]) - self.direction return self def segment( self, length, direction=None, final_width=None, final_distance=None, axis_offset=0, layer=0, datatype=0, ): """ Add a straight section to the path. Parameters ---------- length : number Length of the section to add. direction : '+x', '-x', '+y', '-y' or number Direction or angle (in *radians*) of rotation of the segment. final_width : number If set, the paths of this segment will have their widths linearly changed from their current value to this one. final_distance : number If set, the distance between paths is linearly change from its current value to this one along this segment. axis_offset : number If set, the paths will be offset from their direction by this amount. layer : integer, list The GDSII layer numbers for the elements of each path. If the number of layers in the list is less than the number of paths, the list is repeated. datatype : integer, list The GDSII datatype for the elements of each path (between 0 and 255). If the number of datatypes in the list is less than the number of paths, the list is repeated. Returns ------- out : `Path` This object. """ if direction is None: direction = self.direction else: self.direction = direction if direction == "+x": ca = 1 sa = 0 elif direction == "-x": ca = -1 sa = 0 elif direction == "+y": ca = 0 sa = 1 elif direction == "-y": ca = 0 sa = -1 else: ca = numpy.cos(direction) sa = numpy.sin(direction) old_x = self.x old_y = self.y self.x += length * ca + axis_offset * sa self.y += length * sa - axis_offset * ca old_w = self.w old_distance = self.distance if final_width is not None: self.w = final_width * 0.5 if final_distance is not None: self.distance = final_distance if (self.w != 0) or (old_w != 0): for ii in range(self.n): d0 = ii * self.distance - (self.n - 1) * self.distance * 0.5 old_d0 = ii * old_distance - (self.n - 1) * old_distance * 0.5 self.polygons.append( numpy.array( [ ( old_x + (old_d0 - old_w) * sa, old_y - (old_d0 - old_w) * ca, ), ( old_x + (old_d0 + old_w) * sa, old_y - (old_d0 + old_w) * ca, ), (self.x + (d0 + self.w) * sa, self.y - (d0 + self.w) * ca), (self.x + (d0 - self.w) * sa, self.y - (d0 - self.w) * ca), ] ) ) if self.w == 0: self.polygons[-1] = self.polygons[-1][:-1] if old_w == 0: self.polygons[-1] = self.polygons[-1][1:] self.length += (length ** 2 + axis_offset ** 2) ** 0.5 if isinstance(layer, list): self.layers.extend((layer * (self.n // len(layer) + 1))[: self.n]) else: self.layers.extend(layer for _ in range(self.n)) if isinstance(datatype, list): self.datatypes.extend( (datatype * (self.n // len(datatype) + 1))[: self.n] ) else: self.datatypes.extend(datatype for _ in range(self.n)) return self def arc( self, radius, initial_angle, final_angle, tolerance=0.01, number_of_points=None, max_points=199, final_width=None, final_distance=None, layer=0, datatype=0, ): """ Add a curved section to the path. Parameters ---------- radius : number Central radius of the section. initial_angle : number Initial angle of the curve (in *radians*). final_angle : number Final angle of the curve (in *radians*). tolerance : float Approximate curvature resolution. The number of points is automatically calculated. number_of_points : integer or None Manually define the number of vertices that form the object (polygonal approximation). Overrides `tolerance`. max_points : integer If the number of points in the element is greater than `max_points`, it will be fractured in smaller polygons with at most `max_points` each. If `max_points` is zero no fracture will occur. final_width : number If set, the paths of this segment will have their widths linearly changed from their current value to this one. final_distance : number If set, the distance between paths is linearly change from its current value to this one along this segment. layer : integer, list The GDSII layer numbers for the elements of each path. If the number of layers in the list is less than the number of paths, the list is repeated. datatype : integer, list The GDSII datatype for the elements of each path (between 0 and 255). If the number of datatypes in the list is less than the number of paths, the list is repeated. Returns ------- out : `Path` This object. Notes ----- The original GDSII specification supports only a maximum of 199 vertices per polygon. """ cx = self.x - radius * numpy.cos(initial_angle) cy = self.y - radius * numpy.sin(initial_angle) self.x = cx + radius * numpy.cos(final_angle) self.y = cy + radius * numpy.sin(final_angle) if final_angle > initial_angle: self.direction = final_angle + numpy.pi * 0.5 else: self.direction = final_angle - numpy.pi * 0.5 old_w = self.w old_distance = self.distance if final_width is not None: self.w = final_width * 0.5 if final_distance is not None: self.distance = final_distance if isinstance(number_of_points, float): warnings.warn( "[GDSPY] Use of a floating number as number_of_points is deprecated in favor of tolerance.", category=DeprecationWarning, stacklevel=2, ) tolerance = number_of_points number_of_points = None if number_of_points is None: r = ( radius + max(old_distance, self.distance) * (self.n - 1) * 0.5 + max(old_w, self.w) ) number_of_points = max( 6, 2 + 2 * int( 0.5 * abs(final_angle - initial_angle) / numpy.arccos(1 - tolerance / r) + 0.5 ), ) pieces = ( 1 if max_points == 0 else int(numpy.ceil(number_of_points / float(max_points))) ) number_of_points = number_of_points // pieces widths = numpy.linspace(old_w, self.w, pieces + 1) distances = numpy.linspace(old_distance, self.distance, pieces + 1) angles = numpy.linspace(initial_angle, final_angle, pieces + 1) if (self.w != 0) or (old_w != 0): for jj in range(pieces): for ii in range(self.n): self.polygons.append(numpy.zeros((number_of_points, 2))) r0 = ( radius + ii * distances[jj + 1] - (self.n - 1) * distances[jj + 1] * 0.5 ) old_r0 = ( radius + ii * distances[jj] - (self.n - 1) * distances[jj] * 0.5 ) pts2 = number_of_points // 2 pts1 = number_of_points - pts2 ang = numpy.linspace(angles[jj], angles[jj + 1], pts1) rad = numpy.linspace(old_r0 + widths[jj], r0 + widths[jj + 1], pts1) self.polygons[-1][:pts1, 0] = numpy.cos(ang) * rad + cx self.polygons[-1][:pts1, 1] = numpy.sin(ang) * rad + cy if widths[jj + 1] == 0: pts1 -= 1 pts2 += 1 if widths[jj] == 0: self.polygons[-1][: pts1 - 1] = numpy.array( self.polygons[-1][1:pts1] ) pts1 -= 1 pts2 += 1 ang = numpy.linspace(angles[jj + 1], angles[jj], pts2) rad = numpy.linspace(r0 - widths[jj + 1], old_r0 - widths[jj], pts2) if rad[0] <= 0 or rad[-1] <= 0: warnings.warn( "[GDSPY] Path arc with width larger than radius created: possible self-intersecting polygon.", stacklevel=2, ) self.polygons[-1][pts1:, 0] = numpy.cos(ang) * rad + cx self.polygons[-1][pts1:, 1] = numpy.sin(ang) * rad + cy self.length += abs((angles[jj + 1] - angles[jj]) * radius) if isinstance(layer, list): self.layers.extend((layer * (self.n // len(layer) + 1))[: self.n]) else: self.layers.extend(layer for _ in range(self.n)) if isinstance(datatype, list): self.datatypes.extend( (datatype * (self.n // len(datatype) + 1))[: self.n] ) else: self.datatypes.extend(datatype for _ in range(self.n)) return self def turn( self, radius, angle, tolerance=0.01, number_of_points=None, max_points=199, final_width=None, final_distance=None, layer=0, datatype=0, ): """ Add a curved section to the path. Parameters ---------- radius : number Central radius of the section. angle : 'r', 'l', 'rr', 'll' or number Angle (in *radians*) of rotation of the path. The values 'r' and 'l' represent 90-degree turns cw and ccw, respectively; the values 'rr' and 'll' represent analogous 180-degree turns. tolerance : float Approximate curvature resolution. The number of points is automatically calculated. number_of_points : integer or None Manually define the number of vertices that form the object (polygonal approximation). Overrides `tolerance`. max_points : integer If the number of points in the element is greater than `max_points`, it will be fractured in smaller polygons with at most `max_points` each. If `max_points` is zero no fracture will occur. final_width : number If set, the paths of this segment will have their widths linearly changed from their current value to this one. final_distance : number If set, the distance between paths is linearly change from its current value to this one along this segment. layer : integer, list The GDSII layer numbers for the elements of each path. If the number of layers in the list is less than the number of paths, the list is repeated. datatype : integer, list The GDSII datatype for the elements of each path (between 0 and 255). If the number of datatypes in the list is less than the number of paths, the list is repeated. Returns ------- out : `Path` This object. Notes ----- The original GDSII specification supports only a maximum of 199 vertices per polygon. """ exact = True if angle == "r": delta_i = _halfpi delta_f = 0 elif angle == "rr": delta_i = _halfpi delta_f = -delta_i elif angle == "l": delta_i = -_halfpi delta_f = 0 elif angle == "ll": delta_i = -_halfpi delta_f = -delta_i elif angle < 0: exact = False delta_i = _halfpi delta_f = delta_i + angle else: exact = False delta_i = -_halfpi delta_f = delta_i + angle if self.direction == "+x": self.direction = 0 elif self.direction == "-x": self.direction = numpy.pi elif self.direction == "+y": self.direction = _halfpi elif self.direction == "-y": self.direction = -_halfpi elif exact: exact = False self.arc( radius, self.direction + delta_i, self.direction + delta_f, tolerance, number_of_points, max_points, final_width, final_distance, layer, datatype, ) if exact: self.direction = _directions_list[int(round(self.direction / _halfpi)) % 4] return self def parametric( self, curve_function, curve_derivative=None, tolerance=0.01, number_of_evaluations=5, max_points=199, final_width=None, final_distance=None, relative=True, layer=0, datatype=0, ): """ Add a parametric curve to the path. `curve_function` will be evaluated uniformly in the interval [0, 1] at least `number_of_points` times. More points will be added to the curve at the midpoint between evaluations if that points presents error larger than `tolerance`. Parameters ---------- curve_function : callable Function that defines the curve. Must be a function of one argument (that varies from 0 to 1) that returns a 2-element array with the coordinates of the curve. curve_derivative : callable If set, it should be the derivative of the curve function. Must be a function of one argument (that varies from 0 to 1) that returns a 2-element array. If None, the derivative will be calculated numerically. tolerance : number Acceptable tolerance for the approximation of the curve function by a finite number of evaluations. number_of_evaluations : integer Initial number of points where the curve function will be evaluated. According to `tolerance`, more evaluations will be performed. max_points : integer Elements will be fractured until each polygon has at most `max_points`. If `max_points` is less than 4, no fracture will occur. final_width : number or function If set to a number, the paths of this segment will have their widths linearly changed from their current value to this one. If set to a function, it must be a function of one argument (that varies from 0 to 1) and returns the width of the path. final_distance : number or function If set to a number, the distance between paths is linearly change from its current value to this one. If set to a function, it must be a function of one argument (that varies from 0 to 1) and returns the width of the path. relative : bool If True, the return values of `curve_function` are used as offsets from the current path position, i.e., to ensure a continuous path, ``curve_function(0)`` must be (0, 0). Otherwise, they are used as absolute coordinates. layer : integer, list The GDSII layer numbers for the elements of each path. If the number of layers in the list is less than the number of paths, the list is repeated. datatype : integer, list The GDSII datatype for the elements of each path (between 0 and 255). If the number of datatypes in the list is less than the number of paths, the list is repeated. Returns ------- out : `Path` This object. Notes ----- The norm of the vector returned by `curve_derivative` is not important. Only the direction is used. The original GDSII specification supports only a maximum of 199 vertices per polygon. Examples -------- >>> def my_parametric_curve(t): ... return (2**t, t**2) >>> def my_parametric_curve_derivative(t): ... return (0.69315 * 2**t, 2 * t) >>> my_path.parametric(my_parametric_curve, ... my_parametric_curve_derivative) """ err = tolerance ** 2 points = list(numpy.linspace(0, 1, number_of_evaluations)) values = [numpy.array(curve_function(u)) for u in points] delta = points[1] i = 1 while i < len(points): midpoint = 0.5 * (points[i] + points[i - 1]) midvalue = numpy.array(curve_function(midpoint)) test_err = (values[i] + values[i - 1]) / 2 - midvalue if test_err[0] ** 2 + test_err[1] ** 2 > err: delta = min(delta, points[i] - midpoint) points.insert(i, midpoint) values.insert(i, midvalue) else: i += 1 points = numpy.array(points) values = numpy.array(values) dvs = values[1:] - values[:-1] self.length += ((dvs[:, 0] ** 2 + dvs[:, 1] ** 2) ** 0.5).sum() delta *= 0.5 if curve_derivative is None: derivs = numpy.vstack( ( numpy.array(curve_function(delta)) - values[0], [ numpy.array(curve_function(u + delta)) - numpy.array(curve_function(u - delta)) for u in points[1:-1] ], values[-1] - numpy.array(curve_function(1 - delta)), ) ) else: derivs = numpy.array([curve_derivative(u) for u in points]) if not callable(final_width): if final_width is None: width = numpy.full_like(points, self.w) else: width = self.w + (final_width * 0.5 - self.w) * points self.w = final_width * 0.5 else: width = numpy.array([0.5 * final_width(u) for u in points]) self.w = width[-1] if not callable(final_distance): if final_distance is None: dist = numpy.full_like(points, self.distance) else: dist = self.distance + (final_distance - self.distance) * points self.distance = final_distance else: dist = numpy.array([final_distance(u) for u in points]) self.distance = dist[-1] np = points.shape[0] sh = (np, 1) if relative: x0 = values + numpy.array((self.x, self.y)) else: x0 = values dx = ( derivs[:, ::-1] * _mpone / ((derivs[:, 0] ** 2 + derivs[:, 1] ** 2) ** 0.5).reshape(sh) ) width = width.reshape(sh) dist = dist.reshape(sh) self.x = x0[-1, 0] self.y = x0[-1, 1] self.direction = numpy.arctan2(-dx[-1, 0], dx[-1, 1]) if max_points < 4: max_points = np else: max_points = max_points // 2 i0 = 0 while i0 < np - 1: i1 = min(i0 + max_points, np) for ii in range(self.n): p1 = x0[i0:i1] + dx[i0:i1] * ( dist[i0:i1] * (ii - (self.n - 1) * 0.5) + width[i0:i1] ) p2 = ( x0[i0:i1] + dx[i0:i1] * (dist[i0:i1] * (ii - (self.n - 1) * 0.5) - width[i0:i1]) )[::-1] if width[i1 - 1, 0] == 0: p2 = p2[1:] if width[i0, 0] == 0: p1 = p1[1:] self.polygons.append(numpy.concatenate((p1, p2))) if isinstance(layer, list): self.layers.extend((layer * (self.n // len(layer) + 1))[: self.n]) else: self.layers.extend(layer for _ in range(self.n)) if isinstance(datatype, list): self.datatypes.extend( (datatype * (self.n // len(datatype) + 1))[: self.n] ) else: self.datatypes.extend(datatype for _ in range(self.n)) i0 = i1 - 1 return self def bezier( self, points, tolerance=0.01, number_of_evaluations=5, max_points=199, final_width=None, final_distance=None, relative=True, layer=0, datatype=0, ): """ Add a Bezier curve to the path. A Bezier curve is added to the path starting from its current position and finishing at the last point in the `points` array. Parameters ---------- points : array-like[N][2] Control points defining the Bezier curve. tolerance : number Acceptable tolerance for the approximation of the curve function by a finite number of evaluations. number_of_evaluations : integer Initial number of points where the curve function will be evaluated. According to `tolerance`, more evaluations will be performed. max_points : integer Elements will be fractured until each polygon has at most `max_points`. If `max_points` is zero no fracture will occur. final_width : number or function If set to a number, the paths of this segment will have their widths linearly changed from their current value to this one. If set to a function, it must be a function of one argument (that varies from 0 to 1) and returns the width of the path. final_distance : number or function If set to a number, the distance between paths is linearly change from its current value to this one. If set to a function, it must be a function of one argument (that varies from 0 to 1) and returns the width of the path. relative : bool If True, all coordinates in the `points` array are used as offsets from the current path position, i.e., if the path is at (1, -2) and the last point in the array is (10, 25), the constructed Bezier will end at (1 + 10, -2 + 25) = (11, 23). Otherwise, the points are used as absolute coordinates. layer : integer, list The GDSII layer numbers for the elements of each path. If the number of layers in the list is less than the number of paths, the list is repeated. datatype : integer, list The GDSII datatype for the elements of each path (between 0 and 255). If the number of datatypes in the list is less than the number of paths, the list is repeated. Returns ------- out : `Path` This object. Notes ----- The original GDSII specification supports only a maximum of 199 vertices per polygon. """ if relative: pts = numpy.vstack(([(0, 0)], points)) else: pts = numpy.vstack(([(self.x, self.y)], points)) dpts = (pts.shape[0] - 1) * (pts[1:] - pts[:-1]) self.parametric( _func_bezier(pts), _func_bezier(dpts), tolerance, number_of_evaluations, max_points, final_width, final_distance, relative, layer, datatype, ) return self def smooth( self, points, angles=None, curl_start=1, curl_end=1, t_in=1, t_out=1, cycle=False, tolerance=0.01, number_of_evaluations=5, max_points=199, final_widths=None, final_distances=None, relative=True, layer=0, datatype=0, ): """ Add a smooth interpolating curve through the given points. Uses the Hobby algorithm [1]_ to calculate a smooth interpolating curve made of cubic Bezier segments between each pair of points. Parameters ---------- points : array-like[N][2] Vertices in the interpolating curve. angles : array-like[N + 1] or None Tangent angles at each point (in *radians*). Any angles defined as None are automatically calculated. curl_start : number Ratio between the mock curvatures at the first point and at its neighbor. A value of 1 renders the first segment a good approximation for a circular arc. A value of 0 will better approximate a straight segment. It has no effect for closed curves or when an angle is defined for the first point. curl_end : number Ratio between the mock curvatures at the last point and at its neighbor. It has no effect for closed curves or when an angle is defined for the first point. t_in : number or array-like[N + 1] Tension parameter when arriving at each point. One value per point or a single value used for all points. t_out : number or array-like[N + 1] Tension parameter when leaving each point. One value per point or a single value used for all points. cycle : bool If True, calculates control points for a closed curve, with an additional segment connecting the first and last points. tolerance : number Acceptable tolerance for the approximation of the curve function by a finite number of evaluations. number_of_evaluations : integer Initial number of points where the curve function will be evaluated. According to `tolerance`, more evaluations will be performed. max_points : integer Elements will be fractured until each polygon has at most `max_points`. If `max_points` is zero no fracture will occur. final_widths : array-like[M] Each element corresponds to the final width of a segment in the whole curve. If an element is a number, the paths of this segment will have their widths linearly changed to this value. If a function, it must be a function of one argument (that varies from 0 to 1) and returns the width of the path. The length of the array must be equal to the number of segments in the curve, i.e., M = N - 1 for an open curve and M = N for a closed one. final_distances : array-like[M] Each element corresponds to the final distance between paths of a segment in the whole curve. If an element is a number, the distance between paths is linearly change to this value. If a function, it must be a function of one argument (that varies from 0 to 1) and returns the width of the path. The length of the array must be equal to the number of segments in the curve, i.e., M = N - 1 for an open curve and M = N for a closed one. relative : bool If True, all coordinates in the `points` array are used as offsets from the current path position, i.e., if the path is at (1, -2) and the last point in the array is (10, 25), the constructed curve will end at (1 + 10, -2 + 25) = (11, 23). Otherwise, the points are used as absolute coordinates. layer : integer, list The GDSII layer numbers for the elements of each path. If the number of layers in the list is less than the number of paths, the list is repeated. datatype : integer, list The GDSII datatype for the elements of each path (between 0 and 255). If the number of datatypes in the list is less than the number of paths, the list is repeated. Returns ------- out : `Path` This object. Notes ----- The original GDSII specification supports only a maximum of 199 vertices per polygon. References ---------- .. [1] Hobby, J.D. *Discrete Comput. Geom.* (1986) 1: 123. `DOI: 10.1007/BF02187690 `_ """ if relative: points = numpy.vstack(([(0.0, 0.0)], points)) + numpy.array( (self.x, self.y) ) else: points = numpy.vstack(([(self.x, self.y)], points)) cta, ctb = _hobby(points, angles, curl_start, curl_end, t_in, t_out, cycle) if final_widths is None: final_widths = [None] * cta.shape[0] if final_distances is None: final_distances = [None] * cta.shape[0] for i in range(points.shape[0] - 1): self.bezier( [cta[i], ctb[i], points[i + 1]], tolerance, number_of_evaluations, max_points, final_widths[i], final_distances[i], False, layer, datatype, ) if cycle: self.bezier( [cta[-1], ctb[-1], points[0]], tolerance, number_of_evaluations, max_points, final_widths[-1], final_distances[-1], False, layer, datatype, ) return self class L1Path(PolygonSet): """ Series of geometric objects that form a path or a collection of parallel paths with Manhattan geometry. .. deprecated:: 1.4 `L1Path` is deprecated in favor of FlexPath and will be removed in a future version of Gdspy. Parameters ---------- initial_point : array-like[2] Starting position of the path. direction : '+x', '+y', '-x', '-y' Starting direction of the path. width : number The initial width of each path. length : array-like Lengths of each section to add. turn : array-like Direction to turn before each section. The sign indicate the turn direction (ccw is positive), and the modulus is a multiplicative factor for the path width after each turn. Must have 1 element less then `length`. number_of_paths : positive integer Number of parallel paths to create simultaneously. distance : number Distance between the centers of adjacent paths. max_points : integer The paths will be fractured in polygons with at most `max_points` (must be at least 6). If `max_points` is zero no fracture will occur. layer : integer, list The GDSII layer numbers for the elements of each path. If the number of layers in the list is less than the number of paths, the list is repeated. datatype : integer, list The GDSII datatype for the elements of each path (between 0 and 255). If the number of datatypes in the list is less than the number of paths, the list is repeated. Attributes ---------- x : number Final position of the path in the x direction. y : number Final position of the path in the y direction. direction : '+x', '-x', '+y', '-y' or number Direction or angle (in *radians*) the path points to. The numerical angle is returned only after a rotation of the object. Examples -------- >>> length = [10, 30, 15, 15, 15, 15, 10] >>> turn = [1, -1, -1, 3, -1, 1] >>> l1path = gdspy.L1Path((0, 0), '+x', 2, length, turn) >>> myCell.add(l1path) """ __slots__ = "layers", "datatypes", "polygons", "direction", "x", "y" def __init__( self, initial_point, direction, width, length, turn, number_of_paths=1, distance=0, max_points=199, layer=0, datatype=0, ): warnings.warn( "[GDSPY] L1Path is deprecated favor of FlexPath and will be removed in a future version of Gdspy.", category=DeprecationWarning, stacklevel=2, ) if not isinstance(layer, list): layer = [layer] if not isinstance(datatype, list): datatype = [datatype] layer = (layer * (number_of_paths // len(layer) + 1))[:number_of_paths] datatype = (datatype * (number_of_paths // len(datatype) + 1))[:number_of_paths] w = width * 0.5 points = len(turn) + 1 if max_points == 0 else max_points // 2 - 1 paths = [[[], []] for ii in range(number_of_paths)] self.polygons = [] self.layers = [] self.datatypes = [] self.x = initial_point[0] self.y = initial_point[1] if direction == "+x": direction = 0 for ii in range(number_of_paths): d0 = ii * distance - (number_of_paths - 1) * distance * 0.5 paths[ii][0].append((initial_point[0], d0 + initial_point[1] - w)) paths[ii][1].append((initial_point[0], d0 + initial_point[1] + w)) elif direction == "+y": direction = 1 for ii in range(number_of_paths): d0 = (number_of_paths - 1) * distance * 0.5 - ii * distance paths[ii][0].append((d0 + initial_point[0] + w, initial_point[1])) paths[ii][1].append((d0 + initial_point[0] - w, initial_point[1])) elif direction == "-x": direction = 2 for ii in range(number_of_paths): d0 = (number_of_paths - 1) * distance * 0.5 - ii * distance paths[ii][0].append((initial_point[0], d0 + initial_point[1] + w)) paths[ii][1].append((initial_point[0], d0 + initial_point[1] - w)) elif direction == "-y": direction = 3 for ii in range(number_of_paths): d0 = ii * distance - (number_of_paths - 1) * distance * 0.5 paths[ii][0].append((d0 + initial_point[0] - w, initial_point[1])) paths[ii][1].append((d0 + initial_point[0] + w, initial_point[1])) for jj in range(len(turn)): points -= 1 if direction == 0: for ii in range(number_of_paths): d0 = ii * distance - (number_of_paths - 1) * distance * 0.5 paths[ii][0].append( (self.x + length[jj] - (d0 - w) * turn[jj], paths[ii][0][-1][1]) ) paths[ii][1].append( (self.x + length[jj] - (d0 + w) * turn[jj], paths[ii][1][-1][1]) ) self.x += length[jj] elif direction == 1: for ii in range(number_of_paths): d0 = ii * distance - (number_of_paths - 1) * distance * 0.5 paths[ii][0].append( (paths[ii][0][-1][0], self.y + length[jj] - (d0 - w) * turn[jj]) ) paths[ii][1].append( (paths[ii][1][-1][0], self.y + length[jj] - (d0 + w) * turn[jj]) ) self.y += length[jj] elif direction == 2: for ii in range(number_of_paths): d0 = (number_of_paths - 1) * distance * 0.5 - ii * distance paths[ii][0].append( (self.x - length[jj] - (d0 + w) * turn[jj], paths[ii][0][-1][1]) ) paths[ii][1].append( (self.x - length[jj] - (d0 - w) * turn[jj], paths[ii][1][-1][1]) ) self.x -= length[jj] elif direction == 3: for ii in range(number_of_paths): d0 = (number_of_paths - 1) * distance * 0.5 - ii * distance paths[ii][0].append( (paths[ii][0][-1][0], self.y - length[jj] - (d0 + w) * turn[jj]) ) paths[ii][1].append( (paths[ii][1][-1][0], self.y - length[jj] - (d0 - w) * turn[jj]) ) self.y -= length[jj] if points == 0: for p in paths: if direction % 2 == 0: min_dist = 1e300 for x1 in [p[0][-2][0], p[1][-2][0]]: for x2 in [p[0][-1][0], p[1][-1][0]]: if abs(x1 - x2) < min_dist: x0 = 0.5 * (x1 + x2) min_dist = abs(x1 - x2) p0 = (x0, p[0][-1][1]) p1 = (x0, p[1][-1][1]) else: min_dist = 1e300 for y1 in [p[0][-2][1], p[1][-2][1]]: for y2 in [p[0][-1][1], p[1][-1][1]]: if abs(y1 - y2) < min_dist: y0 = 0.5 * (y1 + y2) min_dist = abs(y1 - y2) p0 = (p[0][-1][0], y0) p1 = (p[1][-1][0], y0) self.polygons.append( numpy.array(p[0][:-1] + [p0, p1] + p[1][-2::-1]) ) p[0] = [p0, p[0][-1]] p[1] = [p1, p[1][-1]] self.layers.extend(layer) self.datatypes.extend(datatype) points = max_points // 2 - 2 if turn[jj] > 0: direction = (direction + 1) % 4 else: direction = (direction - 1) % 4 if direction == 0: for ii in range(number_of_paths): d0 = ii * distance - (number_of_paths - 1) * distance * 0.5 paths[ii][0].append((self.x + length[-1], paths[ii][0][-1][1])) paths[ii][1].append((self.x + length[-1], paths[ii][1][-1][1])) self.x += length[-1] elif direction == 1: for ii in range(number_of_paths): d0 = ii * distance - (number_of_paths - 1) * distance * 0.5 paths[ii][0].append((paths[ii][0][-1][0], self.y + length[-1])) paths[ii][1].append((paths[ii][1][-1][0], self.y + length[-1])) self.y += length[-1] elif direction == 2: for ii in range(number_of_paths): d0 = (number_of_paths - 1) * distance * 0.5 - ii * distance paths[ii][0].append((self.x - length[-1], paths[ii][0][-1][1])) paths[ii][1].append((self.x - length[-1], paths[ii][1][-1][1])) self.x -= length[-1] elif direction == 3: for ii in range(number_of_paths): d0 = (number_of_paths - 1) * distance * 0.5 - ii * distance paths[ii][0].append((paths[ii][0][-1][0], self.y - length[-1])) paths[ii][1].append((paths[ii][1][-1][0], self.y - length[-1])) self.y -= length[jj] self.direction = ["+x", "+y", "-x", "-y"][direction] self.polygons.extend(numpy.array(p[0] + p[1][::-1]) for p in paths) self.layers.extend(layer) self.datatypes.extend(datatype) def __str__(self): return "L1Path (end at ({}, {}) towards {}, {} polygons, {} vertices, layers {}, datatypes {})".format( self.x, self.y, self.direction, len(self.polygons), sum([len(p) for p in self.polygons]), list(set(self.layers)), list(set(self.datatypes)), ) def rotate(self, angle, center=(0, 0)): """ Rotate this object. Parameters ---------- angle : number The angle of rotation (in *radians*). center : array-like[2] Center point for the rotation. Returns ------- out : `L1Path` This object. """ ca = numpy.cos(angle) sa = numpy.sin(angle) * _mpone c0 = numpy.array(center) if isinstance(self.direction, basestring): self.direction = _directions_dict[self.direction] * numpy.pi self.direction += angle cur = numpy.array((self.x, self.y)) - c0 self.x, self.y = cur * ca + cur[::-1] * sa + c0 self.polygons = [ (points - c0) * ca + (points - c0)[:, ::-1] * sa + c0 for points in self.polygons ] return self class PolyPath(PolygonSet): """ Series of geometric objects that form a polygonal path or a collection of parallel polygonal paths. .. deprecated:: 1.4 `PolyPath` is deprecated in favor of FlexPath and will be removed in a future version of Gdspy. Parameters ---------- points : array-like[N][2] Points along the center of the path. width : number or array-like[N] Width of the path. If an array is given, width at each endpoint. number_of_paths : positive integer Number of parallel paths to create simultaneously. distance : number or array-like[N] Distance between the centers of adjacent paths. If an array is given, distance at each endpoint. corners : 'miter' or 'bevel' Type of joins. ends : 'flush', 'round', 'extended' Type of end caps for the paths. max_points : integer The paths will be fractured in polygons with at most `max_points` (must be at least 4). If `max_points` is zero no fracture will occur. layer : integer, list The GDSII layer numbers for the elements of each path. If the number of layers in the list is less than the number of paths, the list is repeated. datatype : integer, list The GDSII datatype for the elements of each path (between 0 and 255). If the number of datatypes in the list is less than the number of paths, the list is repeated. Notes ----- The bevel join will give strange results if the number of paths is greater than 1. """ __slots__ = "layers", "datatypes", "polygons" def __init__( self, points, width, number_of_paths=1, distance=0, corners="miter", ends="flush", max_points=199, layer=0, datatype=0, ): warnings.warn( "[GDSPY] PolyPath is deprecated favor of FlexPath and will be removed in a future version of Gdspy.", category=DeprecationWarning, stacklevel=2, ) if not isinstance(layer, list): layer = [layer] if not isinstance(datatype, list): datatype = [datatype] if hasattr(width, "__iter__"): width = numpy.array(width) * 0.5 else: width = numpy.array([width * 0.5]) len_w = len(width) if hasattr(distance, "__iter__"): distance = numpy.array(distance) else: distance = numpy.array([distance]) len_d = len(distance) points = numpy.array(points, dtype=float) self.polygons = [] self.layers = [] self.datatypes = [] if points.shape[0] == 2 and number_of_paths == 1: v = points[1] - points[0] v = v / (v[0] ** 2 + v[1] ** 2) ** 0.5 w0 = width[0] w1 = width[1 % len_w] if ends == "round": a = numpy.arctan2(v[1], v[0]) + _halfpi self.polygons.append( Round( points[0], w0, initial_angle=a, final_angle=a + numpy.pi, number_of_points=33, ).polygons[0] ) self.polygons.append( Round( points[1], w1, initial_angle=a - numpy.pi, final_angle=a, number_of_points=33, ).polygons[0] ) self.layers.extend(layer[:1] * 2) self.datatypes.extend(datatype[:1] * 2) if ends == "extended": points[0] = points[0] - v * w0 points[1] = points[1] + v * w1 u = numpy.array((-v[1], v[0])) if w0 == 0: self.polygons.append( numpy.array((points[0], points[1] - u * w1, points[1] + u * w1)) ) elif w1 == 0: self.polygons.append( numpy.array((points[0] + u * w0, points[0] - u * w0, points[1])) ) else: self.polygons.append( numpy.array( ( points[0] + u * w0, points[0] - u * w0, points[1] - u * w1, points[1] + u * w1, ) ) ) self.layers.append(layer[0]) self.datatypes.append(datatype[0]) return if corners not in ["miter", "bevel"]: if corners in [0, 1]: corners = ["miter", "bevel"][corners] warnings.warn( "[GDSPY] Argument corners must be one of 'miter' or 'bevel'.", category=DeprecationWarning, stacklevel=2, ) else: raise ValueError( "[GDSPY] Argument corners must be one of 'miter' or 'bevel'." ) bevel = corners == "bevel" if ends not in ["flush", "round", "extended"]: if ends in [0, 1, 2]: ends = ["flush", "round", "extended"][ends] warnings.warn( "[GDSPY] Argument ends must be one of 'flush', 'round', or 'extended'.", category=DeprecationWarning, stacklevel=2, ) else: raise ValueError( "[GDSPY] Argument ends must be one of 'flush', 'round', or 'extended'." ) if ends == "extended": v = points[0] - points[1] v = v / (v[0] ** 2 + v[1] ** 2) ** 0.5 points[0] = points[0] + v * width[0] v = points[-1] - points[-2] v = v / (v[0] ** 2 + v[1] ** 2) ** 0.5 points[-1] = points[-1] + v * width[(points.shape[0] - 1) % len_w] elif ends == "round": v0 = points[1] - points[0] angle0 = numpy.arctan2(v0[1], v0[0]) + _halfpi v0 = numpy.array((-v0[1], v0[0])) / (v0[0] ** 2 + v0[1] ** 2) ** 0.5 d0 = 0.5 * (number_of_paths - 1) * distance[0] v1 = points[-1] - points[-2] angle1 = numpy.arctan2(v1[1], v1[0]) - _halfpi v1 = numpy.array((-v1[1], v1[0])) / (v1[0] ** 2 + v1[1] ** 2) ** 0.5 j1w = (points.shape[0] - 1) % len_w j1d = (points.shape[0] - 1) % len_d d1 = 0.5 * (number_of_paths - 1) * distance[j1d] self.polygons.extend( ( Round( points[0] + v0 * (ii * distance[0] - d0), width[0], initial_angle=angle0, final_angle=angle0 + numpy.pi, number_of_points=33, ).polygons[0] for ii in range(number_of_paths) ) ) self.polygons.extend( ( Round( points[-1] + v1 * (ii * distance[j1d] - d1), width[j1w], initial_angle=angle1, final_angle=angle1 + numpy.pi, number_of_points=33, ).polygons[0] ) for ii in range(number_of_paths) ) self.layers.extend( ((layer * (number_of_paths // len(layer) + 1))[:number_of_paths]) * 2 ) self.datatypes.extend( ((datatype * (number_of_paths // len(datatype) + 1))[:number_of_paths]) * 2 ) v = points[1] - points[0] v = numpy.array((-v[1], v[0])) / (v[0] ** 2 + v[1] ** 2) ** 0.5 d0 = 0.5 * (number_of_paths - 1) * distance[0] d1 = 0.5 * (number_of_paths - 1) * distance[1 % len_d] paths = [ [ [points[0] + (ii * distance[0] - d0 - width[0]) * v], [points[0] + (ii * distance[0] - d0 + width[0]) * v], ] for ii in range(number_of_paths) ] p1 = [ ( points[1] + (ii * distance[1 % len_d] - d1 - width[1 % len_w]) * v, points[1] + (ii * distance[1 % len_d] - d1 + width[1 % len_w]) * v, ) for ii in range(number_of_paths) ] for jj in range(1, points.shape[0] - 1): j0d = jj % len_d j0w = jj % len_w j1d = (jj + 1) % len_d j1w = (jj + 1) % len_w v = points[jj + 1] - points[jj] v = numpy.array((-v[1], v[0])) / (v[0] ** 2 + v[1] ** 2) ** 0.5 d0 = d1 d1 = 0.5 * (number_of_paths - 1) * distance[j1d] p0 = p1 p1 = [] pp = [] for ii in range(number_of_paths): pp.append( ( points[jj] + (ii * distance[j0d] - d0 - width[j0w]) * v, points[jj] + (ii * distance[j0d] - d0 + width[j0w]) * v, ) ) p1.append( ( points[jj + 1] + (ii * distance[j1d] - d1 - width[j1w]) * v, points[jj + 1] + (ii * distance[j1d] - d1 + width[j1w]) * v, ) ) for kk in (0, 1): p0m = paths[ii][kk][-1] - p0[ii][kk] p1p = pp[ii][kk] - p1[ii][kk] vec = p0m[0] * p1p[1] - p1p[0] * p0m[1] if abs(vec) > 1e-30: p = ( _pmone * ( p0m * p1p[::-1] * p1[ii][kk] - p1p * p0m[::-1] * p0[ii][kk] + p0m * p1p * (p0[ii][kk][::-1] - p1[ii][kk][::-1]) ) / vec ) l0 = (p - pp[ii][kk]) * p1p l1 = (p - p0[ii][kk]) * p0m if bevel and l0[0] + l0[1] > 0 and l1[0] + l1[1] < 0: paths[ii][kk].append(p0[ii][kk]) paths[ii][kk].append(pp[ii][kk]) else: paths[ii][kk].append(p) if ( max_points > 0 and len(paths[ii][0]) + len(paths[ii][1]) + 3 > max_points ): diff = paths[ii][0][0] - paths[ii][1][0] if diff[0] ** 2 + diff[1] ** 2 == 0: paths[ii][1] = paths[ii][1][1:] diff = paths[ii][0][-1] - paths[ii][1][-1] if diff[0] ** 2 + diff[1] ** 2 == 0: self.polygons.append( numpy.array(paths[ii][0] + paths[ii][1][-2::-1]) ) else: self.polygons.append( numpy.array(paths[ii][0] + paths[ii][1][::-1]) ) paths[ii][0] = paths[ii][0][-1:] paths[ii][1] = paths[ii][1][-1:] self.layers.append(layer[ii % len(layer)]) self.datatypes.append(datatype[ii % len(datatype)]) for ii in range(number_of_paths): diff = paths[ii][0][0] - paths[ii][1][0] if diff[0] ** 2 + diff[1] ** 2 == 0: paths[ii][1] = paths[ii][1][1:] diff = p1[ii][0] - p1[ii][1] if diff[0] ** 2 + diff[1] ** 2 != 0: paths[ii][0].append(p1[ii][0]) paths[ii][1].append(p1[ii][1]) self.polygons.extend(numpy.array(pol[0] + pol[1][::-1]) for pol in paths) self.layers.extend( (layer * (number_of_paths // len(layer) + 1))[:number_of_paths] ) self.datatypes.extend( (datatype * (number_of_paths // len(datatype) + 1))[:number_of_paths] ) def __str__(self): return "PolyPath ({} polygons, {} vertices, layers {}, datatypes {})".format( len(self.polygons), sum([len(p) for p in self.polygons]), list(set(self.layers)), list(set(self.datatypes)), ) class _SubPath(object): """ Single path component. """ __slots__ = "x", "dx", "off", "wid", "h", "err", "max_evals" def __init__(self, x, dx, off, wid, tolerance, max_evals): self.x = x self.dx = dx self.off = off self.wid = wid self.err = tolerance ** 2 self.h = 0.5 / max_evals self.max_evals = max_evals def __str__(self): return "SubPath ({} - {})".format(self(0, 1e-6, 0), self(1, 1e-6, 0)) def __call__(self, u, arm): v = self.dx(u, self.h)[::-1] * _pmone v /= (v[0] ** 2 + v[1] ** 2) ** 0.5 x = self.x(u) + self.off(u) * v if arm == 0: return x u0 = max(0, u - self.h) u1 = min(1, u + self.h) w = (self(u1, 0) - self(u0, 0))[::-1] * _pmone w /= (w[0] ** 2 + w[1] ** 2) ** 0.5 if arm < 0: return x - 0.5 * self.wid(u) * w return x + 0.5 * self.wid(u) * w def grad(self, u, arm): u0 = max(0, u - self.h) u1 = min(1, u + self.h) return (self(u1, arm) - self(u0, arm)) / (u1 - u0) def points(self, u0, u1, arm): u = [u0, u1] pts = [numpy.array(self(u[0], arm)), numpy.array(self(u[1], arm))] i = 1 while i < len(pts) < self.max_evals: f = 0.2 while f < 1: test_u = u[i - 1] * (1 - f) + u[i] * f test_pt = numpy.array(self(test_u, arm)) test_err = pts[i - 1] * (1 - f) + pts[i] * f - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > self.err: u.insert(i, test_u) pts.insert(i, test_pt) f = 1 i -= 1 else: f += 0.3 i += 1 return pts class FlexPath(object): """ Path object. This class keeps information about the constructive parameters of the path and calculates its boundaries only upon request. It can be stored as a proper path element in the GDSII format, unlike `Path`. In this case, the width must be constant along the whole path. Parameters ---------- points : array-like[N][2] Points along the center of the path. width : number, list Width of each parallel path being created. The number of parallel paths being created is defined by the length of this list. offset : number, list Offsets of each parallel path from the center. If `width` is not a list, the length of this list is used to determine the number of parallel paths being created. Otherwise, offset must be a list with the same length as width, or a number, which is used as distance between adjacent paths. corners : 'natural', 'miter', 'bevel', 'round', 'smooth', 'circular bend', callable, list Type of joins. A callable must receive 6 arguments (vertex and direction vector from both segments being joined, the center and width of the path) and return a list of vertices that make the join. A list can be used to define the join for each parallel path. ends : 'flush', 'extended', 'round', 'smooth', 2-tuple, callable, list Type of end caps for the paths. A 2-element tuple represents the start and end extensions to the paths. A callable must receive 4 arguments (vertex and direction vectors from both sides of the path and return a list of vertices that make the end cap. A list can be used to define the end type for each parallel path. bend_radius : number, list Bend radii for each path when `corners` is 'circular bend'. It has no effect for other corner types. tolerance : number Tolerance used to draw the paths and calculate joins. precision : number Precision for rounding the coordinates of vertices when fracturing the final polygonal boundary. max_points : integer If the number of points in the polygonal path boundary is greater than `max_points`, it will be fractured in smaller polygons with at most `max_points` each. If `max_points` is zero no fracture will occur. gdsii_path : bool If True, treat this object as a GDSII path element. Otherwise, it will be converted into polygonal boundaries when required. width_transform : bool If `gdsii_path` is True, this flag indicates whether the width of the path should transform when scaling this object. It has no effect when `gdsii_path` is False. layer : integer, list The GDSII layer numbers for the elements of each path. If the number of layers in the list is less than the number of paths, the list is repeated. datatype : integer, list The GDSII datatype for the elements of each path (between 0 and 255). If the number of datatypes in the list is less than the number of paths, the list is repeated. Notes ----- The value of `tolerance` should not be smaller than `precision`, otherwise there would be wasted computational effort in calculating the paths. """ __slots__ = ( "n", "ends", "corners", "points", "offsets", "widths", "layers", "datatypes", "tolerance", "precision", "max_points", "gdsii_path", "width_transform", "bend_radius", "_polygon_dict", ) _pathtype_dict = {"flush": 0, "round": 1, "extended": 2, "smooth": 1} def __init__( self, points, width, offset=0, corners="natural", ends="flush", bend_radius=None, tolerance=0.01, precision=1e-3, max_points=199, gdsii_path=False, width_transform=True, layer=0, datatype=0, ): self._polygon_dict = None if isinstance(width, list): self.n = len(width) self.widths = width if isinstance(offset, list): self.offsets = offset else: self.offsets = [ (i - 0.5 * (self.n - 1)) * offset for i in range(self.n) ] else: if isinstance(offset, list): self.n = len(offset) self.offsets = offset else: self.n = 1 self.offsets = [offset] self.widths = [width] * self.n self.widths = numpy.tile(self.widths, (len(points), 1)) self.offsets = numpy.tile(self.offsets, (len(points), 1)) self.points = numpy.array(points) if isinstance(ends, list): self.ends = [ends[i % len(ends)] for i in range(self.n)] else: self.ends = [ends for _ in range(self.n)] if isinstance(corners, list): self.corners = [corners[i % len(corners)] for i in range(self.n)] else: self.corners = [corners for _ in range(self.n)] if isinstance(bend_radius, list): self.bend_radius = [ bend_radius[i % len(bend_radius)] for i in range(self.n) ] else: self.bend_radius = [bend_radius for _ in range(self.n)] if isinstance(layer, list): self.layers = [layer[i % len(layer)] for i in range(self.n)] else: self.layers = [layer] * self.n if isinstance(datatype, list): self.datatypes = [datatype[i % len(datatype)] for i in range(self.n)] else: self.datatypes = [datatype] * self.n self.tolerance = tolerance self.precision = precision self.max_points = max_points self.gdsii_path = gdsii_path self.width_transform = width_transform if self.gdsii_path: if any(end == "smooth" or callable(end) for end in self.ends): warnings.warn( "[GDSPY] Smooth and custom end caps are not supported in `FlexPath` with `gdsii_path == True`.", stacklevel=3, ) if any( corner != "natural" and corner != "circular bend" for corner in self.corners ): warnings.warn( "[GDSPY] Corner specification not supported in `FlexPath` with `gdsii_path == True`.", stacklevel=3, ) def __str__(self): if self.n > 1: return "FlexPath (x{}, {} segments, layers {}, datatypes {})".format( self.n, self.points.shape[0], self.layers, self.datatypes ) else: return "FlexPath ({} segments, layer {}, datatype {})".format( self.points.shape[0], self.layers[0], self.datatypes[0] ) def get_polygons(self, by_spec=False): """ Calculate the polygonal boundaries described by this path. Parameters ---------- by_spec : bool If True, the return value is a dictionary with the polygons of each individual pair (layer, datatype). Returns ------- out : list of array-like[N][2] or dictionary List containing the coordinates of the vertices of each polygon, or dictionary with the list of polygons (if `by_spec` is True). """ if self._polygon_dict is None: self._polygon_dict = {} if self.points.shape[0] == 2: # Common case: single path with 2 points un = self.points[1] - self.points[0] if un[0] == 0 and un[1] == 0: return {} if by_spec else [] un = un[::-1] * _mpone / (un[0] ** 2 + un[1] ** 2) ** 0.5 for kk in range(self.n): end = self.ends[kk] pts = numpy.array( ( self.points[0] + un * self.offsets[0, kk], self.points[1] + un * self.offsets[1, kk], ) ) vn = pts[1] - pts[0] vn = vn[::-1] * _mpone / (vn[0] ** 2 + vn[1] ** 2) ** 0.5 v = ( vn * (0.5 * self.widths[0, kk]), vn * (0.5 * self.widths[1, kk]), ) poly = numpy.array( (pts[0] - v[0], pts[0] + v[0], pts[1] + v[1], pts[1] - v[1]) ) if end != "flush": v0 = poly[3] - poly[0] v1 = poly[2] - poly[1] if callable(end): cap0 = end(poly[0], -v0, poly[1], v1) cap1 = end(poly[3], v0, poly[2], -v1) poly = numpy.array(cap0[::-1] + cap1) elif end == "smooth": angles = [ numpy.arctan2(-v0[1], -v0[0]), numpy.arctan2(v1[1], v1[0]), ] cta, ctb = _hobby(poly[:2], angles) f = _func_bezier( numpy.array([poly[0], cta[0], ctb[0], poly[1]]) ) tol = self.tolerance ** 2 uu = [0, 1] fu = [f(0), f(1)] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 cap = fu cta, ctb = _hobby(poly[2:], [angles[1], angles[0]]) f = _func_bezier( numpy.array([poly[2], cta[0], ctb[0], poly[3]]) ) tol = self.tolerance ** 2 uu = [0, 1] fu = [f(0), f(1)] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 cap.extend(fu) poly = numpy.array(cap) elif end == "round": v = pts[1] - pts[0] r = 0.5 * self.widths[0, kk] np = max( 5, 1 + int( _halfpi / numpy.arccos(1 - self.tolerance / r) + 0.5 ), ) ang = numpy.linspace(_halfpi, -_halfpi, np) + numpy.arctan2( -v[1], -v[0] ) poly = ( pts[0] + r * numpy.vstack((numpy.cos(ang), numpy.sin(ang))).T ) r = 0.5 * self.widths[1, kk] np = max( 5, 1 + int( _halfpi / numpy.arccos(1 - self.tolerance / r) + 0.5 ), ) ang = numpy.linspace(_halfpi, -_halfpi, np) + numpy.arctan2( v[1], v[0] ) poly = numpy.vstack( ( poly, pts[1] + r * numpy.vstack((numpy.cos(ang), numpy.sin(ang))).T, ) ) else: # 'extended'/list v = pts[1] - pts[0] v /= (v[0] ** 2 + v[1] ** 2) ** 0.5 if end == "extended": v0 = 0.5 * self.widths[0, kk] * v v1 = 0.5 * self.widths[1, kk] * v else: v0 = end[0] * v v1 = end[1] * v if self.widths[0, kk] == self.widths[1, kk]: poly[0] -= v0 poly[1] -= v0 poly[2] += v1 poly[3] += v1 else: poly = numpy.array( ( poly[0], poly[0] - v0, poly[1] - v0, poly[1], poly[2], poly[2] + v1, poly[3] + v1, poly[3], ) ) polygons = [poly] if self.max_points > 4 and poly.shape[0] > self.max_points: ii = 0 while ii < len(polygons): if len(polygons[ii]) > self.max_points: pts0 = sorted(polygons[ii][:, 0]) pts1 = sorted(polygons[ii][:, 1]) ncuts = len(pts0) // self.max_points if pts0[-1] - pts0[0] > pts1[-1] - pts1[0]: # Vertical cuts cuts = [ pts0[int(i * len(pts0) / (ncuts + 1.0) + 0.5)] for i in range(1, ncuts + 1) ] chopped = clipper._chop( polygons[ii], cuts, 0, 1 / self.precision ) else: # Horizontal cuts cuts = [ pts1[int(i * len(pts1) / (ncuts + 1.0) + 0.5)] for i in range(1, ncuts + 1) ] chopped = clipper._chop( polygons[ii], cuts, 1, 1 / self.precision ) polygons.pop(ii) polygons.extend( numpy.array(x) for x in itertools.chain.from_iterable(chopped) ) else: ii += 1 key = (self.layers[kk], self.datatypes[kk]) if key in self._polygon_dict: self._polygon_dict[key].extend(polygons) else: self._polygon_dict[key] = polygons else: # More than 1 path or more than 2 points un = self.points[1:] - self.points[:-1] un2 = un[:, 0] ** 2 + un[:, 1] ** 2 if not un2.all(): nz = [0] nz.extend(un2.nonzero()[0] + 1) self.points = self.points[nz, :] self.widths = self.widths[nz, :] self.offsets = self.offsets[nz, :] un = self.points[1:] - self.points[:-1] un2 = un[:, 0] ** 2 + un[:, 1] ** 2 un = un[:, ::-1] * _mpone / (un2 ** 0.5).reshape((un.shape[0], 1)) for kk in range(self.n): corner = self.corners[kk] end = self.ends[kk] if any(self.offsets[:, kk] != 0): pts = numpy.empty(self.points.shape) sa = self.points[:-1] + un * self.offsets[:-1, kk : kk + 1] sb = self.points[1:] + un * self.offsets[1:, kk : kk + 1] vn = sb - sa den = vn[1:, 0] * vn[:-1, 1] - vn[1:, 1] * vn[:-1, 0] idx = numpy.nonzero( den ** 2 < 1e-12 * (vn[1:, 0] ** 2 + vn[1:, 1] ** 2) * (vn[:-1, 0] ** 2 + vn[:-1, 1] ** 2) )[0] if len(idx) > 0: den[idx] = 1 ds = sb[:-1] - sa[1:] u0 = (vn[1:, 1] * ds[:, 0] - vn[1:, 0] * ds[:, 1]) / den u1 = (vn[:-1, 1] * ds[:, 0] - vn[:-1, 0] * ds[:, 1]) / den if any(u0 < -1) or any(u1 > 1): warnings.warn( "[GDSPY] Possible inconsistency found in `FlexPath` due to sharp corner." ) pts[1:-1] = sb[:-1] + u0.reshape((u0.shape[0], 1)) * vn[:-1] if len(idx) > 0: pts[idx + 1] = 0.5 * (sa[idx + 1] + sb[idx]) pts[0] = sa[0] pts[-1] = sb[-1] else: pts = self.points vn = pts[1:] - pts[:-1] vn = ( vn[:, ::-1] * _mpone / ((vn[:, 0] ** 2 + vn[:, 1] ** 2) ** 0.5).reshape( (vn.shape[0], 1) ) ) arms = [[], []] caps = [[], []] for ii in (0, 1): sign = -1 if ii == 0 else 1 pa = pts[:-1] + vn * ( sign * 0.5 * self.widths[:-1, kk : kk + 1] ) pb = pts[1:] + vn * (sign * 0.5 * self.widths[1:, kk : kk + 1]) vec = pb - pa caps[0].append(pa[0]) caps[1].append(pb[-1]) for jj in range(1, self.points.shape[0] - 1): p0 = pb[jj - 1] v0 = vec[jj - 1] p1 = pa[jj] v1 = vec[jj] half_w = 0.5 * self.widths[jj, kk] if corner == "natural": v0 = v0 * (half_w / (v0[0] ** 2 + v0[1] ** 2) ** 0.5) v1 = v1 * (half_w / (v1[0] ** 2 + v1[1] ** 2) ** 0.5) den = v1[1] * v0[0] - v1[0] * v0[1] if den ** 2 < 1e-12 * half_w ** 4: u0 = u1 = 0 p = 0.5 * (p0 + p1) else: dx = p1[0] - p0[0] dy = p1[1] - p0[1] u0 = (v1[1] * dx - v1[0] * dy) / den u1 = (v0[1] * dx - v0[0] * dy) / den p = 0.5 * (p0 + v0 * u0 + p1 + v1 * u1) if u0 < 0 and u1 > 0: arms[ii].append(p) elif u0 <= 1 and u1 >= -1: arms[ii].append( 0.5 * (p0 + min(1, u0) * v0 + p1 + max(-1, u1) * v1) ) else: arms[ii].append(p0 + min(1, u0) * v0) arms[ii].append(p1 + max(-1, u1) * v1) elif corner == "circular bend": v2 = p0 - pts[jj] direction = v0[0] * v1[1] - v0[1] * v1[0] if direction == 0: arms[ii].append(0.5 * (p0 + p1)) else: if direction > 0: a0 = numpy.arctan2(-v0[0], v0[1]) a1 = numpy.arctan2(-v1[0], v1[1]) else: a0 = numpy.arctan2(v0[0], -v0[1]) a1 = numpy.arctan2(v1[0], -v1[1]) if abs(a1 - a0) > numpy.pi: if a1 > a0: a0 += 2 * numpy.pi else: a1 += 2 * numpy.pi side = direction * (v0[0] * v2[1] - v0[1] * v2[0]) if side > 0: r = self.bend_radius[kk] - half_w else: r = self.bend_radius[kk] + half_w da = 0.5 * abs(a1 - a0) d = ( self.bend_radius[kk] * numpy.tan(da) / (v0[0] ** 2 + v0[1] ** 2) ** 0.5 ) np = max( 2, 1 + int( da / numpy.arccos(1 - self.tolerance / r) + 0.5 ), ) angles = numpy.linspace(a0, a1, np) points = ( r * numpy.vstack( (numpy.cos(angles), numpy.sin(angles)) ).T ) arms[ii].extend(points - points[0] + p0 - d * v0) elif callable(corner): arms[ii].extend( corner(p0, v0, p1, v1, pts[jj], self.widths[jj, kk]) ) else: den = v1[1] * v0[0] - v1[0] * v0[1] lim = ( 1e-12 * (v0[0] ** 2 + v0[1] ** 2) * (v1[0] ** 2 + v1[1] ** 2) ) if den ** 2 < lim: u0 = u1 = 0 p = 0.5 * (p0 + p1) else: dx = p1[0] - p0[0] dy = p1[1] - p0[1] u0 = (v1[1] * dx - v1[0] * dy) / den u1 = (v0[1] * dx - v0[0] * dy) / den p = 0.5 * (p0 + v0 * u0 + p1 + v1 * u1) if corner == "miter": arms[ii].append(p) elif u0 <= 0 and u1 >= 0: arms[ii].append(p) elif corner == "bevel": arms[ii].append(p0) arms[ii].append(p1) elif corner == "round": if v0[1] * v1[0] - v0[0] * v1[1] < 0: a0 = numpy.arctan2(-v0[0], v0[1]) a1 = numpy.arctan2(-v1[0], v1[1]) else: a0 = numpy.arctan2(v0[0], -v0[1]) a1 = numpy.arctan2(v1[0], -v1[1]) if abs(a1 - a0) > numpy.pi: if a0 < a1: a0 += 2 * numpy.pi else: a1 += 2 * numpy.pi np = max( 4, 1 + int( 0.5 * abs(a1 - a0) / numpy.arccos(1 - self.tolerance / half_w) + 0.5 ), ) angles = numpy.linspace(a0, a1, np) arms[ii].extend( pts[jj] + half_w * numpy.vstack( (numpy.cos(angles), numpy.sin(angles)) ).T ) elif corner == "smooth": angles = [ numpy.arctan2(v0[1], v0[0]), numpy.arctan2(v1[1], v1[0]), ] bezpts = numpy.vstack((p0, p1)) cta, ctb = _hobby(bezpts, angles) f = _func_bezier( numpy.array( [bezpts[0], cta[0], ctb[0], bezpts[1]] ) ) tol = self.tolerance ** 2 uu = [0, 1] fu = [f(0), f(1)] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 arms[ii].extend(fu) if end != "flush": for ii in (0, 1): if callable(end): vecs = [ caps[ii][0] - arms[0][-ii], arms[1][-ii] - caps[ii][1], ] caps[ii] = end( caps[ii][0], vecs[0], caps[ii][1], vecs[1] ) elif end == "smooth": points = numpy.array(caps[ii]) vecs = [ caps[ii][0] - arms[0][-ii], arms[1][-ii] - caps[ii][1], ] angles = [ numpy.arctan2(vecs[0][1], vecs[0][0]), numpy.arctan2(vecs[1][1], vecs[1][0]), ] cta, ctb = _hobby(points, angles) f = _func_bezier( numpy.array([points[0], cta[0], ctb[0], points[1]]) ) tol = self.tolerance ** 2 uu = [0, 1] fu = [f(0), f(1)] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 caps[ii] = fu elif end == "round": v = pts[0] - pts[1] if ii == 0 else pts[-1] - pts[-2] r = 0.5 * self.widths[-ii, kk] np = max( 5, 1 + int( _halfpi / numpy.arccos(1 - self.tolerance / r) + 0.5 ), ) ang = (2 * ii - 1) * numpy.linspace( -_halfpi, _halfpi, np ) + numpy.arctan2(v[1], v[0]) caps[ii] = list( pts[-ii] + r * numpy.vstack((numpy.cos(ang), numpy.sin(ang))).T ) else: # 'extended'/list v = pts[0] - pts[1] if ii == 0 else pts[-1] - pts[-2] v = v / (v[0] ** 2 + v[1] ** 2) ** 0.5 w = (2 * ii - 1) * v[::-1] * _pmone r = 0.5 * self.widths[-ii, kk] d = r if end == "extended" else end[ii] caps[ii] = [ pts[-ii] + r * w, pts[-ii] + r * w + d * v, pts[-ii] - r * w + d * v, pts[-ii] - r * w, ] poly = caps[0][::-1] poly.extend(arms[0]) poly.extend(caps[1]) poly.extend(arms[1][::-1]) polygons = [numpy.array(poly)] if self.max_points > 4 and polygons[0].shape[0] > self.max_points: ii = 0 while ii < len(polygons): if len(polygons[ii]) > self.max_points: pts0 = sorted(polygons[ii][:, 0]) pts1 = sorted(polygons[ii][:, 1]) ncuts = len(pts0) // self.max_points if pts0[-1] - pts0[0] > pts1[-1] - pts1[0]: # Vertical cuts cuts = [ pts0[int(i * len(pts0) / (ncuts + 1.0) + 0.5)] for i in range(1, ncuts + 1) ] chopped = clipper._chop( polygons[ii], cuts, 0, 1 / self.precision ) else: # Horizontal cuts cuts = [ pts1[int(i * len(pts1) / (ncuts + 1.0) + 0.5)] for i in range(1, ncuts + 1) ] chopped = clipper._chop( polygons[ii], cuts, 1, 1 / self.precision ) polygons.pop(ii) polygons.extend( numpy.array(x) for x in itertools.chain.from_iterable(chopped) ) else: ii += 1 key = (self.layers[kk], self.datatypes[kk]) if key in self._polygon_dict: self._polygon_dict[key].extend(polygons) else: self._polygon_dict[key] = polygons if by_spec: return libcopy.deepcopy(self._polygon_dict) else: return list(itertools.chain.from_iterable(self._polygon_dict.values())) def to_polygonset(self): """ Create a `PolygonSet` representation of this object. The resulting object will be fractured according to the parameter `max_points` used when instantiating this object. Returns ------- out : `PolygonSet` or None A `PolygonSet` that contains all boundaries for this path. If the path is empty, returns None. """ if self.points.shape[0] < 2: return None polygons = self.get_polygons(True) pol = PolygonSet([]) for k, v in polygons.items(): pol.layers.extend([k[0]] * len(v)) pol.datatypes.extend([k[1]] * len(v)) pol.polygons.extend(v) return pol.fracture(self.max_points, self.precision) def to_gds(self, multiplier): """ Convert this object to a series of GDSII elements. If `FlexPath.gdsii_path` is True, GDSII path elements are created instead of boundaries. Such paths do not support variable widths, but their memeory footprint is smaller than full polygonal boundaries. Parameters ---------- multiplier : number A number that multiplies all dimensions written in the GDSII elements. Returns ------- out : string The GDSII binary string that represents this object. """ if len(self.points) == 0: return b"" if self.gdsii_path: sign = 1 if self.width_transform else -1 else: return self.to_polygonset().to_gds(multiplier) data = [] un = self.points[1:] - self.points[:-1] un2 = un[:, 0] ** 2 + un[:, 1] ** 2 if not un2.all(): nz = [0] nz.extend(un2.nonzero()[0] + 1) self.points = self.points[nz, :] self.widths = self.widths[nz, :] self.offsets = self.offsets[nz, :] un = self.points[1:] - self.points[:-1] un2 = un[:, 0] ** 2 + un[:, 1] ** 2 un = un[:, ::-1] * _mpone / (un2 ** 0.5).reshape((un.shape[0], 1)) for ii in range(self.n): pathtype = ( 0 if callable(self.ends[ii]) else FlexPath._pathtype_dict.get(self.ends[ii], 4) ) data.append( struct.pack( ">4Hh2Hh2Hh2Hl", 4, 0x0900, 6, 0x0D02, self.layers[ii], 6, 0x0E02, self.datatypes[ii], 6, 0x2102, pathtype, 8, 0x0F03, sign * int(round(self.widths[0, ii] * multiplier)), ) ) if pathtype == 4: data.append( struct.pack( ">2Hl2Hl", 8, 0x3003, int(round(self.ends[ii][0] * multiplier)), 8, 0x3103, int(round(self.ends[ii][1] * multiplier)), ) ) if any(self.offsets[:, ii] != 0): points = numpy.zeros(self.points.shape) sa = self.points[:-1] + un * self.offsets[:-1, ii : ii + 1] sb = self.points[1:] + un * self.offsets[1:, ii : ii + 1] vn = sb - sa den = vn[1:, 0] * vn[:-1, 1] - vn[1:, 1] * vn[:-1, 0] idx = numpy.nonzero( den ** 2 < 1e-12 * (vn[1:, 0] ** 2 + vn[1:, 1] ** 2) * (vn[:-1, 0] ** 2 + vn[:-1, 1] ** 2) )[0] if len(idx) > 0: den[idx] = 1 u0 = ( vn[1:, 1] * (sb[:-1, 0] - sa[1:, 0]) - vn[1:, 0] * (sb[:-1, 1] - sa[1:, 1]) ) / den points[1:-1] = sb[:-1] + u0.reshape((u0.shape[0], 1)) * vn[:-1] if len(idx) > 0: points[idx + 1] = 0.5 * (sa[idx + 1] + sb[idx]) points[0] = sa[0] points[-1] = sb[-1] else: points = self.points if self.corners[ii] == "circular bend": r = self.bend_radius[ii] p0 = points[0] p1 = points[1] v0 = p1 - p0 bends = [p0] for jj in range(1, points.shape[0] - 1): p2 = points[jj + 1] v1 = p2 - p1 direction = v0[0] * v1[1] - v0[1] * v1[0] if direction == 0: bends.append(p1) else: if direction > 0: a0 = numpy.arctan2(-v0[0], v0[1]) a1 = numpy.arctan2(-v1[0], v1[1]) elif direction < 0: a0 = numpy.arctan2(v0[0], -v0[1]) a1 = numpy.arctan2(v1[0], -v1[1]) if abs(a1 - a0) > numpy.pi: if a1 > a0: a0 += 2 * numpy.pi else: a1 += 2 * numpy.pi da = 0.5 * abs(a1 - a0) d = r * numpy.tan(da) / (v0[0] ** 2 + v0[1] ** 2) ** 0.5 np = max( 2, 1 + int( da / numpy.arccos( 1 - self.tolerance / (r + 0.5 * self.widths[0, ii]) ) + 0.5 ), ) angles = numpy.linspace(a0, a1, np) bpts = ( r * numpy.vstack((numpy.cos(angles), numpy.sin(angles))).T ) bends.extend(bpts - bpts[0] + p1 - d * v0) p0 = p1 p1 = p2 v0 = v1 bends.append(p1) points = numpy.array(bends) points = numpy.round(points * multiplier).astype(">i4") if points.shape[0] > 8191: warnings.warn( "[GDSPY] Paths with more than 8191 are not supported by the official GDSII specification. This GDSII file might not be compatible with all readers.", stacklevel=4, ) i0 = 0 while i0 < points.shape[0]: i1 = min(i0 + 8191, points.shape[0]) data.append(struct.pack(">2H", 4 + 8 * (i1 - i0), 0x1003)) data.append(points[i0:i1].tostring()) i0 = i1 else: data.append(struct.pack(">2H", 4 + 8 * points.shape[0], 0x1003)) data.append(points.tostring()) data.append(struct.pack(">2H", 4, 0x1100)) return b"".join(data) def area(self, by_spec=False): """ Calculate the total area of this object. This functions creates a `PolgonSet` from this object and calculates its area, which means it is computationally expensive. Parameters ---------- by_spec : bool If True, the return value is a dictionary with ``{(layer, datatype): area}``. Returns ------- out : number, dictionary Area of this object. """ return self.to_polygonset().area(by_spec) def translate(self, dx, dy): """ Translate this path. Parameters ---------- dx : number Distance to move in the x-direction dy : number Distance to move in the y-direction Returns ------- out : `FlexPath` This object. """ self._polygon_dict = None self.points = self.points + numpy.array((dx, dy)) return self def rotate(self, angle, center=(0, 0)): """ Rotate this path. Parameters ---------- angle : number The angle of rotation (in *radians*). center : array-like[2] Center point for the rotation. Returns ------- out : `FlexPath` This object. """ self._polygon_dict = None ca = numpy.cos(angle) sa = numpy.sin(angle) * _mpone c0 = numpy.array(center) x = self.points - c0 self.points = x * ca + x[:, ::-1] * sa + c0 return self def scale(self, scale, center=(0, 0)): """ Scale this path. Parameters ---------- scale : number Scaling factor. center : array-like[2] Center point for the scaling operation. Returns ------- out : `FlexPath` This object. """ self._polygon_dict = None c0 = numpy.array(center) * (1 - scale) self.points = self.points * scale + c0 self.widths = self.widths * scale self.offsets = self.offsets * scale for i, end in enumerate(self.paths.ends): # CustomPlus created by bgnextn and endextn if isinstance(end, tuple): self.paths.ends[i] = tuple([e * scale for e in end]) return self def transform(self, translation, rotation, scale, x_reflection, array_trans=None): """ Apply a transform to this path. Parameters ---------- translation : Numpy array[2] Translation vector. rotation : number Rotation angle. scale : number Scaling factor. x_reflection : bool Reflection around the first axis. array_trans : Numpy aray[2] Translation vector before rotation and reflection. Returns ------- out : `FlexPath` This object. Notes ----- Applies the transformations in the same order as a `CellReference` or a `CellArray`. If `width_transform` is False, the widths are not scaled. """ self._polygon_dict = None if translation is None: translation = _zero if array_trans is None: array_trans = _zero if rotation is None: cos = _one sin = _zero else: cos = numpy.cos(rotation) * _one sin = numpy.sin(rotation) * _mpone if scale is not None: cos = cos * scale sin = sin * scale array_trans = array_trans / scale if self.width_transform or not self.gdsii_path: self.widths = self.widths * scale self.offsets = self.offsets * scale if x_reflection: cos[1] = -cos[1] sin[0] = -sin[0] pts = self.points + array_trans self.points = pts * cos + pts[:, ::-1] * sin + translation return self def segment(self, end_point, width=None, offset=None, relative=False): """ Add a straight section to the path. Parameters ---------- end_point : array-like[2] End position of the straight segment. width : number, list If a number, all parallel paths are linearly tapered to this width along the segment. A list can be used where each element defines the width for one of the parallel paths in this object. This argument has no effect if the path was created with `gdsii_path` True. offset : number, list If a number, all parallel paths offsets are linearly *increased* by this amount (which can be negative). A list can be used where each element defines the *absolute* offset (not offset increase) for one of the parallel paths in this object. relative : bool If True, `end_point` is used as an offset from the current path position, i.e., if the path is at (1, -2) and the `end_point` is (10, 25), the segment will be constructed from (1, -2) to (1 + 10, -2 + 25) = (11, 23). Otherwise, `end_point` is used as an absolute coordinate. Returns ------- out : `FlexPath` This object. """ self._polygon_dict = None self.points = numpy.vstack( ( self.points, (self.points[-1] + numpy.array(end_point)) if relative else end_point, ) ) if self.gdsii_path or width is None: self.widths = numpy.vstack((self.widths, self.widths[-1])) elif hasattr(width, "__iter__"): self.widths = numpy.vstack((self.widths, width)) else: self.widths = numpy.vstack((self.widths, numpy.repeat(width, self.n))) if offset is None: self.offsets = numpy.vstack((self.offsets, self.offsets[-1])) elif hasattr(offset, "__iter__"): self.offsets = numpy.vstack((self.offsets, offset)) else: self.offsets = numpy.vstack((self.offsets, self.offsets[-1] + offset)) return self def arc(self, radius, initial_angle, final_angle, width=None, offset=None): """ Add a circular arc section to the path. Parameters ---------- radius : number Radius of the circular arc. initial_angle : number Initial angle of the arc. final_angle : number Final angle of the arc. width : number, list If a number, all parallel paths are linearly tapered to this width along the segment. A list can be used where each element defines the width for one of the parallel paths in this object. This argument has no effect if the path was created with `gdsii_path` True. offset : number, list If a number, all parallel paths offsets are linearly *increased* by this amount (which can be negative). A list can be used where each element defines the *absolute* offset (not offset increase) for one of the parallel paths in this object. Returns ------- out : `FlexPath` This object. """ self._polygon_dict = None if self.gdsii_path: width = None if width is None: wid = self.widths[-1] elif hasattr(width, "__iter__"): wid = numpy.array(width) else: wid = numpy.full(self.n, width) if offset is None: off = self.offsets[-1] elif hasattr(offset, "__iter__"): off = numpy.array(offset) else: off = self.offsets[-1] + offset rmax = radius + max( (self.offsets[-1] + self.widths[-1]).max(), (off + wid).max() ) np = max( 3, 1 + int( 0.5 * abs(final_angle - initial_angle) / numpy.arccos(1 - self.tolerance / rmax) + 0.5 ), ) ang = numpy.linspace(initial_angle, final_angle, np) pts = radius * numpy.vstack((numpy.cos(ang), numpy.sin(ang))).T self.points = numpy.vstack((self.points, pts[1:] + (self.points[-1] - pts[0]))) if width is None: self.widths = numpy.vstack((self.widths, numpy.tile(wid, (np - 1, 1)))) else: u = numpy.linspace(0, 1, np)[1:] self.widths = numpy.vstack( (self.widths, numpy.outer(1 - u, self.widths[-1]) + numpy.outer(u, wid)) ) if offset is None: self.offsets = numpy.vstack((self.offsets, numpy.tile(off, (np - 1, 1)))) else: u = numpy.linspace(0, 1, np)[1:] self.offsets = numpy.vstack( ( self.offsets, numpy.outer(1 - u, self.offsets[-1]) + numpy.outer(u, off), ) ) return self def turn(self, radius, angle, width=None, offset=None): """ Add a circular turn to the path. The initial angle of the arc is calculated from the last path segment. Parameters ---------- radius : number Radius of the circular arc. angle : 'r', 'l', 'rr', 'll' or number Angle (in *radians*) of rotation of the path. The values 'r' and 'l' represent 90-degree turns cw and ccw, respectively; the values 'rr' and 'll' represent analogous 180-degree turns. width : number, list If a number, all parallel paths are linearly tapered to this width along the segment. A list can be used where each element defines the width for one of the parallel paths in this object. This argument has no effect if the path was created with `gdsii_path` True. offset : number, list If a number, all parallel paths offsets are linearly *increased* by this amount (which can be negative). A list can be used where each element defines the *absolute* offset (not offset increase) for one of the parallel paths in this object. Returns ------- out : `FlexPath` This object. """ self._polygon_dict = None if self.points.shape[0] < 2: raise ValueError( "[GDSPY] Cannot define initial angle for turn on a FlexPath withouth previous segments." ) v = self.points[-1] - self.points[-2] angle = _angle_dic.get(angle, angle) initial_angle = numpy.arctan2(v[1], v[0]) + (_halfpi if angle < 0 else -_halfpi) self.arc(radius, initial_angle, initial_angle + angle, width, offset) return self def parametric(self, curve_function, width=None, offset=None, relative=True): """ Add a parametric curve to the path. Parameters ---------- curve_function : callable Function that defines the curve. Must be a function of one argument (that varies from 0 to 1) that returns a 2-element Numpy array with the coordinates of the curve. width : number, list If a number, all parallel paths are linearly tapered to this width along the segment. A list can be used where each element defines the width for one of the parallel paths in this object. This argument has no effect if the path was created with `gdsii_path` True. offset : number, list If a number, all parallel paths offsets are linearly *increased* by this amount (which can be negative). A list can be used where each element defines the *absolute* offset (not offset increase) for one of the parallel paths in this object. relative : bool If True, the return values of `curve_function` are used as offsets from the current path position, i.e., to ensure a continuous path, ``curve_function(0)`` must be (0, 0). Otherwise, they are used as absolute coordinates. Returns ------- out : `FlexPath` This object. """ self._polygon_dict = None if self.gdsii_path: width = None if width is None: wid = self.widths[-1] elif hasattr(width, "__iter__"): wid = numpy.array(width) else: wid = numpy.full(self.n, width) if offset is None: off = self.offsets[-1] elif hasattr(offset, "__iter__"): off = numpy.array(offset) else: off = self.offsets[-1] + offset tol = self.tolerance ** 2 u = [0, 1] pts = [numpy.array(curve_function(0)), numpy.array(curve_function(1))] i = 1 while i < len(pts): f = 0.2 while f < 1: test_u = u[i - 1] * (1 - f) + u[i] * f test_pt = numpy.array(curve_function(test_u)) test_err = pts[i - 1] * (1 - f) + pts[i] * f - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > tol: u.insert(i, test_u) pts.insert(i, test_pt) f = 1 i -= 1 else: f += 0.3 i += 1 pts = numpy.array(pts[1:]) np = pts.shape[0] + 1 self.points = numpy.vstack( (self.points, (pts + self.points[-1]) if relative else pts) ) if width is None: self.widths = numpy.vstack((self.widths, numpy.tile(wid, (np - 1, 1)))) else: u = numpy.linspace(0, 1, np)[1:] self.widths = numpy.vstack( (self.widths, numpy.outer(1 - u, self.widths[-1]) + numpy.outer(u, wid)) ) if offset is None: self.offsets = numpy.vstack((self.offsets, numpy.tile(off, (np - 1, 1)))) else: u = numpy.linspace(0, 1, np)[1:] self.offsets = numpy.vstack( ( self.offsets, numpy.outer(1 - u, self.offsets[-1]) + numpy.outer(u, off), ) ) return self def bezier(self, points, width=None, offset=None, relative=True): """ Add a Bezier curve to the path. A Bezier curve is added to the path starting from its current position and finishing at the last point in the `points` array. Parameters ---------- points : array-like[N][2] Control points defining the Bezier curve. width : number, list If a number, all parallel paths are linearly tapered to this width along the segment. A list can be used where each element defines the width for one of the parallel paths in this object. This argument has no effect if the path was created with `gdsii_path` True. offset : number, list If a number, all parallel paths offsets are linearly *increased* by this amount (which can be negative). A list can be used where each element defines the *absolute* offset (not offset increase) for one of the parallel paths in this object. relative : bool If True, all coordinates in the `points` array are used as offsets from the current path position, i.e., if the path is at (1, -2) and the last point in the array is (10, 25), the constructed Bezier will end at (1 + 10, -2 + 25) = (11, 23). Otherwise, the points are used as absolute coordinates. Returns ------- out : `FlexPath` This object. """ self._polygon_dict = None if relative: ctrl = self.points[-1] + numpy.vstack(([(0, 0)], points)) else: ctrl = numpy.vstack((self.points[-1:], points)) self.parametric(_func_bezier(ctrl), width, offset, False) return self def smooth( self, points, angles=None, curl_start=1, curl_end=1, t_in=1, t_out=1, cycle=False, width=None, offset=None, relative=True, ): """ Add a smooth interpolating curve through the given points. Uses the Hobby algorithm [1]_ to calculate a smooth interpolating curve made of cubic Bezier segments between each pair of points. Parameters ---------- points : array-like[N][2] Vertices in the interpolating curve. angles : array-like[N + 1] or None Tangent angles at each point (in *radians*). Any angles defined as None are automatically calculated. curl_start : number Ratio between the mock curvatures at the first point and at its neighbor. A value of 1 renders the first segment a good approximation for a circular arc. A value of 0 will better approximate a straight segment. It has no effect for closed curves or when an angle is defined for the first point. curl_end : number Ratio between the mock curvatures at the last point and at its neighbor. It has no effect for closed curves or when an angle is defined for the first point. t_in : number or array-like[N + 1] Tension parameter when arriving at each point. One value per point or a single value used for all points. t_out : number or array-like[N + 1] Tension parameter when leaving each point. One value per point or a single value used for all points. cycle : bool If True, calculates control points for a closed curve, with an additional segment connecting the first and last points. width : number, list If a number, all parallel paths are linearly tapered to this width along the segment. A list can be used where each element defines the width for one of the parallel paths in this object. This argument has no effect if the path was created with `gdsii_path` True. offset : number, list If a number, all parallel paths offsets are linearly *increased* by this amount (which can be negative). A list can be used where each element defines the *absolute* offset (not offset increase) for one of the parallel paths in this object. relative : bool If True, all coordinates in the `points` array are used as offsets from the current path position, i.e., if the path is at (1, -2) and the last point in the array is (10, 25), the constructed curve will end at (1 + 10, -2 + 25) = (11, 23). Otherwise, the points are used as absolute coordinates. Returns ------- out : `FlexPath` This object. Notes ----- Arguments `width` and `offset` are repeated for *each* cubic Bezier that composes this path element. References ---------- .. [1] Hobby, J.D. *Discrete Comput. Geom.* (1986) 1: 123. `DOI: 10.1007/BF02187690 `_ """ if relative: points = self.points[-1] + numpy.vstack(([(0, 0)], points)) else: points = numpy.vstack((self.points[-1:], points)) cta, ctb = _hobby(points, angles, curl_start, curl_end, t_in, t_out, cycle) for i in range(points.shape[0] - 1): self.bezier((cta[i], ctb[i], points[i + 1]), width, offset, False) if cycle: self.bezier((cta[-1], ctb[-1], points[0]), width, offset, False) return self class RobustPath(object): """ Path object with lazy evaluation. This class keeps information about the constructive parameters of the path and calculates its boundaries only upon request. The benefits are that joins and path components can be calculated automatically to ensure continuity (except in extreme cases). It can be stored as a proper path element in the GDSII format, unlike `Path`. In this case, the width must be constant along the whole path. The downside of `RobustPath` is that it is more computationally expensive than the other path classes. Parameters ---------- initial_point : array-like[2] Starting position of the path. width : number, list Width of each parallel path being created. The number of parallel paths being created is defined by the length of this list. offset : number, list Offsets of each parallel path from the center. If `width` is not a list, the length of this list is used to determine the number of parallel paths being created. Otherwise, offset must be a list with the same length as width, or a number, which is used as distance between adjacent paths. ends : 'flush', 'extended', 'round', 'smooth', 2-tuple, list Type of end caps for the paths. A 2-element tuple represents the start and end extensions to the paths. A list can be used to define the end type for each parallel path. tolerance : number Tolerance used to draw the paths and calculate joins. precision : number Precision for rounding the coordinates of vertices when fracturing the final polygonal boundary. max_points : integer If the number of points in the polygonal path boundary is greater than `max_points`, it will be fractured in smaller polygons with at most `max_points` each. If `max_points` is zero no fracture will occur. max_evals : integer Limit to the maximal number of evaluations when calculating each path component. gdsii_path : bool If True, treat this object as a GDSII path element. Otherwise, it will be converted into polygonal boundaries when required. width_transform : bool If `gdsii_path` is True, this flag indicates whether the width of the path should transform when scaling this object. It has no effect when `gdsii_path` is False. layer : integer, list The GDSII layer numbers for the elements of each path. If the number of layers in the list is less than the number of paths, the list is repeated. datatype : integer, list The GDSII datatype for the elements of each path (between 0 and 255). If the number of datatypes in the list is less than the number of paths, the list is repeated. Notes ----- The value of `tolerance` should not be smaller than `precision`, otherwise there would be wasted computational effort in calculating the paths. """ __slots__ = ( "n", "ends", "x", "offsets", "widths", "paths", "layers", "datatypes", "tolerance", "precision", "max_points", "max_evals", "gdsii_path", "width_transform", "_polygon_dict", ) _pathtype_dict = {"flush": 0, "round": 1, "extended": 2, "smooth": 1} def __init__( self, initial_point, width, offset=0, ends="flush", tolerance=0.01, precision=1e-3, max_points=199, max_evals=1000, gdsii_path=False, width_transform=True, layer=0, datatype=0, ): self._polygon_dict = None if isinstance(width, list): self.n = len(width) self.widths = width if isinstance(offset, list): self.offsets = offset else: self.offsets = [ (i - 0.5 * (self.n - 1)) * offset for i in range(self.n) ] else: if isinstance(offset, list): self.n = len(offset) self.offsets = offset else: self.n = 1 self.offsets = [offset] self.widths = [width] * self.n self.x = numpy.array(initial_point) self.paths = [[] for _ in range(self.n)] if isinstance(ends, list): self.ends = [ends[i % len(ends)] for i in range(self.n)] else: self.ends = [ends for _ in range(self.n)] if isinstance(layer, list): self.layers = [layer[i % len(layer)] for i in range(self.n)] else: self.layers = [layer] * self.n if isinstance(datatype, list): self.datatypes = [datatype[i % len(datatype)] for i in range(self.n)] else: self.datatypes = [datatype] * self.n self.tolerance = tolerance self.precision = precision self.max_points = max_points self.max_evals = max_evals self.gdsii_path = gdsii_path self.width_transform = width_transform if self.gdsii_path and any(end == "smooth" for end in self.ends): warnings.warn( "[GDSPY] Smooth end caps not supported in `RobustPath` with `gdsii_path == True`.", stacklevel=3, ) def __str__(self): if self.n > 1: return "RobustPath (x{}, end at ({}, {}), length {}, layers {}, datatypes {})".format( self.n, self.x[0], self.x[1], len(self), self.layers, self.datatypes ) else: return "RobustPath (end at ({}, {}), length {}, layer {}, datatype {})".format( self.x[0], self.x[1], len(self), self.layers[0], self.datatypes[0] ) def __len__(self): """ Number of path components. """ return len(self.paths[0]) def __call__(self, u, arm=0): """ Calculate the positions of each parallel path. Parameters ---------- u : number Position along the `RobustPath` to compute. This argument can range from 0 (start of the path) to ``len(self)`` (end of the path). arm : -1, 0, 1 Wether to calculate one of the path boundaries (-1 or 1) or its central spine (0). Returns ------- out : Numpy array[N, 2] Coordinates for each of the N parallel paths in this object. """ i = int(u) u -= i if i == len(self.paths[0]): i -= 1 u = 1 return numpy.array([p[i](u, arm) for p in self.paths]) def grad(self, u, arm=0, side="-"): """ Calculate the direction vector of each parallel path. Parameters ---------- u : number Position along the `RobustPath` to compute. This argument can range from 0 (start of the path) to `len(self)` (end of the path). arm : -1, 0, 1 Wether to calculate one of the path boundaries (-1 or 1) or its central spine (0). side : '-' or '+' At path joins, whether to calculate the direction using the component before or after the join. Returns ------- out : Numpy array[N, 2] Direction vectors for each of the N parallel paths in this object. """ i = int(u) u -= i if u == 0 and ((i > 0 and side == "-") or i == len(self.paths[0])): i -= 1 u = 1 return numpy.array([p[i].grad(u, arm) for p in self.paths]) def width(self, u): """ Calculate the width of each parallel path. Parameters ---------- u : number Position along the `RobustPath` to compute. This argument can range from 0 (start of the path) to `len(self)` (end of the path). Returns ------- out : Numpy array[N] Width for each of the N parallel paths in this object. """ i = int(u) u -= i if u == 0 and i == len(self.paths[0]): i -= 1 u = 1 return numpy.array([p[i].wid(u) for p in self.paths]) def get_polygons(self, by_spec=False): """ Calculate the polygonal boundaries described by this path. Parameters ---------- by_spec : bool If True, the return value is a dictionary with the polygons of each individual pair (layer, datatype). Returns ------- out : list of array-like[N][2] or dictionary List containing the coordinates of the vertices of each polygon, or dictionary with the list of polygons (if `by_spec` is True). """ if self._polygon_dict is None: self._polygon_dict = {} for path, end, layer, datatype in zip( self.paths, self.ends, self.layers, self.datatypes ): poly = [] for arm in [-1, 1]: if not (end == "flush"): i = 0 if arm == 1 else -1 u = abs(i) if end == "smooth": v1 = -arm * path[i].grad(u, -arm) v2 = arm * path[i].grad(u, arm) angles = [ numpy.arctan2(v1[1], v1[0]), numpy.arctan2(v2[1], v2[0]), ] points = numpy.array([path[i](u, -arm), path[i](u, arm)]) cta, ctb = _hobby(points, angles) f = _func_bezier( numpy.array([points[0], cta[0], ctb[0], points[1]]) ) tol = self.tolerance ** 2 uu = [0, 1] fu = [f(0), f(1)] iu = 1 while iu < len(fu) < self.max_evals: test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 poly.extend(fu[1:-1]) else: p = path[i](u, 0) v = -arm * path[i].grad(u, 0) r = 0.5 * path[i].wid(u) if end == "round": np = max( 5, 1 + int( _halfpi / numpy.arccos(1 - self.tolerance / r) + 0.5 ), ) ang = numpy.linspace(-_halfpi, _halfpi, np)[ 1:-1 ] + numpy.arctan2(v[1], v[0]) endpts = ( p + r * numpy.vstack((numpy.cos(ang), numpy.sin(ang))).T ) poly.extend(endpts) else: v /= (v[0] ** 2 + v[1] ** 2) ** 0.5 w = v[::-1] * _pmone d = r if end == "extended" else end[u] poly.append(p + d * v + r * w) poly.append(p + d * v - r * w) path_arm = [] start = 0 tol = self.tolerance ** 2 for sub0, sub1 in zip(path[:-1], path[1:]): p0 = sub0(1, arm) v0 = sub0.grad(1, arm) p1 = sub1(0, arm) v1 = sub1.grad(0, arm) den = v1[1] * v0[0] - v1[0] * v0[1] lim = ( 1e-12 * (v0[0] ** 2 + v0[1] ** 2) * (v1[0] ** 2 + v1[1] ** 2) ) dx = p1[0] - p0[0] dy = p1[1] - p0[1] if den ** 2 < lim or dx ** 2 + dy ** 2 <= tol: u0 = u1 = 0 px = 0.5 * (p0 + p1) else: u0 = (v1[1] * dx - v1[0] * dy) / den u1 = (v0[1] * dx - v0[0] * dy) / den px = 0.5 * (p0 + v0 * u0 + p1 + v1 * u1) u0 = 1 + u0 if u0 < 1 and u1 > 0: delta = sub0(u0, arm) - sub1(u1, arm) err = delta[0] ** 2 + delta[1] ** 2 iters = 0 step = 0.5 while err > tol: iters += 1 if iters > self.max_evals: warnings.warn("[GDSPY] Intersection not found.") break du = delta * sub0.grad(u0, arm) new_u0 = min(1, max(0, u0 - step * (du[0] + du[1]))) du = delta * sub1.grad(u1, arm) new_u1 = min(1, max(0, u1 + step * (du[0] + du[1]))) new_delta = sub0(new_u0, arm) - sub1(new_u1, arm) new_err = new_delta[0] ** 2 + new_delta[1] ** 2 if new_err >= err: step /= 2 continue u0 = new_u0 u1 = new_u1 delta = new_delta err = new_err px = 0.5 * (sub0(u0, arm) + sub1(u1, arm)) if u1 >= 0: if u0 <= 1: path_arm.extend(sub0.points(start, u0, arm)[:-1]) else: path_arm.extend(sub0.points(start, 1, arm)) warnings.warn( "[GDSPY] RobustPath join at ({}, {}) cannot be ensured. Please check the resulting polygon.".format( path_arm[-1][0], path_arm[-1][1] ), stacklevel=3, ) start = u1 else: if u0 <= 1: path_arm.extend(sub0.points(start, u0, arm)) warnings.warn( "[GDSPY] RobustPath join at ({}, {}) cannot be ensured. Please check the resulting polygon.".format( path_arm[-1][0], path_arm[-1][1] ), stacklevel=2, ) else: path_arm.extend(sub0.points(start, 1, arm)) path_arm.append(px) start = 0 path_arm.extend(path[-1].points(start, 1, arm)) poly.extend(path_arm[::arm]) polygons = [numpy.array(poly)] if self.max_points > 4 and polygons[0].shape[0] > self.max_points: ii = 0 while ii < len(polygons): if len(polygons[ii]) > self.max_points: pts0 = sorted(polygons[ii][:, 0]) pts1 = sorted(polygons[ii][:, 1]) ncuts = len(pts0) // self.max_points if pts0[-1] - pts0[0] > pts1[-1] - pts1[0]: # Vertical cuts cuts = [ pts0[int(i * len(pts0) / (ncuts + 1.0) + 0.5)] for i in range(1, ncuts + 1) ] chopped = clipper._chop( polygons[ii], cuts, 0, 1 / self.precision ) else: # Horizontal cuts cuts = [ pts1[int(i * len(pts1) / (ncuts + 1.0) + 0.5)] for i in range(1, ncuts + 1) ] chopped = clipper._chop( polygons[ii], cuts, 1, 1 / self.precision ) polygons.pop(ii) polygons.extend( numpy.array(x) for x in itertools.chain.from_iterable(chopped) ) else: ii += 1 key = (layer, datatype) if key in self._polygon_dict: self._polygon_dict[key].extend(polygons) else: self._polygon_dict[key] = polygons if by_spec: return libcopy.deepcopy(self._polygon_dict) else: return list(itertools.chain.from_iterable(self._polygon_dict.values())) def to_polygonset(self): """ Create a `PolygonSet` representation of this object. The resulting object will be fractured according to the parameter `max_points` used when instantiating this object. Returns ------- out : `PolygonSet` or None A `PolygonSet` that contains all boundaries for this path. If the path is empty, returns None. """ if len(self.paths[0]) == 0: return None polygons = self.get_polygons(True) pol = PolygonSet([]) for k, v in polygons.items(): pol.layers.extend([k[0]] * len(v)) pol.datatypes.extend([k[1]] * len(v)) pol.polygons.extend(v) return pol.fracture(self.max_points, self.precision) def to_gds(self, multiplier): """ Convert this object to a series of GDSII elements. If `RobustPath.gdsii_path` is True, GDSII path elements are created instead of boundaries. Such paths do not support variable widths, but their memeory footprint is smaller than full polygonal boundaries. Parameters ---------- multiplier : number A number that multiplies all dimensions written in the GDSII elements. Returns ------- out : string The GDSII binary string that represents this object. """ if len(self.paths[0]) == 0: return b"" if self.gdsii_path: sign = 1 if self.width_transform else -1 else: return self.to_polygonset().to_gds(multiplier) data = [] for ii in range(self.n): pathtype = RobustPath._pathtype_dict.get(self.ends[ii], 4) data.append( struct.pack( ">4Hh2Hh2Hh2Hl", 4, 0x0900, 6, 0x0D02, self.layers[ii], 6, 0x0E02, self.datatypes[ii], 6, 0x2102, pathtype, 8, 0x0F03, sign * int(round(self.widths[ii] * multiplier)), ) ) if pathtype == 4: data.append( struct.pack( ">2Hl2Hl", 8, 0x3003, int(round(self.ends[ii][0] * multiplier)), 8, 0x3103, int(round(self.ends[ii][1] * multiplier)), ) ) points = [] for path in self.paths[ii]: new_points = numpy.round(numpy.array(path.points(0, 1, 0)) * multiplier) if ( len(points) > 0 and new_points[0, 0] == points[-1][-1, 0] and new_points[0, 1] == points[-1][-1, 1] ): points.append(new_points[1:]) else: points.append(new_points) points = numpy.vstack(points).astype(">i4") if points.shape[0] > 8191: warnings.warn( "[GDSPY] Paths with more than 8191 are not supported by the official GDSII specification. This GDSII file might not be compatible with all readers.", stacklevel=4, ) i0 = 0 while i0 < points.shape[0]: i1 = min(i0 + 8191, points.shape[0]) data.append(struct.pack(">2H", 4 + 8 * (i1 - i0), 0x1003)) data.append(points[i0:i1].tostring()) i0 = i1 else: data.append(struct.pack(">2H", 4 + 8 * points.shape[0], 0x1003)) data.append(points.tostring()) data.append(struct.pack(">2H", 4, 0x1100)) return b"".join(data) def area(self, by_spec=False): """ Calculate the total area of this object. This functions creates a `PolgonSet` from this object and calculates its area, which means it is computationally expensive. Parameters ---------- by_spec : bool If True, the return value is a dictionary with ``{(layer, datatype): area}``. Returns ------- out : number, dictionary Area of this object. """ return self.to_polygonset().area(by_spec) def translate(self, dx, dy): """ Translate this path. Parameters ---------- dx : number Distance to move in the x-direction dy : number Distance to move in the y-direction Returns ------- out : `RobustPath` This object. """ self._polygon_dict = None offset = numpy.array((dx, dy)) self.x = self.x + offset for path in self.paths: for sub in path: sub.x = _func_multadd(sub.x, None, offset) return self def rotate(self, angle, center=(0, 0)): """ Rotate this path. Parameters ---------- angle : number The angle of rotation (in *radians*). center : array-like[2] Center point for the rotation. Returns ------- out : `RobustPath` This object. """ self._polygon_dict = None ca = numpy.cos(angle) sa = numpy.sin(angle) * _mpone c0 = numpy.array(center) x = self.x - c0 self.x = x * ca + x[::-1] * sa + c0 for path in self.paths: for sub in path: sub.x = _func_rotate(sub.x, ca, sa, c0) sub.dx = _func_rotate(sub.dx, ca, sa, nargs=2) return self def scale(self, scale, center=(0, 0)): """ Scale this path. Parameters ---------- scale : number Scaling factor. center : array-like[2] Center point for the scaling operation. Returns ------- out : `RobustPath` This object. """ self._polygon_dict = None c0 = numpy.array(center) * (1 - scale) self.x = self.x * scale + c0 self.widths = [wid * scale for wid in self.widths] self.offsets = [off * scale for off in self.offsets] for path in self.paths: for sub in path: sub.x = _func_multadd(sub.x, scale, c0) sub.dx = _func_multadd(sub.dx, scale, None, nargs=2) sub.wid = _func_multadd(sub.wid, abs(scale), None) sub.off = _func_multadd(sub.off, scale, None) return self def transform(self, translation, rotation, scale, x_reflection, array_trans=None): """ Apply a transform to this path. Parameters ---------- translation : Numpy array[2] Translation vector. rotation : number Rotation angle. scale : number Scaling factor. x_reflection : bool Reflection around the first axis. array_trans : Numpy aray[2] Translation vector before rotation and reflection. Returns ------- out : `RobustPath` This object. Notes ----- Applies the transformations in the same order as a `CellReference` or a `CellArray`. If `width_transform` is False, the widths are not scaled. """ self._polygon_dict = None for ii in range(self.n): for sub in self.paths[ii]: sub.x = _func_trafo( sub.x, translation, rotation, scale, x_reflection, array_trans ) sub.dx = _func_trafo( sub.dx, None, rotation, scale, x_reflection, None, nargs=2 ) if self.width_transform or not self.gdsii_path: sub.wid = _func_multadd(sub.wid, scale, None) sub.off = _func_multadd(sub.off, scale, None) self.x[ii] = self.paths[-1].x(1) self.widths[ii] = self.paths[-1].wid(1) self.offsets[ii] = self.offsets[-1].off(1) return self def _parse_offset(self, arg, idx): if arg is None: return _func_const(self.offsets[idx]) elif hasattr(arg, "__getitem__"): if callable(arg[idx]): return arg[idx] return _func_linear(self.offsets[idx], arg[idx]) elif callable(arg): return _func_multadd(arg, None, self.offsets[idx]) return _func_linear(self.offsets[idx], self.offsets[idx] + arg) def _parse_width(self, arg, idx): if arg is None or self.gdsii_path: if arg is not None: warnings.warn( "[GDSPY] Argument `width` ignored in RobustPath with `gdsii_path == True`.", stacklevel=3, ) return _func_const(self.widths[idx]) elif hasattr(arg, "__getitem__"): if callable(arg[idx]): return arg[idx] return _func_linear(self.widths[idx], arg[idx]) elif callable(arg): return arg return _func_linear(self.widths[idx], arg) def segment(self, end_point, width=None, offset=None, relative=False): """ Add a straight section to the path. Parameters ---------- end_point : array-like[2] End position of the straight segment. width : number, callable, list If a number, all parallel paths are linearly tapered to this width along the segment. If this is callable, it must be a function of one argument (that varies from 0 to 1) that returns the width of the path. A list can be used where each element (number or callable) defines the width for one of the parallel paths in this object. offset : number, callable, list If a number, all parallel paths offsets are linearly *increased* by this amount (which can be negative). If this is callable, it must be a function of one argument (that varies from 0 to 1) that returns the offset *increase*. A list can be used where each element (number or callable) defines the *absolute* offset (not offset increase) for one of the parallel paths in this object. relative : bool If True, `end_point` is used as an offset from the current path position, i.e., if the path is at (1, -2) and the `end_point` is (10, 25), the segment will be constructed from (1, -2) to (1 + 10, -2 + 25) = (11, 23). Otherwise, `end_point` is used as an absolute coordinate. Returns ------- out : `RobustPath` This object. """ self._polygon_dict = None x = (numpy.array(end_point) + self.x) if relative else numpy.array(end_point) f = _func_linear(self.x, x) df = _func_const(x - self.x, 2) self.x = x for i in range(self.n): off = self._parse_offset(offset, i) wid = self._parse_width(width, i) self.paths[i].append( _SubPath(f, df, off, wid, self.tolerance, self.max_evals) ) self.widths[i] = wid(1) self.offsets[i] = off(1) return self def arc(self, radius, initial_angle, final_angle, width=None, offset=None): """ Add a circular arc section to the path. Parameters ---------- radius : number Radius of the circular arc. initial_angle : number Initial angle of the arc. final_angle : number Final angle of the arc. width : number, callable, list If a number, all parallel paths are linearly tapered to this width along the segment. If this is callable, it must be a function of one argument (that varies from 0 to 1) that returns the width of the path. A list can be used where each element (number or callable) defines the width for one of the parallel paths in this object. offset : number, callable, list If a number, all parallel paths offsets are linearly *increased* by this amount (which can be negative). If this is callable, it must be a function of one argument (that varies from 0 to 1) that returns the offset *increase*. A list can be used where each element (number or callable) defines the *absolute* offset (not offset increase) for one of the parallel paths in this object. Returns ------- out : `RobustPath` This object. """ self._polygon_dict = None x0 = self.x - numpy.array( (radius * numpy.cos(initial_angle), radius * numpy.sin(initial_angle)) ) def f(u): angle = initial_angle * (1 - u) + final_angle * u return x0 + numpy.array( (radius * numpy.cos(angle), radius * numpy.sin(angle)) ) def df(u, h): angle = initial_angle * (1 - u) + final_angle * u r = radius * (final_angle - initial_angle) return numpy.array((-r * numpy.sin(angle), r * numpy.cos(angle))) self.x = f(1) for i in range(self.n): off = self._parse_offset(offset, i) wid = self._parse_width(width, i) self.paths[i].append( _SubPath(f, df, off, wid, self.tolerance, self.max_evals) ) self.widths[i] = wid(1) self.offsets[i] = off(1) return self def turn(self, radius, angle, width=None, offset=None): """ Add a circular turn to the path. The initial angle of the arc is calculated from an average of the current directions of all parallel paths in this object. Parameters ---------- radius : number Radius of the circular arc. angle : 'r', 'l', 'rr', 'll' or number Angle (in *radians*) of rotation of the path. The values 'r' and 'l' represent 90-degree turns cw and ccw, respectively; the values 'rr' and 'll' represent analogous 180-degree turns. width : number, callable, list If a number, all parallel paths are linearly tapered to this width along the segment. If this is callable, it must be a function of one argument (that varies from 0 to 1) that returns the width of the path. A list can be used where each element (number or callable) defines the width for one of the parallel paths in this object. offset : number, callable, list If a number, all parallel paths offsets are linearly *increased* by this amount (which can be negative). If this is callable, it must be a function of one argument (that varies from 0 to 1) that returns the offset *increase*. A list can be used where each element (number or callable) defines the *absolute* offset (not offset increase) for one of the parallel paths in this object. Returns ------- out : `RobustPath` This object. """ self._polygon_dict = None i = len(self.paths[0]) - 1 if i < 0: raise ValueError( "[GDSPY] Cannot define initial angle for turn on an empty RobustPath." ) angle = _angle_dic.get(angle, angle) initial_angle = 0 for p in self.paths: v = p[i].grad(1, 0) initial_angle += numpy.arctan2(v[1], v[0]) initial_angle = initial_angle / len(self.paths) + ( _halfpi if angle < 0 else -_halfpi ) self.arc(radius, initial_angle, initial_angle + angle, width, offset) return self def parametric( self, curve_function, curve_derivative=None, width=None, offset=None, relative=True, ): """ Add a parametric curve to the path. Parameters ---------- curve_function : callable Function that defines the curve. Must be a function of one argument (that varies from 0 to 1) that returns a 2-element Numpy array with the coordinates of the curve. curve_derivative : callable If set, it should be the derivative of the curve function. Must be a function of one argument (that varies from 0 to 1) that returns a 2-element Numpy array. If None, the derivative will be calculated numerically. width : number, callable, list If a number, all parallel paths are linearly tapered to this width along the segment. If this is callable, it must be a function of one argument (that varies from 0 to 1) that returns the width of the path. A list can be used where each element (number or callable) defines the width for one of the parallel paths in this object. offset : number, callable, list If a number, all parallel paths offsets are linearly *increased* by this amount (which can be negative). If this is callable, it must be a function of one argument (that varies from 0 to 1) that returns the offset *increase*. A list can be used where each element (number or callable) defines the *absolute* offset (not offset increase) for one of the parallel paths in this object. relative : bool If True, the return values of `curve_function` are used as offsets from the current path position, i.e., to ensure a continuous path, ``curve_function(0)`` must be (0, 0). Otherwise, they are used as absolute coordinates. Returns ------- out : `RobustPath` This object. """ self._polygon_dict = None f = _func_multadd(curve_function, None, self.x) if relative else curve_function if curve_derivative is None: def df(u, h): u0 = max(0, u - h) u1 = min(1, u + h) return (curve_function(u1) - curve_function(u0)) / (u1 - u0) else: def df(u, h): return curve_derivative(u) self.x = f(1) for i in range(self.n): off = self._parse_offset(offset, i) wid = self._parse_width(width, i) self.paths[i].append( _SubPath(f, df, off, wid, self.tolerance, self.max_evals) ) self.widths[i] = wid(1) self.offsets[i] = off(1) return self def bezier(self, points, width=None, offset=None, relative=True): """ Add a Bezier curve to the path. A Bezier curve is added to the path starting from its current position and finishing at the last point in the `points` array. Parameters ---------- points : array-like[N][2] Control points defining the Bezier curve. width : number, callable, list If a number, all parallel paths are linearly tapered to this width along the segment. If this is callable, it must be a function of one argument (that varies from 0 to 1) that returns the width of the path. A list can be used where each element (number or callable) defines the width for one of the parallel paths in this object. offset : number, callable, list If a number, all parallel paths offsets are linearly *increased* by this amount (which can be negative). If this is callable, it must be a function of one argument (that varies from 0 to 1) that returns the offset *increase*. A list can be used where each element (number or callable) defines the *absolute* offset (not offset increase) for one of the parallel paths in this object. relative : bool If True, all coordinates in the `points` array are used as offsets from the current path position, i.e., if the path is at (1, -2) and the last point in the array is (10, 25), the constructed Bezier will end at (1 + 10, -2 + 25) = (11, 23). Otherwise, the points are used as absolute coordinates. Returns ------- out : `RobustPath` This object. """ self._polygon_dict = None if relative: ctrl = self.x + numpy.vstack(([(0, 0)], points)) else: ctrl = numpy.vstack(([self.x], points)) dctrl = (ctrl.shape[0] - 1) * (ctrl[1:] - ctrl[:-1]) self.x = ctrl[-1] f = _func_bezier(ctrl) df = _func_bezier(dctrl, 2) for i in range(self.n): off = self._parse_offset(offset, i) wid = self._parse_width(width, i) self.paths[i].append( _SubPath(f, df, off, wid, self.tolerance, self.max_evals) ) self.widths[i] = wid(1) self.offsets[i] = off(1) return self def smooth( self, points, angles=None, curl_start=1, curl_end=1, t_in=1, t_out=1, cycle=False, width=None, offset=None, relative=True, ): """ Add a smooth interpolating curve through the given points. Uses the Hobby algorithm [1]_ to calculate a smooth interpolating curve made of cubic Bezier segments between each pair of points. Parameters ---------- points : array-like[N][2] Vertices in the interpolating curve. angles : array-like[N + 1] or None Tangent angles at each point (in *radians*). Any angles defined as None are automatically calculated. curl_start : number Ratio between the mock curvatures at the first point and at its neighbor. A value of 1 renders the first segment a good approximation for a circular arc. A value of 0 will better approximate a straight segment. It has no effect for closed curves or when an angle is defined for the first point. curl_end : number Ratio between the mock curvatures at the last point and at its neighbor. It has no effect for closed curves or when an angle is defined for the first point. t_in : number or array-like[N + 1] Tension parameter when arriving at each point. One value per point or a single value used for all points. t_out : number or array-like[N + 1] Tension parameter when leaving each point. One value per point or a single value used for all points. cycle : bool If True, calculates control points for a closed curve, with an additional segment connecting the first and last points. width : number, callable, list If a number, all parallel paths are linearly tapered to this width along the segment. If this is callable, it must be a function of one argument (that varies from 0 to 1) that returns the width of the path. A list can be used where each element (number or callable) defines the width for one of the parallel paths in this object. offset : number, callable, list If a number, all parallel paths offsets are linearly *increased* by this amount (which can be negative). If this is callable, it must be a function of one argument (that varies from 0 to 1) that returns the offset *increase*. A list can be used where each element (number or callable) defines the *absolute* offset (not offset increase) for one of the parallel paths in this object. relative : bool If True, all coordinates in the `points` array are used as offsets from the current path position, i.e., if the path is at (1, -2) and the last point in the array is (10, 25), the constructed curve will end at (1 + 10, -2 + 25) = (11, 23). Otherwise, the points are used as absolute coordinates. Returns ------- out : `RobustPath` This object. Notes ----- Arguments `width` and `offset` are repeated for *each* cubic Bezier that composes this path element. References ---------- .. [1] Hobby, J.D. *Discrete Comput. Geom.* (1986) 1: 123. `DOI: 10.1007/BF02187690 `_ """ if relative: points = self.x + numpy.vstack(([(0, 0)], points)) else: points = numpy.vstack(([self.x], points)) cta, ctb = _hobby(points, angles, curl_start, curl_end, t_in, t_out, cycle) for i in range(points.shape[0] - 1): self.bezier((cta[i], ctb[i], points[i + 1]), width, offset, False) if cycle: self.bezier((cta[-1], ctb[-1], points[0]), width, offset, False) return self class Label(object): """ Text that can be used to label parts of the geometry or display messages. The text does not create additional geometry, it's meant for display and labeling purposes only. Parameters ---------- text : string The text of this label. position : array-like[2] Text anchor position. anchor : 'n', 's', 'e', 'w', 'o', 'ne', 'nw'... Position of the anchor relative to the text. rotation : number Angle of rotation of the label (in *degrees*). magnification : number Magnification factor for the label. x_reflection : bool If True, the label is reflected parallel to the x direction before being rotated (not supported by LayoutViewer). layer : integer The GDSII layer number for these elements. texttype : integer The GDSII text type for the label (between 0 and 63). Attributes ---------- text : string The text of this label. position : array-like[2] Text anchor position. anchor : int Position of the anchor relative to the text. rotation : number Angle of rotation of the label (in *degrees*). magnification : number Magnification factor for the label. x_reflection : bool If True, the label is reflected parallel to the x direction before being rotated (not supported by LayoutViewer). layer : integer The GDSII layer number for these elements. texttype : integer The GDSII text type for the label (between 0 and 63). Examples -------- >>> label = gdspy.Label('Sample label', (10, 0), 'sw') >>> myCell.add(label) """ _anchor = { "nw": 0, "top left": 0, "upper left": 0, "n": 1, "top center": 1, "upper center": 1, "ne": 2, "top right": 2, "upper right": 2, "w": 4, "middle left": 4, "o": 5, "middle center": 5, "e": 6, "middle right": 6, "sw": 8, "bottom left": 8, "lower left": 8, "s": 9, "bottom center": 9, "lower center": 9, "se": 10, "bottom right": 10, "lower right": 10, } __slots__ = ( "layer", "texttype", "text", "position", "anchor", "rotation", "magnification", "x_reflection", ) def __init__( self, text, position, anchor="o", rotation=None, magnification=None, x_reflection=False, layer=0, texttype=0, ): self.layer = layer self.text = text self.position = numpy.array(position) self.anchor = Label._anchor.get(anchor.lower(), None) if self.anchor is None: raise ValueError( "[GDSPY] Label anchors must be one of: '" + "', '".join(Label._anchor.keys()) + "'." ) self.rotation = rotation self.magnification = magnification self.x_reflection = x_reflection self.texttype = texttype def __repr__(self): return 'Label("{0}", ({1[0]}, {1[1]}), {2}, {3}, {4}, {5}, {6})'.format( self.text, self.position, self.rotation, self.magnification, self.x_reflection, self.layer, self.texttype, ) def __str__(self): return 'Label ("{0}", at ({1[0]}, {1[1]}), rotation {2}, magnification {3}, reflection {4}, layer {5}, texttype {6})'.format( self.text, self.position, self.rotation, self.magnification, self.x_reflection, self.layer, self.texttype, ) def to_gds(self, multiplier): """ Convert this label to a GDSII structure. Parameters ---------- multiplier : number A number that multiplies all dimensions written in the GDSII structure. Returns ------- out : string The GDSII binary string that represents this label. """ text = self.text if len(text) % 2 != 0: text = text + "\0" data = struct.pack( ">4Hh2Hh2Hh", 4, 0x0C00, 6, 0x0D02, self.layer, 6, 0x1602, self.texttype, 6, 0x1701, self.anchor, ) if ( (self.rotation is not None) or (self.magnification is not None) or self.x_reflection ): word = 0 values = b"" if self.x_reflection: word += 0x8000 if not (self.magnification is None): # This flag indicates that the magnification is absolute, not # relative (not supported). # word += 0x0004 values += struct.pack(">2H", 12, 0x1B05) + _eight_byte_real( self.magnification ) if not (self.rotation is None): # This flag indicates that the rotation is absolute, not # relative (not supported). # word += 0x0002 values += struct.pack(">2H", 12, 0x1C05) + _eight_byte_real( self.rotation ) data += struct.pack(">3H", 6, 0x1A01, word) + values return ( data + struct.pack( ">2H2l2H", 12, 0x1003, int(round(self.position[0] * multiplier)), int(round(self.position[1] * multiplier)), 4 + len(text), 0x1906, ) + text.encode("ascii") + struct.pack(">2H", 4, 0x1100) ) def translate(self, dx, dy): """ Translate this label. Parameters ---------- dx : number Distance to move in the x-direction dy : number Distance to move in the y-direction Returns ------- out : `Label` This object. Examples -------- >>> text = gdspy.Label((0, 0), (10, 20)) >>> text = text.translate(2, 0) >>> myCell.add(text) """ self.position = numpy.array((dx + self.position[0], dy + self.position[1])) return self class Cell(object): """ Collection of polygons, paths, labels and raferences to other cells. Parameters ---------- name : string The name of the cell. exclude_from_current : bool If True, the cell will not be automatically included in the current library. Attributes ---------- name : string The name of this cell. polygons : list of `PolygonSet` List of cell polygons. paths : list of `RobustPath` or `FlexPath` List of cell paths. labels : list of `Label` List of cell labels. references : list of `CellReference` or `CellArray` List of cell references. """ __slots__ = "name", "polygons", "paths", "labels", "references", "_bb_valid" def __init__(self, name, exclude_from_current=False): self.name = name self.polygons = [] self.paths = [] self.labels = [] self.references = [] self._bb_valid = False if not exclude_from_current: current_library.add(self) def __str__(self): return 'Cell ("{}", {} polygons, {} paths, {} labels, {} references)'.format( self.name, len(self.polygons), len(self.paths), len(self.labels), len(self.references), ) def to_gds(self, multiplier, timestamp=None): """ Convert this cell to a GDSII structure. Parameters ---------- multiplier : number A number that multiplies all dimensions written in the GDSII structure. timestamp : datetime object Sets the GDSII timestamp. If None, the current time is used. Returns ------- out : string The GDSII binary string that represents this cell. """ now = datetime.datetime.today() if timestamp is None else timestamp name = self.name if len(name) % 2 != 0: name = name + "\0" data = [ struct.pack( ">2H12h2H", 28, 0x0502, now.year, now.month, now.day, now.hour, now.minute, now.second, now.year, now.month, now.day, now.hour, now.minute, now.second, 4 + len(name), 0x0606, ), name.encode("ascii"), ] data.extend(polygon.to_gds(multiplier) for polygon in self.polygons) data.extend(path.to_gds(multiplier) for path in self.paths) data.extend(label.to_gds(multiplier) for label in self.labels) data.extend(reference.to_gds(multiplier) for reference in self.references) data.append(struct.pack(">2H", 4, 0x0700)) return b"".join(data) def copy(self, name, exclude_from_current=False, deep_copy=False): """ Creates a copy of this cell. Parameters ---------- name : string The name of the cell. exclude_from_current : bool If True, the cell will not be included in the global list of cells maintained by `gdspy`. deep_copy : bool If False, the new cell will contain only references to the existing elements. If True, copies of all elements are also created. Returns ------- out : `Cell` The new copy of this cell. """ new_cell = Cell(name, exclude_from_current) if deep_copy: new_cell.polygons = libcopy.deepcopy(self.polygons) new_cell.paths = libcopy.deepcopy(self.paths) new_cell.labels = libcopy.deepcopy(self.labels) new_cell.references = libcopy.deepcopy(self.references) for ref in new_cell.get_dependencies(True): if ref._bb_valid: ref._bb_valid = False else: new_cell.polygons = list(self.polygons) new_cell.paths = list(self.paths) new_cell.labels = list(self.labels) new_cell.references = list(self.references) return new_cell def add(self, element): """ Add a new element or list of elements to this cell. Parameters ---------- element : `PolygonSet`, `CellReference`, `CellArray` or iterable The element or iterable of elements to be inserted in this cell. Returns ------- out : `Cell` This cell. """ if isinstance(element, PolygonSet): self.polygons.append(element) elif isinstance(element, RobustPath) or isinstance(element, FlexPath): self.paths.append(element) elif isinstance(element, Label): self.labels.append(element) elif isinstance(element, CellReference) or isinstance(element, CellArray): self.references.append(element) else: for e in element: if isinstance(e, PolygonSet): self.polygons.append(e) elif isinstance(e, RobustPath) or isinstance(e, FlexPath): self.paths.append(e) elif isinstance(e, Label): self.labels.append(e) elif isinstance(e, CellReference) or isinstance(e, CellArray): self.references.append(e) else: raise ValueError( "[GDSPY] Only instances of `PolygonSet`, `FlexPath`, `RobustPath`, `Label`, `CellReference`, and `CellArray` can be added to `Cell`." ) self._bb_valid = False return self def remove_polygons(self, test): """ Remove polygons from this cell. The function or callable `test` is called for each polygon in the cell. If its return value evaluates to True, the corresponding polygon is removed from the cell. Parameters ---------- test : callable Test function to query whether a polygon should be removed. The function is called with arguments: ``(points, layer, datatype)`` Returns ------- out : `Cell` This cell. Examples -------- Remove polygons in layer 1: >>> cell.remove_polygons(lambda pts, layer, datatype: ... layer == 1) Remove polygons with negative x coordinates: >>> cell.remove_polygons(lambda pts, layer, datatype: ... any(pts[:, 0] < 0)) """ empty = [] for element in self.polygons: ii = 0 while ii < len(element.polygons): if test( element.polygons[ii], element.layers[ii], element.datatypes[ii] ): element.polygons.pop(ii) element.layers.pop(ii) element.datatypes.pop(ii) else: ii += 1 if len(element.polygons) == 0: empty.append(element) for element in empty: self.polygons.remove(element) return self def remove_paths(self, test): """ Remove paths from this cell. The function or callable `test` is called for each `FlexPath` or `RobustPath` in the cell. If its return value evaluates to True, the corresponding label is removed from the cell. Parameters ---------- test : callable Test function to query whether a path should be removed. The function is called with the path as the only argument. Returns ------- out : `Cell` This cell. """ ii = 0 while ii < len(self.paths): if test(self.paths[ii]): self.paths.pop(ii) else: ii += 1 return self def remove_labels(self, test): """ Remove labels from this cell. The function or callable `test` is called for each label in the cell. If its return value evaluates to True, the corresponding label is removed from the cell. Parameters ---------- test : callable Test function to query whether a label should be removed. The function is called with the label as the only argument. Returns ------- out : `Cell` This cell. Examples -------- Remove labels in layer 1: >>> cell.remove_labels(lambda lbl: lbl.layer == 1) """ ii = 0 while ii < len(self.labels): if test(self.labels[ii]): self.labels.pop(ii) else: ii += 1 return self def area(self, by_spec=False): """ Calculate the total area of the elements on this cell, including cell references and arrays. Parameters ---------- by_spec : bool If True, the return value is a dictionary with the areas of each individual pair (layer, datatype). Returns ------- out : number, dictionary Area of this cell. """ if by_spec: cell_area = {} for element in itertools.chain(self.polygons, self.paths, self.references): element_area = element.area(True) for ll in element_area.keys(): if ll in cell_area: cell_area[ll] += element_area[ll] else: cell_area[ll] = element_area[ll] else: cell_area = 0 for element in itertools.chain(self.polygons, self.paths, self.references): cell_area += element.area() return cell_area def get_layers(self): """ Return the set of layers in this cell. Returns ------- out : set Set of the layers used in this cell. """ layers = set() for element in itertools.chain(self.polygons, self.paths): layers.update(element.layers) for reference in self.references: layers.update(reference.ref_cell.get_layers()) for label in self.labels: layers.add(label.layer) return layers def get_datatypes(self): """ Return the set of datatypes in this cell. Returns ------- out : set Set of the datatypes used in this cell. """ datatypes = set() for element in itertools.chain(self.polygons, self.paths): datatypes.update(element.datatypes) for reference in self.references: datatypes.update(reference.ref_cell.get_datatypes()) return datatypes def get_bounding_box(self): """ Calculate the bounding box for this cell. Returns ------- out : Numpy array[2, 2] or None Bounding box of this cell [[x_min, y_min], [x_max, y_max]], or None if the cell is empty. """ if ( len(self.polygons) == 0 and len(self.paths) == 0 and len(self.references) == 0 ): return None if not ( self._bb_valid and all(ref._bb_valid for ref in self.get_dependencies(True)) ): bb = numpy.array(((1e300, 1e300), (-1e300, -1e300))) all_polygons = [] for polygon in self.polygons: all_polygons.extend(polygon.polygons) for path in self.paths: all_polygons.extend(path.to_polygonset().polygons) for reference in self.references: reference_bb = reference.get_bounding_box() if reference_bb is not None: bb[0, 0] = min(bb[0, 0], reference_bb[0, 0]) bb[0, 1] = min(bb[0, 1], reference_bb[0, 1]) bb[1, 0] = max(bb[1, 0], reference_bb[1, 0]) bb[1, 1] = max(bb[1, 1], reference_bb[1, 1]) if len(all_polygons) > 0: all_points = numpy.concatenate(all_polygons).transpose() bb[0, 0] = min(bb[0, 0], all_points[0].min()) bb[0, 1] = min(bb[0, 1], all_points[1].min()) bb[1, 0] = max(bb[1, 0], all_points[0].max()) bb[1, 1] = max(bb[1, 1], all_points[1].max()) self._bb_valid = True _bounding_boxes[self] = bb return _bounding_boxes[self] def get_polygons(self, by_spec=False, depth=None): """ Return a list of polygons in this cell. Parameters ---------- by_spec : bool If True, the return value is a dictionary with the polygons of each individual pair (layer, datatype). depth : integer or None If not None, defines from how many reference levels to retrieve polygons. References below this level will result in a bounding box. If `by_spec` is True the key will be the name of this cell. Returns ------- out : list of array-like[N][2] or dictionary List containing the coordinates of the vertices of each polygon, or dictionary with the list of polygons (if `by_spec` is True). Note ---- Instances of `FlexPath` and `RobustPath` are also included in the result by computing their polygonal boundary. """ if depth is not None and depth < 0: bb = self.get_bounding_box() if bb is None: return {} if by_spec else [] pts = [ numpy.array( [ (bb[0, 0], bb[0, 1]), (bb[0, 0], bb[1, 1]), (bb[1, 0], bb[1, 1]), (bb[1, 0], bb[0, 1]), ] ) ] polygons = {self.name: pts} if by_spec else pts else: if by_spec: polygons = {} for polyset in self.polygons: for ii in range(len(polyset.polygons)): key = (polyset.layers[ii], polyset.datatypes[ii]) if key in polygons: polygons[key].append(numpy.array(polyset.polygons[ii])) else: polygons[key] = [numpy.array(polyset.polygons[ii])] for path in self.paths: path_polygons = path.get_polygons(True) for kk in path_polygons.keys(): if kk in polygons: polygons[kk].extend(path_polygons[kk]) else: polygons[kk] = path_polygons[kk] for reference in self.references: if depth is None: next_depth = None else: next_depth = depth - 1 cell_polygons = reference.get_polygons(True, next_depth) for kk in cell_polygons.keys(): if kk in polygons: polygons[kk].extend(cell_polygons[kk]) else: polygons[kk] = cell_polygons[kk] else: polygons = [] for polyset in self.polygons: for points in polyset.polygons: polygons.append(numpy.array(points)) for path in self.paths: polygons.extend(path.get_polygons()) for reference in self.references: if depth is None: next_depth = None else: next_depth = depth - 1 polygons.extend(reference.get_polygons(depth=next_depth)) return polygons def get_polygonsets(self, depth=None): """ Return a list with a copy of the polygons in this cell. Parameters ---------- depth : integer or None If not None, defines from how many reference levels to retrieve polygons from. Returns ------- out : list of `PolygonSet` List containing the polygons in this cell and its references. """ polys = libcopy.deepcopy(self.polygons) if depth is None or depth > 0: for reference in self.references: if depth is None: next_depth = None else: next_depth = depth - 1 polys.extend(reference.get_polygonsets(next_depth)) return polys def get_paths(self, depth=None): """ Return a list with a copy of the paths in this cell. Parameters ---------- depth : integer or None If not None, defines from how many reference levels to retrieve paths from. Returns ------- out : list of `FlexPath` or `RobustPath` List containing the paths in this cell and its references. """ paths = libcopy.deepcopy(self.paths) if depth is None or depth > 0: for reference in self.references: if depth is None: next_depth = None else: next_depth = depth - 1 paths.extend(reference.get_paths(next_depth)) return paths def get_labels(self, depth=None): """ Return a list with a copy of the labels in this cell. Parameters ---------- depth : integer or None If not None, defines from how many reference levels to retrieve labels from. Returns ------- out : list of `Label` List containing the labels in this cell and its references. """ labels = libcopy.deepcopy(self.labels) if depth is None or depth > 0: for reference in self.references: if depth is None: next_depth = None else: next_depth = depth - 1 labels.extend(reference.get_labels(next_depth)) return labels def get_dependencies(self, recursive=False): """ Return a list of the cells included in this cell as references. Parameters ---------- recursive : bool If True returns cascading dependencies. Returns ------- out : set of `Cell` List of the cells referenced by this cell. """ dependencies = set() for reference in self.references: if recursive: dependencies.update(reference.ref_cell.get_dependencies(True)) dependencies.add(reference.ref_cell) return dependencies def flatten(self, single_layer=None, single_datatype=None, single_texttype=None): """ Convert all references into polygons, paths and labels. Parameters ---------- single_layer : integer or None If not None, all polygons will be transfered to the layer indicated by this number. single_datatype : integer or None If not None, all polygons will be transfered to the datatype indicated by this number. single_datatype : integer or None If not None, all labels will be transfered to the texttype indicated by this number. Returns ------- out : `Cell` This cell. """ self.labels = self.get_labels() if single_layer is not None and single_datatype is not None: for lbl in self.labels: lbl.layer = single_layer lbl.texttype = single_texttype elif single_layer is not None: for lbl in self.labels: lbl.layer = single_layer elif single_datatype is not None: for lbl in self.labels: lbl.texttype = single_texttype self.polygons = self.get_polygonsets() self.paths = self.get_paths() if single_layer is not None and single_datatype is not None: for poly in self.polygons: poly.layers = [single_layer] * len(poly.polygons) poly.datatypes = [single_datatype] * len(poly.polygons) for path in self.paths: path.layers = [single_layer] * path.n path.datatypes = [single_datatype] * path.n elif single_layer is not None: for poly in self.polygons: poly.layers = [single_layer] * len(poly.polygons) for path in self.paths: path.layers = [single_layer] * path.n elif single_datatype is not None: for poly in self.polygons: poly.datatypes = [single_datatype] * len(poly.polygons) for path in self.paths: path.datatypes = [single_datatype] * path.n self.references = [] return self class CellReference(object): """ Simple reference to an existing cell. Parameters ---------- ref_cell : `Cell` or string The referenced cell or its name. origin : array-like[2] Position where the reference is inserted. rotation : number Angle of rotation of the reference (in *degrees*). magnification : number Magnification factor for the reference. x_reflection : bool If True the reference is reflected parallel to the x direction before being rotated. ignore_missing : bool If False a warning is issued when the referenced cell is not found. """ __slots__ = ("ref_cell", "origin", "rotation", "magnification", "x_reflection") def __init__( self, ref_cell, origin=(0, 0), rotation=None, magnification=None, x_reflection=False, ignore_missing=False, ): self.origin = origin self.ref_cell = current_library.cell_dict.get(ref_cell, ref_cell) self.rotation = rotation self.magnification = magnification self.x_reflection = x_reflection if not isinstance(self.ref_cell, Cell) and not ignore_missing: warnings.warn( "[GDSPY] Cell {0} not found; operations on this CellReference may not work.".format( self.ref_cell ), stacklevel=2, ) def __str__(self): if isinstance(self.ref_cell, Cell): name = self.ref_cell.name else: name = self.ref_cell return 'CellReference ("{0}", at ({1[0]}, {1[1]}), rotation {2}, magnification {3}, reflection {4})'.format( name, self.origin, self.rotation, self.magnification, self.x_reflection ) def __repr__(self): if isinstance(self.ref_cell, Cell): name = self.ref_cell.name else: name = self.ref_cell return 'CellReference("{0}", ({1[0]}, {1[1]}), {2}, {3}, {4})'.format( name, self.origin, self.rotation, self.magnification, self.x_reflection ) def to_gds(self, multiplier): """ Convert this object to a GDSII element. Parameters ---------- multiplier : number A number that multiplies all dimensions written in the GDSII element. Returns ------- out : string The GDSII binary string that represents this object. """ name = self.ref_cell.name if len(name) % 2 != 0: name = name + "\0" data = struct.pack(">4H", 4, 0x0A00, 4 + len(name), 0x1206) + name.encode( "ascii" ) if ( (self.rotation is not None) or (self.magnification is not None) or self.x_reflection ): word = 0 values = b"" if self.x_reflection: word += 0x8000 if not (self.magnification is None): # This flag indicates that the magnification is absolute, not # relative (not supported). # word += 0x0004 values += struct.pack(">2H", 12, 0x1B05) + _eight_byte_real( self.magnification ) if not (self.rotation is None): # This flag indicates that the rotation is absolute, not # relative (not supported). # word += 0x0002 values += struct.pack(">2H", 12, 0x1C05) + _eight_byte_real( self.rotation ) data += struct.pack(">3H", 6, 0x1A01, word) + values return data + struct.pack( ">2H2l2H", 12, 0x1003, int(round(self.origin[0] * multiplier)), int(round(self.origin[1] * multiplier)), 4, 0x1100, ) def area(self, by_spec=False): """ Calculate the total area of the referenced cell with the magnification factor included. Parameters ---------- by_spec : bool If True, the return value is a dictionary with the areas of each individual pair (layer, datatype). Returns ------- out : number, dictionary Area of this cell. """ if not isinstance(self.ref_cell, Cell): return dict() if by_spec else 0 if self.magnification is None: return self.ref_cell.area(by_spec) else: if by_spec: factor = self.magnification ** 2 cell_area = self.ref_cell.area(True) for kk in cell_area.keys(): cell_area[kk] *= factor return cell_area else: return self.ref_cell.area() * self.magnification ** 2 def get_polygons(self, by_spec=False, depth=None): """ Return the list of polygons created by this reference. Parameters ---------- by_spec : bool If True, the return value is a dictionary with the polygons of each individual pair (layer, datatype). depth : integer or None If not None, defines from how many reference levels to retrieve polygons. References below this level will result in a bounding box. If `by_spec` is True the key will be the name of the referenced cell. Returns ------- out : list of array-like[N][2] or dictionary List containing the coordinates of the vertices of each polygon, or dictionary with the list of polygons (if `by_spec` is True). Note ---- Instances of `FlexPath` and `RobustPath` are also included in the result by computing their polygonal boundary. """ if not isinstance(self.ref_cell, Cell): return dict() if by_spec else [] if self.rotation is not None: ct = numpy.cos(self.rotation * numpy.pi / 180.0) st = numpy.sin(self.rotation * numpy.pi / 180.0) * _mpone if self.x_reflection: xrefl = _pmone_int if self.magnification is not None: mag = self.magnification * _one if self.origin is not None: orgn = numpy.array(self.origin) if by_spec: polygons = self.ref_cell.get_polygons(True, depth) for kk in polygons.keys(): for ii in range(len(polygons[kk])): if self.x_reflection: polygons[kk][ii] = polygons[kk][ii] * xrefl if self.magnification is not None: polygons[kk][ii] = polygons[kk][ii] * mag if self.rotation is not None: polygons[kk][ii] = ( polygons[kk][ii] * ct + polygons[kk][ii][:, ::-1] * st ) if self.origin is not None: polygons[kk][ii] = polygons[kk][ii] + orgn else: polygons = self.ref_cell.get_polygons(depth=depth) for ii in range(len(polygons)): if self.x_reflection: polygons[ii] = polygons[ii] * xrefl if self.magnification is not None: polygons[ii] = polygons[ii] * mag if self.rotation is not None: polygons[ii] = polygons[ii] * ct + polygons[ii][:, ::-1] * st if self.origin is not None: polygons[ii] = polygons[ii] + orgn return polygons def get_polygonsets(self, depth=None): """ Return the list of polygons created by this reference. Parameters ---------- depth : integer or None If not None, defines from how many reference levels to retrieve polygons from. Returns ------- out : list of `PolygonSet` List containing the polygons in this cell and its references. """ if not isinstance(self.ref_cell, Cell): return [] if self.rotation is not None: ct = numpy.cos(self.rotation * numpy.pi / 180.0) st = numpy.sin(self.rotation * numpy.pi / 180.0) * _mpone if self.x_reflection: xrefl = _pmone_int if self.magnification is not None: mag = self.magnification * _one if self.origin is not None: orgn = numpy.array(self.origin) polygonsets = self.ref_cell.get_polygonsets(depth=depth) for ps in polygonsets: for ii in range(len(ps.polygons)): if self.x_reflection: ps.polygons[ii] = ps.polygons[ii] * xrefl if self.magnification is not None: ps.polygons[ii] = ps.polygons[ii] * mag if self.rotation is not None: ps.polygons[ii] = ( ps.polygons[ii] * ct + ps.polygons[ii][:, ::-1] * st ) if self.origin is not None: ps.polygons[ii] = ps.polygons[ii] + orgn return polygonsets def get_paths(self, depth=None): """ Return the list of paths created by this reference. Parameters ---------- depth : integer or None If not None, defines from how many reference levels to retrieve paths from. Returns ------- out : list of `FlexPath` or `RobustPath` List containing the paths in this cell and its references. """ if not isinstance(self.ref_cell, Cell): return [] if self.origin is not None: trans = numpy.array(self.origin) else: trans = None if self.rotation is not None: rot = self.rotation * numpy.pi / 180.0 else: rot = None return [ p.transform(trans, rot, self.magnification, self.x_reflection) for p in self.ref_cell.get_paths(depth=depth) ] def get_labels(self, depth=None): """ Return the list of labels created by this reference. Parameters ---------- depth : integer or None If not None, defines from how many reference levels to retrieve labels from. Returns ------- out : list of `Label` List containing the labels in this cell and its references. """ if not isinstance(self.ref_cell, Cell): return [] if self.rotation is not None: ct = numpy.cos(self.rotation * numpy.pi / 180.0) st = numpy.sin(self.rotation * numpy.pi / 180.0) * _mpone if self.x_reflection: xrefl = _pmone_int if self.magnification is not None: mag = self.magnification * _one if self.origin is not None: orgn = numpy.array(self.origin) labels = self.ref_cell.get_labels(depth=depth) for lbl in labels: if self.x_reflection: lbl.position = lbl.position * xrefl if self.magnification is not None: lbl.position = lbl.position * mag if self.rotation is not None: lbl.position = lbl.position * ct + lbl.position[::-1] * st if self.origin is not None: lbl.position = lbl.position + orgn return labels def get_bounding_box(self): """ Calculate the bounding box for this reference. Returns ------- out : Numpy array[2, 2] or None Bounding box of this cell [[x_min, y_min], [x_max, y_max]], or None if the cell is empty. """ if not isinstance(self.ref_cell, Cell): return None if ( self.rotation is None and self.magnification is None and self.x_reflection is None ): key = self else: key = (self.ref_cell, self.rotation, self.magnification, self.x_reflection) deps = self.ref_cell.get_dependencies(True) if not ( self.ref_cell._bb_valid and all(ref._bb_valid for ref in deps) and key in _bounding_boxes ): for ref in deps: ref.get_bounding_box() self.ref_cell.get_bounding_box() tmp = self.origin self.origin = None polygons = self.get_polygons() self.origin = tmp if len(polygons) == 0: bb = None else: all_points = numpy.concatenate(polygons).transpose() bb = numpy.array( ( (all_points[0].min(), all_points[1].min()), (all_points[0].max(), all_points[1].max()), ) ) _bounding_boxes[key] = bb else: bb = _bounding_boxes[key] if self.origin is None or bb is None: return bb else: return bb + numpy.array( ((self.origin[0], self.origin[1]), (self.origin[0], self.origin[1])) ) def translate(self, dx, dy): """ Translate this reference. Parameters ---------- dx : number Distance to move in the x-direction. dy : number Distance to move in the y-direction. Returns ------- out : `CellReference` This object. """ self.origin = (self.origin[0] + dx, self.origin[1] + dy) return self class CellArray(object): """ Multiple references to an existing cell in an array format. Parameters ---------- ref_cell : `Cell` or string The referenced cell or its name. columns : positive integer Number of columns in the array. rows : positive integer Number of columns in the array. spacing : array-like[2] distances between adjacent columns and adjacent rows. origin : array-like[2] Position where the cell is inserted. rotation : number Angle of rotation of the reference (in *degrees*). magnification : number Magnification factor for the reference. x_reflection : bool If True, the reference is reflected parallel to the x direction before being rotated. ignore_missing : bool If False a warning is issued when the referenced cell is not found. """ __slots__ = ( "ref_cell", "origin", "rotation", "magnification", "x_reflection", "columns", "rows", "spacing", ) def __init__( self, ref_cell, columns, rows, spacing, origin=(0, 0), rotation=None, magnification=None, x_reflection=False, ignore_missing=False, ): self.columns = columns self.rows = rows self.spacing = spacing self.origin = origin self.ref_cell = current_library.cell_dict.get(ref_cell, ref_cell) self.rotation = rotation self.magnification = magnification self.x_reflection = x_reflection if not isinstance(self.ref_cell, Cell) and not ignore_missing: warnings.warn( "[GDSPY] Cell {0} not found; operations on this CellArray may not work.".format( self.ref_cell ), stacklevel=2, ) def __str__(self): if isinstance(self.ref_cell, Cell): name = self.ref_cell.name else: name = self.ref_cell return 'CellArray ("{0}", {1} x {2}, at ({3[0]}, {3[1]}), spacing {4[0]} x {4[1]}, rotation {5}, magnification {6}, reflection {7})'.format( name, self.columns, self.rows, self.origin, self.spacing, self.rotation, self.magnification, self.x_reflection, ) def __repr__(self): if isinstance(self.ref_cell, Cell): name = self.ref_cell.name else: name = self.ref_cell return 'CellArray("{0}", {1}, {2}, ({4[0]}, {4[1]}), ({3[0]}, {3[1]}), {5}, {6}, {7})'.format( name, self.columns, self.rows, self.origin, self.spacing, self.rotation, self.magnification, self.x_reflection, ) def to_gds(self, multiplier): """ Convert this object to a GDSII element. Parameters ---------- multiplier : number A number that multiplies all dimensions written in the GDSII element. Returns ------- out : string The GDSII binary string that represents this object. """ name = self.ref_cell.name if len(name) % 2 != 0: name = name + "\0" data = struct.pack(">4H", 4, 0x0B00, 4 + len(name), 0x1206) + name.encode( "ascii" ) x2 = self.origin[0] + self.columns * self.spacing[0] y2 = self.origin[1] x3 = self.origin[0] y3 = self.origin[1] + self.rows * self.spacing[1] if ( (self.rotation is not None) or (self.magnification is not None) or self.x_reflection ): word = 0 values = b"" if self.x_reflection: word += 0x8000 y3 = 2 * self.origin[1] - y3 if not (self.magnification is None): # This flag indicates that the magnification is absolute, not # relative (not supported). # word += 0x0004 values += struct.pack(">2H", 12, 0x1B05) + _eight_byte_real( self.magnification ) if not (self.rotation is None): # This flag indicates that the rotation is absolute, not # relative (not supported). # word += 0x0002 sa = numpy.sin(self.rotation * numpy.pi / 180.0) ca = numpy.cos(self.rotation * numpy.pi / 180.0) tmp = ( (x2 - self.origin[0]) * ca - (y2 - self.origin[1]) * sa + self.origin[0] ) y2 = ( (x2 - self.origin[0]) * sa + (y2 - self.origin[1]) * ca + self.origin[1] ) x2 = tmp tmp = ( (x3 - self.origin[0]) * ca - (y3 - self.origin[1]) * sa + self.origin[0] ) y3 = ( (x3 - self.origin[0]) * sa + (y3 - self.origin[1]) * ca + self.origin[1] ) x3 = tmp values += struct.pack(">2H", 12, 0x1C05) + _eight_byte_real( self.rotation ) data += struct.pack(">3H", 6, 0x1A01, word) + values return data + struct.pack( ">2H2h2H6l2H", 8, 0x1302, self.columns, self.rows, 28, 0x1003, int(round(self.origin[0] * multiplier)), int(round(self.origin[1] * multiplier)), int(round(x2 * multiplier)), int(round(y2 * multiplier)), int(round(x3 * multiplier)), int(round(y3 * multiplier)), 4, 0x1100, ) def area(self, by_spec=False): """ Calculate the total area of the cell array with the magnification factor included. Parameters ---------- by_spec : bool If True, the return value is a dictionary with the areas of each individual pair (layer, datatype). Returns ------- out : number, dictionary Area of this cell. """ if not isinstance(self.ref_cell, Cell): return dict() if by_spec else 0 if self.magnification is None: factor = self.columns * self.rows else: factor = self.columns * self.rows * self.magnification ** 2 if by_spec: cell_area = self.ref_cell.area(True) for kk in cell_area.keys(): cell_area[kk] *= factor return cell_area else: return self.ref_cell.area() * factor def get_polygons(self, by_spec=False, depth=None): """ Return the list of polygons created by this reference. Parameters ---------- by_spec : bool If True, the return value is a dictionary with the polygons of each individual pair (layer, datatype). depth : integer or None If not None, defines from how many reference levels to retrieve polygons. References below this level will result in a bounding box. If `by_spec` is True the key will be name of the referenced cell. Returns ------- out : list of array-like[N][2] or dictionary List containing the coordinates of the vertices of each polygon, or dictionary with the list of polygons (if `by_spec` is True). Note ---- Instances of `FlexPath` and `RobustPath` are also included in the result by computing their polygonal boundary. """ if not isinstance(self.ref_cell, Cell): return dict() if by_spec else [] if self.rotation is not None: ct = numpy.cos(self.rotation * numpy.pi / 180.0) st = numpy.sin(self.rotation * numpy.pi / 180.0) * _mpone if self.magnification is not None: mag = self.magnification * _one if self.origin is not None: orgn = numpy.array(self.origin) if self.x_reflection: xrefl = _pmone_int if by_spec: cell_polygons = self.ref_cell.get_polygons(True, depth) polygons = {} for kk in cell_polygons.keys(): polygons[kk] = [] for ii in range(self.columns): for jj in range(self.rows): spc = numpy.array([self.spacing[0] * ii, self.spacing[1] * jj]) for points in cell_polygons[kk]: if self.magnification: polygons[kk].append(points * mag + spc) else: polygons[kk].append(points + spc) if self.x_reflection: polygons[kk][-1] = polygons[kk][-1] * xrefl if self.rotation is not None: polygons[kk][-1] = ( polygons[kk][-1] * ct + polygons[kk][-1][:, ::-1] * st ) if self.origin is not None: polygons[kk][-1] = polygons[kk][-1] + orgn else: cell_polygons = self.ref_cell.get_polygons(depth=depth) polygons = [] for ii in range(self.columns): for jj in range(self.rows): spc = numpy.array([self.spacing[0] * ii, self.spacing[1] * jj]) for points in cell_polygons: if self.magnification is not None: polygons.append(points * mag + spc) else: polygons.append(points + spc) if self.x_reflection: polygons[-1] = polygons[-1] * xrefl if self.rotation is not None: polygons[-1] = ( polygons[-1] * ct + polygons[-1][:, ::-1] * st ) if self.origin is not None: polygons[-1] = polygons[-1] + orgn return polygons def get_polygonsets(self, depth=None): """ Return the list of polygons created by this reference. Parameters ---------- depth : integer or None If not None, defines from how many reference levels to retrieve polygons from. Returns ------- out : list of `PolygonSet` List containing the polygons in this cell and its references. """ if not isinstance(self.ref_cell, Cell): return [] if self.rotation is not None: ct = numpy.cos(self.rotation * numpy.pi / 180.0) st = numpy.sin(self.rotation * numpy.pi / 180.0) * _mpone if self.x_reflection: xrefl = _pmone_int if self.magnification is not None: mag = self.magnification * _one if self.origin is not None: orgn = numpy.array(self.origin) polygonsets = self.ref_cell.get_polygonsets(depth=depth) array = [] for i in range(self.columns): for j in range(self.rows): spc = numpy.array([self.spacing[0] * i, self.spacing[1] * j]) for polygonset in polygonsets: ps = libcopy.deepcopy(polygonset) for ii in range(len(ps.polygons)): if self.magnification is not None: ps.polygons[ii] = ps.polygons[ii] * mag + spc else: ps.polygons[ii] = ps.polygons[ii] + spc if self.x_reflection: ps.polygons[ii] = ps.polygons[ii] * xrefl if self.rotation is not None: ps.polygons[ii] = ( ps.polygons[ii] * ct + ps.polygons[ii][:, ::-1] * st ) if self.origin is not None: ps.polygons[ii] = ps.polygons[ii] + orgn array.append(ps) return array def get_paths(self, depth=None): """ Return the list of paths created by this reference. Parameters ---------- depth : integer or None If not None, defines from how many reference levels to retrieve paths from. Returns ------- out : list of `FlexPath` or `RobustPath` List containing the paths in this cell and its references. """ if not isinstance(self.ref_cell, Cell): return [] if self.origin is not None: trans = numpy.array(self.origin) else: trans = None if self.rotation is not None: rot = self.rotation * numpy.pi / 180.0 else: rot = None paths = self.ref_cell.get_paths(depth=depth) array = [] for i in range(self.columns): for j in range(self.rows): spc = numpy.array([self.spacing[0] * i, self.spacing[1] * j]) for path in paths: array.append( libcopy.deepcopy(path).transform( trans, rot, self.magnification, self.x_reflection, spc ) ) return array def get_labels(self, depth=None): """ Return the list of labels created by this reference. Parameters ---------- depth : integer or None If not None, defines from how many reference levels to retrieve labels from. Returns ------- out : list of `Label` List containing the labels in this cell and its references. """ if not isinstance(self.ref_cell, Cell): return [] if self.rotation is not None: ct = numpy.cos(self.rotation * numpy.pi / 180.0) st = numpy.sin(self.rotation * numpy.pi / 180.0) * _mpone if self.magnification is not None: mag = self.magnification * _one if self.origin is not None: orgn = numpy.array(self.origin) if self.x_reflection: xrefl = _pmone_int cell_labels = self.ref_cell.get_labels(depth=depth) labels = [] for ii in range(self.columns): for jj in range(self.rows): spc = numpy.array([self.spacing[0] * ii, self.spacing[1] * jj]) for clbl in cell_labels: lbl = libcopy.deepcopy(clbl) if self.magnification: lbl.position = lbl.position * mag + spc else: lbl.position = lbl.position + spc if self.x_reflection: lbl.position = lbl.position * xrefl if self.rotation is not None: lbl.position = lbl.position * ct + lbl.position[::-1] * st if self.origin is not None: lbl.position = lbl.position + orgn labels.append(lbl) return labels def get_bounding_box(self): """ Calculate the bounding box for this reference. Returns ------- out : Numpy array[2, 2] or None Bounding box of this cell [[x_min, y_min], [x_max, y_max]], or None if the cell is empty. """ if not isinstance(self.ref_cell, Cell): return None key = ( self.ref_cell, self.rotation, self.magnification, self.x_reflection, self.columns, self.rows, self.spacing[0], self.spacing[1], ) deps = self.ref_cell.get_dependencies(True) if not ( self.ref_cell._bb_valid and all(ref._bb_valid for ref in deps) and key in _bounding_boxes ): for ref in deps: ref.get_bounding_box() self.ref_cell.get_bounding_box() tmp = self.origin self.origin = None polygons = self.get_polygons() self.origin = tmp if len(polygons) == 0: bb = None else: all_points = numpy.concatenate(polygons).transpose() bb = numpy.array( ( (all_points[0].min(), all_points[1].min()), (all_points[0].max(), all_points[1].max()), ) ) _bounding_boxes[key] = bb else: bb = _bounding_boxes[key] if self.origin is None or bb is None: return bb else: return bb + numpy.array( ((self.origin[0], self.origin[1]), (self.origin[0], self.origin[1])) ) def translate(self, dx, dy): """ Translate this reference. Parameters ---------- dx : number Distance to move in the x-direction. dy : number Distance to move in the y-direction. Returns ------- out : `CellArray` This object. """ self.origin = (self.origin[0] + dx, self.origin[1] + dy) return self class GdsLibrary(object): """ GDSII library (file). Represent a GDSII library containing a dictionary of cells. Parameters ---------- name : string Name of the GDSII library. Ignored if a name is defined in `infile`. infile : file or string GDSII stream file (or path) to be imported. It must be opened for reading in binary format. kwargs : keyword arguments Arguments passed to `read_gds`. Attributes ---------- name : string Name of the GDSII library. cell_dict : dictionary Dictionary of cells in this library, indexed by name. unit : number Unit size for the objects in the library (in *meters*). precision : number Precision for the dimensions of the objects in the library (in *meters*). """ _record_name = ( "HEADER", "BGNLIB", "LIBNAME", "UNITS", "ENDLIB", "BGNSTR", "STRNAME", "ENDSTR", "BOUNDARY", "PATH", "SREF", "AREF", "TEXT", "LAYER", "DATATYPE", "WIDTH", "XY", "ENDEL", "SNAME", "COLROW", "TEXTNODE", "NODE", "TEXTTYPE", "PRESENTATION", "SPACING", "STRING", "STRANS", "MAG", "ANGLE", "UINTEGER", "USTRING", "REFLIBS", "FONTS", "PATHTYPE", "GENERATIONS", "ATTRTABLE", "STYPTABLE", "STRTYPE", "ELFLAGS", "ELKEY", "LINKTYPE", "LINKKEYS", "NODETYPE", "PROPATTR", "PROPVALUE", "BOX", "BOXTYPE", "PLEX", "BGNEXTN", "ENDTEXTN", "TAPENUM", "TAPECODE", "STRCLASS", "RESERVED", "FORMAT", "MASK", "ENDMASKS", "LIBDIRSIZE", "SRFNAME", "LIBSECUR", ) _unused_records = (0x05, 0x00, 0x01, 0x02, 0x034, 0x38) _import_anchors = ["nw", "n", "ne", None, "w", "o", "e", None, "sw", "s", "se"] _pathtype_dict = {0: "flush", 1: "round", 2: "extended"} __slots__ = "name", "cell_dict", "unit", "precision", "_references" def __init__( self, name="library", infile=None, unit=1e-6, precision=1e-9, **kwargs ): self.name = name self.cell_dict = {} self.unit = unit self.precision = precision if infile is not None: self.read_gds(infile, **kwargs) def __str__(self): return "GdsLibrary (" + ", ".join([c for c in self.cell_dict]) + ")" def add(self, cell, overwrite_duplicate=False): """ Add one or more cells to the library. Parameters ---------- cell : `Cell` or iterable Cells to be included in the library. overwrite_duplicate : bool If True an existing cell with the same name in the library will be overwritten. Returns ------- out : `GdsLibrary` This object. Notes ----- `CellReference` or `CellArray` instances that referred to an overwritten cell are not automatically updated. """ if isinstance(cell, Cell): if ( not overwrite_duplicate and cell.name in self.cell_dict and self.cell_dict[cell.name] is not cell ): raise ValueError( "[GDSPY] Cell named {0} already present in library.".format( cell.name ) ) self.cell_dict[cell.name] = cell else: for c in cell: if ( not overwrite_duplicate and c.name in self.cell_dict and self.cell_dict[c.name] is not c ): raise ValueError( "[GDSPY] Cell named {0} already present in library.".format( c.name ) ) self.cell_dict[c.name] = c return self def write_gds(self, outfile, cells=None, timestamp=None, binary_cells=None): """ Write the GDSII library to a file. The dimensions actually written on the GDSII file will be the dimensions of the objects created times the ratio unit/precision. For example, if a circle with radius 1.5 is created and we set `GdsLibrary.unit` to 1.0e-6 (1 um) and `GdsLibrary.precision` to 1.0e-9` (1 nm), the radius of the circle will be 1.5 um and the GDSII file will contain the dimension 1500 nm. Parameters ---------- outfile : file or string The file (or path) where the GDSII stream will be written. It must be opened for writing operations in binary format. cells : iterable The cells or cell names to be included in the library. If None, all cells are used. timestamp : datetime object Sets the GDSII timestamp. If None, the current time is used. binary_cells : iterable of bytes Iterable with binary data for GDSII cells (from `get_binary_cells`, for example). Notes ----- Only the specified cells are written. The user is responsible for ensuring all cell dependencies are satisfied. """ if isinstance(outfile, basestring): outfile = open(outfile, "wb") close = True else: close = False now = datetime.datetime.today() if timestamp is None else timestamp name = self.name if len(self.name) % 2 == 0 else (self.name + "\0") outfile.write( struct.pack( ">5H12h2H", 6, 0x0002, 0x0258, 28, 0x0102, now.year, now.month, now.day, now.hour, now.minute, now.second, now.year, now.month, now.day, now.hour, now.minute, now.second, 4 + len(name), 0x0206, ) + name.encode("ascii") + struct.pack(">2H", 20, 0x0305) + _eight_byte_real(self.precision / self.unit) + _eight_byte_real(self.precision) ) if cells is None: cells = self.cell_dict.values() else: cells = [self.cell_dict.get(c, c) for c in cells] for cell in cells: outfile.write(cell.to_gds(self.unit / self.precision)) if binary_cells is not None: for bc in binary_cells: outfile.write(bc) outfile.write(struct.pack(">2H", 4, 0x0400)) if close: outfile.close() def read_gds( self, infile, units="skip", rename={}, rename_template="{name}", layers={}, datatypes={}, texttypes={}, ): """ Read a GDSII file into this library. Parameters ---------- infile : file or string GDSII stream file (or path) to be imported. It must be opened for reading in binary format. units : {'convert', 'import', 'skip'} Controls how to scale and use the units in the imported file. 'convert': the imported geometry is scaled to this library units. 'import': the unit and precision in this library are replaced by those from the imported file. 'skip': the imported geometry is not scaled and units are not replaced; the geometry is imported in the *user units* of the file. rename : dictionary Dictionary used to rename the imported cells. Keys and values must be strings. rename_template : string Template string used to rename the imported cells. Appiled only if the cell name is not in the `rename` dictionary. Examples: 'prefix-{name}', '{name}-suffix' layers : dictionary Dictionary used to convert the layers in the imported cells. Keys and values must be integers. datatypes : dictionary Dictionary used to convert the datatypes in the imported cells. Keys and values must be integers. texttypes : dictionary Dictionary used to convert the text types in the imported cells. Keys and values must be integers. Returns ------- out : `GdsLibrary` This object. Notes ----- Not all features from the GDSII specification are currently supported. A warning will be produced if any unsupported features are found in the imported file. """ self._references = [] if isinstance(infile, basestring): infile = open(infile, "rb") close = True else: close = False emitted_warnings = [] kwargs = {} create_element = None factor = 1 cell = None for record in _record_reader(infile): # LAYER if record[0] == 0x0D: kwargs["layer"] = layers.get(record[1][0], record[1][0]) # DATATYPE elif record[0] == 0x0E: kwargs["datatype"] = datatypes.get(record[1][0], record[1][0]) # TEXTTYPE elif record[0] == 0x16: kwargs["texttype"] = texttypes.get(record[1][0], record[1][0]) # XY elif record[0] == 0x10: if "xy" in kwargs: kwargs["xy"] = numpy.concatenate((kwargs["xy"], factor * record[1])) else: kwargs["xy"] = factor * record[1] # WIDTH elif record[0] == 0x0F: kwargs["width"] = factor * abs(record[1][0]) if record[1][0] < 0: kwargs["width_transform"] = False # ENDEL elif record[0] == 0x11: if create_element is not None: cell.add(create_element(**kwargs)) create_element = None kwargs = {} # BOUNDARY elif record[0] == 0x08: create_element = self._create_polygon # PATH elif record[0] == 0x09: create_element = self._create_path # TEXT elif record[0] == 0x0C: create_element = self._create_label # SNAME elif record[0] == 0x12: if record[1] in rename: name = rename[record[1]] else: name = rename_template.format(name=record[1]) kwargs["ref_cell"] = name # COLROW elif record[0] == 0x13: kwargs["columns"] = record[1][0] kwargs["rows"] = record[1][1] # STRANS elif record[0] == 0x1A: kwargs["x_reflection"] = (int(record[1][0]) & 0x8000) > 0 if (int(record[1][0]) & 0x0006) and record[0] not in emitted_warnings: warnings.warn( "[GDSPY] Absolute magnification or rotation of references is not supported. Transformations will be interpreted as relative.", stacklevel=2, ) emitted_warnings.append(record[0]) # MAG elif record[0] == 0x1B: kwargs["magnification"] = record[1][0] # ANGLE elif record[0] == 0x1C: kwargs["rotation"] = record[1][0] # SREF elif record[0] == 0x0A: create_element = self._create_reference # AREF elif record[0] == 0x0B: create_element = self._create_array # STRNAME elif record[0] == 0x06: if record[1] in rename: name = rename[record[1]] else: name = rename_template.format(name=record[1]) cell = Cell(name, exclude_from_current=True) self.cell_dict[name] = cell # STRING elif record[0] == 0x19: kwargs["text"] = record[1] # ENDSTR elif record[0] == 0x07: cell = None # UNITS elif record[0] == 0x03: if units == "skip": factor = record[1][0] elif units == "import": self.unit = record[1][1] / record[1][0] self.precision = record[1][1] factor = record[1][0] elif units == "convert": factor = record[1][1] / self.unit else: raise ValueError( "[GDSPY] units must be one of 'convert', 'import' or 'skip'." ) # LIBNAME elif record[0] == 0x02: self.name = record[1] # PRESENTATION elif record[0] == 0x17: kwargs["anchor"] = GdsLibrary._import_anchors[ int(record[1][0]) & 0x000F ] # PATHTYPE elif record[0] == 0x21: kwargs["ends"] = GdsLibrary._pathtype_dict.get(record[1][0], "extended") # BGNEXTN elif record[0] == 0x30: kwargs["bgnextn"] = factor * record[1][0] # ENDEXTN elif record[0] == 0x31: kwargs["endextn"] = factor * record[1][0] # ENDLIB elif record[0] == 0x04: for ref in self._references: if ref.ref_cell in self.cell_dict: ref.ref_cell = self.cell_dict[ref.ref_cell] elif ref.ref_cell in current_library.cell_dict: ref.ref_cell = current_library.cell_dict[ref.ref_cell] # Not supported elif ( record[0] not in emitted_warnings and record[0] not in GdsLibrary._unused_records ): warnings.warn( "[GDSPY] Record type {0} ({1:02X}) is not supported.".format( GdsLibrary._record_name[record[0]], record[0] ), stacklevel=2, ) emitted_warnings.append(record[0]) if close: infile.close() return self def _create_polygon(self, layer, datatype, xy): return Polygon(xy[:-2].reshape((xy.size // 2 - 1, 2)), layer, datatype) def _create_path(self, **kwargs): xy = kwargs.pop("xy") if "bgnextn" in kwargs or "endextn" in kwargs: kwargs["ends"] = (kwargs.pop("bgnextn", 0), kwargs.pop("endextn", 0)) kwargs["points"] = xy.reshape((xy.size // 2, 2)) kwargs["gdsii_path"] = True return FlexPath(**kwargs) def _create_label(self, xy, width=None, ends=None, **kwargs): kwargs["position"] = xy return Label(**kwargs) def _create_reference(self, **kwargs): kwargs["origin"] = kwargs.pop("xy") kwargs["ignore_missing"] = True ref = CellReference(**kwargs) ref.ref_cell = kwargs["ref_cell"] self._references.append(ref) return ref def _create_array(self, **kwargs): xy = kwargs.pop("xy") kwargs["origin"] = xy[0:2] if "x_reflection" in kwargs: if "rotation" in kwargs: sa = -numpy.sin(kwargs["rotation"] * numpy.pi / 180.0) ca = numpy.cos(kwargs["rotation"] * numpy.pi / 180.0) x2 = (xy[2] - xy[0]) * ca - (xy[3] - xy[1]) * sa + xy[0] y3 = (xy[4] - xy[0]) * sa + (xy[5] - xy[1]) * ca + xy[1] else: x2 = xy[2] y3 = xy[5] if kwargs["x_reflection"]: y3 = 2 * xy[1] - y3 kwargs["spacing"] = ( (x2 - xy[0]) / kwargs["columns"], (y3 - xy[1]) / kwargs["rows"], ) else: kwargs["spacing"] = ( (xy[2] - xy[0]) / kwargs["columns"], (xy[5] - xy[1]) / kwargs["rows"], ) kwargs["ignore_missing"] = True ref = CellArray(**kwargs) ref.ref_cell = kwargs["ref_cell"] self._references.append(ref) return ref def extract(self, cell, overwrite_duplicate=False): """ Extract a cell from the this GDSII file and include it in the current global library, including referenced dependencies. Parameters ---------- cell : `Cell` or string Cell or name of the cell to be extracted from the imported file. Referenced cells will be automatically extracted as well. overwrite_duplicate : bool If True an existing cell with the same name in the current global library will be overwritten. Returns ------- out : `Cell` The extracted cell. Notes ----- `CellReference` or `CellArray` instances that referred to an overwritten cell are not automatically updated. """ cell = self.cell_dict.get(cell, cell) current_library.add(cell, overwrite_duplicate=overwrite_duplicate) current_library.add( cell.get_dependencies(True), overwrite_duplicate=overwrite_duplicate ) return cell def top_level(self): """ Output the top level cells from the GDSII data. Top level cells are those that are not referenced by any other cells. Returns ------- out : list List of top level cells. """ top = list(self.cell_dict.values()) for cell in self.cell_dict.values(): for dependency in cell.get_dependencies(): if dependency in top: top.remove(dependency) return top class GdsWriter(object): """ GDSII strem library writer. The dimensions actually written on the GDSII file will be the dimensions of the objects created times the ratio unit/precision. For example, if a circle with radius 1.5 is created and we set `unit` to 1.0e-6 (1 um) and `precision` to 1.0e-9 (1 nm), the radius of the circle will be 1.5 um and the GDSII file will contain the dimension 1500 nm. Parameters ---------- outfile : file or string The file (or path) where the GDSII stream will be written. It must be opened for writing operations in binary format. name : string Name of the GDSII library (file). unit : number Unit size for the objects in the library (in *meters*). precision : number Precision for the dimensions of the objects in the library (in *meters*). timestamp : datetime object Sets the GDSII timestamp. If None, the current time is used. Notes ----- This class can be used for incremental output of the geometry in case the complete layout is too large to be kept in memory all at once. Examples -------- >>> writer = gdspy.GdsWriter('out-file.gds', unit=1.0e-6, ... precision=1.0e-9) >>> for i in range(10): ... cell = gdspy.Cell('C{}'.format(i), True) ... # Add the contents of this cell... ... writer.write_cell(cell) ... # Clear the memory: erase Cell objects and any other objects ... # that won't be needed. ... del cell >>> writer.close() """ __slots__ = "_outfile", "_close", "_res" def __init__( self, outfile, name="library", unit=1.0e-6, precision=1.0e-9, timestamp=None ): if isinstance(outfile, basestring): self._outfile = open(outfile, "wb") self._close = True else: self._outfile = outfile self._close = False self._res = unit / precision now = datetime.datetime.today() if timestamp is None else timestamp if len(name) % 2 != 0: name = name + "\0" self._outfile.write( struct.pack( ">5H12h2H", 6, 0x0002, 0x0258, 28, 0x0102, now.year, now.month, now.day, now.hour, now.minute, now.second, now.year, now.month, now.day, now.hour, now.minute, now.second, 4 + len(name), 0x0206, ) + name.encode("ascii") + struct.pack(">2H", 20, 0x0305) + _eight_byte_real(precision / unit) + _eight_byte_real(precision) ) def write_cell(self, cell, timestamp=None): """ Write the specified cell to the file. Parameters ---------- cell : `Cell` Cell to be written. timestamp : datetime object Sets the GDSII timestamp. If None, the current time is used. Notes ----- Only the specified cell is written. Dependencies must be manually included. Returns ------- out : `GdsWriter` This object. """ self._outfile.write(cell.to_gds(self._res, timestamp)) return self def write_binary_cells(self, binary_cells): """ Write the specified binary cells to the file. Parameters ---------- binary_cells : iterable of bytes Iterable with binary data for GDSII cells (from `get_binary_cells`, for example). Returns ------- out : `GdsWriter` This object. """ for bc in binary_cells: self._outfile.write(bc) return self def close(self): """ Finalize the GDSII stream library. """ self._outfile.write(struct.pack(">2H", 4, 0x0400)) if self._close: self._outfile.close() def get_gds_units(infile): """ Return the unit and precision used in the GDS stream file. Parameters ---------- infile : file or string GDSII stream file to be queried. Returns ------- out : 2-tuple Return ``(unit, precision)`` from the file. """ if isinstance(infile, basestring): infile = open(infile, "rb") close = True else: close = False unit = precision = None for rec_type, data in _raw_record_reader(infile): # UNITS if rec_type == 0x03: db_user = _eight_byte_real_to_float(data[4:12]) db_meters = _eight_byte_real_to_float(data[12:]) unit = db_meters / db_user precision = db_meters break if close: infile.close() return (unit, precision) def get_binary_cells(infile): """ Load all cells from a GDSII stream file in binary format. Parameters ---------- infile : file or string GDSII stream file (or path) to be loaded. It must be opened for reading in binary format. Returns ------- out : dictionary Dictionary of binary cell representations indexed by name. Notes ----- The returned cells inherit the units of the loaded file. If they are used in a new library, the new library must use compatible units. """ if isinstance(infile, basestring): infile = open(infile, "rb") close = True else: close = False cells = {} name = None cell_data = None for rec_type, data in _raw_record_reader(infile): # BGNSTR if rec_type == 0x05: cell_data = [data] # STRNAME elif rec_type == 0x06: cell_data.append(data) if str is not bytes: if data[-1] == 0: name = data[4:-1].decode("ascii") else: name = data[4:].decode("ascii") else: if data[-1] == "\0": name = data[4:-1] else: name = data[4:] # ENDSTR elif rec_type == 0x07: cell_data.append(data) cells[name] = b"".join(cell_data) cell_data = None elif cell_data is not None: cell_data.append(data) if close: infile.close() return cells def slice(polygons, position, axis, precision=1e-3, layer=0, datatype=0): """ Slice polygons and polygon sets at given positions along an axis. Parameters ---------- polygons : `PolygonSet`, `CellReference`, `CellArray` or iterable Operand of the slice operation. If this is an iterable, each element must be a `PolygonSet`, `CellReference`, `CellArray`, or an array-like[N][2] of vertices of a polygon. position : number or list of numbers Positions to perform the slicing operation along the specified axis. axis : 0 or 1 Axis along which the polygon will be sliced. precision : float Desired precision for rounding vertice coordinates. layer : integer, list The GDSII layer numbers for the elements between each division. If the number of layers in the list is less than the number of divided regions, the list is repeated. datatype : integer, list The GDSII datatype for the resulting element (between 0 and 255). If the number of datatypes in the list is less than the number of divided regions, the list is repeated. Returns ------- out : list[N] of `PolygonSet` or None Result of the slicing operation, with N = len(positions) + 1. Each PolygonSet comprises all polygons between 2 adjacent slicing positions, in crescent order. Examples -------- >>> ring = gdspy.Round((0, 0), 10, inner_radius = 5) >>> result = gdspy.slice(ring, [-7, 7], 0) >>> cell.add(result[1]) """ polys = _gather_polys(polygons) if not isinstance(layer, list): layer = [layer] if not isinstance(datatype, list): datatype = [datatype] if not isinstance(position, list): pos = [position] else: pos = sorted(position) result = [[] for _ in range(len(pos) + 1)] scaling = 1 / precision for pol in polys: for r, p in zip(result, clipper._chop(pol, pos, axis, scaling)): r.extend(p) for i in range(len(result)): if len(result[i]) == 0: result[i] = None else: result[i] = PolygonSet( result[i], layer[i % len(layer)], datatype[i % len(datatype)] ) return result def offset( polygons, distance, join="miter", tolerance=2, precision=0.001, join_first=False, max_points=199, layer=0, datatype=0, ): """ Shrink or expand a polygon or polygon set. Parameters ---------- polygons : `PolygonSet`, `CellReference`, `CellArray` or iterable Polygons to be offset. If this is an iterable, each element must be a `PolygonSet`, `CellReference`, `CellArray`, or an array-like[N][2] of vertices of a polygon. distance : number Offset distance. Positive to expand, negative to shrink. join : 'miter', 'bevel', 'round' Type of join used to create the offset polygon. tolerance : number For miter joints, this number must be at least 2 and it represents the maximal distance in multiples of offset between new vertices and their original position before beveling to avoid spikes at acute joints. For round joints, it indicates the curvature resolution in number of points per full circle. precision : float Desired precision for rounding vertex coordinates. join_first : bool Join all paths before offsetting to avoid unnecessary joins in adjacent polygon sides. max_points : integer If greater than 4, fracture the resulting polygons to ensure they have at most `max_points` vertices. This is not a tessellating function, so this number should be as high as possible. For example, it should be set to 199 for polygons being drawn in GDSII files. layer : integer The GDSII layer number for the resulting element. datatype : integer The GDSII datatype for the resulting element (between 0 and 255). Returns ------- out : `PolygonSet` or None Return the offset shape as a set of polygons. """ result = clipper.offset( _gather_polys(polygons), distance, join, tolerance, 1 / precision, 1 if join_first else 0, ) if len(result) == 0: return None return PolygonSet(result, layer, datatype).fracture(max_points, precision) def boolean( operand1, operand2, operation, precision=0.001, max_points=199, layer=0, datatype=0 ): """ Execute any boolean operation between 2 polygons or polygon sets. Parameters ---------- operand1 : `PolygonSet`, `CellReference`, `CellArray` or iterable First operand. If this is an iterable, each element must be a `PolygonSet`, `CellReference`, `CellArray`, or an array-like[N][2] of vertices of a polygon. operand2 : None, `PolygonSet`, `CellReference`, `CellArray` or iterable Second operand. If this is an iterable, each element must be a `PolygonSet`, `CellReference`, `CellArray`, or an array-like[N][2] of vertices of a polygon. operation : {'or', 'and', 'xor', 'not'} Boolean operation to be executed. The 'not' operation returns the difference ``operand1 - operand2``. precision : float Desired precision for rounding vertice coordinates. max_points : integer If greater than 4, fracture the resulting polygons to ensure they have at most `max_points` vertices. This is not a tessellating function, so this number should be as high as possible. For example, it should be set to 199 for polygons being drawn in GDSII files. layer : integer The GDSII layer number for the resulting element. datatype : integer The GDSII datatype for the resulting element (between 0 and 255). Returns ------- out : PolygonSet or None Result of the boolean operation. """ poly1 = _gather_polys(operand1) poly2 = _gather_polys(operand2) if len(poly2) == 0: if operation in ["not", "xor"]: if len(poly1) == 0: return None return PolygonSet(poly1, layer, datatype).fracture(max_points, precision) poly2.append(poly1.pop()) result = clipper.clip(poly1, poly2, operation, 1 / precision) if len(result) == 0: return None return PolygonSet(result, layer, datatype).fracture(max_points, precision) fast_boolean = boolean def inside(points, polygons, short_circuit="any", precision=0.001): """ Test whether each of the points is within the given set of polygons. Parameters ---------- points : array-like[N][2] or sequence of array-like[N][2] Coordinates of the points to be tested or groups of points to be tested together. polygons : `PolygonSet`, `CellReference`, `CellArray` or iterable Polygons to be tested against. If this is an iterable, each element must be a `PolygonSet`, `CellReference`, `CellArray`, or an array-like[N][2] of vertices of a polygon. short_circuit : {'any', 'all'} If `points` is a sequence of point groups, testing within each group will be short-circuited if any of the points in the group is inside ('any') or outside ('all') the polygons. If `points` is simply a sequence of points, this parameter has no effect. precision : float Desired precision for rounding vertice coordinates. Returns ------- out : tuple Tuple of booleans indicating if each of the points or point groups is inside the set of polygons. """ polys = _gather_polys(polygons) if numpy.isscalar(points[0][0]): pts = (points,) sc = 0 else: pts = points sc = 1 if short_circuit == "any" else -1 return clipper.inside(pts, polys, sc, 1 / precision) def copy(obj, dx=0, dy=0): """ Create a copy of `obj` and translate it by (dx, dy). Parameters ---------- obj : translatable object Object to be copied. dx : number Distance to move in the x-direction. dy : number Distance to move in the y-direction. Returns ------- out : translatable object Translated copy of original `obj` Examples -------- >>> rectangle = gdspy.Rectangle((0, 0), (10, 20)) >>> rectangle2 = gdspy.copy(rectangle, 2,0) >>> myCell.add(rectangle) >>> myCell.add(rectangle2) """ newObj = libcopy.deepcopy(obj) if dx != 0 or dy != 0: newObj.translate(dx, dy) return newObj def write_gds( outfile, cells=None, name="library", unit=1.0e-6, precision=1.0e-9, timestamp=None, binary_cells=None, ): """ Write the current GDSII library to a file. The dimensions actually written on the GDSII file will be the dimensions of the objects created times the ratio unit/precision. For example, if a circle with radius 1.5 is created and we set `unit` to 1.0e-6 (1 um) and `precision` to 1.0e-9 (1 nm), the radius of the circle will be 1.5 um and the GDSII file will contain the dimension 1500 nm. Parameters ---------- outfile : file or string The file (or path) where the GDSII stream will be written. It must be opened for writing operations in binary format. cells : array-like The sequence of cells or cell names to be included in the library. If None, all cells are used. name : string Name of the GDSII library. unit : number Unit size for the objects in the library (in *meters*). precision : number Precision for the dimensions of the objects in the library (in *meters*). timestamp : datetime object Sets the GDSII timestamp. If None, the current time is used. binary_cells : iterable of bytes Iterable with binary data for GDSII cells (from `get_binary_cells`, for example). """ current_library.name = name current_library.unit = unit current_library.precision = precision current_library.write_gds(outfile, cells, timestamp, binary_cells) def gdsii_hash(filename, engine=None): """ Calculate the a hash value for a GDSII file. The hash is generated based only on the contents of the cells in the GDSII library, ignoring any timestamp records present in the file structure. Parameters ---------- filename : string Full path to the GDSII file. engine : hashlib-like engine The engine that executes the hashing algorithm. It must provide the methods `update` and `hexdigest` as defined in the hashlib module. If None, the dafault `hashlib.sha1()` is used. Returns ------- out : string The hash correponding to the library contents in hex format. """ with open(filename, "rb") as fin: data = fin.read() contents = [] start = pos = 0 while pos < len(data): size, rec = struct.unpack(">HH", data[pos : pos + 4]) if rec == 0x0502: start = pos + 28 elif rec == 0x0700: contents.append(data[start:pos]) pos += size h = hashlib.sha1() if engine is None else engine for x in sorted(contents): h.update(x) return h.hexdigest() current_library = GdsLibrary() """ Current `GdsLibrary` instance for automatic creation of GDSII files. This variable can be freely overwritten by the user with a new instance of `GdsLibrary`. Examples -------- >>> gdspy.Cell('MAIN') >>> gdspy.current_library = GdsLibrary() # Reset current library >>> gdspy.Cell('MAIN') # A new MAIN cell is created without error """ from gdspy.curve import Curve from gdspy import clipper try: from gdspy.viewer import LayoutViewer except ImportError as e: warnings.warn( "[GDSPY] LayoutViewer not available: " + repr(e), category=ImportWarning, stacklevel=2, ) gdspy-1.4.2/gdspy/clipper.cpp000066400000000000000000004705641354474061200161620ustar00rootroot00000000000000/******************************************************************************* * * * Author : Angus Johnson * * Version : 6.4.2 * * Date : 27 February 2017 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2017 * * * * License: * * Use, modification & distribution is subject to Boost Software License Ver 1. * * http://www.boost.org/LICENSE_1_0.txt * * * * Attributions: * * The code in this library is an extension of Bala Vatti's clipping algorithm: * * "A generic solution to polygon clipping" * * Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * * http://portal.acm.org/citation.cfm?id=129906 * * * * Computer graphics and geometric modeling: implementation and algorithms * * By Max K. Agoston * * Springer; 1 edition (January 4, 2005) * * http://books.google.com/books?q=vatti+clipping+agoston * * * * See also: * * "Polygon Offsetting by Computing Winding Numbers" * * Paper no. DETC2005-85513 pp. 565-575 * * ASME 2005 International Design Engineering Technical Conferences * * and Computers and Information in Engineering Conference (IDETC/CIE2005) * * September 24-28, 2005 , Long Beach, California, USA * * http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * * * *******************************************************************************/ /******************************************************************************* * * * This is a translation of the Delphi Clipper library and the naming style * * used has retained a Delphi flavour. * * * *******************************************************************************/ // Begin of GDSPY additions #define _USE_MATH_DEFINES #include #ifdef DEBUG #include #endif //DEBUG // End of GDSPY additions #include "clipper.hpp" #include #include #include #include #include #include #include #include namespace ClipperLib { static double const pi = 3.141592653589793238; static double const two_pi = pi *2; static double const def_arc_tolerance = 0.25; enum Direction { dRightToLeft, dLeftToRight }; static int const Unassigned = -1; //edge not currently 'owning' a solution static int const Skip = -2; //edge that would otherwise close a path #define HORIZONTAL (-1.0E+40) #define TOLERANCE (1.0e-20) #define NEAR_ZERO(val) (((val) > -TOLERANCE) && ((val) < TOLERANCE)) struct TEdge { IntPoint Bot; IntPoint Curr; //current (updated for every new scanbeam) IntPoint Top; double Dx; PolyType PolyTyp; EdgeSide Side; //side only refers to current side of solution poly int WindDelta; //1 or -1 depending on winding direction int WindCnt; int WindCnt2; //winding count of the opposite polytype int OutIdx; TEdge *Next; TEdge *Prev; TEdge *NextInLML; TEdge *NextInAEL; TEdge *PrevInAEL; TEdge *NextInSEL; TEdge *PrevInSEL; }; struct IntersectNode { TEdge *Edge1; TEdge *Edge2; IntPoint Pt; }; struct LocalMinimum { cInt Y; TEdge *LeftBound; TEdge *RightBound; }; struct OutPt; //OutRec: contains a path in the clipping solution. Edges in the AEL will //carry a pointer to an OutRec when they are part of the clipping solution. struct OutRec { int Idx; bool IsHole; bool IsOpen; OutRec *FirstLeft; //see comments in clipper.pas PolyNode *PolyNd; OutPt *Pts; OutPt *BottomPt; }; struct OutPt { int Idx; IntPoint Pt; OutPt *Next; OutPt *Prev; }; struct Join { OutPt *OutPt1; OutPt *OutPt2; IntPoint OffPt; }; struct LocMinSorter { inline bool operator()(const LocalMinimum& locMin1, const LocalMinimum& locMin2) { return locMin2.Y < locMin1.Y; } }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ inline cInt Round(double val) { if ((val < 0)) return static_cast(val - 0.5); else return static_cast(val + 0.5); } //------------------------------------------------------------------------------ inline cInt Abs(cInt val) { return val < 0 ? -val : val; } //------------------------------------------------------------------------------ // PolyTree methods ... //------------------------------------------------------------------------------ void PolyTree::Clear() { for (PolyNodes::size_type i = 0; i < AllNodes.size(); ++i) delete AllNodes[i]; AllNodes.resize(0); Childs.resize(0); } //------------------------------------------------------------------------------ PolyNode* PolyTree::GetFirst() const { if (!Childs.empty()) return Childs[0]; else return 0; } //------------------------------------------------------------------------------ int PolyTree::Total() const { int result = (int)AllNodes.size(); //with negative offsets, ignore the hidden outer polygon ... if (result > 0 && Childs[0] != AllNodes[0]) result--; return result; } //------------------------------------------------------------------------------ // PolyNode methods ... //------------------------------------------------------------------------------ PolyNode::PolyNode(): Parent(0), Index(0), m_IsOpen(false) { } //------------------------------------------------------------------------------ int PolyNode::ChildCount() const { return (int)Childs.size(); } //------------------------------------------------------------------------------ void PolyNode::AddChild(PolyNode& child) { unsigned cnt = (unsigned)Childs.size(); Childs.push_back(&child); child.Parent = this; child.Index = cnt; } //------------------------------------------------------------------------------ PolyNode* PolyNode::GetNext() const { if (!Childs.empty()) return Childs[0]; else return GetNextSiblingUp(); } //------------------------------------------------------------------------------ PolyNode* PolyNode::GetNextSiblingUp() const { if (!Parent) //protects against PolyTree.GetNextSiblingUp() return 0; else if (Index == Parent->Childs.size() - 1) return Parent->GetNextSiblingUp(); else return Parent->Childs[Index + 1]; } //------------------------------------------------------------------------------ bool PolyNode::IsHole() const { bool result = true; PolyNode* node = Parent; while (node) { result = !result; node = node->Parent; } return result; } //------------------------------------------------------------------------------ bool PolyNode::IsOpen() const { return m_IsOpen; } //------------------------------------------------------------------------------ #ifndef use_int32 //------------------------------------------------------------------------------ // Int128 class (enables safe math on signed 64bit integers) // eg Int128 val1((long64)9223372036854775807); //ie 2^63 -1 // Int128 val2((long64)9223372036854775807); // Int128 val3 = val1 * val2; // val3.AsString => "85070591730234615847396907784232501249" (8.5e+37) //------------------------------------------------------------------------------ class Int128 { public: ulong64 lo; long64 hi; Int128(long64 _lo = 0) { lo = (ulong64)_lo; if (_lo < 0) hi = -1; else hi = 0; } Int128(const Int128 &val): lo(val.lo), hi(val.hi){} Int128(const long64& _hi, const ulong64& _lo): lo(_lo), hi(_hi){} Int128& operator = (const long64 &val) { lo = (ulong64)val; if (val < 0) hi = -1; else hi = 0; return *this; } bool operator == (const Int128 &val) const {return (hi == val.hi && lo == val.lo);} bool operator != (const Int128 &val) const { return !(*this == val);} bool operator > (const Int128 &val) const { if (hi != val.hi) return hi > val.hi; else return lo > val.lo; } bool operator < (const Int128 &val) const { if (hi != val.hi) return hi < val.hi; else return lo < val.lo; } bool operator >= (const Int128 &val) const { return !(*this < val);} bool operator <= (const Int128 &val) const { return !(*this > val);} Int128& operator += (const Int128 &rhs) { hi += rhs.hi; lo += rhs.lo; if (lo < rhs.lo) hi++; return *this; } Int128 operator + (const Int128 &rhs) const { Int128 result(*this); result+= rhs; return result; } Int128& operator -= (const Int128 &rhs) { *this += -rhs; return *this; } Int128 operator - (const Int128 &rhs) const { Int128 result(*this); result -= rhs; return result; } Int128 operator-() const //unary negation { if (lo == 0) return Int128(-hi, 0); else return Int128(~hi, ~lo + 1); } operator double() const { const double shift64 = 18446744073709551616.0; //2^64 if (hi < 0) { if (lo == 0) return (double)hi * shift64; else return -(double)(~lo + ~hi * shift64); } else return (double)(lo + hi * shift64); } }; //------------------------------------------------------------------------------ Int128 Int128Mul (long64 lhs, long64 rhs) { bool negate = (lhs < 0) != (rhs < 0); if (lhs < 0) lhs = -lhs; ulong64 int1Hi = ulong64(lhs) >> 32; ulong64 int1Lo = ulong64(lhs & 0xFFFFFFFF); if (rhs < 0) rhs = -rhs; ulong64 int2Hi = ulong64(rhs) >> 32; ulong64 int2Lo = ulong64(rhs & 0xFFFFFFFF); //nb: see comments in clipper.pas ulong64 a = int1Hi * int2Hi; ulong64 b = int1Lo * int2Lo; ulong64 c = int1Hi * int2Lo + int1Lo * int2Hi; Int128 tmp; tmp.hi = long64(a + (c >> 32)); tmp.lo = long64(c << 32); tmp.lo += long64(b); if (tmp.lo < b) tmp.hi++; if (negate) tmp = -tmp; return tmp; }; #endif //------------------------------------------------------------------------------ // Miscellaneous global functions //------------------------------------------------------------------------------ bool Orientation(const Path &poly) { return Area(poly) >= 0; } //------------------------------------------------------------------------------ double Area(const Path &poly) { int size = (int)poly.size(); if (size < 3) return 0; double a = 0; for (int i = 0, j = size -1; i < size; ++i) { a += ((double)poly[j].X + poly[i].X) * ((double)poly[j].Y - poly[i].Y); j = i; } return -a * 0.5; } //------------------------------------------------------------------------------ double Area(const OutPt *op) { const OutPt *startOp = op; if (!op) return 0; double a = 0; do { a += (double)(op->Prev->Pt.X + op->Pt.X) * (double)(op->Prev->Pt.Y - op->Pt.Y); op = op->Next; } while (op != startOp); return a * 0.5; } //------------------------------------------------------------------------------ double Area(const OutRec &outRec) { return Area(outRec.Pts); } //------------------------------------------------------------------------------ bool PointIsVertex(const IntPoint &Pt, OutPt *pp) { OutPt *pp2 = pp; do { if (pp2->Pt == Pt) return true; pp2 = pp2->Next; } while (pp2 != pp); return false; } //------------------------------------------------------------------------------ //See "The Point in Polygon Problem for Arbitrary Polygons" by Hormann & Agathos //http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf int PointInPolygon(const IntPoint &pt, const Path &path) { //returns 0 if false, +1 if true, -1 if pt ON polygon boundary int result = 0; size_t cnt = path.size(); if (cnt < 3) return 0; IntPoint ip = path[0]; for(size_t i = 1; i <= cnt; ++i) { IntPoint ipNext = (i == cnt ? path[0] : path[i]); if (ipNext.Y == pt.Y) { if ((ipNext.X == pt.X) || (ip.Y == pt.Y && ((ipNext.X > pt.X) == (ip.X < pt.X)))) return -1; } if ((ip.Y < pt.Y) != (ipNext.Y < pt.Y)) { if (ip.X >= pt.X) { if (ipNext.X > pt.X) result = 1 - result; else { double d = (double)(ip.X - pt.X) * (ipNext.Y - pt.Y) - (double)(ipNext.X - pt.X) * (ip.Y - pt.Y); if (!d) return -1; if ((d > 0) == (ipNext.Y > ip.Y)) result = 1 - result; } } else { if (ipNext.X > pt.X) { double d = (double)(ip.X - pt.X) * (ipNext.Y - pt.Y) - (double)(ipNext.X - pt.X) * (ip.Y - pt.Y); if (!d) return -1; if ((d > 0) == (ipNext.Y > ip.Y)) result = 1 - result; } } } ip = ipNext; } return result; } //------------------------------------------------------------------------------ int PointInPolygon (const IntPoint &pt, OutPt *op) { //returns 0 if false, +1 if true, -1 if pt ON polygon boundary int result = 0; OutPt* startOp = op; for(;;) { if (op->Next->Pt.Y == pt.Y) { if ((op->Next->Pt.X == pt.X) || (op->Pt.Y == pt.Y && ((op->Next->Pt.X > pt.X) == (op->Pt.X < pt.X)))) return -1; } if ((op->Pt.Y < pt.Y) != (op->Next->Pt.Y < pt.Y)) { if (op->Pt.X >= pt.X) { if (op->Next->Pt.X > pt.X) result = 1 - result; else { double d = (double)(op->Pt.X - pt.X) * (op->Next->Pt.Y - pt.Y) - (double)(op->Next->Pt.X - pt.X) * (op->Pt.Y - pt.Y); if (!d) return -1; if ((d > 0) == (op->Next->Pt.Y > op->Pt.Y)) result = 1 - result; } } else { if (op->Next->Pt.X > pt.X) { double d = (double)(op->Pt.X - pt.X) * (op->Next->Pt.Y - pt.Y) - (double)(op->Next->Pt.X - pt.X) * (op->Pt.Y - pt.Y); if (!d) return -1; if ((d > 0) == (op->Next->Pt.Y > op->Pt.Y)) result = 1 - result; } } } op = op->Next; if (startOp == op) break; } return result; } //------------------------------------------------------------------------------ bool Poly2ContainsPoly1(OutPt *OutPt1, OutPt *OutPt2) { OutPt* op = OutPt1; do { //nb: PointInPolygon returns 0 if false, +1 if true, -1 if pt on polygon int res = PointInPolygon(op->Pt, OutPt2); if (res >= 0) return res > 0; op = op->Next; } while (op != OutPt1); return true; } //---------------------------------------------------------------------- bool SlopesEqual(const TEdge &e1, const TEdge &e2, bool UseFullInt64Range) { #ifndef use_int32 if (UseFullInt64Range) return Int128Mul(e1.Top.Y - e1.Bot.Y, e2.Top.X - e2.Bot.X) == Int128Mul(e1.Top.X - e1.Bot.X, e2.Top.Y - e2.Bot.Y); else #endif return (e1.Top.Y - e1.Bot.Y) * (e2.Top.X - e2.Bot.X) == (e1.Top.X - e1.Bot.X) * (e2.Top.Y - e2.Bot.Y); } //------------------------------------------------------------------------------ bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, const IntPoint pt3, bool UseFullInt64Range) { #ifndef use_int32 if (UseFullInt64Range) return Int128Mul(pt1.Y-pt2.Y, pt2.X-pt3.X) == Int128Mul(pt1.X-pt2.X, pt2.Y-pt3.Y); else #endif return (pt1.Y-pt2.Y)*(pt2.X-pt3.X) == (pt1.X-pt2.X)*(pt2.Y-pt3.Y); } //------------------------------------------------------------------------------ bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, const IntPoint pt3, const IntPoint pt4, bool UseFullInt64Range) { #ifndef use_int32 if (UseFullInt64Range) return Int128Mul(pt1.Y-pt2.Y, pt3.X-pt4.X) == Int128Mul(pt1.X-pt2.X, pt3.Y-pt4.Y); else #endif return (pt1.Y-pt2.Y)*(pt3.X-pt4.X) == (pt1.X-pt2.X)*(pt3.Y-pt4.Y); } //------------------------------------------------------------------------------ inline bool IsHorizontal(TEdge &e) { return e.Dx == HORIZONTAL; } //------------------------------------------------------------------------------ inline double GetDx(const IntPoint pt1, const IntPoint pt2) { return (pt1.Y == pt2.Y) ? HORIZONTAL : (double)(pt2.X - pt1.X) / (pt2.Y - pt1.Y); } //--------------------------------------------------------------------------- inline void SetDx(TEdge &e) { cInt dy = (e.Top.Y - e.Bot.Y); if (dy == 0) e.Dx = HORIZONTAL; else e.Dx = (double)(e.Top.X - e.Bot.X) / dy; } //--------------------------------------------------------------------------- inline void SwapSides(TEdge &Edge1, TEdge &Edge2) { EdgeSide Side = Edge1.Side; Edge1.Side = Edge2.Side; Edge2.Side = Side; } //------------------------------------------------------------------------------ inline void SwapPolyIndexes(TEdge &Edge1, TEdge &Edge2) { int OutIdx = Edge1.OutIdx; Edge1.OutIdx = Edge2.OutIdx; Edge2.OutIdx = OutIdx; } //------------------------------------------------------------------------------ inline cInt TopX(TEdge &edge, const cInt currentY) { return ( currentY == edge.Top.Y ) ? edge.Top.X : edge.Bot.X + Round(edge.Dx *(currentY - edge.Bot.Y)); } //------------------------------------------------------------------------------ void IntersectPoint(TEdge &Edge1, TEdge &Edge2, IntPoint &ip) { #ifdef use_xyz ip.Z = 0; #endif double b1, b2; if (Edge1.Dx == Edge2.Dx) { ip.Y = Edge1.Curr.Y; ip.X = TopX(Edge1, ip.Y); return; } else if (Edge1.Dx == 0) { ip.X = Edge1.Bot.X; if (IsHorizontal(Edge2)) ip.Y = Edge2.Bot.Y; else { b2 = Edge2.Bot.Y - (Edge2.Bot.X / Edge2.Dx); ip.Y = Round(ip.X / Edge2.Dx + b2); } } else if (Edge2.Dx == 0) { ip.X = Edge2.Bot.X; if (IsHorizontal(Edge1)) ip.Y = Edge1.Bot.Y; else { b1 = Edge1.Bot.Y - (Edge1.Bot.X / Edge1.Dx); ip.Y = Round(ip.X / Edge1.Dx + b1); } } else { b1 = Edge1.Bot.X - Edge1.Bot.Y * Edge1.Dx; b2 = Edge2.Bot.X - Edge2.Bot.Y * Edge2.Dx; double q = (b2-b1) / (Edge1.Dx - Edge2.Dx); ip.Y = Round(q); if (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) ip.X = Round(Edge1.Dx * q + b1); else ip.X = Round(Edge2.Dx * q + b2); } if (ip.Y < Edge1.Top.Y || ip.Y < Edge2.Top.Y) { if (Edge1.Top.Y > Edge2.Top.Y) ip.Y = Edge1.Top.Y; else ip.Y = Edge2.Top.Y; if (std::fabs(Edge1.Dx) < std::fabs(Edge2.Dx)) ip.X = TopX(Edge1, ip.Y); else ip.X = TopX(Edge2, ip.Y); } //finally, don't allow 'ip' to be BELOW curr.Y (ie bottom of scanbeam) ... if (ip.Y > Edge1.Curr.Y) { ip.Y = Edge1.Curr.Y; //use the more vertical edge to derive X ... if (std::fabs(Edge1.Dx) > std::fabs(Edge2.Dx)) ip.X = TopX(Edge2, ip.Y); else ip.X = TopX(Edge1, ip.Y); } } //------------------------------------------------------------------------------ void ReversePolyPtLinks(OutPt *pp) { if (!pp) return; OutPt *pp1, *pp2; pp1 = pp; do { pp2 = pp1->Next; pp1->Next = pp1->Prev; pp1->Prev = pp2; pp1 = pp2; } while( pp1 != pp ); } //------------------------------------------------------------------------------ void DisposeOutPts(OutPt*& pp) { if (pp == 0) return; pp->Prev->Next = 0; while( pp ) { OutPt *tmpPp = pp; pp = pp->Next; delete tmpPp; } } //------------------------------------------------------------------------------ inline void InitEdge(TEdge* e, TEdge* eNext, TEdge* ePrev, const IntPoint& Pt) { std::memset(e, 0, sizeof(TEdge)); e->Next = eNext; e->Prev = ePrev; e->Curr = Pt; e->OutIdx = Unassigned; } //------------------------------------------------------------------------------ void InitEdge2(TEdge& e, PolyType Pt) { if (e.Curr.Y >= e.Next->Curr.Y) { e.Bot = e.Curr; e.Top = e.Next->Curr; } else { e.Top = e.Curr; e.Bot = e.Next->Curr; } SetDx(e); e.PolyTyp = Pt; } //------------------------------------------------------------------------------ TEdge* RemoveEdge(TEdge* e) { //removes e from double_linked_list (but without removing from memory) e->Prev->Next = e->Next; e->Next->Prev = e->Prev; TEdge* result = e->Next; e->Prev = 0; //flag as removed (see ClipperBase.Clear) return result; } //------------------------------------------------------------------------------ inline void ReverseHorizontal(TEdge &e) { //swap horizontal edges' Top and Bottom x's so they follow the natural //progression of the bounds - ie so their xbots will align with the //adjoining lower edge. [Helpful in the ProcessHorizontal() method.] std::swap(e.Top.X, e.Bot.X); #ifdef use_xyz std::swap(e.Top.Z, e.Bot.Z); #endif } //------------------------------------------------------------------------------ void SwapPoints(IntPoint &pt1, IntPoint &pt2) { IntPoint tmp = pt1; pt1 = pt2; pt2 = tmp; } //------------------------------------------------------------------------------ bool GetOverlapSegment(IntPoint pt1a, IntPoint pt1b, IntPoint pt2a, IntPoint pt2b, IntPoint &pt1, IntPoint &pt2) { //precondition: segments are Collinear. if (Abs(pt1a.X - pt1b.X) > Abs(pt1a.Y - pt1b.Y)) { if (pt1a.X > pt1b.X) SwapPoints(pt1a, pt1b); if (pt2a.X > pt2b.X) SwapPoints(pt2a, pt2b); if (pt1a.X > pt2a.X) pt1 = pt1a; else pt1 = pt2a; if (pt1b.X < pt2b.X) pt2 = pt1b; else pt2 = pt2b; return pt1.X < pt2.X; } else { if (pt1a.Y < pt1b.Y) SwapPoints(pt1a, pt1b); if (pt2a.Y < pt2b.Y) SwapPoints(pt2a, pt2b); if (pt1a.Y < pt2a.Y) pt1 = pt1a; else pt1 = pt2a; if (pt1b.Y > pt2b.Y) pt2 = pt1b; else pt2 = pt2b; return pt1.Y > pt2.Y; } } //------------------------------------------------------------------------------ bool FirstIsBottomPt(const OutPt* btmPt1, const OutPt* btmPt2) { OutPt *p = btmPt1->Prev; while ((p->Pt == btmPt1->Pt) && (p != btmPt1)) p = p->Prev; double dx1p = std::fabs(GetDx(btmPt1->Pt, p->Pt)); p = btmPt1->Next; while ((p->Pt == btmPt1->Pt) && (p != btmPt1)) p = p->Next; double dx1n = std::fabs(GetDx(btmPt1->Pt, p->Pt)); p = btmPt2->Prev; while ((p->Pt == btmPt2->Pt) && (p != btmPt2)) p = p->Prev; double dx2p = std::fabs(GetDx(btmPt2->Pt, p->Pt)); p = btmPt2->Next; while ((p->Pt == btmPt2->Pt) && (p != btmPt2)) p = p->Next; double dx2n = std::fabs(GetDx(btmPt2->Pt, p->Pt)); if (std::max(dx1p, dx1n) == std::max(dx2p, dx2n) && std::min(dx1p, dx1n) == std::min(dx2p, dx2n)) return Area(btmPt1) > 0; //if otherwise identical use orientation else return (dx1p >= dx2p && dx1p >= dx2n) || (dx1n >= dx2p && dx1n >= dx2n); } //------------------------------------------------------------------------------ OutPt* GetBottomPt(OutPt *pp) { OutPt* dups = 0; OutPt* p = pp->Next; while (p != pp) { if (p->Pt.Y > pp->Pt.Y) { pp = p; dups = 0; } else if (p->Pt.Y == pp->Pt.Y && p->Pt.X <= pp->Pt.X) { if (p->Pt.X < pp->Pt.X) { dups = 0; pp = p; } else { if (p->Next != pp && p->Prev != pp) dups = p; } } p = p->Next; } if (dups) { //there appears to be at least 2 vertices at BottomPt so ... while (dups != p) { if (!FirstIsBottomPt(p, dups)) pp = dups; dups = dups->Next; while (dups->Pt != pp->Pt) dups = dups->Next; } } return pp; } //------------------------------------------------------------------------------ bool Pt2IsBetweenPt1AndPt3(const IntPoint pt1, const IntPoint pt2, const IntPoint pt3) { if ((pt1 == pt3) || (pt1 == pt2) || (pt3 == pt2)) return false; else if (pt1.X != pt3.X) return (pt2.X > pt1.X) == (pt2.X < pt3.X); else return (pt2.Y > pt1.Y) == (pt2.Y < pt3.Y); } //------------------------------------------------------------------------------ bool HorzSegmentsOverlap(cInt seg1a, cInt seg1b, cInt seg2a, cInt seg2b) { if (seg1a > seg1b) std::swap(seg1a, seg1b); if (seg2a > seg2b) std::swap(seg2a, seg2b); return (seg1a < seg2b) && (seg2a < seg1b); } //------------------------------------------------------------------------------ // ClipperBase class methods ... //------------------------------------------------------------------------------ ClipperBase::ClipperBase() //constructor { m_CurrentLM = m_MinimaList.begin(); //begin() == end() here m_UseFullRange = false; } //------------------------------------------------------------------------------ ClipperBase::~ClipperBase() //destructor { Clear(); } //------------------------------------------------------------------------------ void RangeTest(const IntPoint& Pt, bool& useFullRange) { if (useFullRange) { if (Pt.X > hiRange || Pt.Y > hiRange || -Pt.X > hiRange || -Pt.Y > hiRange) throw clipperException("Coordinate outside allowed range"); } else if (Pt.X > loRange|| Pt.Y > loRange || -Pt.X > loRange || -Pt.Y > loRange) { useFullRange = true; RangeTest(Pt, useFullRange); } } //------------------------------------------------------------------------------ TEdge* FindNextLocMin(TEdge* E) { for (;;) { while (E->Bot != E->Prev->Bot || E->Curr == E->Top) E = E->Next; if (!IsHorizontal(*E) && !IsHorizontal(*E->Prev)) break; while (IsHorizontal(*E->Prev)) E = E->Prev; TEdge* E2 = E; while (IsHorizontal(*E)) E = E->Next; if (E->Top.Y == E->Prev->Bot.Y) continue; //ie just an intermediate horz. if (E2->Prev->Bot.X < E->Bot.X) E = E2; break; } return E; } //------------------------------------------------------------------------------ TEdge* ClipperBase::ProcessBound(TEdge* E, bool NextIsForward) { TEdge *Result = E; TEdge *Horz = 0; if (E->OutIdx == Skip) { //if edges still remain in the current bound beyond the skip edge then //create another LocMin and call ProcessBound once more if (NextIsForward) { while (E->Top.Y == E->Next->Bot.Y) E = E->Next; //don't include top horizontals when parsing a bound a second time, //they will be contained in the opposite bound ... while (E != Result && IsHorizontal(*E)) E = E->Prev; } else { while (E->Top.Y == E->Prev->Bot.Y) E = E->Prev; while (E != Result && IsHorizontal(*E)) E = E->Next; } if (E == Result) { if (NextIsForward) Result = E->Next; else Result = E->Prev; } else { //there are more edges in the bound beyond result starting with E if (NextIsForward) E = Result->Next; else E = Result->Prev; MinimaList::value_type locMin; locMin.Y = E->Bot.Y; locMin.LeftBound = 0; locMin.RightBound = E; E->WindDelta = 0; Result = ProcessBound(E, NextIsForward); m_MinimaList.push_back(locMin); } return Result; } TEdge *EStart; if (IsHorizontal(*E)) { //We need to be careful with open paths because this may not be a //true local minima (ie E may be following a skip edge). //Also, consecutive horz. edges may start heading left before going right. if (NextIsForward) EStart = E->Prev; else EStart = E->Next; if (IsHorizontal(*EStart)) //ie an adjoining horizontal skip edge { if (EStart->Bot.X != E->Bot.X && EStart->Top.X != E->Bot.X) ReverseHorizontal(*E); } else if (EStart->Bot.X != E->Bot.X) ReverseHorizontal(*E); } EStart = E; if (NextIsForward) { while (Result->Top.Y == Result->Next->Bot.Y && Result->Next->OutIdx != Skip) Result = Result->Next; if (IsHorizontal(*Result) && Result->Next->OutIdx != Skip) { //nb: at the top of a bound, horizontals are added to the bound //only when the preceding edge attaches to the horizontal's left vertex //unless a Skip edge is encountered when that becomes the top divide Horz = Result; while (IsHorizontal(*Horz->Prev)) Horz = Horz->Prev; if (Horz->Prev->Top.X > Result->Next->Top.X) Result = Horz->Prev; } while (E != Result) { E->NextInLML = E->Next; if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Prev->Top.X) ReverseHorizontal(*E); E = E->Next; } if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Prev->Top.X) ReverseHorizontal(*E); Result = Result->Next; //move to the edge just beyond current bound } else { while (Result->Top.Y == Result->Prev->Bot.Y && Result->Prev->OutIdx != Skip) Result = Result->Prev; if (IsHorizontal(*Result) && Result->Prev->OutIdx != Skip) { Horz = Result; while (IsHorizontal(*Horz->Next)) Horz = Horz->Next; if (Horz->Next->Top.X == Result->Prev->Top.X || Horz->Next->Top.X > Result->Prev->Top.X) Result = Horz->Next; } while (E != Result) { E->NextInLML = E->Prev; if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Next->Top.X) ReverseHorizontal(*E); E = E->Prev; } if (IsHorizontal(*E) && E != EStart && E->Bot.X != E->Next->Top.X) ReverseHorizontal(*E); Result = Result->Prev; //move to the edge just beyond current bound } return Result; } //------------------------------------------------------------------------------ bool ClipperBase::AddPath(const Path &pg, PolyType PolyTyp, bool Closed) { #ifdef use_lines if (!Closed && PolyTyp == ptClip) throw clipperException("AddPath: Open paths must be subject."); #else if (!Closed) throw clipperException("AddPath: Open paths have been disabled."); #endif int highI = (int)pg.size() -1; if (Closed) while (highI > 0 && (pg[highI] == pg[0])) --highI; while (highI > 0 && (pg[highI] == pg[highI -1])) --highI; if ((Closed && highI < 2) || (!Closed && highI < 1)) return false; //create a new edge array ... TEdge *edges = new TEdge [highI +1]; bool IsFlat = true; //1. Basic (first) edge initialization ... try { edges[1].Curr = pg[1]; RangeTest(pg[0], m_UseFullRange); RangeTest(pg[highI], m_UseFullRange); InitEdge(&edges[0], &edges[1], &edges[highI], pg[0]); InitEdge(&edges[highI], &edges[0], &edges[highI-1], pg[highI]); for (int i = highI - 1; i >= 1; --i) { RangeTest(pg[i], m_UseFullRange); InitEdge(&edges[i], &edges[i+1], &edges[i-1], pg[i]); } } catch(...) { delete [] edges; throw; //range test fails } TEdge *eStart = &edges[0]; //2. Remove duplicate vertices, and (when closed) collinear edges ... TEdge *E = eStart, *eLoopStop = eStart; for (;;) { //nb: allows matching start and end points when not Closed ... if (E->Curr == E->Next->Curr && (Closed || E->Next != eStart)) { if (E == E->Next) break; if (E == eStart) eStart = E->Next; E = RemoveEdge(E); eLoopStop = E; continue; } if (E->Prev == E->Next) break; //only two vertices else if (Closed && SlopesEqual(E->Prev->Curr, E->Curr, E->Next->Curr, m_UseFullRange) && (!m_PreserveCollinear || !Pt2IsBetweenPt1AndPt3(E->Prev->Curr, E->Curr, E->Next->Curr))) { //Collinear edges are allowed for open paths but in closed paths //the default is to merge adjacent collinear edges into a single edge. //However, if the PreserveCollinear property is enabled, only overlapping //collinear edges (ie spikes) will be removed from closed paths. if (E == eStart) eStart = E->Next; E = RemoveEdge(E); E = E->Prev; eLoopStop = E; continue; } E = E->Next; if ((E == eLoopStop) || (!Closed && E->Next == eStart)) break; } if ((!Closed && (E == E->Next)) || (Closed && (E->Prev == E->Next))) { delete [] edges; return false; } if (!Closed) { m_HasOpenPaths = true; eStart->Prev->OutIdx = Skip; } //3. Do second stage of edge initialization ... E = eStart; do { InitEdge2(*E, PolyTyp); E = E->Next; if (IsFlat && E->Curr.Y != eStart->Curr.Y) IsFlat = false; } while (E != eStart); //4. Finally, add edge bounds to LocalMinima list ... //Totally flat paths must be handled differently when adding them //to LocalMinima list to avoid endless loops etc ... if (IsFlat) { if (Closed) { delete [] edges; return false; } E->Prev->OutIdx = Skip; MinimaList::value_type locMin; locMin.Y = E->Bot.Y; locMin.LeftBound = 0; locMin.RightBound = E; locMin.RightBound->Side = esRight; locMin.RightBound->WindDelta = 0; for (;;) { if (E->Bot.X != E->Prev->Top.X) ReverseHorizontal(*E); if (E->Next->OutIdx == Skip) break; E->NextInLML = E->Next; E = E->Next; } m_MinimaList.push_back(locMin); m_edges.push_back(edges); return true; } m_edges.push_back(edges); bool leftBoundIsForward; TEdge* EMin = 0; //workaround to avoid an endless loop in the while loop below when //open paths have matching start and end points ... if (E->Prev->Bot == E->Prev->Top) E = E->Next; for (;;) { E = FindNextLocMin(E); if (E == EMin) break; else if (!EMin) EMin = E; //E and E.Prev now share a local minima (left aligned if horizontal). //Compare their slopes to find which starts which bound ... MinimaList::value_type locMin; locMin.Y = E->Bot.Y; if (E->Dx < E->Prev->Dx) { locMin.LeftBound = E->Prev; locMin.RightBound = E; leftBoundIsForward = false; //Q.nextInLML = Q.prev } else { locMin.LeftBound = E; locMin.RightBound = E->Prev; leftBoundIsForward = true; //Q.nextInLML = Q.next } if (!Closed) locMin.LeftBound->WindDelta = 0; else if (locMin.LeftBound->Next == locMin.RightBound) locMin.LeftBound->WindDelta = -1; else locMin.LeftBound->WindDelta = 1; locMin.RightBound->WindDelta = -locMin.LeftBound->WindDelta; E = ProcessBound(locMin.LeftBound, leftBoundIsForward); if (E->OutIdx == Skip) E = ProcessBound(E, leftBoundIsForward); TEdge* E2 = ProcessBound(locMin.RightBound, !leftBoundIsForward); if (E2->OutIdx == Skip) E2 = ProcessBound(E2, !leftBoundIsForward); if (locMin.LeftBound->OutIdx == Skip) locMin.LeftBound = 0; else if (locMin.RightBound->OutIdx == Skip) locMin.RightBound = 0; m_MinimaList.push_back(locMin); if (!leftBoundIsForward) E = E2; } return true; } //------------------------------------------------------------------------------ bool ClipperBase::AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed) { bool result = false; for (Paths::size_type i = 0; i < ppg.size(); ++i) if (AddPath(ppg[i], PolyTyp, Closed)) result = true; return result; } //------------------------------------------------------------------------------ void ClipperBase::Clear() { DisposeLocalMinimaList(); for (EdgeList::size_type i = 0; i < m_edges.size(); ++i) { TEdge* edges = m_edges[i]; delete [] edges; } m_edges.clear(); m_UseFullRange = false; m_HasOpenPaths = false; } //------------------------------------------------------------------------------ void ClipperBase::Reset() { m_CurrentLM = m_MinimaList.begin(); if (m_CurrentLM == m_MinimaList.end()) return; //ie nothing to process std::sort(m_MinimaList.begin(), m_MinimaList.end(), LocMinSorter()); m_Scanbeam = ScanbeamList(); //clears/resets priority_queue //reset all edges ... for (MinimaList::iterator lm = m_MinimaList.begin(); lm != m_MinimaList.end(); ++lm) { InsertScanbeam(lm->Y); TEdge* e = lm->LeftBound; if (e) { e->Curr = e->Bot; e->Side = esLeft; e->OutIdx = Unassigned; } e = lm->RightBound; if (e) { e->Curr = e->Bot; e->Side = esRight; e->OutIdx = Unassigned; } } m_ActiveEdges = 0; m_CurrentLM = m_MinimaList.begin(); } //------------------------------------------------------------------------------ void ClipperBase::DisposeLocalMinimaList() { m_MinimaList.clear(); m_CurrentLM = m_MinimaList.begin(); } //------------------------------------------------------------------------------ bool ClipperBase::PopLocalMinima(cInt Y, const LocalMinimum *&locMin) { if (m_CurrentLM == m_MinimaList.end() || (*m_CurrentLM).Y != Y) return false; locMin = &(*m_CurrentLM); ++m_CurrentLM; return true; } //------------------------------------------------------------------------------ IntRect ClipperBase::GetBounds() { IntRect result; MinimaList::iterator lm = m_MinimaList.begin(); if (lm == m_MinimaList.end()) { result.left = result.top = result.right = result.bottom = 0; return result; } result.left = lm->LeftBound->Bot.X; result.top = lm->LeftBound->Bot.Y; result.right = lm->LeftBound->Bot.X; result.bottom = lm->LeftBound->Bot.Y; while (lm != m_MinimaList.end()) { //todo - needs fixing for open paths result.bottom = std::max(result.bottom, lm->LeftBound->Bot.Y); TEdge* e = lm->LeftBound; for (;;) { TEdge* bottomE = e; while (e->NextInLML) { if (e->Bot.X < result.left) result.left = e->Bot.X; if (e->Bot.X > result.right) result.right = e->Bot.X; e = e->NextInLML; } result.left = std::min(result.left, e->Bot.X); result.right = std::max(result.right, e->Bot.X); result.left = std::min(result.left, e->Top.X); result.right = std::max(result.right, e->Top.X); result.top = std::min(result.top, e->Top.Y); if (bottomE == lm->LeftBound) e = lm->RightBound; else break; } ++lm; } return result; } //------------------------------------------------------------------------------ void ClipperBase::InsertScanbeam(const cInt Y) { m_Scanbeam.push(Y); } //------------------------------------------------------------------------------ bool ClipperBase::PopScanbeam(cInt &Y) { if (m_Scanbeam.empty()) return false; Y = m_Scanbeam.top(); m_Scanbeam.pop(); while (!m_Scanbeam.empty() && Y == m_Scanbeam.top()) { m_Scanbeam.pop(); } // Pop duplicates. return true; } //------------------------------------------------------------------------------ void ClipperBase::DisposeAllOutRecs(){ for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) DisposeOutRec(i); m_PolyOuts.clear(); } //------------------------------------------------------------------------------ void ClipperBase::DisposeOutRec(PolyOutList::size_type index) { OutRec *outRec = m_PolyOuts[index]; if (outRec->Pts) DisposeOutPts(outRec->Pts); delete outRec; m_PolyOuts[index] = 0; } //------------------------------------------------------------------------------ void ClipperBase::DeleteFromAEL(TEdge *e) { TEdge* AelPrev = e->PrevInAEL; TEdge* AelNext = e->NextInAEL; if (!AelPrev && !AelNext && (e != m_ActiveEdges)) return; //already deleted if (AelPrev) AelPrev->NextInAEL = AelNext; else m_ActiveEdges = AelNext; if (AelNext) AelNext->PrevInAEL = AelPrev; e->NextInAEL = 0; e->PrevInAEL = 0; } //------------------------------------------------------------------------------ OutRec* ClipperBase::CreateOutRec() { OutRec* result = new OutRec; result->IsHole = false; result->IsOpen = false; result->FirstLeft = 0; result->Pts = 0; result->BottomPt = 0; result->PolyNd = 0; m_PolyOuts.push_back(result); result->Idx = (int)m_PolyOuts.size() - 1; return result; } //------------------------------------------------------------------------------ void ClipperBase::SwapPositionsInAEL(TEdge *Edge1, TEdge *Edge2) { //check that one or other edge hasn't already been removed from AEL ... if (Edge1->NextInAEL == Edge1->PrevInAEL || Edge2->NextInAEL == Edge2->PrevInAEL) return; if (Edge1->NextInAEL == Edge2) { TEdge* Next = Edge2->NextInAEL; if (Next) Next->PrevInAEL = Edge1; TEdge* Prev = Edge1->PrevInAEL; if (Prev) Prev->NextInAEL = Edge2; Edge2->PrevInAEL = Prev; Edge2->NextInAEL = Edge1; Edge1->PrevInAEL = Edge2; Edge1->NextInAEL = Next; } else if (Edge2->NextInAEL == Edge1) { TEdge* Next = Edge1->NextInAEL; if (Next) Next->PrevInAEL = Edge2; TEdge* Prev = Edge2->PrevInAEL; if (Prev) Prev->NextInAEL = Edge1; Edge1->PrevInAEL = Prev; Edge1->NextInAEL = Edge2; Edge2->PrevInAEL = Edge1; Edge2->NextInAEL = Next; } else { TEdge* Next = Edge1->NextInAEL; TEdge* Prev = Edge1->PrevInAEL; Edge1->NextInAEL = Edge2->NextInAEL; if (Edge1->NextInAEL) Edge1->NextInAEL->PrevInAEL = Edge1; Edge1->PrevInAEL = Edge2->PrevInAEL; if (Edge1->PrevInAEL) Edge1->PrevInAEL->NextInAEL = Edge1; Edge2->NextInAEL = Next; if (Edge2->NextInAEL) Edge2->NextInAEL->PrevInAEL = Edge2; Edge2->PrevInAEL = Prev; if (Edge2->PrevInAEL) Edge2->PrevInAEL->NextInAEL = Edge2; } if (!Edge1->PrevInAEL) m_ActiveEdges = Edge1; else if (!Edge2->PrevInAEL) m_ActiveEdges = Edge2; } //------------------------------------------------------------------------------ void ClipperBase::UpdateEdgeIntoAEL(TEdge *&e) { if (!e->NextInLML) throw clipperException("UpdateEdgeIntoAEL: invalid call"); e->NextInLML->OutIdx = e->OutIdx; TEdge* AelPrev = e->PrevInAEL; TEdge* AelNext = e->NextInAEL; if (AelPrev) AelPrev->NextInAEL = e->NextInLML; else m_ActiveEdges = e->NextInLML; if (AelNext) AelNext->PrevInAEL = e->NextInLML; e->NextInLML->Side = e->Side; e->NextInLML->WindDelta = e->WindDelta; e->NextInLML->WindCnt = e->WindCnt; e->NextInLML->WindCnt2 = e->WindCnt2; e = e->NextInLML; e->Curr = e->Bot; e->PrevInAEL = AelPrev; e->NextInAEL = AelNext; if (!IsHorizontal(*e)) InsertScanbeam(e->Top.Y); } //------------------------------------------------------------------------------ bool ClipperBase::LocalMinimaPending() { return (m_CurrentLM != m_MinimaList.end()); } //------------------------------------------------------------------------------ // TClipper methods ... //------------------------------------------------------------------------------ Clipper::Clipper(int initOptions) : ClipperBase() //constructor { m_ExecuteLocked = false; m_UseFullRange = false; m_ReverseOutput = ((initOptions & ioReverseSolution) != 0); m_StrictSimple = ((initOptions & ioStrictlySimple) != 0); m_PreserveCollinear = ((initOptions & ioPreserveCollinear) != 0); m_HasOpenPaths = false; #ifdef use_xyz m_ZFill = 0; #endif } //------------------------------------------------------------------------------ #ifdef use_xyz void Clipper::ZFillFunction(ZFillCallback zFillFunc) { m_ZFill = zFillFunc; } //------------------------------------------------------------------------------ #endif bool Clipper::Execute(ClipType clipType, Paths &solution, PolyFillType fillType) { return Execute(clipType, solution, fillType, fillType); } //------------------------------------------------------------------------------ bool Clipper::Execute(ClipType clipType, PolyTree &polytree, PolyFillType fillType) { return Execute(clipType, polytree, fillType, fillType); } //------------------------------------------------------------------------------ bool Clipper::Execute(ClipType clipType, Paths &solution, PolyFillType subjFillType, PolyFillType clipFillType) { if( m_ExecuteLocked ) return false; if (m_HasOpenPaths) throw clipperException("Error: PolyTree struct is needed for open path clipping."); m_ExecuteLocked = true; solution.resize(0); m_SubjFillType = subjFillType; m_ClipFillType = clipFillType; m_ClipType = clipType; m_UsingPolyTree = false; bool succeeded = ExecuteInternal(); if (succeeded) BuildResult(solution); DisposeAllOutRecs(); m_ExecuteLocked = false; return succeeded; } //------------------------------------------------------------------------------ bool Clipper::Execute(ClipType clipType, PolyTree& polytree, PolyFillType subjFillType, PolyFillType clipFillType) { if( m_ExecuteLocked ) return false; m_ExecuteLocked = true; m_SubjFillType = subjFillType; m_ClipFillType = clipFillType; m_ClipType = clipType; m_UsingPolyTree = true; bool succeeded = ExecuteInternal(); if (succeeded) BuildResult2(polytree); DisposeAllOutRecs(); m_ExecuteLocked = false; return succeeded; } //------------------------------------------------------------------------------ void Clipper::FixHoleLinkage(OutRec &outrec) { //skip OutRecs that (a) contain outermost polygons or //(b) already have the correct owner/child linkage ... if (!outrec.FirstLeft || (outrec.IsHole != outrec.FirstLeft->IsHole && outrec.FirstLeft->Pts)) return; OutRec* orfl = outrec.FirstLeft; while (orfl && ((orfl->IsHole == outrec.IsHole) || !orfl->Pts)) orfl = orfl->FirstLeft; outrec.FirstLeft = orfl; } //------------------------------------------------------------------------------ bool Clipper::ExecuteInternal() { bool succeeded = true; try { Reset(); m_Maxima = MaximaList(); m_SortedEdges = 0; succeeded = true; cInt botY, topY; if (!PopScanbeam(botY)) return false; InsertLocalMinimaIntoAEL(botY); while (PopScanbeam(topY) || LocalMinimaPending()) { ProcessHorizontals(); ClearGhostJoins(); if (!ProcessIntersections(topY)) { succeeded = false; break; } ProcessEdgesAtTopOfScanbeam(topY); botY = topY; InsertLocalMinimaIntoAEL(botY); } } catch(...) { succeeded = false; } if (succeeded) { //fix orientations ... for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { OutRec *outRec = m_PolyOuts[i]; if (!outRec->Pts || outRec->IsOpen) continue; if ((outRec->IsHole ^ m_ReverseOutput) == (Area(*outRec) > 0)) ReversePolyPtLinks(outRec->Pts); } if (!m_Joins.empty()) JoinCommonEdges(); //unfortunately FixupOutPolygon() must be done after JoinCommonEdges() for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { OutRec *outRec = m_PolyOuts[i]; if (!outRec->Pts) continue; if (outRec->IsOpen) FixupOutPolyline(*outRec); else FixupOutPolygon(*outRec); } if (m_StrictSimple) DoSimplePolygons(); } ClearJoins(); ClearGhostJoins(); return succeeded; } //------------------------------------------------------------------------------ void Clipper::SetWindingCount(TEdge &edge) { TEdge *e = edge.PrevInAEL; //find the edge of the same polytype that immediately preceeds 'edge' in AEL while (e && ((e->PolyTyp != edge.PolyTyp) || (e->WindDelta == 0))) e = e->PrevInAEL; if (!e) { if (edge.WindDelta == 0) { PolyFillType pft = (edge.PolyTyp == ptSubject ? m_SubjFillType : m_ClipFillType); edge.WindCnt = (pft == pftNegative ? -1 : 1); } else edge.WindCnt = edge.WindDelta; edge.WindCnt2 = 0; e = m_ActiveEdges; //ie get ready to calc WindCnt2 } else if (edge.WindDelta == 0 && m_ClipType != ctUnion) { edge.WindCnt = 1; edge.WindCnt2 = e->WindCnt2; e = e->NextInAEL; //ie get ready to calc WindCnt2 } else if (IsEvenOddFillType(edge)) { //EvenOdd filling ... if (edge.WindDelta == 0) { //are we inside a subj polygon ... bool Inside = true; TEdge *e2 = e->PrevInAEL; while (e2) { if (e2->PolyTyp == e->PolyTyp && e2->WindDelta != 0) Inside = !Inside; e2 = e2->PrevInAEL; } edge.WindCnt = (Inside ? 0 : 1); } else { edge.WindCnt = edge.WindDelta; } edge.WindCnt2 = e->WindCnt2; e = e->NextInAEL; //ie get ready to calc WindCnt2 } else { //nonZero, Positive or Negative filling ... if (e->WindCnt * e->WindDelta < 0) { //prev edge is 'decreasing' WindCount (WC) toward zero //so we're outside the previous polygon ... if (Abs(e->WindCnt) > 1) { //outside prev poly but still inside another. //when reversing direction of prev poly use the same WC if (e->WindDelta * edge.WindDelta < 0) edge.WindCnt = e->WindCnt; //otherwise continue to 'decrease' WC ... else edge.WindCnt = e->WindCnt + edge.WindDelta; } else //now outside all polys of same polytype so set own WC ... edge.WindCnt = (edge.WindDelta == 0 ? 1 : edge.WindDelta); } else { //prev edge is 'increasing' WindCount (WC) away from zero //so we're inside the previous polygon ... if (edge.WindDelta == 0) edge.WindCnt = (e->WindCnt < 0 ? e->WindCnt - 1 : e->WindCnt + 1); //if wind direction is reversing prev then use same WC else if (e->WindDelta * edge.WindDelta < 0) edge.WindCnt = e->WindCnt; //otherwise add to WC ... else edge.WindCnt = e->WindCnt + edge.WindDelta; } edge.WindCnt2 = e->WindCnt2; e = e->NextInAEL; //ie get ready to calc WindCnt2 } //update WindCnt2 ... if (IsEvenOddAltFillType(edge)) { //EvenOdd filling ... while (e != &edge) { if (e->WindDelta != 0) edge.WindCnt2 = (edge.WindCnt2 == 0 ? 1 : 0); e = e->NextInAEL; } } else { //nonZero, Positive or Negative filling ... while ( e != &edge ) { edge.WindCnt2 += e->WindDelta; e = e->NextInAEL; } } } //------------------------------------------------------------------------------ bool Clipper::IsEvenOddFillType(const TEdge& edge) const { if (edge.PolyTyp == ptSubject) return m_SubjFillType == pftEvenOdd; else return m_ClipFillType == pftEvenOdd; } //------------------------------------------------------------------------------ bool Clipper::IsEvenOddAltFillType(const TEdge& edge) const { if (edge.PolyTyp == ptSubject) return m_ClipFillType == pftEvenOdd; else return m_SubjFillType == pftEvenOdd; } //------------------------------------------------------------------------------ bool Clipper::IsContributing(const TEdge& edge) const { PolyFillType pft, pft2; if (edge.PolyTyp == ptSubject) { pft = m_SubjFillType; pft2 = m_ClipFillType; } else { pft = m_ClipFillType; pft2 = m_SubjFillType; } switch(pft) { case pftEvenOdd: //return false if a subj line has been flagged as inside a subj polygon if (edge.WindDelta == 0 && edge.WindCnt != 1) return false; break; case pftNonZero: if (Abs(edge.WindCnt) != 1) return false; break; case pftPositive: if (edge.WindCnt != 1) return false; break; default: //pftNegative if (edge.WindCnt != -1) return false; } switch(m_ClipType) { case ctIntersection: switch(pft2) { case pftEvenOdd: case pftNonZero: return (edge.WindCnt2 != 0); case pftPositive: return (edge.WindCnt2 > 0); default: return (edge.WindCnt2 < 0); } break; case ctUnion: switch(pft2) { case pftEvenOdd: case pftNonZero: return (edge.WindCnt2 == 0); case pftPositive: return (edge.WindCnt2 <= 0); default: return (edge.WindCnt2 >= 0); } break; case ctDifference: if (edge.PolyTyp == ptSubject) switch(pft2) { case pftEvenOdd: case pftNonZero: return (edge.WindCnt2 == 0); case pftPositive: return (edge.WindCnt2 <= 0); default: return (edge.WindCnt2 >= 0); } else switch(pft2) { case pftEvenOdd: case pftNonZero: return (edge.WindCnt2 != 0); case pftPositive: return (edge.WindCnt2 > 0); default: return (edge.WindCnt2 < 0); } break; case ctXor: if (edge.WindDelta == 0) //XOr always contributing unless open switch(pft2) { case pftEvenOdd: case pftNonZero: return (edge.WindCnt2 == 0); case pftPositive: return (edge.WindCnt2 <= 0); default: return (edge.WindCnt2 >= 0); } else return true; break; default: return true; } } //------------------------------------------------------------------------------ OutPt* Clipper::AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) { OutPt* result; TEdge *e, *prevE; if (IsHorizontal(*e2) || ( e1->Dx > e2->Dx )) { result = AddOutPt(e1, Pt); e2->OutIdx = e1->OutIdx; e1->Side = esLeft; e2->Side = esRight; e = e1; if (e->PrevInAEL == e2) prevE = e2->PrevInAEL; else prevE = e->PrevInAEL; } else { result = AddOutPt(e2, Pt); e1->OutIdx = e2->OutIdx; e1->Side = esRight; e2->Side = esLeft; e = e2; if (e->PrevInAEL == e1) prevE = e1->PrevInAEL; else prevE = e->PrevInAEL; } if (prevE && prevE->OutIdx >= 0 && prevE->Top.Y < Pt.Y && e->Top.Y < Pt.Y) { cInt xPrev = TopX(*prevE, Pt.Y); cInt xE = TopX(*e, Pt.Y); if (xPrev == xE && (e->WindDelta != 0) && (prevE->WindDelta != 0) && SlopesEqual(IntPoint(xPrev, Pt.Y), prevE->Top, IntPoint(xE, Pt.Y), e->Top, m_UseFullRange)) { OutPt* outPt = AddOutPt(prevE, Pt); AddJoin(result, outPt, e->Top); } } return result; } //------------------------------------------------------------------------------ void Clipper::AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) { AddOutPt( e1, Pt ); if (e2->WindDelta == 0) AddOutPt(e2, Pt); if( e1->OutIdx == e2->OutIdx ) { e1->OutIdx = Unassigned; e2->OutIdx = Unassigned; } else if (e1->OutIdx < e2->OutIdx) AppendPolygon(e1, e2); else AppendPolygon(e2, e1); } //------------------------------------------------------------------------------ void Clipper::AddEdgeToSEL(TEdge *edge) { //SEL pointers in PEdge are reused to build a list of horizontal edges. //However, we don't need to worry about order with horizontal edge processing. if( !m_SortedEdges ) { m_SortedEdges = edge; edge->PrevInSEL = 0; edge->NextInSEL = 0; } else { edge->NextInSEL = m_SortedEdges; edge->PrevInSEL = 0; m_SortedEdges->PrevInSEL = edge; m_SortedEdges = edge; } } //------------------------------------------------------------------------------ bool Clipper::PopEdgeFromSEL(TEdge *&edge) { if (!m_SortedEdges) return false; edge = m_SortedEdges; DeleteFromSEL(m_SortedEdges); return true; } //------------------------------------------------------------------------------ void Clipper::CopyAELToSEL() { TEdge* e = m_ActiveEdges; m_SortedEdges = e; while ( e ) { e->PrevInSEL = e->PrevInAEL; e->NextInSEL = e->NextInAEL; e = e->NextInAEL; } } //------------------------------------------------------------------------------ void Clipper::AddJoin(OutPt *op1, OutPt *op2, const IntPoint OffPt) { Join* j = new Join; j->OutPt1 = op1; j->OutPt2 = op2; j->OffPt = OffPt; m_Joins.push_back(j); } //------------------------------------------------------------------------------ void Clipper::ClearJoins() { for (JoinList::size_type i = 0; i < m_Joins.size(); i++) delete m_Joins[i]; m_Joins.resize(0); } //------------------------------------------------------------------------------ void Clipper::ClearGhostJoins() { for (JoinList::size_type i = 0; i < m_GhostJoins.size(); i++) delete m_GhostJoins[i]; m_GhostJoins.resize(0); } //------------------------------------------------------------------------------ void Clipper::AddGhostJoin(OutPt *op, const IntPoint OffPt) { Join* j = new Join; j->OutPt1 = op; j->OutPt2 = 0; j->OffPt = OffPt; m_GhostJoins.push_back(j); } //------------------------------------------------------------------------------ void Clipper::InsertLocalMinimaIntoAEL(const cInt botY) { const LocalMinimum *lm; while (PopLocalMinima(botY, lm)) { TEdge* lb = lm->LeftBound; TEdge* rb = lm->RightBound; OutPt *Op1 = 0; if (!lb) { //nb: don't insert LB into either AEL or SEL InsertEdgeIntoAEL(rb, 0); SetWindingCount(*rb); if (IsContributing(*rb)) Op1 = AddOutPt(rb, rb->Bot); } else if (!rb) { InsertEdgeIntoAEL(lb, 0); SetWindingCount(*lb); if (IsContributing(*lb)) Op1 = AddOutPt(lb, lb->Bot); InsertScanbeam(lb->Top.Y); } else { InsertEdgeIntoAEL(lb, 0); InsertEdgeIntoAEL(rb, lb); SetWindingCount( *lb ); rb->WindCnt = lb->WindCnt; rb->WindCnt2 = lb->WindCnt2; if (IsContributing(*lb)) Op1 = AddLocalMinPoly(lb, rb, lb->Bot); InsertScanbeam(lb->Top.Y); } if (rb) { if (IsHorizontal(*rb)) { AddEdgeToSEL(rb); if (rb->NextInLML) InsertScanbeam(rb->NextInLML->Top.Y); } else InsertScanbeam( rb->Top.Y ); } if (!lb || !rb) continue; //if any output polygons share an edge, they'll need joining later ... if (Op1 && IsHorizontal(*rb) && m_GhostJoins.size() > 0 && (rb->WindDelta != 0)) { for (JoinList::size_type i = 0; i < m_GhostJoins.size(); ++i) { Join* jr = m_GhostJoins[i]; //if the horizontal Rb and a 'ghost' horizontal overlap, then convert //the 'ghost' join to a real join ready for later ... if (HorzSegmentsOverlap(jr->OutPt1->Pt.X, jr->OffPt.X, rb->Bot.X, rb->Top.X)) AddJoin(jr->OutPt1, Op1, jr->OffPt); } } if (lb->OutIdx >= 0 && lb->PrevInAEL && lb->PrevInAEL->Curr.X == lb->Bot.X && lb->PrevInAEL->OutIdx >= 0 && SlopesEqual(lb->PrevInAEL->Bot, lb->PrevInAEL->Top, lb->Curr, lb->Top, m_UseFullRange) && (lb->WindDelta != 0) && (lb->PrevInAEL->WindDelta != 0)) { OutPt *Op2 = AddOutPt(lb->PrevInAEL, lb->Bot); AddJoin(Op1, Op2, lb->Top); } if(lb->NextInAEL != rb) { if (rb->OutIdx >= 0 && rb->PrevInAEL->OutIdx >= 0 && SlopesEqual(rb->PrevInAEL->Curr, rb->PrevInAEL->Top, rb->Curr, rb->Top, m_UseFullRange) && (rb->WindDelta != 0) && (rb->PrevInAEL->WindDelta != 0)) { OutPt *Op2 = AddOutPt(rb->PrevInAEL, rb->Bot); AddJoin(Op1, Op2, rb->Top); } TEdge* e = lb->NextInAEL; if (e) { while( e != rb ) { //nb: For calculating winding counts etc, IntersectEdges() assumes //that param1 will be to the Right of param2 ABOVE the intersection ... IntersectEdges(rb , e , lb->Curr); //order important here e = e->NextInAEL; } } } } } //------------------------------------------------------------------------------ void Clipper::DeleteFromSEL(TEdge *e) { TEdge* SelPrev = e->PrevInSEL; TEdge* SelNext = e->NextInSEL; if( !SelPrev && !SelNext && (e != m_SortedEdges) ) return; //already deleted if( SelPrev ) SelPrev->NextInSEL = SelNext; else m_SortedEdges = SelNext; if( SelNext ) SelNext->PrevInSEL = SelPrev; e->NextInSEL = 0; e->PrevInSEL = 0; } //------------------------------------------------------------------------------ #ifdef use_xyz void Clipper::SetZ(IntPoint& pt, TEdge& e1, TEdge& e2) { if (pt.Z != 0 || !m_ZFill) return; else if (pt == e1.Bot) pt.Z = e1.Bot.Z; else if (pt == e1.Top) pt.Z = e1.Top.Z; else if (pt == e2.Bot) pt.Z = e2.Bot.Z; else if (pt == e2.Top) pt.Z = e2.Top.Z; else (*m_ZFill)(e1.Bot, e1.Top, e2.Bot, e2.Top, pt); } //------------------------------------------------------------------------------ #endif void Clipper::IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &Pt) { bool e1Contributing = ( e1->OutIdx >= 0 ); bool e2Contributing = ( e2->OutIdx >= 0 ); #ifdef use_xyz SetZ(Pt, *e1, *e2); #endif #ifdef use_lines //if either edge is on an OPEN path ... if (e1->WindDelta == 0 || e2->WindDelta == 0) { //ignore subject-subject open path intersections UNLESS they //are both open paths, AND they are both 'contributing maximas' ... if (e1->WindDelta == 0 && e2->WindDelta == 0) return; //if intersecting a subj line with a subj poly ... else if (e1->PolyTyp == e2->PolyTyp && e1->WindDelta != e2->WindDelta && m_ClipType == ctUnion) { if (e1->WindDelta == 0) { if (e2Contributing) { AddOutPt(e1, Pt); if (e1Contributing) e1->OutIdx = Unassigned; } } else { if (e1Contributing) { AddOutPt(e2, Pt); if (e2Contributing) e2->OutIdx = Unassigned; } } } else if (e1->PolyTyp != e2->PolyTyp) { //toggle subj open path OutIdx on/off when Abs(clip.WndCnt) == 1 ... if ((e1->WindDelta == 0) && abs(e2->WindCnt) == 1 && (m_ClipType != ctUnion || e2->WindCnt2 == 0)) { AddOutPt(e1, Pt); if (e1Contributing) e1->OutIdx = Unassigned; } else if ((e2->WindDelta == 0) && (abs(e1->WindCnt) == 1) && (m_ClipType != ctUnion || e1->WindCnt2 == 0)) { AddOutPt(e2, Pt); if (e2Contributing) e2->OutIdx = Unassigned; } } return; } #endif //update winding counts... //assumes that e1 will be to the Right of e2 ABOVE the intersection if ( e1->PolyTyp == e2->PolyTyp ) { if ( IsEvenOddFillType( *e1) ) { int oldE1WindCnt = e1->WindCnt; e1->WindCnt = e2->WindCnt; e2->WindCnt = oldE1WindCnt; } else { if (e1->WindCnt + e2->WindDelta == 0 ) e1->WindCnt = -e1->WindCnt; else e1->WindCnt += e2->WindDelta; if ( e2->WindCnt - e1->WindDelta == 0 ) e2->WindCnt = -e2->WindCnt; else e2->WindCnt -= e1->WindDelta; } } else { if (!IsEvenOddFillType(*e2)) e1->WindCnt2 += e2->WindDelta; else e1->WindCnt2 = ( e1->WindCnt2 == 0 ) ? 1 : 0; if (!IsEvenOddFillType(*e1)) e2->WindCnt2 -= e1->WindDelta; else e2->WindCnt2 = ( e2->WindCnt2 == 0 ) ? 1 : 0; } PolyFillType e1FillType, e2FillType, e1FillType2, e2FillType2; if (e1->PolyTyp == ptSubject) { e1FillType = m_SubjFillType; e1FillType2 = m_ClipFillType; } else { e1FillType = m_ClipFillType; e1FillType2 = m_SubjFillType; } if (e2->PolyTyp == ptSubject) { e2FillType = m_SubjFillType; e2FillType2 = m_ClipFillType; } else { e2FillType = m_ClipFillType; e2FillType2 = m_SubjFillType; } cInt e1Wc, e2Wc; switch (e1FillType) { case pftPositive: e1Wc = e1->WindCnt; break; case pftNegative: e1Wc = -e1->WindCnt; break; default: e1Wc = Abs(e1->WindCnt); } switch(e2FillType) { case pftPositive: e2Wc = e2->WindCnt; break; case pftNegative: e2Wc = -e2->WindCnt; break; default: e2Wc = Abs(e2->WindCnt); } if ( e1Contributing && e2Contributing ) { if ((e1Wc != 0 && e1Wc != 1) || (e2Wc != 0 && e2Wc != 1) || (e1->PolyTyp != e2->PolyTyp && m_ClipType != ctXor) ) { AddLocalMaxPoly(e1, e2, Pt); } else { AddOutPt(e1, Pt); AddOutPt(e2, Pt); SwapSides( *e1 , *e2 ); SwapPolyIndexes( *e1 , *e2 ); } } else if ( e1Contributing ) { if (e2Wc == 0 || e2Wc == 1) { AddOutPt(e1, Pt); SwapSides(*e1, *e2); SwapPolyIndexes(*e1, *e2); } } else if ( e2Contributing ) { if (e1Wc == 0 || e1Wc == 1) { AddOutPt(e2, Pt); SwapSides(*e1, *e2); SwapPolyIndexes(*e1, *e2); } } else if ( (e1Wc == 0 || e1Wc == 1) && (e2Wc == 0 || e2Wc == 1)) { //neither edge is currently contributing ... cInt e1Wc2, e2Wc2; switch (e1FillType2) { case pftPositive: e1Wc2 = e1->WindCnt2; break; case pftNegative : e1Wc2 = -e1->WindCnt2; break; default: e1Wc2 = Abs(e1->WindCnt2); } switch (e2FillType2) { case pftPositive: e2Wc2 = e2->WindCnt2; break; case pftNegative: e2Wc2 = -e2->WindCnt2; break; default: e2Wc2 = Abs(e2->WindCnt2); } if (e1->PolyTyp != e2->PolyTyp) { AddLocalMinPoly(e1, e2, Pt); } else if (e1Wc == 1 && e2Wc == 1) switch( m_ClipType ) { case ctIntersection: if (e1Wc2 > 0 && e2Wc2 > 0) AddLocalMinPoly(e1, e2, Pt); break; case ctUnion: if ( e1Wc2 <= 0 && e2Wc2 <= 0 ) AddLocalMinPoly(e1, e2, Pt); break; case ctDifference: if (((e1->PolyTyp == ptClip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || ((e1->PolyTyp == ptSubject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) AddLocalMinPoly(e1, e2, Pt); break; case ctXor: AddLocalMinPoly(e1, e2, Pt); } else SwapSides( *e1, *e2 ); } } //------------------------------------------------------------------------------ void Clipper::SetHoleState(TEdge *e, OutRec *outrec) { TEdge *e2 = e->PrevInAEL; TEdge *eTmp = 0; while (e2) { if (e2->OutIdx >= 0 && e2->WindDelta != 0) { if (!eTmp) eTmp = e2; else if (eTmp->OutIdx == e2->OutIdx) eTmp = 0; } e2 = e2->PrevInAEL; } if (!eTmp) { outrec->FirstLeft = 0; outrec->IsHole = false; } else { outrec->FirstLeft = m_PolyOuts[eTmp->OutIdx]; outrec->IsHole = !outrec->FirstLeft->IsHole; } } //------------------------------------------------------------------------------ OutRec* GetLowermostRec(OutRec *outRec1, OutRec *outRec2) { //work out which polygon fragment has the correct hole state ... if (!outRec1->BottomPt) outRec1->BottomPt = GetBottomPt(outRec1->Pts); if (!outRec2->BottomPt) outRec2->BottomPt = GetBottomPt(outRec2->Pts); OutPt *OutPt1 = outRec1->BottomPt; OutPt *OutPt2 = outRec2->BottomPt; if (OutPt1->Pt.Y > OutPt2->Pt.Y) return outRec1; else if (OutPt1->Pt.Y < OutPt2->Pt.Y) return outRec2; else if (OutPt1->Pt.X < OutPt2->Pt.X) return outRec1; else if (OutPt1->Pt.X > OutPt2->Pt.X) return outRec2; else if (OutPt1->Next == OutPt1) return outRec2; else if (OutPt2->Next == OutPt2) return outRec1; else if (FirstIsBottomPt(OutPt1, OutPt2)) return outRec1; else return outRec2; } //------------------------------------------------------------------------------ bool OutRec1RightOfOutRec2(OutRec* outRec1, OutRec* outRec2) { do { outRec1 = outRec1->FirstLeft; if (outRec1 == outRec2) return true; } while (outRec1); return false; } //------------------------------------------------------------------------------ OutRec* Clipper::GetOutRec(int Idx) { OutRec* outrec = m_PolyOuts[Idx]; while (outrec != m_PolyOuts[outrec->Idx]) outrec = m_PolyOuts[outrec->Idx]; return outrec; } //------------------------------------------------------------------------------ void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) { //get the start and ends of both output polygons ... OutRec *outRec1 = m_PolyOuts[e1->OutIdx]; OutRec *outRec2 = m_PolyOuts[e2->OutIdx]; OutRec *holeStateRec; if (OutRec1RightOfOutRec2(outRec1, outRec2)) holeStateRec = outRec2; else if (OutRec1RightOfOutRec2(outRec2, outRec1)) holeStateRec = outRec1; else holeStateRec = GetLowermostRec(outRec1, outRec2); //get the start and ends of both output polygons and //join e2 poly onto e1 poly and delete pointers to e2 ... OutPt* p1_lft = outRec1->Pts; OutPt* p1_rt = p1_lft->Prev; OutPt* p2_lft = outRec2->Pts; OutPt* p2_rt = p2_lft->Prev; //join e2 poly onto e1 poly and delete pointers to e2 ... if( e1->Side == esLeft ) { if( e2->Side == esLeft ) { //z y x a b c ReversePolyPtLinks(p2_lft); p2_lft->Next = p1_lft; p1_lft->Prev = p2_lft; p1_rt->Next = p2_rt; p2_rt->Prev = p1_rt; outRec1->Pts = p2_rt; } else { //x y z a b c p2_rt->Next = p1_lft; p1_lft->Prev = p2_rt; p2_lft->Prev = p1_rt; p1_rt->Next = p2_lft; outRec1->Pts = p2_lft; } } else { if( e2->Side == esRight ) { //a b c z y x ReversePolyPtLinks(p2_lft); p1_rt->Next = p2_rt; p2_rt->Prev = p1_rt; p2_lft->Next = p1_lft; p1_lft->Prev = p2_lft; } else { //a b c x y z p1_rt->Next = p2_lft; p2_lft->Prev = p1_rt; p1_lft->Prev = p2_rt; p2_rt->Next = p1_lft; } } outRec1->BottomPt = 0; if (holeStateRec == outRec2) { if (outRec2->FirstLeft != outRec1) outRec1->FirstLeft = outRec2->FirstLeft; outRec1->IsHole = outRec2->IsHole; } outRec2->Pts = 0; outRec2->BottomPt = 0; outRec2->FirstLeft = outRec1; int OKIdx = e1->OutIdx; int ObsoleteIdx = e2->OutIdx; e1->OutIdx = Unassigned; //nb: safe because we only get here via AddLocalMaxPoly e2->OutIdx = Unassigned; TEdge* e = m_ActiveEdges; while( e ) { if( e->OutIdx == ObsoleteIdx ) { e->OutIdx = OKIdx; e->Side = e1->Side; break; } e = e->NextInAEL; } outRec2->Idx = outRec1->Idx; } //------------------------------------------------------------------------------ OutPt* Clipper::AddOutPt(TEdge *e, const IntPoint &pt) { if( e->OutIdx < 0 ) { OutRec *outRec = CreateOutRec(); outRec->IsOpen = (e->WindDelta == 0); OutPt* newOp = new OutPt; outRec->Pts = newOp; newOp->Idx = outRec->Idx; newOp->Pt = pt; newOp->Next = newOp; newOp->Prev = newOp; if (!outRec->IsOpen) SetHoleState(e, outRec); e->OutIdx = outRec->Idx; return newOp; } else { OutRec *outRec = m_PolyOuts[e->OutIdx]; //OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most' OutPt* op = outRec->Pts; bool ToFront = (e->Side == esLeft); if (ToFront && (pt == op->Pt)) return op; else if (!ToFront && (pt == op->Prev->Pt)) return op->Prev; OutPt* newOp = new OutPt; newOp->Idx = outRec->Idx; newOp->Pt = pt; newOp->Next = op; newOp->Prev = op->Prev; newOp->Prev->Next = newOp; op->Prev = newOp; if (ToFront) outRec->Pts = newOp; return newOp; } } //------------------------------------------------------------------------------ OutPt* Clipper::GetLastOutPt(TEdge *e) { OutRec *outRec = m_PolyOuts[e->OutIdx]; if (e->Side == esLeft) return outRec->Pts; else return outRec->Pts->Prev; } //------------------------------------------------------------------------------ void Clipper::ProcessHorizontals() { TEdge* horzEdge; while (PopEdgeFromSEL(horzEdge)) ProcessHorizontal(horzEdge); } //------------------------------------------------------------------------------ inline bool IsMinima(TEdge *e) { return e && (e->Prev->NextInLML != e) && (e->Next->NextInLML != e); } //------------------------------------------------------------------------------ inline bool IsMaxima(TEdge *e, const cInt Y) { return e && e->Top.Y == Y && !e->NextInLML; } //------------------------------------------------------------------------------ inline bool IsIntermediate(TEdge *e, const cInt Y) { return e->Top.Y == Y && e->NextInLML; } //------------------------------------------------------------------------------ TEdge *GetMaximaPair(TEdge *e) { if ((e->Next->Top == e->Top) && !e->Next->NextInLML) return e->Next; else if ((e->Prev->Top == e->Top) && !e->Prev->NextInLML) return e->Prev; else return 0; } //------------------------------------------------------------------------------ TEdge *GetMaximaPairEx(TEdge *e) { //as GetMaximaPair() but returns 0 if MaxPair isn't in AEL (unless it's horizontal) TEdge* result = GetMaximaPair(e); if (result && (result->OutIdx == Skip || (result->NextInAEL == result->PrevInAEL && !IsHorizontal(*result)))) return 0; return result; } //------------------------------------------------------------------------------ void Clipper::SwapPositionsInSEL(TEdge *Edge1, TEdge *Edge2) { if( !( Edge1->NextInSEL ) && !( Edge1->PrevInSEL ) ) return; if( !( Edge2->NextInSEL ) && !( Edge2->PrevInSEL ) ) return; if( Edge1->NextInSEL == Edge2 ) { TEdge* Next = Edge2->NextInSEL; if( Next ) Next->PrevInSEL = Edge1; TEdge* Prev = Edge1->PrevInSEL; if( Prev ) Prev->NextInSEL = Edge2; Edge2->PrevInSEL = Prev; Edge2->NextInSEL = Edge1; Edge1->PrevInSEL = Edge2; Edge1->NextInSEL = Next; } else if( Edge2->NextInSEL == Edge1 ) { TEdge* Next = Edge1->NextInSEL; if( Next ) Next->PrevInSEL = Edge2; TEdge* Prev = Edge2->PrevInSEL; if( Prev ) Prev->NextInSEL = Edge1; Edge1->PrevInSEL = Prev; Edge1->NextInSEL = Edge2; Edge2->PrevInSEL = Edge1; Edge2->NextInSEL = Next; } else { TEdge* Next = Edge1->NextInSEL; TEdge* Prev = Edge1->PrevInSEL; Edge1->NextInSEL = Edge2->NextInSEL; if( Edge1->NextInSEL ) Edge1->NextInSEL->PrevInSEL = Edge1; Edge1->PrevInSEL = Edge2->PrevInSEL; if( Edge1->PrevInSEL ) Edge1->PrevInSEL->NextInSEL = Edge1; Edge2->NextInSEL = Next; if( Edge2->NextInSEL ) Edge2->NextInSEL->PrevInSEL = Edge2; Edge2->PrevInSEL = Prev; if( Edge2->PrevInSEL ) Edge2->PrevInSEL->NextInSEL = Edge2; } if( !Edge1->PrevInSEL ) m_SortedEdges = Edge1; else if( !Edge2->PrevInSEL ) m_SortedEdges = Edge2; } //------------------------------------------------------------------------------ TEdge* GetNextInAEL(TEdge *e, Direction dir) { return dir == dLeftToRight ? e->NextInAEL : e->PrevInAEL; } //------------------------------------------------------------------------------ void GetHorzDirection(TEdge& HorzEdge, Direction& Dir, cInt& Left, cInt& Right) { if (HorzEdge.Bot.X < HorzEdge.Top.X) { Left = HorzEdge.Bot.X; Right = HorzEdge.Top.X; Dir = dLeftToRight; } else { Left = HorzEdge.Top.X; Right = HorzEdge.Bot.X; Dir = dRightToLeft; } } //------------------------------------------------------------------------ /******************************************************************************* * Notes: Horizontal edges (HEs) at scanline intersections (ie at the Top or * * Bottom of a scanbeam) are processed as if layered. The order in which HEs * * are processed doesn't matter. HEs intersect with other HE Bot.Xs only [#] * * (or they could intersect with Top.Xs only, ie EITHER Bot.Xs OR Top.Xs), * * and with other non-horizontal edges [*]. Once these intersections are * * processed, intermediate HEs then 'promote' the Edge above (NextInLML) into * * the AEL. These 'promoted' edges may in turn intersect [%] with other HEs. * *******************************************************************************/ void Clipper::ProcessHorizontal(TEdge *horzEdge) { Direction dir; cInt horzLeft, horzRight; bool IsOpen = (horzEdge->WindDelta == 0); GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); TEdge* eLastHorz = horzEdge, *eMaxPair = 0; while (eLastHorz->NextInLML && IsHorizontal(*eLastHorz->NextInLML)) eLastHorz = eLastHorz->NextInLML; if (!eLastHorz->NextInLML) eMaxPair = GetMaximaPair(eLastHorz); MaximaList::const_iterator maxIt; MaximaList::const_reverse_iterator maxRit; if (m_Maxima.size() > 0) { //get the first maxima in range (X) ... if (dir == dLeftToRight) { maxIt = m_Maxima.begin(); while (maxIt != m_Maxima.end() && *maxIt <= horzEdge->Bot.X) maxIt++; if (maxIt != m_Maxima.end() && *maxIt >= eLastHorz->Top.X) maxIt = m_Maxima.end(); } else { maxRit = m_Maxima.rbegin(); while (maxRit != m_Maxima.rend() && *maxRit > horzEdge->Bot.X) maxRit++; if (maxRit != m_Maxima.rend() && *maxRit <= eLastHorz->Top.X) maxRit = m_Maxima.rend(); } } OutPt* op1 = 0; for (;;) //loop through consec. horizontal edges { bool IsLastHorz = (horzEdge == eLastHorz); TEdge* e = GetNextInAEL(horzEdge, dir); while(e) { //this code block inserts extra coords into horizontal edges (in output //polygons) whereever maxima touch these horizontal edges. This helps //'simplifying' polygons (ie if the Simplify property is set). if (m_Maxima.size() > 0) { if (dir == dLeftToRight) { while (maxIt != m_Maxima.end() && *maxIt < e->Curr.X) { if (horzEdge->OutIdx >= 0 && !IsOpen) AddOutPt(horzEdge, IntPoint(*maxIt, horzEdge->Bot.Y)); maxIt++; } } else { while (maxRit != m_Maxima.rend() && *maxRit > e->Curr.X) { if (horzEdge->OutIdx >= 0 && !IsOpen) AddOutPt(horzEdge, IntPoint(*maxRit, horzEdge->Bot.Y)); maxRit++; } } }; if ((dir == dLeftToRight && e->Curr.X > horzRight) || (dir == dRightToLeft && e->Curr.X < horzLeft)) break; //Also break if we've got to the end of an intermediate horizontal edge ... //nb: Smaller Dx's are to the right of larger Dx's ABOVE the horizontal. if (e->Curr.X == horzEdge->Top.X && horzEdge->NextInLML && e->Dx < horzEdge->NextInLML->Dx) break; if (horzEdge->OutIdx >= 0 && !IsOpen) //note: may be done multiple times { #ifdef use_xyz if (dir == dLeftToRight) SetZ(e->Curr, *horzEdge, *e); else SetZ(e->Curr, *e, *horzEdge); #endif op1 = AddOutPt(horzEdge, e->Curr); TEdge* eNextHorz = m_SortedEdges; while (eNextHorz) { if (eNextHorz->OutIdx >= 0 && HorzSegmentsOverlap(horzEdge->Bot.X, horzEdge->Top.X, eNextHorz->Bot.X, eNextHorz->Top.X)) { OutPt* op2 = GetLastOutPt(eNextHorz); AddJoin(op2, op1, eNextHorz->Top); } eNextHorz = eNextHorz->NextInSEL; } AddGhostJoin(op1, horzEdge->Bot); } //OK, so far we're still in range of the horizontal Edge but make sure //we're at the last of consec. horizontals when matching with eMaxPair if(e == eMaxPair && IsLastHorz) { if (horzEdge->OutIdx >= 0) AddLocalMaxPoly(horzEdge, eMaxPair, horzEdge->Top); DeleteFromAEL(horzEdge); DeleteFromAEL(eMaxPair); return; } if(dir == dLeftToRight) { IntPoint Pt = IntPoint(e->Curr.X, horzEdge->Curr.Y); IntersectEdges(horzEdge, e, Pt); } else { IntPoint Pt = IntPoint(e->Curr.X, horzEdge->Curr.Y); IntersectEdges( e, horzEdge, Pt); } TEdge* eNext = GetNextInAEL(e, dir); SwapPositionsInAEL( horzEdge, e ); e = eNext; } //end while(e) //Break out of loop if HorzEdge.NextInLML is not also horizontal ... if (!horzEdge->NextInLML || !IsHorizontal(*horzEdge->NextInLML)) break; UpdateEdgeIntoAEL(horzEdge); if (horzEdge->OutIdx >= 0) AddOutPt(horzEdge, horzEdge->Bot); GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); } //end for (;;) if (horzEdge->OutIdx >= 0 && !op1) { op1 = GetLastOutPt(horzEdge); TEdge* eNextHorz = m_SortedEdges; while (eNextHorz) { if (eNextHorz->OutIdx >= 0 && HorzSegmentsOverlap(horzEdge->Bot.X, horzEdge->Top.X, eNextHorz->Bot.X, eNextHorz->Top.X)) { OutPt* op2 = GetLastOutPt(eNextHorz); AddJoin(op2, op1, eNextHorz->Top); } eNextHorz = eNextHorz->NextInSEL; } AddGhostJoin(op1, horzEdge->Top); } if (horzEdge->NextInLML) { if(horzEdge->OutIdx >= 0) { op1 = AddOutPt( horzEdge, horzEdge->Top); UpdateEdgeIntoAEL(horzEdge); if (horzEdge->WindDelta == 0) return; //nb: HorzEdge is no longer horizontal here TEdge* ePrev = horzEdge->PrevInAEL; TEdge* eNext = horzEdge->NextInAEL; if (ePrev && ePrev->Curr.X == horzEdge->Bot.X && ePrev->Curr.Y == horzEdge->Bot.Y && ePrev->WindDelta != 0 && (ePrev->OutIdx >= 0 && ePrev->Curr.Y > ePrev->Top.Y && SlopesEqual(*horzEdge, *ePrev, m_UseFullRange))) { OutPt* op2 = AddOutPt(ePrev, horzEdge->Bot); AddJoin(op1, op2, horzEdge->Top); } else if (eNext && eNext->Curr.X == horzEdge->Bot.X && eNext->Curr.Y == horzEdge->Bot.Y && eNext->WindDelta != 0 && eNext->OutIdx >= 0 && eNext->Curr.Y > eNext->Top.Y && SlopesEqual(*horzEdge, *eNext, m_UseFullRange)) { OutPt* op2 = AddOutPt(eNext, horzEdge->Bot); AddJoin(op1, op2, horzEdge->Top); } } else UpdateEdgeIntoAEL(horzEdge); } else { if (horzEdge->OutIdx >= 0) AddOutPt(horzEdge, horzEdge->Top); DeleteFromAEL(horzEdge); } } //------------------------------------------------------------------------------ bool Clipper::ProcessIntersections(const cInt topY) { if( !m_ActiveEdges ) return true; try { BuildIntersectList(topY); size_t IlSize = m_IntersectList.size(); if (IlSize == 0) return true; if (IlSize == 1 || FixupIntersectionOrder()) ProcessIntersectList(); else return false; } catch(...) { m_SortedEdges = 0; DisposeIntersectNodes(); throw clipperException("ProcessIntersections error"); } m_SortedEdges = 0; return true; } //------------------------------------------------------------------------------ void Clipper::DisposeIntersectNodes() { for (size_t i = 0; i < m_IntersectList.size(); ++i ) delete m_IntersectList[i]; m_IntersectList.clear(); } //------------------------------------------------------------------------------ void Clipper::BuildIntersectList(const cInt topY) { if ( !m_ActiveEdges ) return; //prepare for sorting ... TEdge* e = m_ActiveEdges; m_SortedEdges = e; while( e ) { e->PrevInSEL = e->PrevInAEL; e->NextInSEL = e->NextInAEL; e->Curr.X = TopX( *e, topY ); e = e->NextInAEL; } //bubblesort ... bool isModified; do { isModified = false; e = m_SortedEdges; while( e->NextInSEL ) { TEdge *eNext = e->NextInSEL; IntPoint Pt; if(e->Curr.X > eNext->Curr.X) { IntersectPoint(*e, *eNext, Pt); if (Pt.Y < topY) Pt = IntPoint(TopX(*e, topY), topY); IntersectNode * newNode = new IntersectNode; newNode->Edge1 = e; newNode->Edge2 = eNext; newNode->Pt = Pt; m_IntersectList.push_back(newNode); SwapPositionsInSEL(e, eNext); isModified = true; } else e = eNext; } if( e->PrevInSEL ) e->PrevInSEL->NextInSEL = 0; else break; } while ( isModified ); m_SortedEdges = 0; //important } //------------------------------------------------------------------------------ void Clipper::ProcessIntersectList() { for (size_t i = 0; i < m_IntersectList.size(); ++i) { IntersectNode* iNode = m_IntersectList[i]; { IntersectEdges( iNode->Edge1, iNode->Edge2, iNode->Pt); SwapPositionsInAEL( iNode->Edge1 , iNode->Edge2 ); } delete iNode; } m_IntersectList.clear(); } //------------------------------------------------------------------------------ bool IntersectListSort(IntersectNode* node1, IntersectNode* node2) { return node2->Pt.Y < node1->Pt.Y; } //------------------------------------------------------------------------------ inline bool EdgesAdjacent(const IntersectNode &inode) { return (inode.Edge1->NextInSEL == inode.Edge2) || (inode.Edge1->PrevInSEL == inode.Edge2); } //------------------------------------------------------------------------------ bool Clipper::FixupIntersectionOrder() { //pre-condition: intersections are sorted Bottom-most first. //Now it's crucial that intersections are made only between adjacent edges, //so to ensure this the order of intersections may need adjusting ... CopyAELToSEL(); std::sort(m_IntersectList.begin(), m_IntersectList.end(), IntersectListSort); size_t cnt = m_IntersectList.size(); for (size_t i = 0; i < cnt; ++i) { if (!EdgesAdjacent(*m_IntersectList[i])) { size_t j = i + 1; while (j < cnt && !EdgesAdjacent(*m_IntersectList[j])) j++; if (j == cnt) return false; std::swap(m_IntersectList[i], m_IntersectList[j]); } SwapPositionsInSEL(m_IntersectList[i]->Edge1, m_IntersectList[i]->Edge2); } return true; } //------------------------------------------------------------------------------ void Clipper::DoMaxima(TEdge *e) { TEdge* eMaxPair = GetMaximaPairEx(e); if (!eMaxPair) { if (e->OutIdx >= 0) AddOutPt(e, e->Top); DeleteFromAEL(e); return; } TEdge* eNext = e->NextInAEL; while(eNext && eNext != eMaxPair) { IntersectEdges(e, eNext, e->Top); SwapPositionsInAEL(e, eNext); eNext = e->NextInAEL; } if(e->OutIdx == Unassigned && eMaxPair->OutIdx == Unassigned) { DeleteFromAEL(e); DeleteFromAEL(eMaxPair); } else if( e->OutIdx >= 0 && eMaxPair->OutIdx >= 0 ) { if (e->OutIdx >= 0) AddLocalMaxPoly(e, eMaxPair, e->Top); DeleteFromAEL(e); DeleteFromAEL(eMaxPair); } #ifdef use_lines else if (e->WindDelta == 0) { if (e->OutIdx >= 0) { AddOutPt(e, e->Top); e->OutIdx = Unassigned; } DeleteFromAEL(e); if (eMaxPair->OutIdx >= 0) { AddOutPt(eMaxPair, e->Top); eMaxPair->OutIdx = Unassigned; } DeleteFromAEL(eMaxPair); } #endif else throw clipperException("DoMaxima error"); } //------------------------------------------------------------------------------ void Clipper::ProcessEdgesAtTopOfScanbeam(const cInt topY) { TEdge* e = m_ActiveEdges; while( e ) { //1. process maxima, treating them as if they're 'bent' horizontal edges, // but exclude maxima with horizontal edges. nb: e can't be a horizontal. bool IsMaximaEdge = IsMaxima(e, topY); if(IsMaximaEdge) { TEdge* eMaxPair = GetMaximaPairEx(e); IsMaximaEdge = (!eMaxPair || !IsHorizontal(*eMaxPair)); } if(IsMaximaEdge) { if (m_StrictSimple) m_Maxima.push_back(e->Top.X); TEdge* ePrev = e->PrevInAEL; DoMaxima(e); if( !ePrev ) e = m_ActiveEdges; else e = ePrev->NextInAEL; } else { //2. promote horizontal edges, otherwise update Curr.X and Curr.Y ... if (IsIntermediate(e, topY) && IsHorizontal(*e->NextInLML)) { UpdateEdgeIntoAEL(e); if (e->OutIdx >= 0) AddOutPt(e, e->Bot); AddEdgeToSEL(e); } else { e->Curr.X = TopX( *e, topY ); e->Curr.Y = topY; #ifdef use_xyz e->Curr.Z = topY == e->Top.Y ? e->Top.Z : (topY == e->Bot.Y ? e->Bot.Z : 0); #endif } //When StrictlySimple and 'e' is being touched by another edge, then //make sure both edges have a vertex here ... if (m_StrictSimple) { TEdge* ePrev = e->PrevInAEL; if ((e->OutIdx >= 0) && (e->WindDelta != 0) && ePrev && (ePrev->OutIdx >= 0) && (ePrev->Curr.X == e->Curr.X) && (ePrev->WindDelta != 0)) { IntPoint pt = e->Curr; #ifdef use_xyz SetZ(pt, *ePrev, *e); #endif OutPt* op = AddOutPt(ePrev, pt); OutPt* op2 = AddOutPt(e, pt); AddJoin(op, op2, pt); //StrictlySimple (type-3) join } } e = e->NextInAEL; } } //3. Process horizontals at the Top of the scanbeam ... m_Maxima.sort(); ProcessHorizontals(); m_Maxima.clear(); //4. Promote intermediate vertices ... e = m_ActiveEdges; while(e) { if(IsIntermediate(e, topY)) { OutPt* op = 0; if( e->OutIdx >= 0 ) op = AddOutPt(e, e->Top); UpdateEdgeIntoAEL(e); //if output polygons share an edge, they'll need joining later ... TEdge* ePrev = e->PrevInAEL; TEdge* eNext = e->NextInAEL; if (ePrev && ePrev->Curr.X == e->Bot.X && ePrev->Curr.Y == e->Bot.Y && op && ePrev->OutIdx >= 0 && ePrev->Curr.Y > ePrev->Top.Y && SlopesEqual(e->Curr, e->Top, ePrev->Curr, ePrev->Top, m_UseFullRange) && (e->WindDelta != 0) && (ePrev->WindDelta != 0)) { OutPt* op2 = AddOutPt(ePrev, e->Bot); AddJoin(op, op2, e->Top); } else if (eNext && eNext->Curr.X == e->Bot.X && eNext->Curr.Y == e->Bot.Y && op && eNext->OutIdx >= 0 && eNext->Curr.Y > eNext->Top.Y && SlopesEqual(e->Curr, e->Top, eNext->Curr, eNext->Top, m_UseFullRange) && (e->WindDelta != 0) && (eNext->WindDelta != 0)) { OutPt* op2 = AddOutPt(eNext, e->Bot); AddJoin(op, op2, e->Top); } } e = e->NextInAEL; } } //------------------------------------------------------------------------------ void Clipper::FixupOutPolyline(OutRec &outrec) { OutPt *pp = outrec.Pts; OutPt *lastPP = pp->Prev; while (pp != lastPP) { pp = pp->Next; if (pp->Pt == pp->Prev->Pt) { if (pp == lastPP) lastPP = pp->Prev; OutPt *tmpPP = pp->Prev; tmpPP->Next = pp->Next; pp->Next->Prev = tmpPP; delete pp; pp = tmpPP; } } if (pp == pp->Prev) { DisposeOutPts(pp); outrec.Pts = 0; return; } } //------------------------------------------------------------------------------ void Clipper::FixupOutPolygon(OutRec &outrec) { //FixupOutPolygon() - removes duplicate points and simplifies consecutive //parallel edges by removing the middle vertex. OutPt *lastOK = 0; outrec.BottomPt = 0; OutPt *pp = outrec.Pts; bool preserveCol = m_PreserveCollinear || m_StrictSimple; for (;;) { if (pp->Prev == pp || pp->Prev == pp->Next) { DisposeOutPts(pp); outrec.Pts = 0; return; } //test for duplicate points and collinear edges ... if ((pp->Pt == pp->Next->Pt) || (pp->Pt == pp->Prev->Pt) || (SlopesEqual(pp->Prev->Pt, pp->Pt, pp->Next->Pt, m_UseFullRange) && (!preserveCol || !Pt2IsBetweenPt1AndPt3(pp->Prev->Pt, pp->Pt, pp->Next->Pt)))) { lastOK = 0; OutPt *tmp = pp; pp->Prev->Next = pp->Next; pp->Next->Prev = pp->Prev; pp = pp->Prev; delete tmp; } else if (pp == lastOK) break; else { if (!lastOK) lastOK = pp; pp = pp->Next; } } outrec.Pts = pp; } //------------------------------------------------------------------------------ int PointCount(OutPt *Pts) { if (!Pts) return 0; int result = 0; OutPt* p = Pts; do { result++; p = p->Next; } while (p != Pts); return result; } //------------------------------------------------------------------------------ void Clipper::BuildResult(Paths &polys) { polys.reserve(m_PolyOuts.size()); for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { if (!m_PolyOuts[i]->Pts) continue; Path pg; OutPt* p = m_PolyOuts[i]->Pts->Prev; int cnt = PointCount(p); if (cnt < 2) continue; pg.reserve(cnt); for (int i = 0; i < cnt; ++i) { pg.push_back(p->Pt); p = p->Prev; } polys.push_back(pg); } } //------------------------------------------------------------------------------ void Clipper::BuildResult2(PolyTree& polytree) { polytree.Clear(); polytree.AllNodes.reserve(m_PolyOuts.size()); //add each output polygon/contour to polytree ... for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) { OutRec* outRec = m_PolyOuts[i]; int cnt = PointCount(outRec->Pts); if ((outRec->IsOpen && cnt < 2) || (!outRec->IsOpen && cnt < 3)) continue; FixHoleLinkage(*outRec); PolyNode* pn = new PolyNode(); //nb: polytree takes ownership of all the PolyNodes polytree.AllNodes.push_back(pn); outRec->PolyNd = pn; pn->Parent = 0; pn->Index = 0; pn->Contour.reserve(cnt); OutPt *op = outRec->Pts->Prev; for (int j = 0; j < cnt; j++) { pn->Contour.push_back(op->Pt); op = op->Prev; } } //fixup PolyNode links etc ... polytree.Childs.reserve(m_PolyOuts.size()); for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) { OutRec* outRec = m_PolyOuts[i]; if (!outRec->PolyNd) continue; if (outRec->IsOpen) { outRec->PolyNd->m_IsOpen = true; polytree.AddChild(*outRec->PolyNd); } else if (outRec->FirstLeft && outRec->FirstLeft->PolyNd) outRec->FirstLeft->PolyNd->AddChild(*outRec->PolyNd); else polytree.AddChild(*outRec->PolyNd); } } //------------------------------------------------------------------------------ void SwapIntersectNodes(IntersectNode &int1, IntersectNode &int2) { //just swap the contents (because fIntersectNodes is a single-linked-list) IntersectNode inode = int1; //gets a copy of Int1 int1.Edge1 = int2.Edge1; int1.Edge2 = int2.Edge2; int1.Pt = int2.Pt; int2.Edge1 = inode.Edge1; int2.Edge2 = inode.Edge2; int2.Pt = inode.Pt; } //------------------------------------------------------------------------------ inline bool E2InsertsBeforeE1(TEdge &e1, TEdge &e2) { if (e2.Curr.X == e1.Curr.X) { if (e2.Top.Y > e1.Top.Y) return e2.Top.X < TopX(e1, e2.Top.Y); else return e1.Top.X > TopX(e2, e1.Top.Y); } else return e2.Curr.X < e1.Curr.X; } //------------------------------------------------------------------------------ bool GetOverlap(const cInt a1, const cInt a2, const cInt b1, const cInt b2, cInt& Left, cInt& Right) { if (a1 < a2) { if (b1 < b2) {Left = std::max(a1,b1); Right = std::min(a2,b2);} else {Left = std::max(a1,b2); Right = std::min(a2,b1);} } else { if (b1 < b2) {Left = std::max(a2,b1); Right = std::min(a1,b2);} else {Left = std::max(a2,b2); Right = std::min(a1,b1);} } return Left < Right; } //------------------------------------------------------------------------------ inline void UpdateOutPtIdxs(OutRec& outrec) { OutPt* op = outrec.Pts; do { op->Idx = outrec.Idx; op = op->Prev; } while(op != outrec.Pts); } //------------------------------------------------------------------------------ void Clipper::InsertEdgeIntoAEL(TEdge *edge, TEdge* startEdge) { if(!m_ActiveEdges) { edge->PrevInAEL = 0; edge->NextInAEL = 0; m_ActiveEdges = edge; } else if(!startEdge && E2InsertsBeforeE1(*m_ActiveEdges, *edge)) { edge->PrevInAEL = 0; edge->NextInAEL = m_ActiveEdges; m_ActiveEdges->PrevInAEL = edge; m_ActiveEdges = edge; } else { if(!startEdge) startEdge = m_ActiveEdges; while(startEdge->NextInAEL && !E2InsertsBeforeE1(*startEdge->NextInAEL , *edge)) startEdge = startEdge->NextInAEL; edge->NextInAEL = startEdge->NextInAEL; if(startEdge->NextInAEL) startEdge->NextInAEL->PrevInAEL = edge; edge->PrevInAEL = startEdge; startEdge->NextInAEL = edge; } } //---------------------------------------------------------------------- OutPt* DupOutPt(OutPt* outPt, bool InsertAfter) { OutPt* result = new OutPt; result->Pt = outPt->Pt; result->Idx = outPt->Idx; if (InsertAfter) { result->Next = outPt->Next; result->Prev = outPt; outPt->Next->Prev = result; outPt->Next = result; } else { result->Prev = outPt->Prev; result->Next = outPt; outPt->Prev->Next = result; outPt->Prev = result; } return result; } //------------------------------------------------------------------------------ bool JoinHorz(OutPt* op1, OutPt* op1b, OutPt* op2, OutPt* op2b, const IntPoint Pt, bool DiscardLeft) { Direction Dir1 = (op1->Pt.X > op1b->Pt.X ? dRightToLeft : dLeftToRight); Direction Dir2 = (op2->Pt.X > op2b->Pt.X ? dRightToLeft : dLeftToRight); if (Dir1 == Dir2) return false; //When DiscardLeft, we want Op1b to be on the Left of Op1, otherwise we //want Op1b to be on the Right. (And likewise with Op2 and Op2b.) //So, to facilitate this while inserting Op1b and Op2b ... //when DiscardLeft, make sure we're AT or RIGHT of Pt before adding Op1b, //otherwise make sure we're AT or LEFT of Pt. (Likewise with Op2b.) if (Dir1 == dLeftToRight) { while (op1->Next->Pt.X <= Pt.X && op1->Next->Pt.X >= op1->Pt.X && op1->Next->Pt.Y == Pt.Y) op1 = op1->Next; if (DiscardLeft && (op1->Pt.X != Pt.X)) op1 = op1->Next; op1b = DupOutPt(op1, !DiscardLeft); if (op1b->Pt != Pt) { op1 = op1b; op1->Pt = Pt; op1b = DupOutPt(op1, !DiscardLeft); } } else { while (op1->Next->Pt.X >= Pt.X && op1->Next->Pt.X <= op1->Pt.X && op1->Next->Pt.Y == Pt.Y) op1 = op1->Next; if (!DiscardLeft && (op1->Pt.X != Pt.X)) op1 = op1->Next; op1b = DupOutPt(op1, DiscardLeft); if (op1b->Pt != Pt) { op1 = op1b; op1->Pt = Pt; op1b = DupOutPt(op1, DiscardLeft); } } if (Dir2 == dLeftToRight) { while (op2->Next->Pt.X <= Pt.X && op2->Next->Pt.X >= op2->Pt.X && op2->Next->Pt.Y == Pt.Y) op2 = op2->Next; if (DiscardLeft && (op2->Pt.X != Pt.X)) op2 = op2->Next; op2b = DupOutPt(op2, !DiscardLeft); if (op2b->Pt != Pt) { op2 = op2b; op2->Pt = Pt; op2b = DupOutPt(op2, !DiscardLeft); }; } else { while (op2->Next->Pt.X >= Pt.X && op2->Next->Pt.X <= op2->Pt.X && op2->Next->Pt.Y == Pt.Y) op2 = op2->Next; if (!DiscardLeft && (op2->Pt.X != Pt.X)) op2 = op2->Next; op2b = DupOutPt(op2, DiscardLeft); if (op2b->Pt != Pt) { op2 = op2b; op2->Pt = Pt; op2b = DupOutPt(op2, DiscardLeft); }; }; if ((Dir1 == dLeftToRight) == DiscardLeft) { op1->Prev = op2; op2->Next = op1; op1b->Next = op2b; op2b->Prev = op1b; } else { op1->Next = op2; op2->Prev = op1; op1b->Prev = op2b; op2b->Next = op1b; } return true; } //------------------------------------------------------------------------------ bool Clipper::JoinPoints(Join *j, OutRec* outRec1, OutRec* outRec2) { OutPt *op1 = j->OutPt1, *op1b; OutPt *op2 = j->OutPt2, *op2b; //There are 3 kinds of joins for output polygons ... //1. Horizontal joins where Join.OutPt1 & Join.OutPt2 are vertices anywhere //along (horizontal) collinear edges (& Join.OffPt is on the same horizontal). //2. Non-horizontal joins where Join.OutPt1 & Join.OutPt2 are at the same //location at the Bottom of the overlapping segment (& Join.OffPt is above). //3. StrictSimple joins where edges touch but are not collinear and where //Join.OutPt1, Join.OutPt2 & Join.OffPt all share the same point. bool isHorizontal = (j->OutPt1->Pt.Y == j->OffPt.Y); if (isHorizontal && (j->OffPt == j->OutPt1->Pt) && (j->OffPt == j->OutPt2->Pt)) { //Strictly Simple join ... if (outRec1 != outRec2) return false; op1b = j->OutPt1->Next; while (op1b != op1 && (op1b->Pt == j->OffPt)) op1b = op1b->Next; bool reverse1 = (op1b->Pt.Y > j->OffPt.Y); op2b = j->OutPt2->Next; while (op2b != op2 && (op2b->Pt == j->OffPt)) op2b = op2b->Next; bool reverse2 = (op2b->Pt.Y > j->OffPt.Y); if (reverse1 == reverse2) return false; if (reverse1) { op1b = DupOutPt(op1, false); op2b = DupOutPt(op2, true); op1->Prev = op2; op2->Next = op1; op1b->Next = op2b; op2b->Prev = op1b; j->OutPt1 = op1; j->OutPt2 = op1b; return true; } else { op1b = DupOutPt(op1, true); op2b = DupOutPt(op2, false); op1->Next = op2; op2->Prev = op1; op1b->Prev = op2b; op2b->Next = op1b; j->OutPt1 = op1; j->OutPt2 = op1b; return true; } } else if (isHorizontal) { //treat horizontal joins differently to non-horizontal joins since with //them we're not yet sure where the overlapping is. OutPt1.Pt & OutPt2.Pt //may be anywhere along the horizontal edge. op1b = op1; while (op1->Prev->Pt.Y == op1->Pt.Y && op1->Prev != op1b && op1->Prev != op2) op1 = op1->Prev; while (op1b->Next->Pt.Y == op1b->Pt.Y && op1b->Next != op1 && op1b->Next != op2) op1b = op1b->Next; if (op1b->Next == op1 || op1b->Next == op2) return false; //a flat 'polygon' op2b = op2; while (op2->Prev->Pt.Y == op2->Pt.Y && op2->Prev != op2b && op2->Prev != op1b) op2 = op2->Prev; while (op2b->Next->Pt.Y == op2b->Pt.Y && op2b->Next != op2 && op2b->Next != op1) op2b = op2b->Next; if (op2b->Next == op2 || op2b->Next == op1) return false; //a flat 'polygon' cInt Left, Right; //Op1 --> Op1b & Op2 --> Op2b are the extremites of the horizontal edges if (!GetOverlap(op1->Pt.X, op1b->Pt.X, op2->Pt.X, op2b->Pt.X, Left, Right)) return false; //DiscardLeftSide: when overlapping edges are joined, a spike will created //which needs to be cleaned up. However, we don't want Op1 or Op2 caught up //on the discard Side as either may still be needed for other joins ... IntPoint Pt; bool DiscardLeftSide; if (op1->Pt.X >= Left && op1->Pt.X <= Right) { Pt = op1->Pt; DiscardLeftSide = (op1->Pt.X > op1b->Pt.X); } else if (op2->Pt.X >= Left&& op2->Pt.X <= Right) { Pt = op2->Pt; DiscardLeftSide = (op2->Pt.X > op2b->Pt.X); } else if (op1b->Pt.X >= Left && op1b->Pt.X <= Right) { Pt = op1b->Pt; DiscardLeftSide = op1b->Pt.X > op1->Pt.X; } else { Pt = op2b->Pt; DiscardLeftSide = (op2b->Pt.X > op2->Pt.X); } j->OutPt1 = op1; j->OutPt2 = op2; return JoinHorz(op1, op1b, op2, op2b, Pt, DiscardLeftSide); } else { //nb: For non-horizontal joins ... // 1. Jr.OutPt1.Pt.Y == Jr.OutPt2.Pt.Y // 2. Jr.OutPt1.Pt > Jr.OffPt.Y //make sure the polygons are correctly oriented ... op1b = op1->Next; while ((op1b->Pt == op1->Pt) && (op1b != op1)) op1b = op1b->Next; bool Reverse1 = ((op1b->Pt.Y > op1->Pt.Y) || !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange)); if (Reverse1) { op1b = op1->Prev; while ((op1b->Pt == op1->Pt) && (op1b != op1)) op1b = op1b->Prev; if ((op1b->Pt.Y > op1->Pt.Y) || !SlopesEqual(op1->Pt, op1b->Pt, j->OffPt, m_UseFullRange)) return false; }; op2b = op2->Next; while ((op2b->Pt == op2->Pt) && (op2b != op2))op2b = op2b->Next; bool Reverse2 = ((op2b->Pt.Y > op2->Pt.Y) || !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange)); if (Reverse2) { op2b = op2->Prev; while ((op2b->Pt == op2->Pt) && (op2b != op2)) op2b = op2b->Prev; if ((op2b->Pt.Y > op2->Pt.Y) || !SlopesEqual(op2->Pt, op2b->Pt, j->OffPt, m_UseFullRange)) return false; } if ((op1b == op1) || (op2b == op2) || (op1b == op2b) || ((outRec1 == outRec2) && (Reverse1 == Reverse2))) return false; if (Reverse1) { op1b = DupOutPt(op1, false); op2b = DupOutPt(op2, true); op1->Prev = op2; op2->Next = op1; op1b->Next = op2b; op2b->Prev = op1b; j->OutPt1 = op1; j->OutPt2 = op1b; return true; } else { op1b = DupOutPt(op1, true); op2b = DupOutPt(op2, false); op1->Next = op2; op2->Prev = op1; op1b->Prev = op2b; op2b->Next = op1b; j->OutPt1 = op1; j->OutPt2 = op1b; return true; } } } //---------------------------------------------------------------------- static OutRec* ParseFirstLeft(OutRec* FirstLeft) { while (FirstLeft && !FirstLeft->Pts) FirstLeft = FirstLeft->FirstLeft; return FirstLeft; } //------------------------------------------------------------------------------ void Clipper::FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec) { //tests if NewOutRec contains the polygon before reassigning FirstLeft for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { OutRec* outRec = m_PolyOuts[i]; OutRec* firstLeft = ParseFirstLeft(outRec->FirstLeft); if (outRec->Pts && firstLeft == OldOutRec) { if (Poly2ContainsPoly1(outRec->Pts, NewOutRec->Pts)) outRec->FirstLeft = NewOutRec; } } } //---------------------------------------------------------------------- void Clipper::FixupFirstLefts2(OutRec* InnerOutRec, OutRec* OuterOutRec) { //A polygon has split into two such that one is now the inner of the other. //It's possible that these polygons now wrap around other polygons, so check //every polygon that's also contained by OuterOutRec's FirstLeft container //(including 0) to see if they've become inner to the new inner polygon ... OutRec* orfl = OuterOutRec->FirstLeft; for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { OutRec* outRec = m_PolyOuts[i]; if (!outRec->Pts || outRec == OuterOutRec || outRec == InnerOutRec) continue; OutRec* firstLeft = ParseFirstLeft(outRec->FirstLeft); if (firstLeft != orfl && firstLeft != InnerOutRec && firstLeft != OuterOutRec) continue; if (Poly2ContainsPoly1(outRec->Pts, InnerOutRec->Pts)) outRec->FirstLeft = InnerOutRec; else if (Poly2ContainsPoly1(outRec->Pts, OuterOutRec->Pts)) outRec->FirstLeft = OuterOutRec; else if (outRec->FirstLeft == InnerOutRec || outRec->FirstLeft == OuterOutRec) outRec->FirstLeft = orfl; } } //---------------------------------------------------------------------- void Clipper::FixupFirstLefts3(OutRec* OldOutRec, OutRec* NewOutRec) { //reassigns FirstLeft WITHOUT testing if NewOutRec contains the polygon for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) { OutRec* outRec = m_PolyOuts[i]; OutRec* firstLeft = ParseFirstLeft(outRec->FirstLeft); if (outRec->Pts && firstLeft == OldOutRec) outRec->FirstLeft = NewOutRec; } } //---------------------------------------------------------------------- void Clipper::JoinCommonEdges() { for (JoinList::size_type i = 0; i < m_Joins.size(); i++) { Join* join = m_Joins[i]; OutRec *outRec1 = GetOutRec(join->OutPt1->Idx); OutRec *outRec2 = GetOutRec(join->OutPt2->Idx); if (!outRec1->Pts || !outRec2->Pts) continue; if (outRec1->IsOpen || outRec2->IsOpen) continue; //get the polygon fragment with the correct hole state (FirstLeft) //before calling JoinPoints() ... OutRec *holeStateRec; if (outRec1 == outRec2) holeStateRec = outRec1; else if (OutRec1RightOfOutRec2(outRec1, outRec2)) holeStateRec = outRec2; else if (OutRec1RightOfOutRec2(outRec2, outRec1)) holeStateRec = outRec1; else holeStateRec = GetLowermostRec(outRec1, outRec2); if (!JoinPoints(join, outRec1, outRec2)) continue; if (outRec1 == outRec2) { //instead of joining two polygons, we've just created a new one by //splitting one polygon into two. outRec1->Pts = join->OutPt1; outRec1->BottomPt = 0; outRec2 = CreateOutRec(); outRec2->Pts = join->OutPt2; //update all OutRec2.Pts Idx's ... UpdateOutPtIdxs(*outRec2); if (Poly2ContainsPoly1(outRec2->Pts, outRec1->Pts)) { //outRec1 contains outRec2 ... outRec2->IsHole = !outRec1->IsHole; outRec2->FirstLeft = outRec1; if (m_UsingPolyTree) FixupFirstLefts2(outRec2, outRec1); if ((outRec2->IsHole ^ m_ReverseOutput) == (Area(*outRec2) > 0)) ReversePolyPtLinks(outRec2->Pts); } else if (Poly2ContainsPoly1(outRec1->Pts, outRec2->Pts)) { //outRec2 contains outRec1 ... outRec2->IsHole = outRec1->IsHole; outRec1->IsHole = !outRec2->IsHole; outRec2->FirstLeft = outRec1->FirstLeft; outRec1->FirstLeft = outRec2; if (m_UsingPolyTree) FixupFirstLefts2(outRec1, outRec2); if ((outRec1->IsHole ^ m_ReverseOutput) == (Area(*outRec1) > 0)) ReversePolyPtLinks(outRec1->Pts); } else { //the 2 polygons are completely separate ... outRec2->IsHole = outRec1->IsHole; outRec2->FirstLeft = outRec1->FirstLeft; //fixup FirstLeft pointers that may need reassigning to OutRec2 if (m_UsingPolyTree) FixupFirstLefts1(outRec1, outRec2); } } else { //joined 2 polygons together ... outRec2->Pts = 0; outRec2->BottomPt = 0; outRec2->Idx = outRec1->Idx; outRec1->IsHole = holeStateRec->IsHole; if (holeStateRec == outRec2) outRec1->FirstLeft = outRec2->FirstLeft; outRec2->FirstLeft = outRec1; if (m_UsingPolyTree) FixupFirstLefts3(outRec2, outRec1); } } } //------------------------------------------------------------------------------ // ClipperOffset support functions ... //------------------------------------------------------------------------------ DoublePoint GetUnitNormal(const IntPoint &pt1, const IntPoint &pt2) { if(pt2.X == pt1.X && pt2.Y == pt1.Y) return DoublePoint(0, 0); double Dx = (double)(pt2.X - pt1.X); double dy = (double)(pt2.Y - pt1.Y); double f = 1 *1.0/ std::sqrt( Dx*Dx + dy*dy ); Dx *= f; dy *= f; return DoublePoint(dy, -Dx); } //------------------------------------------------------------------------------ // ClipperOffset class //------------------------------------------------------------------------------ ClipperOffset::ClipperOffset(double miterLimit, double arcTolerance) { this->MiterLimit = miterLimit; this->ArcTolerance = arcTolerance; m_lowest.X = -1; } //------------------------------------------------------------------------------ ClipperOffset::~ClipperOffset() { Clear(); } //------------------------------------------------------------------------------ void ClipperOffset::Clear() { for (int i = 0; i < m_polyNodes.ChildCount(); ++i) delete m_polyNodes.Childs[i]; m_polyNodes.Childs.clear(); m_lowest.X = -1; } //------------------------------------------------------------------------------ void ClipperOffset::AddPath(const Path& path, JoinType joinType, EndType endType) { int highI = (int)path.size() - 1; if (highI < 0) return; PolyNode* newNode = new PolyNode(); newNode->m_jointype = joinType; newNode->m_endtype = endType; //strip duplicate points from path and also get index to the lowest point ... if (endType == etClosedLine || endType == etClosedPolygon) while (highI > 0 && path[0] == path[highI]) highI--; newNode->Contour.reserve(highI + 1); newNode->Contour.push_back(path[0]); int j = 0, k = 0; for (int i = 1; i <= highI; i++) if (newNode->Contour[j] != path[i]) { j++; newNode->Contour.push_back(path[i]); if (path[i].Y > newNode->Contour[k].Y || (path[i].Y == newNode->Contour[k].Y && path[i].X < newNode->Contour[k].X)) k = j; } if (endType == etClosedPolygon && j < 2) { delete newNode; return; } m_polyNodes.AddChild(*newNode); //if this path's lowest pt is lower than all the others then update m_lowest if (endType != etClosedPolygon) return; if (m_lowest.X < 0) m_lowest = IntPoint(m_polyNodes.ChildCount() - 1, k); else { IntPoint ip = m_polyNodes.Childs[(int)m_lowest.X]->Contour[(int)m_lowest.Y]; if (newNode->Contour[k].Y > ip.Y || (newNode->Contour[k].Y == ip.Y && newNode->Contour[k].X < ip.X)) m_lowest = IntPoint(m_polyNodes.ChildCount() - 1, k); } } //------------------------------------------------------------------------------ void ClipperOffset::AddPaths(const Paths& paths, JoinType joinType, EndType endType) { for (Paths::size_type i = 0; i < paths.size(); ++i) AddPath(paths[i], joinType, endType); } //------------------------------------------------------------------------------ void ClipperOffset::FixOrientations() { //fixup orientations of all closed paths if the orientation of the //closed path with the lowermost vertex is wrong ... if (m_lowest.X >= 0 && !Orientation(m_polyNodes.Childs[(int)m_lowest.X]->Contour)) { for (int i = 0; i < m_polyNodes.ChildCount(); ++i) { PolyNode& node = *m_polyNodes.Childs[i]; if (node.m_endtype == etClosedPolygon || (node.m_endtype == etClosedLine && Orientation(node.Contour))) ReversePath(node.Contour); } } else { for (int i = 0; i < m_polyNodes.ChildCount(); ++i) { PolyNode& node = *m_polyNodes.Childs[i]; if (node.m_endtype == etClosedLine && !Orientation(node.Contour)) ReversePath(node.Contour); } } } //------------------------------------------------------------------------------ void ClipperOffset::Execute(Paths& solution, double delta) { solution.clear(); FixOrientations(); DoOffset(delta); //now clean up 'corners' ... Clipper clpr; clpr.AddPaths(m_destPolys, ptSubject, true); if (delta > 0) { clpr.Execute(ctUnion, solution, pftPositive, pftPositive); } else { IntRect r = clpr.GetBounds(); Path outer(4); outer[0] = IntPoint(r.left - 10, r.bottom + 10); outer[1] = IntPoint(r.right + 10, r.bottom + 10); outer[2] = IntPoint(r.right + 10, r.top - 10); outer[3] = IntPoint(r.left - 10, r.top - 10); clpr.AddPath(outer, ptSubject, true); clpr.ReverseSolution(true); clpr.Execute(ctUnion, solution, pftNegative, pftNegative); if (solution.size() > 0) solution.erase(solution.begin()); } } //------------------------------------------------------------------------------ void ClipperOffset::Execute(PolyTree& solution, double delta) { solution.Clear(); FixOrientations(); DoOffset(delta); //now clean up 'corners' ... Clipper clpr; clpr.AddPaths(m_destPolys, ptSubject, true); if (delta > 0) { clpr.Execute(ctUnion, solution, pftPositive, pftPositive); } else { IntRect r = clpr.GetBounds(); Path outer(4); outer[0] = IntPoint(r.left - 10, r.bottom + 10); outer[1] = IntPoint(r.right + 10, r.bottom + 10); outer[2] = IntPoint(r.right + 10, r.top - 10); outer[3] = IntPoint(r.left - 10, r.top - 10); clpr.AddPath(outer, ptSubject, true); clpr.ReverseSolution(true); clpr.Execute(ctUnion, solution, pftNegative, pftNegative); //remove the outer PolyNode rectangle ... if (solution.ChildCount() == 1 && solution.Childs[0]->ChildCount() > 0) { PolyNode* outerNode = solution.Childs[0]; solution.Childs.reserve(outerNode->ChildCount()); solution.Childs[0] = outerNode->Childs[0]; solution.Childs[0]->Parent = outerNode->Parent; for (int i = 1; i < outerNode->ChildCount(); ++i) solution.AddChild(*outerNode->Childs[i]); } else solution.Clear(); } } //------------------------------------------------------------------------------ void ClipperOffset::DoOffset(double delta) { m_destPolys.clear(); m_delta = delta; //if Zero offset, just copy any CLOSED polygons to m_p and return ... if (NEAR_ZERO(delta)) { m_destPolys.reserve(m_polyNodes.ChildCount()); for (int i = 0; i < m_polyNodes.ChildCount(); i++) { PolyNode& node = *m_polyNodes.Childs[i]; if (node.m_endtype == etClosedPolygon) m_destPolys.push_back(node.Contour); } return; } //see offset_triginometry3.svg in the documentation folder ... if (MiterLimit > 2) m_miterLim = 2/(MiterLimit * MiterLimit); else m_miterLim = 0.5; double y; if (ArcTolerance <= 0.0) y = def_arc_tolerance; else if (ArcTolerance > std::fabs(delta) * def_arc_tolerance) y = std::fabs(delta) * def_arc_tolerance; else y = ArcTolerance; //see offset_triginometry2.svg in the documentation folder ... double steps = pi / std::acos(1 - y / std::fabs(delta)); if (steps > std::fabs(delta) * pi) steps = std::fabs(delta) * pi; //ie excessive precision check m_sin = std::sin(two_pi / steps); m_cos = std::cos(two_pi / steps); m_StepsPerRad = steps / two_pi; if (delta < 0.0) m_sin = -m_sin; m_destPolys.reserve(m_polyNodes.ChildCount() * 2); for (int i = 0; i < m_polyNodes.ChildCount(); i++) { PolyNode& node = *m_polyNodes.Childs[i]; m_srcPoly = node.Contour; int len = (int)m_srcPoly.size(); if (len == 0 || (delta <= 0 && (len < 3 || node.m_endtype != etClosedPolygon))) continue; m_destPoly.clear(); if (len == 1) { if (node.m_jointype == jtRound) { double X = 1.0, Y = 0.0; for (cInt j = 1; j <= steps; j++) { m_destPoly.push_back(IntPoint( Round(m_srcPoly[0].X + X * delta), Round(m_srcPoly[0].Y + Y * delta))); double X2 = X; X = X * m_cos - m_sin * Y; Y = X2 * m_sin + Y * m_cos; } } else { double X = -1.0, Y = -1.0; for (int j = 0; j < 4; ++j) { m_destPoly.push_back(IntPoint( Round(m_srcPoly[0].X + X * delta), Round(m_srcPoly[0].Y + Y * delta))); if (X < 0) X = 1; else if (Y < 0) Y = 1; else X = -1; } } m_destPolys.push_back(m_destPoly); continue; } //build m_normals ... m_normals.clear(); m_normals.reserve(len); for (int j = 0; j < len - 1; ++j) m_normals.push_back(GetUnitNormal(m_srcPoly[j], m_srcPoly[j + 1])); if (node.m_endtype == etClosedLine || node.m_endtype == etClosedPolygon) m_normals.push_back(GetUnitNormal(m_srcPoly[len - 1], m_srcPoly[0])); else m_normals.push_back(DoublePoint(m_normals[len - 2])); if (node.m_endtype == etClosedPolygon) { int k = len - 1; for (int j = 0; j < len; ++j) OffsetPoint(j, k, node.m_jointype); m_destPolys.push_back(m_destPoly); } else if (node.m_endtype == etClosedLine) { int k = len - 1; for (int j = 0; j < len; ++j) OffsetPoint(j, k, node.m_jointype); m_destPolys.push_back(m_destPoly); m_destPoly.clear(); //re-build m_normals ... DoublePoint n = m_normals[len -1]; for (int j = len - 1; j > 0; j--) m_normals[j] = DoublePoint(-m_normals[j - 1].X, -m_normals[j - 1].Y); m_normals[0] = DoublePoint(-n.X, -n.Y); k = 0; for (int j = len - 1; j >= 0; j--) OffsetPoint(j, k, node.m_jointype); m_destPolys.push_back(m_destPoly); } else { int k = 0; for (int j = 1; j < len - 1; ++j) OffsetPoint(j, k, node.m_jointype); IntPoint pt1; if (node.m_endtype == etOpenButt) { int j = len - 1; pt1 = IntPoint((cInt)Round(m_srcPoly[j].X + m_normals[j].X * delta), (cInt)Round(m_srcPoly[j].Y + m_normals[j].Y * delta)); m_destPoly.push_back(pt1); pt1 = IntPoint((cInt)Round(m_srcPoly[j].X - m_normals[j].X * delta), (cInt)Round(m_srcPoly[j].Y - m_normals[j].Y * delta)); m_destPoly.push_back(pt1); } else { int j = len - 1; k = len - 2; m_sinA = 0; m_normals[j] = DoublePoint(-m_normals[j].X, -m_normals[j].Y); if (node.m_endtype == etOpenSquare) DoSquare(j, k); else DoRound(j, k); } //re-build m_normals ... for (int j = len - 1; j > 0; j--) m_normals[j] = DoublePoint(-m_normals[j - 1].X, -m_normals[j - 1].Y); m_normals[0] = DoublePoint(-m_normals[1].X, -m_normals[1].Y); k = len - 1; for (int j = k - 1; j > 0; --j) OffsetPoint(j, k, node.m_jointype); if (node.m_endtype == etOpenButt) { pt1 = IntPoint((cInt)Round(m_srcPoly[0].X - m_normals[0].X * delta), (cInt)Round(m_srcPoly[0].Y - m_normals[0].Y * delta)); m_destPoly.push_back(pt1); pt1 = IntPoint((cInt)Round(m_srcPoly[0].X + m_normals[0].X * delta), (cInt)Round(m_srcPoly[0].Y + m_normals[0].Y * delta)); m_destPoly.push_back(pt1); } else { k = 1; m_sinA = 0; if (node.m_endtype == etOpenSquare) DoSquare(0, 1); else DoRound(0, 1); } m_destPolys.push_back(m_destPoly); } } } //------------------------------------------------------------------------------ void ClipperOffset::OffsetPoint(int j, int& k, JoinType jointype) { //cross product ... m_sinA = (m_normals[k].X * m_normals[j].Y - m_normals[j].X * m_normals[k].Y); if (std::fabs(m_sinA * m_delta) < 1.0) { //dot product ... double cosA = (m_normals[k].X * m_normals[j].X + m_normals[j].Y * m_normals[k].Y ); if (cosA > 0) // angle => 0 degrees { m_destPoly.push_back(IntPoint(Round(m_srcPoly[j].X + m_normals[k].X * m_delta), Round(m_srcPoly[j].Y + m_normals[k].Y * m_delta))); return; } //else angle => 180 degrees } else if (m_sinA > 1.0) m_sinA = 1.0; else if (m_sinA < -1.0) m_sinA = -1.0; if (m_sinA * m_delta < 0) { m_destPoly.push_back(IntPoint(Round(m_srcPoly[j].X + m_normals[k].X * m_delta), Round(m_srcPoly[j].Y + m_normals[k].Y * m_delta))); m_destPoly.push_back(m_srcPoly[j]); m_destPoly.push_back(IntPoint(Round(m_srcPoly[j].X + m_normals[j].X * m_delta), Round(m_srcPoly[j].Y + m_normals[j].Y * m_delta))); } else switch (jointype) { case jtMiter: { double r = 1 + (m_normals[j].X * m_normals[k].X + m_normals[j].Y * m_normals[k].Y); if (r >= m_miterLim) DoMiter(j, k, r); else DoSquare(j, k); break; } case jtSquare: DoSquare(j, k); break; case jtRound: DoRound(j, k); break; } k = j; } //------------------------------------------------------------------------------ void ClipperOffset::DoSquare(int j, int k) { double dx = std::tan(std::atan2(m_sinA, m_normals[k].X * m_normals[j].X + m_normals[k].Y * m_normals[j].Y) / 4); m_destPoly.push_back(IntPoint( Round(m_srcPoly[j].X + m_delta * (m_normals[k].X - m_normals[k].Y * dx)), Round(m_srcPoly[j].Y + m_delta * (m_normals[k].Y + m_normals[k].X * dx)))); m_destPoly.push_back(IntPoint( Round(m_srcPoly[j].X + m_delta * (m_normals[j].X + m_normals[j].Y * dx)), Round(m_srcPoly[j].Y + m_delta * (m_normals[j].Y - m_normals[j].X * dx)))); } //------------------------------------------------------------------------------ void ClipperOffset::DoMiter(int j, int k, double r) { double q = m_delta / r; m_destPoly.push_back(IntPoint(Round(m_srcPoly[j].X + (m_normals[k].X + m_normals[j].X) * q), Round(m_srcPoly[j].Y + (m_normals[k].Y + m_normals[j].Y) * q))); } //------------------------------------------------------------------------------ void ClipperOffset::DoRound(int j, int k) { double a = std::atan2(m_sinA, m_normals[k].X * m_normals[j].X + m_normals[k].Y * m_normals[j].Y); int steps = std::max((int)Round(m_StepsPerRad * std::fabs(a)), 1); double X = m_normals[k].X, Y = m_normals[k].Y, X2; for (int i = 0; i < steps; ++i) { m_destPoly.push_back(IntPoint( Round(m_srcPoly[j].X + X * m_delta), Round(m_srcPoly[j].Y + Y * m_delta))); X2 = X; X = X * m_cos - m_sin * Y; Y = X2 * m_sin + Y * m_cos; } m_destPoly.push_back(IntPoint( Round(m_srcPoly[j].X + m_normals[j].X * m_delta), Round(m_srcPoly[j].Y + m_normals[j].Y * m_delta))); } //------------------------------------------------------------------------------ // Miscellaneous public functions //------------------------------------------------------------------------------ void Clipper::DoSimplePolygons() { PolyOutList::size_type i = 0; while (i < m_PolyOuts.size()) { OutRec* outrec = m_PolyOuts[i++]; OutPt* op = outrec->Pts; if (!op || outrec->IsOpen) continue; do //for each Pt in Polygon until duplicate found do ... { OutPt* op2 = op->Next; while (op2 != outrec->Pts) { if ((op->Pt == op2->Pt) && op2->Next != op && op2->Prev != op) { //split the polygon into two ... OutPt* op3 = op->Prev; OutPt* op4 = op2->Prev; op->Prev = op4; op4->Next = op; op2->Prev = op3; op3->Next = op2; outrec->Pts = op; OutRec* outrec2 = CreateOutRec(); outrec2->Pts = op2; UpdateOutPtIdxs(*outrec2); if (Poly2ContainsPoly1(outrec2->Pts, outrec->Pts)) { //OutRec2 is contained by OutRec1 ... outrec2->IsHole = !outrec->IsHole; outrec2->FirstLeft = outrec; if (m_UsingPolyTree) FixupFirstLefts2(outrec2, outrec); } else if (Poly2ContainsPoly1(outrec->Pts, outrec2->Pts)) { //OutRec1 is contained by OutRec2 ... outrec2->IsHole = outrec->IsHole; outrec->IsHole = !outrec2->IsHole; outrec2->FirstLeft = outrec->FirstLeft; outrec->FirstLeft = outrec2; if (m_UsingPolyTree) FixupFirstLefts2(outrec, outrec2); } else { //the 2 polygons are separate ... outrec2->IsHole = outrec->IsHole; outrec2->FirstLeft = outrec->FirstLeft; if (m_UsingPolyTree) FixupFirstLefts1(outrec, outrec2); } op2 = op; //ie get ready for the Next iteration } op2 = op2->Next; } op = op->Next; } while (op != outrec->Pts); } } //------------------------------------------------------------------------------ void ReversePath(Path& p) { std::reverse(p.begin(), p.end()); } //------------------------------------------------------------------------------ void ReversePaths(Paths& p) { for (Paths::size_type i = 0; i < p.size(); ++i) ReversePath(p[i]); } //------------------------------------------------------------------------------ void SimplifyPolygon(const Path &in_poly, Paths &out_polys, PolyFillType fillType) { Clipper c; c.StrictlySimple(true); c.AddPath(in_poly, ptSubject, true); c.Execute(ctUnion, out_polys, fillType, fillType); } //------------------------------------------------------------------------------ void SimplifyPolygons(const Paths &in_polys, Paths &out_polys, PolyFillType fillType) { Clipper c; c.StrictlySimple(true); c.AddPaths(in_polys, ptSubject, true); c.Execute(ctUnion, out_polys, fillType, fillType); } //------------------------------------------------------------------------------ void SimplifyPolygons(Paths &polys, PolyFillType fillType) { SimplifyPolygons(polys, polys, fillType); } //------------------------------------------------------------------------------ inline double DistanceSqrd(const IntPoint& pt1, const IntPoint& pt2) { double Dx = ((double)pt1.X - pt2.X); double dy = ((double)pt1.Y - pt2.Y); return (Dx*Dx + dy*dy); } //------------------------------------------------------------------------------ double DistanceFromLineSqrd( const IntPoint& pt, const IntPoint& ln1, const IntPoint& ln2) { //The equation of a line in general form (Ax + By + C = 0) //given 2 points (x,y) & (x,y) is ... //(y - y)x + (x - x)y + (y - y)x - (x - x)y = 0 //A = (y - y); B = (x - x); C = (y - y)x - (x - x)y //perpendicular distance of point (x,y) = (Ax + By + C)/Sqrt(A + B) //see http://en.wikipedia.org/wiki/Perpendicular_distance double A = double(ln1.Y - ln2.Y); double B = double(ln2.X - ln1.X); double C = A * ln1.X + B * ln1.Y; C = A * pt.X + B * pt.Y - C; return (C * C) / (A * A + B * B); } //--------------------------------------------------------------------------- bool SlopesNearCollinear(const IntPoint& pt1, const IntPoint& pt2, const IntPoint& pt3, double distSqrd) { //this function is more accurate when the point that's geometrically //between the other 2 points is the one that's tested for distance. //ie makes it more likely to pick up 'spikes' ... if (Abs(pt1.X - pt2.X) > Abs(pt1.Y - pt2.Y)) { if ((pt1.X > pt2.X) == (pt1.X < pt3.X)) return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; else if ((pt2.X > pt1.X) == (pt2.X < pt3.X)) return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; else return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; } else { if ((pt1.Y > pt2.Y) == (pt1.Y < pt3.Y)) return DistanceFromLineSqrd(pt1, pt2, pt3) < distSqrd; else if ((pt2.Y > pt1.Y) == (pt2.Y < pt3.Y)) return DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; else return DistanceFromLineSqrd(pt3, pt1, pt2) < distSqrd; } } //------------------------------------------------------------------------------ bool PointsAreClose(IntPoint pt1, IntPoint pt2, double distSqrd) { double Dx = (double)pt1.X - pt2.X; double dy = (double)pt1.Y - pt2.Y; return ((Dx * Dx) + (dy * dy) <= distSqrd); } //------------------------------------------------------------------------------ OutPt* ExcludeOp(OutPt* op) { OutPt* result = op->Prev; result->Next = op->Next; op->Next->Prev = result; result->Idx = 0; return result; } //------------------------------------------------------------------------------ void CleanPolygon(const Path& in_poly, Path& out_poly, double distance) { //distance = proximity in units/pixels below which vertices //will be stripped. Default ~= sqrt(2). size_t size = in_poly.size(); if (size == 0) { out_poly.clear(); return; } OutPt* outPts = new OutPt[size]; for (size_t i = 0; i < size; ++i) { outPts[i].Pt = in_poly[i]; outPts[i].Next = &outPts[(i + 1) % size]; outPts[i].Next->Prev = &outPts[i]; outPts[i].Idx = 0; } double distSqrd = distance * distance; OutPt* op = &outPts[0]; while (op->Idx == 0 && op->Next != op->Prev) { if (PointsAreClose(op->Pt, op->Prev->Pt, distSqrd)) { op = ExcludeOp(op); size--; } else if (PointsAreClose(op->Prev->Pt, op->Next->Pt, distSqrd)) { ExcludeOp(op->Next); op = ExcludeOp(op); size -= 2; } else if (SlopesNearCollinear(op->Prev->Pt, op->Pt, op->Next->Pt, distSqrd)) { op = ExcludeOp(op); size--; } else { op->Idx = 1; op = op->Next; } } if (size < 3) size = 0; out_poly.resize(size); for (size_t i = 0; i < size; ++i) { out_poly[i] = op->Pt; op = op->Next; } delete [] outPts; } //------------------------------------------------------------------------------ void CleanPolygon(Path& poly, double distance) { CleanPolygon(poly, poly, distance); } //------------------------------------------------------------------------------ void CleanPolygons(const Paths& in_polys, Paths& out_polys, double distance) { out_polys.resize(in_polys.size()); for (Paths::size_type i = 0; i < in_polys.size(); ++i) CleanPolygon(in_polys[i], out_polys[i], distance); } //------------------------------------------------------------------------------ void CleanPolygons(Paths& polys, double distance) { CleanPolygons(polys, polys, distance); } //------------------------------------------------------------------------------ void Minkowski(const Path& poly, const Path& path, Paths& solution, bool isSum, bool isClosed) { int delta = (isClosed ? 1 : 0); size_t polyCnt = poly.size(); size_t pathCnt = path.size(); Paths pp; pp.reserve(pathCnt); if (isSum) for (size_t i = 0; i < pathCnt; ++i) { Path p; p.reserve(polyCnt); for (size_t j = 0; j < poly.size(); ++j) p.push_back(IntPoint(path[i].X + poly[j].X, path[i].Y + poly[j].Y)); pp.push_back(p); } else for (size_t i = 0; i < pathCnt; ++i) { Path p; p.reserve(polyCnt); for (size_t j = 0; j < poly.size(); ++j) p.push_back(IntPoint(path[i].X - poly[j].X, path[i].Y - poly[j].Y)); pp.push_back(p); } solution.clear(); solution.reserve((pathCnt + delta) * (polyCnt + 1)); for (size_t i = 0; i < pathCnt - 1 + delta; ++i) for (size_t j = 0; j < polyCnt; ++j) { Path quad; quad.reserve(4); quad.push_back(pp[i % pathCnt][j % polyCnt]); quad.push_back(pp[(i + 1) % pathCnt][j % polyCnt]); quad.push_back(pp[(i + 1) % pathCnt][(j + 1) % polyCnt]); quad.push_back(pp[i % pathCnt][(j + 1) % polyCnt]); if (!Orientation(quad)) ReversePath(quad); solution.push_back(quad); } } //------------------------------------------------------------------------------ void MinkowskiSum(const Path& pattern, const Path& path, Paths& solution, bool pathIsClosed) { Minkowski(pattern, path, solution, true, pathIsClosed); Clipper c; c.AddPaths(solution, ptSubject, true); c.Execute(ctUnion, solution, pftNonZero, pftNonZero); } //------------------------------------------------------------------------------ void TranslatePath(const Path& input, Path& output, const IntPoint delta) { //precondition: input != output output.resize(input.size()); for (size_t i = 0; i < input.size(); ++i) output[i] = IntPoint(input[i].X + delta.X, input[i].Y + delta.Y); } //------------------------------------------------------------------------------ void MinkowskiSum(const Path& pattern, const Paths& paths, Paths& solution, bool pathIsClosed) { Clipper c; for (size_t i = 0; i < paths.size(); ++i) { Paths tmp; Minkowski(pattern, paths[i], tmp, true, pathIsClosed); c.AddPaths(tmp, ptSubject, true); if (pathIsClosed) { Path tmp2; TranslatePath(paths[i], tmp2, pattern[0]); c.AddPath(tmp2, ptClip, true); } } c.Execute(ctUnion, solution, pftNonZero, pftNonZero); } //------------------------------------------------------------------------------ void MinkowskiDiff(const Path& poly1, const Path& poly2, Paths& solution) { Minkowski(poly1, poly2, solution, false, true); Clipper c; c.AddPaths(solution, ptSubject, true); c.Execute(ctUnion, solution, pftNonZero, pftNonZero); } //------------------------------------------------------------------------------ enum NodeType {ntAny, ntOpen, ntClosed}; void AddPolyNodeToPaths(const PolyNode& polynode, NodeType nodetype, Paths& paths) { bool match = true; if (nodetype == ntClosed) match = !polynode.IsOpen(); else if (nodetype == ntOpen) return; if (!polynode.Contour.empty() && match) paths.push_back(polynode.Contour); for (int i = 0; i < polynode.ChildCount(); ++i) AddPolyNodeToPaths(*polynode.Childs[i], nodetype, paths); } //------------------------------------------------------------------------------ void PolyTreeToPaths(const PolyTree& polytree, Paths& paths) { paths.resize(0); paths.reserve(polytree.Total()); AddPolyNodeToPaths(polytree, ntAny, paths); } //------------------------------------------------------------------------------ void ClosedPathsFromPolyTree(const PolyTree& polytree, Paths& paths) { paths.resize(0); paths.reserve(polytree.Total()); AddPolyNodeToPaths(polytree, ntClosed, paths); } //------------------------------------------------------------------------------ void OpenPathsFromPolyTree(PolyTree& polytree, Paths& paths) { paths.resize(0); paths.reserve(polytree.Total()); //Open paths are top level only, so ... for (int i = 0; i < polytree.ChildCount(); ++i) if (polytree.Childs[i]->IsOpen()) paths.push_back(polytree.Childs[i]->Contour); } //------------------------------------------------------------------------------ std::ostream& operator <<(std::ostream &s, const IntPoint &p) { s << "(" << p.X << "," << p.Y << ")"; return s; } //------------------------------------------------------------------------------ std::ostream& operator <<(std::ostream &s, const Path &p) { if (p.empty()) return s; Path::size_type last = p.size() -1; for (Path::size_type i = 0; i < last; i++) s << "(" << p[i].X << "," << p[i].Y << "), "; s << "(" << p[last].X << "," << p[last].Y << ")\n"; return s; } //------------------------------------------------------------------------------ std::ostream& operator <<(std::ostream &s, const Paths &p) { for (Paths::size_type i = 0; i < p.size(); i++) s << p[i]; s << "\n"; return s; } //------------------------------------------------------------------------------ /********************************************************************** * GDSPY additions below this comment * **********************************************************************/ short parse_polygon(PyObject *py_polygon, Path &path, double scaling, bool check_orientation) { /* Parse a complete polygon */ PyObject *py_point, *py_coord; long num_points = PySequence_Length(py_polygon); cInt orientation = 0; if (!PySequence_Check(py_polygon)) { Py_DECREF(py_polygon); PyErr_SetString(PyExc_TypeError, "Polygon must be a sequence."); return -1; } path.resize(num_points); for (long j = 0; j < num_points; ++j) { if ((py_point = PySequence_ITEM(py_polygon, j)) == NULL) { Py_DECREF(py_polygon); return -1; } if ((py_coord = PySequence_GetItem(py_point, 0)) == NULL) { Py_DECREF(py_point); Py_DECREF(py_polygon); return -1; } double x = PyFloat_AsDouble(py_coord); Py_DECREF(py_coord); if ((py_coord = PySequence_GetItem(py_point, 1)) == NULL) { Py_DECREF(py_point); Py_DECREF(py_polygon); return -1; } double y = PyFloat_AsDouble(py_coord); Py_DECREF(py_coord); Py_DECREF(py_point); path[j].X = Round(scaling * x); path[j].Y = Round(scaling * y); #ifdef DEBUG std::cout << path[j].X << "," << path[j].Y << std::endl; #endif //DEBUG if (check_orientation == true && j > 1) orientation += (path[0].X - path[j].X) * (path[j-1].Y - path[0].Y) - (path[0].Y - path[j].Y) * (path[j-1].X - path[0].X); } if (check_orientation == true && orientation < 0) { reverse(path.begin(), path.end()); #ifdef DEBUG std::cout << "Reversed" << std::endl; #endif //DEBUG } return 0; } short parse_polygon_set(PyObject *polyset, Paths &paths, double scaling, bool check_orientation) { PyObject *py_polygon; long num = PySequence_Length(polyset); paths.resize(num); for (long i = 0; i < num; ++i) { #ifdef DEBUG std::cout << std::endl << "Polygon " << i << std::endl; #endif //DEBUG if ((py_polygon = PySequence_ITEM(polyset, i)) == NULL) { return -1; } if (parse_polygon(py_polygon, paths[i], scaling, check_orientation) != 0) { Py_DECREF(py_polygon); return -1; } Py_DECREF(py_polygon); } return 0; } //------------------------------------------------------------------------------ inline bool point_compare(IntPoint &p1, IntPoint &p2) { return p1.X < p2.X; } //------------------------------------------------------------------------------ bool path_compare(Path &p1, Path &p2) { Path::iterator pt1 = min_element(p1.begin(), p1.end(), point_compare); Path::iterator pt2 = min_element(p2.begin(), p2.end(), point_compare); return point_compare(*pt1, *pt2); } //------------------------------------------------------------------------------ void link_holes(PolyNode *node, Paths &out) { Path result = node->Contour; Paths holes(node->ChildCount()); Paths unsorted(0); unsorted.reserve(node->ChildCount()); int size = result.size(); for (PolyNodes::iterator child = node->Childs.begin(); child != node->Childs.end(); ++child) { size += (*child)->Contour.size() + 3; unsorted.push_back((*child)->Contour); } result.reserve(size); // sort holes by smallest x-coordinate partial_sort_copy(unsorted.begin(), unsorted.end(), holes.begin(), holes.end(), path_compare); // insert holes in order for (Paths::iterator h = holes.begin(); h != holes.end(); ++h) { // holes are guaranteed to be oriented opposite to their parent Path::iterator p = min_element(h->begin(), h->end(), point_compare); Path::iterator p1 = result.end(); Path::iterator pprev = --result.end(); Path::iterator pnext = result.begin(); cInt xnew = 0; for (; pnext != result.end(); pprev = pnext++) { if ((pnext->Y <= p->Y && p->Y < pprev->Y) || (pprev->Y < p->Y && p->Y <= pnext->Y)) { cInt x = pnext->X + ((pprev->X - pnext->X) * (p->Y - pnext->Y)) / (pprev->Y - pnext->Y); if ((x > xnew || p1 == result.end()) && x <= p->X) { xnew = x; p1 = pnext; } } } IntPoint pnew(xnew, p->Y); if (pnew.X != p1->X || pnew.Y != p1->Y) result.insert(p1, pnew); result.insert(p1, h->begin(), p+1); result.insert(p1, p, h->end()); result.insert(p1, pnew); } out.push_back(result); } //------------------------------------------------------------------------------ void tree2paths(PolyTree &tree, Paths &out) { PolyNode *node = tree.GetFirst(); // Rough estimate for the number of polygons out.reserve(tree.ChildCount()); #ifdef DEBUG std::cout << std::endl << "Output tree" << std::endl; #endif //DEBUG while (node) { if (!node->IsHole()) { if (node->ChildCount() > 0) { #ifdef DEBUG std::cout << "Hole" << std::endl; #endif //DEBUG link_holes(node, out); } else { #ifdef DEBUG std::cout << "Polygon" << std::endl; #endif //DEBUG out.push_back(node->Contour); } } node = node->GetNext(); } } //------------------------------------------------------------------------------ PyObject* build_polygon_tuple(Paths &polygons, double scaling) { PyObject *result; if ((result = PyTuple_New(polygons.size())) == NULL) return NULL; for (Paths::size_type i = 0; i < polygons.size(); ++i) { #ifdef DEBUG std::cout << std::endl << "Result " << i << std::endl; #endif //DEBUG Path poly = polygons[i]; PyObject *polyt = PyTuple_New(poly.size()); if (polyt == NULL) { Py_DECREF(result); return NULL; } for (Path::size_type j = 0; j < poly.size(); ++j) { PyObject *pt = PyTuple_New(2); PyObject *x = PyFloat_FromDouble(poly[j].X / scaling); PyObject *y = PyFloat_FromDouble(poly[j].Y / scaling); #ifdef DEBUG std::cout << poly[j].X << "," << poly[j].Y << std::endl; #endif //DEBUG if (pt == NULL || x == NULL || y == NULL) { Py_DECREF(result); Py_DECREF(polyt); Py_XDECREF(pt); Py_XDECREF(x); Py_XDECREF(y); return NULL; } PyTuple_SET_ITEM(pt, 0, x); PyTuple_SET_ITEM(pt, 1, y); PyTuple_SET_ITEM(polyt, j, pt); } PyTuple_SET_ITEM(result, i, polyt); } return result; } //------------------------------------------------------------------------------ cInt bounding_box(Path& points, cInt* bb) { bb[0] = points[0].X; bb[1] = points[0].X; bb[2] = points[0].Y; bb[3] = points[0].Y; for(Path::iterator it = points.begin(); it != points.end(); ++it) { if (it->X < bb[0]) bb[0] = it->X; if (it->X > bb[1]) bb[1] = it->X; if (it->Y < bb[2]) bb[2] = it->Y; if (it->Y > bb[3]) bb[3] = it->Y; } return (bb[1] - bb[0])*(bb[3] - bb[2]); } //------------------------------------------------------------------------------ extern "C" { static PyObject* clip(PyObject *self, PyObject *args) { PyObject *polyA, *polyB; const char *operation; double scaling; Paths subj, clip, result; PolyTree solution; ClipType oper; Clipper clpr; if (!PyArg_ParseTuple(args, "OOsd:clip", &polyA, &polyB, &operation, &scaling)) return NULL; if (strcmp(operation, "or") == 0) oper = ctUnion; else if (strcmp(operation, "and") == 0) oper = ctIntersection; else if (strcmp(operation, "xor") == 0) oper = ctXor; else if (strcmp(operation, "not") == 0) oper = ctDifference; else { PyErr_SetString(PyExc_TypeError, "Operation must be one of 'or', 'and', 'xor', 'not'."); return NULL; } if (!PySequence_Check(polyA) || !PySequence_Check(polyB)) { PyErr_SetString(PyExc_TypeError, "First and second arguments must be sequences."); return NULL; } if (parse_polygon_set(polyA, subj, scaling, true) != 0) return NULL; if (parse_polygon_set(polyB, clip, scaling, true) != 0) return NULL; clpr.AddPaths(subj, ptSubject, true); clpr.AddPaths(clip, ptClip, true); clpr.Execute(oper, solution, pftNonZero, pftNonZero); tree2paths(solution, result); return build_polygon_tuple(result, scaling); } //------------------------------------------------------------------------------ static PyObject* offset(PyObject *self, PyObject *args) { PyObject *polygons; const char *join; double distance, tolerance, scaling; Paths subj, result; PolyTree solution; JoinType jt; ClipperOffset clprof; unsigned char joinFirst; if (!PyArg_ParseTuple(args, "Odsddb:offset", &polygons, &distance, &join, &tolerance, &scaling, &joinFirst)) return NULL; if (strcmp(join, "bevel") == 0) jt = jtSquare; else if (strcmp(join, "miter") == 0) { jt = jtMiter; clprof.MiterLimit = tolerance; } else if (strcmp(join, "round") == 0) { jt = jtRound; clprof.ArcTolerance = distance * scaling * (1.0 - cos(M_PI/tolerance)); } else { PyErr_SetString(PyExc_TypeError, "Join must be one of 'miter', 'bevel', 'round'."); return NULL; } if (!PySequence_Check(polygons)) { PyErr_SetString(PyExc_TypeError, "First argument must be a sequence."); return NULL; } if (parse_polygon_set(polygons, subj, scaling, true) != 0) return NULL; if (joinFirst > 0) { Paths intermediate; ClipperOffset clprof_join; clprof_join.AddPaths(subj, jtSquare, etClosedPolygon); clprof_join.Execute(intermediate, 0); clprof.AddPaths(intermediate, jt, etClosedPolygon); } else { clprof.AddPaths(subj, jt, etClosedPolygon); } clprof.Execute(solution, distance * scaling); tree2paths(solution, result); return build_polygon_tuple(result, scaling); } //------------------------------------------------------------------------------ static PyObject* inside(PyObject *self, PyObject *args) { double scaling; short short_circuit; Paths groups, polygons; PyObject *pts, *poly, *result; if (!PyArg_ParseTuple(args, "OOhd:inside", &pts, &poly, &short_circuit, &scaling)) return NULL; if (!PySequence_Check(pts) || !PySequence_Check(poly)) { PyErr_SetString(PyExc_TypeError, "First and second arguments must be sequences."); return NULL; } if (parse_polygon_set(pts, groups, scaling, false) != 0) return NULL; if (parse_polygon_set(poly, polygons, scaling, true) != 0) return NULL; unsigned long numgroups = groups.size(); unsigned long numpolygons = polygons.size(); std::vector polygons_bb(numpolygons); std::vector polygons_bb_areas(numpolygons); // Pre-calculate the bounding boxes of the polygons for (unsigned long p = 0; p < numpolygons; ++p) { polygons_bb[p] = (cInt*) malloc(sizeof(cInt) * 4); polygons_bb_areas[p] = bounding_box(polygons[p], polygons_bb[p]); } if (short_circuit == 0) { // No short-circuit unsigned long numpoints = groups[0].size(); cInt all_bb[4]; all_bb[0] = polygons_bb[0][0]; all_bb[1] = polygons_bb[0][1]; all_bb[2] = polygons_bb[0][2]; all_bb[3] = polygons_bb[0][3]; for (unsigned long p = 1; p < numpolygons; ++p) { if (all_bb[0] > polygons_bb[p][0]) all_bb[0] = polygons_bb[p][0]; if (all_bb[1] < polygons_bb[p][1]) all_bb[1] = polygons_bb[p][1]; if (all_bb[2] > polygons_bb[p][2]) all_bb[2] = polygons_bb[p][2]; if (all_bb[3] < polygons_bb[p][3]) all_bb[3] = polygons_bb[p][3]; } result = PyTuple_New(numpoints); if (!result) return NULL; for (unsigned long i = 0; i < numpoints; ++i) { bool in = false; if (groups[0][i].X >= all_bb[0] && groups[0][i].X <= all_bb[1] && groups[0][i].Y >= all_bb[2] && groups[0][i].Y <= all_bb[3]) for (unsigned long p = 0; p < numpolygons && in == false; ++p) if (groups[0][i].X >= polygons_bb[p][0] && groups[0][i].X <= polygons_bb[p][1] && groups[0][i].Y >= polygons_bb[p][2] && groups[0][i].Y <= polygons_bb[p][3]) if (PointInPolygon(groups[0][i], polygons[p]) != 0) in = true; PyTuple_SET_ITEM(result, i, PyBool_FromLong(in)); } } else if (short_circuit > 0) { // Short-circuit: ANY result = PyTuple_New(numgroups); if (!result) return NULL; for (unsigned long j = 0; j < numgroups; ++j) { cInt group_bb[4]; bool in = false; unsigned long numpoints = groups[j].size(); bounding_box(groups[j], group_bb); for (unsigned long p = 0; p < numpolygons && in == false; ++p) if (group_bb[0] <= polygons_bb[p][1] && group_bb[1] >= polygons_bb[p][0] && group_bb[2] <= polygons_bb[p][3] && group_bb[3] >= polygons_bb[p][2]) for (unsigned long i = 0; i < numpoints && in == false; ++i) if (groups[j][i].X >= polygons_bb[p][0] && groups[j][i].X <= polygons_bb[p][1] && groups[j][i].Y >= polygons_bb[p][2] && groups[j][i].Y <= polygons_bb[p][3]) if (PointInPolygon(groups[j][i], polygons[p]) != 0) in = true; PyTuple_SET_ITEM(result, j, PyBool_FromLong(in)); } } else { // Short-circuit: ALL cInt all_bb[4]; all_bb[0] = polygons_bb[0][0]; all_bb[1] = polygons_bb[0][1]; all_bb[2] = polygons_bb[0][2]; all_bb[3] = polygons_bb[0][3]; for (unsigned long p = 1; p < numpolygons; ++p) { if (all_bb[0] > polygons_bb[p][0]) all_bb[0] = polygons_bb[p][0]; if (all_bb[1] < polygons_bb[p][1]) all_bb[1] = polygons_bb[p][1]; if (all_bb[2] > polygons_bb[p][2]) all_bb[2] = polygons_bb[p][2]; if (all_bb[3] < polygons_bb[p][3]) all_bb[3] = polygons_bb[p][3]; } result = PyTuple_New(numgroups); if (!result) return NULL; for (unsigned long j = 0; j < numgroups; ++j) { bool in = true; unsigned long numpoints = groups[j].size(); for (unsigned long i = 0; i < numpoints && in == true; ++i) { bool this_in = false; if (groups[j][i].X >= all_bb[0] && groups[j][i].X <= all_bb[1] && groups[j][i].Y >= all_bb[2] && groups[j][i].Y <= all_bb[3]) for (unsigned long p = 0; p < numpolygons && this_in == false; ++p) if (groups[j][i].X >= polygons_bb[p][0] && groups[j][i].X <= polygons_bb[p][1] && groups[j][i].Y >= polygons_bb[p][2] && groups[j][i].Y <= polygons_bb[p][3]) if (PointInPolygon(groups[j][i], polygons[p]) != 0) this_in = true; in = this_in; } PyTuple_SET_ITEM(result, j, PyBool_FromLong(in)); } } for (unsigned long p = 0; p < numpolygons; ++p) free(polygons_bb[p]); return result; } static PyObject* chop(PyObject *self, PyObject *args) { PyObject *polygon, *positions; unsigned char axis; double scaling; PyObject *resultitem, *returntuple, *position; Paths result; Paths subj(1); Paths clip(1, Path(4)); PolyTree solution; Clipper clpr; cInt bb[4]; cInt pos; long num_cuts; if (!PyArg_ParseTuple(args, "OOBd:_chop", &polygon, &positions, &axis, &scaling)) return NULL; if (parse_polygon(polygon, subj[0], scaling, true) != 0) return NULL; bounding_box(subj[0], bb); clip[0][0].X = clip[0][3].X = bb[0]; clip[0][1].X = clip[0][2].X = bb[1]; clip[0][0].Y = clip[0][1].Y = bb[2]; clip[0][2].Y = clip[0][3].Y = bb[3]; if (!PySequence_Check(positions)) { PyErr_SetString(PyExc_TypeError, "Positions must be a sequence."); return NULL; } num_cuts = PySequence_Length(positions); if ((returntuple = PyTuple_New(num_cuts + 1)) == NULL) return NULL; pos = axis == 0 ? bb[0] : bb[2]; for (long i = 0; i <= num_cuts; ++i) { if (axis == 0) { clip[0][0].X = clip[0][3].X = pos; if (i < num_cuts) { position = PySequence_ITEM(positions, i); pos = Round(scaling * PyFloat_AsDouble(position)); Py_DECREF(position); if (PyErr_Occurred() != NULL) { PyErr_SetString(PyExc_TypeError, "Positions must be a sequence of numbers."); Py_DECREF(returntuple); return NULL; } } else { pos = bb[1]; } clip[0][1].X = clip[0][2].X = pos; } else { clip[0][0].Y = clip[0][1].Y = pos; if (i < num_cuts) { position = PySequence_ITEM(positions, i); pos = Round(scaling * PyFloat_AsDouble(position)); Py_DECREF(position); if (PyErr_Occurred() != NULL) { PyErr_SetString(PyExc_TypeError, "Positions must be a sequence of numbers."); Py_DECREF(returntuple); return NULL; } } else { pos = bb[3]; } clip[0][2].Y = clip[0][3].Y = pos; } clpr.Clear(); clpr.AddPaths(subj, ptSubject, true); clpr.AddPaths(clip, ptClip, true); clpr.Execute(ctIntersection, solution, pftNonZero, pftNonZero); result.clear(); tree2paths(solution, result); if ((resultitem = build_polygon_tuple(result, scaling)) == NULL) { Py_DECREF(returntuple); return NULL; } PyTuple_SET_ITEM(returntuple, i, resultitem); } return returntuple; } } // extern "C" static const char doc[] = "\ Clipper is a Python C++ extension based on the clipper library by Angus\n\ Johnson (http://www.angusj.com). It implements polygon clipping and\n\ offsetting and it is used for boolean operations in the context of\n\ *gdspy*."; static PyMethodDef clipperMethods[] = { {"clip", clip, METH_VARARGS,\ "Perform a boolean operation (clipping) between 2 sets of polygons.\n\n\ Parameters\n\ ----------\n\ polyA : list of array-like[N][2]\n\ List of subject polygons. Each polygon is an array-like[N][2]\n\ object with the coordinates of the vertices of the polygon.\n\ polyB : list of array-like[N][2]\n\ List of clip polygons. Each polygon is an array-like[N][2]\n\ object with the coordinates of the vertices of the polygon.\n\ operation : {'or', 'and', 'xor', 'not'}\n\ Boolean operation to be executed. The 'not' operation returns\n\ the difference ``polyA - polyB``.\n\n\ scaling : float\n\ Because *clipper* uses integer coordinates internally, it is\n\ useful to scale polygon coordinates before any operation and\n\ rescale the result back to the original size. For example, a\n\ value of 100 will preserve the first 2 decimal places of all\n\ coordinates.\n\ Returns\n\ -------\n\ out : list of array-like[N][2]\n\ List of polygons resulting from the boolean operation."}, {"offset", offset, METH_VARARGS,\ "Offset (inflate or deflate) a set of polygons.\n\n\ Parameters\n\ ----------\n\ polygons : list of array-like[N][2]\n\ List of subject polygons. Each polygon is an array-like[N][2]\n\ object with the coordinates of the vertices of the polygon.\n\ distance : number\n\ Offset distance. Positive to expand, negative to shrink.\n\ join : {'miter', 'bevel', 'round'}\n\ Type of join used to create the offset polygon.\n\ tolerance : number\n\ For miter joints, this number must be at least 2 and it\n\ represents the maximum distance in multiples of offset between\n\ new vertices and their original position before squaring to avoid\n\ spikes at acute joints. For round joints it indicates the\n\ curvature resolution in number of points per full circle.\n\ scaling : float\n\ Because *clipper* uses integer coordinates internally, it is\n\ useful to scale polygon coordinates before any operation and\n\ rescale the result back to the original size. For example, a\n\ value of 100 will preserve the first 2 decimal places of all\n\ coordinates.\n\ joinFirst : bool\n\ Join all paths before offsetting to avoid unnecessary joins in\n\ adjacent polygon sides.\n\ Returns\n\ -------\n\ out : list of array-like[N][2]\n\ List of polygons resulting from the offset operation."}, {"inside", inside, METH_VARARGS,\ "Perform a point inside polygons test between each point in a set of\n\ vertices and a set of polygons.\n\n\ Parameters\n\ ----------\n\ pts : list of array-like[N][2]\n\ List of point groups. Each group is an array-like[N][2] object\n\ with the coordinates of the points.\n\ poly : list of array-like[N][2]\n\ List of polygons. Each polygon is an array-like[N][2] object with\n\ the coordinates of the vertices of the polygon.\n\ short_circuit : integer\n\ If 0, all points are tested. If positive, tests whether any of\n\ the points within each group is inside the polygon set. If\n\ negative, tests whether all of the points within the group are\n\ inside the polygon set.\n\ scaling : float\n\ Because *clipper* uses integer coordinates internally, it is\n\ useful to scale polygon coordinates before any operation and\n\ rescale the result back to the original size. For example, a\n\ value of 100 will preserve the first 2 decimal places of all\n\ coordinates.\n\n\ Returns\n\ -------\n\ out : list\n\ List of booleans indicating if each of the points or point groups\n\ is inside the set of polygons."}, {"_chop", chop, METH_VARARGS,\ "Slice polygon at given positions along an axis.\n\n\ Parameters\n\ ----------\n\ polygon : array-like[N][2]\n\ Coordinates of the vertices of the polygon.\n\ position : list of numbers\n\ Positions to perform the slicing operation along the specified\n\ axis.\n\ axis : 0 or 1\n\ Axis along which the polygon will be sliced.\n\ scaling : float\n\ Because *clipper* uses integer coordinates internally, it is\n\ useful to scale polygon coordinates before any operation and\n\ rescale the result back to the original size. For example, a\n\ value of 100 will preserve the first 2 decimal places of all\n\ coordinates.\n\n\ Returns\n\ -------\n\ out : tuple[2]\n\ Each element is a list of polygons (array-like[N][2]). The first\n\ list contains the polygons left before the slicing position, and\n\ the second, the polygons left after that position."}, {NULL, NULL, 0, NULL} }; #if PY_MAJOR_VERSION >= 3 static struct PyModuleDef clipperModule = { PyModuleDef_HEAD_INIT, "clipper", doc, -1, clipperMethods }; PyMODINIT_FUNC PyInit_clipper(void) { return PyModule_Create(&clipperModule); } #else PyMODINIT_FUNC initclipper(void) { (void) Py_InitModule3("clipper", clipperMethods, doc); } #endif } //ClipperLib namespace /* vim: set shiftwidth=2 softtabstop=2 tabstop=2 expandtab : */ gdspy-1.4.2/gdspy/clipper.hpp000066400000000000000000000362241354474061200161560ustar00rootroot00000000000000/******************************************************************************* * * * Author : Angus Johnson * * Version : 6.4.2 * * Date : 27 February 2017 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2017 * * * * License: * * Use, modification & distribution is subject to Boost Software License Ver 1. * * http://www.boost.org/LICENSE_1_0.txt * * * * Attributions: * * The code in this library is an extension of Bala Vatti's clipping algorithm: * * "A generic solution to polygon clipping" * * Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * * http://portal.acm.org/citation.cfm?id=129906 * * * * Computer graphics and geometric modeling: implementation and algorithms * * By Max K. Agoston * * Springer; 1 edition (January 4, 2005) * * http://books.google.com/books?q=vatti+clipping+agoston * * * * See also: * * "Polygon Offsetting by Computing Winding Numbers" * * Paper no. DETC2005-85513 pp. 565-575 * * ASME 2005 International Design Engineering Technical Conferences * * and Computers and Information in Engineering Conference (IDETC/CIE2005) * * September 24-28, 2005 , Long Beach, California, USA * * http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * * * *******************************************************************************/ #ifndef clipper_hpp #define clipper_hpp #define CLIPPER_VERSION "6.4.2" //use_int32: When enabled 32bit ints are used instead of 64bit ints. This //improve performance but coordinate values are limited to the range +/- 46340 //#define use_int32 //use_xyz: adds a Z member to IntPoint. Adds a minor cost to perfomance. //#define use_xyz //use_lines: Enables line clipping. Adds a very minor cost to performance. #define use_lines //use_deprecated: Enables temporary support for the obsolete functions //#define use_deprecated // Begin of GDSPY additions #define _USE_MATH_DEFINES // End of GDSPY additions #include #include #include #include #include #include #include #include #include namespace ClipperLib { enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor }; enum PolyType { ptSubject, ptClip }; //By far the most widely used winding rules for polygon filling are //EvenOdd & NonZero (GDI, GDI+, XLib, OpenGL, Cairo, AGG, Quartz, SVG, Gr32) //Others rules include Positive, Negative and ABS_GTR_EQ_TWO (only in OpenGL) //see http://glprogramming.com/red/chapter11.html enum PolyFillType { pftEvenOdd, pftNonZero, pftPositive, pftNegative }; #ifdef use_int32 typedef int cInt; static cInt const loRange = 0x7FFF; static cInt const hiRange = 0x7FFF; #else typedef signed long long cInt; static cInt const loRange = 0x3FFFFFFF; static cInt const hiRange = 0x3FFFFFFFFFFFFFFFLL; typedef signed long long long64; //used by Int128 class typedef unsigned long long ulong64; #endif struct IntPoint { cInt X; cInt Y; #ifdef use_xyz cInt Z; IntPoint(cInt x = 0, cInt y = 0, cInt z = 0): X(x), Y(y), Z(z) {}; #else IntPoint(cInt x = 0, cInt y = 0): X(x), Y(y) {}; #endif friend inline bool operator== (const IntPoint& a, const IntPoint& b) { return a.X == b.X && a.Y == b.Y; } friend inline bool operator!= (const IntPoint& a, const IntPoint& b) { return a.X != b.X || a.Y != b.Y; } }; //------------------------------------------------------------------------------ typedef std::vector< IntPoint > Path; typedef std::vector< Path > Paths; inline Path& operator <<(Path& poly, const IntPoint& p) {poly.push_back(p); return poly;} inline Paths& operator <<(Paths& polys, const Path& p) {polys.push_back(p); return polys;} std::ostream& operator <<(std::ostream &s, const IntPoint &p); std::ostream& operator <<(std::ostream &s, const Path &p); std::ostream& operator <<(std::ostream &s, const Paths &p); struct DoublePoint { double X; double Y; DoublePoint(double x = 0, double y = 0) : X(x), Y(y) {} DoublePoint(IntPoint ip) : X((double)ip.X), Y((double)ip.Y) {} }; //------------------------------------------------------------------------------ #ifdef use_xyz typedef void (*ZFillCallback)(IntPoint& e1bot, IntPoint& e1top, IntPoint& e2bot, IntPoint& e2top, IntPoint& pt); #endif enum InitOptions {ioReverseSolution = 1, ioStrictlySimple = 2, ioPreserveCollinear = 4}; enum JoinType {jtSquare, jtRound, jtMiter}; enum EndType {etClosedPolygon, etClosedLine, etOpenButt, etOpenSquare, etOpenRound}; class PolyNode; typedef std::vector< PolyNode* > PolyNodes; class PolyNode { public: PolyNode(); virtual ~PolyNode(){}; Path Contour; PolyNodes Childs; PolyNode* Parent; PolyNode* GetNext() const; bool IsHole() const; bool IsOpen() const; int ChildCount() const; private: //PolyNode& operator =(PolyNode& other); unsigned Index; //node index in Parent.Childs bool m_IsOpen; JoinType m_jointype; EndType m_endtype; PolyNode* GetNextSiblingUp() const; void AddChild(PolyNode& child); friend class Clipper; //to access Index friend class ClipperOffset; }; class PolyTree: public PolyNode { public: ~PolyTree(){ Clear(); }; PolyNode* GetFirst() const; void Clear(); int Total() const; private: //PolyTree& operator =(PolyTree& other); PolyNodes AllNodes; friend class Clipper; //to access AllNodes }; bool Orientation(const Path &poly); double Area(const Path &poly); int PointInPolygon(const IntPoint &pt, const Path &path); void SimplifyPolygon(const Path &in_poly, Paths &out_polys, PolyFillType fillType = pftEvenOdd); void SimplifyPolygons(const Paths &in_polys, Paths &out_polys, PolyFillType fillType = pftEvenOdd); void SimplifyPolygons(Paths &polys, PolyFillType fillType = pftEvenOdd); void CleanPolygon(const Path& in_poly, Path& out_poly, double distance = 1.415); void CleanPolygon(Path& poly, double distance = 1.415); void CleanPolygons(const Paths& in_polys, Paths& out_polys, double distance = 1.415); void CleanPolygons(Paths& polys, double distance = 1.415); void MinkowskiSum(const Path& pattern, const Path& path, Paths& solution, bool pathIsClosed); void MinkowskiSum(const Path& pattern, const Paths& paths, Paths& solution, bool pathIsClosed); void MinkowskiDiff(const Path& poly1, const Path& poly2, Paths& solution); void PolyTreeToPaths(const PolyTree& polytree, Paths& paths); void ClosedPathsFromPolyTree(const PolyTree& polytree, Paths& paths); void OpenPathsFromPolyTree(PolyTree& polytree, Paths& paths); void ReversePath(Path& p); void ReversePaths(Paths& p); struct IntRect { cInt left; cInt top; cInt right; cInt bottom; }; //enums that are used internally ... enum EdgeSide { esLeft = 1, esRight = 2}; //forward declarations (for stuff used internally) ... struct TEdge; struct IntersectNode; struct LocalMinimum; struct OutPt; struct OutRec; struct Join; typedef std::vector < OutRec* > PolyOutList; typedef std::vector < TEdge* > EdgeList; typedef std::vector < Join* > JoinList; typedef std::vector < IntersectNode* > IntersectList; //------------------------------------------------------------------------------ //ClipperBase is the ancestor to the Clipper class. It should not be //instantiated directly. This class simply abstracts the conversion of sets of //polygon coordinates into edge objects that are stored in a LocalMinima list. class ClipperBase { public: ClipperBase(); virtual ~ClipperBase(); virtual bool AddPath(const Path &pg, PolyType PolyTyp, bool Closed); bool AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed); virtual void Clear(); IntRect GetBounds(); bool PreserveCollinear() {return m_PreserveCollinear;}; void PreserveCollinear(bool value) {m_PreserveCollinear = value;}; protected: void DisposeLocalMinimaList(); TEdge* AddBoundsToLML(TEdge *e, bool IsClosed); virtual void Reset(); TEdge* ProcessBound(TEdge* E, bool IsClockwise); void InsertScanbeam(const cInt Y); bool PopScanbeam(cInt &Y); bool LocalMinimaPending(); bool PopLocalMinima(cInt Y, const LocalMinimum *&locMin); OutRec* CreateOutRec(); void DisposeAllOutRecs(); void DisposeOutRec(PolyOutList::size_type index); void SwapPositionsInAEL(TEdge *edge1, TEdge *edge2); void DeleteFromAEL(TEdge *e); void UpdateEdgeIntoAEL(TEdge *&e); typedef std::vector MinimaList; MinimaList::iterator m_CurrentLM; MinimaList m_MinimaList; bool m_UseFullRange; EdgeList m_edges; bool m_PreserveCollinear; bool m_HasOpenPaths; PolyOutList m_PolyOuts; TEdge *m_ActiveEdges; typedef std::priority_queue ScanbeamList; ScanbeamList m_Scanbeam; }; //------------------------------------------------------------------------------ class Clipper : public virtual ClipperBase { public: Clipper(int initOptions = 0); bool Execute(ClipType clipType, Paths &solution, PolyFillType fillType = pftEvenOdd); bool Execute(ClipType clipType, Paths &solution, PolyFillType subjFillType, PolyFillType clipFillType); bool Execute(ClipType clipType, PolyTree &polytree, PolyFillType fillType = pftEvenOdd); bool Execute(ClipType clipType, PolyTree &polytree, PolyFillType subjFillType, PolyFillType clipFillType); bool ReverseSolution() { return m_ReverseOutput; }; void ReverseSolution(bool value) {m_ReverseOutput = value;}; bool StrictlySimple() {return m_StrictSimple;}; void StrictlySimple(bool value) {m_StrictSimple = value;}; //set the callback function for z value filling on intersections (otherwise Z is 0) #ifdef use_xyz void ZFillFunction(ZFillCallback zFillFunc); #endif protected: virtual bool ExecuteInternal(); private: JoinList m_Joins; JoinList m_GhostJoins; IntersectList m_IntersectList; ClipType m_ClipType; typedef std::list MaximaList; MaximaList m_Maxima; TEdge *m_SortedEdges; bool m_ExecuteLocked; PolyFillType m_ClipFillType; PolyFillType m_SubjFillType; bool m_ReverseOutput; bool m_UsingPolyTree; bool m_StrictSimple; #ifdef use_xyz ZFillCallback m_ZFill; //custom callback #endif void SetWindingCount(TEdge& edge); bool IsEvenOddFillType(const TEdge& edge) const; bool IsEvenOddAltFillType(const TEdge& edge) const; void InsertLocalMinimaIntoAEL(const cInt botY); void InsertEdgeIntoAEL(TEdge *edge, TEdge* startEdge); void AddEdgeToSEL(TEdge *edge); bool PopEdgeFromSEL(TEdge *&edge); void CopyAELToSEL(); void DeleteFromSEL(TEdge *e); void SwapPositionsInSEL(TEdge *edge1, TEdge *edge2); bool IsContributing(const TEdge& edge) const; bool IsTopHorz(const cInt XPos); void DoMaxima(TEdge *e); void ProcessHorizontals(); void ProcessHorizontal(TEdge *horzEdge); void AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); OutPt* AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); OutRec* GetOutRec(int idx); void AppendPolygon(TEdge *e1, TEdge *e2); void IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &pt); OutPt* AddOutPt(TEdge *e, const IntPoint &pt); OutPt* GetLastOutPt(TEdge *e); bool ProcessIntersections(const cInt topY); void BuildIntersectList(const cInt topY); void ProcessIntersectList(); void ProcessEdgesAtTopOfScanbeam(const cInt topY); void BuildResult(Paths& polys); void BuildResult2(PolyTree& polytree); void SetHoleState(TEdge *e, OutRec *outrec); void DisposeIntersectNodes(); bool FixupIntersectionOrder(); void FixupOutPolygon(OutRec &outrec); void FixupOutPolyline(OutRec &outrec); bool IsHole(TEdge *e); bool FindOwnerFromSplitRecs(OutRec &outRec, OutRec *&currOrfl); void FixHoleLinkage(OutRec &outrec); void AddJoin(OutPt *op1, OutPt *op2, const IntPoint offPt); void ClearJoins(); void ClearGhostJoins(); void AddGhostJoin(OutPt *op, const IntPoint offPt); bool JoinPoints(Join *j, OutRec* outRec1, OutRec* outRec2); void JoinCommonEdges(); void DoSimplePolygons(); void FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec); void FixupFirstLefts2(OutRec* InnerOutRec, OutRec* OuterOutRec); void FixupFirstLefts3(OutRec* OldOutRec, OutRec* NewOutRec); #ifdef use_xyz void SetZ(IntPoint& pt, TEdge& e1, TEdge& e2); #endif }; //------------------------------------------------------------------------------ class ClipperOffset { public: ClipperOffset(double miterLimit = 2.0, double roundPrecision = 0.25); ~ClipperOffset(); void AddPath(const Path& path, JoinType joinType, EndType endType); void AddPaths(const Paths& paths, JoinType joinType, EndType endType); void Execute(Paths& solution, double delta); void Execute(PolyTree& solution, double delta); void Clear(); double MiterLimit; double ArcTolerance; private: Paths m_destPolys; Path m_srcPoly; Path m_destPoly; std::vector m_normals; double m_delta, m_sinA, m_sin, m_cos; double m_miterLim, m_StepsPerRad; IntPoint m_lowest; PolyNode m_polyNodes; void FixOrientations(); void DoOffset(double delta); void OffsetPoint(int j, int& k, JoinType jointype); void DoSquare(int j, int k); void DoMiter(int j, int k, double r); void DoRound(int j, int k); }; //------------------------------------------------------------------------------ class clipperException : public std::exception { public: clipperException(const char* description): m_descr(description) {} virtual ~clipperException() throw() {} virtual const char* what() const throw() {return m_descr.c_str();} private: std::string m_descr; }; //------------------------------------------------------------------------------ } //ClipperLib namespace #endif //clipper_hpp gdspy-1.4.2/gdspy/curve.py000066400000000000000000000741501354474061200155050ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### """ Curve class. """ import numpy from gdspy import _func_bezier, _hobby, _zero class Curve(object): """ Generation of curves loosely based on SVG paths. Short summary of available methods: ====== ============================= Method Primitive ====== ============================= L/l Line segments H/h Horizontal line segments V/v Vertical line segments C/c Cubic Bezier curve S/s Smooth cubic Bezier curve Q/q Quadratic Bezier curve T/t Smooth quadratic Bezier curve B/b General degree Bezier curve I/i Smooth interpolating curve arc Elliptical arc ====== ============================= The uppercase version of the methods considers that all coordinates are absolute, whereas the lowercase considers that they are relative to the current end point of the curve. Parameters ---------- x : number X-coordinate of the starting point of the curve. If this is a complex number, the value of `y` is ignored and the starting point becomes ``(x.real, x.imag)``. y : number Y-coordinate of the starting point of the curve. tolerance : number Tolerance used to calculate a polygonal approximation to the curve. Notes ----- In all methods of this class that accept coordinate pairs, a single complex number can be passed to be split into its real and imaginary parts. This feature can be useful in expressing coordinates in polar form. All commands follow the SVG 2 specification, except for elliptical arcs and smooth interpolating curves, which are inspired by the Metapost syntax. Examples -------- >>> curve = gdspy.Curve(3, 4).H(1).q(0.5, 1, 2j).L(2 + 3j, 2, 2) >>> pol = gdspy.Polygon(curve.get_points()) """ __slots__ = "points", "tol", "last_c", "last_q" def __init__(self, x, y=0, tolerance=0.01): self.last_c = self.last_q = None self.tol = tolerance ** 2 if isinstance(x, complex): self.points = [numpy.array((x.real, x.imag))] else: self.points = [numpy.array((x, y))] def get_points(self): """ Get the polygonal points that approximate this curve. Returns ------- out : Numpy array[N, 2] Vertices of the polygon. """ delta = (self.points[-1] - self.points[0]) ** 2 if delta[0] + delta[1] < self.tol: return numpy.array(self.points[:-1]) return numpy.array(self.points) def L(self, *xy): """ Add straight line segments to the curve. Parameters ---------- xy : numbers Endpoint coordinates of the line segments. Returns ------- out : `Curve` This curve. """ self.last_c = self.last_q = None i = 0 while i < len(xy): if isinstance(xy[i], complex): self.points.append(numpy.array((xy[i].real, xy[i].imag))) i += 1 else: self.points.append(numpy.array((xy[i], xy[i + 1]))) i += 2 return self def l(self, *xy): """ Add straight line segments to the curve. Parameters ---------- xy : numbers Endpoint coordinates of the line segments relative to the current end point. Returns ------- out : `Curve` This curve. """ self.last_c = self.last_q = None o = self.points[-1] i = 0 while i < len(xy): if isinstance(xy[i], complex): self.points.append(o + numpy.array((xy[i].real, xy[i].imag))) i += 1 else: self.points.append(o + numpy.array((xy[i], xy[i + 1]))) i += 2 return self def H(self, *x): """ Add horizontal line segments to the curve. Parameters ---------- x : numbers Endpoint x-coordinates of the line segments. Returns ------- out : `Curve` This curve. """ self.last_c = self.last_q = None y0 = self.points[-1][1] self.points.extend(numpy.array((xx, y0)) for xx in x) return self def h(self, *x): """ Add horizontal line segments to the curve. Parameters ---------- x : numbers Endpoint x-coordinates of the line segments relative to the current end point. Returns ------- out : `Curve` This curve. """ self.last_c = self.last_q = None x0, y0 = self.points[-1] self.points.extend(numpy.array((x0 + xx, y0)) for xx in x) return self def V(self, *y): """ Add vertical line segments to the curve. Parameters ---------- y : numbers Endpoint y-coordinates of the line segments. Returns ------- out : `Curve` This curve. """ self.last_c = self.last_q = None x0 = self.points[-1][0] self.points.extend(numpy.array((x0, yy)) for yy in y) return self def v(self, *y): """ Add vertical line segments to the curve. Parameters ---------- y : numbers Endpoint y-coordinates of the line segments relative to the current end point. Returns ------- out : `Curve` This curve. """ self.last_c = self.last_q = None x0, y0 = self.points[-1] self.points.extend(numpy.array((x0, y0 + yy)) for yy in y) return self def arc(self, radius, initial_angle, final_angle, rotation=0): """ Add an elliptical arc to the curve. Parameters ---------- radius : number, array-like[2] Arc radius. An elliptical arc can be created by passing an array with 2 radii. initial_angle : number Initial angle of the arc (in *radians*). final_angle : number Final angle of the arc (in *radians*). rotation : number Rotation of the axis of the ellipse. Returns ------- out : `Curve` This curve. """ self.last_c = self.last_q = None if hasattr(radius, "__iter__"): rx, ry = radius radius = max(radius) else: rx = ry = radius full_angle = abs(final_angle - initial_angle) number_of_points = max( 3, 1 + int(0.5 * full_angle / numpy.arccos(1 - self.tol ** 0.5 / radius) + 0.5), ) angles = numpy.linspace( initial_angle - rotation, final_angle - rotation, number_of_points ) pts = numpy.vstack((rx * numpy.cos(angles), ry * numpy.sin(angles))).T if rotation != 0: rot = numpy.empty_like(pts) c = numpy.cos(rotation) s = numpy.sin(rotation) rot[:, 0] = pts[:, 0] * c - pts[:, 1] * s rot[:, 1] = pts[:, 0] * s + pts[:, 1] * c else: rot = pts pts = rot[1:] - rot[0] + self.points[-1] self.points.extend(xy for xy in pts) return self def C(self, *xy): """ Add cubic Bezier curves to the curve. Parameters ---------- xy : numbers Coordinate pairs. Each set of 3 pairs are interpreted as the control point at the beginning of the curve, the control point at the end of the curve and the endpoint of the curve. Returns ------- out : `Curve` This curve. """ self.last_q = None i = 0 while i < len(xy): ctrl = numpy.empty((4, 2)) ctrl[0] = self.points[-1] for j in range(1, 4): if isinstance(xy[i], complex): ctrl[j, 0] = xy[i].real ctrl[j, 1] = xy[i].imag i += 1 else: ctrl[j, 0] = xy[i] ctrl[j, 1] = xy[i + 1] i += 2 f = _func_bezier(ctrl) uu = [0, 0.2, 0.5, 0.8, 1] fu = [f(u) for u in uu] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > self.tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 self.points.extend(xy for xy in fu[1:]) self.last_c = ctrl[2] return self def c(self, *xy): """ Add cubic Bezier curves to the curve. Parameters ---------- xy : numbers Coordinate pairs. Each set of 3 pairs are interpreted as the control point at the beginning of the curve, the control point at the end of the curve and the endpoint of the curve. All coordinates are relative to the current end point. Returns ------- out : `Curve` This curve. """ self.last_q = None x0, y0 = self.points[-1] i = 0 while i < len(xy): ctrl = numpy.empty((4, 2)) ctrl[0, 0] = x0 ctrl[0, 1] = y0 for j in range(1, 4): if isinstance(xy[i], complex): ctrl[j, 0] = x0 + xy[i].real ctrl[j, 1] = y0 + xy[i].imag i += 1 else: ctrl[j, 0] = x0 + xy[i] ctrl[j, 1] = y0 + xy[i + 1] i += 2 f = _func_bezier(ctrl) uu = [0, 0.2, 0.5, 0.8, 1] fu = [f(u) for u in uu] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > self.tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 self.points.extend(xy for xy in fu[1:]) self.last_c = ctrl[2] return self def S(self, *xy): """ Add smooth cubic Bezier curves to the curve. Parameters ---------- xy : numbers Coordinate pairs. Each set of 2 pairs are interpreted as the control point at the end of the curve and the endpoint of the curve. The control point at the beginning of the curve is assumed to be the reflection of the control point at the end of the last curve relative to the starting point of the curve. If the previous curve is not a cubic Bezier, the control point is coincident with the starting point. Returns ------- out : `Curve` This curve. """ self.last_q = None if self.last_c is None: self.last_c = self.points[-1] i = 0 while i < len(xy): ctrl = numpy.empty((4, 2)) ctrl[0] = self.points[-1] ctrl[1] = 2 * ctrl[0] - self.last_c for j in range(2, 4): if isinstance(xy[i], complex): ctrl[j, 0] = xy[i].real ctrl[j, 1] = xy[i].imag i += 1 else: ctrl[j, 0] = xy[i] ctrl[j, 1] = xy[i + 1] i += 2 f = _func_bezier(ctrl) uu = [0, 0.2, 0.5, 0.8, 1] fu = [f(u) for u in uu] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > self.tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 self.points.extend(xy for xy in fu[1:]) self.last_c = ctrl[2] return self def s(self, *xy): """ Add smooth cubic Bezier curves to the curve. Parameters ---------- xy : numbers Coordinate pairs. Each set of 2 pairs are interpreted as the control point at the end of the curve and the endpoint of the curve. The control point at the beginning of the curve is assumed to be the reflection of the control point at the end of the last curve relative to the starting point of the curve. If the previous curve is not a cubic Bezier, the control point is coincident with the starting point. All coordinates are relative to the current end point. Returns ------- out : `Curve` This curve. """ self.last_q = None if self.last_c is None: self.last_c = self.points[-1] x0, y0 = self.points[-1] i = 0 while i < len(xy): ctrl = numpy.empty((4, 2)) ctrl[0, 0] = x0 ctrl[0, 1] = y0 ctrl[1] = 2 * ctrl[0] - self.last_c for j in range(2, 4): if isinstance(xy[i], complex): ctrl[j, 0] = x0 + xy[i].real ctrl[j, 1] = y0 + xy[i].imag i += 1 else: ctrl[j, 0] = x0 + xy[i] ctrl[j, 1] = y0 + xy[i + 1] i += 2 f = _func_bezier(ctrl) uu = [0, 0.2, 0.5, 0.8, 1] fu = [f(u) for u in uu] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > self.tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 self.points.extend(xy for xy in fu[1:]) self.last_c = ctrl[2] return self def Q(self, *xy): """ Add quadratic Bezier curves to the curve. Parameters ---------- xy : numbers Coordinate pairs. Each set of 2 pairs are interpreted as the control point and the endpoint of the curve. Returns ------- out : `Curve` This curve. """ self.last_c = None i = 0 while i < len(xy): ctrl = numpy.empty((3, 2)) ctrl[0] = self.points[-1] for j in range(1, 3): if isinstance(xy[i], complex): ctrl[j, 0] = xy[i].real ctrl[j, 1] = xy[i].imag i += 1 else: ctrl[j, 0] = xy[i] ctrl[j, 1] = xy[i + 1] i += 2 f = _func_bezier(ctrl) uu = [0, 0.2, 0.5, 0.8, 1] fu = [f(u) for u in uu] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > self.tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 self.points.extend(xy for xy in fu[1:]) self.last_q = ctrl[1] return self def q(self, *xy): """ Add quadratic Bezier curves to the curve. Parameters ---------- xy : numbers Coordinate pairs. Each set of 2 pairs are interpreted as the control point and the endpoint of the curve. All coordinates are relative to the current end point. Returns ------- out : `Curve` This curve. """ self.last_c = None x0, y0 = self.points[-1] i = 0 while i < len(xy): ctrl = numpy.empty((3, 2)) ctrl[0, 0] = x0 ctrl[0, 1] = y0 for j in range(1, 3): if isinstance(xy[i], complex): ctrl[j, 0] = x0 + xy[i].real ctrl[j, 1] = y0 + xy[i].imag i += 1 else: ctrl[j, 0] = x0 + xy[i] ctrl[j, 1] = y0 + xy[i + 1] i += 2 f = _func_bezier(ctrl) uu = [0, 0.2, 0.5, 0.8, 1] fu = [f(u) for u in uu] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > self.tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 self.points.extend(xy for xy in fu[1:]) self.last_q = ctrl[1] return self def T(self, *xy): """ Add smooth quadratic Bezier curves to the curve. Parameters ---------- xy : numbers Coordinates of the endpoints of the curves. The control point is assumed to be the reflection of the control point of the last curve relative to the starting point of the curve. If the previous curve is not a quadratic Bezier, the control point is coincident with the starting point. Returns ------- out : `Curve` This curve. """ self.last_c = None if self.last_q is None: self.last_q = self.points[-1] i = 0 while i < len(xy): ctrl = numpy.empty((3, 2)) ctrl[0] = self.points[-1] ctrl[1] = 2 * ctrl[0] - self.last_q if isinstance(xy[i], complex): ctrl[2, 0] = xy[i].real ctrl[2, 1] = xy[i].imag i += 1 else: ctrl[2, 0] = xy[i] ctrl[2, 1] = xy[i + 1] i += 2 f = _func_bezier(ctrl) uu = [0, 0.2, 0.5, 0.8, 1] fu = [f(u) for u in uu] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > self.tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 self.points.extend(xy for xy in fu[1:]) self.last_q = ctrl[1] return self def t(self, *xy): """ Add smooth quadratic Bezier curves to the curve. Parameters ---------- xy : numbers Coordinates of the endpoints of the curves. The control point is assumed to be the reflection of the control point of the last curve relative to the starting point of the curve. If the previous curve is not a quadratic Bezier, the control point is coincident with the starting point. All coordinates are relative to the current end point. Returns ------- out : `Curve` This curve. """ self.last_c = None if self.last_q is None: self.last_q = self.points[-1] x0, y0 = self.points[-1] i = 0 while i < len(xy): ctrl = numpy.empty((3, 2)) ctrl[0, 0] = x0 ctrl[0, 1] = y0 ctrl[1] = 2 * ctrl[0] - self.last_q if isinstance(xy[i], complex): ctrl[2, 0] = x0 + xy[i].real ctrl[2, 1] = y0 + xy[i].imag i += 1 else: ctrl[2, 0] = x0 + xy[i] ctrl[2, 1] = y0 + xy[i + 1] i += 2 f = _func_bezier(ctrl) uu = [0, 0.2, 0.5, 0.8, 1] fu = [f(u) for u in uu] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > self.tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 self.points.extend(xy for xy in fu[1:]) self.last_q = ctrl[1] return self def B(self, *xy): """ Add a general degree Bezier curve. Parameters ---------- xy : numbers Coordinate pairs. The last coordinate is the endpoint of curve and all other are control points. Returns ------- out : `Curve` This curve. """ self.last_c = self.last_q = None i = 0 ctrl = [self.points[-1]] while i < len(xy): if isinstance(xy[i], complex): ctrl.append((xy[i].real, xy[i].imag)) i += 1 else: ctrl.append((xy[i], xy[i + 1])) i += 2 ctrl = numpy.array(ctrl) f = _func_bezier(ctrl) uu = numpy.linspace(-1, 1, ctrl.shape[0] + 1) uu = list(0.5 * (1 + numpy.sign(uu) * numpy.abs(uu) ** 0.8)) fu = [f(u) for u in uu] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > self.tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 self.points.extend(xy for xy in fu[1:]) return self def b(self, *xy): """ Add a general degree Bezier curve. Parameters ---------- xy : numbers Coordinate pairs. The last coordinate is the endpoint of curve and all other are control points. All coordinates are relative to the current end point. Returns ------- out : `Curve` This curve. """ self.last_c = self.last_q = None x0, y0 = self.points[-1] i = 0 ctrl = [self.points[-1]] while i < len(xy): if isinstance(xy[i], complex): ctrl.append((x0 + xy[i].real, y0 + xy[i].imag)) i += 1 else: ctrl.append((x0 + xy[i], y0 + xy[i + 1])) i += 2 ctrl = numpy.array(ctrl) f = _func_bezier(ctrl) uu = numpy.linspace(-1, 1, ctrl.shape[0] + 1) uu = list(0.5 * (1 + numpy.sign(uu) * numpy.abs(uu) ** 0.8)) fu = [f(u) for u in uu] iu = 1 while iu < len(fu): test_u = 0.5 * (uu[iu - 1] + uu[iu]) test_pt = f(test_u) test_err = 0.5 * (fu[iu - 1] + fu[iu]) - test_pt if test_err[0] ** 2 + test_err[1] ** 2 > self.tol: uu.insert(iu, test_u) fu.insert(iu, test_pt) else: iu += 1 self.points.extend(xy for xy in fu[1:]) return self def I( self, points, angles=None, curl_start=1, curl_end=1, t_in=1, t_out=1, cycle=False, ): """ Add a smooth interpolating curve through the given points. Uses the Hobby algorithm [1]_ to calculate a smooth interpolating curve made of cubic Bezier segments between each pair of points. Parameters ---------- points : array-like[N][2] Vertices in the interpolating curve. angles : array-like[N + 1] or None Tangent angles at each point (in *radians*). Any angles defined as None are automatically calculated. curl_start : number Ratio between the mock curvatures at the first point and at its neighbor. A value of 1 renders the first segment a good approximation for a circular arc. A value of 0 will better approximate a straight segment. It has no effect for closed curves or when an angle is defined for the first point. curl_end : number Ratio between the mock curvatures at the last point and at its neighbor. It has no effect for closed curves or when an angle is defined for the first point. t_in : number or array-like[N + 1] Tension parameter when arriving at each point. One value per point or a single value used for all points. t_out : number or array-like[N + 1] Tension parameter when leaving each point. One value per point or a single value used for all points. cycle : bool If True, calculates control points for a closed curve, with an additional segment connecting the first and last points. Returns ------- out : `Curve` This curve. Examples -------- >>> c1 = gdspy.Curve(0, 1).I([(1, 1), (2, 1), (1, 0)]) >>> c2 = gdspy.Curve(0, 2).I([(1, 2), (2, 2), (1, 1)], ... cycle=True) >>> ps = gdspy.PolygonSet([c1.get_points(), c2.get_points()]) References ---------- .. [1] Hobby, J.D. *Discrete Comput. Geom.* (1986) 1: 123. `DOI: 10.1007/BF02187690 `_ """ pts = numpy.vstack((self.points[-1:], points)) cta, ctb = _hobby(pts, angles, curl_start, curl_end, t_in, t_out, cycle) args = [] args.extend( x for i in range(pts.shape[0] - 1) for x in [ cta[i, 0], cta[i, 1], ctb[i, 0], ctb[i, 1], pts[i + 1, 0], pts[i + 1, 1], ] ) if cycle: args.extend( [cta[-1, 0], cta[-1, 1], ctb[-1, 0], ctb[-1, 1], pts[0, 0], pts[0, 1]] ) return self.C(*args) def i( self, points, angles=None, curl_start=1, curl_end=1, t_in=1, t_out=1, cycle=False, ): """ Add a smooth interpolating curve through the given points. Uses the Hobby algorithm [1]_ to calculate a smooth interpolating curve made of cubic Bezier segments between each pair of points. Parameters ---------- points : array-like[N][2] Vertices in the interpolating curve (relative to teh current endpoint). angles : array-like[N + 1] or None Tangent angles at each point (in *radians*). Any angles defined as None are automatically calculated. curl_start : number Ratio between the mock curvatures at the first point and at its neighbor. A value of 1 renders the first segment a good approximation for a circular arc. A value of 0 will better approximate a straight segment. It has no effect for closed curves or when an angle is defined for the first point. curl_end : number Ratio between the mock curvatures at the last point and at its neighbor. It has no effect for closed curves or when an angle is defined for the first point. t_in : number or array-like[N + 1] Tension parameter when arriving at each point. One value per point or a single value used for all points. t_out : number or array-like[N + 1] Tension parameter when leaving each point. One value per point or a single value used for all points. cycle : bool If True, calculates control points for a closed curve, with an additional segment connecting the first and last points. Returns ------- out : `Curve` This curve. Examples -------- >>> c1 = gdspy.Curve(0, 1).i([(1, 0), (2, 0), (1, -1)]) >>> c2 = gdspy.Curve(0, 2).i([(1, 0), (2, 0), (1, -1)], ... cycle=True) >>> ps = gdspy.PolygonSet([c1.get_points(), c2.get_points()]) References ---------- .. [1] Hobby, J.D. *Discrete Comput. Geom.* (1986) 1: 123. `DOI: 10.1007/BF02187690 `_ """ pts = numpy.vstack((_zero.reshape((1, 2)), points)) + self.points[-1] cta, ctb = _hobby(pts, angles, curl_start, curl_end, t_in, t_out, cycle) args = [] args.extend( x for i in range(pts.shape[0] - 1) for x in [ cta[i, 0], cta[i, 1], ctb[i, 0], ctb[i, 1], pts[i + 1, 0], pts[i + 1, 1], ] ) if cycle: args.extend( [cta[-1, 0], cta[-1, 1], ctb[-1, 0], ctb[-1, 1], pts[0, 0], pts[0, 1]] ) return self.C(*args) gdspy-1.4.2/gdspy/data/000077500000000000000000000000001354474061200147115ustar00rootroot00000000000000gdspy-1.4.2/gdspy/data/00.xbm000066400000000000000000000004271354474061200156430ustar00rootroot00000000000000#define 00_width 16 #define 00_height 16 static unsigned char 00_bits[] = { 0x88, 0x88, 0x22, 0x22, 0x88, 0x88, 0x22, 0x22, 0x88, 0x88, 0x22, 0x22, 0x88, 0x88, 0x22, 0x22, 0x88, 0x88, 0x22, 0x22, 0x88, 0x88, 0x22, 0x22, 0x88, 0x88, 0x22, 0x22, 0x88, 0x88, 0x22, 0x22 }; gdspy-1.4.2/gdspy/data/01.xbm000066400000000000000000000004271354474061200156440ustar00rootroot00000000000000#define 01_width 16 #define 01_height 16 static unsigned char 01_bits[] = { 0x06, 0x06, 0x03, 0x03, 0x81, 0x81, 0xc0, 0xc0, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0c, 0x0c, 0x06, 0x06, 0x03, 0x03, 0x81, 0x81, 0xc0, 0xc0, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0c, 0x0c }; gdspy-1.4.2/gdspy/data/02.xbm000066400000000000000000000004271354474061200156450ustar00rootroot00000000000000#define 02_width 16 #define 02_height 16 static unsigned char 02_bits[] = { 0x60, 0x60, 0xc0, 0xc0, 0x81, 0x81, 0x03, 0x03, 0x06, 0x06, 0x0c, 0x0c, 0x18, 0x18, 0x30, 0x30, 0x60, 0x60, 0xc0, 0xc0, 0x81, 0x81, 0x03, 0x03, 0x06, 0x06, 0x0c, 0x0c, 0x18, 0x18, 0x30, 0x30 }; gdspy-1.4.2/gdspy/data/03.xbm000066400000000000000000000004271354474061200156460ustar00rootroot00000000000000#define 03_width 16 #define 03_height 16 static unsigned char 03_bits[] = { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 }; gdspy-1.4.2/gdspy/data/04.xbm000066400000000000000000000004271354474061200156470ustar00rootroot00000000000000#define 04_width 16 #define 04_height 16 static unsigned char 04_bits[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; gdspy-1.4.2/gdspy/data/05.xbm000066400000000000000000000004271354474061200156500ustar00rootroot00000000000000#define 05_width 16 #define 05_height 16 static unsigned char 05_bits[] = { 0xc3, 0xc3, 0x66, 0x66, 0x3c, 0x3c, 0x18, 0x18, 0x3c, 0x3c, 0x66, 0x66, 0xc3, 0xc3, 0x81, 0x81, 0xc3, 0xc3, 0x66, 0x66, 0x3c, 0x3c, 0x18, 0x18, 0x3c, 0x3c, 0x66, 0x66, 0xc3, 0xc3, 0x81, 0x81 }; gdspy-1.4.2/gdspy/data/06.xbm000066400000000000000000000004271354474061200156510ustar00rootroot00000000000000#define 06_width 16 #define 06_height 16 static unsigned char 06_bits[] = { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xff, 0xff, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xff, 0xff, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 }; gdspy-1.4.2/gdspy/data/07.xbm000066400000000000000000000004271354474061200156520ustar00rootroot00000000000000#define 07_width 16 #define 07_height 16 static unsigned char 07_bits[] = { 0x00, 0x00, 0x22, 0x22, 0x00, 0x00, 0x88, 0x88, 0x00, 0x00, 0x22, 0x22, 0x00, 0x00, 0x88, 0x88, 0x00, 0x00, 0x22, 0x22, 0x00, 0x00, 0x88, 0x88, 0x00, 0x00, 0x22, 0x22, 0x00, 0x00, 0x88, 0x88 }; gdspy-1.4.2/gdspy/data/08.xbm000066400000000000000000000004271354474061200156530ustar00rootroot00000000000000#define 08_width 16 #define 08_height 16 static unsigned char 08_bits[] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; gdspy-1.4.2/gdspy/data/09.xbm000066400000000000000000000004271354474061200156540ustar00rootroot00000000000000#define 09_width 16 #define 09_height 16 static unsigned char 09_bits[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; gdspy-1.4.2/gdspy/data/down.xbm000066400000000000000000000004351354474061200163720ustar00rootroot00000000000000#define down_width 16 #define down_height 16 static unsigned char down_bits[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x0f, 0x30, 0x0c, 0x20, 0x04, 0x60, 0x06, 0x40, 0x02, 0xc0, 0x03, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; gdspy-1.4.2/gdspy/data/outline.xbm000066400000000000000000000004461354474061200171040ustar00rootroot00000000000000#define outline_width 16 #define outline_height 16 static unsigned char outline_bits[] = { 0xff, 0xff, 0xff, 0xff, 0x03, 0xc0, 0x03, 0xc0, 0x03, 0xc0, 0x03, 0xc0, 0x03, 0xc0, 0x03, 0xc0, 0x03, 0xc0, 0x03, 0xc0, 0x03, 0xc0, 0x03, 0xc0, 0x03, 0xc0, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff }; gdspy-1.4.2/gdspy/data/up.xbm000066400000000000000000000004271354474061200160500ustar00rootroot00000000000000#define up_width 16 #define up_height 16 static unsigned char up_bits[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0xc0, 0x03, 0x40, 0x02, 0x60, 0x06, 0x20, 0x04, 0x30, 0x0c, 0xf0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; gdspy-1.4.2/gdspy/viewer.py000066400000000000000000001051241354474061200156560ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### """ Classes and functions for the visualization of layouts created with the gdspy Python module. """ from __future__ import division from __future__ import unicode_literals from __future__ import print_function from __future__ import absolute_import import sys if sys.version_info.major < 3: from builtins import super from builtins import range from builtins import dict from builtins import int from builtins import str from future import standard_library standard_library.install_aliases() import os import colorsys import numpy import tkinter import tkinter.messagebox import tkinter.colorchooser import gdspy _stipple = tuple( "@" + os.path.join(os.path.dirname(gdspy.__file__), "data", "{:02d}.xbm".format(n)) for n in range(10) ) _icon_up = "@" + os.path.join(os.path.dirname(gdspy.__file__), "data", "up.xbm") _icon_down = "@" + os.path.join(os.path.dirname(gdspy.__file__), "data", "down.xbm") _icon_outline = "@" + os.path.join( os.path.dirname(gdspy.__file__), "data", "outline.xbm" ) _invisible = 9 _tkinteranchors = [ tkinter.NW, tkinter.N, tkinter.NE, None, tkinter.W, tkinter.CENTER, tkinter.E, None, tkinter.SW, tkinter.S, tkinter.SE, ] class ColorDict(dict): def __init__(self, default): super(ColorDict, self).__init__() self.default = default def __missing__(self, key): if self.default is None: layer, datatype = key rgb = "#{0[0]:02x}{0[1]:02x}{0[2]:02x}".format( [ int(255 * c + 0.5) for c in colorsys.hsv_to_rgb( (layer % 3) / 3.0 + (layer % 6 // 3) / 6.0 + (layer // 6) / 11.0, 1 - ((layer + datatype) % 8) / 12.0, 1 - (datatype % 3) / 4.0, ) ] ) else: rgb = self.default self[key] = rgb return rgb class PatternDict(dict): def __init__(self, default): super(PatternDict, self).__init__() self.default = default def __missing__(self, key): if self.default is None: pat = (key[0] + key[1]) % 8 else: pat = self.default self[key] = pat return pat class LayoutViewer(tkinter.Frame): """ Provide a GUI where the layout can be viewed. The view can be scrolled vertically with the mouse wheel, and horizontally by holding the shift key and using the mouse wheel. Dragging the 2nd mouse button also scrolls the view, and if control is held down, it scrolls 10 times faster. You can zoom in or out using control plus the mouse wheel, or drag a rectangle on the window with the 1st mouse button to zoom into that area. A ruler is available by clicking the 1st mouse button anywhere on the view and moving the mouse around. The distance is shown in the status area. Double-clicking on any polygon gives some information about it. Color and pattern for each layer/datatype specification can be changed by left and right clicking on the icon in the layer/datatype list. Left and right clicking the text label changes the visibility. Parameters ---------- library : ``GdsLibrary`` GDSII library to display. If ``None``, the current library is used. cells : Cell, string or array-like The array of cells to be included in the view. If ``None``, all cells listed in the current library are used. hidden_types : array-like The array of tuples (layer, datatype) to start in hidden state. depth : integer Initial depth of referenced cells to be displayed. color : dictionary Dictionary of colors for each tuple (layer, datatype). The colors must be strings in the format ``#rrggbb``. A value with key ``default`` will be used as default color. pattern : dictionary Dictionary of patterns for each tuple (layer, datatype). The patterns must be integers between 0 and 9, inclusive. A value with key ``default`` will be used as default pattern. background : string Canvas background color in the format ``#rrggbb``. width : integer Horizontal size of the viewer canvas. height : integer Vertical size of the viewer canvas. Examples -------- White background, filled shapes: >>> gdspy.LayoutViewer(pattern={'default': 8}, ... background='#FFFFFF') No filling, black color for layer 0, datatype 1, automatic for others: >>> gdspy.LayoutViewer(pattern={'default': 9}, ... color={(0, 1): '#000000'}) """ def __init__( self, library=None, cells=None, hidden_types=[], depth=0, color={}, pattern={}, background="#202020", width=800, height=600, ): tkinter.Frame.__init__(self, None) if library is None: library = gdspy.current_library self.current_cell = tkinter.StringVar() if cells is None: self.cells = library.cell_dict cell_names = list(library.cell_dict.keys()) self.cell_bb = dict([(s, None) for s in self.cells]) else: if isinstance(cells, str) or isinstance(cells, gdspy.Cell): cells = (cells,) self.cells = {} cell_names = [] self.cell_bb = {} for c in cells: cell = library.cell_dict.get(c, c) self.cells[cell.name] = cell cell_names.append(cell.name) self.cell_bb[cell.name] = None self.current_cell.set(cell_names[0]) self.depth = tkinter.IntVar() self.depth.set(depth) self.hidden_layers = hidden_types self.color = ColorDict(color.get("default", None)) self.color.update(color) self.pattern = PatternDict(pattern.get("default", None)) self.pattern.update(pattern) # Setup resizable window self.grid(sticky="nsew") top = self.winfo_toplevel() top.rowconfigure(0, weight=1) top.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) # Setup canvas self.canvas = tkinter.Canvas( self, width=width, height=height, xscrollincrement=0, yscrollincrement=0 ) self.canvas.grid(row=0, column=0, sticky="nsew") self.canvas.configure(bg=background) bg = [int(c, 16) for c in (background[1:3], background[3:5], background[5:7])] self.default_outline = "#{0[0]:02x}{0[1]:02x}{0[2]:02x}".format( [(0 if c > 127 else 255) for c in bg] ) self.default_grey = "#{0[0]:02x}{0[1]:02x}{0[2]:02x}".format( [ ( (c + 256) // 2 if c < 85 else (c // 2 if c > 171 else (255 if c > 127 else 0)) ) for c in bg ] ) # Setup scrollbars self.xscroll = tkinter.Scrollbar( self, orient=tkinter.HORIZONTAL, command=self.canvas.xview ) self.xscroll.grid(row=1, column=0, sticky="ew") self.yscroll = tkinter.Scrollbar( self, orient=tkinter.VERTICAL, command=self.canvas.yview ) self.yscroll.grid(row=0, column=1, sticky="ns") self.canvas["xscrollcommand"] = self.xscroll.set self.canvas["yscrollcommand"] = self.yscroll.set # Setup toolbar self.frame = tkinter.Frame(self) self.frame.columnconfigure(6, weight=1) self.frame.grid(row=2, column=0, columnspan=2, padx=2, pady=2, sticky="ew") # Setup buttons self.home = tkinter.Button( self.frame, text="Extents", command=self._update_canvas ) self.zoom_in = tkinter.Button(self.frame, text="In", command=self._zoom_in) self.zoom_out = tkinter.Button(self.frame, text="Out", command=self._zoom_out) self.cell_menu = tkinter.OptionMenu(self.frame, self.current_cell, *cell_names) self.depth_spin = tkinter.Spinbox( self.frame, textvariable=self.depth, command=self._update_depth, from_=-1, to=128, increment=1, justify=tkinter.RIGHT, width=3, ) self.home.grid(row=0, column=0, sticky="w") self.zoom_in.grid(row=0, column=1, sticky="w") self.zoom_out.grid(row=0, column=2, sticky="w") self.cell_menu.grid(row=0, column=3, sticky="w") tkinter.Label(self.frame, text="Ref level:").grid(row=0, column=4, sticky="w") self.depth_spin.grid(row=0, column=5, sticky="w") self.bind_all("", self._update_canvas) self.bind_all("", self._update_canvas) self.bind_all("", self._update_canvas) # Setup coordinates box self.coords = tkinter.Label(self.frame, text="0, 0") self.coords.grid(row=0, column=6, sticky="e") # Layers self.l_canvas = tkinter.Canvas(self) self.l_canvas.grid(row=0, column=2, rowspan=3, sticky="nsew") self.l_scroll = tkinter.Scrollbar( self, orient=tkinter.VERTICAL, command=self.l_canvas.yview ) self.l_scroll.grid(row=0, column=3, rowspan=3, sticky="ns") self.l_canvas["yscrollcommand"] = self.l_scroll.set # Change current cell self.current_cell.trace_variable("w", self._update_canvas) # Update depth self.depth_spin.bind("", self._update_depth) # Drag-scroll: button 2 self.canvas.bind("", lambda evt: self.canvas.scan_mark(evt.x, evt.y)) self.canvas.bind("", self._mouse_move) # Y scroll: scroll wheel self.canvas.bind( "", lambda evt: self.canvas.yview( tkinter.SCROLL, 1 if evt.delta < 0 else -1, tkinter.UNITS ), ) self.canvas.bind( "", lambda evt: self.canvas.yview(tkinter.SCROLL, -1, tkinter.UNITS), ) self.canvas.bind( "", lambda evt: self.canvas.yview(tkinter.SCROLL, 1, tkinter.UNITS), ) self.l_canvas.bind( "", lambda evt: self.l_canvas.yview( tkinter.SCROLL, 1 if evt.delta < 0 else -1, tkinter.UNITS ), ) self.l_canvas.bind( "", lambda evt: self.l_canvas.yview(tkinter.SCROLL, -1, tkinter.UNITS), ) self.l_canvas.bind( "", lambda evt: self.l_canvas.yview(tkinter.SCROLL, 1, tkinter.UNITS), ) # X scroll: shift + scroll wheel self.bind_all( "", lambda evt: self.canvas.xview( tkinter.SCROLL, 1 if evt.delta < 0 else -1, tkinter.UNITS ), ) self.canvas.bind( "", lambda evt: self.canvas.xview(tkinter.SCROLL, -1, tkinter.UNITS), ) self.canvas.bind( "", lambda evt: self.canvas.xview(tkinter.SCROLL, 1, tkinter.UNITS), ) # Object properties: double button 1 # Zoom rectangle: drag button 1 # Measure tool: button 1 (click + click, no drag) self.canvas.bind("", self._zoom_rect_mark) self.canvas.bind("", self._mouse_btn_1) self.canvas.bind("", self._properties) # Zoom: control + scroll wheel self.bind_all("", self._zoom) self.canvas.bind("", self._zoom) self.canvas.bind("", self._zoom) # Update the viewer self.shown_cell = None self.shown_depth = depth self.canvas_margins = None self._update_canvas() self.master.title("Gdspy - Layout Viewer") self.mainloop() def _update_depth(self, *args): try: d = self.depth.get() except: self.depth.set(self.shown_depth) return if d != self.shown_depth: self.shown_cell = self.current_cell.get() self.shown_depth = d self._update_data() def _update_canvas(self, *args): if self.shown_cell is None: width = float(self.canvas.cget("width")) height = float(self.canvas.cget("height")) else: width = float(self.canvas.winfo_width()) - self.canvas_margins[0] height = float(self.canvas.winfo_height()) - self.canvas_margins[1] self.shown_cell = self.current_cell.get() if self.cell_bb[self.current_cell.get()] is None: bb = [1e300, 1e300, -1e300, -1e300] pol_dict = self.cells[self.current_cell.get()].get_polygons( by_spec=True, depth=self.shown_depth ) for pols in pol_dict.values(): for pol in pols: bb[0] = min(bb[0], pol[:, 0].min()) bb[1] = min(bb[1], -pol[:, 1].max()) bb[2] = max(bb[2], pol[:, 0].max()) bb[3] = max(bb[3], -pol[:, 1].min()) self.cell_bb[self.current_cell.get()] = tuple(bb) else: bb = list(self.cell_bb[self.current_cell.get()]) if bb[2] < bb[0]: tkinter.messagebox.showwarning("Warning", "The selected cell is empty.") bb = [-1, -1, 1, 1] self.scale = ((bb[3] - bb[1]) / height, (bb[2] - bb[0]) / width) if self.scale[0] > self.scale[1]: self.scale = self.scale[0] * 1.05 add = (width * self.scale - bb[2] + bb[0]) * 0.5 bb[0] -= add bb[2] += add add = (bb[3] - bb[1]) * 0.025 bb[1] -= add bb[3] += add else: self.scale = self.scale[1] * 1.05 add = (height * self.scale - bb[3] + bb[1]) * 0.5 bb[1] -= add bb[3] += add add = (bb[2] - bb[0]) * 0.025 bb[0] -= add bb[2] += add self._update_data() self.canvas.configure(scrollregion=tuple([x / self.scale for x in bb])) self.canvas.zoom_rect = None if self.canvas_margins is None: self.update() self.canvas_margins = ( int(self.canvas.winfo_width()) - width, int(self.canvas.winfo_height()) - height, ) def _update_data(self): self.canvas.delete(tkinter.ALL) self.l_canvas.delete(tkinter.ALL) self.canvas.ruler = None self.canvas.x_rl = 0 self.canvas.y_rl = 0 pol_dict = self.cells[self.current_cell.get()].get_polygons( by_spec=True, depth=self.shown_depth ) lbl_dict = {} for label in self.cells[self.current_cell.get()].get_labels( depth=self.shown_depth ): key = (label.layer, label.texttype) if key in lbl_dict: lbl_dict[key].append(label) else: lbl_dict[key] = [label] layers = list(set(list(pol_dict.keys()) + list(lbl_dict.keys()))) layers.sort( reverse=True, key=lambda i: (-1, -1) if not isinstance(i, tuple) else i ) self.l_canvas_info = [] pos = 0 wid = None hei = None bg = self.canvas["bg"] self.l_canvas.configure(bg=bg) for i in layers: if i in self.hidden_layers: state = "hidden" fg = self.default_grey else: state = "normal" fg = self.default_outline if isinstance(i, tuple): lbl = ( tkinter.Label( self, bitmap=_icon_outline if self.pattern[i] == _invisible else _stipple[self.pattern[i]], bd=0, fg=self.color[i], bg=bg, anchor="c", ), tkinter.Label( self, text="{0[0]}/{0[1]}".format(i), bd=0, fg=fg, bg=bg, height=1, anchor="c", padx=8, ), tkinter.Label( self, bitmap=_icon_up, bd=0, fg=self.default_outline, bg=bg, anchor="c", ), tkinter.Label( self, bitmap=_icon_down, bd=0, fg=self.default_outline, bg=bg, anchor="c", ), ) lbl[0].bind("", self._change_color(lbl[0], i)) lbl[0].bind("", self._change_pattern(lbl[0], i)) lbl[0].bind("", self._change_pattern(lbl[0], i)) lbl[1].bind("", self._change_visibility(lbl[1], i)) lbl[1].bind("", self._change_other_visibility(i)) lbl[1].bind("", self._change_other_visibility(i)) lbl[2].bind("", self._raise(i)) lbl[3].bind("", self._lower(i)) for l in lbl: l.bind( "", lambda evt: self.l_canvas.yview( tkinter.SCROLL, 1 if evt.delta < 0 else -1, tkinter.UNITS ), ) l.bind( "", lambda evt: self.l_canvas.yview( tkinter.SCROLL, -1, tkinter.UNITS ), ) l.bind( "", lambda evt: self.l_canvas.yview( tkinter.SCROLL, 1, tkinter.UNITS ), ) if wid is None: lbl[1].configure(text="255/255") hei = max(lbl[0].winfo_reqheight(), lbl[1].winfo_reqheight()) wid = lbl[1].winfo_reqwidth() lbl[1].configure(text="{0[0]}/{0[1]}".format(i)) ids = ( self.l_canvas.create_window(0, pos, window=lbl[0], anchor="sw"), self.l_canvas.create_window(hei, pos, window=lbl[1], anchor="sw"), self.l_canvas.create_window( hei + wid, pos, window=lbl[2], anchor="sw" ), self.l_canvas.create_window( 2 * hei + wid, pos, window=lbl[3], anchor="sw" ), ) self.l_canvas_info.append((i, ids, lbl)) pos -= hei if i in pol_dict: if not isinstance(i, tuple): for pol in pol_dict[i]: self.canvas.create_polygon( *list((numpy.array((1, -1)) * pol / self.scale).flatten()), fill="", outline=self.default_outline, activeoutline=self.default_outline, activewidth=2, tag=("L" + str(i), "V" + str(pol.shape[0])), state=state, dash=(8, 8) ) self.canvas.create_text( pol[:, 0].mean() / self.scale, pol[:, 1].mean() / -self.scale, text=i, anchor=tkinter.CENTER, fill=self.default_outline, tag=("L" + str(i), "TEXT"), ) else: for pol in pol_dict[i]: self.canvas.create_polygon( *list((numpy.array((1, -1)) * pol / self.scale).flatten()), fill=self.color[i], stipple=_stipple[self.pattern[i]], offset="{},{}".format(*numpy.random.randint(16, size=2)), outline=self.color[i], activeoutline=self.default_outline, activewidth=2, tag=("L" + str(i), "V" + str(pol.shape[0])), state=state ) if i in lbl_dict: for label in lbl_dict[i]: self.canvas.create_text( label.position[0] / self.scale, label.position[1] / -self.scale, text=label.text, anchor=_tkinteranchors[label.anchor], fill=self.color[i], activefill=self.default_outline, tag=("L" + str(i), "TEXT"), state=state, ) if (wid is None) or (hei is None) or (pos is None): pos = -12 hei = 12 wid = 12 self.l_canvas.configure( width=3 * hei + wid, scrollregion=(0, pos, 3 * hei + wid, 0), yscrollincrement=hei, ) def _change_color(self, lbl, layer): def func(*args): rgb, color = tkinter.colorchooser.askcolor( self.color[layer], title="Select color" ) if color is not None: self.color[layer] = color lbl.configure(fg=color) for i in self.canvas.find_withtag("L" + str(layer)): self.canvas.itemconfigure(i, fill=color) if layer[0] >= 0 and "TEXT" not in self.canvas.gettags(i): self.canvas.itemconfigure(i, outline=color) return func def _change_pattern(self, lbl, layer): def func(*args): pattern = [] dlg = tkinter.Toplevel() dlg.title("Select pattern") dlg.resizable(False, False) for i in range(10): choice = tkinter.Button( dlg, bitmap=_stipple[i], command=(lambda x: (lambda: pattern.append(x) or dlg.destroy()))(i), ) choice.grid(row=0, column=i, padx=3, pady=3) choice = tkinter.Button(dlg, text="Cancel", command=dlg.destroy) choice.grid(row=1, column=0, columnspan=10, padx=3, pady=3, sticky="e") dlg.focus_set() dlg.wait_visibility() dlg.grab_set() dlg.wait_window(dlg) if len(pattern) > 0: self.pattern[layer] = pattern[0] lbl.configure( bitmap=_icon_outline if pattern[0] == _invisible else _stipple[pattern[0]] ) for i in self.canvas.find_withtag("L" + str(layer)): if "TEXT" not in self.canvas.gettags(i): self.canvas.itemconfigure(i, stipple=_stipple[pattern[0]]) return func def _change_visibility(self, lbl, layer): def func(*args): if layer in self.hidden_layers: self.hidden_layers.remove(layer) lbl.configure(fg=self.default_outline) for j in self.canvas.find_withtag("L" + str(layer)): self.canvas.itemconfigure(j, state="normal") else: self.hidden_layers.append(layer) lbl.configure(fg=self.default_grey) for j in self.canvas.find_withtag("L" + str(layer)): self.canvas.itemconfigure(j, state="hidden") return func def _change_other_visibility(self, layer): def func(*args): for other, ids, lbl in self.l_canvas_info: if layer != other: unhide = other in self.hidden_layers break for other, ids, lbl in self.l_canvas_info: if layer != other: if unhide and (other in self.hidden_layers): self.hidden_layers.remove(other) lbl[1].configure(fg=self.default_outline) for j in self.canvas.find_withtag("L" + str(other)): self.canvas.itemconfigure(j, state="normal") elif not (unhide or (other in self.hidden_layers)): self.hidden_layers.append(other) lbl[1].configure(fg=self.default_grey) for j in self.canvas.find_withtag("L" + str(other)): self.canvas.itemconfigure(j, state="hidden") return func def _raise(self, layer): def func(*args): idx = 0 while self.l_canvas_info[idx][0] != layer: idx += 1 if idx < len(self.l_canvas_info) - 1: hei = int(self.l_canvas["yscrollincrement"]) under, idu, _ = self.l_canvas_info[idx] above, ida, _ = self.l_canvas_info[idx + 1] self.canvas.tag_raise("L" + str(under), "L" + str(above)) for i in idu: self.l_canvas.move(i, 0, -hei) for i in ida: self.l_canvas.move(i, 0, hei) self.l_canvas.yview_scroll(-1, tkinter.UNITS) self.l_canvas_info[idx], self.l_canvas_info[idx + 1] = ( self.l_canvas_info[idx + 1], self.l_canvas_info[idx], ) return func def _lower(self, layer): def func(*args): idx = 0 while self.l_canvas_info[idx][0] != layer: idx += 1 if idx > 0: hei = int(self.l_canvas["yscrollincrement"]) under, idu, _ = self.l_canvas_info[idx - 1] above, ida, _ = self.l_canvas_info[idx] self.canvas.tag_raise("L" + str(under), "L" + str(above)) for i in idu: self.l_canvas.move(i, 0, -hei) for i in ida: self.l_canvas.move(i, 0, hei) self.l_canvas.yview_scroll(1, tkinter.UNITS) self.l_canvas_info[idx], self.l_canvas_info[idx - 1] = ( self.l_canvas_info[idx - 1], self.l_canvas_info[idx], ) return func def _mouse_move(self, evt): x = self.canvas.canvasx(evt.x) y = self.canvas.canvasy(evt.y) if self.canvas.ruler is None: self.coords.configure( text="{0:g}, {1:g}".format(x * self.scale, -y * self.scale) ) else: self.canvas.coords( self.canvas.ruler, self.canvas.x_rl, self.canvas.y_rl, x, y ) dx = (x - self.canvas.x_rl) * self.scale dy = (self.canvas.y_rl - y) * self.scale self.coords.configure( text="Distance: {0:g} | dx = {1:g} | dy = {2:g}".format( (dx ** 2 + dy ** 2) ** 0.5, dx, dy ) ) if int(evt.state) & 0x0200: if int(evt.state) & 0x0004: self.canvas.scan_dragto(evt.x, evt.y, 10) else: self.canvas.scan_dragto(evt.x, evt.y, 1) elif int(evt.state) & 0x0100: if self.canvas.zoom_rect is None: self.canvas.zoom_rect = self.canvas.create_rectangle( self.canvas.x_zr, self.canvas.y_zr, self.canvas.x_zr, self.canvas.y_zr, outline="#DDD", ) self.canvas.coords( self.canvas.zoom_rect, self.canvas.x_zr, self.canvas.y_zr, x, y ) def _zoom(self, evt): if evt.num == 4: evt.delta = 1 elif evt.num == 5: evt.delta = -1 s = 1.5 if evt.delta > 0 else 1 / 1.5 self.scale /= s x0 = s * self.canvas.canvasx(evt.x) - evt.x y0 = s * self.canvas.canvasy(evt.y) - evt.y self.canvas.scale(tkinter.ALL, 0, 0, s, s) self.canvas.x_rl *= s self.canvas.y_rl *= s bb = self.canvas.bbox(tkinter.ALL) if bb is not None: w = (bb[2] - bb[0]) * 1.2 h = (bb[3] - bb[1]) * 1.2 bb = (bb[0] - w, bb[1] - h, bb[2] + w, bb[3] + h) self.canvas["scrollregion"] = bb self.canvas.xview(tkinter.MOVETO, (x0 - bb[0]) / (bb[2] - bb[0])) self.canvas.yview(tkinter.MOVETO, (y0 - bb[1]) / (bb[3] - bb[1])) def _zoom_in(self): s = 1.5 self.scale /= s self.canvas.scale(tkinter.ALL, 0, 0, s, s) self.canvas.x_rl *= s self.canvas.y_rl *= s bb = self.canvas.bbox(tkinter.ALL) w = (bb[2] - bb[0]) * 1.2 h = (bb[3] - bb[1]) * 1.2 bb = (bb[0] - w, bb[1] - h, bb[2] + w, bb[3] + h) self.canvas["scrollregion"] = bb x0 = self.xscroll.get() x0 = 0.5 * (x0[1] + x0[0]) - 0.5 * ( (float(self.canvas.winfo_width()) - self.canvas_margins[0]) / (bb[2] - bb[0]) ) y0 = self.yscroll.get() y0 = 0.5 * (y0[1] + y0[0]) - 0.5 * ( (float(self.canvas.winfo_height()) - self.canvas_margins[1]) / (bb[3] - bb[1]) ) self.canvas.xview(tkinter.MOVETO, x0) self.canvas.yview(tkinter.MOVETO, y0) def _zoom_out(self): s = 1 / 1.5 self.scale /= s self.canvas.scale(tkinter.ALL, 0, 0, s, s) self.canvas.x_rl *= s self.canvas.y_rl *= s bb = self.canvas.bbox(tkinter.ALL) w = (bb[2] - bb[0]) * 1.2 h = (bb[3] - bb[1]) * 1.2 bb = (bb[0] - w, bb[1] - h, bb[2] + w, bb[3] + h) self.canvas["scrollregion"] = bb x0 = self.xscroll.get() x0 = 0.5 * (x0[1] + x0[0]) - 0.5 * ( (float(self.canvas.winfo_width()) - self.canvas_margins[0]) / (bb[2] - bb[0]) ) y0 = self.yscroll.get() y0 = 0.5 * (y0[1] + y0[0]) - 0.5 * ( (float(self.canvas.winfo_height()) - self.canvas_margins[1]) / (bb[3] - bb[1]) ) self.canvas.xview(tkinter.MOVETO, x0) self.canvas.yview(tkinter.MOVETO, y0) def _zoom_rect_mark(self, evt): self.canvas.x_zr = float(self.canvas.canvasx(evt.x)) self.canvas.y_zr = float(self.canvas.canvasy(evt.y)) def _mouse_btn_1(self, evt): if self.canvas.zoom_rect is None: if self.canvas.ruler is None: x0 = self.canvas.canvasx(evt.x) y0 = self.canvas.canvasy(evt.y) self.canvas.ruler = self.canvas.create_line( x0, y0, x0, y0, arrow=tkinter.BOTH, fill=self.default_outline, width=2, ) self.canvas.x_rl = x0 self.canvas.y_rl = y0 else: self.canvas.delete(self.canvas.ruler) self.canvas.ruler = None else: x1 = float(self.canvas.winfo_width()) - self.canvas_margins[0] sx = float(self.canvas.canvasx(evt.x)) dx = abs(self.canvas.x_zr - sx) sx += self.canvas.x_zr y1 = float(self.canvas.winfo_height()) - self.canvas_margins[1] sy = float(self.canvas.canvasy(evt.y)) dy = abs(self.canvas.y_zr - sy) sy += self.canvas.y_zr self.canvas.delete(self.canvas.zoom_rect) self.canvas.zoom_rect = None if abs(dx * dy) > 1.0e-12: s = (x1 / dx, y1 / dy) if s[0] < s[1]: s = s[0] y0 = 0.5 * (s * sy - y1) x0 = 0.5 * s * (sx - dx) else: s = s[1] x0 = 0.5 * (s * sx - x1) y0 = 0.5 * s * (sy - dy) self.scale /= s self.canvas.scale(tkinter.ALL, 0, 0, s, s) self.canvas.x_rl *= s self.canvas.y_rl *= s bb = self.canvas.bbox(tkinter.ALL) if bb is not None: w = (bb[2] - bb[0]) * 1.5 h = (bb[3] - bb[1]) * 1.5 bb = (bb[0] - w, bb[1] - h, bb[2] + w, bb[3] + h) self.canvas["scrollregion"] = bb self.canvas.xview(tkinter.MOVETO, (x0 - bb[0]) / (bb[2] - bb[0])) self.canvas.yview(tkinter.MOVETO, (y0 - bb[1]) / (bb[3] - bb[1])) def _properties(self, evt): if self.canvas.ruler is not None: self.canvas.delete(self.canvas.ruler) self.canvas.ruler = -1 i = self.canvas.find_closest( self.canvas.canvasx(evt.x), self.canvas.canvasy(evt.y) ) bb = self.canvas.bbox(i) if bb is not None: bb = ( bb[0] * self.scale, -bb[3] * self.scale, bb[2] * self.scale, -bb[1] * self.scale, ) tags = self.canvas.gettags(i) if "TEXT" not in tags: tkinter.messagebox.showinfo( "Element information", "Layer/datatpe: {0}\nVertices: {1}\nApproximate bounding box:\n({2[0]:g}, {2[1]:g}) - ({2[2]:g}, {2[3]:g})".format( tags[0][1:], tags[1][1:], bb ), parent=self.canvas, ) gdspy-1.4.2/requirements.txt000066400000000000000000000001221354474061200161310ustar00rootroot00000000000000setuptools numpy future; python_version < "3" pytest == 4.6; python_version < "3" gdspy-1.4.2/setup.cfg000066400000000000000000000003771354474061200145020ustar00rootroot00000000000000[sdist] formats=zip [bdist] formats=wininst [bdist_wininst] user-access-control=auto [build_sphinx] source-dir=docs config-dir=docs build-dir=docs/_build [aliases] test=pytest [tool:pytest] addopts = --maxfail=1 testpaths = tests python_files = *.py gdspy-1.4.2/setup.py000066400000000000000000000056521354474061200143740ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### import sys import platform from setuptools import setup, Extension from distutils.version import LooseVersion with open("README.md") as fin: long_description = fin.read() with open("gdspy/__init__.py") as fin: for line in fin: if line.startswith("__version__ ="): version = eval(line[14:]) break setup_requires = [] if {"pytest", "test", "ptr"}.intersection(sys.argv): setup_requires.append("pytest-runner") if "build_sphinx" in sys.argv: setup_requires.extend(["sphinx", "sphinx_rtd_theme"]) # Mac OS X Mojave C++ compile + linking arguments extra_compile_args = [] extra_link_args = [] if platform.system() == "Darwin" and LooseVersion(platform.release()) >= LooseVersion( "17.7" ): extra_compile_args = ["-std=c++11", "-mmacosx-version-min=10.9"] extra_link_args = ["-stdlib=libc++", "-mmacosx-version-min=10.9"] setup( name="gdspy", version=version, author="Lucas Heitzmann Gabrielli", author_email="heitzmann@gmail.com", license="Boost Software License v1.0", url="https://github.com/heitzmann/gdspy", description="Python module for creating/importing/merging GDSII files.", long_description=long_description, keywords="GDSII CAD layout", packages=["gdspy"], package_dir={"gdspy": "gdspy"}, package_data={"gdspy": ["data/*"]}, ext_modules=[ Extension( "gdspy.clipper", ["gdspy/clipper.cpp"], extra_compile_args=extra_compile_args, extra_link_args=extra_link_args, ) ], provides=["gdspy"], install_requires=["numpy"] + (["future"] if sys.version_info.major < 3 else []), setup_requires=setup_requires, tests_require=["pytest"], platforms="OS Independent", classifiers=[ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Manufacturing", "Intended Audience :: Science/Research", "License :: OSI Approved :: Boost Software License 1.0 (BSL-1.0)", "Operating System :: OS Independent", "Programming Language :: C", "Programming Language :: C++", "Programming Language :: Python", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", ], zip_safe=False, ) gdspy-1.4.2/tests/000077500000000000000000000000001354474061200140145ustar00rootroot00000000000000gdspy-1.4.2/tests/cell.py000066400000000000000000000154601354474061200153130ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### import pytest import gdspy import numpy import uuid def unique(): return str(uuid.uuid4()) @pytest.fixture def tree(): p1 = gdspy.Polygon(((0, 0), (0, 1), (1, 0)), 0, 0) p2 = gdspy.Polygon(((2, 0), (2, 1), (1, 0)), 1, 1) l1 = gdspy.Label("label1", (0, 0), layer=11) l2 = gdspy.Label("label2", (2, 1), layer=12) c1 = gdspy.Cell("tree_" + unique()) c1.add(p1) c1.add(l1) c2 = gdspy.Cell("tree_" + unique()) c2.add(l2) c2.add(p2) c2.add(gdspy.CellReference(c1)) c3 = gdspy.Cell("tree_" + unique()) c3.add(gdspy.CellArray(c2, 3, 2, (3, 3))) return c3, c2, c1 def test_duplicate(): gdspy.current_library = gdspy.GdsLibrary() name = "c_duplicate" gdspy.Cell(name) with pytest.raises(ValueError) as e: gdspy.Cell(name) assert name in str(e.value) def test_ignore_duplicate(): gdspy.current_library = gdspy.GdsLibrary() c1 = gdspy.Cell("c_ignore_duplicate") gdspy.Cell(c1.name, True) def test_str(): gdspy.current_library = gdspy.GdsLibrary() c = gdspy.Cell("c_str") assert str(c) == 'Cell ("c_str", 0 polygons, 0 paths, 0 labels, 0 references)' def test_add_element(): gdspy.current_library = gdspy.GdsLibrary() p = gdspy.Polygon(((0, 0), (1, 0), (0, 1))) c = gdspy.Cell("c_add_element") assert c.add(p) is c assert c.add([p, p]) is c assert c.polygons == [p, p, p] def test_add_label(): gdspy.current_library = gdspy.GdsLibrary() lbl = gdspy.Label("label", (0, 0)) c = gdspy.Cell("c_add_label") assert c.add(lbl) is c assert c.add([lbl, lbl]) is c assert c.labels == [lbl, lbl, lbl] def test_copy(): gdspy.current_library = gdspy.GdsLibrary() name = "c_copy" p = gdspy.Polygon(((0, 0), (1, 0), (0, 1))) lbl = gdspy.Label("label", (0, 0)) c1 = gdspy.Cell(name) c1.add(p) c1.add(lbl) with pytest.raises(ValueError) as e: c1.copy(name) assert name in str(e.value) c3 = c1.copy(name, True) assert c3.polygons == c1.polygons and c3.polygons is not c1.polygons assert c3.labels == c1.labels and c3.labels is not c1.labels cref = gdspy.Cell("c_ref").add(gdspy.Rectangle((-1, -1), (-2, -2))) c1.add(gdspy.CellReference(cref)) c1.get_bounding_box() c4 = c1.copy("c_copy_1", False, True) assert c4.polygons != c1.polygons assert c4.labels != c1.labels assert c1._bb_valid assert cref._bb_valid assert not c4._bb_valid def test_remove(tree): c3, c2, c1 = tree c1.remove_polygons(lambda p, layer, d: layer == 1) assert len(c1.polygons) == 1 c1.remove_polygons(lambda p, layer, d: layer == 0) assert len(c1.polygons) == 0 c1.remove_labels(lambda lbl: lbl.layer == 12) assert len(c1.labels) == 1 c1.remove_labels(lambda lbl: lbl.layer == 11) assert len(c1.labels) == 0 def test_area(): gdspy.current_library = gdspy.GdsLibrary() c = gdspy.Cell("c_area") c.add(gdspy.Rectangle((0, 0), (1, 1), layer=0)) c.add(gdspy.Rectangle((0, 0), (1, 1), layer=1)) c.add(gdspy.Rectangle((1, 1), (2, 2), layer=1)) c.add(gdspy.Rectangle((1, 1), (2, 2), datatype=2)) assert c.area() == 4.0 assert c.area(True) == {(0, 0): 1.0, (1, 0): 2.0, (0, 2): 1} def test_flatten_00(tree): c3, c2, c1 = tree c3.flatten() assert len(c3.polygons) == 12 for i in range(12): assert c3.polygons[i].layers == [0] or c3.polygons[i].layers == [1] assert c3.polygons[i].layers == c3.polygons[i].datatypes assert len(c3.labels) == 12 def test_flatten_01(tree): c3, c2, c1 = tree c3.flatten(None, 2, 3) assert len(c3.polygons) == 12 for i in range(12): assert c3.polygons[i].layers == [0] or c3.polygons[i].layers == [1] assert c3.polygons[i].datatypes == [2] assert len(c3.labels) == 12 assert all(lbl.texttype == 3 for lbl in c3.labels) def test_flatten_10(tree): c3, c2, c1 = tree c3.flatten(2) assert len(c3.polygons) == 12 for i in range(12): assert c3.polygons[i].datatypes == [0] or c3.polygons[i].datatypes == [1] assert c3.polygons[i].layers == [2] assert len(c3.labels) == 12 assert all(lbl.layer == 2 for lbl in c3.labels) def test_flatten_11(tree): c3, c2, c1 = tree c3.flatten(2, 3, 4) assert len(c3.polygons) == 12 assert all(p.layers == [2] for p in c3.polygons) assert all(p.datatypes == [3] for p in c3.polygons) assert len(c3.labels) == 12 assert all(lbl.layer == 2 for lbl in c3.labels) assert all(lbl.texttype == 4 for lbl in c3.labels) def test_bb(tree): c3, c2, c1 = tree err = numpy.array(((0, 0), (8, 4))) - c3.get_bounding_box() assert numpy.max(numpy.abs(err)) == 0 p2 = gdspy.Polygon(((-1, 2), (-1, 1), (0, 2)), 2, 2) c2.add(p2) err = numpy.array(((-1, 0), (8, 5))) - c3.get_bounding_box() assert numpy.max(numpy.abs(err)) == 0 p1 = gdspy.Polygon(((0, 3), (0, 2), (1, 3)), 3, 3) c1.add(p1) err = numpy.array(((-1, 0), (8, 6))) - c3.get_bounding_box() assert numpy.max(numpy.abs(err)) == 0 def test_layers(tree): assert tree[0].get_layers() == {0, 1, 11, 12} def test_datatypes(tree): assert tree[0].get_datatypes() == {0, 1} def test_get_polygons1(tree): c3, c2, c1 = tree p1 = gdspy.Polygon(((0, 3), (0, 2), (1, 3)), 3, 3) c1.add(p1) assert len(c3.get_polygons()) == 18 assert len(c3.get_polygons(False, 0)) == 6 assert len(c3.get_polygons(False, 1)) == 12 assert set(c3.get_polygons(True).keys()) == {(0, 0), (1, 1), (3, 3)} assert set(c3.get_polygons(True, 0).keys()) == {c2.name} assert set(c3.get_polygons(True, 1).keys()) == {c1.name, (1, 1)} def test_get_polygons2(tree): c3, c2, c1 = tree c1.add(gdspy.Rectangle((0, 0), (1, 1), 0, 0)) assert len(c1.get_polygons()) == 2 d = c1.get_polygons(True) assert len(d) == 1 assert (0, 0) in d assert len(d[(0, 0)]) == 2 c3.add(gdspy.CellReference(c1)) d = c3.get_polygons(True) assert len(d) == 2 assert (0, 0) in d and (1, 1) in d assert len(d[(0, 0)]) == 14 assert len(d[(1, 1)]) == 6 def test_get_polygons3(): gdspy.current_library = gdspy.GdsLibrary() c0 = gdspy.Cell("empty", False) assert len(c0.get_polygons()) == 0 assert len(c0.get_polygons(True)) == 0 assert len(c0.get_polygons(False, -1)) == 0 gdspy-1.4.2/tests/cellarray.py000066400000000000000000000044361354474061200163530ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### import pytest import gdspy import numpy def test_noreference(): name = "ca_noreference" with pytest.warns(UserWarning): ref = gdspy.CellArray(name, 2, 3, (3, 2), (1, -1), 90, 2, True) ref.translate(-1, 1) assert ref.ref_cell == name assert ref.area() == 0 assert ref.area(True) == dict() assert ref.get_bounding_box() is None assert ref.get_polygons() == [] assert ref.get_polygons(True) == dict() assert ref.origin[0] == ref.origin[1] == 0 def test_empty(): name = "ca_empty" c = gdspy.Cell(name) ref = gdspy.CellArray(name, 2, 3, (3, 2), (1, -1), 90, 2, True) ref.translate(-1, 1) assert ref.area() == 0 assert ref.area(True) == dict() assert ref.get_bounding_box() is None assert ref.get_polygons() == [] assert ref.get_polygons(True) == dict() assert ref.origin[0] == ref.origin[1] == 0 def test_notempty(): name = "ca_notempty" c = gdspy.Cell(name) ref = gdspy.CellArray(name, 2, 3, (3, 2), (1, -1), 90, 2, True) ref.translate(-1, 1) c.add(gdspy.Rectangle((0, 0), (1, 2), 2, 3)) assert ref.area() == 48 assert ref.area(True) == {(2, 3): 48} err = numpy.array(((0, 0), (8, 5))) - ref.get_bounding_box() assert numpy.max(numpy.abs(err)) < 1e-15 assert ref.origin[0] == ref.origin[1] == 0 r = gdspy.boolean( [gdspy.Rectangle((0, 0), (8, 2)), gdspy.Rectangle((0, 3), (8, 5))], ref.get_polygons(), "xor", 1e-6, 0, ) assert r is None d = ref.get_polygons(True) assert len(d.keys()) == 1 r = gdspy.boolean( [gdspy.Rectangle((0, 0), (8, 2)), gdspy.Rectangle((0, 3), (8, 5))], d[(2, 3)], "xor", 1e-6, 0, ) assert r is None gdspy-1.4.2/tests/cellreference.py000066400000000000000000000041501354474061200171640ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### import pytest import gdspy import numpy def test_noreference(): name = "cr_noreference" with pytest.warns(UserWarning): ref = gdspy.CellReference(name, (1, -1), 90, 2, True) ref.translate(-1, 1) assert ref.ref_cell == name assert ref.area() == 0 assert ref.area(True) == dict() assert ref.get_bounding_box() is None assert ref.get_polygons() == [] assert ref.get_polygons(True) == dict() assert ref.origin[0] == ref.origin[1] == 0 def test_empty(): name = "cr_empty" c = gdspy.Cell(name) ref = gdspy.CellReference(name, (1, -1), 90, 2, True) ref.translate(-1, 1) assert ref.area() == 0 assert ref.area(True) == dict() assert ref.get_bounding_box() is None assert ref.get_polygons() == [] assert ref.get_polygons(True) == dict() assert ref.origin[0] == ref.origin[1] == 0 def test_notempty(): name = "cr_notempty" c = gdspy.Cell(name) ref = gdspy.CellReference(name, (1, -1), 90, 2, True) ref.translate(-1, 1) c.add(gdspy.Rectangle((0, 0), (1, 2), 2, 3)) assert ref.area() == 8 assert ref.area(True) == {(2, 3): 8} err = numpy.array(((0, 0), (4, 2))) - ref.get_bounding_box() assert numpy.max(numpy.abs(err)) < 1e-15 assert ref.origin[0] == ref.origin[1] == 0 r = gdspy.boolean( ref.get_polygons(), gdspy.Rectangle((0, 0), (4, 2)), "xor", 1e-6, 0 ) assert r is None d = ref.get_polygons(True) assert len(d.keys()) == 1 r = gdspy.boolean(d[(2, 3)], gdspy.Rectangle((0, 0), (4, 2)), "xor", 1e-6, 0) assert r is None gdspy-1.4.2/tests/curve.py000066400000000000000000000166311354474061200155210ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### from tutils import target, assertsame import numpy import pytest import gdspy def test_hobby1(target): cell = gdspy.Cell("test", True) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)]) cell.add(gdspy.Polygon(c.get_points(), layer=1)) c = gdspy.Curve(2, 0, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[numpy.pi / 3, None, None, None]) cell.add(gdspy.Polygon(c.get_points(), layer=3)) c = gdspy.Curve(4, 0, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, None, None, 2 / 3.0 * numpy.pi]) cell.add(gdspy.Polygon(c.get_points(), layer=5)) c = gdspy.Curve(0, 2, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[numpy.pi / 3, None, None, 3 / 4.0 * numpy.pi]) cell.add(gdspy.Polygon(c.get_points(), layer=7)) c = gdspy.Curve(2, 2, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, None, numpy.pi / 2, None]) cell.add(gdspy.Polygon(c.get_points(), layer=9)) c = gdspy.Curve(4, 2, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, 0, None, None]) cell.add(gdspy.Polygon(c.get_points(), layer=11)) c = gdspy.Curve(0, 4, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, 0, None, -numpy.pi / 2]) cell.add(gdspy.Polygon(c.get_points(), layer=13)) c = gdspy.Curve(2, 4, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, 0, -numpy.pi, -numpy.pi / 2]) cell.add(gdspy.Polygon(c.get_points(), layer=15)) c = gdspy.Curve(4, 4, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[-numpy.pi / 4, 0, numpy.pi / 2, -numpy.pi]) cell.add(gdspy.Polygon(c.get_points(), layer=17)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=2)) c = gdspy.Curve(2, 0, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[numpy.pi / 3, None, None, None], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=4)) c = gdspy.Curve(4, 0, tolerance=1e-3) c.i( [(1, 0), (1, 1), (0, 1)], angles=[None, None, None, 2 / 3.0 * numpy.pi], cycle=True, ) cell.add(gdspy.Polygon(c.get_points(), layer=6)) c = gdspy.Curve(0, 2, tolerance=1e-3) c.i( [(1, 0), (1, 1), (0, 1)], angles=[numpy.pi / 3, None, None, 3 / 4.0 * numpy.pi], cycle=True, ) cell.add(gdspy.Polygon(c.get_points(), layer=8)) c = gdspy.Curve(2, 2, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, None, numpy.pi / 2, None], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=10)) c = gdspy.Curve(4, 2, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, 0, None, None], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=12)) c = gdspy.Curve(0, 4, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, 0, None, -numpy.pi / 2], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=14)) c = gdspy.Curve(2, 4, tolerance=1e-3) c.i( [(1, 0), (1, 1), (0, 1)], angles=[None, 0, -numpy.pi, -numpy.pi / 2], cycle=True ) cell.add(gdspy.Polygon(c.get_points(), layer=16)) c = gdspy.Curve(4, 4, tolerance=1e-3) c.i( [(1, 0), (1, 1), (0, 1)], angles=[-numpy.pi / 4, 0, numpy.pi / 2, -numpy.pi], cycle=True, ) cell.add(gdspy.Polygon(c.get_points(), layer=18)) assertsame(target["Hobby1"], cell) def test_hobby2(target): cell = gdspy.Cell("test", True) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)]) cell.add(gdspy.Polygon(c.get_points(), layer=1)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], curl_start=0) cell.add(gdspy.Polygon(c.get_points(), layer=2)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], curl_end=0) cell.add(gdspy.Polygon(c.get_points(), layer=3)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], curl_start=0, curl_end=0) cell.add(gdspy.Polygon(c.get_points(), layer=4)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i( [(1, 2), (2, 1), (3, 2), (4, 0)], angles=[numpy.pi / 2, None, None, None, -numpy.pi / 2], curl_start=0, curl_end=0, ) cell.add(gdspy.Polygon(c.get_points(), layer=5)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i( [(1, 2), (2, 1), (3, 2), (4, 0)], angles=[None, 0, None, 0, None], curl_start=0, curl_end=1, ) cell.add(gdspy.Polygon(c.get_points(), layer=6)) assertsame(target["Hobby2"], cell) def test_hobby3(target): cell = gdspy.Cell("test", True) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)]) cell.add(gdspy.Polygon(c.get_points(), layer=1)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], t_in=2) cell.add(gdspy.Polygon(c.get_points(), layer=2)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], t_out=2) cell.add(gdspy.Polygon(c.get_points(), layer=3)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], t_in=2, t_out=2) cell.add(gdspy.Polygon(c.get_points(), layer=4)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], t_in=[2, 1, 1, 1, 1], t_out=[1, 1, 1, 1, 2]) cell.add(gdspy.Polygon(c.get_points(), layer=5)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], t_in=[1, 1, 2, 1, 1], t_out=[1, 2, 1, 1, 1]) cell.add(gdspy.Polygon(c.get_points(), layer=6)) assertsame(target["Hobby3"], cell) def test_hobby4(target): cell = gdspy.Cell("test", True) c = gdspy.Curve(0, 3, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=10)) c = gdspy.Curve(0, 3, tolerance=1e-3) c.i( [(1, 2), (2, 1), (3, 2), (4, 0)], t_in=[2, 1, 1, 1, 1], t_out=[1, 1, 1, 1, 2], cycle=True, ) cell.add(gdspy.Polygon(c.get_points(), layer=11)) c = gdspy.Curve(0, 3, tolerance=1e-3) c.i( [(1, 2), (2, 1), (3, 2), (4, 0)], t_in=[1, 1, 2, 1, 1], t_out=[1, 2, 1, 1, 1], cycle=True, ) cell.add(gdspy.Polygon(c.get_points(), layer=12)) c = gdspy.Curve(0, 3, tolerance=1e-3) c.i( [(1, 2), (2, 1), (3, 2), (4, 0)], angles=[numpy.pi * 3 / 4.0, None, None, None, -numpy.pi * 3 / 4.0], t_in=[2, 1, 1, 1, 1], t_out=[1, 1, 1, 1, 2], cycle=True, ) cell.add(gdspy.Polygon(c.get_points(), layer=13)) c = gdspy.Curve(0, 3, tolerance=1e-3) c.i( [(1, 2), (2, 1), (3, 2), (4, 0)], angles=[numpy.pi * 3 / 4.0, None, None, None, -numpy.pi * 3 / 4.0], t_in=[1, 1, 1, 1, 1], t_out=[1, 1, 1, 1, 1], cycle=True, ) cell.add(gdspy.Polygon(c.get_points(), layer=14)) assertsame(target["Hobby4"], cell) gdspy-1.4.2/tests/flexpath.py000066400000000000000000000252741354474061200162130ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### from tutils import target, assertsame import numpy import pytest import gdspy def broken(p0, v0, p1, v1, p2, w): den = v1[1] * v0[0] - v1[0] * v0[1] lim = 1e-12 * (v0[0] ** 2 + v0[1] ** 2) * (v1[0] ** 2 + v1[1] ** 2) if den ** 2 < lim: u0 = u1 = 0 p = 0.5 * (p0 + p1) else: dx = p1[0] - p0[0] dy = p1[1] - p0[1] u0 = (v1[1] * dx - v1[0] * dy) / den u1 = (v0[1] * dx - v0[0] * dy) / den p = 0.5 * (p0 + v0 * u0 + p1 + v1 * u1) if u0 <= 0 and u1 >= 0: return [p] return [p0, p2, p1] def pointy(p0, v0, p1, v1): r = 0.5 * numpy.sqrt(numpy.sum((p0 - p1) ** 2)) v0 /= numpy.sqrt(numpy.sum(v0 ** 2)) v1 /= numpy.sqrt(numpy.sum(v1 ** 2)) return [p0, 0.5 * (p0 + p1) + 0.5 * (v0 - v1) * r, p1] def test_flexpath_warnings(): f = lambda *args: None for ends in ["smooth", f]: with pytest.warns(UserWarning): gdspy.FlexPath([(0, 0)], 1, ends=ends, gdsii_path=True) for corners in ["miter", "bevel", "round", "smooth", f]: with pytest.warns(UserWarning): gdspy.FlexPath([(0, 0)], 1, corners=corners, gdsii_path=True) def test_flexpath1(target): cell = gdspy.Cell("test", True) fp = gdspy.FlexPath([(0, 0), (1, 1)], 0.1, layer=[1], gdsii_path=True) cell.add(fp) fp = gdspy.FlexPath( [(1, 0), (2, 1)], 0.1, [-0.1, 0.1], tolerance=1e-5, ends=["round", "extended"], layer=[2, 3], max_points=6, ) cell.add(fp) fp = gdspy.FlexPath( [(2, 0), (3, 1)], [0.1, 0.2], 0.2, ends=(0.2, 0.1), layer=4, datatype=[1, 1] ) cell.add(fp) fp = gdspy.FlexPath( [(3, 0), (4, 1)], [0.1, 0.2, 0.1], [-0.2, 0, 0.2], ends=[(0.2, 0.1), "smooth", pointy], datatype=5, ) cell.add(fp) assertsame(target["FlexPath1"], cell) def test_flexpath2(target): cell = gdspy.Cell("test", True) fp = gdspy.FlexPath( [(0, 0), (0.5, 0), (1, 0), (1, 1), (0, 1), (-1, -2), (-2, 0)], 0.05, [0, -0.1, 0, 0.1], corners=["natural", "circular bend", "circular bend", "circular bend"], ends=["flush", "extended", (0.1, 0.2), "round"], tolerance=1e-4, layer=[0, 1, 1, 2], bend_radius=[0, 0.3, 0.3, 0.2], max_points=10, ) cell.add(fp) assertsame(target["FlexPath2"], cell) cell = gdspy.Cell("test2", True) def test_flexpath3(target): cell = gdspy.Cell("test", True) pts = numpy.array( [ (0, 0), (0.5, 0), (1, 0), (1, 2), (3, 0), (2, -1), (2, -2), (0, -1), (1, -2), (1, -3), ] ) fp = gdspy.FlexPath( pts + numpy.array((0, 5)), [0.1, 0.1, 0.1], 0.15, layer=[1, 2, 3], corners=["natural", "miter", "bevel"], ends=(0.5, 0), ) cell.add(fp) fp = gdspy.FlexPath( pts + numpy.array((5, 0)), [0.1, 0.1, 0.1], 0.15, layer=[4, 5, 6], corners=["round", "smooth", broken], ends=[pointy, "smooth", (0, 0.5)], ) cell.add(fp) assertsame(target["FlexPath3"], cell) def test_flexpath4(target): cell = gdspy.Cell("test", True) fp = gdspy.FlexPath( [(0, 0)], [0.1, 0.2, 0.1], 0.15, layer=[1, 2, 3], corners=["natural", "miter", "bevel"], ) fp.segment((1, 0)) fp.segment((1, 1), 0.1, 0.05) fp.segment((1, 1), [0.2, 0.1, 0.1], -0.05, True) fp.segment((-1, 1), 0.2, [-0.2, 0, 0.3], True) fp.arc(2, 0, 0.5 * numpy.pi) fp.arc(3, 0.5 * numpy.pi, numpy.pi, 0.1, 0) fp.arc(1, 0.4 * numpy.pi, -0.4 * numpy.pi, [0.1, 0.2, 0.1], [0.2, 0, -0.2]) fp.turn(1, 0.4 * numpy.pi) fp.turn(1, "ll", 0.15, 0) fp.turn(0.5, "r", [0.1, 0.05, 0.1], [0.15, 0, -0.15]) cell.add(fp) fp = gdspy.FlexPath([(-5, 6)], 0.8, layer=20, ends="round", tolerance=1e-4) fp.segment((1, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath([(-5, 6)], 0.8, layer=21, ends="extended", tolerance=1e-4) fp.segment((1, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath([(-5, 6)], 0.8, layer=22, ends=(0.1, 0.2), tolerance=1e-4) fp.segment((1, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath([(-5, 6)], 0.8, layer=23, ends="smooth", tolerance=1e-4) fp.segment((1, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath( [(-3, 6)], 0.8, layer=10, corners="round", ends="round", tolerance=1e-5 ) fp.segment((1, 0), 0.1, relative=True) fp.segment((0, 1), 0.8, relative=True) cell.add(fp) fp = gdspy.FlexPath( [(-3, 6)], 0.8, layer=11, corners="smooth", ends="extended", tolerance=1e-5 ) fp.segment((1, 0), 0.1, relative=True) fp.segment((0, 1), 0.8, relative=True) cell.add(fp) fp = gdspy.FlexPath( [(-3, 6)], 0.8, layer=12, corners="smooth", ends="smooth", tolerance=1e-5 ) fp.segment((1, 0), 0.1, relative=True) fp.segment((0, 1), 0.8, relative=True) cell.add(fp) fp = gdspy.FlexPath( [(-3, 8)], 0.1, layer=13, corners="round", ends="round", tolerance=1e-5 ) fp.segment((1, 0), 0.8, relative=True) fp.segment((0, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath( [(-3, 8)], 0.1, layer=14, corners="smooth", ends=(0.2, 0.2), tolerance=1e-5 ) fp.segment((1, 0), 0.8, relative=True) fp.segment((0, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath( [(-3, 8)], 0.1, layer=15, corners="round", ends="smooth", tolerance=1e-5 ) fp.segment((1, 0), 0.8, relative=True) fp.segment((0, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath([(5, 2)], [0.05, 0.1, 0.2], [-0.2, 0, 0.4], layer=[4, 5, 6]) fp.parametric(lambda u: numpy.array((5.5 + 3 * u, 2 + 3 * u ** 2)), relative=False) fp.segment((0, 1), relative=True) fp.parametric( lambda u: numpy.array( (2 * numpy.cos(0.5 * numpy.pi * u) - 2, 3 * numpy.sin(0.5 * numpy.pi * u)) ), [0.2, 0.1, 0.05], [-0.3, 0, 0.3], ) fp.parametric(lambda u: numpy.array((-2 * u, 0)), 0.1, 0.2) fp.bezier([(-3, 0), (-2, -3), (0, -4), (0, -5)], offset=[-0.2, 0, 0.2]) fp.bezier( [(5, 0), (1, -1), (1, 5), (3, 2), (5, 2)], [0.05, 0.1, 0.2], [-0.2, 0, 0.4], relative=False, ) cell.add(fp) fp = gdspy.FlexPath([(2, -1)], 0.1, layer=7, tolerance=1e-5, max_points=0) fp.smooth( [(1, 0), (1, -1), (0, -1)], angles=[numpy.pi / 3, None, -2 / 3.0 * numpy.pi, None], cycle=True, ) cell.add(fp) fp = gdspy.FlexPath([(2.5, -1.5)], 0.1, layer=8) fp.smooth( [(3, -1.5), (4, -2), (5, -1), (6, -2), (7, -1.5), (7.5, -1.5)], relative=False, width=0.2, ) cell.add(fp) assertsame(target["FlexPath4"], cell) def test_flexpath_gdsiipath(): cells = [] for gdsii_path in [True, False]: cells.append(gdspy.Cell(str(gdsii_path), True)) fp = gdspy.FlexPath( [(0, 0), (0.5, 0), (1, 0), (1, 1), (0, 1), (-1, -2), (-2, 0)], 0.05, [-0.1, 0.1], corners=["natural", "circular bend"], ends=["extended", (0.1, 0.2)], layer=[0, 1], bend_radius=[0, 0.3], gdsii_path=gdsii_path, ) cells[-1].add(fp) assertsame(*cells) def test_flexpath_getpolygons(): fp = gdspy.FlexPath( [(0, 0), (0.5, 0), (1, 0), (1, 1), (0, 1), (-1, -2), (-2, 0)], 0.05, [-0.1, 0.1], corners=["natural", "circular bend"], ends=["extended", (0.1, 0.2)], bend_radius=[0, 0.3], layer=[0, 1], datatype=[1, 0], ) d = fp.get_polygons(True) l = fp.get_polygons() assert len(d) == 2 assert (1, 0) in d assert (0, 1) in d assert sum(len(p) for p in d.values()) == len(l) assert sum(sum(len(x) for x in p) for p in d.values()) == sum(len(x) for x in l) ps = fp.to_polygonset() assert len(ps.layers) == len(ps.datatypes) == len(ps.polygons) assert gdspy.FlexPath([(0, 0)], 1).to_polygonset() == None def test_flexpath_togds(tmpdir): cell = gdspy.Cell("flexpath") fp = gdspy.FlexPath([(0, 0), (0.5, 0.5), (1, 1)], 0.1, layer=[1], gdsii_path=True) cell.add(fp) fp = gdspy.FlexPath([(3, 0), (3.5, 0.5), (4, 1)], 0.1, layer=[21]) cell.add(fp) fp = gdspy.FlexPath( [(1, 0), (2, 1)], 0.1, [-0.1, 0.1], max_points=6, ends=["round", "extended"], layer=[2, 3], gdsii_path=True, ) cell.add(fp) fp = gdspy.FlexPath( [(2, 0), (3, 1)], [0.1, 0.2], 0.2, ends=(0.2, 0.1), layer=4, datatype=[10, 10], gdsii_path=True, ) cell.add(fp) fp = gdspy.FlexPath( [(0, 0), (0.5, 0), (1, 0), (1, 1), (0, 1), (-1, -2), (-2, 0)], 0.05, [0, -0.1, 0, 0.1], corners=["natural", "circular bend", "circular bend", "circular bend"], tolerance=1e-5, ends=["flush", "extended", (0.1, 0.2), "flush"], layer=[10, 11, 11, 12], bend_radius=[0, 0.3, 0.3, 0.2], gdsii_path=True, ).translate(-5, 0) cell.add(fp) fp = gdspy.FlexPath( [(i, 2 + i ** 2) for i in numpy.linspace(0, 1, 8192)], 0.01, gdsii_path=True ) cell.add(fp) fname = str(tmpdir.join("test.gds")) with pytest.warns(UserWarning): gdspy.write_gds(fname, unit=1, precision=1e-7) lib = gdspy.GdsLibrary(infile=fname, rename={"flexpath": "file"}) assertsame(lib.cell_dict["file"], cell, tolerance=1e-3) gdspy.current_library = gdspy.GdsLibrary() def test_flexpath_duplicates(): fp = gdspy.FlexPath([(1.1, 2), (1.1, 2.0)], 0.1) poly = fp.get_polygons() assert isinstance(poly, list) and len(poly) == 0 poly = fp.get_polygons(True) assert isinstance(poly, dict) and len(poly) == 0 fp1 = gdspy.FlexPath( [(1.1, 2), (1.1, 2.0), (1, 1), (1, 1), (1, 1.0), (0, 1), (0.0, 1.0)], 0.1 ) fp2 = gdspy.FlexPath([(1.1, 2), (1, 1.0), (0.0, 1.0)], 0.1) assertsame(gdspy.Cell("DUPS").add(fp1), gdspy.Cell("SNGL").add(fp2)) gdspy-1.4.2/tests/functions.py000066400000000000000000000253401354474061200164020ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### import pytest import datetime import numpy import gdspy def equals(x, y): return gdspy.boolean(x, y, "xor") is None @pytest.fixture() def library(): lib = gdspy.GdsLibrary() c1 = gdspy.Cell("cell1", True) c1.add(gdspy.Rectangle((0, -1), (1, 2), 2, 4)) c1.add(gdspy.Label("label", (1, -1), "w", 45, 1.5, True, 5, 6)) c2 = gdspy.Cell("cell2", True) c2.add(gdspy.Round((0, 0), 1, number_of_points=32, max_points=20)) c3 = gdspy.Cell("cell3", True) c3.add(gdspy.CellReference(c1, (0, 1), -90, 2, True)) c4 = gdspy.Cell("cell04", True) c4.add(gdspy.CellArray(c2, 2, 3, (1, 4), (-1, -2), 180, 0.5, True)) lib.add([c1, c2, c3, c4]) return lib def test_8b_f(): f = gdspy._eight_byte_real_to_float assert f(b"\x00\x00\x00\x00\x00\x00\x00\x00") == 0 assert f(b"\x41\x10\x00\x00\x00\x00\x00\x00") == 1 assert f(b"\x41\x20\x00\x00\x00\x00\x00\x00") == 2 assert f(b"\xC1\x30\x00\x00\x00\x00\x00\x00") == -3 def test_f_8b(): g = gdspy._eight_byte_real assert b"\x00\x00\x00\x00\x00\x00\x00\x00" == g(0) assert b"\x41\x10\x00\x00\x00\x00\x00\x00" == g(1) assert b"\x41\x20\x00\x00\x00\x00\x00\x00" == g(2) assert b"\xC1\x30\x00\x00\x00\x00\x00\x00" == g(-3) def test_twoway(): f = gdspy._eight_byte_real_to_float g = gdspy._eight_byte_real for x in [0, 1.5, -numpy.pi, 1 / 3.0e12, -1.0e12 / 7, 1.1e75, -0.9e-78]: assert x == f(g(x)) for _ in range(10000): x = 10 ** (numpy.random.random() * 150 - 75) assert x == f(g(x)) def test_gather(): def same_points(x, y): for px, py in zip(x, y): for ptx, pty in zip(px, py): for cx, cy in zip(ptx, pty): if cx != cy: return False return True gdspy.current_library = gdspy.GdsLibrary() pts = [(0, 0), (1, 1), (1, 0)] ps1 = gdspy.Round((10, 10), 1, inner_radius=0.2) ps2 = gdspy.Path(0.1, (-1, -1), 2, 1).segment(2, "-x") c = gdspy.Cell("C1").add(gdspy.Rectangle((-4, 3), (-5, 4))) cr = gdspy.CellReference(c, (10, -10)) ca = gdspy.CellArray(c, 2, 1, (2, 0)) assert gdspy._gather_polys(None) == [] assert same_points(gdspy._gather_polys([pts]), [pts]) assert same_points(gdspy._gather_polys(ps1), ps1.polygons) assert same_points(gdspy._gather_polys(ps2), ps2.polygons) assert same_points(gdspy._gather_polys(cr), cr.get_polygons()) assert same_points(gdspy._gather_polys(ca), ca.get_polygons()) result = [pts] result.extend(ps2.polygons) result.extend(cr.get_polygons()) assert same_points(gdspy._gather_polys([pts, ps2, cr]), result) def test_slice(): poly = gdspy.Path(1, (1, 0), 2, 3).segment(2, "-x") left = gdspy.Path(1, (0, 0), 2, 3).segment(1, "-x") right = gdspy.Path(1, (1, 0), 2, 3).segment(1, "-x") result = gdspy.slice(poly, 0, 0) assert equals(result[0], left) assert equals(result[1], right) bot = gdspy.Path(1, (1, -1.5)).segment(2, "-x") top = gdspy.Path(1, (1, 1.5)).segment(2, "-x") result = gdspy.slice(poly, [0.1, -0.1], 1) assert equals(result[0], bot) assert equals(result[2], top) assert result[1] is None def test_offset(): gdspy.current_library = gdspy.GdsLibrary() r = gdspy.Rectangle((0, 0), (1, 2)) result = gdspy.Rectangle((-1, -1), (2, 3)) assert equals(gdspy.offset(r, 1), result) c = gdspy.Cell("OFFSET").add(r) ca = gdspy.CellArray(c, 2, 1, (1, 0)) result = gdspy.Rectangle((0.2, 0.2), (1.8, 1.8)) assert equals(gdspy.offset([ca], -0.2, join_first=True), result) v = [gdspy.Rectangle((-1, -1), (1, 1)), [(0, 0), (1, 0), (1, 1), (0, 1)]] x = 1 + 0.1 * numpy.tan(numpy.pi / 8) result = gdspy.Polygon( [ (-1.1, -x), (-1.1, x), (-x, 1.1), (x, 1.1), (1.1, x), (1.1, -x), (x, -1.1), (-x, -1.1), ], layer=8, ) assert equals(gdspy.offset(v, 0.1, join="bevel", layer=12), result) def test_boolean(): op1 = gdspy.Rectangle((0, 0), (3, 3)) op2 = gdspy.Rectangle((1, 1), (2, 2)) result = [ [(0, 0), (3, 0), (3, 3), (0, 3), (0, 0), (1, 1), (1, 2), (2, 2), (2, 1), (1, 1)] ] assert equals(gdspy.boolean(op1, op2, "not"), result) op3 = gdspy.Rectangle((0, 0), (2, 2)) assert equals(gdspy.boolean([op2, op3], None, "or"), op3) def test_inside(): gdspy.current_library = gdspy.GdsLibrary() polygons = [ gdspy.Round((0, 0), 10, inner_radius=5, number_of_points=180), gdspy.Rectangle((20, -10), (40, 10)).polygons[0], gdspy.CellReference(gdspy.Cell("X").add(gdspy.Rectangle((-10, 0), (10, 20)))), ] assert gdspy.inside([(0, 0)], polygons[0]) == (False,) assert gdspy.inside([(0, 0)], polygons[2]) == (True,) assert gdspy.inside([(0, 0)], polygons) == (True,) assert gdspy.inside([(0, 0), (0, 30), (30, 0), (0, -1)], polygons) == ( True, False, True, False, ) assert gdspy.inside( [[(0, 0), (0, 30), (30, 0), (0, -1)], [(0, -1), (0, 30)], [(0, 0), (30, 0)]], polygons, "any", ) == (True, False, True) assert gdspy.inside( [[(0, 0), (0, 30), (30, 0), (0, -1)], [(0, -1), (0, 30)], [(0, 0), (30, 0)]], polygons, "all", ) == (False, False, True) def test_copy(): gdspy.current_library = gdspy.GdsLibrary() p = gdspy.Rectangle((0, 0), (1, 1)) q = gdspy.copy(p, 1, -1) assert set(p.polygons[0][:, 0]) == {0, 1} assert set(p.polygons[0][:, 1]) == {0, 1} assert set(q.polygons[0][:, 0]) == {1, 2} assert set(q.polygons[0][:, 1]) == {-1, 0} p = gdspy.PolygonSet([[(0, 0), (1, 0), (0, 1)], [(2, 2), (3, 2), (2, 3)]]) q = gdspy.copy(p, 1, -1) assert set(p.polygons[0][:, 0]) == {0, 1} assert set(p.polygons[0][:, 1]) == {0, 1} assert set(q.polygons[0][:, 0]) == {1, 2} assert set(q.polygons[0][:, 1]) == {-1, 0} assert set(p.polygons[1][:, 0]) == {2, 3} assert set(p.polygons[1][:, 1]) == {2, 3} assert set(q.polygons[1][:, 0]) == {3, 4} assert set(q.polygons[1][:, 1]) == {1, 2} l = gdspy.Label("text", (0, 1)) m = gdspy.copy(l, -1, 1) assert l.position[0] == 0 and l.position[1] == 1 assert m.position[0] == -1 and m.position[1] == 2 c = gdspy.CellReference("empty", (0, 1), ignore_missing=True) d = gdspy.copy(c, -1, 1) assert c.origin == (0, 1) assert d.origin == (-1, 2) c = gdspy.CellArray("empty", 2, 3, (1, 0), (0, 1), ignore_missing=True) d = gdspy.copy(c, -1, 1) assert c.origin == (0, 1) assert d.origin == (-1, 2) def test_write_gds(library, tmpdir): gdspy.current_library = library fname1 = str(tmpdir.join("test1.gds")) gdspy.write_gds(fname1, name="lib", unit=2e-6, precision=1e-8) lib1 = gdspy.GdsLibrary( infile=fname1, units="convert", rename={"cell1": "1"}, layers={2: 4}, datatypes={4: 2}, texttypes={6: 7}, ) assert lib1.name == "lib" assert len(lib1.cell_dict) == 4 assert set(lib1.cell_dict.keys()) == {"1", "cell2", "cell3", "cell04"} c = lib1.cell_dict["1"] assert len(c.polygons) == len(c.labels) == 1 assert c.polygons[0].area() == 12.0 assert c.polygons[0].layers == [4] assert c.polygons[0].datatypes == [2] assert c.labels[0].text == "label" assert c.labels[0].position[0] == 2 and c.labels[0].position[1] == -2 assert c.labels[0].anchor == 4 assert c.labels[0].rotation == 45 assert c.labels[0].magnification == 1.5 assert c.labels[0].x_reflection == True assert c.labels[0].layer == 5 assert c.labels[0].texttype == 7 c = lib1.cell_dict["cell2"] assert len(c.polygons) == 2 assert isinstance(c.polygons[0], gdspy.Polygon) and isinstance( c.polygons[1], gdspy.Polygon ) c = lib1.cell_dict["cell3"] assert len(c.references) == 1 assert c.references[0].ref_cell == lib1.cell_dict["1"] assert c.references[0].origin[0] == 0 and c.references[0].origin[1] == 2 assert c.references[0].rotation == -90 assert c.references[0].magnification == 2 assert c.references[0].x_reflection == True c = lib1.cell_dict["cell04"] assert len(c.references) == 1 assert c.references[0].ref_cell == lib1.cell_dict["cell2"] assert c.references[0].origin[0] == -2 and c.references[0].origin[1] == -4 assert c.references[0].rotation == 180 assert c.references[0].magnification == 0.5 assert c.references[0].x_reflection == True assert c.references[0].spacing[0] == 2 and c.references[0].spacing[1] == 8 assert c.references[0].columns == 2 assert c.references[0].rows == 3 fname2 = str(tmpdir.join("test2.gds")) with open(fname2, "wb") as fout: gdspy.write_gds(fout, name="lib2", unit=2e-3, precision=1e-5) with open(fname2, "rb") as fin: lib2 = gdspy.GdsLibrary() lib2.read_gds(fin) assert lib2.name == "lib2" assert len(lib2.cell_dict) == 4 def test_gdsii_hash(library, tmpdir): out1 = str(tmpdir.join("test1.gds")) out2 = str(tmpdir.join("test2.gds")) library.write_gds(out1) library.write_gds(out2, timestamp=datetime.datetime.today() + datetime.timedelta(1)) assert gdspy.gdsii_hash(out1) == gdspy.gdsii_hash(out2) def test_get_gds_units(tmpdir): out = str(tmpdir.join("test1.gds")) lib = gdspy.GdsLibrary(unit=10.0, precision=0.1) lib.write_gds(out) assert (10.0, 0.1) == gdspy.get_gds_units(out) lib.unit = 0.2 lib.precision = 5e-5 out = str(tmpdir.join("test2.gds")) lib.write_gds(out) with open(out, "rb") as fin: assert (0.2, 5e-5) == gdspy.get_gds_units(fin) def test_get_binary_cells(library, tmpdir): out = str(tmpdir.join("test.gds")) now = datetime.datetime.today() library.write_gds(out, timestamp=now) bincells = gdspy.get_binary_cells(out) for name, cell in library.cell_dict.items(): bindata = cell.to_gds(library.unit / library.precision, timestamp=now) assert bindata == bincells[name] with open(out, "rb") as fin: bincells = gdspy.get_binary_cells(fin) for name, cell in library.cell_dict.items(): bindata = cell.to_gds(library.unit / library.precision, timestamp=now) assert bindata == bincells[name] gdspy-1.4.2/tests/gdslibrary.py000066400000000000000000000152451354474061200165370ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### import pytest import gdspy import uuid def unique(): return str(uuid.uuid4()) def test_add(): lib = gdspy.GdsLibrary() c1 = gdspy.Cell("gl_add_1", exclude_from_current=True) c2 = gdspy.Cell("gl_add_2", exclude_from_current=True) c3 = gdspy.Cell("gl_add_3", exclude_from_current=True) lib.add(c1) lib.add((c2, c3)) assert lib.cell_dict == {"gl_add_1": c1, "gl_add_2": c2, "gl_add_3": c3} def test_duplicate(): name = "gl_duplicate" lib = gdspy.GdsLibrary() lib.add(gdspy.Cell(name, exclude_from_current=True)) c = gdspy.Cell(name, exclude_from_current=True) with pytest.raises(ValueError) as e: lib.add(c) assert name in str(e.value) lib.add(c, True) assert lib.cell_dict == {name: c} cl = [ gdspy.Cell(name, exclude_from_current=True), gdspy.Cell(name + "1", exclude_from_current=True), ] with pytest.raises(ValueError) as e: lib.add(cl) assert name in str(e.value) lib.add(cl, True) assert lib.cell_dict == {name: cl[0], name + "1": cl[1]} @pytest.fixture def tree(): c = [gdspy.Cell("tree_" + unique(), True) for _ in range(8)] lib = gdspy.GdsLibrary() lib.add(c) c[0].add(gdspy.CellReference(c[1])) c[0].add(gdspy.CellReference(c[3])) c[1].add(gdspy.CellReference(c[2])) c[1].add(gdspy.CellArray(c[2], 2, 1, (0, 0))) c[1].add(gdspy.CellArray(c[3], 2, 1, (0, 0))) c[4].add(gdspy.CellReference(c[3])) c[6].add(gdspy.CellArray(c[5], 2, 1, (0, 0))) return lib, c def test_top_level_1(tree): lib, c = tree tl = lib.top_level() assert len(tl) == 4 and c[0] in tl and c[4] in tl and c[6] in tl and c[7] in tl def test_top_level_2(tree): lib, c = tree c[7].add(gdspy.CellReference(c[0])) c[7].add(gdspy.CellReference(c[4])) c[7].add(gdspy.CellReference(c[6])) assert lib.top_level() == [c[7]] def test_top_level_3(tree): lib, c = tree c[7].add(gdspy.CellReference(c[0])) c[3].add(gdspy.CellReference(c[4])) c[2].add(gdspy.CellReference(c[6])) c[1].add(gdspy.CellReference(c[7])) assert lib.top_level() == [] def test_extract(): c = [gdspy.Cell("tree_" + unique(), True) for _ in range(8)] gdspy.current_library = gdspy.GdsLibrary() lib = gdspy.GdsLibrary() lib.add(c) c[0].add(gdspy.CellReference(c[1])) c[0].add(gdspy.CellReference(c[3])) c[1].add(gdspy.CellReference(c[2])) c[1].add(gdspy.CellArray(c[2], 2, 1, (0, 0))) c[1].add(gdspy.CellArray(c[3], 2, 1, (0, 0))) c[4].add(gdspy.CellReference(c[3])) c[6].add(gdspy.CellArray(c[5], 2, 1, (0, 0))) assert len(gdspy.current_library.cell_dict) == 0 lib.extract(c[7]) assert gdspy.current_library.cell_dict == {c[7].name: c[7]} lib.extract(c[1]) assert gdspy.current_library.cell_dict == { c[7].name: c[7], c[1].name: c[1], c[2].name: c[2], c[3].name: c[3], } lib.extract(c[0]) assert gdspy.current_library.cell_dict == { c[7].name: c[7], c[0].name: c[0], c[1].name: c[1], c[2].name: c[2], c[3].name: c[3], } def test_rw_gds(tmpdir): lib = gdspy.GdsLibrary("lib", unit=2e-3, precision=1e-5) c1 = gdspy.Cell("gl_rw_gds_1", True) c1.add(gdspy.Rectangle((0, -1), (1, 2), 2, 4)) c1.add(gdspy.Label("label", (1, -1), "w", 45, 1.5, True, 5, 6)) c2 = gdspy.Cell("gl_rw_gds_2", True) c2.add(gdspy.Round((0, 0), 1, number_of_points=32, max_points=20)) c3 = gdspy.Cell("gl_rw_gds_3", True) c3.add(gdspy.CellReference(c1, (0, 1), -90, 2, True)) c4 = gdspy.Cell("gl_rw_gds_4", True) c4.add(gdspy.CellArray(c2, 2, 3, (1, 4), (-1, -2), 180, 0.5, True)) lib.add((c1, c2, c3, c4)) fname1 = str(tmpdir.join("test1.gds")) lib.write_gds(fname1) lib1 = gdspy.GdsLibrary( infile=fname1, unit=1e-3, precision=1e-6, units="convert", rename={"gl_rw_gds_1": "1"}, layers={2: 4}, datatypes={4: 2}, texttypes={6: 7}, ) assert lib1.name == "lib" assert len(lib1.cell_dict) == 4 assert set(lib1.cell_dict.keys()) == { "1", "gl_rw_gds_2", "gl_rw_gds_3", "gl_rw_gds_4", } c = lib1.cell_dict["1"] assert len(c.polygons) == len(c.labels) == 1 assert c.polygons[0].area() == 12.0 assert c.polygons[0].layers == [4] assert c.polygons[0].datatypes == [2] assert c.labels[0].text == "label" assert c.labels[0].position[0] == 2 and c.labels[0].position[1] == -2 assert c.labels[0].anchor == 4 assert c.labels[0].rotation == 45 assert c.labels[0].magnification == 1.5 assert c.labels[0].x_reflection == True assert c.labels[0].layer == 5 assert c.labels[0].texttype == 7 c = lib1.cell_dict["gl_rw_gds_2"] assert len(c.polygons) == 2 assert isinstance(c.polygons[0], gdspy.Polygon) and isinstance( c.polygons[1], gdspy.Polygon ) c = lib1.cell_dict["gl_rw_gds_3"] assert len(c.references) == 1 assert isinstance(c.references[0], gdspy.CellReference) assert c.references[0].ref_cell == lib1.cell_dict["1"] assert c.references[0].origin[0] == 0 and c.references[0].origin[1] == 2 assert c.references[0].rotation == -90 assert c.references[0].magnification == 2 assert c.references[0].x_reflection == True c = lib1.cell_dict["gl_rw_gds_4"] assert len(c.references) == 1 assert isinstance(c.references[0], gdspy.CellArray) assert c.references[0].ref_cell == lib1.cell_dict["gl_rw_gds_2"] assert c.references[0].origin[0] == -2 and c.references[0].origin[1] == -4 assert c.references[0].rotation == 180 assert c.references[0].magnification == 0.5 assert c.references[0].x_reflection == True assert c.references[0].spacing[0] == 2 and c.references[0].spacing[1] == 8 assert c.references[0].columns == 2 assert c.references[0].rows == 3 fname2 = str(tmpdir.join("test2.gds")) lib.name = "lib2" with open(fname2, "wb") as fout: lib.write_gds(fout) with open(fname2, "rb") as fin: lib2 = gdspy.GdsLibrary() lib2.read_gds(fin) assert lib2.name == "lib2" assert len(lib2.cell_dict) == 4 gdspy-1.4.2/tests/gdswriter.py000066400000000000000000000075011354474061200164030ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### import gdspy def test_writer_gds(tmpdir): lib = gdspy.GdsLibrary() c1 = gdspy.Cell("gw_rw_gds_1", True) c1.add(gdspy.Rectangle((0, -1), (1, 2), 2, 4)) c1.add(gdspy.Label("label", (1, -1), "w", 45, 1.5, True, 5, 6)) c2 = gdspy.Cell("gw_rw_gds_2", True) c2.add(gdspy.Round((0, 0), 1, number_of_points=32, max_points=20)) c3 = gdspy.Cell("gw_rw_gds_3", True) c3.add(gdspy.CellReference(c1, (0, 1), -90, 2, True)) c4 = gdspy.Cell("gw_rw_gds_4", True) c4.add(gdspy.CellArray(c2, 2, 3, (1, 4), (-1, -2), 180, 0.5, True)) lib.add((c1, c2, c3, c4)) fname1 = str(tmpdir.join("test1.gds")) writer1 = gdspy.GdsWriter(fname1, name="lib", unit=2e-3, precision=1e-5) for c in lib.cell_dict.values(): writer1.write_cell(c) writer1.close() lib1 = gdspy.GdsLibrary(unit=1e-3) lib1.read_gds( fname1, units="convert", rename={"gw_rw_gds_1": "1"}, layers={2: 4}, datatypes={4: 2}, texttypes={6: 7}, ) assert lib1.name == "lib" assert len(lib1.cell_dict) == 4 assert set(lib1.cell_dict.keys()) == { "1", "gw_rw_gds_2", "gw_rw_gds_3", "gw_rw_gds_4", } c = lib1.cell_dict["1"] assert len(c.polygons) == len(c.labels) == 1 assert c.polygons[0].area() == 12.0 assert c.polygons[0].layers == [4] assert c.polygons[0].datatypes == [2] assert c.labels[0].text == "label" assert c.labels[0].position[0] == 2 and c.labels[0].position[1] == -2 assert c.labels[0].anchor == 4 assert c.labels[0].rotation == 45 assert c.labels[0].magnification == 1.5 assert c.labels[0].x_reflection == True assert c.labels[0].layer == 5 assert c.labels[0].texttype == 7 c = lib1.cell_dict["gw_rw_gds_2"] assert len(c.polygons) == 2 assert isinstance(c.polygons[0], gdspy.Polygon) and isinstance( c.polygons[1], gdspy.Polygon ) c = lib1.cell_dict["gw_rw_gds_3"] assert len(c.references) == 1 assert isinstance(c.references[0], gdspy.CellReference) assert c.references[0].ref_cell == lib1.cell_dict["1"] assert c.references[0].origin[0] == 0 and c.references[0].origin[1] == 2 assert c.references[0].rotation == -90 assert c.references[0].magnification == 2 assert c.references[0].x_reflection == True c = lib1.cell_dict["gw_rw_gds_4"] assert len(c.references) == 1 assert isinstance(c.references[0], gdspy.CellArray) assert c.references[0].ref_cell == lib1.cell_dict["gw_rw_gds_2"] assert c.references[0].origin[0] == -2 and c.references[0].origin[1] == -4 assert c.references[0].rotation == 180 assert c.references[0].magnification == 0.5 assert c.references[0].x_reflection == True assert c.references[0].spacing[0] == 2 and c.references[0].spacing[1] == 8 assert c.references[0].columns == 2 assert c.references[0].rows == 3 fname2 = str(tmpdir.join("test2.gds")) with open(fname2, "wb") as fout: writer2 = gdspy.GdsWriter(fout, name="lib2", unit=2e-3, precision=1e-5) for c in lib.cell_dict.values(): writer2.write_cell(c) writer2.close() with open(fname2, "rb") as fin: lib2 = gdspy.GdsLibrary() lib2.read_gds(fin) assert lib2.name == "lib2" assert len(lib2.cell_dict) == 4 gdspy-1.4.2/tests/polygonset.py000066400000000000000000000136161354474061200166000ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### from tutils import target, assertsame import numpy import pytest import gdspy def test_polygonset(target): ps = gdspy.PolygonSet( [ [(10, 0), (11, 0), (10, 1)], numpy.array([(11.0, 0), (10, 1), (11, 1)]), [numpy.array((11, 1)), numpy.array((12.0, 1.0)), (11, 2)], ], 1, 2, ) assert str(ps) == "PolygonSet (3 polygons, 9 vertices, layers [1], datatypes [2])" assert ps.area() == 1.5 assert ps.area(True) == {(1, 2): 1.5} bb = ps.get_bounding_box() assert bb.shape == (2, 2) assert numpy.max(numpy.abs(bb - numpy.array(((10, 0), (12, 2))))) == 0 assert gdspy.PolygonSet([]).get_bounding_box() == None cell = gdspy.Cell("test", True).add(ps) assertsame(cell, target["PolygonSet"]) def test_translate(): ps = gdspy.PolygonSet([[(0, 0), (1, 0), (1, 2), (0, 2)]]) ps.translate(-1, -2) tgt = gdspy.PolygonSet([[(-1, -2), (0, -2), (0, 0), (-1, 0)]]) assertsame(gdspy.Cell("test", True).add(ps), gdspy.Cell("TGT", True).add(tgt)) def test_rotation(): ps = gdspy.PolygonSet([[(0, 0), (1, 0), (1, 2), (0, 2)]]) ps.rotate(-numpy.pi / 6, center=(1, 0)) x = 3 ** 0.5 / 2 a = numpy.arctan(2) + numpy.pi / 6 l = 5 ** 0.5 tgt = gdspy.PolygonSet( [[(1 - x, 0.5), (1, 0), (2, 2 * x), (1 - l * numpy.cos(a), l * numpy.sin(a))]] ) assertsame(gdspy.Cell("test", True).add(ps), gdspy.Cell("TGT", True).add(tgt)) def test_mirror(): ps = gdspy.PolygonSet([[(0, 0), (1, 0), (1, 2), (0, 2)]]) ps.mirror((-1, 1), (1, -1)) tgt = gdspy.PolygonSet([[(0, 0), (-2, 0), (-2, -1), (0, -1)]]) assertsame(gdspy.Cell("test", True).add(ps), gdspy.Cell("TGT", True).add(tgt)) def test_scale(): ps = gdspy.PolygonSet([[(0, 0), (1, 0), (1, 2), (0, 2)]]) cell = gdspy.Cell("test", True).add(ps) ps.scale(0.5) tgt = gdspy.PolygonSet([[(0, 0), (0.5, 0), (0.5, 1), (0, 1)]]) assertsame(cell, gdspy.Cell("TGT", True).add(tgt)) ps = gdspy.PolygonSet([[(0, 0), (1, 0), (1, 2), (0, 2)]]) cell = gdspy.Cell("test", True).add(ps) ps.scale(0.2, 2, center=(1, 2)) tgt = gdspy.PolygonSet([[(0.8, -2), (1, -2), (1, 2), (0.8, 2)]]) assertsame(cell, gdspy.Cell("TGT", True).add(tgt)) def test_togds(tmpdir): ps = gdspy.PolygonSet([[(10 + i * 1e-3, i ** 2 * 1e-6) for i in range(8191)]]) cell = gdspy.Cell("LARGE", True).add(ps) fname = str(tmpdir.join("large.gds")) lib = gdspy.GdsLibrary(unit=1, precision=1e-7).add(cell) with pytest.warns(UserWarning): lib.write_gds(fname) lib = gdspy.GdsLibrary(infile=fname) assertsame(lib.cell_dict["LARGE"], cell) def test_fracture(): ps1 = gdspy.PolygonSet( [[(-2, -1), (-2, 1), (-1, 2), (1, 2), (2, 1), (2, -1), (1, -2), (-1, -2)]] ) ps2 = gdspy.PolygonSet( [[(-2, -1), (-2, 1), (-1, 2), (1, 2), (2, 1), (2, -1), (1, -2), (-1, -2)]] ) ps2.fracture(5) assert all(len(p) <= 5 for p in ps2.polygons) assertsame(gdspy.Cell("1", True).add(ps1), gdspy.Cell("2", True).add(ps2)) def spiral(u): r = 4 - 3 * u theta = 5 * u * numpy.pi x = r * numpy.cos(theta) - 4 y = r * numpy.sin(theta) return (x, y) def dspiral_dt(u): theta = 5 * u * numpy.pi dx_dt = -numpy.sin(theta) dy_dt = numpy.cos(theta) return (dx_dt, dy_dt) pt1 = gdspy.Path(0.5, (0, 0)) pt1.parametric( spiral, dspiral_dt, number_of_evaluations=512, tolerance=10, max_points=0 ) assert len(pt1.polygons) == 1 assert len(pt1.polygons[0]) == 1024 pt2 = gdspy.Path(0.5, (0, 0)) pt2.parametric( spiral, dspiral_dt, number_of_evaluations=512, tolerance=10, max_points=0 ) pt2.fracture(199, precision=1e-6) assert all(len(p) <= 199 for p in pt2.polygons) assertsame(gdspy.Cell("3", True).add(pt1), gdspy.Cell("4", True).add(pt2)) def test_fillet(target): cell = gdspy.Cell("test") orig = gdspy.PolygonSet( [ [ (0, 0), (-1, 0), (0, -1), (0.5, -0.5), (1, 0), (1, 1), (4, -1), (1, 3), (1, 2), (0, 1), ], [(2, -1), (3, -1), (2.5, -2)], ] ) orig.datatypes = [0, 1] p = gdspy.copy(orig, 0, 5) p.layers = [1, 1] p.fillet(0.3, max_points=0) cell.add(p) p = gdspy.copy(orig, 5, 5) p.layers = [2, 2] p.fillet( [0.3, 0.2, 0.1, 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.4, 0.1, 0.2, 0], max_points=0 ) cell.add(p) p = gdspy.copy(orig, 5, 0) p.layers = [3, 3] p.fillet( [[0.1, 0.1, 0.4, 0, 0.4, 0.1, 0.1, 0.4, 0.4, 0.1], [0.2, 0.2, 0.5]], max_points=0, ) cell.add(p) p = gdspy.PolygonSet( [ [ (0, 0), (0, 0), (-1, 0), (0, -1), (0.5, -0.5), (1, 0), (1, 0), (1, 1), (4, -1), (1, 3), (1, 2), (0, 1), ], [(2, -1), (3, -1), (2.5, -2), (2, -1)], ], layer=4, ) p.datatypes = [0, 1] p.fillet([0.8, [10.0, 10.0, 20.0, 20.0]], max_points=199, precision=1e-6) cell.add(p) assertsame(cell, target["PolygonSet_fillet"]) gdspy-1.4.2/tests/robustpath.py000066400000000000000000000247501354474061200165710ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### from tutils import target, assertsame import numpy import pytest import gdspy def test_robustpath_warnings(): with pytest.warns(UserWarning): gdspy.RobustPath((0, 0), 1, ends="smooth", gdsii_path=True) with pytest.warns(UserWarning): gdspy.RobustPath((0, 0), [1, 1], ends=["flush", "smooth"], gdsii_path=True) def test_robustpath_len(): rp = gdspy.RobustPath((0, 0), [0.1, 0.2, 0.1], 0.15, layer=[1, 2, 3]) assert len(rp) == 0 rp.segment((1, 0)) rp.segment((1, 1), 0.1, 0.05) rp.segment((1, 1), [0.2, 0.1, 0.1], -0.05, True) rp.segment((-1, 1), 0.2, [-0.2, 0, 0.3], True) rp.arc(2, 0, 0.5 * numpy.pi) rp.arc(3, 0.7 * numpy.pi, numpy.pi, 0.1, 0) rp.arc(2, 0.4 * numpy.pi, -0.4 * numpy.pi, [0.1, 0.2, 0.1], [0.2, 0, -0.2]) rp.turn(1, -0.3 * numpy.pi) rp.turn(1, "rr", 0.15) rp.turn(0.5, "l", [0.05, 0.1, 0.05], [0.15, 0, -0.15]) assert len(rp) == 10 def test_robustpath_call(): rp = gdspy.RobustPath((0, 0), [0.1, 0.2, 0.1], 0.15, layer=[1, 2, 3]) rp.segment((1, 0)) rp.turn(5, "ll") rp.segment((-1, 0), relative=True) assert ( numpy.sum((rp(0) - numpy.array([(0, 0.15), (0, 0), (0, -0.15)])) ** 2) < 1e-12 ) assert ( numpy.sum((rp(0.5) - numpy.array([(0.5, 0.15), (0.5, 0), (0.5, -0.15)])) ** 2) < 1e-12 ) assert ( numpy.sum((rp(1) - numpy.array([(1, 0.15), (1, 0), (1, -0.15)])) ** 2) < 1e-12 ) assert ( numpy.sum((rp(1.5, arm=1) - numpy.array([(5.9, 5), (6.1, 5), (6.2, 5)])) ** 2) < 1e-12 ) assert ( numpy.sum((rp(1.5, arm=-1) - numpy.array([(5.8, 5), (5.9, 5), (6.1, 5)])) ** 2) < 1e-12 ) assert ( numpy.sum((rp(2) - numpy.array([(1, 9.85), (1, 10), (1, 10.15)])) ** 2) < 1e-12 ) assert ( numpy.sum((rp(3) - numpy.array([(0, 9.85), (0, 10), (0, 10.15)])) ** 2) < 1e-12 ) def test_robustpath_grad(): rp = gdspy.RobustPath((0, 0), 0.1) rp.segment((1, 0), 0.3) rp.segment((1, 1), 0.1) assert numpy.sum((numpy.array((1, 0)) - rp.grad(0)) ** 2) < 1e-12 assert numpy.sum((numpy.array((1, 0)) - rp.grad(1)) ** 2) < 1e-12 assert numpy.sum((numpy.array((0, 1)) - rp.grad(1, side="+")) ** 2) < 1e-12 assert numpy.sum((numpy.array((0, 1)) - rp.grad(2)) ** 2) < 1e-12 assert numpy.sum((numpy.array((0.1, 1)) - rp.grad(2, arm=-1)) ** 2) < 1e-12 assert numpy.sum((numpy.array((-0.1, 1)) - rp.grad(2, arm=1)) ** 2) < 1e-12 def test_robustpath_width(): rp = gdspy.RobustPath((0, 0), [0.1, 0.3]) rp.segment((1, 0), 0.3) rp.segment((1, 1), [0.1, 0.2]) assert numpy.sum((numpy.array((0.1, 0.3)) - rp.width(0)) ** 2) < 1e-12 assert numpy.sum((numpy.array((0.2, 0.3)) - rp.width(0.5)) ** 2) < 1e-12 assert numpy.sum((numpy.array((0.3, 0.3)) - rp.width(1)) ** 2) < 1e-12 assert numpy.sum((numpy.array((0.2, 0.25)) - rp.width(1.5)) ** 2) < 1e-12 assert numpy.sum((numpy.array((0.1, 0.2)) - rp.width(2)) ** 2) < 1e-12 def test_robustpath1(target): cell = gdspy.Cell("test", True) rp = gdspy.RobustPath((0, 0), 0.1, layer=[1], gdsii_path=True) rp.segment((1, 1)) cell.add(rp) rp = gdspy.RobustPath( (1, 0), 0.1, [-0.1, 0.1], tolerance=1e-5, ends=["round", "extended"], layer=[2, 3], max_points=6, ) rp.segment((2, 1)) cell.add(rp) rp = gdspy.RobustPath( (2, 0), [0.1, 0.2], 0.2, ends=(0.2, 0.1), layer=4, datatype=[1, 1] ) rp.segment((3, 1)) cell.add(rp) rp = gdspy.RobustPath( (3, 0), [0.1, 0.2, 0.1], [-0.2, 0, 0.2], ends=[(0.2, 0.1), "smooth", "flush"], datatype=5, ) rp.segment((4, 1)) cell.add(rp) assertsame(target["RobustPath1"], cell) def test_robustpath2(target): cell = gdspy.Cell("test", True) rp = gdspy.RobustPath((0, 0), [0.1, 0.2, 0.1], 0.15, layer=[1, 2, 3]) assert len(rp) == 0 rp.segment((1, 0)) rp.segment((1, 1), 0.1, 0.05) rp.segment((1, 1), [0.2, 0.1, 0.1], -0.05, True) rp.segment((-1, 1), 0.2, [-0.2, 0, 0.3], True) rp.arc(2, 0, 0.5 * numpy.pi) rp.arc(3, 0.7 * numpy.pi, numpy.pi, 0.1, 0) rp.arc(2, 0.4 * numpy.pi, -0.4 * numpy.pi, [0.1, 0.2, 0.1], [0.2, 0, -0.2]) rp.turn(1, -0.3 * numpy.pi) rp.turn(1, "rr", 0.15) rp.turn(0.5, "l", [0.05, 0.1, 0.05], [0.15, 0, -0.15]) assert len(rp) == 10 cell.add(rp) rp = gdspy.RobustPath((-5, 6), 0.8, layer=20, ends="round", tolerance=1e-4) rp.segment((1, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((-5, 6), 0.8, layer=21, ends="extended", tolerance=1e-4) rp.segment((1, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((-5, 6), 0.8, layer=22, ends=(0.1, 0.2), tolerance=1e-4) rp.segment((1, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((-5, 6), 0.8, layer=23, ends="smooth", tolerance=1e-4) rp.segment((1, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((-3, 6), 0.8, layer=10, ends="round", tolerance=1e-5) rp.segment((1, 0), 0.1, relative=True) rp.segment((0, 1), 0.8, relative=True) cell.add(rp) rp = gdspy.RobustPath((-3, 6), 0.8, layer=11, ends="extended", tolerance=1e-5) rp.segment((1, 0), 0.1, relative=True) rp.segment((0, 1), 0.8, relative=True) cell.add(rp) rp = gdspy.RobustPath((-3, 6), 0.8, layer=12, ends="smooth", tolerance=1e-5) rp.segment((1, 0), 0.1, relative=True) rp.segment((0, 1), 0.8, relative=True) cell.add(rp) rp = gdspy.RobustPath((-3, 8), 0.1, layer=13, ends="round", tolerance=1e-5) rp.segment((1, 0), 0.8, relative=True) rp.segment((0, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((-3, 8), 0.1, layer=14, ends=(0.2, 0.2), tolerance=1e-5) rp.segment((1, 0), 0.8, relative=True) rp.segment((0, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((-3, 8), 0.1, layer=15, ends="smooth", tolerance=1e-5) rp.segment((1, 0), 0.8, relative=True) rp.segment((0, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((5, 2), [0.05, 0.1, 0.2], [-0.2, 0, 0.4], layer=[4, 5, 6]) rp.parametric(lambda u: numpy.array((5.5 + 3 * u, 2 + 3 * u ** 2)), relative=False) rp.segment((0, 1), relative=True) rp.parametric( lambda u: numpy.array( (2 * numpy.cos(0.5 * numpy.pi * u) - 2, 3 * numpy.sin(0.5 * numpy.pi * u)) ), width=[0.2, 0.1, 0.05], offset=[-0.3, 0, 0.3], ) rp.parametric(lambda u: numpy.array((-2 * u, 0)), width=0.1, offset=0.2) rp.bezier([(-3, 0), (-2, -3), (0, -4), (0, -5)], offset=[-0.2, 0, 0.2]) rp.bezier( [(4.5, 0), (1, -1), (1, 5), (3, 2), (5, 2)], width=[0.05, 0.1, 0.2], offset=[-0.2, 0, 0.4], relative=False, ) cell.add(rp) rp = gdspy.RobustPath((2, -1), 0.1, layer=7, tolerance=1e-4, max_points=0) rp.smooth( [(1, 0), (1, -1), (0, -1)], angles=[numpy.pi / 3, None, -2 / 3.0 * numpy.pi, None], cycle=True, ) cell.add(rp) rp = gdspy.RobustPath((2.5, -1.5), 0.1, layer=8) rp.smooth( [(3, -1.5), (4, -2), (5, -1), (6, -2), (7, -1.5), (7.5, -1.5)], relative=False, width=0.2, ) cell.add(rp) assertsame(target["RobustPath2"], cell) def test_robustpath3(target): cell = gdspy.Cell("test", True) rp = gdspy.RobustPath((0, 0), 0.1) rp.parametric( lambda u: numpy.array( (3 * numpy.sin(numpy.pi * u), -3 * numpy.cos(numpy.pi * u)) ), relative=False, ) rp.parametric( lambda u: numpy.array( (3.5 - 3 * numpy.cos(numpy.pi * u), -0.5 + 3 * numpy.sin(numpy.pi * u)) ), lambda u: numpy.array((numpy.sin(numpy.pi * u), numpy.cos(numpy.pi * u))), relative=True, ) cell.add(rp) assertsame(target["RobustPath3"], cell) def test_robustpath_gdsiipath(): cells = [] for gdsii_path in [True, False]: cells.append(gdspy.Cell(str(gdsii_path), True)) rp = gdspy.RobustPath( (0, 0), 0.05, [-0.1, 0.1], ends=["extended", (0.1, 0.2)], layer=[0, 1], gdsii_path=gdsii_path, ) rp.segment((1, 1)) rp.parametric(lambda u: numpy.array((u, u - u ** 2))) cells[-1].add(rp) assertsame(*cells) def test_robustpath_getpolygons(): rp = gdspy.RobustPath( (0, 0), 0.05, [-0.1, 0.1], ends=["extended", (0.1, 0.2)], layer=[0, 1], datatype=[1, 0], ) rp.segment((1, 1)) rp.parametric(lambda u: numpy.array((u, u - u ** 2))) d = rp.get_polygons(True) l = rp.get_polygons() assert len(d) == 2 assert (1, 0) in d assert (0, 1) in d assert sum(len(p) for p in d.values()) == len(l) assert sum(sum(len(x) for x in p) for p in d.values()) == sum(len(x) for x in l) ps = rp.to_polygonset() assert len(ps.layers) == len(ps.datatypes) == len(ps.polygons) assert gdspy.RobustPath((0, 0), 1).to_polygonset() == None def test_robustpath_togds(tmpdir): cell = gdspy.Cell("robustpath") rp = gdspy.RobustPath((0, 0), 0.1, layer=[1]) rp.segment((1, 1)) rp.segment((2, 3), 0) cell.add(rp) rp = gdspy.RobustPath( (2, 0), [0.1, 0.2], 0.2, ends=["round", (0.2, 0.1)], layer=4, datatype=[1, 1], gdsii_path=True, ) rp.segment((3, 1)) cell.add(rp) rp = gdspy.RobustPath( (0, 0), 0.1, layer=5, tolerance=1e-5, max_points=0, max_evals=1e6, gdsii_path=True, ) rp.segment((10, 0)) rp.turn(20, "ll") rp.turn(20, "rr") rp.turn(20, "ll") cell.add(rp) fname = str(tmpdir.join("test.gds")) with pytest.warns(UserWarning): gdspy.write_gds(fname, unit=1, precision=1e-7) lib = gdspy.GdsLibrary(infile=fname, rename={"robustpath": "file"}) assertsame(lib.cell_dict["file"], cell, tolerance=1e-3) gdspy.current_library = gdspy.GdsLibrary() gdspy-1.4.2/tests/test.gds000066400000000000000000004624761354474061200155150ustar00rootroot00000000000000X":": library;򚼯H;򚼯H":":PolygonSet $w $www $w'w1-w":":PolygonSet_fillet   hXX 2O 3_y bl"tODY#$/a [=:OifG'@2Y?(@&e$x!ȵH=&DJIRfb?Mt˒l' " f -k >^Y_$p 3pbbF7<i ݲfp8b'w,Ԋ9@. %S#dX)0)! TVLHVB)eW=EvaGZ;ą=vN km,(ꑒ/5S:Fg)5#l~)[N?^}852?^s,[l)u#lq5Fщ:S)/κh^ ;mvN ɅO=vR;+G ߡhOXn~1,B|!{BzQy~Nx*1wh8w6vdv7xu)uq"uS(uSyuqȈukv Yvd w6Jwx*y~N1z8{0|~1,X ^hhLK@@.(((2wEW߉wB].Rb1`T'waxz7N$TZ/^ ib  H`hJqL7 xNfd]P׍RcTVŭXfZ; \G]L_o-amVbh{dueL\f;h1tiej}klmm\IHn -no_Doٯp:8pap4php pOpgQMpoo#:n϶mэAmYvlZk vj t|c\ GE\I)bUYAHbb??9y-!3i}k<Lǣ1#nn.Y? ]zRXoa^;}vigY 1o?É,`ĻH> BkǾi Qȋ l2>BմQHʱ&FbN)| !_ʢ#=%ѣɼ( j*-i ,OǗ.hƪ0uťX2vĆ4jP6O8$rĭSݡۈ͕T"Z$ 2аwX^#6vws658| z<?}?@a,uX'O]/#+_2qFrp "͊Cp?BbĒd'yKv\|~ }S|kv{.y`?b<Ԏ^:8\T6%Z7N3$1x/w-1`+'.)bB'߉%W#η2w" E U.(( hXX ڐ /<z , K% vX 'J<Pd^CMKr7=8r 45K{~zp|qpzc5{xWL5vN tJHrrKpTndy=l~IjKhҟglCeZ^cPbYJ` _$[ v]Ú K\v [=<ZhY /X RL wQU"(Py$BOj&fO*("N*N-M/CcM1cMyK3OM}6M"8U3M9:NN7<՟N?OBAA=OCkPEQG_R<ISKTMV3OxcWQFCY VSZT\;OV? ]W8_iY'9aZy.cz[@es!\Ԫgy%]ܵi^ʼkA_(mд`Vtp`,r7asttLafvCb WxbK{>VbZ*bZkbK;=b W|4afas`,`VtG?_(d^ʼw[]ܵ}_\Ԫu[@^Zy.8Y'9W81V? VT*S[QFCOxcM8KQDIPpG_5aECkdAA=A?<՟G:NQ^8U3r6w53O^1c)/Cc-i*V("9&fwh$B"( wڐ    gu  F|j7Z,Nj>N E4F,}pz>(@v&e`$i]!ȵHzg=D:Bb0t\ "r Wj -k쳕.YPM$`3`R77 أf`bKwQ,{L)  IZc_+2'YTkBVV%%}rFj]o30`d m pxF<K;GlH`%U*iWl*n>wlӐ'K;$ <оIp_  7 ̻0w>oj]rr׺&]m$#lKl{kk02jjgjGzi¹i}iB}i!?\hKhRhMhFSuhFhMhRhKci!#iBi}i¹Oejjg jVk0kl+1lm$#G;@xc?RlH+S[AjO!b]kԙa;Hs\[t1äKBZ6\ EZ]Wr_p$`{bc defoQgTKi0\jkkM̲m n;Bǎo` pwXq})rzcseTt@lbu S)uævk `ww wxQcxx;xcy"y5x/xgx~n2x,Ywƨ"wNQ]vfv&?WuwDlts~c\ˌGnEc\%IhbEAx Hb?*VjC"-!$$i}[<LL#ڱnnY?]zxa^+iW αo/y`)HN>n Bk® Q|/ l2.4B'HŢM&FlbNY N X!_œ7#.%ѣĭ9(*-Y,O‡.hy0u2vwO4j@6OY8$c`ݡ͕ZM аfwt7yHD6v wd6% k]<0}|?@ia,e'? f#++02qrpEH͊4da)߿27Ē}' ;yKvM ~Ή}|k{.y`KoyGA(D=6@<@=9-=5H2F.S3+'C$~d!TfD5Qt{9}.n gu xhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀxhÀ7(LD6վLE63M6KN^S69O 5OH5XP5QRv5`Rv5DR5S5TXf5U5U؊5}V?6$WQ6^X 65XN6Yru7CZ 7+ZS8[o08u\8\9g]<9Y]9:w^Q; `^н;N_I6`>a*??@ap@Ta'AadA˴b OBb.CJbFD 2bU1DbZSbZ֜bPbY)b3bWaїajFaN`p_~<_[ڕ^*]p]te\8&@[G{ZH OY:qX^VUØT`S:gQ7PO&1ZM+LI~J}WeIWG4>FVxDyCP5?A@JlP>("=NƎ;W:b֛8*C76/xhÀ  hzEWڇ<fA1B7yKZu7}Ep DzB@}|gu t  /s o#=[ X  =aMڊD/4[86B&Qy13ZJ1He%O<f3<(?%]|O- 9p t@0pK1[չ6s{a™W Y֨b!2XOA~Sm~H}lR|? |fc{.{dkzԳz7zy%yePCyxҝxy+xcVrx9Txx<w}lwFwx>xx9TîxcVxyOxҝyyeP7y[zzCz{d{.~y|fcՎ03^ه4ۥؕ :vQݽ#SN'%e9Y~~#B}}v}TG]}T7}vm}D?~#~ {Y '%NC#jQzk :;=Y4a^ RG;]JJI=_M;R//1d sZ _Sl;Lt.IhH||ע},9~]Հm6uTN *ik_ˮ6&W$G0j?X_fl*6@1XLeSSJ _Gb  <MUrTS\Go}1 7\,Tz).4? U6.r@ +-Tlu*!% ju %:x lKlk*U@rGzlC!jaH:)Van`j  *!pO"2:"݄`#Ut|$&Fۭ$J%YJ%?(&u&O'vP'2y(Z9v(c)o`x)t% N)N%*o*AA&!*sg* *XB***|*oVm*s*V*]a*khL*7N[)/e)0n5)e~])Hq(o(G0' c`!BjM+0f}!tn7)WytH}DF6m d׶%ڔk]0|Nie7\?5 f2+0AExH 4d3a) ߿2r7} ;ZM (/yΉ{Y=~Jut.Gvh;DyAlZ>;18d5/30\R-+;=(M&IJ#]!wLh ƯƘt'Ljwf Iç -B `" %є a>bb2|MDRPn$}͵{}yowauSsG͵q?$o:m<.PkDiTMDgn2eca `J %^> `"] [gZX WfwV.YLU tScƯSpB2;?%A< @>@y?r&@ @?B^?e0Cڨ?.EZm? AFm>H_>IQ?BKfM?=L?zNf?{O@+QX@&RʫA&T6AUjBiVC$XJ%CYD̂ZE\Fd].G1^IHѵ_WI@`VK%aGL`b(MbUNcGPPDdkNQe SeTfUfzHWyfXgZxWgC[g_]~dgiSgi֜g_Y)gCgWfїfzHFfee KfMxIQyH_?FmEZmlPCڨ"B^Ǝ@W?r&֛>C< ;?% ڐƯtLzw, %X' < `"d % Mr72MDP8$4͵K~z|qzc5xWL͵vN$tJHrKPpTndyMDl~I2jhҟgl eZ %c `"bY `_$[]Ú \vw[=LZhtY ƯXR$Q&P(PW*\OY-N/QN^1!N 39M\6M8jMV:M<N"?FN~6AONeCOFP;rH0bQJVQLr6R1NT PU@/RvV"TZ/WV,mYlWjZY \[1@^\\ `)^ ub_uc`eagbWjcўl6d nc}eapeGrf@u'fw]g/KygZ{gi~giIgZg/KYfrf@WeGead ncўbWa`_um^ u\ L[1@Y WjV,mc^TZ/QRvPONLr6JVH0beFCrJAO?F <.*:28j$6391!"/Q-)*\+(L&S$ڐ }v} S~# t~HY'"L#zhQ~ :J!i4#qHZ%bPtՒ?x:|`؎8z,&_KPs p^T >DB &"W"DBQ>KFT@Ms:^K5P&/,*28$z|L`x:tՒ l#WnZ4.ؒP4ƚHZR Nq X~(hgy,r*lifRJĠnF,C48#B?r0x4;804bX0-Hl[)6&{#7L 6[PvlXA\辤 x f Qل\ e(ҷ63r˝`mcHXܾb4;`. x%.#hgb0&0T|RWB&fmA~tnuloB׀p`qmrkIJs/-sttn t.uJh:u `u84uԒlu uuvLu$>tPt3D>s~϶rXэ<rJYxq+p/zo2 jfd T*޶R):, \R7h4{>1ft@Hl`,86SVq:6`0m" tGF\xXV6@<jD(xjtH1B%: @Zh Rw zdc2"#T>]ȶɢ !Mx֤#|%Xɘt(N*[ ,2f.͚03#<85@N7SF&9X%;P=:.?rĭVۈBP"j$+60~! l)XvXb#F:hs~ײ56ږz?NuZ7^O^4/2^AH v"C4p ~<Btbd Z\(.~zV{\x=~c.p]W"&"p,2.":":FlexPath1  !B@ ,/@Zp p/@ ,אp א@ˀא 4y0LPsps^%R %y0L <c `psPsc 4&7 u@b@ &7  <@!!pPb@u@@! <P**UEUP* <PP0c*P*P <A ,K ƠZ3#`3A  <BD`BkpB@-,K A BD` <C|CKC6@1{ 6@/`aC|aC| <BD-A A@6] @4րB BD- 4?>9@8?6? ,=:b0:b0:{; =; =:b0 <%%"אgא ,R %% <x 0 0Z*`Qpxox  4 x xo!@!  < UUgP @ U 4EU U @0 EU 4@ tMpP@P@  4P @@PP <3*`p@"00 3 ,#`33#`3 <CPC.`)C.`a/`a-B@CP 4CUp`C|aC.`aC.`w CUp@CUp` <CUpCUp 3O 1{ 6@C|6@C|nCUp ,C`C|ҀC|6@C6@C` <B0jB 4ր3O CP CPPB0j ,C.`2CP0CP CUp C.`2 <A p6`p6Pf`60?P6] @A @A p ,A2?PA pA @A@A2?P <@H0@! )7)7n6`p@H0p@H0 4@`@Pې@H0@H0pA p@` <?6881w8 P7)?6)?6 4?w?w?6?6)@! )?w <=0; :-; 9ߐ99j`=0=0;  4>sp>%P=0; =0>>sp ,]: #&b*1-`] ,+< 1o+< ,ku]ԍ`k ,(rc^o<}SW1ozgL(rc \ԍ5}n]5@S^] #W`cm$#`qܒC"m$#]ԍ5 <)W)/4L/4R*ŴR*G`)W":":FlexPath2 Diڀ`PАqАf0ؾ.`wRPeɢiڀ ,gАА/pg/pgА Lgg@gqА`PА АgАgg T6[4]P8<Q;wS`8`W<6[46Mx8C0K :I`;H!>]FL>]P8 \_DZ@DXNPD*V+pDQ SDPS< T}<VjFjFLeEpcD@_D_]H>]FL?PEpA CD*B|`FLAjH@YHH <EbJAMQ@ `N`>]P8>]HHHEbJ TSDPQE;HE;H@YJ?o M*>O=R =L@S< SDP DOEMxFKVGpIZ0HoHHHE;QE;OE Tl?HoX@2pq{ACsŐBUPupCupG7Pl$pG7PjFj>l?H DupLtK}rJDpC I n @Gl$pG7PupG7PupL Lx PD`z FL|G}װI@}װOyCOupLupCx PD` <}װT}T|QR zpQIpyCO}װO}װT LpKVZ M*Ng`PPg`Xu`!Xu`}װT}װI@pKV Dg`bP0`.^D\mZ Zq0!Xu`g`Xu`g`bP L%Xp{P`0@@ ` @z0%Xp <@@pP` @ @@ Lp +` N`|[0|[0:p  D`Gjpp~0|[`|[`G \ęwRP203P-5+6np)09{%9{3.`ę \xΠzp~ 'P!,0+J})v zU@&hx% PxpxΠ T +J //1ڐP6npdP| @z0&:?&)( +J ,А/p/pАА ,А/p@/p@АА LS+@O``ppPgr@0@~ S ,@А@/pp/ppА@А \qz:p ur Jp J^ NL @TpPpq ,pАp/p J/p JАpА T FpB@P?P.OD0 q J^ Jp Fp , JА J/p/pА JА \ې>)aw gg1|.?Pې ,А/p /p АА ,m@g g m@m@g , /pm@/pm@А А /p <spېspr"m@gm@q)spې <m@/pr"˰spspqlm@Аm@/p Dspېv`?Px=@y'-y'D0vݰpspspې Dy'RPy'hx=@pv`W0spspvݰy'RP D~~H`{qy'D0y'-z` |[`~~~~H` D{q~~N ~~|[ z` y'hy'RP{q L$ @(Pp ~~H`~~zҀO`$ :p$ @ Lp`(P$  @$ \O` z ~~~~N p` LinK0b0$ @$ :pT@ii LK04PnipiB@ $ \$  @K04P D:pn`iiT@:p$ :pn D` :p ':pr`T@iip`  L@` :pn:p$ O`Ҁz~~@ L ` @ҀG :pr`:p ' ` <@~~`|[z` x=@i@ <, Y@6``: @, Lwnix0?Pv`ېsp)qwocw L(pww)2)'ې$?P"|  Y@,(p \$D0np!|$)|tlpvݰD0y'{q p $ <&sg+Ugm@r"|tl|$)&s TAC(?@*`9{*`9{%;)`#<"# > up@ACRAC( D>6+ <-;/i:f1e`9{39{*`?@*`>6+  LKV!J!AC!ACRBph0DPI KV0KV! <HH"pFL$B&AC(AC!J!HH"p LVǰT}@KV@KV0MP؀RZ0VǰQVǰ <P8N<0 LP KV!KV@T}@P8 L\E/[4VXVǰVǰQW<*Yܰ\E\E/ 4\Ea@a@V_P/\E/\E Lcf1hxhA fXd50}a@Va@c <k0m\pm\j`hA hxk0 Lotr_ tapxpxdo dm\m\pot <x% Pv#t"q@qp N`o dxdx% P Lk#',0&)(&0 0 P.P,!k# D&:?P8p6*4@20 &0 &:? L"0x0z9|[@~ @epe| dP"0x0 <@0Mppe@e@0 Lb0KPn0  mm+|w)2wb0KP D p(Pb0K0wwocm m p T˰i/pX@@ Em mt˰ TETKl րP:@V@˰E=ET TV+pT"SE@3P0NP NP@QπS`ؾV+p^V+p \^h܎\ [ pݠW&V+pV+p^W}/pހ/p} m @ \(p 4p_ pwu0u0lTp_  \cpİ+@(p \+@ p c qp qpc T=`u v `AS ~|00{Jpwp_ =`u ,pp q q p \ x=@ y {J Fp| iP~iPi 1`  ` v x=@ ,  qiP qiP  \ހi0pvp 9``0>iPiiP~ހi0 ,iPiP q` q`iP ,`` q q ` ,l$ $ ll$ , ql ql  q <qPnpְl$lpNpqP`qP <np qP qP pNll qnp  Du~ w u0qPqP`qPspu~ `u~ w  Du Pu~ `u~ 5sppq0qP qP u P Lz9{Pxـwu~ w u~ `w+vpxp z9z9{P Lw xـ~z90z9xp`w+ u~ 5u~ `w  L~W P|BzP-0z9{Pz9{@|0~W~W|0~W P LzPiP|S~W0~WP|0>{@z9z90zPiP Dr@p~W P~W|0@{z9xـr@ Dpİ$@]p@~WP~W0pİ D`Đr@xـ xpvpw+`u~ `Đ D``#`vp!j $@ D5`Đ`u~ spPqppN5ny@5 D55*@p(HP&$`#``5 L'0ְ)$+$lְnpqP`s `%`'0 LNPh@F0h@GӐ㺐I  JD@K MݠNP@NPh@ <NP L`ETE=F0h@NPh@NP  Li0ԟ`kmbnoMopgipgiQ@i0ԟ` <oc`l*jܵi0@gipopoc` Lp`֛0t!v]3@x PDx PMqMoc`oMp`֛0 Dx P}wH㓀v[tIs)P8 qMx PMx P} Ly}0{?ܵ}װM}װ0zp0yRx P}x PDy}0 <}װ};pm |0{8zp0}װ0}װ L~%ߛ^P"0+Pg`}װ}װM~%ߛ <*/pP /ph}װ}װg`* Lr@w@pw@0:p`0` D $$ְw@w@p  L P>BBll+5*@5 DB| P~Wp55ny@lBlB| L-00!0#`%``s Bs B-00 D0uwxـ-0zPB|Bs `s 0u":":FlexPath3 @@llLK@lllpun $d"K^zuz1-z1-T@1-T@1-T@qz !+ !h/:y|@LK@@@ O`O`LK@O`7O`7=])e)\h~3u`ƚ}`1-`1-71-71-7&F8 kG8 _0ԍ`>`LK@ uu 2 2LK@ 2  2T@T@PWBO^D @o@X@o@wT=n h1-1-1-1->أ^-DOOUoU1hZzc_HzuLK@uu dO`{G;{j`{:+L  uvIt ۍr  p 3o0:{"u.u6<|.`D6<DW3Qy?(ʽ QjЩgN+_@۫@m+~+eqPIj`XOPI@8@G; L 7>O`7:^G;^^( ( &olΑ$|`gi$|`+9Yn7ˬ5 C,%r<0ltK'X+X*I #Aތu!RuNe*i0T\W\Xms_4Srj5{]%xtU"̜ J-W H'ǬM@@R1^վOcs!mg!buFSk)TD0@ș@qxuZDoMGy4eIS0W&;8v%_)U/EƧ"ϝS &x*=ro$NWm Z<|/l* JI;0OOs:M|جԟ*%'ge Ac5Q5kF\|j7` 5jUoK} &U̙5pv2HucS2@0X)Q|TsmTU|`^ hn $٧4g(и(F` h,(0 #JyA)i5}wax[tbr7ځ+!l %}:o*H͇X -">^Y t k4,q}gm gk*E>uJ]k%>|\{DStĨXy;☾YB!yl3I6I .Mqa/$:vbƩP<n .0#a&+ܦ,sM9:ЅNTOciwk :A{}" r)1S ,& %"[$ZDۘE=(;INp(lDCv"vڃL6L.|j$t`N4y?)g &5X4]f1\"Af\:fCL4/XW5&]s)j2c&Z/e t<q?Jx 08 h^u.=:fy,-.d*b^'Ѳa[8%_#.^qK ]%MZ[ȗZ48Y=Y%RXWMWW>_ V? *VdnVVlVd:V?6W>_JW%XWY%YViZ4Ѧ[Y]%o^qK䖀_MNa[8b^dfyh^udjYF5 liRnpaosugέwf}JzI&i|sS^dʚC.DHh)k o(h o)h\rJD6pʚsSi@}Jέ<aoI2x5 d@12$2R33qtw3h3]3P3;3K(22l1֐_1) d0gRr?/3Ө.-ǐ>u,NF)+'/*)})7#(;GF'pj &o08 h^u L08 h^u=5j@16p#, &o08 h^u L08 h^u%m]Rh@14GWqa}&bLd0 (e fhf&i\A!kNdlEnIǚo]qU$Qrtovlnw%=y0aazL|jP~ "3HQO:!KHYIWQNN!mZS60Po2.ԂB,O%e! s?T "L{tL  Ň32ɣB(&)Y̻ c}fXO`͑,E Eʪ`v@11+)2*>n2cO2.2`8k2yj22^-2yg2LN21Jiu1]1&;j00VpK^//dj.W.Vx>-(-05,!+ 4n+T8*.*)_F(( g'a &o08 h^u  k[ђ  @ ʠP`?Epӵ *0/xPVԟ`0;hPb`yyK0K0FKKaPV`/PP@ BX@lD@O0dZ Z p33ZZVl0&Dcply `@/PV`Z`pP;~QPp|`<{zYNx0xPwLpput?0`rrPqY0p0op`o6 n+m֐m:P=@lw j@ 0i pfѰ Wf\,ddcvPO`c bdaK0azaP`in__ ^`][@] \0[@Ҁ[[8!ZN!0Zp#wYd0$b0Y= %%X&X&0Xy'XR(X)k@Wݐ*.W+Wp+@Wh`-W@.tPV0/^V00"V 1 `V2V3pV~6<V~k[   , . / 1M 28 f2P f3 ?4 6} ʐ7A 8+p |p8 U`9 @:p 0; 0Y@h pAS0!C D]EJFpLPGYGFpH@EJDC AS0Lp@h0>0 =;=4m? @@A@Az@BC'C'BAz@@@A? @6 >m]0=]0=[@ӠU)PӠUŐ^pW% ^pWP ^x `^@ Q0`% ` ` a^P faӀ ?b! b ʐc cZ U`cP .PdD 0d e keU Ce f@fpfܐ2gQ`gPho0hc0!h`i&it]ijpj@kalZŰlPlDlZk j@jH@i iti&h`6hc0hgsgQ%`fܐPfp0f@be ;eUedwdDPcPcZ pc Pbf@b!?0aӀa^P` `T`%ߠ^@^x GWPGW% `UŐ`U)P@T>@S]0S-]0RP6 RC 6 QQ1PPGPOO5M@MJ rJzrFPF C0C'C' C0  ,)pmPPno@`p+` qr Pr`0rp6ss$sptJPtq  t@u `Ou4puuа>`v@vE{v,vw 0iwWPpw~`Pẁ1 wxAmxhxx5Py, yS ryz0yPy`9zpPzdO zz{p{N { |@0p|``F|Ր[}#`}q@~\0~P+~`VmlpP0/B`ip3@Ht ޠՉޠxPѹ0Ч,@,-@ޠ1ޠ] rÈ`ipvB`0#9O0mzp~`~P~\0 }qJP}#_|Րu|``@|@{ {N0{zVzzdlzp`y`0yP4yz0yS py,x`x80xhxAtwẁw~`<`wWP@w 0yv*vvEgvuаPuV0u4pu `t@tq ϐtJps @spsrpr`pr @qp+`po@ʰn|mPPlŰl)pmP  Up?*0   0֛0 0onP3@`pP ܎1yp1( րpę`nG@1 0@ ߐ wbђ Up   ` ɽ ʧp j fU ?p 0 ʐϰ К |p^0 U`H .P 0@ Թ kգ Cg@*@t ]Jt ^4M^M_@_` `0ba;aB a`pbpԟ`bSQ@bz*0b bӵcӍcec ʠc0@dO`ђ dO`kdvpddЀdĐ2dĐpddspdĐ%PdĐ0d̉d:dvpdO`dO``c0ʴPc cecɢb{bTbzbSߐbpȸa`j`aB CPa `0ǧ` ǧ_X_@X^1^41]Jހ6<U 5R U 5.4g.3ˀ3}`3VP˸2kC2D1΀1ʧp13pʀ`1 `2@0@ 000 0p ɖ0"n/Ұ/ȫ/7@/7s /L..Ư.pa.p.`p.`â.pTp.pP.¸0.j//7/7  % ` 4[㺐::A0a`a+  d0@0'`@蜐@P_$`$`#0Kp`r r[0 `W5̰\ SPp  0dG@nP(`@p``1r 5PpCѐj F0@`X0-pPTpBɰ>Te P ep)@PPpOŀ :a@`KL s06p 6n GX0Xht ]Jt {1{1]Jހ6<U U 6<k@?KpӵKpԟ`PPb@0M0@ 7p ׬aؗa :π:Dۤ@` [   pC@߀C?`f PwQ@:;` נ0%LPss`70P6p]!o!3H  @@PDp`2Y. @@ p C Q  0 @0 U`&p  ʐ ^  ?"@ fp`  3   l0 * P Q0/ x@} P Ph ` p+P yp  ;c b b' u@ p _  "  [p%Р%EMM~Ptpt, z  ttBPMM%%` O  9  $P rp  ; ;G0 ` ` Pj ĴXhX0 p $1Z1Z11`2P2k223VP3VP3}`3ˀrp3rp3K`4$P4@$P4g@4@4 5 5+5R 5y0a5@a5P95`96p6<`rP\p0Cp{ X>p1( րpę`nG@1 0@ ߐ wbђ Up?*0   0֛0 0onP3@`pP ܎1y0ܰo Z0x|Pq 0ɰɰ]0ic@9c3Po `VpOBD1@f0*v@ lw80[<`y>[@ <\iPwAA3TP3TP4>4>d00M)p'b%@AmP ^@1@겐!GxAa@]?spG*`5E@g#8༠z0t-T`sgPpҀ7R`g0]JR0退I&@@L -`<ǀ0@pސPZ@sW`P@0`v 0`P5֠6@ R{ױf*0rVp{90B@@`G q'^p>G ɰɰ>YHBP_bPs Ps.B@Ѐr_%cB>>0͔PP1pDPZ Pt@0x[F,0@mI@ zP ͔!003ɀ;8EPN:Yi a@dO``d༠{yTs+@n+0 +0` P겐{ "0`^0ǚ0@@ eɰɰ< Ӏ^+%)PHpZݎkt @K}P !uf804p,0T@)1-)Zp*O0ii+y2>p`p`i`yݙ ` Rhp,0~st`7lPM_O@UPNRLP(,P=(,P='A>AM GP`R8`Ȁְi,b tŰ³@ē`M@, bބbӀ^ Q&PmKp΀jI@7 p`` p0 0"U8c3菰ɰ֎PPU{'^*=fpF Kp028T" `ZdP^40c=߫0dW Fg ) F@mDD@A< #. 3 @+e#kpn`@ AP0c@P``)`Nfv?]L@I`H<ܰ[3_+kIQ@ng9>۠ ^ `0v~@ a0I[P00pJ`F`b@ x P4@0gP6`{ Đ^b`[08R8@,O0x 8m $|`@`L(Z0_`@v  |@DhQ( ̩V(@駐p0P[P( %qD-oy`5)`7$p7$PU``T"BOgPpK`,kFg ? @=۠UŐ00q" JP`څ@S^Q4sPPPbP} ^ _ ^ @`)  r8PS.a0%0Q&P fr"Pv` ~W:cx@(Ϡ)霰 P1T6P\ȼc*pad`_`^^^̰^V _aP`ȀyoPeun6 sbSPÐ m20VacZ -܀=-܀{_@q y{``yPyqk\#P~T`~;`}x@٬tL!ԣDڗ@-܀ʑ-܀0ŀPLY0lPհl8poy`AIz0<0_5@"`{ʐQ\Q If1`n] ?[p6?6]=: =:$U/*O0dJ1@rB0QWRfJF0PSAd<@g<{r`r@la@hBfa<70P6`Pr``|q0j` V@0tPCܐC 1`d 8 P?PxH ep@XLP<p/ `'0#q`` 0# =:]=:!0!N &pd0(L;@pHӰ0W@l 3~*qԠыqfr s8D#j g0 @88y`ĩ EգC0^0,SWx+P i`upX@`-W @&@m+@Wp- 13-@``Pr_Je0M`/P `ŀp j\װ@-КePp0hY`w 0p`L@Pܯ@*P`0`Hp] m}({pK@Ѐp@0sPzNadȕLʑ-܀ڗ@-܀0N5ԣf*|#spj/`)e@ %Pz'0͐U:e0&P P>`& =U@/pS aӀ\w~`ޫGPypfHLȫر0Ppq"İ4@{pMY@t @p P ^ + ^p)B0`$9  =!]_&*I08`bPMP`gr00 3pyALސ&Pvr¸00p`Ş`ĴĴ  G0pxY@Ua=-܀cZ -܀s8D# T*Ek= 7ƻc7cV82dN8zdO8e>9 e9fr9g^:/g}s:n:gK:`h%:ȱhaw:hr;*h3;kYiL;qis<j)6BZmc>cm?#7n'?n(?7oP@0Xob=@ioe@oՠ@p>;APpAæq!BqBiqнB8rBtr>6C4rqCsCs`DAPsgD~sD5t.E{ts#EvtЄEčuIF:uQ_F3u}VFkuG'v3BG@Pvj%GqvGvHBwEHtwn]HݩwIFmxIxm}JxJyDKMyKDyLz=L\zpBL:zMtzMz{9M{7NK{"N|8OX|O2|֧PGf} P+}9sP}[Q8}Qz}R[~KR~SY~@S OS4T69ST+U"RU,V(UVVπW cÙWWZ6XqlXR~Y_YVhYl BZMe=ZfZ[5NV[D\/2Q\{ 7\7b]64g3]']^oى^_j11_r[`TcaXaڲbCiqbU27bؕE1ca!l c;deLe˯}f1Z(Uf;f(JgFi>g\hjBhRiR@iikjjj&kq=}kٱOl,\lmgl|m{mn7BnxgNoQo՞oqoppiq.qn 'qr&"s-s7atiEuOuoVpv8[Avl^vjawCelwҷhx{kkmyTlylmyVlz.lzIkf{iK{fd| c~|b`|^v}1NX}R~'M~}HI~5DPL;f]1.+5q\%=H' `WT0lO ˾h8r;e6&Z5BK*UҐ>xGd 8Z6vXU 9T`R%xE.UӅ-|nW!3:Sv[.ZID|j'm8Y)*kfac }^3c'_^c$eUKt=lUX@R5 ϤOy:Z g zp\*>V1I6~ ~~s~0,}HfH}} -}$C|ۏ6|N|qHd|Kv{?{݁{^r{>+ze\zzV6rq04q-Ʉp\?|pAo)o"or.oitnnn$dom+mdmLl<Olcqlk})kk wjVjHj xi<q/i hڇ;h\gxgaafGvf{5,f4em d^dz£gd$ڮcOc^c5õbijwbGKsa `I`]q_ۍ&_s_a_Ə<^p^VG]Ѡ][?\[ȕ[<[l[9ZԱɐZJMYhJ}YʚXLX$ W+3WK˄+VVqUZU{V̊dU#̯RTTPEIS¬~`ST3ͫRRER&]QΈQ>^ΪPPg3OT OϖO No_MWMOqлLd- KFKwDJѝJ;ѻ&IIT2 Hr҆G׬ȩG"BF$FEE]EwӐDDKCz+CPoBWmVՀ>eը={Ɛ=UEޚ<1<<7;;uQ;Cc&:ֆ$:/֨F9ָM9Iq%8}87w5o6d(5ׇ5&ע4{j׸13x3Ջ3D 21"1970:H0%=T/^K/uxq.?؃.8ؔ- ء-!\ث,Vس,p²+Ѕ+1y**Ql*V)b((^ (O'oi&!&)%/x%>5!$9#@".C"-7EH!LF!FFa FZ GED:AT?w֣=>q;H"6~1E%,](e7$9a sL]e%r>[,Qػ%آm؍j{ l`WqBK,o9^9~@   0׻. <ׇ >f &D [ | -ֳM<(֒w:bJ?PO,6oՖg4.6d`[8ԤLn%GAK 4]Ӻ9ӊOn=;ttұ.fa+ ]u}3ѝ'т1vIQ%РhEp_nSϿnpvώ|gH23d?wuN͝ksN o")Ur i \`]ʈݟYx߫ ɘWC[_+#%Ǐ.bb1P4dk ١ƔIח_֘rĖ*xNԨ@,ÆH(Aң` +N{—Ѯe+[FY).jgбAao=6}sKϢV9ΖR;FCͺd_͍^k#-L4-,̳{̕<SI?^˶)*x$<oHʣ,ZE!]dɵY|z C ,ӣS/jȔ`8e]^gǬG<LJjUQJ&bZ'ƨkHeK3T )4EՆ|Ŝ|Jx">% AzW=ijĒ|rKkJ-5w`<]:jdý.0âÍ6v|pWVfL<)iߠAFeLA6N&"[.³Ož`„mnF8l*Z/x2LO>1:op.Y!-MpXv?a&VpNHjD6: 2+E%,V[co0`L z & H^a AQ\h([b ( 4.,4g9X=P&F_OjZ& bRioV"|)4`oCx4Í>0E0~N(gRub^’jZªc¾ a)ؙ͠RLuD1njÑîHpײ%]f%cŸV?~ ğ-vĹY=0b&VRAMuwJő@Xb)B-^/KT&3Ɖ9w" sk'YĞh:SǪfhNJY!hwȱ*y!'2E` vɾt(Td*ʝ9]5h.˅~z̮}f&?y j w υdwsvLV5tڛкshkr֪Ѩq pfҘoonLӅmcaly4@kӭiGhdգh*HgH%gSf+xce e+d13dWRcqהbwbka55a jt}`Oز-_.^T^eM]vٖ\6l\[ [=<ZASvZP~YڨYTYXrk:W!W4VzXVb{U۞U2ۿTD\SR6xRl;MaR _'QmQxox.P+܌P}ܠP1"ܬOܶO{8kONmNLMӂ\MZf &L"LL +K<98KBJIJUOJRIYI_`HaHaeGohGo*jFOkF58jEh<ErdDaIDjj]D'ZC5SC,mKpBBDB7(7Az% @@c&@?R?n@f>>qbܸ=ܕ=v<\i<>7F[;4 ;$;{;#: :@ۻ9tۗ9D1z8a8kM8<8#wW77Iy6ګ6ڎy6_v6+b 5̫;l5k5 {44=٭4R!ٖ31j3A=q3J93k2H2#رb2"؀1_1D<1X70ܪ0ɦ׾0n׈0P/'/V.y}.S.--չ-r Տ)-@rj, '&,,KԱ,ԍ9+Vq +6+Z++P++*Ӊl*o,S#*L:9*^)3)nҟ)'o(!K(W(Ѷ'֒ђ8'uX'd_'<1&4!&ʕ&{؏&f Я&"Ї&v[%O%}+ %>% $ϭU$Hσ$ cO#vJ#6Y#'#Qo#=""`"&g"@Ψ]"GΏ&"w!P!n4!7/! _'  Ϧ J `K= -;ͯG͔l͂f<t_i5au&[O:D98-33'jlJZ 1 Gք-g4BXQ)ޝ{g@[8m}Z; [tUSXfBGL^Ous{u'r>/f3|` 5F>-GF)wH6Q]YVbdՏiC{9v͇͑o͚+b͡\'ͤCͲ*/ͽnoJK/a!g1Ct=fHM_NGa7oRΚCθ̓x(d>?i$ψOϣϸ>\n39XuqgІc^~ДEFи05ۚ/g/ p`¢ѓ_-ѰCƛgml3iaHEҁ*ҹ[5)c Ӎ(!X{Pk#{=^!ԝTԷ ?Nu+*8l,խauy!,n-]~Qhע$T4n`a {%$gVٜ;žӹ0vI,=ۅ۾~5nJ`ܩQGSq>ݐU8<ݿ+#ވ96 {GZ߰6$Yamd]j~C5r\Nr' Vi7n>4e + WL6铧,('fA`V9|5xD}#y\E؟Z{/"_IR1"?"/w=,:KsZ2TsS:UO=\ tiA۪]!#)09;=%R#hk!u I _ǝ>sTĘ-@y/KY]Kקbc'4ii i, 8u =Շ c;  "% ΐL zqGdA6O 9C)$BZv]I/~k2>j|=KO,ܫqj7ڳE qH+-юZrVl Nxw !Y"sF#P $-v%k^&= '{(I(G@)P*<+)S K+mY,z-\J.>oL/ 0 =1 82 3S!3'!B44`!5P!:6@"V$7&"8 #DS9{#:$3;$jN'%R?@&F^@&Ah'.B'PC 'ŵD 7'D'(hE(F)^G)̏H*$IX*lI1*Jjw+(K_~+5LS+MB,ZN,N,O-WP 9-P./Q .|8RU.5R>/5!St/Tv0UQ0>U>0VS1=jWX1|X.1-Y"2!Z3 [ 3w[-4\*]q5I^5_:6`}i6\a6a&76)b7'c*k7)wj/)`i&0)h(gH(Ag('sf$&Td&CcGW%bGo% aHD$`I#_JZ#^B#3]ʀ"\ˮ"m\K "[!Z!Y Y,? ,Xl $WĜVC)UP%UET=jSRLQSPPfNHOcN:NL~VKϖ JpvJQ1I5]H GBWF E91DD;)C>2BPJB =AZh@cO?.=#<Ł;͚c:,N9ׯ9YOS8MQ770}54Wt3iNJ2|'v1W0y0,$w/5j.?/-q-(1#,h M+x ~*M l() ' rc'S )& %8 $ x# 3"z " n! ! Z ! 8 y ؾM e GC&dۅBoKZ5B9Y[,߿?: q/VX845YfakB&C p!Q@ dy A u ! Lx[ZԒGjT)È gCaVߩ2:+^C+1sl\UT%;W#?xY:&\5C #:@رsZW(`@IE.!X>q#6o< %5 F`i4m Dk;ZDE|H,vapSFj#7d%)bۘ_ ^[XTIQI|P.ROuNNPN(O~R)TcX叹ZF\+`eem(JwxpGm*R߫A,"Eޚk.ݠZ`vQ;ܰ@5w+ۻ |+i/9 ЛJZ]?pl{O.O'؇$׆am֟c(žB6Szc\z"@ӱȮDث kq$M<фj:~ų8LnT@) CkidXsc Z͋85̋c*|@lە7 ە-ҷZ.)Í ..ܤ./C:7N/Tp/ď%0 `ĴU0SW01"j12 O2g`F24ƅ30Y3K>35'4i4/Nj4iǶ4#/5 v>5Ob[5ȎQ5`06 6K 6f46%Oq6p6@ə7>lʪ7^ 7s,88P8Is8Mʗd8ʥ8XN991O9T 9+9K29X: nj:;Pˁv:w˞H:E˻:(H:L;#u;_*;j;8;iC_<(RK8 >D)5>B9L?.7J?KRQ-?s:Z?Efm?t@2ͅ_@D͙@AFͱ(Ah8;A6:AB oBZKBPC8cCC D?DD2E{ER"E$E&F](F)OGA(G'IG% HL"&HuM Hq7I5IbUI ~I ?JFJKS/KIhL ֽLjZLHͿ Ls͹)MWLͫM}͝M~͖N!͍N>}VNwl4O cO`ZXOdJQP7PQ1QORw̹R3̝(StS`T2QTV1GU6_Uu5UNnVN|V؅˟WkˌWq9t}WUXc.(Y YʹZh2ʐZf-[y/\?TK\ɿ]Ʌ].i ^O-J_?_Z_ȫ`ial&ab8$bǥvc{ad[ eeuƝftgŹoh~_icKj{{kWilÒ mrW~n9¿!p^%qغqxsw xD_z#k{!nf|7}~~f[B3z9R6cn*ַ!Fo j+8UNY]  Wi)ߟsJ7/h(E,nK[HUtY "0Xx;3- i +V]jWF{0^Rl8^cN2oEt?I,"zz6lySy[L\ DW8;S0M-S#XT |^bU }uޕ 0H(JMHla!!.ʆ5yt:hcكl])p@H« :,pz) })%6F_1\py]}$D\^W n.*q}>X[zg*/IP\jrphʗ'HU5|{9+-T|a"^\;EZ%'Qj@f!fi,^K}QG]R*^ /(Hsann.ru5Y(l ¿vɏ zdM3÷ >j/Xĩ^mň`&p6$ qƼKt3ǁ0Ǫm<ېpd~aj6ɀ5?rJV=|kb%)i+F$XU~ˠS:  ;l:EĨrS̸U.wfͻ_c+g?ǎSNΡ&ZȊ A$jfkϨ 5rlp.ʯ^˵+h_;øў4mEOr9#O]e&lѿԇҚu9hiPa_*K՛&zvoR20^\M,.*3.fbx!أ۲ Wܑ+ٺp)Bnu-z1ێ۽.@>mܗK_Iݎ̰ݱ\)?盙*Rݮޣ}齽B2߾TU6APupY^r@I356jbJrXP.M0-gkKCi gA[uxoN-|B7U)m Y?`RNƘt p OH  g t"G (l. 2= LO g?~4m\ӈLg!^R >[(dF vXIl'Y.|Z-:~EKQ\ZCc.g.SkqRaw|+~or胊 3舚 >舡 R!f!f"b聕#Iz$huM%n% g&+_'8['sQ(H"G,(A)V':.)-*y +,y+\a+~,-6z-Oڵ.CgV.T/?+u/L0}1Qx+1i2iW2?3T!4,Z455i6s7揸7gp8<3X9|:g;;-@/>>?Z@PD@AmAv@BKpB[CQ rC㶂DE5dDTE FF'cFdG ⻗GuH8C#I  SI#J^ZJtK _K"$LLMOyrN5MNϽOo͈O٦ߡPH)iP#xQ~ERTXxS]TBTyUR.U VLsܹVIkWC=WXO^X-oYDYZNΩZxy[o$F\e\c \-{]Fh$]F~^o_"@_t{_v׽6`U'``ga֊bO)bPc<ոcjTdHdԊeqf\ӂHf7gKgҞAh@I3hiMvVZjЬJj.>k~,lMllGm-ԁmRΔmWDninbxohJo`pTpʓ~0q\kqRr~s ɾ9sV=slȽ tozgts(uǥSu"7vxveƋ5v"w$ŝw_Kx.u4xr }Ox{y*Íy\z*{zj4zR{9vq {b|=|K}&#}}q~'t1~~&5h:gRX0-w-$ZNx$',wK>;K-LO!(vڶ<ج&Ft\9w5U<UR54+ϸzm6XDqw,[QGR`_$zc$}oy#dؼD#i6 u8pg;4fDT{  JmTCr HhU#r/̶Z (?i*|h~L߂;j! 5Me.E1RYy>Ohz\JIˀf?&2b4ӴsncoOtQB \`x51}Qx$l2Xn_WxWExLVZV6V=UtJ:U(7T˞zTXJSW ?S3RRRMQ PqPlP"``O3OW2N8N\MMi9LLa4oK3(KipWJJI0 I_ H?dHz96H:vG=GIlFvUE״{ EU#DՠLDJkCH CxקC"aSB~JB04A=j@ @:f??;G6>3s>|#=y=5+#=D:<<{bD)<ם;;vYJ;:3:(X 9~9B~9Cc~Cc8}8[-}\7|u7-|7c|]7 {56{6Ez>5zv50'y4y4yyj4[y3xD3jw\2Qw'h2\av1v1u1{u1*uuQ0Ht0Gs/s/N7r.r$.Tq.Rq0.*pA-p-pT-ron-yoU:,Vn",)m+{m3+fl+l*k`*Ek=  <{)5"cKس^[OHA< j:um/Vy|%!,t1/;`OiepxW͂<ۢ7IRC$-R?<!EɊLL`Pm~DRnnVGXLW V$hYL%C3S,M<9HzR[ [}csiEjQԉ{D$'gn!q _2"H  >IqȎQ,?[bJs't z|`)h/OQp1Ei1&7}p.4&)7v Nx:sIq%)XLWݿR4~vz:{sUlcF NJ'_<Wb-Oeߝݺt)au1Evo(t'nζj$c+ZZhMʜ<v&f z}כֿpadߝ_+P\/`B:FА@ 4ET{PE <    -0{PT   <+ P+1`h``+ <++;,r@pp+ 4-3p-P"P-ѠP-3p <+0+`@+98P!u8P P++0 4*`"8!_`!u8P+98P*` <*( *""0""_Ԑ"8*`*(  ,($[$ (Ȑ( ,(@$$(S`(@ ,' %` %@'i'  <Pp0pP[`[` 4p pY [`p < ``B~@W0    ` 400 ?PW0` `  00  4n٠n dp0P0٠n٠ 4٠0٠00 P++٠ <n ` `  G G 'n٠n ` , `n `n٠٠ ` <n G 5G  J `n `n  ,k@ n n ` `k@  40 P0@ @ k@ 0 <p͠` Xp P00p ,pp00p <, ,@tP,@7t7@,r,  ,-P`-\p7,@7,@tP-P` ,-ѠP-P-I-Ѡ0-ѠP 4-P-Ѡ-Ѡ@- ,-p-Ѡ-Ѡ--p <-5`@-P`-P<y<-5`-5`@ 4--\py0-5`@-5`-- <,K&,#u+ Py<,K<,K& 4, ,r,K&,K<-P<,  <(Ȑ##q`p#JPI"0"(Ȑ"(Ȑ 4)dp)p(Ȑ(Ȑ"*")dp <@ m m `n @n @ @ m ,@ 0 m@ m@  4--P"Pt7-\p7- ,-P-P--"-P 4AYi+i*k|0AYi 4F<p60?P+iAYiF L(Ŵ^oLŴ /4 i3+e^o(Ŵ L!]ԍ`À1-k1-uFFkW:!] L\R*YG`)W5eL/4R*Ŵ\ \qܒC"m$#`cW`] #@S^]5}nԍ5m$#]qܒC" ,r4gL}SW})r4gL":":RobustPath2 NV&A:Gb1THrj+˵qxS"vhD/D"l(@UaRfY\kW:hTxHSTRYX&aR}m?sV>}Dh "$#W>h>ZQ63tE}`3RZka/*T9OR~xquSj-ޣc+YZ!d-4_CMY_sFYB{v(>!_"aŰlؗ+o2ȭ "?&xE9P|nS4^BfjU~sjjѹxj9I.U7\ #p {/B@B@4"sa^-:&20i Y<T& T@ &)Ox~{v4"Zn[C]3c(:f1?\IڹJ#7tFƆ lyANB2V4 +#XР`4Dop,Ohʩl6?}NezEmnl9Kc&pMBi:XP4HJ R1 YL]eC2Tn,yƇr,Ntv3vhpEBqg#>Nĭ54F"ĕ 852ڻxՊ{8 Իٌ@P>܃e@ưq{+ fPZPMQ.?@FP50Sap2(c?/pwd0-m.@ <m@)m@B@B@m@ 6P+;@1-6060P tPp1^p@S]Pspx0a2K 8p%pˣ [` 6ppiP]PT gi 2b`p 2^hL<:U(UpbZv9 = P`/ _@H$vs`ف`k aPYNptFb$F13@݀>t0G@U0a'fP j4 orro@$[ phP =p .X ',0w= %̠b.'zPQ*հDmP.0+ 8p\G@UA^a@0m 0@0zpٳPNӃ4PGs5gK`(P]7tEY~~uݐ$`{Pm|] P JOp$0@=P0*yP @ PjpDZc|" :R@ ΡگmcqwW`I` :0$@+J?` @M0__ v`w U0pWPZU%~b0/lː)PqԠ0jf;PeQ2eQ fpjhq7pzpuݐrt0}}zzpvh̉l&a 0X@6UU0 LhpN p{`{ DF@X0.GMF@.нCp(70nJ@Z&zu@@p g% @$ 0}QzqpSvR `uAPv\UOu,N-ǐӨ.r?/3d0gR_1) 1֐2l2;3K(P3]3h3tw33qR322$@1d5 xI2aoέ<}Ji@sSʚpD6Jh\r) o o(h)khDH.ʚCsS^di|}JzI&έwfugaospn5 liRdjYFh^ufydb^MNa[8䖀_o^qKY]%Ѧ[ViZ4YY%%XWJW6W>_:V?lVdVnV *Vd V?W>_MWRXW=Y%8YȗZ4MZ[ ]%#.^qK%_'Ѳa[8*b^,-.d.=:fy08 h^u &oF'pj Lp#,6@1j=508 h^u &op#, LW-W.Vxj./dK^/0Vpj0]1&;iu11JN22L-2yg2^j28k2y.2`O2>n2c)2*1+@1ʪ`vE E͑,XO`fc}̻ &)YɣB(2Ň3L  "L{t ?T s%e!O,ԂB.o2P0S6ZN!mNQWYI!KHO:HQ"3~ |jPaazL=y0nw%vltoQrqU$ǚo]EnIdlA!kN&i\hf f (ed0&bL>qa}^`-sˆ<^U̵1]@\b(3[,kYѵXWW\VչUT؂S*R$`QhbPROOUN?ݤM7i6LQ`LK|P JWJYTIIc@/H 5H'HGWH ( GƺG{;Gw&HGc% G[~HGa*GuToGGš0H۶HNeHx 0IG JI J&J0ʘKLlL L'MDN OyrPDQ`RÊ*S UK|"'V#X%Y&^[3(\)q^*`c,8bE-d9.fB*08 h^u &og'a  xPt @0FP0@N:P<pPKaPV`/PP@ BX@lD@O0dZ Z p33ZZVl0&Dcply `@/PV`Z`pP;~QPp|`<{zYNx0xPwLpput?0`rrPqY0p0op`o6 n+m֐m:P=@lw j@ 0i pfѰ Wf\,ddcvPO`c bdaK0azaP`in__ ^`][@] \0[@Ҁ[[8!ZN!0Zp#wYd0$b0Y= %%X&X&0Xy'XR(X)k@Wݐ*.W+Wp+@Wh`-W@.tPV0/^V00"V 1 `V2V3pV~6<V~   , . / 1M 28 f2P f3 ?4 6} ʐ7A 8+p |p8 U`9 @:p 0; 0Y@h pAS0!C D]EJFpLPG0HXIް0IްHXYGFpH@EJDC AS0Lp@h0>0 =;=4t  , i&  ưpQǏP|ź.E`þYI⦆­P_0Hp0M^ؐp@S]0S-]0RP6 RC 6 QQ1PPGPOO5M@MJ rJzrFPF C0C'BAz@@@A? @6 >m]0=]0=[@m? @@A@Az@BC' C0 F 30FP30Jz J  MM@O5OPGPPQ1oQoRC HRPHS-!S!T>ӠU)PӠUŐ^pW% ^pWP ^x `^@ Q0`% ` ` a^P faӀ ?b! b ʐc cZ U`cP .PdD 0d e keU Ce f@fpfܐ2gQ`gPho0hc0!h`i&it]ijpj@kalZ)pmPPno@`p+` qr Pr`0rp6ss$sptJPtq  t@u `Ou4puuа>`v@vE{v,vw 0iwWPpw~`Pẁ1 wxAmxhxx5Py, yS ryz0yP` ,  ,r]&m]f>^2[x^M^L<_(_c2_n]_۷`>ռ8`ՙ`ˑtaM,aR"aa ƍbԔbL1`b*Eb;b|ӷ#c%szcV <c2cҹcvbc@1ed" dCѣpdaZd}df@dzd.dodؽϔdGpd'd=ά+d=^d'%ddؽwYdo+7dd̕dfKNd}da˺dCtd"/c@cʪ+cic2+rcV c%sɴCb|{b;E]b>bL1sbȰa ȃ,aXaR1a `ˑ`I`>ǭ_Ǔ_n}_c2i5_X[^LJ^?^28 ]3k]&1]J6<U 5S5SN5T G>5!1vjs11Sd,24n92w+2o33HI3M3ْt4#^4nJ549[5!+5T  5S 5k6<,vf͊ Ĵt ]Jr]& lt {1{1]J6<U U 6<,vf͊ Ĵt ]Jt { d]uwTc]Cx]]1]M^ ^B$ ^i ^F^պo^ٴդW^Ս_!lv__DE^_fpF_-$___Y `"`%>Ԩp`BԌf`_oo`{R`5``˱[`ڧ`ӻaӜ a-[aYRacaғ3aςMaWebwѿb,wbF.~b]bqК$b}Ob`bϷbkbbbb5Άb:Tb7b͢`bVb` b}_bqwb].^bF b,˞bwX*aWaςΝaʋaJaY a-uaY`ɬ`Ɏ\`˱p`S`6`{`_o`B`%>`"Ȭg_YȒ^_x___G{_fp/_DEZ_!l^^ٴ^5^FǬ^i Ǚ^^B$džl^t$]Mb]]Q]Ad]uw1]J6<U 6E5?5c5$x55lq5D5 44,˰4L˛o4˅W4en4BW_4 ?3'3A$33ڙ3 3aʤ3DSʉp3'mf3 }P232\2O2[2>ɻ2q9ɜ2Y} 2-9<2ER1ȸc1~t31.1ve1ZǠ1@hX1)g~1š1{$000r0Ř05L0&b0Ĵ0&g05T070rÃ`0711¢_1)gX1@h^1Z 1v19*1~12El2-9+2Y2q9u2>Y22Oo\2\Q243 }3'3DSސ3a33g3s^3AY3@4 ({4B4eZ44L̑4,4555Dz^5lqgl5U$5C5?26"d6<,vf͊ Ĵt ]Jd]uw 4,`>P2=pQCPdcm뿐y=`@p`1 WUNKSPVh@h]@@_Av~`<0fp^ @B @`mp"`Ob`i e@0s?%e@/܈0v  X FqC@fS@Z8J0G07z 0r)=hB!ðYà[F;&1P' " ' $?6+U2eb@80la@?6s0M7`^~"pjP`mh Pڳpup@0=Evb0k(SaP[#07 pzN_@mVpbYpG0Ғ@3FsY Á0? Ðې|epU}J 0A4aP *Pp%P #q`0" ' ' Mp 0*ݵ8X @pMpP^~`w]@0O`p0,` WAPL}0d9N~*@S[i\ `ac`oepo̠ܰ?~p c@ @Z P @9<wp *qѐ`DvLP-h[cp|`Zc U}VTPG;SG;KWAPL}0 06hLY΀px+P]`KP2KP%PŰW0pN /g@WP@}q`@  c@ 2JS5`*$p16a<0n>@U`TIlPp?AG G=fp 3TP3*S "܈0̩ 0 x0Җ@1%0Pfv ^x PJ"`"OI Nph9' SI`p~ €iPPP0(`P{`{PňbZ:I¢pPB @^ @іUI,bZo~ɀ@o@9@@Y0^u>m/p'E\ \J`a`Mѡp1z@(%ē`Nrta^P@xDf@``@ ʹ˿Ppr`60}q `%&P/5C9@8n@A@E@Fg K AGp A* ppIP)" NP0eѐ*F+0/W װ`0>q-܀P-܀w`;7<]p>P}Pk(sRp])YNp7e5P 0rXtgܰko}pfyToGpFaAYYu;`N 6;`4:@' C|' E*080I HNyQ@RX^0pc`lPl:0to&tPv%@7PtŰqp̀d#3 V`XnJπmD?_L(S>`IS%`4PכP@B:=R@ ` X xo0wk%pbv0`[QIoPExPC`C|' 4:@' 4p/60p:nC| OVpPa(oM@*b—b^ Z'*O0iB+y2>pp0E\  c"y=`s1C@`.@ul bZJe0P-܀q-܀@JF`bZx@@yPpLoǀqQj3p(^@G@p1@@H EqZtPxmA@۾pPNP&4߼9BMX7d(Phtn-Pr0Pt-U``T"pOgPpK`,kFg ? @=۠UŐ00q" J``څ@S^Q4L@PPbPƤ ^  ^ @٠.qR-` @-܀Z-܀mKp ĈȀ }>0h`109p9p!D QPd{>bP'iЅ = `>f@|%` P`ymhPN(Je0)OG010]PKm}q0 ] pq06`DfLJ`V01V0`Kv 6~px+̐oRP]0w6NS@S:T9u@/P(`5$CP~pO*TP0UU/7`Rz Pp1KkhBCʠd0>`SP1@^"8vLP"8w,ry2k809ߐd;? ;f0:P8 ȋ4g`(S`I(70<`$?Z`pr pBpϠ_p&M0p`FMq@Q}PxH vp@vLP"8^"8^!ð_ bO  *@i >ud0|M*M0[`@G@MÞ Ĉ _ szpP*z`0.:pIH[gPP00ҝ iP&;`FPV0uV0ן6Őxh.@F`}x@&``p0GM1Pĩ  =00 x`nLp]4PG;f@G;_   b25w0pU40€]J ]J [0Y+ QA GQ0<0@H11Xv0 7̣@e *,0Nxfr@[uېCv/ `E `~06`7  (^@$<0pt PiXp `_m`9pJ^3TPr f `0Ԓ\[#0`Z%]0ob`Pn`h8yxo0 zP@v'@sy)Ȱ)$ Hi`L] plE@u20{5 ~`o0b1V0V0b2fsq`ja@!px0i $0TP@5} ``3>ժ`||pbwp>8>9~ 0P-Pqu0uV0PV0 b2 t*Pk9(h &d\&cF% aHC#,^" $WRWONxL΍IIBFwE8XB3?//= ň;ͪ`85pU4VJ2|&z$0,w-;* *M '(O rc'S $Ğ ![  GC&ڝoL[46]k&/A3 v6~ xHjT(tIV\.s<UadrpTF_f?TH`O(`OPR0\Zm!Kxwup++ޜܱiJZׅ$cʴ ϙLхad@ ó͌ ,1c*<:YwKȱNڬ m < w  uZ g)å vN >Xw?MԌ>Y|ws:)/ВBmQ.3ErY-U% ) T-b=*s|CBQx`5 o`F!~K"j#b$'z$*%Zc&l'L2(ln(Kw)V/*+/,Ez,(d-YY8.0į1"j81Y2gbF3J4iǵ56lL7?˝784P8#ʧ 9Vn :(?=eJ@͙AWͰBسCCD?EM!F_K(F7)YG'NHwx ~IJKQ^L L͸N"͍xOYPYPeQ2QPsR)̝-T4%QQUe%WgwAY pYʺRZ;f \?GQ]5h`ȫb+d\- Dev?Ɲf{!h_j{mWCqxvvCx}{@nZGAnEowDrg ۬.F-w@]ad$P2:f8~l-7#^!nӥ-[ʏ0rea_H(0E\=zu&(lZ]z Z/E83,a$-Fi@>P]apdi{'n5W ]ۓ bŊ@(J$,s \iƒi7^q{LiW[}4ġ xgc+kΠ%#ijeb^/і&O]dwlu9 ՚ ]LWc۱mId` Մ߀B4j Ip#U5֚Ԃ,k~K i  ujڹ?_xN-晀k O CRD=x*4 uW.gYQOqQ `!D聕#Hz$in% ['5:=)(,Au/;k2 ! 4-`<6eg8a39;<ϙ8>nAgDgFBI KZMxxNGߦP:)RTWS_TVKVXNZ$]4 _\ aac ԊepUf]iҦvh4^VjЫ.y //x>^g0MP5B'&y?i~y?{aysIw=us}hr|pq:Wdo2mZkH3hB,wfad"mb`_IQ]\ 5%[!YnXo0V(6TXJIQ' O)9^Nf\sLluJ}<;I8В}7l|g6z4yt3kw52w%1u0t40Hs/s.Tr.pO-oU,n,mU+fl*k:7c8zdP9 e{9Xfq:sg;kiM.< j*<j=Nl1>mu?"yn&.@6oizAPpBpKqךCsEutGv2^HAwDrIFjxJxrJyCeLzBMz{9fNM{GN|8\PO8}Q}RZ~K/SU"cV4.WE6ZL=\.]^_T[`MbL8cWdƘf:h*gɱi[lڗjx&mzno{q,r%"s7thEv6[w>hyvm {iM}]R<1 3ˋuqzJ*`I 9|q#B2 sIih|S3#7t>nWcSKWG_~s9}weu}~U|q{zgyzUFqy6x{Iw=w~v,uf:t<D&srm\4q-]ohnOlcj|pi8gza_f)f edq3õbtaɹ a -_iH]Nҕ]%[c6mYXVrDSC]5Q3AO3 NoWMMл_Le{J'2H)҆G؃(F39#V@"F( GDg1Jܩb~rfK?ػVآt,o ׈ D Z: ,V֏guՖ6b=Ӊ_sҲ)/*$;РϹaxurfn Gɘcvddk ƓטĎC(Ҥe\*bKϢ vΗD͵M.i6*xF(p'ʣxG"9Em06 c?9DŽ%ƦPv zz7w>ɔr jý6M=afe>ž*nl[GM kVBp/MN2B~%{@ 'W <u 4ODz;E*L) RN ¬Q Cðf+%͆Ģ&0&&0xǻĩh5eKjX7fhˆ~̮|h7͟sh!kpҘnLӅlz-@khs(e+ Iba wtw_^fMw]vHٗ[$YYکW۵!Vb{U4ۿ=TCc*R`OP|^ܡ&O7$MӊXLLLs+JJI_uGxhFkF3jD`C,tQܸ=ܕ&=;v_;2:#:ۺ8_*7(6 ڋh5ki3D<2 m40_0P/Yܕ.&|-oմ4,8+[\S*p7S)/)&1n(V!'ѝя&L&#yЈ%z+$ $}ϭ#KH#"<ΨV"Fv!Qb!  `zG0ͮ^ ͕_hgC93^0|nBwv9?atME=xu.0EYY͇.;GR "2Dt @ΘPsb@Ϥ0EsёIn!0*ҹK,F˴Ӌi~**Wհ!bS4m~fmwۋG`ܩއՈcrDO rj1砇6,ꍠ6Ѿ.rN=+:ZMUsA: $hjdSK(4mhz L zv m/2>>e=V3 M"sO$-O%1(ZB+(-\JL/ Ѵ0 b3d!36@"8 #D&9{$qW<]%?@&F/@P'C(Fk*-IjC+0LS-ЭP/T1= W2!Z3[4[]6 I`7ȰcV*Pk9 |~"q)Cg/Q0u1EB1#7 .G&c)C  <11! 5抻Å "/ݦ'~A뒹yu^2s&Wk bl\W+rH=G<>W)-e>ߟn4t9a&uvvtm_njɓj Zcz)Z[Mg|<X&5 O{7Ѻp5yeD߹^9ݥ:PmT Ckm<E?ٹTLIMPih릗VXjX'@W˹Tm MR6.Y 6<`mGjQ]~Z7KbI Vsi՚oj~]hVO#2/p){٫*k Vm11 g>IQx'IbOɅr g~"q)C";";RobustPath3 s`}yhECQݑi <r!I4;^9" ^q?=bu2ZKz y#Ps FHB݀|IY:_mP{'Yfo*?ěa|}TIU@uLK>OpcІ\|c& >:=lDqUP!㫪iV(wT\F(@pnˡדK jul*+a Gt (t9q<Z5QKC8Z7:x%tcla#YqP_֌H/5ABw=ݠ.`6n1b99ApJ?RLf ѿ}6&Q@xTA%{lc0H~TTGH9t?FsfLK@eR fX^۰td >knאp#vZ|fU I 6 _u7+z!&]'-X3v 9z?Fs0LK@R zX ^۰Xd kn]pzvZ+|fu I66 Iu|f+vZzp]knd X^۰ XzR LK@0Fs?z9 3vX-']!&z+7u _ 1-3aj6oh9B +<3 ?BBn2EIybNQ=SY[y_ie#kLiwi,ʦTv07T`}y3r À%s R'J  K VR0~X8OL!#'w-#4N:tATVHsWOb\{V]]ZzdRmk:uH r Y:x:Yj\^À8^%Da22Q3p l |Bm|}x@qt>m|er Y N2N2QF?O^%8#|81-1- bZiP|]p|8[w_Nd/rs>)@眊B)ƙs`p2d/qaͦ8[]7 Fj$g=d:߀u & -\ #4D%;;'B9a(WI4$)VP")V);](PdG&j$#p "yv8|Fe@. 8Jw znR#t1v`$h}どH7δvfpAi @B1Dz_E{G$!iG'bH -2G2F8EE%"BzhQ>s^}:&kS 6Yx2(p1$Z0BW081-q2*4=UI6JF8;X_>O{kAQKEvJ'Pǜ˸WG^ne6<lZesM{Շ*ԕkКI͐PeF_Nr6l?`%9Ñ6kmN0À.˹)/ˁy1ʨ mth`ŁH_rpnuo z1녧~d}:Z.*1Gv,d4?v`|0/mmQ'_3(sĒ|gRڦZ*qW$Z*{C 0QGԻ lq y %skir . &_t T!9l+m`t o>BfJ }88GvsoTl,gI `PyPLAe9f%%1S j*##%J[&X&_&Kt% Z%$c #- ( ?TPvi6 :bZ 1-bZObZ}x@bZ?bZÀbZ3bBpcu4ld9:f$lhxpk n*r8sx~ T+0W1TWL@6eWL10 T+׌ުs4Upt$lb:l~GBpL3zÀ|YtQwo4 Lu-VmsxfE`-YW)S=uM* HvtC;E ?'ǟ;Ft\9(g6m5D3f 2 1ڙX<1Xkc1-1-bZ ,bZbZhv\n٫Xu}UiV|]Sw.Q;QP%6P2QnSm*U]|X!V'^`jaFbgbZkcbX<c c effqh,mgj.(t\lsǟpTE t;yt ~*u=W)ZEsxVZ LJ4ĜQwYt!|ĚkcBX<ng J Xgt\ǟEE  MV ؊CugW)z.E*sx{VL LeKQw(Ytu9k|bZbZbZ   _7 U!&'#-א3v>9t?FsfLK@eR fX^۰td >knאp#vZ|fU I 6 _u7+z!&]'-X3v 9z?Fs0LK@R zX ^۰Xd kn]pzvZ+|fu I66 Iu|f+vZzp]knd X^۰ XzR LK@0Fs?z9 3vX-']!&z+7u _6 IU|fvZ#pאkn>d t^۰XfR eLK@fFs?t9>3vא-#'!&U 7 _ |1-38*6g 95_ _<" ?-WBTڴEsHN)FSY(C1^w6MdISjΒnw"uThD[&%>K/!KÀml%hQ= Ș} zժd@<4Z>1#H"(//25Ƶ<CqJqDQxX&2_:ǂf5m}$?t6zN4ܗOӿupuÀF$fY+2!X=~)+ y skgOW\+oP\j@1-%ڻDuL ct S<p{bcu,Qnpb<i33cӓi^U#(XgSaDrMVMG}cAŢ8;׈64!p*ɞ1$n > 擹p ~Lwl@qG1uNh䵣5> k㗹gd䤶I枏+4!#K',1- bZaj`#_h-_-X{`D`b[{cgfKjLcn-sqcyTxD$)a>caQnX6ņRƾi"˵(ϋ;mW Ԅ/8NHx+Pk[  AN ?xD$V4#+JVL2;% 9% @ĜF˾.M}=TnZq`ljnf0{lRrp,bwF4}uoAPLU Uw\ [+E.fϨTCȍ{%>5?:5 Ptz/wJ$s/pynmjOigdkbZ_?4\x<ZFT<W=?U٧S#WQOND/LIYJ$HFa3Ep‚DK`DBC_yCODD`TDX-=E{PFAj? G(EH.{J(M91P%3S7 }(RYeddC_ JTaZ>tcF2jCdE*e #F|et6Aee\Zed^ cgbZ 1-5) 8;! v> @B1Dz_E{G$!iG'bH -2G2F8EE%"BzhQ>s^}:&kS 6Yx2(p1$Z0BW081-q2*4=UI6JF8;X_>O{kAQKEvJ'Pǜ˸WG^ne6<lZesM{Շ*ԕkКI͐PeF_Nr6l?`%9Ñ6kmN0À.˹)/ˁy1ʨ mth`ŁH_rH3Ci(IXܶOӊS `V˂Z($]Ĺa8ԉeȧHin)Jrmwv{i=`wߺY}9̱?¨p,s\&d3I'=۵p䅗jےSli`njat!kpJ)$L1-  1-1-7J).> #F@5M&(SԊY`:mcf ~Pm  s  Vz L H vm Cg'  ŀ  LgEx_aTQ"s)À1-͝8 Э? RF+ԱGMjՍRX|cco;Zoz~)Bʈ8iÀÀɓB@XҚ!X΄X.ި{T4Tvъo<v*Ғyvi88)v6AHYʅ?fpz~sHLk Ed J#\~U u-P-Kc28F}B[ߏ=^ڮ 9Vl5,ɬ1-À,ڜ{B(%fi"Jlܧ3l{1oкoc 4;XgaRA )M$/Eь',>,E71-1-  bZ1-i7-Rp7*BwX'.~&?9%b$Ռ$hZ%-&VB.q.0hH01-o@1\1(X2 4 5R47t9 Ko< v@/D"DV&iJ)ơOH,cU .u\2?/b0Yi12?q%0pxj/.HL+EHw(#6%'q -w0™eN??% I"gbÀB2Չإ ܃dMl> ᾌ+ RݼEA~LWvoThΪbZÀZ.I6TLR`Nm5IS(-gF(C@c>PUx=g;Vfu;G4;vzf8;:Cs;݈mY<g8>ygat@rxZoBU !EOUI IN+B,TP%/ܖP;[2QG%nSm*S*U]_[|X!w'^2`jbaFbgbZKbv8beCcVdue VCf-FiЍl>pnuo z1녧~d}:Z.*1Gv,d4?v`|0/mmQ'_3(sĒ|gRڦZ*qW$Z*{C 0QGԻ lq y %skir . &_t T!9l+m`t o>BfJ }88GvsoTl,gI `PyPLAe9f%%1S j*##%J[&X&_&Kt% Z%$c #- ( ?TPvi6 :gj-V'~!|]*n82W6]%;.w|]Vu}Tn٫hvbZ 1-bZ7I\=XDPUiVK0SwP}.Q;UQ[kP%a Pl2QxRnSm*WU]|X!)'^_`jaFbgÀbZ3bBpcu4ld9:f$lhxpk n*r8sx~ T+0W1TWL@6eWL10 T+׌ުs4Upt$lb:l~GBpL3zÀ|YtQwo4 Lu-VmsxfE`-YW)S=uM* HvtC;E ?'ǟ;Ft\9(g6m5D3f 2 1ڙX<1Xkc1-0g0.j-ZV''!|$]"@*n e26% ;.!w|]$ML/f_z˝rhP PG%c*8O0[5+;\ATGȼMUWoGaHYCkX~/uXZeciH#w6)Rԍer_F׆1:W@Vk !b\M)'*Ő/.;5aL;5mAtF'KE'vO.Rg~T(.Vp"q W]4)WQ1A"W;8̛WQb@mV;H$UX7OeSWQb_%OEwgL`yoIwE9 @!<.XN6 1-?*&IC"F5ˆi{D86`\"-F+m@jBJ 5T OY=YY%uQa+01-7)K<aBb YG@L-YQY=V$ ZBJ`hfl+q"-w#``|s8{xfvV5"F]*&IÀ1-6 8<.e1@E9 I L`yOEw "QblS5UX7!V;)eWQb1W;8&WQ?W]4G+VpQT(\JRgfsO.pSYKE'y!F'$A `;:5a.'*\M!ulV@:<׆Fpԍek)҉6ݢ#Uil\cޤZ?*u^kX~ٹaHYЖWoˑDM=GéAT.4;V5!0*}%cE PGcrhsN,_z /} >uek$TbZ wT7/*78*FFe]Sxa,# nJpe{# lwCǰEy޹B0 t!MdWDxG 'PQF#ĕfM(_.U-x1#7'l<&w+&oAhj.PFy1#LB,3Q 5GW7s_:9/g:<o:w2:6j9C7=u ^4e1-F.K(+FЛ("%6:~5T_xgߞ'KFb5(V ,jHgLaN YP.xe9p*( 2s%xh+.%1-7?s=MBlXGbGM|Q*VO[-Oagb_mFrFw |Ɩ:*#AeN0Y~WJ!C4)cÀ1-7Ac861E*b"%v{ǘ!mQl-7꧹ zkڤݪi I]s#T㳴C㛷r:֡A_3"uyxKkإ7`;V9MJ Gz(@ݑ::95׺/k)m$duCr:_"6 ~v uPBl6QbZ #OPB㶊 # ";:C(k$E/)mF/k 5:}@ݑ9\GzQ$MJV9`Fk&uyx_A~Ir:~LC6Tks?IaigMks=꧹"Uwl-8mCǘ "%v}*nd1E86z>܌RD'$I& NRy0U9DXYT Zd(cZ/;Z 7nZ?[9YG-EWǙO>VWSI_Q)g%]N oHJywxGFjA kfMC QFGW* 'K{GOuWDSC!MV ZWw]0`i޹cEyhǰlKCp6 lr1sr{tEnJps2a,#rPSqFFeoK8m*5*7j"fTbZ ~2^+9 .FTrb !oX!*ygr$j<l4Ӕ%rvA4mm_D҃=OE*a݊KU f琎X GMd#mn I#((\/,!!B1^%=b6})<#,|0Aq /FU2\cL^+4BQ6W-8z _k:AgK_; >oJ@;Hwd:ɂ97341-}X.f)B+Q(d%/ @IF"VFޕUzaS%E)d9h e' $RV׌\w%5w+ ϩ1-7N0ϩ=$RwBGM׌Q $VhZwh`9fl0gqS>v4ZEzߟSBU;"VI9g{%/<(0+Qܨ.fÀ1-&4|7+9X:ɂ;H; >=:A 8z _64B2\c /&6,|0+e)0u%=b5d!B:1/>]C]uGW IKnnO%#SMWG[6^P琎amUdV݊g *ai}=kˡDmՓo4mq4vrlsl4tg$jtu8yu8CoXtƎb sSTrQFpGL9 m+j1^f~2bZ A*{49oN;oQgR ]6y+- fn40A%2%-+;6.S( 1>5#U8<%@kY"Dw&TH*M-CQ0V+2ZD4a6Vh89oդ8w_Q8618;[6Ј4YS1-K.r,N8e)`|`%&%"|@N"}.#t6.j~KJpX`HIGo4ɰ (v C)?Ho%&*~+т1-7Wkт=3~BHMSQC)Vr7(Zɰ`lfIGl?`qu9Kzp.j+tAp}dHt@WW"&%T)`|Q,N8ĵ.À1-҉x4YS}6#8;8\889"6V^4 .}2+0VK-Cf*&T!"%)&-6U0>4 7:6= P?KEmKw(Q9:~VF[O1dmt\ǽzzۄ!˼sPlï'h~^NṽӬogQ6 _{ WȹP>Ew®;k}Q>16y="'~tao!<i bZ";"; Hobby3 tsr$TRTe_ >ML/f_z˝rhP PG%c*8O0[5+;\ATGȼMUWoGaHYCkX~/uXZeciH#w6)Rԍer_F׆1:W@Vk !b\M)'*Ő/.;5aL;5mAtF'KE'vO.Rg~T(.Vp"q W]4)WQ1A"W;8̛WQb@mV;H$UX7OeSWQb_%OEwgL`yoIwE9 @!<.XN6 1-?*&IC"F5ˆi{D86`\"-F+m@jBJ 5T OY=YY%uQa+01-7)K<aBb YG@L-YQY=V$ ZBJ`hfl+q"-w#``|s8{xfvV5"F]*&IÀ1-6 8<.e1@E9 I L`yOEw "QblS5UX7!V;)eWQb1W;8&WQ?W]4G+VpQT(\JRgfsO.pSYKE'y!F'$A `;:5a.'*\M!ulV@:<׆Fpԍek)҉6ݢ#Uil\cޤZ?*u^kX~ٹaHYЖWoˑDM=GéAT.4;V5!0*}%cE PGcrhsN,_z /} >uek$TbZ  a ]5.> ;d8U⢬Xz$(-z625;?PgF&*aMTTKT\&cjkVrM zb~<OJ%7_ !7܆P pԑ%>!7!.^:g_GC’NyOiU|] Xf#oQ'xwf+&`Y-ϋe/Vx0t1-0ݑT 0.-QM+VD(pz&M}#I/6oBR}_7 cӽ6][t&: CWtjCs0!Zwm3@ #\(<*˨, / 1-5?g:y?yDA'IbMMRLWO~Oq^c/>efBlUc s,"yc>k[Ɵ6Gy3<g/ 7y9ؒ 8,$x\<1) -PÀ1-ί+;Ӱ;FkOP6=WU$ _jf^Zm20r&xx3|lMx6f8br}f6Y59'ZO|B/T7¼J?8Gy{O.{w[Wnu^nef[g4mR^tvUI{'L:^Aj74+ =*)OF &i8vیxyƕJ4't2_ՃVdxfjl`'gTM'obE>8(Z51xb+s d%1y0B}- wn Ip˰fiňQbZ  xQPf I WKB) bys%1+s $18(Z>E{M'ZT`'gƖluxfêqՃ'2tJ4ƕz;یv&i 0jk/?)S^ =v+ڤ974WAj2L:^aUIӮ^g4]Jne u{w[{"*DJ2ì:|BCF59HOYNUB6SFfY`br^66fd[|lMiψx3o:Gr&tm20yf^5_mWUOP6Fk;1-DI-P) !$x\O {ؒ 7yA/2y3<d#Ɵ6~kfGPcsi-e"c HB> Oq6LQM"p''y,kg1-3Ph5 7.˨:x>*D& H8wmLws0PUW_W0:i&uC6]{% cR}h6#I&M(a+VS-QYA.00ݑÀ1-њ0tٯ/V-ϋo+&a'x#~' X I|is’'|_4D^@E7!L>W!ԑb}zPl8I7t|7 e?zbH+rMkV3[c\~ T}MT F&*$?#5EE-zyw$(TXzyUs+ ;kP0]5fbZ Djʝ T 0r&/{/B.8 '?~FՖINFz`Uû]oDe;u}8|L%$warzVc)>幉3>CRiDH 0N#S4Y!^ &f%NCmy<)tڬ-A|0</s14}22g1-l06@b.y,,*<'i$)k!_lzFp $C$:J{ܖ <J] HbcZ>1+N:s#'w*>- 1-4l4 70>;BnsF(NI>M֞SYdH_ f9JlrjܖJ5:: $}7ML/f_z˝rhP PG%c*8O0[5+;\ATGȼMUWoGaHYCkX~/uXZeciH#w6)Rԍer_F׆1:W@Vk !b\M)'*Ő/.;5aL;5mAtF'KE'vO.Rg~T(.Vp"q W]4)WQ1A"W;8̛WQb@mV;H$UX7OeSWQb_%OEwgL`yoIwE9 @!<.XN6 1-?*&IC"F5ˆi{D86`\"-F+m@jBJ 5T OY=YY%uQa+01-7)K<aBb YG@L-YQY=V$ ZBJ`hfl+q"-w#``|s8{xfvV5"F]*&IÀ1-6 8<.e1@E9 I L`yOEw "QblS5UX7!V;)eWQb1W;8&WQ?W]4G+VpQT(\JRgfsO.pSYKE'y!F'$A `;:5a.'*\M!ulV@:<׆Fpԍek)҉6ݢ#Uil\cޤZ?*u^kX~ٹaHYЖWoˑDM=GéAT.4;V5!0*}%cE PGcrhsN,_z /} >uek$TbZ :is& 'nUŇ!e;t9=u"1a'dA(,ԎIV2}8]j>pMDfK#U|y_d~xiuoTtrcqE pV_+p$pT@tqobs'ZvU~σy}T|fa$>P|/'"5+4 :V;wC0 IӸOݲbUM-Y|]Rd`Sc$"-d}cd#Udd+gd^3Acs;ZbD(`9LO"]T[\zWefSMmO}vZJ~En?8e1-/:(8,ɲ*nc'J$+!9 *4՚td@F%٠']ro >f 7fSmS"&#)&c@, -.p'1-7"M<4WB[}GPiL;VQ*VGZ$iaACBg`m0 rMxd\Y}#5 o'M~?p̼9(ygUwIu/b'Y0À1-:wC{,K]8RAYT_6}dbi>m#q-?,yt 5B0vS,=x Fy5O/ny2Wwyd2y*xp}wL|tf?pk^pfq_}9X0 P* Gw )>&؅4AxH) _ rΩ^\E 6  -QGt xk M C' g Q~gQ?rb/af|~[N RPYEV숄?$8Ň2sG*,`&ٸy!f_qv,7i)DlV ЧR{Nk|o]d{ibZ";"; Hobby4  Àv׾ucz7Sri#0YYԇo*J l7мAw VK5c{Tσ^Mg}Wp yЎӸջH2uN8<R_$3r_a(~ Es8M$#j*HD1;oF7(6>țEL"V_ i%Fr`|FZ 9Q=W C:bJ /r!Sdpyց SZF̂3 [U]<i~Exas!n; Kl wijlgstemd>d%G}c4w+6bi1-bZ7Cbi=c4wB~d>dGemLgsQYiVWl wZn;`s!fѻxlT6~qiv{ANUt3FPց p*!S zw$C  ÀQ܎9Q4FR  ?O#e*d61aoF7D>6DJPƍV\H ~a5f(kpruAy $}vtR_!~u@M2jHDyЎpcg}^MTwK5cAw N7ґ*Jw60Yrizsck׾bZÀV=GJ>_kA1Gpqs#txD!m;@cM\^0VmbPakiVJhGE @N2<7p8o5~2q0#De .bX0,#K)f+o>,*Ó1-*A$-*Ó0+o :,#O.bp0#Dw2=5H8<7YS@N}E Jh7Pak)Vmu\^jcM[86mLZxD>%t1qs#Aj- iS=GÀ  À%nVbxWNSc.'mz7 A(m*D(z gu+*%/]w%9Z_C|+5bMX |Dbidlw,*ą& 9a ]y!Qח7U粲_\(-y EXgQ5 Ideln ˬް!+w>('>q/9t؂6`=fDLHSx [5Eeb>pjWIϩq yg JW aC;6P,_;azڔW } gY:F{hk"}owoir#Fn5 00kz5i(X?g%;evd %pc&+. b1-bZ7+bK{PUT_c8nhlrA  I{AfQ5NXS -y(A"粲"'UUחM8X7y  cą]/w,׌l׸biB2X |$M[C|+9Zε/]}%.&*¬'uL gz/QjKLPA7+/'c~N|xtM҈W`,iaak Z t } Ǒ@VGG{Fbv*3mM>VA s>R%*Ȑ;9#vv)D_eQ  R|LJwRpO"mD& j})!g(,Qe.c 1-bZ7#^=\CZI)FXƶNzCXS=XXbX]`Ye`SZc\{i`Pzoee*t̓jLyq~XxGm{dY;MN[*lCJ-NʛQӢ iN 'l,eG_4!ÀaV42KnDu K b3$y y Jp#D&Y*O N1G 8>H@ F4METv.ZkaNGVgpƎmYn~sݸxqv1}XTq/5h pE"rYBS6 | r$v_i si_sTJ ,@ 1ؔ#kRr)k|Pwx)s5+mzhN\bZÀVV;OI <5E/iW;!+RbY.ulaXWfv`Z"Ug;PLaKH{anDTA~}mZ?ApR<d5;':Wg9J+8>-8k1RA7<$:88 )9m;)ɑ=3c?Dخ A<`E1HTLgyP0rUnIZd̓`x4f llh]ZuO-_@n3 {A%xI  À  ÀPևDqn9rQ߷}iFC ϯD/Z%9h~f2bE<4.E|OcYELb:;l-usb|%~K Š^56^Fl|˻ϐC!eG}O"`$L퀟 *P*@74 Z :/E#>,+64%2K: Bh XM=@ W b m (Qx{h7F/0Azjܘ{U՗wRi9ݺW2V#~&IaxlOcPs8gn kiudzg\3e5<d5%Sc0s+ba1-bZ7=ba=c0sBd5Ge5L?g\QSiudVK=kZ7n`s8gfٟxlOlf~&IqBVv2{xbK9RiV՗e#ܘ{jBf~zÀ\xL/0ޘ7 (Q  ]q   X'7 /r7#4%>x>,FEMd2 T|[e74b7hn*Pta z +7$Oeɽ'(l|$^^ ~:fusbl-bu(YEL?OcSEn<4.ۚ2b%9h ϯ[F}~Ky rsn`|ևDhbZÀ_qƤ[_9ÙWf{{S_BI9}?3c&zCm P)5 b*ڕ![ͨiPsl rIP!]?hlVNL1-HLK#Vkh8?rPrPs[{/nPc$W4P)IBPCm;.#Jd9};B {ÙSƤÀ <ÀPևDqn9rQ߷}iFC ϯD/Z%9h~f2bE<4.E|OcYELb:;l-usb|%~K Š^56^Fl|˻ϐC!eG}O"`$L퀟 *P*@74 Z :/E#>,+64%2K: Bh XM=@ W b m (Qx{h7F/0Azjܘ{U՗wRi9ݺW2V#~&IaxlOcPs8gn kiudzg\3e5<d5%Sc0s+ba1-bZ7=ba=c0sBd5Ge5L?g\QSiudVK=kZ7n`s8gfٟxlOlf~&IqBVv2{xbK9RiV՗e#ܘ{jBf~zÀ\xL/0ޘ7 (Q  ]q   X'7 /r7#4%>x>,FEMd2 T|[e74b7hn*Pta z +7$Oeɽ'(l|$^^ ~:fusbl-bu(YEL?OcSEn<4.ۚ2b%9h ϯ[F}~Ky rsn`|ևDhbZÀUp\HO:{,2Au%#K^ YUxgsZYfn2iϭietd`e ]!|Y*VT{nQPo!O̽bqN3-VjLJ4L=K1-K[$WKUL LN3-8O̽@QPګTyVYYLH]!T@`esdJien2i{rsZphx`YR4KC#u5~A'{ r yp\Àgdspy-1.4.2/tests/tutils.py000066400000000000000000000036271354474061200157220ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### import os import pytest import gdspy @pytest.fixture def target(): return gdspy.GdsLibrary(infile="tests" + os.sep + "test.gds").cell_dict def assertsame(c1, c2, tolerance=1e-6): d1 = c1.get_polygons(by_spec=True) d2 = c2.get_polygons(by_spec=True) for key in d1: assert key in d2 result = gdspy.boolean( d1[key], d2[key], "xor", precision=1e-7, layer=key[0], datatype=100 ) if result is not None: r1 = gdspy.boolean( d1[key], gdspy.offset(d2[key], tolerance, precision=1e-7), "not", precision=1e-7, layer=key[0], datatype=99, ) r2 = gdspy.boolean( d2[key], gdspy.offset(d1[key], tolerance, precision=1e-7), "not", precision=1e-7, layer=key[0], datatype=99, ) # if not (r1 is None and r2 is None): # c1.add(result) # c2.add(result) # if r1 is not None: # c1.add(r1) # if r2 is not None: # c2.add(r2) # gdspy.LayoutViewer(cells=[c1, c2]) assert r1 is None assert r2 is None else: assert result is None gdspy-1.4.2/tools/000077500000000000000000000000001354474061200140125ustar00rootroot00000000000000gdspy-1.4.2/tools/maketestgds.py000066400000000000000000000422711354474061200167050ustar00rootroot00000000000000###################################################################### # # # Copyright 2009-2019 Lucas Heitzmann Gabrielli. # # This file is part of gdspy, distributed under the terms of the # # Boost Software License - Version 1.0. See the accompanying # # LICENSE file or # # # ###################################################################### import gdspy import numpy ### PolygonSet cell = gdspy.Cell("PolygonSet") p = gdspy.PolygonSet( [ [(10, 0), (11, 0), (10, 1)], [(11, 0), (10, 1), (11, 1)], [(11, 1), (12, 1), (11, 2)], ], 1, 2, ) cell.add(p) cell = gdspy.Cell("PolygonSet_fillet") orig = gdspy.PolygonSet( [ [ (0, 0), (-1, 0), (0, -1), (0.5, -0.5), (1, 0), (1, 1), (4, -1), (1, 3), (1, 2), (0, 1), ], [(2, -1), (3, -1), (2.5, -2)], ] ) orig.datatypes = [0, 1] p = gdspy.copy(orig, 0, 5) p.layers = [1, 1] p.fillet(0.3, max_points=0) cell.add(p) p = gdspy.copy(orig, 5, 5) p.layers = [2, 2] p.fillet([0.3, 0.2, 0.1, 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.4, 0.1, 0.2, 0], max_points=0) cell.add(p) p = gdspy.copy(orig, 5, 0) p.layers = [3, 3] p.fillet( [[0.1, 0.1, 0.4, 0, 0.4, 0.1, 0.1, 0.4, 0.4, 0.1], [0.2, 0.2, 0.5]], max_points=0 ) cell.add(p) p = gdspy.copy(orig, 0, 0) p.layers = [4, 4] p.fillet([0.8, [10.0, 10.0, 20.0]], max_points=199, precision=1e-6) cell.add(p) ### FlexPath def broken(p0, v0, p1, v1, p2, w): den = v1[1] * v0[0] - v1[0] * v0[1] lim = 1e-12 * (v0[0] ** 2 + v0[1] ** 2) * (v1[0] ** 2 + v1[1] ** 2) if den ** 2 < lim: u0 = u1 = 0 p = 0.5 * (p0 + p1) else: dx = p1[0] - p0[0] dy = p1[1] - p0[1] u0 = (v1[1] * dx - v1[0] * dy) / den u1 = (v0[1] * dx - v0[0] * dy) / den p = 0.5 * (p0 + v0 * u0 + p1 + v1 * u1) if u0 <= 0 and u1 >= 0: return [p] return [p0, p2, p1] def pointy(p0, v0, p1, v1): r = 0.5 * numpy.sqrt(numpy.sum((p0 - p1) ** 2)) v0 /= numpy.sqrt(numpy.sum(v0 ** 2)) v1 /= numpy.sqrt(numpy.sum(v1 ** 2)) return [p0, 0.5 * (p0 + p1) + 0.5 * (v0 - v1) * r, p1] cell = gdspy.Cell("FlexPath1") fp = gdspy.FlexPath([(0, 0), (1, 1)], 0.1, layer=[1], gdsii_path=True) cell.add(fp) fp = gdspy.FlexPath( [(1, 0), (2, 1)], 0.1, [-0.1, 0.1], tolerance=1e-5, ends=["round", "extended"], layer=[2, 3], max_points=6, ) cell.add(fp) fp = gdspy.FlexPath( [(2, 0), (3, 1)], [0.1, 0.2], 0.2, ends=(0.2, 0.1), layer=4, datatype=[1, 1] ) cell.add(fp) fp = gdspy.FlexPath( [(3, 0), (4, 1)], [0.1, 0.2, 0.1], [-0.2, 0, 0.2], ends=[(0.2, 0.1), "smooth", pointy], datatype=5, ) cell.add(fp) cell = gdspy.Cell("FlexPath2") fp = gdspy.FlexPath( [(0, 0), (0.5, 0), (1, 0), (1, 1), (0, 1), (-1, -2), (-2, 0)], 0.05, [0, -0.1, 0, 0.1], corners=["natural", "circular bend", "circular bend", "circular bend"], ends=["flush", "extended", (0.1, 0.2), "round"], tolerance=1e-4, layer=[0, 1, 1, 2], bend_radius=[0, 0.3, 0.3, 0.2], max_points=10, ) cell.add(fp) cell = gdspy.Cell("FlexPath3") pts = numpy.array( [ (0, 0), (0.5, 0), (1, 0), (1, 2), (3, 0), (2, -1), (2, -2), (0, -1), (1, -2), (1, -3), ] ) fp = gdspy.FlexPath( pts + numpy.array((0, 5)), [0.1, 0.1, 0.1], 0.15, layer=[1, 2, 3], corners=["natural", "miter", "bevel"], ends=(0.5, 0), ) cell.add(fp) fp = gdspy.FlexPath( pts + numpy.array((5, 0)), [0.1, 0.1, 0.1], 0.15, layer=[4, 5, 6], corners=["round", "smooth", broken], ends=[pointy, "smooth", (0, 0.5)], ) cell.add(fp) cell = gdspy.Cell("FlexPath4") fp = gdspy.FlexPath( [(0, 0)], [0.1, 0.2, 0.1], 0.15, layer=[1, 2, 3], corners=["natural", "miter", "bevel"], ) fp.segment((1, 0)) fp.segment((1, 1), 0.1, 0.05) fp.segment((1, 1), [0.2, 0.1, 0.1], -0.05, True) fp.segment((-1, 1), 0.2, [-0.2, 0, 0.3], True) fp.arc(2, 0, 0.5 * numpy.pi) fp.arc(3, 0.5 * numpy.pi, numpy.pi, 0.1, 0) fp.arc(1, 0.4 * numpy.pi, -0.4 * numpy.pi, [0.1, 0.2, 0.1], [0.2, 0, -0.2]) fp.turn(1, 0.4 * numpy.pi) fp.turn(1, "ll", 0.15, 0) fp.turn(0.5, "r", [0.1, 0.05, 0.1], [0.15, 0, -0.15]) cell.add(fp) fp = gdspy.FlexPath([(-5, 6)], 0.8, layer=20, ends="round", tolerance=1e-4) fp.segment((1, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath([(-5, 6)], 0.8, layer=21, ends="extended", tolerance=1e-4) fp.segment((1, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath([(-5, 6)], 0.8, layer=22, ends=(0.1, 0.2), tolerance=1e-4) fp.segment((1, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath([(-5, 6)], 0.8, layer=23, ends="smooth", tolerance=1e-4) fp.segment((1, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath( [(-3, 6)], 0.8, layer=10, corners="round", ends="round", tolerance=1e-5 ) fp.segment((1, 0), 0.1, relative=True) fp.segment((0, 1), 0.8, relative=True) cell.add(fp) fp = gdspy.FlexPath( [(-3, 6)], 0.8, layer=11, corners="smooth", ends="extended", tolerance=1e-5 ) fp.segment((1, 0), 0.1, relative=True) fp.segment((0, 1), 0.8, relative=True) cell.add(fp) fp = gdspy.FlexPath( [(-3, 6)], 0.8, layer=12, corners="smooth", ends="smooth", tolerance=1e-5 ) fp.segment((1, 0), 0.1, relative=True) fp.segment((0, 1), 0.8, relative=True) cell.add(fp) fp = gdspy.FlexPath( [(-3, 8)], 0.1, layer=13, corners="round", ends="round", tolerance=1e-5 ) fp.segment((1, 0), 0.8, relative=True) fp.segment((0, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath( [(-3, 8)], 0.1, layer=14, corners="smooth", ends=(0.2, 0.2), tolerance=1e-5 ) fp.segment((1, 0), 0.8, relative=True) fp.segment((0, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath( [(-3, 8)], 0.1, layer=15, corners="round", ends="smooth", tolerance=1e-5 ) fp.segment((1, 0), 0.8, relative=True) fp.segment((0, 1), 0.1, relative=True) cell.add(fp) fp = gdspy.FlexPath([(5, 2)], [0.05, 0.1, 0.2], [-0.2, 0, 0.4], layer=[4, 5, 6]) fp.parametric(lambda u: numpy.array((5.5 + 3 * u, 2 + 3 * u ** 2)), relative=False) fp.segment((0, 1), relative=True) fp.parametric( lambda u: numpy.array( (2 * numpy.cos(0.5 * numpy.pi * u) - 2, 3 * numpy.sin(0.5 * numpy.pi * u)) ), [0.2, 0.1, 0.05], [-0.3, 0, 0.3], ) fp.parametric(lambda u: numpy.array((-2 * u, 0)), 0.1, 0.2) fp.bezier([(-3, 0), (-2, -3), (0, -4), (0, -5)], offset=[-0.2, 0, 0.2]) fp.bezier( [(5, 0), (1, -1), (1, 5), (3, 2), (5, 2)], [0.05, 0.1, 0.2], [-0.2, 0, 0.4], relative=False, ) cell.add(fp) fp = gdspy.FlexPath([(2, -1)], 0.1, layer=7, tolerance=1e-5, max_points=0) fp.smooth( [(1, 0), (1, -1), (0, -1)], angles=[numpy.pi / 3, None, -2 / 3.0 * numpy.pi, None], cycle=True, ) cell.add(fp) fp = gdspy.FlexPath([(2.5, -1.5)], 0.1, layer=8) fp.smooth( [(3, -1.5), (4, -2), (5, -1), (6, -2), (7, -1.5), (7.5, -1.5)], relative=False, width=0.2, ) cell.add(fp) ### RobustPath cell = gdspy.Cell("RobustPath1") rp = gdspy.RobustPath((0, 0), 0.1, layer=[1], gdsii_path=True) rp.segment((1, 1)) cell.add(rp) rp = gdspy.RobustPath( (1, 0), 0.1, [-0.1, 0.1], tolerance=1e-5, ends=["round", "extended"], layer=[2, 3], max_points=6, ) rp.segment((2, 1)) cell.add(rp) rp = gdspy.RobustPath( (2, 0), [0.1, 0.2], 0.2, ends=(0.2, 0.1), layer=4, datatype=[1, 1] ) rp.segment((3, 1)) cell.add(rp) rp = gdspy.RobustPath( (3, 0), [0.1, 0.2, 0.1], [-0.2, 0, 0.2], ends=[(0.2, 0.1), "smooth", "flush"], datatype=5, ) rp.segment((4, 1)) cell.add(rp) cell = gdspy.Cell("RobustPath2") rp = gdspy.RobustPath((0, 0), [0.1, 0.2, 0.1], 0.15, layer=[1, 2, 3]) rp.segment((1, 0)) rp.segment((1, 1), 0.1, 0.05) rp.segment((1, 1), [0.2, 0.1, 0.1], -0.05, True) rp.segment((-1, 1), 0.2, [-0.2, 0, 0.3], True) rp.arc(2, 0, 0.5 * numpy.pi) rp.arc(3, 0.7 * numpy.pi, numpy.pi, 0.1, 0) rp.arc(2, 0.4 * numpy.pi, -0.4 * numpy.pi, [0.1, 0.2, 0.1], [0.2, 0, -0.2]) rp.turn(1, -0.3 * numpy.pi) rp.turn(1, "rr", 0.15) rp.turn(0.5, "l", [0.05, 0.1, 0.05], [0.15, 0, -0.15]) cell.add(rp) rp = gdspy.RobustPath((-5, 6), 0.8, layer=20, ends="round", tolerance=1e-4) rp.segment((1, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((-5, 6), 0.8, layer=21, ends="extended", tolerance=1e-4) rp.segment((1, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((-5, 6), 0.8, layer=22, ends=(0.1, 0.2), tolerance=1e-4) rp.segment((1, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((-5, 6), 0.8, layer=23, ends="smooth", tolerance=1e-4) rp.segment((1, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((-3, 6), 0.8, layer=10, ends="round", tolerance=1e-5) rp.segment((1, 0), 0.1, relative=True) rp.segment((0, 1), 0.8, relative=True) cell.add(rp) rp = gdspy.RobustPath((-3, 6), 0.8, layer=11, ends="extended", tolerance=1e-5) rp.segment((1, 0), 0.1, relative=True) rp.segment((0, 1), 0.8, relative=True) cell.add(rp) rp = gdspy.RobustPath((-3, 6), 0.8, layer=12, ends="smooth", tolerance=1e-5) rp.segment((1, 0), 0.1, relative=True) rp.segment((0, 1), 0.8, relative=True) cell.add(rp) rp = gdspy.RobustPath((-3, 8), 0.1, layer=13, ends="round", tolerance=1e-5) rp.segment((1, 0), 0.8, relative=True) rp.segment((0, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((-3, 8), 0.1, layer=14, ends=(0.2, 0.2), tolerance=1e-5) rp.segment((1, 0), 0.8, relative=True) rp.segment((0, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((-3, 8), 0.1, layer=15, ends="smooth", tolerance=1e-5) rp.segment((1, 0), 0.8, relative=True) rp.segment((0, 1), 0.1, relative=True) cell.add(rp) rp = gdspy.RobustPath((5, 2), [0.05, 0.1, 0.2], [-0.2, 0, 0.4], layer=[4, 5, 6]) rp.parametric(lambda u: numpy.array((5.5 + 3 * u, 2 + 3 * u ** 2)), relative=False) rp.segment((0, 1), relative=True) rp.parametric( lambda u: numpy.array( (2 * numpy.cos(0.5 * numpy.pi * u) - 2, 3 * numpy.sin(0.5 * numpy.pi * u)) ), width=[0.2, 0.1, 0.05], offset=[-0.3, 0, 0.3], ) rp.parametric(lambda u: numpy.array((-2 * u, 0)), width=0.1, offset=0.2) rp.bezier([(-3, 0), (-2, -3), (0, -4), (0, -5)], offset=[-0.2, 0, 0.2]) rp.bezier( [(4.5, 0), (1, -1), (1, 5), (3, 2), (5, 2)], width=[0.05, 0.1, 0.2], offset=[-0.2, 0, 0.4], relative=False, ) cell.add(rp) rp = gdspy.RobustPath((2, -1), 0.1, layer=7, tolerance=1e-4, max_points=0) rp.smooth( [(1, 0), (1, -1), (0, -1)], angles=[numpy.pi / 3, None, -2 / 3.0 * numpy.pi, None], cycle=True, ) cell.add(rp) rp = gdspy.RobustPath((2.5, -1.5), 0.1, layer=8) rp.smooth( [(3, -1.5), (4, -2), (5, -1), (6, -2), (7, -1.5), (7.5, -1.5)], relative=False, width=0.2, ) cell.add(rp) cell = gdspy.Cell("RobustPath3") rp = gdspy.RobustPath((0, 0), 0.1) rp.parametric( lambda u: numpy.array((3 * numpy.sin(numpy.pi * u), -3 * numpy.cos(numpy.pi * u))), relative=False, ) rp.parametric( lambda u: numpy.array( (3.5 - 3 * numpy.cos(numpy.pi * u), -0.5 + 3 * numpy.sin(numpy.pi * u)) ), lambda u: numpy.array((numpy.sin(numpy.pi * u), numpy.cos(numpy.pi * u))), relative=True, ) cell.add(rp) ### Curve cell = gdspy.Cell("Hobby1") c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)]) cell.add(gdspy.Polygon(c.get_points(), layer=1)) c = gdspy.Curve(2, 0, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[numpy.pi / 3, None, None, None]) cell.add(gdspy.Polygon(c.get_points(), layer=3)) c = gdspy.Curve(4, 0, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, None, None, 2 / 3.0 * numpy.pi]) cell.add(gdspy.Polygon(c.get_points(), layer=5)) c = gdspy.Curve(0, 2, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[numpy.pi / 3, None, None, 3 / 4.0 * numpy.pi]) cell.add(gdspy.Polygon(c.get_points(), layer=7)) c = gdspy.Curve(2, 2, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, None, numpy.pi / 2, None]) cell.add(gdspy.Polygon(c.get_points(), layer=9)) c = gdspy.Curve(4, 2, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, 0, None, None]) cell.add(gdspy.Polygon(c.get_points(), layer=11)) c = gdspy.Curve(0, 4, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, 0, None, -numpy.pi / 2]) cell.add(gdspy.Polygon(c.get_points(), layer=13)) c = gdspy.Curve(2, 4, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, 0, -numpy.pi, -numpy.pi / 2]) cell.add(gdspy.Polygon(c.get_points(), layer=15)) c = gdspy.Curve(4, 4, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[-numpy.pi / 4, 0, numpy.pi / 2, -numpy.pi]) cell.add(gdspy.Polygon(c.get_points(), layer=17)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=2)) c = gdspy.Curve(2, 0, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[numpy.pi / 3, None, None, None], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=4)) c = gdspy.Curve(4, 0, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, None, None, 2 / 3.0 * numpy.pi], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=6)) c = gdspy.Curve(0, 2, tolerance=1e-3) c.i( [(1, 0), (1, 1), (0, 1)], angles=[numpy.pi / 3, None, None, 3 / 4.0 * numpy.pi], cycle=True, ) cell.add(gdspy.Polygon(c.get_points(), layer=8)) c = gdspy.Curve(2, 2, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, None, numpy.pi / 2, None], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=10)) c = gdspy.Curve(4, 2, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, 0, None, None], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=12)) c = gdspy.Curve(0, 4, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, 0, None, -numpy.pi / 2], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=14)) c = gdspy.Curve(2, 4, tolerance=1e-3) c.i([(1, 0), (1, 1), (0, 1)], angles=[None, 0, -numpy.pi, -numpy.pi / 2], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=16)) c = gdspy.Curve(4, 4, tolerance=1e-3) c.i( [(1, 0), (1, 1), (0, 1)], angles=[-numpy.pi / 4, 0, numpy.pi / 2, -numpy.pi], cycle=True, ) cell.add(gdspy.Polygon(c.get_points(), layer=18)) cell = gdspy.Cell("Hobby2") c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)]) cell.add(gdspy.Polygon(c.get_points(), layer=1)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], curl_start=0) cell.add(gdspy.Polygon(c.get_points(), layer=2)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], curl_end=0) cell.add(gdspy.Polygon(c.get_points(), layer=3)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], curl_start=0, curl_end=0) cell.add(gdspy.Polygon(c.get_points(), layer=4)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i( [(1, 2), (2, 1), (3, 2), (4, 0)], angles=[numpy.pi / 2, None, None, None, -numpy.pi / 2], curl_start=0, curl_end=0, ) cell.add(gdspy.Polygon(c.get_points(), layer=5)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i( [(1, 2), (2, 1), (3, 2), (4, 0)], angles=[None, 0, None, 0, None], curl_start=0, curl_end=1, ) cell.add(gdspy.Polygon(c.get_points(), layer=6)) cell = gdspy.Cell("Hobby3") c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)]) cell.add(gdspy.Polygon(c.get_points(), layer=1)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], t_in=2) cell.add(gdspy.Polygon(c.get_points(), layer=2)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], t_out=2) cell.add(gdspy.Polygon(c.get_points(), layer=3)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], t_in=2, t_out=2) cell.add(gdspy.Polygon(c.get_points(), layer=4)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], t_in=[2, 1, 1, 1, 1], t_out=[1, 1, 1, 1, 2]) cell.add(gdspy.Polygon(c.get_points(), layer=5)) c = gdspy.Curve(0, 0, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], t_in=[1, 1, 2, 1, 1], t_out=[1, 2, 1, 1, 1]) cell.add(gdspy.Polygon(c.get_points(), layer=6)) cell = gdspy.Cell("Hobby4") c = gdspy.Curve(0, 3, tolerance=1e-3) c.i([(1, 2), (2, 1), (3, 2), (4, 0)], cycle=True) cell.add(gdspy.Polygon(c.get_points(), layer=10)) c = gdspy.Curve(0, 3, tolerance=1e-3) c.i( [(1, 2), (2, 1), (3, 2), (4, 0)], t_in=[2, 1, 1, 1, 1], t_out=[1, 1, 1, 1, 2], cycle=True, ) cell.add(gdspy.Polygon(c.get_points(), layer=11)) c = gdspy.Curve(0, 3, tolerance=1e-3) c.i( [(1, 2), (2, 1), (3, 2), (4, 0)], t_in=[1, 1, 2, 1, 1], t_out=[1, 2, 1, 1, 1], cycle=True, ) cell.add(gdspy.Polygon(c.get_points(), layer=12)) c = gdspy.Curve(0, 3, tolerance=1e-3) c.i( [(1, 2), (2, 1), (3, 2), (4, 0)], angles=[numpy.pi * 3 / 4.0, None, None, None, -numpy.pi * 3 / 4.0], t_in=[2, 1, 1, 1, 1], t_out=[1, 1, 1, 1, 2], cycle=True, ) cell.add(gdspy.Polygon(c.get_points(), layer=13)) c = gdspy.Curve(0, 3, tolerance=1e-3) c.i( [(1, 2), (2, 1), (3, 2), (4, 0)], angles=[numpy.pi * 3 / 4.0, None, None, None, -numpy.pi * 3 / 4.0], t_in=[1, 1, 1, 1, 1], t_out=[1, 1, 1, 1, 1], cycle=True, ) cell.add(gdspy.Polygon(c.get_points(), layer=14)) ### END gdspy.write_gds("tests/test.gds", unit=1, precision=1e-7) gdspy.LayoutViewer(cells=[cell]) gdspy-1.4.2/tools/release.sh000077500000000000000000000012001354474061200157620ustar00rootroot00000000000000#!/bin/sh LAST_VER=$(git tag -l | tail -n 1) CURR_VER=$(python -c 'import gdspy; print(gdspy.__version__)') if [ "$LAST_VER" = "v$CURR_VER" ]; then echo "Version $CURR_VER (from package) already tagged. Did you forget to update __version__?" exit 1 fi if ! grep "### Version $CURR_VER" README.md > /dev/null 2>&1; then echo "Version $CURR_VER not found in the release notes of README.md" exit 1 fi echo "Release version $CURR_VER [y/n]?" read -r GOON if [ "$GOON" = 'y' ] ; then git commit -m "Release v$CURR_VER" git tag -am "Release v$CURR_VER" "v$CURR_VER" echo "Review the status and 'git push' to finish release" fi gdspy-1.4.2/tools/test.sh000077500000000000000000000010161354474061200153260ustar00rootroot00000000000000#!/bin/sh VERSION=$1 if [ "$(command -v "virtualenv$VERSION")" ]; then ENV=~/gdspy-env-$VERSION [ ! -d "$ENV" ] && "virtualenv$VERSION" --system-site-packages "$ENV" [ -d "$ENV/lib/python$VERSION/site-packages/gdspy" ] && rm -r "$ENV/lib/python$VERSION/site-packages/gdspy" . "$ENV/bin/activate" python setup.py build && python setup.py install echo echo "Testing $VERSION" cd docs/_static || exit for i in *.py; do python "$i" done cd - || exit deactivate else echo "Usage: $0 version" fi