├── MANIFEST.in ├── setup.cfg ├── setup.py ├── pointwise ├── __init__.py ├── glyphapi │ ├── __init__.py │ ├── glyphobj.py │ └── utilities.py └── glyph_client.py ├── .gitignore ├── examples ├── HelloWorld.py ├── ShowDomInfo.py ├── ShowConSegmentInfo.py ├── ShowBlockDomOrients.py ├── VersionCompatibility.py ├── DomainToEllipse.py └── BackstepTutorial.py ├── LICENSE └── README.rst /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # The code is written to work on both Python 2 and Python 3. 3 | universal=1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | def readme(): 6 | with open('README.rst') as f: 7 | return f.read() 8 | 9 | setup(name='pointwise-glyph-client', 10 | version='2.0.13', 11 | description='Glyph client in Python with Python-like API to Pointwise Glyph Server', 12 | url='http://github.com/pointwise/GlyphClientPython', 13 | install_requires=['numpy'], 14 | packages=['pointwise', 'pointwise.glyphapi'], 15 | classifiers=[ 16 | 'Programming Language :: Python :: 3.6', 17 | 'Programming Language :: Python :: 3.7', 18 | 'Programming Language :: Python :: 3.8', 19 | 'Programming Language :: Python :: 3.9', 20 | 'Programming Language :: Python :: 3.10', 21 | 'Programming Language :: Python :: 3.11', 22 | 'Programming Language :: Python :: 3.12', 23 | 'Programming Language :: Python :: 2.7'], 24 | author='Cadence Design Systems, Inc.', 25 | author_email='pw-pypi@cadence.com') 26 | -------------------------------------------------------------------------------- /pointwise/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | ############################################################################# 4 | # 5 | # (C) 2021 Cadence Design Systems, Inc. All rights reserved worldwide. 6 | # 7 | # This sample script is not supported by Cadence Design Systems, Inc. 8 | # It is provided freely for demonstration purposes only. 9 | # SEE THE WARRANTY DISCLAIMER AT THE BOTTOM OF THIS FILE. 10 | # 11 | ############################################################################# 12 | 13 | from .glyph_client import GlyphClient, GlyphError 14 | 15 | ############################################################################# 16 | # 17 | # This file is licensed under the Cadence Public License Version 1.0 (the 18 | # "License"), a copy of which is found in the included file named "LICENSE", 19 | # and is distributed "AS IS." TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE 20 | # LAW, CADENCE DISCLAIMS ALL WARRANTIES AND IN NO EVENT SHALL BE LIABLE TO 21 | # ANY PARTY FOR ANY DAMAGES ARISING OUT OF OR RELATING TO USE OF THIS FILE. 22 | # Please see the License for the full text of applicable terms. 23 | # 24 | ############################################################################# 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | test/* 92 | internal/* 93 | examples/BuffonNeedleExperiment.py 94 | README-pip 95 | .gitignore 96 | setup.cfg 97 | setup.py 98 | -------------------------------------------------------------------------------- /examples/HelloWorld.py: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # 3 | # (C) 2021 Cadence Design Systems, Inc. All rights reserved worldwide. 4 | # 5 | # This sample script is not supported by Cadence Design Systems, Inc. 6 | # It is provided freely for demonstration purposes only. 7 | # SEE THE WARRANTY DISCLAIMER AT THE BOTTOM OF THIS FILE. 8 | # 9 | ############################################################################# 10 | 11 | """ This is an example script that creates a Glyph Server process, 12 | connects to it, and prints a Glyph message that is captured 13 | and printed to the console window. 14 | """ 15 | 16 | from pointwise import GlyphClient 17 | from pointwise.glyphapi import * 18 | 19 | # Callback function for output from Glyph Server 20 | def echo(line): 21 | print("Script: {0}".format(line), end='') # Not Python 2.7 compatible 22 | 23 | # Port 0 indicates a non-interactive server process should be created 24 | # in the background (consumes a Pointwise license). 25 | with GlyphClient(port=0, callback=echo) as glf: 26 | 27 | # Use the Glyph API for Python 28 | pw = glf.get_glyphapi() 29 | 30 | glf.puts("Hello from '%s'" % pw.Application.getVersion()) 31 | 32 | ############################################################################# 33 | # 34 | # This file is licensed under the Cadence Public License Version 1.0 (the 35 | # "License"), a copy of which is found in the included file named "LICENSE", 36 | # and is distributed "AS IS." TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE 37 | # LAW, CADENCE DISCLAIMS ALL WARRANTIES AND IN NO EVENT SHALL BE LIABLE TO 38 | # ANY PARTY FOR ANY DAMAGES ARISING OUT OF OR RELATING TO USE OF THIS FILE. 39 | # Please see the License for the full text of applicable terms. 40 | # 41 | ############################################################################# 42 | -------------------------------------------------------------------------------- /pointwise/glyphapi/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | ############################################################################# 4 | # 5 | # (C) 2021 Cadence Design Systems, Inc. All rights reserved worldwide. 6 | # 7 | # This sample script is not supported by Cadence Design Systems, Inc. 8 | # It is provided freely for demonstration purposes only. 9 | # SEE THE WARRANTY DISCLAIMER AT THE BOTTOM OF THIS FILE. 10 | # 11 | ############################################################################# 12 | 13 | """ 14 | This module provides a Python-like interface to Glyph. 15 | 16 | GlyphAPI provides the service of automatically converting Python commands 17 | that look like static method calls into Glyph static actions. 18 | """ 19 | from pointwise import GlyphClient 20 | from pointwise import GlyphError 21 | 22 | from .glyphobj import GlyphObj, GlyphVar 23 | from .utilities import * 24 | 25 | class GlyphAPI(object): 26 | """ This class provides access to Glyph static actions. """ 27 | 28 | def __init__(self, glyph_client): 29 | """ Initialize a GlyphAPI object from a connected GlyphClient. 30 | """ 31 | self.glf = glyph_client 32 | 33 | # Acquire the list of all valid Glyph class names to seed the 34 | # name list 35 | if self.glf is not None and self.glf.is_connected(): 36 | self._names = self.glf.eval( 37 | 'pw::Application getAllCommandNames') 38 | else: 39 | self._names = [] 40 | raise GlyphError('', 'Not connected') 41 | 42 | def __getattr__(self, name): 43 | """ Create a GlyphObj object as needed for a Glyph class to invoke 44 | its static actions. 45 | """ 46 | glfFunction = 'pw::' + name 47 | glfObj = None 48 | 49 | if glfFunction in self._names: 50 | glfObj = GlyphObj(glfFunction, self.glf) 51 | setattr(self, name, glfObj) 52 | 53 | return glfObj 54 | 55 | ############################################################################# 56 | # 57 | # This file is licensed under the Cadence Public License Version 1.0 (the 58 | # "License"), a copy of which is found in the included file named "LICENSE", 59 | # and is distributed "AS IS." TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE 60 | # LAW, CADENCE DISCLAIMS ALL WARRANTIES AND IN NO EVENT SHALL BE LIABLE TO 61 | # ANY PARTY FOR ANY DAMAGES ARISING OUT OF OR RELATING TO USE OF THIS FILE. 62 | # Please see the License for the full text of applicable terms. 63 | # 64 | ############################################################################# 65 | -------------------------------------------------------------------------------- /examples/ShowDomInfo.py: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # 3 | # (C) 2021 Cadence Design Systems, Inc. All rights reserved worldwide. 4 | # 5 | # This sample script is not supported by Cadence Design Systems, Inc. 6 | # It is provided freely for demonstration purposes only. 7 | # SEE THE WARRANTY DISCLAIMER AT THE BOTTOM OF THIS FILE. 8 | # 9 | ############################################################################# 10 | 11 | """ This is an example script that prints information about domains. 12 | """ 13 | 14 | from pointwise import GlyphClient 15 | from pointwise.glyphapi import * 16 | 17 | # Connect to Glyph Server (Pointwise) on the default port 18 | with GlyphClient() as glf: 19 | pw = glf.get_glyphapi() 20 | 21 | # Since selection is required, this script can only be run in interactive mode 22 | if not pw.Application.isInteractive(): 23 | raise Exception("This script can only be run in interactive mode") 24 | 25 | # Create a Tcl variable to capture the selection results 26 | selection = GlyphVar() 27 | # Select domains only, of any type 28 | sm = pw.Display.createSelectionMask(requireDomain=[]) 29 | # Grab the current selection 30 | pw.Display.getSelectedEntities(selection, selectionmask=sm) 31 | 32 | doms = selection["Domains"] 33 | 34 | # If no domains were selected already, ask the user to select some 35 | if len(doms) == 0 and pw.Display.selectEntities(selection, selectionmask=sm, \ 36 | description="Select domains to display segment information"): 37 | doms = selection["Domains"] 38 | 39 | if (len(doms) > 0): 40 | # For each selected domain... 41 | for dom in doms: 42 | glf.puts("Domain %s" % dom.getName()) 43 | # For each edge in the domain... 44 | for e in range(1, dom.getEdgeCount()+1): 45 | glf.puts(" Edge %d" % e) 46 | # For each connector in the edge... 47 | edge = dom.getEdge(e) 48 | for k in range(1, edge.getConnectorCount()+1): 49 | con = edge.getConnector(k) 50 | glf.puts(" Connector %d: %s %s" % (k, con.getName(), edge.getConnectorDirection(k))) 51 | 52 | ############################################################################# 53 | # 54 | # This file is licensed under the Cadence Public License Version 1.0 (the 55 | # "License"), a copy of which is found in the included file named "LICENSE", 56 | # and is distributed "AS IS." TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE 57 | # LAW, CADENCE DISCLAIMS ALL WARRANTIES AND IN NO EVENT SHALL BE LIABLE TO 58 | # ANY PARTY FOR ANY DAMAGES ARISING OUT OF OR RELATING TO USE OF THIS FILE. 59 | # Please see the License for the full text of applicable terms. 60 | # 61 | ############################################################################# 62 | -------------------------------------------------------------------------------- /examples/ShowConSegmentInfo.py: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # 3 | # (C) 2021 Cadence Design Systems, Inc. All rights reserved worldwide. 4 | # 5 | # This sample script is not supported by Cadence Design Systems, Inc. 6 | # It is provided freely for demonstration purposes only. 7 | # SEE THE WARRANTY DISCLAIMER AT THE BOTTOM OF THIS FILE. 8 | # 9 | ############################################################################# 10 | 11 | """ This is an example script that prints information about each 12 | curve segment of a connector. 13 | """ 14 | 15 | from pointwise import GlyphClient 16 | from pointwise.glyphapi import * 17 | 18 | # Connect to Glyph Server (Pointwise) on the default port 19 | with GlyphClient() as glf: 20 | pw = glf.get_glyphapi() 21 | 22 | # Since selection is required, this script can only be run in interactive mode 23 | if not pw.Application.isInteractive(): 24 | raise Exception("This script can only be run in interactive mode") 25 | 26 | # Create a Tcl variable to capture the selection results 27 | selection = GlyphVar() 28 | # Select connectors only, of any type 29 | sm = pw.Display.createSelectionMask(requireConnector=[]) 30 | # Grab the current selection 31 | pw.Display.getSelectedEntities(selection, selectionmask=sm) 32 | 33 | cons = selection["Connectors"] 34 | 35 | # If no connectors were selected already, ask the user to select some 36 | if len(cons) == 0 and pw.Display.selectEntities(selection, selectionmask=sm, \ 37 | description="Select connectors to display segment information"): 38 | cons = selection["Connectors"] 39 | 40 | if (len(cons) > 0): 41 | # For each selected connector... 42 | for con in cons: 43 | # Print the name of the connector to the message window 44 | glf.puts("Connector %s" % con.getName()) 45 | # For each segment of that connector... 46 | for n in range(1, con.getSegmentCount()+1): 47 | glf.puts(" Segment %d" % n) 48 | segment = con.getSegment(n) 49 | for k in range(1, segment.getPointCount()+1): 50 | xyz = Vector3(segment.getXYZ(control=k)) 51 | glf.puts(" Point %3d: %15.8f %15.8f %15.8f" % (k, xyz.x, xyz.y, xyz.z)) 52 | 53 | ############################################################################# 54 | # 55 | # This file is licensed under the Cadence Public License Version 1.0 (the 56 | # "License"), a copy of which is found in the included file named "LICENSE", 57 | # and is distributed "AS IS." TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE 58 | # LAW, CADENCE DISCLAIMS ALL WARRANTIES AND IN NO EVENT SHALL BE LIABLE TO 59 | # ANY PARTY FOR ANY DAMAGES ARISING OUT OF OR RELATING TO USE OF THIS FILE. 60 | # Please see the License for the full text of applicable terms. 61 | # 62 | ############################################################################# 63 | -------------------------------------------------------------------------------- /examples/ShowBlockDomOrients.py: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # 3 | # (C) 2021 Cadence Design Systems, Inc. All rights reserved worldwide. 4 | # 5 | # This sample script is not supported by Cadence Design Systems, Inc. 6 | # It is provided freely for demonstration purposes only. 7 | # SEE THE WARRANTY DISCLAIMER AT THE BOTTOM OF THIS FILE. 8 | # 9 | ############################################################################# 10 | 11 | """ This is an example script that prints the orientation of all 12 | the faces in a block, and the relative orientation of all the 13 | domains in each face. 14 | """ 15 | 16 | from pointwise import GlyphClient 17 | from pointwise.glyphapi import * 18 | 19 | # Connect to Glyph Server (Pointwise) on the default port 20 | with GlyphClient() as glf: 21 | pw = glf.get_glyphapi() 22 | 23 | # Since selection is required, this script can only be run in interactive mode 24 | if not pw.Application.isInteractive(): 25 | raise Exception("This script can only be run in interactive mode") 26 | 27 | # Create a Tcl variable to capture the selection results 28 | selection = GlyphVar() 29 | # Select blocks only, of any type 30 | sm = pw.Display.createSelectionMask(requireBlock=[]) 31 | # Grab the current selection 32 | pw.Display.getSelectedEntities(selection, selectionmask=sm) 33 | 34 | blocks = selection["Blocks"] 35 | 36 | # If no blocks were selected already, ask the user to select some 37 | if len(blocks) == 0 and pw.Display.selectEntities(selection, selectionmask=sm, \ 38 | description="Select blocks to display face/domain orientations"): 39 | blocks = selection["Blocks"] 40 | 41 | if (len(blocks) > 0): 42 | # For each selected block... 43 | for block in blocks: 44 | # Print the name of the block to the message window 45 | glf.puts("Block %s" % block.getName()) 46 | # For each face of that block... 47 | for n in range(1, block.getFaceCount()+1): 48 | face = block.getFace(n) 49 | if face.isOfType("pw::FaceUnstructured"): 50 | # Print the face orientation (In or Out) 51 | glf.puts(" Face %d %s" % (n, face.getNormalOrientation())) 52 | else: 53 | glf.puts(" Face %d" % n) 54 | # for each domain in the face... 55 | for k in range(1, face.getDomainCount()+1): 56 | dom = face.getDomain(k) 57 | # Print the domain orientation (Same or Opposite), where "Same" means the domain 58 | # is oriented the same direction as the face) 59 | glf.puts(" Domain %s: %s" % (dom.getName(), face.getDomainOrientation(k))) 60 | 61 | ############################################################################# 62 | # 63 | # This file is licensed under the Cadence Public License Version 1.0 (the 64 | # "License"), a copy of which is found in the included file named "LICENSE", 65 | # and is distributed "AS IS." TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE 66 | # LAW, CADENCE DISCLAIMS ALL WARRANTIES AND IN NO EVENT SHALL BE LIABLE TO 67 | # ANY PARTY FOR ANY DAMAGES ARISING OUT OF OR RELATING TO USE OF THIS FILE. 68 | # Please see the License for the full text of applicable terms. 69 | # 70 | ############################################################################# 71 | -------------------------------------------------------------------------------- /examples/VersionCompatibility.py: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # 3 | # (C) 2021 Cadence Design Systems, Inc. All rights reserved worldwide. 4 | # 5 | # This sample script is not supported by Cadence Design Systems, Inc. 6 | # It is provided freely for demonstration purposes only. 7 | # SEE THE WARRANTY DISCLAIMER AT THE BOTTOM OF THIS FILE. 8 | # 9 | ############################################################################# 10 | 11 | """ This is an example script that creates a Glyph Server process, 12 | connects to it with version compatability set, and prints some 13 | version-specific information to the console window. 14 | """ 15 | 16 | # About Glyph Version Compatability: 17 | # 18 | # When running Pointwise versions prior to V18.3, the Glyph version 19 | # compatability level will always be the implemented Glyph version. When 20 | # running V18.3 or later, a compatability version may be requested. 21 | # Compatability levels are used to modify certain default behaviors, e.g., 22 | # meshing parameters, and correspond directly to a Glyph package version. 23 | 24 | from pointwise import GlyphClient 25 | from pointwise.glyphapi import * 26 | 27 | DomDefaults = { 28 | "TRexGrowthRate", 29 | "TRexSpacingSmoothing" } 30 | 31 | BlkDefaults = { 32 | "TRexGrowthRate", 33 | "TRexCollisionBuffer", 34 | "TRexSkewCriteriaMaximumAngle" } 35 | 36 | def printDefaults(pw, v): 37 | print("Pointwise Version : %s" % pw.Application.getVersion()) 38 | print("Glyph Compatability Version : %s" % pw.glf._version) 39 | print("DomainUnstructured:") 40 | for d in DomDefaults: 41 | print(" %-30.30s: %s" % (d, pw.DomainUnstructured.getDefault(d))) 42 | print("BlockUnstructured:") 43 | for b in BlkDefaults: 44 | print(" %-30.30s: %s" % (b, pw.BlockUnstructured.getDefault(b))) 45 | print("-----") 46 | 47 | 48 | # Port 0 indicates a non-interactive server process should be created in the 49 | # background (consumes a Pointwise license). 50 | 51 | # 'tclsh' must be in your PATH and, for non-Windows platforms, all other 52 | # environment variables needed to load the Glyph package must be set 53 | # accordingly. 54 | 55 | # No version compatability 56 | with GlyphClient(port=0) as glf: 57 | 58 | pw = glf.get_glyphapi() 59 | printDefaults(pw, "None") 60 | 61 | # V18.0 compatability 62 | with GlyphClient(port=0, version="2.18.0") as glf: 63 | 64 | pw = glf.get_glyphapi() 65 | printDefaults(pw, "2.18.0") 66 | 67 | # V18.2 compatability 68 | with GlyphClient(port=0, version="2.18.2") as glf: 69 | 70 | pw = glf.get_glyphapi() 71 | printDefaults(pw, "2.18.2") 72 | 73 | # V18.3 compatability 74 | with GlyphClient(port=0, version="3.18.3") as glf: 75 | 76 | pw = glf.get_glyphapi() 77 | printDefaults(pw, "3.18.3") 78 | 79 | ############################################################################# 80 | # 81 | # This file is licensed under the Cadence Public License Version 1.0 (the 82 | # "License"), a copy of which is found in the included file named "LICENSE", 83 | # and is distributed "AS IS." TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE 84 | # LAW, CADENCE DISCLAIMS ALL WARRANTIES AND IN NO EVENT SHALL BE LIABLE TO 85 | # ANY PARTY FOR ANY DAMAGES ARISING OUT OF OR RELATING TO USE OF THIS FILE. 86 | # Please see the License for the full text of applicable terms. 87 | # 88 | ############################################################################# 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Cadence Public License Version 1.0 2 | 3 | May 2021 4 | 5 | BY USING, REPRODUCING, DISTRIBUTING, AND/OR OTHERWISE ACCESSING THIS 6 | TECHNOLOGY, YOU AGREE TO THE TERMS OF THIS CADENCE PUBLIC LICENSE 7 | VERSION 1.0 (THE "AGREEMENT"). 8 | 9 | 1. License Grant. Subject to the terms and conditions specified herein, You 10 | are hereby granted a perpetual, non-exclusive, no-charge license to Use the 11 | Technology for any purpose, which includes without limitation, the right to 12 | access, copy, modify, merge, publish, redistribute, sublicense, and/or sell 13 | (collectively, to "Use") copies of the Technology. 14 | 15 | 2. Distributions. 16 | 17 | i. Copyright Notice and Agreement Text. The Copyright Notice and the full 18 | text of this Cadence Public License Version 1.0 shall be included in all 19 | copies of the Technology, or substantial portions thereof. (For clarity, if 20 | You are redistributing the Technology in source code form, then you 21 | must ensure that the Copyright Notice and Agreement are left intact; if 22 | You are redistributing the Technology in binary or non-source code 23 | form, then you must ensure that the Copyright Notice and Agreement are 24 | reproduced and included in an attribution file, such as a "NOTICES", 25 | "COPYRIGHT", or other equivalent file.) 26 | 27 | ii. Modifications. Any distribution of modified Technology must include a 28 | prominent notice in the corresponding file(s) indicating that the file(s) 29 | was modified by You. You may license Your modifications under different 30 | or additional license terms. 31 | 32 | 3. Endorsement. Neither the name of the copyright holder nor the names of its 33 | contributors may be used to endorse or promote products derived from the 34 | Technology without specific prior written permission. 35 | 36 | 4. Trademarks. Except for attribution purposes (as described in Sec. 2(i), 37 | above) and as may otherwise be agreed in writing, You do not have 38 | permission to use the trade names, trademarks, service marks, or product 39 | names of the copyright holder or contributor(s). 40 | 41 | 5. Termination. 42 | 43 | i. Patent Litigation. If You institute patent litigation against any entity 44 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 45 | Technology constitutes direct or contributory patent infringement, then any 46 | patent licenses granted to You under this Agreement shall terminate as of 47 | the date such litigation is filed. 48 | 49 | ii. Breach. This Agreement, and the license granted hereunder, shall 50 | immediately terminate if You (a) fail to comply with any material terms or 51 | conditions and (b) fail to cure your lack of compliance within a reasonable 52 | time of becoming aware. 53 | 54 | 6. Disclaimer or Warranty. UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 55 | WRITING, THE TECHNOLOGY IS PROVIDED BY THE COPYRIGHT HOLDERS AND 56 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 57 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 58 | PARTICULAR PURPOSE ARE DISCLAIMED. 59 | 60 | 7. Limitation of Liability. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 61 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 62 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 63 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 64 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 65 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 66 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 67 | TECHNOLOGY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 68 | 69 | 8. Stewardship. While everyone is permitted to copy and distribute copies of 70 | this Agreement, it may only be modified by the Steward. The Steward may 71 | publish new versions (including revisions) of the Agreement. For clarity, 72 | the Technology may always be distributed subject to the version of the 73 | Agreement under which it was received. In addition, after a new version of 74 | the Agreement is published, Licensor may elect to distribute the Technology 75 | under the new version. 76 | 77 | 9. Definitions. 78 | 79 | i. "Copyright Notice" means a notice that includes the name of the 80 | copyright holder, a copyright symbol (or equivalent), a date, and, at the 81 | owner's election, a statement of rights. 82 | 83 | ii. "Licensee" means any person or entity that receives the Technology 84 | under this Agreement. 85 | 86 | iii. "Licensor" means any person or entity that distributes the Technology. 87 | 88 | iv. "Steward" means Cadence Design Systems, Inc., which may assign the 89 | responsibility to serve as Steward to a suitable separate entity. 90 | 91 | v. "Technology" means the work of authorship, whether in source code, 92 | binary, or other form, made available under this Agreement, as indicated by 93 | a Copyright Notice that is included in or attached to the Technology. 94 | 95 | vi. "You" (or "Your") means a person or entity exercising permissions 96 | granted by this Agreement. 97 | -------------------------------------------------------------------------------- /examples/DomainToEllipse.py: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # 3 | # (C) 2021 Cadence Design Systems, Inc. All rights reserved worldwide. 4 | # 5 | # This sample script is not supported by Cadence Design Systems, Inc. 6 | # It is provided freely for demonstration purposes only. 7 | # SEE THE WARRANTY DISCLAIMER AT THE BOTTOM OF THIS FILE. 8 | # 9 | ############################################################################# 10 | 11 | from pointwise import GlyphClient 12 | from pointwise.glyphapi import * 13 | import copy 14 | 15 | ########################################################## 16 | # Main Program 17 | 18 | with GlyphClient() as glf: 19 | pw = glf.get_glyphapi() 20 | 21 | # Any value smaller than this is considered to be zero. 22 | zeroTol = 0.000001 23 | 24 | # How many times to iterate on the perimeter's shape. 25 | numIterations = 10000 26 | 27 | # How many perimeters to keep in the history. 28 | maxHistory = 88 29 | History = [] 30 | 31 | # Note: Colors are designed for a black background so that 32 | # the last/oldest curve is invisible. 33 | # Default DB color is af6df0 = 170, 109, 240 34 | colorFirst = [255, 255, 255] 35 | colorLast = [0, 0, 0] 36 | 37 | # Compute the RGB range between the two colors. 38 | colorRange = [] 39 | for i in range(0,3): 40 | colorRange.append(colorLast[i] - colorFirst[i]) 41 | 42 | # Create a list containing a color for each entitiy in the history. 43 | # Each color is an RGB triplet blended from the first and last colors. 44 | # RGB is scaled between [0-1] not [0-255]. 45 | colorHistory = [] 46 | for c in range(0, maxHistory): 47 | f = c / (maxHistory - 1.0) 48 | rgb = [] 49 | for i in range(0,3): 50 | c1 = colorFirst[i] 51 | cr = colorRange[i] 52 | rgb.append((c1 + f + cr) / 255.0) 53 | colorHistory.append(rgb) 54 | 55 | # Ask user to select a single domain. 56 | selMask = pw.Display.createSelectionMask(requireDomain="") 57 | selResults = GlyphVar() 58 | 59 | if not pw.Display.selectEntities(selResults, \ 60 | description="Select a domain.", selectionmask=selMask, single=True): 61 | # Nothing was selected 62 | exit() 63 | else: 64 | # Get the selected domain from the selection results. 65 | glf.puts(str(selResults.value)) 66 | domList = selResults["Domains"] 67 | domSelected = domList[0] 68 | 69 | # Initialize a bounding box. 70 | bbox = Extents() 71 | 72 | # Create a single DB line of the domain's perimeter grid points. 73 | # While doing this, compute the domain's bounding box for use in scaling. 74 | seg = pw.SegmentSpline() 75 | # For each connector on each edge of the domain 76 | # copy its grid points into the DB perimeter line 77 | numEdges = domSelected.getEdgeCount() 78 | for e in range(1, numEdges + 1): 79 | edge = domSelected.getEdge(e) 80 | numCons = edge.getConnectorCount() 81 | for c in range(1, numCons + 1): 82 | con = edge.getConnector(c) 83 | numPoints = con.getDimension() 84 | istart = 1 85 | if c > 1: 86 | # Don't duplicate the point shared by two cons on the same edge 87 | istart = 2 88 | elif (e > 1 and c == 1): 89 | # Don't duplicate the point at the node shared by edges. 90 | istart = 2 91 | for i in range(istart, numPoints + 1): 92 | P = con.getXYZ(grid=i) 93 | seg.addPoint(P) 94 | #update bounding box 95 | bbox = bbox.enclose(P) 96 | 97 | Perimeter = pw.Curve() 98 | Perimeter.addSegment(seg) 99 | History.append(Perimeter) 100 | 101 | # Save the original perimeter's bounding box for scaling calculations. 102 | bbSizeOrig = bbox.maximum() - bbox.minimum() 103 | 104 | # Iterate on the following: 105 | # Go around the perimeter and create a new perimeter 106 | # from the mid points of the line segments on the old perimeter. 107 | for i in range(1, numIterations + 1): 108 | #print(i) 109 | bbox = Extents() 110 | segNew = pw.SegmentSpline() 111 | segOld = Perimeter.getSegment(1) 112 | numPoints = segOld.getPointCount() 113 | # Create a point on the new segment at 114 | # the midpoint of two points on the old segment. 115 | for n in range(2, numPoints + 1): 116 | A = segOld.getPoint(n) 117 | B = segOld.getPoint(n - 1) 118 | Cx = (A[0] + B[0])/2 119 | Cy = (A[1] + B[1])/2 120 | Cz = (A[2] + B[2])/2 121 | P = [Cx, Cy, Cz] 122 | if n == 2: 123 | # Save this first point to re-use as the last point 124 | P1 = copy.deepcopy(P) 125 | segNew.addPoint(P) 126 | # Update the bounding box. 127 | bbox = bbox.enclose(P) 128 | # add the first point as the last point to close the perimeter 129 | segNew.addPoint(P1) 130 | newPerimeter = pw.Curve() 131 | newPerimeter.addSegment(segNew) 132 | # Compare the total length of the new and old perimeters 133 | # to see if we can quit if they're not changing too much. 134 | newLength = newPerimeter.getTotalLength() 135 | oldLength = Perimeter.getTotalLength() 136 | chgLength = abs(newLength - oldLength) / oldLength 137 | 138 | # Add the new perimeter to the end of the history. 139 | History.append(newPerimeter) 140 | if len(History) > maxHistory: 141 | # If the history is too long, delete the oldest and remove it. 142 | pw.Entity.delete(History[0]) 143 | del History[0] 144 | # Update the current perimeter curve 145 | Perimeter = newPerimeter 146 | 147 | # set scale factors based on relative sizes of 148 | # original and new perimeters 149 | bbSizeNew = bbox.maximum() - bbox.minimum() 150 | 151 | #print(pw.Vector3.X(bbSizeOrig)) 152 | scaleFactorX = bbSizeOrig[0] / bbSizeNew[0] 153 | scaleFactorY = bbSizeOrig[1] / bbSizeNew[1] 154 | if abs(bbSizeNew[2]) < zeroTol: 155 | scaleFactorZ = 1.0 156 | else: 157 | scaleFactorZ = bbSizeOrig[2] / bbSizeNew[2] 158 | scaleFactor = [scaleFactorX, scaleFactorY, scaleFactorZ] 159 | # Scale around the center of the new perimeter's bounding box 160 | scaleAnchor = bbox.minimum() + bbSizeNew/2.0 161 | # The new perimeter has to be scaled up for visual reasons 162 | # otherwise it shrinks to nothing. 163 | xform = Transform.scaling(scaleFactor, scaleAnchor) 164 | pw.Entity.transform(xform, Perimeter) 165 | 166 | # Give each perimeter in the history a unique color. 167 | npoints = 0 168 | for p in History: 169 | p.setRenderAttribute("ColorMode", "Entity") 170 | p.setColor(colorHistory[npoints]) 171 | npoints += 1 172 | # Update the display. 173 | pw.Display.update() 174 | 175 | # If all iterations complete, delete all but the last curve 176 | del History[-1] 177 | pw.Entity.delete(History) 178 | 179 | ############################################################################# 180 | # 181 | # This file is licensed under the Cadence Public License Version 1.0 (the 182 | # "License"), a copy of which is found in the included file named "LICENSE", 183 | # and is distributed "AS IS." TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE 184 | # LAW, CADENCE DISCLAIMS ALL WARRANTIES AND IN NO EVENT SHALL BE LIABLE TO 185 | # ANY PARTY FOR ANY DAMAGES ARISING OUT OF OR RELATING TO USE OF THIS FILE. 186 | # Please see the License for the full text of applicable terms. 187 | # 188 | ############################################################################# 189 | -------------------------------------------------------------------------------- /examples/BackstepTutorial.py: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # 3 | # (C) 2021 Cadence Design Systems, Inc. All rights reserved worldwide. 4 | # 5 | # This sample script is not supported by Cadence Design Systems, Inc. 6 | # It is provided freely for demonstration purposes only. 7 | # SEE THE WARRANTY DISCLAIMER AT THE BOTTOM OF THIS FILE. 8 | # 9 | ############################################################################# 10 | 11 | """ This is an example script that generates the structured block topology 12 | of the Backward Step from the Pointwise Tutorial Workbook. This 13 | example uses the Glyph API for Python. 14 | 15 | For demonstration purposes, many of the Python commands are preceded 16 | by a comment that shows the equivalent Tcl command. This script can 17 | be run in either Python 2.7+ or Python 3.6+. 18 | 19 | To run this example in Python 3.6+: 20 | - Start a Pointwise GUI instance 21 | - Go to Script, Glyph Server... 22 | - Set Listen Mode to Active 23 | - OK 24 | - Run 'python backstep.py' at a command prompt on the same host 25 | as the Pointwise instance 26 | """ 27 | 28 | from pointwise import GlyphClient 29 | from pointwise.glyphapi import * 30 | 31 | # Connect to the Pointwise server listening on localhost at the default port 32 | # with no authentication token... 33 | 34 | # glf = GlyphClient() 35 | 36 | # ... or create a pointwise server as a subprocess and connect to that. 37 | # Note: this will consume a Pointwise license 38 | def echo(line): 39 | print("Script: {0}".format(line), end='') # Not Python 2.7 compatible 40 | 41 | # Run in batch mode 42 | glf = GlyphClient(port=0, callback=echo) 43 | 44 | # Run in GUI, default port 45 | # glf = GlyphClient(callback=echo) 46 | 47 | glf.connect() 48 | 49 | # Use the Glyph API for Python 50 | pw = glf.get_glyphapi() 51 | 52 | # Allow error messages to be printed on the server 53 | pw.Database._setVerbosity("Errors") 54 | pw.Application._setVerbosity("Errors") 55 | 56 | # Reset the server's workspace 57 | pw.Application.setUndoMaximumLevels(10) 58 | pw.Application.reset() 59 | pw.Application.markUndoLevel("Journal Reset") 60 | 61 | # pw::Connector setDefault Dimension 30 62 | pw.Connector.setDefault("Dimension", 30) 63 | 64 | # set creator [pw::Application begin Create] 65 | with pw.Application.begin("Create") as creator: 66 | # set seg [pw::SegmentSpline create] 67 | seg = pw.SegmentSpline() 68 | 69 | # $seg addPoint {0 0 0} 70 | seg.addPoint((0, 0, 0)) 71 | seg.addPoint((20, 0, 0)) 72 | 73 | # set con [pw::Connector create] 74 | con = pw.Connector() 75 | 76 | # $con addSegment $seg 77 | con.addSegment(seg) 78 | 79 | # $con calculateDimension 80 | con.calculateDimension() 81 | 82 | # "$creator end" is implied 83 | 84 | pw.Application.markUndoLevel("Create 2 Point Connector") 85 | 86 | with pw.Application.begin("Create") as creator: 87 | seg = pw.SegmentSpline() 88 | seg.addPoint((20, 0, 0)) 89 | seg.addPoint((60, 0, 0)) 90 | con = pw.Connector() 91 | con.addSegment(seg) 92 | con.calculateDimension() 93 | 94 | pw.Application.markUndoLevel("Create 2 Point Connector") 95 | 96 | # pw::Display resetView -Z 97 | pw.Display.resetView("-Z") 98 | 99 | cons = { } 100 | # set cons(con-1) [pw::GridEntity getByName "con-1"] 101 | cons["con-1"] = pw.GridEntity.getByName("con-1") 102 | cons["con-2"] = pw.GridEntity.getByName("con-2") 103 | 104 | # set coll [pw::Collection create] 105 | coll = pw.Collection() 106 | 107 | # foreach { k v } [array get cons] { lappend clist $v } 108 | # $coll set $clist 109 | coll.set(cons.values()) 110 | 111 | # $coll do setRenderAttribute RenderMode Intervals 112 | coll.do("setRenderAttribute", "RenderMode", "Intervals") 113 | 114 | # $coll delete 115 | coll.delete() 116 | 117 | pw.Application.markUndoLevel("Modify Entity Display") 118 | 119 | # set modifier [pw::Application begin Modify] 120 | with pw.Application.begin("Modify", cons["con-1"]) as modifier: 121 | # [$cons(con-1) getDistribution 1] setBeginSpacing 1 122 | cons["con-1"].getDistribution(1).setBeginSpacing(1) 123 | 124 | # "$modifier end" is implied 125 | 126 | pw.Application.markUndoLevel("Change Spacing") 127 | 128 | with pw.Application.begin("Modify", cons.values()) as modifier: 129 | cons["con-1"].getDistribution(1).setEndSpacing(0.1) 130 | cons["con-2"].getDistribution(1).setEndSpacing(0.1) 131 | 132 | pw.Application.markUndoLevel("Change Spacings") 133 | 134 | with pw.Application.begin("Modify", cons["con-2"]) as modifier: 135 | cons["con-2"].getDistribution(1).setEndSpacing(2) 136 | 137 | pw.Application.markUndoLevel("Change Spacing") 138 | 139 | with pw.Application.begin("Create") as creator: 140 | # set edge [pw::Edge createFromConnectors -single $cons(con-2)] 141 | edge = pw.Edge.createFromConnectors(cons["con-2"], single=True) 142 | 143 | # set dom [pw::DomainuStructured create] 144 | dom = pw.DomainStructured() 145 | 146 | # $dom addEdge $edge 147 | dom.addEdge(edge) 148 | 149 | # set extruder [pw::Application begin ExtrusionSolver $dom] 150 | with pw.Application.begin("ExtrusionSolver", dom) as extruder: 151 | # $dom setExtrusionSolverAttribute Mode Translate 152 | dom.setExtrusionSolverAttribute("Mode", "Translate") 153 | 154 | # $dom setExtrusionSolverAttribute TranslateDirection {0 -1 0} 155 | dom.setExtrusionSolverAttribute("TranslateDirection", (0, -1, 0)) 156 | 157 | # $dom setExtrusionSolverAttribute TranslateDistance 8 158 | dom.setExtrusionSolverAttribute("TranslateDistance", 8) 159 | 160 | # $extruder run 29 161 | extruder.run(29) 162 | 163 | # "$extruder end" is implied 164 | 165 | pw.Application.markUndoLevel("Translate") 166 | 167 | with pw.Application.begin("Create") as creator: 168 | edge = pw.Edge.createFromConnectors(cons.values(), single=True) 169 | dom = pw.DomainStructured() 170 | dom.addEdge(edge) 171 | 172 | with pw.Application.begin("ExtrusionSolver", dom) as extruder: 173 | dom.setExtrusionSolverAttribute("Mode", "Translate") 174 | dom.setExtrusionSolverAttribute("TranslateDirection", Vector3(0, -1, 0).negate()) 175 | dom.setExtrusionSolverAttribute("TranslateDistance", 20) 176 | extruder.run(29) 177 | 178 | pw.Application.markUndoLevel("Translate") 179 | 180 | cons["con-3"] = pw.GridEntity.getByName("con-3") 181 | cons["con-5"] = pw.GridEntity.getByName("con-5") 182 | cons["con-6"] = pw.GridEntity.getByName("con-6") 183 | cons["con-8"] = pw.GridEntity.getByName("con-8") 184 | 185 | with pw.Application.begin("Modify", cons.values()) as modifier: 186 | cons["con-3"].getDistribution(1).setBeginSpacing(0.1) 187 | cons["con-5"].getDistribution(1).setEndSpacing(0.1) 188 | cons["con-6"].getDistribution(1).setBeginSpacing(0.1) 189 | cons["con-8"].getDistribution(1).setEndSpacing(0.1) 190 | 191 | pw.Application.markUndoLevel("Change Spacings") 192 | 193 | doms = { } 194 | doms["dom-1"] = pw.GridEntity.getByName("dom-1") 195 | doms["dom-2"] = pw.GridEntity.getByName("dom-2") 196 | 197 | # foreach { k v } [array get doms] { lappend dlist $v } 198 | # set solver [pw::Application begin EllipticSolver $dlist] 199 | with pw.Application.begin("EllipticSolver", doms.values()) as solver: 200 | # $solver Initialize 201 | solver.run("Initialize") 202 | # "$solver end" is implied 203 | 204 | pw.Application.markUndoLevel("Initialize") 205 | 206 | pw.Connector.setDefault("Dimension", 21) 207 | 208 | # pw::Application setClipboard $dlist 209 | pw.Application.setClipboard(doms.values()) 210 | 211 | # set paster [pw::Application begin Paste] 212 | with pw.Application.begin("Paste") as paster: 213 | # set ents [$paster getEntities] 214 | ents = paster.getEntities() 215 | 216 | # set modifier [pw::Application begin Modify $ents] 217 | with pw.Application.begin("Modify", ents) as modifier: 218 | # set xforments [$modifier getEntities] 219 | xforments = modifier.getEntities() 220 | 221 | # set coll [pw::Collection create] 222 | coll = pw.Collection() 223 | 224 | # $coll set $xforments 225 | coll.set(xforments) 226 | 227 | # set xform [pw::Transform translation {0 0 15}] 228 | xform = Transform.translation((0, 0, 15)) 229 | 230 | # pw::Entity transform $xform [$coll list] 231 | pw.Entity.transform(xform, coll.list()) 232 | 233 | # $coll delete 234 | coll.delete() 235 | 236 | # "$modifier end" is implied 237 | 238 | # "$paster end" is implied 239 | 240 | pw.Application.markUndoLevel("Paste") 241 | 242 | pw.Display.setShowDomains(False) 243 | 244 | with pw.Application.begin("Create") as creator: 245 | seg = pw.SegmentSpline() 246 | seg.addPoint(pw.GridEntity.getByName("con-13").getPosition(arc=0)) 247 | seg.addPoint(pw.GridEntity.getByName("con-1").getPosition(arc=0)) 248 | con = pw.Connector() 249 | con.addSegment(seg) 250 | con.calculateDimension() 251 | 252 | pw.Application.markUndoLevel("Create 2 Point Connector") 253 | 254 | with pw.Application.begin("Create") as creator: 255 | seg = pw.SegmentSpline() 256 | seg.addPoint(pw.GridEntity.getByName("con-9").getPosition(arc=0)) 257 | seg.addPoint(pw.GridEntity.getByName("con-1").getPosition(arc=1)) 258 | con = pw.Connector() 259 | con.addSegment(seg) 260 | con.calculateDimension() 261 | 262 | pw.Application.markUndoLevel("Create 2 Point Connector") 263 | 264 | with pw.Application.begin("Create") as creator: 265 | seg = pw.SegmentSpline() 266 | seg.addPoint(pw.GridEntity.getByName("con-4").getPosition(arc=1)) 267 | seg.addPoint(pw.GridEntity.getByName("con-11").getPosition(arc=1)) 268 | con = pw.Connector() 269 | con.addSegment(seg) 270 | con.calculateDimension() 271 | 272 | pw.Application.markUndoLevel("Create 2 Point Connector") 273 | 274 | with pw.Application.begin("Create") as creator: 275 | seg = pw.SegmentSpline() 276 | seg.addPoint(pw.GridEntity.getByName("con-10").getPosition(arc=1)) 277 | seg.addPoint(pw.GridEntity.getByName("con-4").getPosition(arc=0)) 278 | con = pw.Connector() 279 | con.addSegment(seg) 280 | con.calculateDimension() 281 | 282 | pw.Application.markUndoLevel("Create 2 Point Connector") 283 | 284 | with pw.Application.begin("Create") as creator: 285 | seg = pw.SegmentSpline() 286 | seg.addPoint(pw.GridEntity.getByName("con-2").getPosition(arc=1)) 287 | seg.addPoint(pw.GridEntity.getByName("con-9").getPosition(arc=1)) 288 | con = pw.Connector() 289 | con.addSegment(seg) 290 | con.calculateDimension() 291 | 292 | pw.Application.markUndoLevel("Create 2 Point Connector") 293 | 294 | with pw.Application.begin("Create") as creator: 295 | seg = pw.SegmentSpline() 296 | seg.addPoint(pw.GridEntity.getByName("con-14").getPosition(arc=1)) 297 | seg.addPoint(pw.GridEntity.getByName("con-7").getPosition(arc=0)) 298 | con = pw.Connector() 299 | con.addSegment(seg) 300 | con.calculateDimension() 301 | 302 | pw.Application.markUndoLevel("Create 2 Point Connector") 303 | 304 | with pw.Application.begin("Create") as creator: 305 | seg = pw.SegmentSpline() 306 | seg.addPoint(pw.GridEntity.getByName("con-7").getPosition(arc=1)) 307 | seg.addPoint(pw.GridEntity.getByName("con-15").getPosition(arc=1)) 308 | con = pw.Connector() 309 | con.addSegment(seg) 310 | con.calculateDimension() 311 | 312 | pw.Application.markUndoLevel("Create 2 Point Connector") 313 | 314 | pw.Display.setShowDomains(True) 315 | pw.Display.resetView("-Z") 316 | 317 | for con in pw.Grid.getAll(type="pw::Connector"): 318 | cons[con.getName()] = con 319 | 320 | unusedCons = GlyphVar("unusedCons") 321 | poleDoms = GlyphVar() 322 | unusedDoms = GlyphVar() 323 | 324 | # pw::DomainStructured createFromConnectors -reject unusedCons -solid $clist 325 | pw.DomainStructured.createFromConnectors(cons.values(), reject=unusedCons, solid=True) 326 | 327 | # pw::BlockStructured createFromDomains -poleDomains poleDoms -reject unusedDoms [pw::Grid getAll -type pw::Domain] 328 | pw.BlockStructured.createFromDomains(pw.Grid.getAll(type="pw::Domain"), poleDomains=poleDoms, reject=unusedDoms) 329 | 330 | # GlyphVar usage: 'var.value' is the processed Tcl variable, which may 331 | # contain string or numeric values, or Glyph objects 332 | if len(unusedDoms.value) > 0: 333 | glf.puts("First block assembly, some domains are unused:") 334 | for ud in unusedDoms.value: 335 | # print to the Pointwise message window 336 | glf.puts(" %s" % ud.getName()) 337 | 338 | if len(poleDoms.value) > 0: 339 | glf.puts("First block assembly, some domains are poles (fatal):") 340 | # foreach pd $poleDoms { puts [format "Domain %s is a pole domain" [$pd getName]] } 341 | for pd in poleDoms.value: 342 | glf.puts(" %s is a pole domain" % pd.getName()) 343 | exit(1) 344 | 345 | pw.Application.markUndoLevel("Assemble Blocks") 346 | 347 | # The connection domain could not be automatically assembled from the full set of 348 | # connectors, so we must isolate them and create the domain separately 349 | connection_cons = [] 350 | for i in (2, 9, 18, 21): connection_cons.append(pw.GridEntity.getByName("con-%d" % i)) 351 | 352 | pw.DomainStructured.createFromConnectors(connection_cons, reject=unusedCons, solid=True) 353 | 354 | pw.BlockStructured.createFromDomains(pw.Grid.getAll(type="pw::Domain"), poleDomains=poleDoms, reject=unusedDoms) 355 | 356 | pw.Application.markUndoLevel("Assemble Blocks") 357 | 358 | doms = { } 359 | # foreach d [pw::Grid getAll -type pw::DomainStructured] { set doms([$d getName]) $d } 360 | for d in pw.Grid.getAll(type="pw::DomainStructured"): doms[d.getName()] = d 361 | 362 | blks = { } 363 | # set blks(blk-1) [pw::GridEntity getByName "blk-1"] 364 | blks["blk-1"] = pw.GridEntity.getByName("blk-1") 365 | 366 | # set blks(blk-2) [pw::GridEntity getByName "blk-2"] 367 | blks["blk-2"] = pw.GridEntity.getByName("blk-2") 368 | 369 | # set bc [pw::BoundaryConditon create] 370 | bc = pw.BoundaryCondition() 371 | 372 | # $bc setName Inflow 373 | bc.setName("Inflow") 374 | 375 | # $bc setPhysicalType Inflow 376 | bc.setPhysicalType("Inflow") 377 | 378 | # $bc apply [list [list $blks(blk-2) $doms(dom-9)]] 379 | bc.apply([[blks["blk-2"], doms["dom-9"]]]) 380 | 381 | bc = pw.BoundaryCondition() 382 | bc.setName("Outflow") 383 | bc.setPhysicalType("Outflow") 384 | 385 | # $bc apply [list [list $blks(blk-2) $doms(dom-8)] [list $blks(blk-1) $doms(dom-5)]] 386 | bc.apply([[blks["blk-2"], doms["dom-8"]], [blks["blk-1"], doms["dom-5"]]]) 387 | 388 | bc = pw.BoundaryCondition() 389 | bc.setName("Wall") 390 | bc.setPhysicalType("Wall") 391 | 392 | bc.apply([[blks["blk-2"], doms["dom-7"]], 393 | [blks["blk-1"], doms["dom-6"]], 394 | [blks["blk-1"], doms["dom-10"]]]) 395 | 396 | bc = pw.BoundaryCondition() 397 | bc.setName("Symmetry") 398 | bc.setPhysicalType("Symmetry Plane") 399 | 400 | bc.apply([[blks["blk-2"], doms["dom-4"]], 401 | [blks["blk-2"], doms["dom-2"]], 402 | [blks["blk-2"], doms["dom-11"]], 403 | [blks["blk-1"], doms["dom-1"]], 404 | [blks["blk-1"], doms["dom-3"]]]) 405 | 406 | pw.Application.markUndoLevel("Set BC") 407 | 408 | # The remainder of this script is for demonstration purposes 409 | # only, and is not part of the Back Step tutorial. 410 | 411 | # set exam [pw::Examine create "BlockJacobian"] 412 | with pw.Examine("BlockJacobian") as exam: 413 | # $exam addEntity $blist 414 | exam.addEntity(blks.values()) 415 | 416 | # $exam examine 417 | exam.examine() 418 | 419 | # puts [format "Min/Max Jacobian: %f/%f" [$exam getMinimum] [$exam getMaximum]] 420 | glf.puts("Min/Max Jacobian: %f/%f" % (exam.getMinimum(), exam.getMaximum())) 421 | 422 | # Note: exam.delete() is optional since exam doubles as a context manager 423 | 424 | # pw::CutPlane applyMetric BlockJacobian 425 | pw.CutPlane.applyMetric("BlockJacobian") 426 | 427 | # set cut [pw::CutPlane create] 428 | cut = pw.CutPlane() 429 | 430 | # $cut setConstant -J 11 431 | cut.setConstant(J=11) 432 | 433 | # $cut addBlock $blks(blk-1) 434 | cut.addBlock(blks.values()) 435 | 436 | # $cut setTransparency 0.25 437 | cut.setTransparency(0.25) 438 | 439 | # $cut setShrinkFactor 0.9 440 | cut.setShrinkFactor(0.9) 441 | 442 | # pw::Application save backstep.pw 443 | pw.Application.save("backstep.pw") 444 | 445 | # foreach { k v } [array get blks] { lappend blist $v } 446 | # pw::Application export -precision Single $blist backstep.cgns 447 | pw.Application.export(blks.values(), "backstep.cgns", precision="Single") 448 | 449 | glf.close() 450 | 451 | ############################################################################# 452 | # 453 | # This file is licensed under the Cadence Public License Version 1.0 (the 454 | # "License"), a copy of which is found in the included file named "LICENSE", 455 | # and is distributed "AS IS." TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE 456 | # LAW, CADENCE DISCLAIMS ALL WARRANTIES AND IN NO EVENT SHALL BE LIABLE TO 457 | # ANY PARTY FOR ANY DAMAGES ARISING OUT OF OR RELATING TO USE OF THIS FILE. 458 | # Please see the License for the full text of applicable terms. 459 | # 460 | ############################################################################# 461 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Glyph API for Python 2 | ==================== 3 | Copyright 2021 Cadence Design Systems, Inc. All rights reserved worldwide. 4 | 5 | This is a Python implementation of the Pointwise Glyph API. Glyph is 6 | implemented as a set of Tcl procedures that have an object-oriented 7 | feel. Pointwise supports non-Tcl scripting (or binary code) applications 8 | through a feature called *Glyph Server*, first avaialable in V18.0. 9 | This package allows low level communication with a Glyph Server, but 10 | doing so amounts to constructing Glyph/Tcl commands as strings and 11 | sending them to the Glyph Server for processing. The recommended way 12 | to use this package however is to use the higher level Glyph API for 13 | Python. This API leverages some of the introspective features of the 14 | Python language to automatically convert Python expressions into Glyph/Tcl 15 | command strings to be executed on the Glyph Server, and to convert the 16 | results back into Python objects. 17 | 18 | Glyph commands are formulated in Python using a JSON structure and 19 | passed to Glyph through a special dispatching command on the server that 20 | handles only JSON-encoded commands. (This dispatching command is 21 | available in Pointwise V18.2 and later.) The results from this command 22 | are returned as JSON structures that are subsequently processed and 23 | converted into Python number, string and special GlyphObj objects. 24 | 25 | The core components of this API are the *GlyphObj* and *GlyphVar* classes. 26 | All Glyph objects and classes are represented as instances of GlyphObj, which 27 | provides transparent access to all Glyph actions as though they were 28 | implemented in Python. GlyphVar provides a way to set and access Tcl variables 29 | on the server, primarily for use with Glyph actions that accept Tcl variable 30 | names as arguments. 31 | 32 | Installation 33 | ~~~~~~~~~~~~ 34 | 35 | The easiest way to install is using PIP: 36 | 37 | .. code:: python 38 | 39 | python -m pip install --user --upgrade pointwise-glyph-client 40 | 41 | The client can also be pulled from this repository and added to PYTHONPATH environment variable. 42 | 43 | 44 | Usage 45 | ~~~~~ 46 | 47 | The most basic usage of the Glyph API for Python is to: 48 | 49 | 1. Import GlyphClient from pointwise package 50 | 2. Create a GlyphClient object 51 | 3. Request a GlyphAPI object from the client object, which automatically 52 | connects to the GlyphServer if the GlyphClient is not already connected. 53 | 4. Issue Glyph actions through the GlyphAPI 54 | 55 | Example Usage 56 | ~~~~~~~~~~~~~ 57 | 58 | .. code:: python 59 | 60 | from pointwise import GlyphClient 61 | 62 | glf = GlyphClient() 63 | pw = glf.get_glyphapi() 64 | 65 | pw.Connector.setCalculateDimensionMethod("Spacing") 66 | pw.Connector.setCalculateDimensionSpacing(.3) 67 | with pw.Application.begin("Create") as creator: 68 | conic1 = pw.SegmentConic() 69 | conic1.addPoint((-25,8,0)) 70 | conic1.addPoint((-8,8,0)) 71 | conic1.setIntersectPoint((-20,20,0)) 72 | conic2 = pw.SegmentConic() 73 | conic2.addPoint(conic1.getPoint(conic1.getPointCount())) 74 | conic2.addPoint((10,16,0)) 75 | conic2.setShoulderPoint((8,8,0)) 76 | con = pw.Connector() 77 | con.addSegment(conic1) 78 | con.addSegment(conic2) 79 | con.calculateDimension() 80 | 81 | Usage Notes 82 | =========== 83 | 84 | Platform Support 85 | ~~~~~~~~~~~~~~~~ 86 | 87 | The Glyph API has been sufficiently tested with both Python 2.6+ and Python 3.3+ 88 | on Windows and Linux. It has not been tested extensively on Mac OS/X. The API 89 | also depends on a widely-available third-party library called 90 | numpy_. It is recommended that at least version 1.12 91 | be installed, but older versions may work as well. 92 | 93 | .. _numpy: http://www.numpy.org/ 94 | 95 | 96 | GlyphClient object 97 | ~~~~~~~~~~~~~~~~~~ 98 | 99 | GlyphClient implements only the client-server communication from the 100 | Python script to a Pointwise server. It can be used completely 101 | independently from the Glyph API as it provides methods for connecting to 102 | a server, evaluating Tcl/Glyph expressions, retrieving the raw Tcl 103 | string results, and disconnecting from the server. 104 | 105 | A GlyphClient can be used as a Python context manager. This allows a 106 | script to access a Pointwise Glyph server using idiomatic context 107 | management. 108 | 109 | Example: 110 | 111 | .. code:: python 112 | 113 | with GlyphClient() as glf: 114 | glf.eval("puts {Hello World}") 115 | 116 | The GlyphClient constructor has an optional argument of a port number. Most 117 | of the time this can be left unspecified, which means it will use the value 118 | of the PWI_GLYPH_SERVER_PORT, or 2807 if the environment variable is not 119 | defined. When running a script from within the Pointwise GUI, before the 120 | script is executed, the PWI_GLYPH_SERVER_PORT environment variable is set 121 | to the current port that the Glyph Server has been configured to listen to. 122 | Only when a script needs to communicate with multiple instances of Pointwise 123 | would specifying the port be necessary, and it is recommended that the script 124 | allow user input for specifying the port so that the script is not hard coded. 125 | 126 | The GlyphClient constructor can also start Pointwise in batch mode as a 127 | subprocess with a actively listening Glyph Server by specifying the port number 128 | as zero. Note that this will consume a Pointwise license, if one is available. 129 | Standard and error output from the Pointwise subprocess can be captured by 130 | specifying a callback function. 131 | 132 | Note that, in order to use port=0, the directory path of the 133 | Pointwise-installed version of 'tclsh' (for Windows platforms) or the 134 | 'pointwise' launch script (for non-Windows) must be in the environment 135 | PATH variable, along with any other necessary environment needed to 136 | run the script. 137 | 138 | Example: 139 | 140 | .. code:: python 141 | 142 | def echo(text): 143 | print("Server:", text) 144 | 145 | with GlyphClient(port=0, callback=echo) as glf: 146 | glf.puts("Hello World") 147 | 148 | Should produce: 149 | 150 | :: 151 | 152 | Server: Hello World 153 | 154 | GlyphAPI object 155 | ~~~~~~~~~~~~~~~ 156 | 157 | GlyphAPI extends the GlyphClient functionality by providing the transparent 158 | access needed to make Glyph calls in a very Pythonic manner. A GlyphAPI object 159 | should never be constructed directly, and only be created by a connected 160 | GlyphClient object. Connections to multiple Pointwise servers are possible, and 161 | all Glyph actions invoked within the context of a GlyphAPI are done so on the 162 | associated server connection. 163 | 164 | Example: 165 | 166 | .. code:: python 167 | 168 | glf1 = GlyphClient(port=2807) 169 | glf2 = GlyphClient(port=2808) 170 | 171 | pw1 = glf1.get_glyphapi() 172 | pw2 = glf2.get_glyphapi() 173 | 174 | con1 = pw1.GridEntity.getByName("con-1") 175 | con2 = pw2.GridEntity.getByNAme("con-2") 176 | 177 | con1.join(con2) # Behavior undefined! 178 | 179 | GlyphVar object 180 | ~~~~~~~~~~~~~~~ 181 | 182 | A GlyphVar is required for Glyph actions that expect a Tcl variable name 183 | as an argument. These actions typically set the variable to some 184 | ancillary result value, independent of the action's direct result. A 185 | GlyphVar object is not coupled to a specific GlyphClient connection, as 186 | it is used only in the context of a Glyph action in order to retrieve 187 | some result value stored in a Tcl variable. A GlyphVar may be assigned a 188 | Tcl variable name, but it is not required; when unassigned, a unique 189 | temporary Tcl variable name will be generated. 190 | 191 | Example: 192 | 193 | .. code:: python 194 | 195 | poleDoms = GlyphVar() 196 | pw.BlockStructured.createFromDomains(doms, poleDomains=poleDoms) 197 | for d in poleDoms.value: print(d.getName()) 198 | 199 | GlyphObj object 200 | ~~~~~~~~~~~~~~~ 201 | 202 | GlyphObj is the primary Python interface to Glyph classes, objects and 203 | their associated actions. A GlyphObj instance is created automatically 204 | in the following ways: 205 | 206 | - When the method name of a call to GlyphAPI matches the base name of a 207 | published Glyph class name (**Application** for **pw::Application**) 208 | - When the result of some Glyph action returns a Glyph function name 209 | (a Glyph object, such as **::pw::Connector_1**) 210 | - When a GlyphVar's value contains a Glyph function name (Glyph object) 211 | - When constructed directly using a Glyph function name (Glyph object) 212 | 213 | Notes: 214 | 215 | - The list of Glyph classes that are wrap-able with **GlyphObj** are those 216 | that are returned from the Tcl command 'pw::Application getAllCommandNames'. 217 | This list does not include classes that are for internal use only, nor the 218 | classes that implement the Glyph Server itself (e.g., pw::Script). 219 | 220 | Examples: 221 | 222 | .. code:: python 223 | 224 | # There are two GlyphObj instances created here, one for "pw::Connector" class 225 | # and one for "::pw::Connector_1" object returned by pw.Connector() 226 | con1 = pw.Connector() 227 | 228 | # There are two GlyphObj instances created here as well, one for 229 | # "pw::GridEntity" class and one for "::pw::Connector_1" object returned 230 | # by "pw::GridEntity getByName con-1" 231 | con2 = pw.GridEntity.getByName("con-1") 232 | 233 | # This generates GlyphObj instances for "pw::BlockStructured", all the blocks 234 | # returned by "createFromDomains" and all the domains (if any) returned in 235 | # the "pdoms" Tcl variable passed to the action. 236 | poleDoms = GlyphVar("pdoms") 237 | blk = pw.BlockStructured.createFromDomains(doms, poleDomains=poleDoms) 238 | for d in poleDoms.value: print(d.getName()) 239 | 240 | Generating Glyph Actions Automatically 241 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 242 | 243 | Glyph actions are method invocations on either a Glyph class or a Glyph 244 | function (object). (These are called "functions" because Glyph generates a 245 | mapping from a Tcl proc to an internal object as a way of simulating 246 | object-oriented behavior in Glyph. This is a common pattern in Tcl package 247 | implementations.) There are two types of actions: *static actions* and 248 | *instance actions*. Further, every Glyph object that can be instantiated 249 | directly in a script has a static "create" action. So, by exploiting Python 250 | language features, the following syntaxes generates an associated 251 | Tcl/Glyph command: 252 | 253 | - A GlyphObj that represents a Glyph class that is called directly 254 | (i.e., appears to be a Python constructor) becomes a "create" action 255 | call. Arguments can be passed to these constructor-type calls as needed 256 | and as allowed by the corresponding Glyph "create" action. 257 | - A method call on a GlyphObj that represents a Glyph class is 258 | translated into a static action call on the Glyph class. 259 | - A method call on a GlyphObj that represents a Glyph object is 260 | translated into an instance action call on the object. 261 | 262 | *Note: Some Glyph action names conflict with Python reserved words (e.g. pw::Grid import). For conflicting action names, an underscore must be appended to the Python function name:* 263 | 264 | .. code:: python 265 | 266 | # This invokes "pw::Grid import $filename" 267 | pw.Grid.import_(filename) 268 | 269 | Example: 270 | 271 | .. code:: python 272 | 273 | # This invokes "pw::Connector create" with no arguments 274 | con = pw.Connector() 275 | 276 | # This invokes "pw::Examine create ConnectorLengthI" 277 | exam = pw.Examine("ConnectorLengthI") 278 | 279 | # This invokes "pw::Connector getAdjacentConnectors $cons" 280 | cons = pw.Connector.getAdjacentConnectors(cons) 281 | 282 | # This invokes "$con1 join $con2" 283 | con1 = con1.join(con2) 284 | 285 | Passing Arguments and Flags to Glyph Actions 286 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 287 | 288 | Many Glyph actions accept both *positional* and *flag* arguments. The Python 289 | equivalent of these are, respectively, *positional* and *keyword* arguments, 290 | but there are some strict rules that must be followed in order for the 291 | corresponding Glyph action commands to be generated correctly. All positional 292 | arguments must appear first in the Python method invocation, as is the 293 | requirement of the language, followed by all optional keyword arguments. 294 | GlyphObj converts all keyword arguments in the following way: 295 | 296 | - If the keyword does not end in an underscore ("\_"): 297 | 298 | - If the keyword argument is False, the flag is not added to the command 299 | - Otherwise, the keyword is prepended with a dash ("-") and added to 300 | the command. Then: 301 | 302 | - If the keyword argument is a **bool** and is **True**, no argument is 303 | added to the command 304 | - Otherwise, the keyword argument value is added as a single element to 305 | the Glyph command 306 | 307 | - If the keyword ends in an underscore, special handling is used: 308 | 309 | - The keyword is prepended with a dash, and the trailing underscore 310 | is removed, and the flag is added to the command. Then: 311 | 312 | - If the keyword argument value is a list or tuple of values, each value is 313 | added as a separate command argument. Note that any embedded list/tuple 314 | will remain as a Tcl list in the Glyph action command. 315 | - Otherwise, the keyword argument value is added to the command, even 316 | if a boolean value. 317 | 318 | Note that any positional argument that is a list or tuple will be passed as a Tcl 319 | list in the command. 320 | 321 | Examples: 322 | 323 | .. code:: python 324 | 325 | # set pt [$con getPosition -arc 1.0] 326 | pt = con.getPosition(1.0, arc=True) 327 | 328 | # set pt [$con getXYZ 1] 329 | pt = con.getXYZ(1, arc=False) 330 | 331 | # set ents [$bc getEntities -visibility true] 332 | ents = bc.getEntities(visibility_=True) 333 | 334 | # pw::Entity project -type Linear -axis {0 0 0} {0 0 1} $ents 335 | pw.Entity.project(ents, type="Linear", axis_=[(0, 0, 0), (0, 0, 1)]) 336 | 337 | # $shape polygon -points { { 0 0 0 } { 0 1 0 } { 1 0 0 } } 338 | shape.polygon(points=[(0, 0, 0), (0, 1, 0), (1, 0, 0)]) 339 | 340 | Glyph Objects as Context Managers 341 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 342 | 343 | In many cases it is convenient to use a GlyphObj that represents certain 344 | transient Glyph objects as Python context managers. Specifically, Glyph 345 | *Mode* and *Examine* objects are typically short-lived and are used in 346 | very specific contexts. For these Glyph object types only, context 347 | management is implemented in GlyphObj. 348 | 349 | Examples: 350 | 351 | .. code:: python 352 | 353 | with pw.Application.begin("Create") as creator: 354 | con = pw.Connector() 355 | ... 356 | # a mode will be implicitly ended when the context ends, unless 357 | # an exception occurs, in which case the mode is aborted and all 358 | # modifications made in the mode are discarded 359 | 360 | with pw.Examine("BlockJacobian") as exam: 361 | exam.addEntity([blk1, blk2]) 362 | exam.examine() 363 | ... 364 | # Examine objects are automatically deleted when the context exits, 365 | 366 | Glyph Utility Classes 367 | ~~~~~~~~~~~~~~~~~~~~~ 368 | 369 | The standard Tcl/Glyph command set includes a number of utility classes 370 | to perform vector algebra, extent box computation, transformation 371 | matrices, etc. To improve the overall usefulness and speed of this API, 372 | these classes were implemented directly in Python, rather than through 373 | the Glyph Server. Many of the mathematical vector and matrix operations 374 | are performed using the "numpy" package. These utilty classes include, 375 | along with their Glyph counterparts: 376 | 377 | - ``Vector2 - pwu::Vector2`` 378 | - ``Vector3 - pwu::Vector3`` 379 | - ``Quaternion - pwu::Quaternion`` 380 | - ``Plane - pwu::Plane`` 381 | - ``Transform - pwu::Transform`` 382 | - ``Extents - pwu::Extents`` 383 | 384 | Nearly the complete set of functions documented at 385 | https://www.pointwise.com/glyph under the "Utilities" section have been 386 | implemented as Python classes. 387 | 388 | Example: 389 | 390 | .. code:: python 391 | 392 | # set v1 [pwu::Vector3 set { 0 1 2 } 393 | v1 = Vector3([0, 1, 2]) # Vector3(0, 0, 0) also works 394 | 395 | # set v2 [pwu::Vector3 add $v1 { 2 4 6 } 396 | v2 = v1 + Vector3(2, 4, 6) 397 | 398 | # set v3 [pwu::Vector3 cross $v1 $v2] 399 | v3 = v1 * v2 # cross product 400 | 401 | # set v3 [pwu::Vector3 normalize $v3] 402 | v3 = v3.normalize() 403 | 404 | 405 | Disclaimer 406 | ~~~~~~~~~~ 407 | This file is licensed under the Cadence Public License Version 1.0 (the "License"), a copy of which is found in the LICENSE file, and is distributed "AS IS." 408 | TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, CADENCE DISCLAIMS ALL WARRANTIES AND IN NO EVENT SHALL BE LIABLE TO ANY PARTY FOR ANY DAMAGES ARISING OUT OF OR RELATING TO USE OF THIS FILE. 409 | Please see the License for the full text of applicable terms. 410 | -------------------------------------------------------------------------------- /pointwise/glyphapi/glyphobj.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | ############################################################################# 4 | # 5 | # (C) 2021 Cadence Design Systems, Inc. All rights reserved worldwide. 6 | # 7 | # This sample script is not supported by Cadence Design Systems, Inc. 8 | # It is provided freely for demonstration purposes only. 9 | # SEE THE WARRANTY DISCLAIMER AT THE BOTTOM OF THIS FILE. 10 | # 11 | ############################################################################# 12 | 13 | """ 14 | This module provides a Python-like interface to Glyph. 15 | 16 | GlyphObj provides the service of automatically converting Python 17 | function calls into the equivalent Glyph command string and creates 18 | Python objects to represent Glyph classes and objects. 19 | 20 | GlyphVar provides a convenient interface for representing Tcl variables 21 | as Python objects for Glyph commands that accept Tcl variable names as 22 | arguments. These are typically used to return some optional Glyph result 23 | value. 24 | """ 25 | 26 | from pointwise import GlyphClient, GlyphError 27 | import keyword, json, re 28 | 29 | class GlyphVar(object): 30 | """ A GlyphVar object wraps a Tcl variable name that may be passed to 31 | a Glyph action, which is typically used to return some value 32 | that is not the return value from the action. A variable name 33 | can be supplied; if not, a temporary Tcl variable name will be 34 | generated. 35 | 36 | Tcl: 37 | pw::Display selectEntities -selectionmask $mask ents 38 | 39 | where 'ents' is a Tcl variable name 40 | 41 | Python: 42 | m = pw.Display.createSelectionMask(requireConnector="Dimensioned") 43 | entsVar = GlyphVar() 44 | pw.Display.selectEntities(entsVar, selectionMask=m) 45 | cons = entsVar["Connectors"] 46 | 47 | where 'entsVar' is a GlyphVar object containing the 'ents' result value 48 | """ 49 | 50 | def __init__(self, varname=None, value=None): 51 | """ Initializes GlyphVar object with its initial value """ 52 | self.value = value 53 | self.varname_ = varname 54 | 55 | def __getitem__(self, item): 56 | """ Allows the GlyphVar to be subscriptable if applicable """ 57 | return self.value[item] 58 | 59 | def __setitem__(self, key, value): 60 | """ Allows the GlyphVar to have values assigned to it if applicable """ 61 | self.value[key] = value 62 | 63 | def __iter__(self): 64 | """ Allows the GlyphVar to be iterable """ 65 | return (e for e in self.value) 66 | 67 | 68 | class GlyphObj(object): 69 | """ This class implements a wrapper around Glyph Objects. In this context, 70 | a Glyph object can be a Glyph function (instance) for instance actions, 71 | or a Glyph class for static actions. 72 | """ 73 | 74 | # pre-compiled regular expression matching Glyph function (object) names 75 | _functionRE = re.compile(r"::pw::[a-zA-Z]+_\d+( |$)") 76 | 77 | def __init__(self, function, glf): 78 | """ Glyph object constructor from a Glyph function name. Glyph wrappers 79 | are typically created automatically from the result of other Glyph 80 | actions. 81 | 82 | Args: 83 | function - the glyph function name or JSON dict with 84 | command element 85 | 86 | glf - the GlyphClient connection in use 87 | 88 | Example: 89 | con = GlyphObj("pw::Connector_1", pw) 90 | """ 91 | self.glf = glf 92 | if isinstance(function, dict): 93 | self._function = function['command'] 94 | self._type = function['type'] 95 | elif isinstance(function, GlyphObj): 96 | self._function = function._function 97 | self._type = function._type 98 | else: 99 | try: 100 | self._function = function 101 | if GlyphObj._functionRE.match(function): 102 | self._type = glf.eval("%s getType" % function) 103 | else: 104 | self._type = None 105 | except Exception: 106 | print(str(type(self._function))) 107 | print(str(self._function)) 108 | raise 109 | 110 | def __enter__(self): 111 | """ Glyph objects derived from pw::Mode or pw::Examine can be 112 | used as context managers. For modes, the context will 113 | automatically issue the 'mode end' action upon exit, unless 114 | an exception occurs in which case 'mode abort' will be issued. 115 | Examine, the Glyph examine object will be destroyed when the 116 | context exits. 117 | 118 | Ex. 119 | with pw.Application.begin("Create") as creator: 120 | con = pw.Connector() 121 | ... 122 | 123 | with pw.Examine("BlockJacobian") as exam: 124 | exam.addEntity(blk) 125 | exam.examine() 126 | ... 127 | """ 128 | self._isMode = self._isExamine = self._open = False 129 | 130 | try: 131 | x = self.glf.eval(self._function + " isOfType pw::Examine") 132 | self._isExamine = bool(int(x)) 133 | except Exception: 134 | self._isExamine = False 135 | 136 | if not self._isExamine: 137 | try: 138 | x = self.glf.eval(self._function + " isOfType pw::Mode") 139 | self._isMode = bool(int(x)) 140 | except Exception: 141 | self._isMode = False 142 | 143 | if self._isMode: 144 | self._open = True 145 | 146 | def _end(): 147 | try: 148 | self.glf.eval(self._function + " end") 149 | self._open = False 150 | except: 151 | pass 152 | 153 | setattr(self, "end", _end) 154 | elif self._isExamine: 155 | self._open = True 156 | 157 | def _delete(): 158 | try: 159 | self.glf.eval(self._function + " delete") 160 | self._open = False 161 | except: 162 | pass 163 | 164 | setattr(self, "delete", _delete) 165 | else: 166 | raise GlyphError("", "A GlyphObj can be used as a context " + \ 167 | "manager for mode and examine objects only.") 168 | 169 | return self 170 | 171 | 172 | def __exit__(self, ex_type, ex_val, ex_tb): 173 | """ Exit context, ending or aborting mode or deleting examine object 174 | as needed 175 | """ 176 | try: 177 | if self._open: 178 | if self._isMode: 179 | if ex_type is None: 180 | self.glf.eval(self._function + " end") 181 | else: 182 | self.glf.eval(self._function + " abort") 183 | elif self._isExamine: 184 | self.glf.eval(self._function + " delete") 185 | self._open = False 186 | return False 187 | finally: 188 | del self._isMode 189 | del self._isExamine 190 | del self._open 191 | 192 | 193 | # Create Glyph object when called 194 | def __call__(self, *args): 195 | """ Creates GlyphObj from the result of the glyph command 196 | ' create'. This is valid only for 197 | objects that represent Glyph classes that implement 198 | the 'create' action. 199 | 200 | Args: 201 | args - a list of positional arguments to be passed to 202 | the Glyph 'create' action 203 | 204 | Returns: 205 | GlyphObj object that was created 206 | 207 | Example: 208 | con = pw.Connector() 209 | exa = pw.Examine("ConnectorLengthI") 210 | """ 211 | gvars = {} 212 | kwargs = {} 213 | cmd = self._buildGlyphCmd(self._function, 'create', args, kwargs, gvars) 214 | result = self._runGlyphCmd(cmd) 215 | return result 216 | 217 | 218 | def __str__(self): 219 | """ Returns a string representation of the Glyph object. """ 220 | name = self.glf.eval("if [%s isOfType pw::Entity] { %s getName }" % \ 221 | (self._function, self._function)) 222 | return "%s (%s)" % (self._function, name) 223 | 224 | 225 | # Method for comparing GlyphObj objects 226 | def __eq__(self, other): 227 | """ Compare two Glyph objects for equality. Glyph objects are 228 | equal if their Glyph functions refer to the same Glyph object. 229 | 230 | Returns: 231 | True if GlyphObj objects refer to the same Glyph object. 232 | """ 233 | cmd = [self._function, 'equals', other._function] 234 | return bool(self._runGlyphCmd(cmd)) 235 | 236 | 237 | # Add hash support so GlyphObj objects can be used in dictionaries 238 | def __hash__(self): 239 | return hash(self._function) 240 | 241 | 242 | @property 243 | def glyphType(self): 244 | """ Return the Glyph type of a GlyphObj. """ 245 | return self._type 246 | 247 | 248 | @staticmethod 249 | def _toGlyph(value, varDict): 250 | """ Helper function to process a Glyph result value. """ 251 | result = None 252 | if isinstance(value, GlyphVar): 253 | # Create variable in Glyph and store its reference name in varDict 254 | if value.varname_ is None: 255 | value.varname_ = '_TMPvar_%d' % len(varDict) 256 | result = value.varname_ 257 | varDict[value] = result 258 | elif isinstance(value, GlyphObj): 259 | # Substitute the Glyph function name 260 | result = value._function 261 | elif isinstance(value, str): 262 | # Handle string arguments 263 | result = value 264 | else: 265 | try: 266 | # See if the object is iterable 267 | it = iter(value) 268 | result = [GlyphObj._toGlyph(v, varDict) for v in it] 269 | except: 270 | result = value 271 | pass 272 | 273 | return result 274 | 275 | 276 | @staticmethod 277 | def _buildGlyphCmd(function, action, args, kwargs, gvars): 278 | """ Helper function that builds command list to be converted to JSON. 279 | 280 | Args: 281 | function: the Glyph object or class on which the action 282 | is to be run 283 | 284 | action: the Glyph action to be run on the Glyph object or class 285 | 286 | args - list of non-keyword (positional) arguments for the 287 | Glyph action. Positional arguments always appear at 288 | the end of a Glyph action, after all command switches. 289 | 290 | kwargs - list of keyword (switch) arguments for the Glyph 291 | action. Switch arguments always appear immediately 292 | after the action name. The following rules apply 293 | when keyward arguments are present: 294 | 295 | - the keyword is added to the Glyph command as a switch 296 | (e.g., '-orient') 297 | - if the keyword argument value is None, the switch 298 | has no arguments in the action 299 | - if the keyword argument value is a scalar (non-list) 300 | value, the value is added right after the switch 301 | (e.g., '-orient Best') 302 | - if the keyword argument value is a list, a Tcl 303 | list is added right after the switch, UNLESS the 304 | keyword itself has a trailing underscore. In that 305 | case, the list arguments are added one at a time 306 | after the switch. For example, 307 | 308 | Python Tcl 309 | arg=[s1, s2] => -arg [list s1 s2] 310 | arg_=[s1, s2] => -arg s1 s2 311 | 312 | gvars: dictionary used to keep track of Glyph variables. Key 313 | is GlyphVar object and value is Glyph variable name 314 | 315 | Returns: 316 | JSON serializable list to be sent to _runGlyphCmd function 317 | """ 318 | cmd = [] 319 | 320 | # Some glyph commands are python keywords, and so must be passed with 321 | # a trailing underscore 322 | if keyword.iskeyword(action[:-1]) and action[-1] == '_': 323 | action = action[:-1] 324 | 325 | cmd.append(function) 326 | cmd.append(action) 327 | 328 | # Add keyword args as flags first (Glyph standard command notation) 329 | for flag, value in kwargs.items(): 330 | # special convention for flags that accept multiple arguments: 331 | # foo = [a, b] => -foo {a b} 332 | # foo_ = [a, b] => -foo a b 333 | # foo = [[a, b], [c, d]] => -foo {{a b} {c d}} 334 | # foo_ = [[a, b], [c, d]] => -foo {a b} {c d} 335 | 336 | # special convention for keywords with boolean values: 337 | # foo=True => -foo 338 | # foo_=True => -foo true 339 | # foo=False => (flag excluded from command) 340 | # foo_=False => -foo false 341 | 342 | flatten = flag.endswith('_') 343 | 344 | if flatten: 345 | flag = flag[:-1] 346 | 347 | if not flatten and isinstance(value, bool) and bool(value) == False: 348 | continue 349 | 350 | gval = GlyphObj._toGlyph(value, gvars) 351 | 352 | cmd.append("-%s" % flag) 353 | 354 | if flatten: 355 | cmd += gval 356 | elif not isinstance(value, bool): 357 | cmd.append(gval) 358 | 359 | # Add positional args (never flattened) to the command 360 | cmd += [GlyphObj._toGlyph(arg, gvars) for arg in args] 361 | 362 | return cmd 363 | 364 | 365 | @staticmethod 366 | def _isGlyphFunction(arg): 367 | """ Helper function to determine if an object or list of objects can 368 | be converted to a GlyphObj 369 | 370 | Args: 371 | arg - element to check if it is something returned from JSON 372 | 373 | Returns: 374 | True if element can be converted to a GlyphObj 375 | """ 376 | 377 | if isinstance(arg, dict): 378 | # JSON result from Glyph action 379 | return all(k in arg for k in ('command', 'type')) 380 | elif isinstance(arg, str) and GlyphObj._functionRE.match(arg): 381 | # Glyph function name 382 | return True 383 | else: 384 | return False 385 | 386 | 387 | def _runGlyphCmd(self, cmd): 388 | """ Helper function to run a Glyph action that is represented as 389 | a list of string command arguments 390 | 391 | Args: 392 | cmd - A JSON serializable list that represents the Glyph action 393 | 394 | Returns: 395 | Return value from Glyph action, converted to a GlyphObj if 396 | possible 397 | """ 398 | 399 | # convert the command list to a JSON string, execute the command, and 400 | # convert the result back from a JSON string to a Python list 401 | result = json.loads(self.glf.command(json.dumps(cmd))) 402 | 403 | # If the result is a list, convert any JSON dict elements to GlyphObj, 404 | # allowing for nested lists 405 | if isinstance(result, list): 406 | result = [self._toPythonObj(k) for k in result] 407 | else: 408 | result = self._toPythonObj(result) 409 | 410 | return result 411 | 412 | 413 | def __getattr__(self, action): 414 | """ Create a method on demand that mimics some Glyph action. 415 | Note: Glyph action names that conflict with Python reserved words 416 | must be appended with an underscore. E.g. 417 | 418 | pw.Grid.import_(filename) 419 | """ 420 | def _action_(*args, **kwargs): 421 | """ Used to generate Glyph calls 'on the fly'. A JSON command is 422 | created based on the GlyphObj action called. The value that 423 | is returned from the server is then converted back to GlyphObj 424 | object if applicable. 425 | 426 | Returns: 427 | Whatever Glyph command would return as GlyphObj object 428 | type if applicable 429 | """ 430 | function = self._function 431 | 432 | # dict of GlyphObj (Tcl variable) 433 | gvars = {} 434 | 435 | # create command token list 436 | cmd = self._buildGlyphCmd(function, action, args, kwargs, gvars) 437 | try: 438 | result = self._runGlyphCmd(cmd) 439 | # evaluate Glyph variable contents 440 | if gvars: 441 | for pyVar, tclVarName in gvars.items(): 442 | pyVar.value = self._evalTclVar(tclVarName) 443 | self._delTclVar(gvars) 444 | if self._isMode and action in ['end', 'abort']: 445 | self._open = False 446 | elif self._isExamine and action == 'delete': 447 | self._open = False 448 | return result 449 | except GlyphError: 450 | raise 451 | 452 | setattr(self, action, _action_) 453 | return _action_ 454 | 455 | 456 | def _delTclVar(self, gvars): 457 | """ Helper function to delete (unset) a dictionary of Glyph (Tcl) 458 | variables 459 | 460 | Args: 461 | gvars - dictionary of Glyph variables with key as 462 | GlyphVar object and value as Glyph name 463 | 464 | Returns: 465 | None 466 | """ 467 | _unset = "if [info exists %s] { unset %s }" 468 | cmd = [(_unset % (var, var)) for var in gvars.values()] 469 | self.glf.eval('; '.join(cmd)) 470 | 471 | 472 | def _toPythonObj(self, tclArg): 473 | """ Helper function to convert a Tcl string to a Python string or 474 | GlyphObj object 475 | 476 | Args: 477 | tclArg - item to convert 478 | 479 | Returns: 480 | Python string or GlyphObj 481 | """ 482 | result = tclArg 483 | # handle nested lists 484 | if isinstance(result, list): 485 | result = [self._toPythonObj(k) for k in result] 486 | elif GlyphObj._isGlyphFunction(tclArg): 487 | result = GlyphObj(tclArg, self.glf) 488 | elif isinstance(tclArg, str): 489 | tclArg = tclArg.strip() 490 | if len(tclArg) > 0: 491 | try: 492 | result = int(tclArg) 493 | except ValueError: 494 | try: 495 | result = float(tclArg) 496 | except ValueError: 497 | result = tclArg 498 | else: 499 | result = None 500 | return result 501 | 502 | 503 | def _toPythonList(self, tclArg): 504 | """ Helper function to convert a Tcl list to a Python list, converting 505 | Glyph function names to GlyphObj objects where possible. The 506 | input list can be as deeply nested as needed, and the resulting 507 | Python list will be nested to the same depth. Tcl strings that can 508 | be converted to numeric Python objects will be so converted. 509 | 510 | Args: 511 | tclArg - Tcl list or value to be converted to Python list of 512 | string or GlyphObj values. A Tcl list must be in the 513 | form "{ v1 v2 v3 }". Nested Tcl lists are handled. 514 | 515 | Tcl Python 516 | { { 0 0 1 } { 0 1 0 } } => [[0, 0, 1], [0, 1, 0]] 517 | 518 | { 0.0 1.0 ::pw::Surface_1 } => [0.0, 1.0, gobj] 519 | where gobj = GlyphObj("::pw::Surface_1") 520 | 521 | {::pw::Surface_1 ::pw::Connector_1 } => [gobj1, gobj2] 522 | where gobj1 = GlyphObj("::pw::Surface_1") 523 | gobj2 = GlyphObj("::pw::Connector_1") 524 | 525 | Returns: 526 | Python list where every element is a string, number, or 527 | GlyphObj object 528 | """ 529 | out = [] 530 | cache = [out] 531 | element = '' 532 | escape = False 533 | for char in tclArg: 534 | if escape: 535 | if char not in ["\\", "{", "}", "[", "]", "$"]: 536 | raise ValueError("Incorrect escape character %s" % char) 537 | element += char 538 | escape = False 539 | elif char == "\\": 540 | escape = True 541 | elif char in [" ", "\t", "\r", "\n"]: 542 | element = self._toPythonObj(element) 543 | if element is not None: 544 | cache[-1].append(element) 545 | element = '' 546 | elif char == "{": 547 | level = [] 548 | cache[-1].append(level) 549 | cache.append(level) 550 | elif char == "}": 551 | if len(cache) < 2: 552 | raise ValueError("Close bracket without opening bracket.") 553 | element = self._toPythonObj(element) 554 | if element is not None: 555 | cache[-1].append(element) 556 | cache.pop() 557 | element = '' 558 | else: 559 | element += char 560 | 561 | element = self._toPythonObj(element) 562 | if element is not None: 563 | cache[-1].append(element) 564 | 565 | if len(cache) != 1: 566 | raise ValueError("Mismatched brackets.") 567 | 568 | if len(out) == 1: 569 | out = out[0] 570 | return out 571 | 572 | 573 | def _evalTclVar(self, tclVarName): 574 | """ Evaluate a Tcl variable and convert to Python. A Tcl array will 575 | be converted to a Python dictionary. All other Tcl values will 576 | be converted to a list of Python values. 577 | 578 | Note: scalar Tcl values will be returned as a list with a 579 | single element. Values that are Glyph functions (objects) 580 | will be converted to GlyphObj objects. 581 | 582 | Args: 583 | tclVarName - Glyph variable name to be evaluated and 584 | stored in a Python dictionary or list 585 | 586 | Returns: 587 | A Python Dictionary 588 | """ 589 | # ask Tcl interpreter if the variable exists 590 | isvar = int(self.glf.eval("info exists %s" % tclVarName)) 591 | if not isvar: 592 | return None 593 | 594 | # ask Tcl interpreter if the variable is an array 595 | isdict = int(self.glf.eval("array exists %s" % tclVarName)) 596 | if isdict: 597 | # Convert Tcl array to Python dict 598 | tcl = self.glf.eval("array get %s" % tclVarName) 599 | else: 600 | # Convert Tcl list (or single value) to a Python list 601 | tcl = self.glf.eval("lrange $%s 0 end" % tclVarName) 602 | 603 | result = self._toPythonList(tcl) 604 | 605 | if isdict: 606 | # make each dictionary value a list as needed 607 | i = iter(result) 608 | result = dict(zip(i, i)) 609 | for key, value in result.items(): 610 | if not isinstance(value, list): 611 | result[key] = [value] 612 | 613 | return result 614 | 615 | ############################################################################# 616 | # 617 | # This file is licensed under the Cadence Public License Version 1.0 (the 618 | # "License"), a copy of which is found in the included file named "LICENSE", 619 | # and is distributed "AS IS." TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE 620 | # LAW, CADENCE DISCLAIMS ALL WARRANTIES AND IN NO EVENT SHALL BE LIABLE TO 621 | # ANY PARTY FOR ANY DAMAGES ARISING OUT OF OR RELATING TO USE OF THIS FILE. 622 | # Please see the License for the full text of applicable terms. 623 | # 624 | ############################################################################# 625 | -------------------------------------------------------------------------------- /pointwise/glyph_client.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | ############################################################################# 4 | # 5 | # (C) 2021 Cadence Design Systems, Inc. All rights reserved worldwide. 6 | # 7 | # This sample script is not supported by Cadence Design Systems, Inc. 8 | # It is provided freely for demonstration purposes only. 9 | # SEE THE WARRANTY DISCLAIMER AT THE BOTTOM OF THIS FILE. 10 | # 11 | ############################################################################# 12 | 13 | """ 14 | This module provides a wrapper around TCP communication of a client with 15 | a Glyph Server. 16 | 17 | Example: 18 | from pointwise import GlyphClient, GlyphError 19 | 20 | glf = GlyphClient() 21 | if glf.connect(): 22 | try: 23 | result = glf.eval('pw::Application getVersion') 24 | print('Pointwise version is {0}'.format(result)) 25 | except GlyphError as e: 26 | print('Error in command {0}\n{1}'.format(e.command, e)) 27 | 28 | elif glf.is_busy(): 29 | print('The Glyph Server is busy') 30 | elif glf.auth_failed(): 31 | print('Glyph Server authentication failed') 32 | else: 33 | print('Failed to connect to the Glyph Server') 34 | """ 35 | 36 | import os, time, socket, struct, errno, sys, re, platform 37 | 38 | class GlyphError(Exception): 39 | """ This exception is raised when a command passed to the Glyph Server 40 | encounters an error. 41 | """ 42 | def __init__(self, command, message, *args, **kwargs): 43 | Exception.__init__(self, message, *args, **kwargs) 44 | self.command = command 45 | 46 | 47 | class NoLicenseError(Exception): 48 | """ This exception is raised when a Glyph server subprocess could 49 | not acquire a valid Pointwise license. 50 | """ 51 | def __init__(self, message, *args, **kwargs): 52 | Exception.__init__(self, message, *args, **kwargs) 53 | 54 | 55 | class GlyphClient(object): 56 | """ This class is a wrapper around TCP client communications with a 57 | Glyph Server. Optionally, it can start a batch Glyph Server process 58 | as a subprocess (which consumes a Pointwise license). To start 59 | a server subprocess, initialize the GlyphClient with port = 0. 60 | For Windows platforms, the default program to run is 'tclsh'. For 61 | Linux and Mac OS platforms, the default program is 'pointwise -b'. 62 | (Optionally, the 'prog' argument may be specified to indicate the 63 | program or shell script to run as the batch Glyph server process. 64 | This is typically used in environments where multiple versions of 65 | Pointwise are installed.) 66 | """ 67 | 68 | def __init__(self, port=None, auth=None, version=None, host='localhost', \ 69 | callback=None, prog=None, timeout=10): 70 | """ Initialize a GlyphClient object 71 | 72 | Args: 73 | port (str): the port number of the Glyph Server to connect to. 74 | If the port is not given, it defaults to the environment 75 | variable PWI_GLYPH_SERVER_PORT, or 2807 if not defined. 76 | If port is 0, a Pointwise batch subprocess will be started 77 | if a license is available. 78 | auth (str): the authorization code of the Glyph Server. If the 79 | auth is not given, it defaults to the environment variable 80 | PWI_GLYPH_SERVER_AUTH, or an empty string if not defined. 81 | version (str): the Glyph version number to use for 82 | compatibility. The string should be of the form X.Y.Z 83 | where X, Y, and Z are positive integer values. The Y and 84 | Z values are optional and default to a zero value. A blank 85 | value always uses the current version. 86 | host (str): the host name of the Glyph Server to connect to. 87 | The default value is 'localhost'. 88 | callback (func): a callback function that takes a string 89 | parameter that represents the response messages from the 90 | Glyph Server. The default callback is None. 91 | prog (str): The batch executable program to use when port is 0. 92 | If prog is None, 'tclsh' will be used on Windows platforms 93 | and 'pointwise -b' will be used for Linux/macOS platforms. 94 | Otherwise the given program will be used to execute the 95 | Pointwise batch mode. 96 | timeout (float): The number of seconds to continue to try to 97 | connect to the server before giving up. The default timeout 98 | is 10 seconds. 99 | """ 100 | self._port = port 101 | self._auth = auth 102 | self._host = host 103 | self._version = version 104 | self._timeout = timeout 105 | 106 | self._socket = None 107 | self._busy = False 108 | self._auth_failed = False 109 | self._serverVersion = None 110 | 111 | if self._port is None: 112 | self._port = os.environ.get('PWI_GLYPH_SERVER_PORT', '2807') 113 | 114 | if self._auth is None: 115 | self._auth = os.environ.get('PWI_GLYPH_SERVER_AUTH', '') 116 | 117 | if self._port == 0: 118 | self._startServer(callback, prog) 119 | 120 | 121 | def __del__(self): 122 | if hasattr(self, "_server") and self._server is not None: 123 | self._server.stdout = None 124 | self._server.stderr = None 125 | self._server.stdin = None 126 | self.close() 127 | 128 | 129 | def __eq__(self, other): 130 | return self is other 131 | 132 | 133 | def __enter__(self): 134 | """ When using GlyphClient as a context manager, connect and return self 135 | """ 136 | if not self.connect(): 137 | if self._auth_failed: 138 | raise GlyphError('connect', 'Glyph Server AUTH failed') 139 | elif self._busy: 140 | raise GlyphError('connect', 'Glyph Server BUSY') 141 | else: 142 | raise GlyphError('connect', 'Could not connect to Glyph Server') 143 | return self 144 | 145 | 146 | def __exit__(self, *args): 147 | """ When using GlyphClient as a context manager, disconnect """ 148 | self.close() 149 | 150 | def __str__(self): 151 | s = 'GlyphClient(' + str(self._host) + '@' + str(self._port) + \ 152 | ') connected=' + str(self.is_connected()) 153 | if self.is_connected(): 154 | s = s + ' Server=' + self._serverVersion 155 | return s 156 | 157 | 158 | def connect(self, retries=None): 159 | """ Connect to a Glyph server at the given host and port. 160 | 161 | Args: 162 | retries (int): the number of times to retry the connection 163 | before giving up. DEPRECATED: if not None, retries will be 164 | used to determine a suitable value for timeout. 165 | 166 | Returns: 167 | bool: True if successfully connected, False otherwise. If an 168 | initial connection is made, but the Glyph Server is busy, 169 | calling is_busy() will return True. 170 | """ 171 | self._closeSocket() 172 | 173 | self._busy = False 174 | self._auth_failed = False 175 | 176 | timeout = self._timeout 177 | if retries is not None: 178 | timeout = 0.1 * retries 179 | 180 | start = time.time() 181 | while self._socket is None and time.time() - start < timeout: 182 | try: 183 | self._socket = self._connect(self._host, self._port) 184 | except: 185 | self._socket = None 186 | if self._socket is None: 187 | time.sleep(0.1) # sleep for a bit before retry 188 | 189 | if self._socket is None: 190 | return False 191 | 192 | self._send('AUTH', self._auth) 193 | 194 | response = self._recv() 195 | if response is None: 196 | self._socket.close() 197 | self._socket = None 198 | return False 199 | 200 | rtype, payload = response 201 | 202 | self._auth_failed = (rtype == 'AUTHFAIL') 203 | self._busy = (rtype == 'BUSY') 204 | 205 | if rtype != 'READY': 206 | self.close() 207 | else: 208 | problem = None 209 | if self._version is not None: 210 | try: 211 | self.control('version', self._version) 212 | except Exception as excCode: 213 | problem = excCode 214 | self._serverVersion = self.eval('pw::Application getVersion') 215 | if problem is not None: 216 | m = re.search('Pointwise V(\d+).(\d+)', self._serverVersion) 217 | if len(m.groups()) == 2: 218 | try: 219 | serverVers = int(m.groups()[0]) * 10 + \ 220 | int(m.groups()[1]) 221 | except: 222 | serverVers = 0 223 | if serverVers >= 183: 224 | self.close() 225 | raise problem 226 | 227 | return self._socket is not None 228 | 229 | 230 | def is_busy(self): 231 | """ Check if the reason for the last failed attempt to connect() was 232 | because the Glyph Server is busy. 233 | 234 | Returns: 235 | bool: True if the last call to connect failed because the 236 | Glyph Server was busy, False otherwise. 237 | """ 238 | return self._busy 239 | 240 | 241 | def auth_failed(self): 242 | """ Check if the reason for the last failed attempt to connect() was 243 | because the Glyph Server authentication failed. 244 | 245 | Returns: 246 | bool: True if the last call to connect failed because the 247 | Glyph Server authentication failed, False otherwise. 248 | """ 249 | return self._auth_failed 250 | 251 | 252 | def eval(self, command): 253 | """ Evaluate a Glyph command on the Glyph Server, including nested 254 | commands and variable substitution. 255 | 256 | Args: 257 | command (str): A Glyph command to be evaluated on the 258 | Glyph Server. 259 | 260 | Returns: 261 | str: The result from evaluating the command on the Glyph Server. 262 | 263 | Raises: 264 | GlyphError: If the command evaluation on the Glyph Server 265 | resulted in an error. 266 | """ 267 | return self._sendcmd('EVAL', command) 268 | 269 | 270 | def command(self, command): 271 | """ Execute a Glyph command on the Glyph Server, with no support 272 | for nested commands and variable substitution. 273 | 274 | Args: 275 | command (str): A JSON encoded string of an array of a 276 | Glyph command and the parameters to be executed. 277 | 278 | Returns: 279 | str: The JSON encoded string result from executing the 280 | command on the Glyph Server. 281 | 282 | Raises: 283 | GlyphError: If the command execution on the Glyph Server 284 | resulted in an error. 285 | """ 286 | return self._sendcmd('COMMAND', command) 287 | 288 | 289 | def control(self, setting, value=None): 290 | """ Send a control message to the Glyph Server 291 | 292 | Args: 293 | setting (str): The name of the control setting 294 | value (str): The value to set the setting to. If none the value 295 | will only be queried. 296 | 297 | Returns: 298 | str: The current value of the control setting 299 | 300 | Raises: 301 | GlyphError: If the control setting or the value is invalid. 302 | """ 303 | command = setting 304 | if value is not None: 305 | command += '=' + value 306 | return self._sendcmd('CONTROL', command) 307 | 308 | 309 | def get_glyphapi(self): 310 | """ Creates and returns a GlyphAPI object for this client 311 | 312 | Returns: 313 | GlyphAPI object for automatic Glyph command execution 314 | through this client connection 315 | """ 316 | from pointwise import glyphapi 317 | if not self.is_connected(): 318 | self.connect() 319 | 320 | if self.is_connected(): 321 | return glyphapi.GlyphAPI(glyph_client=self) 322 | else: 323 | raise GlyphError('GlyphAPI', 324 | 'The client is not connected to a Glyph Server') 325 | 326 | 327 | def is_connected(self): 328 | """ Check if there is a connection to the Glyph Client 329 | 330 | Returns: 331 | True if there is a valid connection, False otherwise. 332 | """ 333 | result = False 334 | try: 335 | result = self._socket is not None and self.ping() 336 | except: 337 | result = False 338 | return result 339 | 340 | 341 | def ping(self): 342 | """ Ping the Glyph Server 343 | 344 | Returns: 345 | bool: True if the the Glyph Server was successfully pinged, 346 | False otherwise. 347 | """ 348 | result = False 349 | try: 350 | result = self._sendcmd('PING', '') == "OK" 351 | except: 352 | result = False 353 | return result 354 | 355 | 356 | def close(self): 357 | """ Close the connection with the Glyph server """ 358 | 359 | if hasattr(self, "_server") and self._server is not None and \ 360 | hasattr(self, "_socket") and self._socket is not None: 361 | try: 362 | # request graceful shutdown of server 363 | self.eval("exit") 364 | except socket.error as err: 365 | pass 366 | 367 | self._closeSocket() 368 | 369 | if hasattr(self, "_server") and self._server is not None: 370 | # safe to call even if server has already shut down 371 | self._server.terminate() 372 | self._server.wait() 373 | 374 | # resource warnings may occur if pipes are not closed from the 375 | # client side (typically on Windows) 376 | if self._server.stdout is not None: 377 | try: 378 | self._server.stdout.close() 379 | self._server.stdout = None 380 | except IOError: 381 | pass 382 | 383 | # wait for the pipe reader thread to finish 384 | if self._othread is not None: 385 | self._othread.join(0.5) 386 | del self._othread 387 | self._othread = None 388 | 389 | if self._server.stdin is not None: 390 | try: 391 | self._server.stdin.close() 392 | self._server.stdin = None 393 | except IOError: 394 | pass 395 | 396 | if self._server.stderr is not None: 397 | try: 398 | self._server.stderr.close() 399 | self._server.stderr = None 400 | except IOError: 401 | pass 402 | 403 | self._server = None 404 | 405 | 406 | def disconnect(self): 407 | """ Close the connection with the Glyph server """ 408 | self.close() 409 | 410 | 411 | def _connect(self, host, port): 412 | """ Helper function for connecting a socket to the given host and port 413 | """ 414 | # try to connect using both IPv4 and IPv6 415 | s = None 416 | for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, 417 | socket.SOCK_STREAM): 418 | af, socktype, proto, canonname, sa = res 419 | try: 420 | s = socket.socket(af, socktype, proto) 421 | s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 422 | except (socket.error, OSError) as msg: 423 | s = None 424 | continue 425 | try: 426 | s.connect(sa) 427 | except (socket.error, OSError) as msg: 428 | s.close() 429 | s = None 430 | continue 431 | break 432 | return s 433 | 434 | 435 | def _sendcmd(self, type, command): 436 | """ Helper function for sending a command to the Glyph Server """ 437 | if self._socket is None: 438 | raise GlyphError(command, 439 | 'The client is not connected to a Glyph Server') 440 | 441 | try: 442 | self._send(type, command) 443 | response = self._recv() 444 | except socket.error as e: 445 | # Python 3: BrokenPipeError 446 | if e.errno == errno.EPIPE: 447 | raise GlyphError(command, 448 | 'The Glyph Server connection is closed') 449 | else: 450 | raise 451 | 452 | if response is None: 453 | if not self._socket.fileno() == -1: 454 | # socket is closed, assume command ended server session 455 | return None 456 | raise GlyphError(command, 'No response from the Glyph Server') 457 | 458 | type, payload = response 459 | if type != 'OK': 460 | raise GlyphError(command, payload) 461 | 462 | return payload 463 | 464 | 465 | def _send(self, type, payload): 466 | """ Helper function for sending a message on the socket """ 467 | message_bytes = type.encode('utf-8').ljust(8) + payload.encode('utf-8') 468 | message_length = struct.pack('!I', len(message_bytes)) 469 | self._socket.sendall(message_length) 470 | self._socket.sendall(message_bytes) 471 | 472 | 473 | def _recv(self): 474 | """ Helper function for receiving a message on the socket """ 475 | message_length = self._recvall(4) 476 | if message_length is None: 477 | return None 478 | 479 | # Python 2: 'bytes' is an alias for 'str' 480 | # Python 3: 'bytes' is an immutable bytearray 481 | message_length = struct.unpack('!I', bytes(message_length))[0] 482 | if message_length == 0: 483 | return ('', '') 484 | 485 | message_bytes = self._recvall(message_length) 486 | if message_bytes is None: 487 | return None 488 | 489 | # Python 2: convert decoded bytes (type 'unicode') to string (type 490 | # 'str') 491 | type = str(message_bytes[0:8].decode('utf-8')).strip() 492 | payload = str(message_bytes[8:].decode('utf-8')) 493 | return (type, payload) 494 | 495 | 496 | def _recvall(self, size): 497 | """ Helper function to recv size bytes or return None if EOF is hit """ 498 | data = bytearray() 499 | while len(data) < size: 500 | packet = self._socket.recv(size - len(data)) 501 | if not packet: 502 | return None 503 | data.extend(packet) 504 | return data 505 | 506 | def puts(self, *args): 507 | self.eval("puts {%s}" % ' '.join(args)) 508 | 509 | 510 | def _closeSocket(self): 511 | """ Close the connection with the Glyph server """ 512 | if self._socket: 513 | if not self._socket.fileno() == -1: 514 | try: 515 | self._socket.shutdown(socket.SHUT_RDWR) 516 | except: 517 | None 518 | self._socket.close() 519 | del self._socket 520 | self._socket = None 521 | 522 | 523 | def _startServer(self, callback, prog): 524 | """ Create a server if possible on any open port """ 525 | self._server = None 526 | self._othread = None 527 | try: 528 | if prog is None: 529 | if platform.system() == 'Windows': 530 | prog = ['tclsh'] 531 | else: 532 | prog = ['pointwise', '-b'] 533 | elif isinstance(prog, tuple): 534 | prog = list(prog) 535 | elif not isinstance(prog, list): 536 | prog = [prog] 537 | 538 | import shutil 539 | if not hasattr(shutil, 'which'): 540 | # Python 3: shutil.which exists 541 | shutil.which = __which__ 542 | 543 | target = shutil.which(prog[0]) 544 | 545 | if target is None: 546 | raise GlyphError('server', '%s not found in path' % prog[0]) 547 | 548 | prog[0] = target 549 | 550 | # find an open port 551 | try: 552 | tsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 553 | tsock.settimeout(0) 554 | tsock.bind(('', 0)) 555 | self._port = tsock.getsockname()[1] 556 | finally: 557 | tsock.close() 558 | time.sleep(0.1) 559 | 560 | if callback is None: 561 | def __default_callback__(*args): 562 | pass 563 | callback = __default_callback__ 564 | 565 | # start the server subprocess 566 | import subprocess 567 | self._server = subprocess.Popen(prog, stdin=subprocess.PIPE, 568 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 569 | 570 | self._server.stdin.write(bytearray(( 571 | "if [catch {package require PWI_Glyph} server_ver] {\n" + 572 | " puts {Could not load Glyph package. Ensure that the " + 573 | "Pointwise version of tclsh is the only one in your PATH.}\n" + 574 | " puts $server_ver\n" + 575 | " exit\n" + 576 | "}\n" + 577 | "if { [package vcompare $server_ver 2.18.2] < 0 } {" + 578 | " puts {Server must be 18.2 or higher}\n" 579 | " exit\n" 580 | "}\n" + 581 | "puts \"GLYPH_SERVER_VERSION: $server_ver\"\n" + 582 | "pw::Script setServerPort %d\n" + 583 | "puts \"Server: [pw::Application getVersion]\"\n" + 584 | "pw::Script processServerMessages -timeout %s\n" + 585 | "puts \"Server: Subprocess completed.\"\n") % 586 | (self._port, str(int(self._timeout))), "utf-8")) 587 | self._server.stdin.flush() 588 | 589 | ver_re = re.compile(r"GLYPH_SERVER_VERSION: (\d+\.\d+\.\d+)") 590 | lic_re = re.compile(r".*unable to obtain a license.*") 591 | 592 | # process output from the server until we can determine 593 | # that the server is running and a valid license was obtained 594 | ver = str(self._server.stdout.readline().decode('utf-8')) 595 | ver_match = ver_re.match(ver) 596 | lic_match = lic_re.match(ver) 597 | 598 | # Read the server output, looking for a special line with the 599 | # server version, or a line with a known license failure message, 600 | # or until EOF. Print all output preceding any of these conditions. 601 | if ver_match is None and lic_match is None: 602 | # Note: not strictly Python 2 compatible, but deemed acceptable 603 | print(ver) 604 | for ver in iter(self._server.stdout.readline, b''): 605 | ver = str(ver.decode('utf-8')) 606 | ver_match = ver_re.match(ver) 607 | if ver_match is not None: 608 | break 609 | lic_match = lic_re.match(ver) 610 | if lic_match is not None: 611 | break 612 | print(ver) 613 | 614 | if lic_match is not None or ver_match is None: 615 | # license or other failure 616 | for line in iter(self._server.stdout.readline, b''): 617 | callback(str(line.decode('utf-8'))) 618 | self.close() 619 | if lic_match is not None: 620 | raise NoLicenseError(ver) 621 | else: 622 | raise GlyphError('server', ver) 623 | 624 | # capture stdout/stderr from the server 625 | import threading 626 | class ReaderThread(threading.Thread): 627 | def __init__(self, ios, callback): 628 | threading.Thread.__init__(self) 629 | self._ios = ios 630 | self._error = None 631 | self._cb = callback 632 | self.daemon = True 633 | 634 | def run(self): 635 | try: 636 | for line in iter(self._ios.readline, b''): 637 | # Python 2: convert decoded bytes to 'str' 638 | self._cb(str(line.decode('utf-8'))) 639 | except Exception as ex: 640 | self._error = str(ex) 641 | self._ios = None 642 | 643 | self._othread = ReaderThread(self._server.stdout, callback) 644 | self._othread.start() 645 | except Exception as ex: 646 | if self._server is not None: 647 | self._server.kill() 648 | self._server.wait() 649 | self._server = None 650 | if self._othread is not None: 651 | self._othread.join(0.5) 652 | if self._othread._error is not None: 653 | callback(self._othread._error) 654 | raise 655 | 656 | 657 | def __which__(cmd, mode=os.F_OK | os.X_OK, path=None): 658 | """Given a command, mode, and a PATH string, return the path which 659 | conforms to the given mode on the PATH, or None if there is no such 660 | file. 661 | `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result 662 | of os.environ.get("PATH"), or can be overridden with a custom search 663 | path. 664 | 665 | Courtesy Python 3.3 source code, for Python 2.x backward compatibility. 666 | """ 667 | 668 | # Check that a given file can be accessed with the correct mode. 669 | # Additionally check that `file` is not a directory, as on Windows 670 | # directories pass the os.access check. 671 | def __access_check__(fn, mode): 672 | return (os.path.exists(fn) and os.access(fn, mode) and 673 | not os.path.isdir(fn)) 674 | 675 | # Short circuit. If we're given a full path which matches the mode 676 | # and it exists, we're done here. 677 | if __access_check__(cmd, mode): 678 | return cmd 679 | 680 | path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep) 681 | 682 | if sys.platform == "win32": 683 | # The current directory takes precedence on Windows. 684 | if os.curdir not in path: 685 | path.insert(0, os.curdir) 686 | 687 | # PATHEXT is necessary to check on Windows. 688 | pathext = os.environ.get("PATHEXT", "").split(os.pathsep) 689 | # See if the given file matches any of the expected path extensions. 690 | # This will allow us to short circuit when given "python.exe". 691 | matches = [cmd for ext in pathext if cmd.lower().endswith(ext.lower())] 692 | # If it does match, only test that one, otherwise we have to try 693 | # others. 694 | files = [cmd] if matches else [cmd + ext.lower() for ext in pathext] 695 | else: 696 | # On other platforms you don't have things like PATHEXT to tell you 697 | # what file suffixes are executable, so just pass on cmd as-is. 698 | files = [cmd] 699 | 700 | seen = set() 701 | for dir in path: 702 | dir = os.path.normcase(dir) 703 | if dir not in seen: 704 | seen.add(dir) 705 | for thefile in files: 706 | name = os.path.join(dir, thefile) 707 | if __access_check__(name, mode): 708 | return name 709 | return None 710 | 711 | ############################################################################# 712 | # 713 | # This file is licensed under the Cadence Public License Version 1.0 (the 714 | # "License"), a copy of which is found in the included file named "LICENSE", 715 | # and is distributed "AS IS." TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE 716 | # LAW, CADENCE DISCLAIMS ALL WARRANTIES AND IN NO EVENT SHALL BE LIABLE TO 717 | # ANY PARTY FOR ANY DAMAGES ARISING OUT OF OR RELATING TO USE OF THIS FILE. 718 | # Please see the License for the full text of applicable terms. 719 | # 720 | ############################################################################# 721 | -------------------------------------------------------------------------------- /pointwise/glyphapi/utilities.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | ############################################################################# 4 | # 5 | # (C) 2021 Cadence Design Systems, Inc. All rights reserved worldwide. 6 | # 7 | # This sample script is not supported by Cadence Design Systems, Inc. 8 | # It is provided freely for demonstration purposes only. 9 | # SEE THE WARRANTY DISCLAIMER AT THE BOTTOM OF THIS FILE. 10 | # 11 | ############################################################################# 12 | 13 | import numpy as np 14 | import math 15 | 16 | 17 | """ 18 | Utility classes that mimic the behavior of the Glyph pwu::* utilites. 19 | """ 20 | 21 | class GlyphUtility: 22 | tolerance = 1e-7 23 | 24 | class Vector2(object): 25 | """ Utility functions for two dimensional vectors, which are represented 26 | as a list of two real values. 27 | """ 28 | 29 | def __init__(self, *args): 30 | """ Construct a 2D vector, all zeroes by default. """ 31 | self.vector_ = None 32 | if len(args) == 0: 33 | self.vector_ = np.zeros([2]) 34 | elif len(args) == 1: 35 | if isinstance(args[0], (list, tuple)) and len(args[0]) == 2: 36 | self.vector_ = np.array([float(i) for i in args[0]]) 37 | elif isinstance(args[0], np.ndarray): 38 | self.vector_ = args[0] 39 | elif isinstance(args[0], Vector2): 40 | self.vector_ = args[0].vector_ 41 | elif len(args) == 2: 42 | self.vector_ = np.array([float(i) for i in args]) 43 | 44 | if self.vector_ is None: 45 | raise ValueError("Invalid parameter %s" % str(args)) 46 | elif self.vector_.size != 2 or self.vector_.ndim != 1: 47 | raise ValueError("Vector2 must be a list/tuple of 2 values") 48 | 49 | def __eq__(self, other): 50 | """ Check for equality of two vectors """ 51 | return self.equal(other) 52 | 53 | def __add__(self, other): 54 | """ Add two vectors together and return the result as a new Vector2 """ 55 | return Vector2(np.add(tuple(self), tuple(other))) 56 | 57 | def __sub__(self, other): 58 | """ Subtract one vector from another and return the result as a 59 | new Vector2. 60 | """ 61 | return Vector2(np.subtract(tuple(self), tuple(other))) 62 | 63 | def __mul__(self, other): 64 | """ Multiply the components of a vector by either the components of 65 | another vector or a scalar and return the result as a new 66 | Vector2. 67 | """ 68 | if isinstance(other, (float, int)): 69 | other = (other, other) 70 | 71 | return Vector2(np.multiply(tuple(self), tuple(other))) 72 | 73 | def __truediv__(self, other): 74 | """ Divide the components of a vector by either the components of 75 | another vector or a scalar and return the result as a new 76 | Vector2. 77 | """ 78 | if isinstance(other, (float,int)): 79 | other = (other, other) 80 | 81 | return Vector2(np.true_divide(tuple(self), tuple(other))) 82 | 83 | def __div__(self, other): 84 | """ Divide the components of a vector by either the components of 85 | another vector or a scalar and return the result as a new 86 | Vector2. 87 | """ 88 | return self.__truediv__(other) 89 | 90 | def __str__(self): 91 | """ Return a string representation of a Vector2 """ 92 | return str(tuple(self)) 93 | 94 | def __iter__(self): 95 | return (e for e in self.vector_) 96 | 97 | def __len__(self): 98 | return len(self.vector_) 99 | 100 | def index(self, i): 101 | """ Return a component of a Vector2 """ 102 | return self.vector_[i] 103 | 104 | @property 105 | def x(self): 106 | """ The X component of a Vector2 """ 107 | return self.vector_[0] 108 | 109 | @x.setter 110 | def x(self, v): 111 | """ Set the X component of a Vector2 """ 112 | self.vector_[0] = v 113 | 114 | @property 115 | def y(self): 116 | """ The X component of a Vector2 """ 117 | return self.vector_[1] 118 | 119 | @y.setter 120 | def y(self, v): 121 | """ Set the Y component of a Vector2 """ 122 | self.vector_[1] = v 123 | 124 | def equal(self, other, tolerance=GlyphUtility.tolerance): 125 | """ Check for equality between two vectors within an optional tolerance 126 | """ 127 | return np.linalg.norm(np.subtract(tuple(self), tuple(other))) \ 128 | <= tolerance 129 | 130 | def notEqual(self, other, tolerance=GlyphUtility.tolerance): 131 | """ Check for inequality between two vectors within an optional 132 | tolerance 133 | """ 134 | return not self.equal(other, tolerance) 135 | 136 | def add(self, vec2): 137 | """ Add the components of two vectors and return the result as a new 138 | Vector2 139 | """ 140 | return self + vec2 141 | 142 | def subtract(self, vec2): 143 | """ Subtract one vector from another and return the result as a new 144 | Vector2 145 | """ 146 | return self - vec2 147 | 148 | def negate(self): 149 | """ Negate the component values and return the result as a new Vector2 150 | """ 151 | return self * -1 152 | 153 | def scale(self, scalar): 154 | """ Scale the component values by a scalar and return the result as 155 | a new Vector2 156 | """ 157 | return self * scalar 158 | 159 | def divide(self, scalar): 160 | """ Divide the component values by a scalar and return the result as 161 | a new Vector2 162 | """ 163 | return self / scalar 164 | 165 | def multiply(self, vec2): 166 | """ Multiply the component values of a vector by the component values 167 | of another vector and return the result as a new Vector2 168 | """ 169 | return self * vec2 170 | 171 | def dot(self, vec2): 172 | """ Return the scalar dot product of two vectors """ 173 | return np.dot(self.vector_, vec2.vector_) 174 | 175 | def normalize(self): 176 | """ Return the normalized form of a vector as a new Vector2 """ 177 | norm = np.linalg.norm(self.vector_) 178 | if norm == 0: 179 | raise ValueError('Vector is zero vector') 180 | else: 181 | return self / norm 182 | 183 | def length(self): 184 | """ Return the scalar length of a vector """ 185 | return np.linalg.norm(self.vector_) 186 | 187 | @staticmethod 188 | def zero(): 189 | """ Return the 2-dimensional zero vector """ 190 | return Vector2(0.0, 0.0) 191 | 192 | @staticmethod 193 | def minimum(vec1, vec2): 194 | """ Return the minimum components of two vectors as a new Vector2 """ 195 | return Vector2(np.minimum(tuple(vec1), tuple(vec2))) 196 | 197 | @staticmethod 198 | def maximum(vec1, vec2): 199 | """ Return the maximum components of two vectors as a new Vector2 """ 200 | return Vector2(np.maximum(tuple(vec1), tuple(vec2))) 201 | 202 | 203 | class Vector3(object): 204 | """ Utility functions for three dimensional vectors, which are represented 205 | as a list of three real values. 206 | """ 207 | def __init__(self, *args): 208 | """ Construct a 3D vector, all zeroes by default. """ 209 | self.vector_ = None 210 | if len(args) == 0: 211 | self.vector_ = np.zeros([3]) 212 | elif len(args) == 1: 213 | if isinstance(args[0], (list, tuple)) and len(args[0]) == 3: 214 | self.vector_ = np.array([float(i) for i in args[0]]) 215 | elif isinstance(args[0], np.ndarray): 216 | self.vector_ = args[0] 217 | elif isinstance(args[0], Vector3): 218 | self.vector_ = args[0].vector_ 219 | elif isinstance(args[0], Vector2): 220 | self.vector_ = args[0].vector_ 221 | self.vector_.append(0.0) 222 | elif len(args) == 3: 223 | self.vector_ = np.array([float(i) for i in args]) 224 | 225 | if self.vector_ is None: 226 | raise ValueError("Invalid parameter %s" % str(args)) 227 | elif self.vector_.size != 3 or self.vector_.ndim != 1: 228 | raise ValueError("Vector3 must be 3 float values") 229 | 230 | def __eq__(self, other): 231 | """ Check for equality of two vectors """ 232 | return self.equal(other) 233 | 234 | def __add__(self, other): 235 | """ Add two vectors together and return the result as a new Vector3. 236 | """ 237 | return Vector3(np.add(tuple(self), tuple(other))) 238 | 239 | def __sub__(self, other): 240 | """ Subtract one vector from another and return the result as a 241 | new Vector3. 242 | """ 243 | return Vector3(np.subtract(tuple(self), tuple(other))) 244 | 245 | def __mul__(self, other): 246 | """ Multiply the components of a vector by either the components of 247 | another vector or a scalar and return the result as a new 248 | Vector3. 249 | """ 250 | if isinstance(other, (float, int)): 251 | other = (other, other, other) 252 | 253 | return Vector3(np.multiply(self.vector_, tuple(other))) 254 | 255 | def __truediv__(self, other): 256 | """ Divide the components of a vector by either the components of 257 | another vector or a scalar and return the result as a new 258 | Vector3. 259 | """ 260 | if isinstance(other, (float, int)) and other != 0.0: 261 | return Vector3(1.0 / other * self.vector_) 262 | else: 263 | return np.true_divide(self.vector_, tuple(other)) 264 | 265 | def __div__(self, other): 266 | """ Divide the components of a vector by either the components of 267 | another vector or a scalar and return the result as a new 268 | Vector3. 269 | """ 270 | return self.__truediv__(other) 271 | 272 | def __str__(self): 273 | """ Return a string representation of a Vector3 """ 274 | return str(tuple(self)) 275 | 276 | def __iter__(self): 277 | return (e for e in self.vector_) 278 | 279 | def __len__(self): 280 | return len(self.vector_) 281 | 282 | def index(self, i): 283 | """ Return a component of a Vector3 """ 284 | return self.vector_[i] 285 | 286 | @property 287 | def x(self): 288 | """ The X component of a Vector3 """ 289 | return self.vector_[0] 290 | 291 | @x.setter 292 | def x(self, v): 293 | """ Set the X component of a Vector3 """ 294 | self.vector_[0] = v 295 | 296 | @property 297 | def y(self): 298 | """ The Y component of a Vector3 """ 299 | return self.vector_[1] 300 | 301 | @y.setter 302 | def y(self, v): 303 | """ Set the Y component of a Vector3 """ 304 | self.vector_[1] = v 305 | 306 | @property 307 | def z(self): 308 | """ The Z component of a Vector3 """ 309 | return self.vector_[2] 310 | 311 | @z.setter 312 | def z(self, v): 313 | """ Set the Z component of a Vector3 """ 314 | self.vector_[2] = v 315 | 316 | def equal(self, other, tolerance=GlyphUtility.tolerance): 317 | """ Check for equality between two vectors within an optional tolerance 318 | """ 319 | return np.linalg.norm(np.subtract(tuple(self), tuple(other))) \ 320 | <= tolerance 321 | 322 | def notEqual(self, other, tolerance=GlyphUtility.tolerance): 323 | """ Check for inequality between two vectors within an optional 324 | tolerance 325 | """ 326 | return not self.equal(other, tolerance) 327 | 328 | def add(self, other): 329 | """ Add the components of two vectors and return the result as a new 330 | Vector3 331 | """ 332 | return self + other 333 | 334 | def subtract(self, other): 335 | """ Subtract one vector from another and return the result as a new 336 | Vector3 337 | """ 338 | return self - other 339 | 340 | def negate(self): 341 | """ Negate the component values and return the result as a new Vector3 342 | """ 343 | return self * -1.0 344 | 345 | def scale(self, scalar): 346 | """ Scale the component values by a scalar and return the result as 347 | a new Vector3 348 | """ 349 | return self * scalar 350 | 351 | def divide(self, scalar): 352 | """ Divide the component values by a scalar and return the result as 353 | a new Vector3 354 | """ 355 | return self / scalar 356 | 357 | def multiply(self, scalar): 358 | """ Multiply the component values of a vector by the component values 359 | of another vector and return the result as a new Vector3 360 | """ 361 | return self * scalar 362 | 363 | def cross(self, other): 364 | """ Return the cross product of two vectors as a new Vector3 """ 365 | return Vector3(np.cross(self.vector_, tuple(other))) 366 | 367 | def dot(self, other): 368 | """ Return the scalar dot product of two vectors """ 369 | return np.dot(self.vector_, tuple(other)) 370 | 371 | def normalize(self): 372 | """ Return the normalized form of a vector as a new Vector3 """ 373 | norm = np.linalg.norm(self.vector_) 374 | if norm == 0: 375 | raise ValueError('Vector is zero vector') 376 | else: 377 | return self / norm 378 | 379 | def length(self): 380 | """ Return the scalar length of a vector """ 381 | return np.linalg.norm(self.vector_) 382 | 383 | def distanceToLine(self, pt, direction): 384 | """ Return the scalar distance from a vector to a line defined 385 | by a point and direction. 386 | """ 387 | lineVec = Vector3(direction).normalize() 388 | ptVec = self - Vector3(pt) 389 | ptProj = lineVec * ptVec.dot(lineVec) 390 | return (ptVec - ptProj).length() 391 | 392 | @staticmethod 393 | def zero(): 394 | """ Return the 3-dimensional zero vector """ 395 | return Vector3(0.0, 0.0, 0.0) 396 | 397 | @staticmethod 398 | def minimum(vec1, vec2): 399 | """ Return the minimum components of two vectors as a new Vector3 """ 400 | return Vector3(np.minimum(tuple(vec1), tuple(vec2))) 401 | 402 | @staticmethod 403 | def maximum(vec1, vec2): 404 | """ Return the maximum components of two vectors as a new Vector3 """ 405 | return Vector3(np.maximum(tuple(vec1), tuple(vec2))) 406 | 407 | @staticmethod 408 | def affine(scalar, vec1, vec2): 409 | """ Return a new Vector3 that is the affine combination of two vectors 410 | """ 411 | return Vector3(np.add( 412 | np.multiply(tuple(vec1), (scalar, scalar, scalar)), 413 | np.multiply(tuple(vec2), (1.0-scalar, 1.0-scalar, 1.0-scalar)))) 414 | 415 | @staticmethod 416 | def barycentric(pt, vec1, vec2, vec3, clamp=False): 417 | """ Return a new Vector3 that has the barycentric coordinates of 418 | the given point in the frame of the given three vectors. 419 | """ 420 | pt = Vector3(pt) 421 | v1 = Vector3(vec1) 422 | v2 = Vector3(vec2) 423 | v3 = Vector3(vec3) 424 | a = b = 0.0 425 | 426 | v12 = v2 - v1 427 | v13 = v3 - v1 428 | 429 | cross = v12.cross(v13) 430 | area = 0.5 * cross.length() 431 | 432 | if area == 0.0: 433 | mode = 23 434 | len12 = v12.length() 435 | len13 = v13.length() 436 | len23 = (v2 - v3).length() 437 | if len12 >= len13: 438 | if len12 >= len23: 439 | mode = 11 if len12 == 0.0 else 12 440 | elif len13 >= len23: 441 | mode = 13 442 | 443 | if mode == 12: 444 | a = (pt - v2).length() / len12 445 | b = 1.0 - a 446 | elif mode == 13: 447 | a = (pt - v3).length() / len13 448 | b = 0.0 449 | elif mode == 23: 450 | a = 0.0 451 | b = (pt - v3).legnth() / len23 452 | else: # mode = 11 453 | a = 1.0 454 | b = 0.0 455 | else: 456 | cross1 = (v2 - pt).cross(v3 - pt) 457 | a = 0.5 * cross1.length() / area 458 | if cross.dot(cross1) < 0.0: 459 | a = -a 460 | cross2 = (pt - v1).cross(v13) 461 | b = 0.5 * cross2.length() / area 462 | if cross.dot(cross2) < 0.0: 463 | b = -b 464 | 465 | a = 0.0 if clamp and a < 0.0 else a 466 | b = 0.0 if clamp and b < 0.0 else b 467 | b = 1.0 - a if clamp and (a + b) > 1.0 else b 468 | 469 | return Vector3((a, b, 1.0 - a - b)) 470 | 471 | 472 | class Quaternion(object): 473 | """ Utility functions for quaternions, which are represented as a list 474 | of four real values (x, y, z, and angle) 475 | """ 476 | 477 | def __init__(self, axis=(0.0, 0.0, 0.0), angle=0, _quat=None): 478 | """ Construct a quaternion, with a degenerate axis and zero angle 479 | by default 480 | """ 481 | if _quat is not None: 482 | self.quat_ = _quat 483 | # normalize 484 | self.quat_ /= np.sqrt(sum(k*k for k in self.quat_)) 485 | self.angle_ = np.degrees(np.arccos(self.quat_[0]) * 2) 486 | self.axis_ = Vector3(self.quat_[1:4]) 487 | self.axis_ = self.axis_ / np.sin(np.radians(self.angle_ / 2)) 488 | elif len(axis) == 3: 489 | if not isinstance(axis, Vector3): 490 | axis = Vector3(axis) 491 | if axis.length() == 0.0 or angle == 0.0: 492 | self.quat_ = (1.0, 0.0, 0.0, 0.0) 493 | self.angle_ = 0.0 494 | self.axis_ = axis 495 | else: 496 | length = axis.length() 497 | 498 | # clamp the angle between -2*pi and +2*pi 499 | while angle < -360.0: 500 | angle += 360.0 501 | while angle > 360.0: 502 | angle -= 360.0 503 | 504 | if angle == 90.0 or angle == -270.0: 505 | w = math.sqrt(0.5) 506 | u = w / length 507 | elif angle == 180.0 or angle == -180.0: 508 | w = 0.0 509 | u = 1.0 / length 510 | elif angle == -90.0 or angle == 270.0: 511 | w = math.sqrt(0.5) 512 | u = -w / length 513 | else: 514 | angle = angle / 2.0 515 | w = np.cos(np.radians(angle)) 516 | u = np.sin(np.radians(angle)) / length 517 | 518 | # normalize, since that's what Glyph does 519 | self.quat_ = (w, u*axis.x, u*axis.y, u*axis.z) 520 | self.quat_ /= np.sqrt(sum(k*k for k in self.quat_)) 521 | 522 | angleRadians = np.arccos(w) * 2.0 523 | self.angle_ = np.degrees(angleRadians) 524 | self.axis_ = Vector3(self.quat_[1:4]) / np.sin(angleRadians/2.0) 525 | else: 526 | raise ValueError("Invalid parameter") 527 | 528 | if len(self.quat_) != 4: 529 | raise ValueError("Quaternion must be list/tuple of 4 values") 530 | 531 | def __mul__(self, other): 532 | """ Return the quaternion product of two quaternions as a new Quaternion 533 | """ 534 | if isinstance(other, (float, int)): 535 | return Quaternion(_quat=(self.quat_ * other)) 536 | else: 537 | w0, x0, y0, z0 = self.quat_ 538 | w1, x1, y1, z1 = other.quat_ 539 | quatProduct = np.array( 540 | [w1*w0 - x1*x0 - y1*y0 - z1*z0, \ 541 | w1*x0 + x1*w0 + y1*z0 - z1*y0, \ 542 | w1*y0 + y1*w0 + z1*x0 - x1*z0, \ 543 | w1*z0 + z1*w0 + x1*y0 - y1*x0]) 544 | return Quaternion(_quat=quatProduct) 545 | 546 | def __truediv__(self, other): 547 | """ Return the quaternion divided by a scalar as a new Quaternion """ 548 | other = (other, other, other, other) 549 | return Quaternion(_quat=(np.true_divide(self.quat_, other))) 550 | 551 | def __div__(self, other): 552 | return self.__truediv__(other) 553 | 554 | def __str__(self): 555 | """ Return the string form of a Quaternion as axis and angle """ 556 | return "(%s, %f)" % (str(tuple(self.axis_)), self.angle_) 557 | 558 | def __iter__(self): 559 | return (e for e in (tuple(self.axis_) + (self.angle_,))) 560 | 561 | @property 562 | def axis(self): 563 | """ Return the rotation axis of a quaternion as a Vector3 """ 564 | return self.axis_ 565 | 566 | @property 567 | def angle(self): 568 | """ Return the scalar rotation angle in degrees """ 569 | return self.angle_ 570 | 571 | def equal(self, quat2): 572 | """ Compare two quaternions for equality """ 573 | return np.array_equal(self.quat_, quat2.quat_) 574 | 575 | def notEquals(self, quat2): 576 | """ Compare two quaternions for inequality """ 577 | return not self.equal(quat2) 578 | 579 | def rotate(self, quat2): 580 | """ Rotate a quaternion by another quaternion and return the result 581 | as a new Quaternion 582 | """ 583 | return self * quat2 584 | 585 | def conjugate(self): 586 | """ Return the conjugate of a quaternion as a new Quaternion """ 587 | return Quaternion(_quat=np.append(self.quat_[0], -1 * self.quat_[1:4])) 588 | 589 | def norm(self): 590 | """ Return the scalar normal for a quaternion """ 591 | return np.linalg.norm(self.quat_) 592 | 593 | def inverse(self): 594 | """ Return the inverse of a quaternion as a new Quaternion """ 595 | conj = self.conjugate().quat_ 596 | norm2 = np.square(self.norm()) 597 | return Quaternion(_quat=(conj / norm2)) 598 | 599 | def normalize(self): 600 | """ Return the normalized version of a quaternion as a new Quaternion. 601 | Note: When constructed with an arbitrary axis and angle, a 602 | Quaternion is already normalized by default. 603 | """ 604 | return self / self.norm() 605 | 606 | def asTransform(self): 607 | """ Compute and return a rotation Transform from a quaternion. 608 | This produces an exact Cartesian transformation when a quaternion 609 | axis is aligned with a Cartesian axis. 610 | """ 611 | w, x, y, z = self.quat_ 612 | q = x * x + y * y + z * z + w * w 613 | s = (2.0 / q) if q > 0.0 else 0.0 614 | xs = x * s 615 | ys = y * s 616 | zs = z * s 617 | wx = w * xs 618 | wy = w * ys 619 | wz = w * zs 620 | xx = x * xs 621 | xy = x * ys 622 | xz = x * zs 623 | yy = y * ys 624 | yz = y * zs 625 | zz = z * zs 626 | 627 | m = np.zeros([16]) 628 | 629 | # In Glyph order 630 | 631 | # If along a Cartesian axis, snap to -1, 0, and 1 if within tolerance 632 | if (x == 0.0 and y == 0.0) or (x == 0.0 and z == 0.0) or \ 633 | (y == 0.0 and z == 0.0): 634 | m[ 0] = _clamp(1.0 - (yy + zz), 1.0, -1.0, 1e-15) 635 | m[ 1] = _clamp(xy + wz, 1.0, -1.0, 1e-15) 636 | m[ 2] = _clamp(xz - wy, 1.0, -1.0, 1e-15) 637 | m[ 3] = 0.0 638 | m[ 4] = _clamp(xy - wz, 1.0, -1.0, 1e-15) 639 | m[ 5] = _clamp(1.0 - (xx + zz), 1.0, -1.0, 1e-15) 640 | m[ 6] = _clamp(yz + wx, 1.0, -1.0, 1e-15) 641 | m[ 7] = 0.0 642 | m[ 8] = _clamp(xz + wy, 1.0, -1.0, 1e-15) 643 | m[ 9] = _clamp(yz - wx, 1.0, -1.0, 1e-15) 644 | m[10] = _clamp(1.0 - (xx + yy), 1.0, -1.0, 1e-15) 645 | m[11] = 0.0 646 | m[12] = 0.0 647 | m[13] = 0.0 648 | m[14] = 0.0 649 | m[15] = 1.0 650 | else: 651 | m[ 0] = 1.0 - (yy + zz) 652 | m[ 1] = xy + wz 653 | m[ 2] = xz - wy 654 | m[ 3] = 0.0 655 | m[ 4] = xy - wz 656 | m[ 5] = 1.0 - (xx + zz) 657 | m[ 6] = yz + wx 658 | m[ 7] = 0.0 659 | m[ 8] = xz + wy 660 | m[ 9] = yz - wx 661 | m[10] = 1.0 - (xx + yy) 662 | m[11] = 0.0 663 | m[12] = 0.0 664 | m[13] = 0.0 665 | m[14] = 0.0 666 | m[15] = 1.0 667 | 668 | # Python order 669 | m = m.reshape((4,4)).transpose() 670 | 671 | return Transform(m) 672 | 673 | class Plane(object): 674 | """ Utility functions for infinite planes, which are represented as a list 675 | of four plane coefficient values. 676 | """ 677 | def __init__(self, **kargs): 678 | if len(kargs) == 1 and 'coeffs' in kargs: 679 | coeffs = kargs['coeffs'] 680 | if not isinstance(coeffs, (list, tuple)) or len(coeffs) != 4: 681 | raise ValueError("Coefficients must be a list of 4 values") 682 | self.normal_ = Vector3(coeffs[0:3]).normalize() 683 | self.d_ = coeffs[3] 684 | elif len(kargs) == 2 and set(kargs.keys()) == set(('normal', 'origin')): 685 | self.normal_ = Vector3(kargs['normal']).normalize() 686 | self.d_ = self.normal_.dot(Vector3(kargs['origin'])) 687 | elif len(kargs) == 3 and set(kargs.keys()) == set(('p1', 'p2', 'p3')): 688 | v0 = Vector3(kargs['p1']) 689 | v1 = Vector3(kargs['p2']) 690 | v2 = Vector3(kargs['p3']) 691 | self.normal_ = (((v1 - v0).normalize()).cross( 692 | (v2 - v0).normalize())).normalize() 693 | self.d_ = self.normal_.dot(v0) 694 | elif len(kargs) == 4 and set(kargs.keys()) == set(('A', 'B', 'C', 'D')): 695 | self.normal_ = \ 696 | Vector3((kargs['A'], kargs['B'], kargs['C'])).normalize() 697 | self.d_ = kargs['D'] 698 | else: 699 | raise ValueError('Plane must be initialized with coefficient ' + 700 | 'list, point and normal, three points (p1, p2, p3), or ' + 701 | 'coefficients (A, B, C, D)') 702 | 703 | def __iter__(self): 704 | return (e for e in (tuple(self.normal_) + (self.d_,))) 705 | 706 | def __str__(self): 707 | """ Return the string form of a plane represented as a tuple of 708 | the four plane coefficients 709 | """ 710 | return str(tuple(self)) 711 | 712 | def equation(self): 713 | """ Return the plane equation as tuple of the four plane coefficients 714 | """ 715 | return tuple(self) 716 | 717 | @property 718 | def A(self): 719 | """ Return the A plane coefficient """ 720 | return self.normal_.vector_[0] 721 | 722 | @property 723 | def B(self): 724 | """ Return the B plane coefficient """ 725 | return self.normal_.vector_[1] 726 | 727 | @property 728 | def C(self): 729 | """ Return the C plane coefficient """ 730 | return self.normal_.vector_[2] 731 | 732 | @property 733 | def D(self): 734 | """ Return the D plane coefficient """ 735 | return self.d_ 736 | 737 | def normal(self): 738 | """ Return the plane normal as a tuple """ 739 | return tuple(self.normal_) 740 | 741 | def constant(self): 742 | """ Return the scalar plane constant """ 743 | return self.d_ 744 | 745 | def inHalfSpace(self, vec): 746 | """ Check if a point is in the positive half space of a plane """ 747 | return self.normal_.dot(Vector3(vec)) >= self.d_ 748 | 749 | def distance(self, vec): 750 | """ Return the positive scalar distance of a point to a plane """ 751 | return math.fabs(self.normal_.dot(Vector3(vec)) - self.d_) 752 | 753 | def line(self, origin, direction): 754 | """ Return the intersection of a line represented as a point and 755 | distance to a plane. If the line does not intersect the plane, 756 | an exception is raised. 757 | """ 758 | if not isinstance(origin, Vector3): 759 | origin = Vector3(origin) 760 | if not isinstance(direction, Vector3): 761 | direction = Vector3(direction).normalize() 762 | 763 | den = self.normal_.dot(direction) 764 | if den < 1e-10 and den > -1e-10: 765 | raise ValueError("Line does not intersect plane") 766 | 767 | s = (self.d_ - self.normal_.dot(origin)) / den 768 | 769 | return origin + direction * s 770 | 771 | def segment(self, p1, p2): 772 | """ Return the intersection of a line represented as two points 773 | to a plane as a new Vector3. If the segment does not 774 | intersect the plane, an exception is raised. 775 | """ 776 | if not isinstance(p1, Vector3): 777 | p1 = Vector3(p1) 778 | if not isinstance(p2, Vector3): 779 | p2 = Vector3(p2) 780 | 781 | ndp1 = self.normal_.dot(p1) 782 | ndp2 = self.normal_.dot(p2) 783 | 784 | if ((ndp1 < self.d_ and ndp2 < self.d_) or \ 785 | (ndp1 > self.d_ and ndp2 > self.d_)): 786 | raise ValueError("Segment does not intersect plane") 787 | 788 | return self.line(p1, tuple(p2 - p1)) 789 | 790 | def project(self, pt): 791 | """ Return the closest point projection of a point onto a plane 792 | as a new Vector3 793 | """ 794 | if not isinstance(pt, Vector3): 795 | pt = Vector3(pt) 796 | return pt + self.normal_ * (self.d_ - pt.dot(self.normal_)) 797 | 798 | 799 | class Extents(object): 800 | """ Utility functions for extent boxes, which are represented as a 801 | list of two vectors (the min and max of the box). 802 | """ 803 | def __init__(self, *args): 804 | """ Construct an extent box with the given min/max or None. 805 | Extents((xmin, ymin, zmin), (xmax, ymax, zmax)) 806 | Extents(Vector3, Vector3) 807 | Extents(xmin, ymin, zmin, xmax, ymax, zmax) 808 | """ 809 | self.box_ = None 810 | if len(args) == 1: 811 | if isinstance(args[0], Extents): 812 | self.box_ = args[0].box_ 813 | else: 814 | self.box_ = np.array(args[0]) 815 | elif len(args) == 2: 816 | if isinstance(args[0], Vector3) and isinstance(args[1], Vector3): 817 | self.box_ = np.array([args[0].vector_, args[1].vector_]) 818 | elif isinstance(args[0], (list, tuple, np.ndarray)) and \ 819 | isinstance(args[1], (list, tuple, np.ndarray)): 820 | self.box_ = np.array([args[0], args[1]]) 821 | else: 822 | raise ValueError("Invalid argument %s" % str(args)) 823 | elif len(args) == 3: 824 | self.box_ = np.array([args, args]) 825 | elif len(args) == 6: 826 | self.box_ = np.array(args) 827 | self.box_.shape = [2,3] 828 | elif len(args) != 0: 829 | raise ValueError("Invalid argument %s" % str(args)) 830 | 831 | if self.box_ is not None: 832 | if self.box_.size == 3 and self.box_.ndim == 1: 833 | self.box_ = np.array([self.box_, self.box_]) 834 | if self.box_.size != 6 or self.box_.ndim != 2: 835 | raise ValueError("Extent box must be 2x3 matrix") 836 | elif not np.less_equal(self.box_[0], self.box_[1]).all(): 837 | raise ValueError("Min must be less than or equal to Max") 838 | 839 | def __repr__(self): 840 | return str(self.box_) 841 | 842 | def __iter__(self): 843 | if self.box_ is not None: 844 | return (e for e in self.box_.flatten()) 845 | 846 | def __eq__(self, other): 847 | """ Check for equality of two extent boxes """ 848 | if self.box_ is not None: 849 | try: 850 | return np.array_equal(self.box_, other.box_) 851 | except: 852 | return np.array_equal(self.box_, other) 853 | else: 854 | return False 855 | 856 | def minimum(self): 857 | """ Return the minimum corner point of an extent box """ 858 | if self.box_ is None: 859 | raise ValueError("Self is empty") 860 | return self.box_[0] 861 | 862 | def maximum(self): 863 | """ Return the maximum corner point of an extent box """ 864 | if self.box_ is None: 865 | raise ValueError("Self is empty") 866 | return self.box_[1] 867 | 868 | def isEmpty(self): 869 | """ Check if an extent box is empty """ 870 | return self.box_ is None 871 | 872 | def diagonal(self): 873 | """ Return the length of the diagonal of an extent box """ 874 | if self.box_ is not None: 875 | return np.linalg.norm(self.box_[0] - self.box_[1]) 876 | else: 877 | return 0.0 878 | 879 | def enclose(self, pt): 880 | """ Return a new Extents that encloses the given point """ 881 | if isinstance(pt, Vector3): 882 | pt = pt.vector_ 883 | if self.box_ is None: 884 | return Extents([np.array(pt), np.array(pt)]) 885 | else: 886 | return Extents([np.minimum(self.box_[0], pt), 887 | np.maximum(self.box_[1], pt)]) 888 | 889 | def expand(self, value): 890 | """ Return a new Extents box that is expanded by the given amount 891 | at both minimum and maximum corners 892 | """ 893 | return Extents([self.box_[0] - value, self.box_[1] + value]) 894 | 895 | def isIntersecting(self, other): 896 | """ Return true if two extent boxes intersect or share a corner, edge 897 | or face 898 | """ 899 | if isinstance(other, (list, tuple)): 900 | other = Extents(other) 901 | elif not isinstance(other, Extents): 902 | raise ValueError("Invalid argument") 903 | elif self.box_ is None: 904 | return False 905 | 906 | return (max(self.box_[0, 0], other.box_[0, 0]) <= \ 907 | min(self.box_[1, 0], other.box_[1, 0])) and \ 908 | (max(self.box_[0, 1], other.box_[0, 1]) <= \ 909 | min(self.box_[1, 1], other.box_[1, 1])) and \ 910 | (max(self.box_[0, 2], other.box_[0, 2]) <= \ 911 | min(self.box_[1, 2], other.box_[1, 2])) 912 | 913 | def isInside(self, pt, tol=0.0): 914 | """ Return true if a point is within an extent box, within an 915 | optional tolerance 916 | """ 917 | if self.box_ is None: 918 | return False 919 | 920 | pt = tuple(pt) 921 | 922 | return (self.box_[0, 0] + tol) <= pt[0] and \ 923 | (self.box_[1, 0] - tol) >= pt[0] and \ 924 | (self.box_[0, 1] + tol) <= pt[1] and \ 925 | (self.box_[1, 1] - tol) >= pt[1] and \ 926 | (self.box_[0, 2] + tol) <= pt[2] and \ 927 | (self.box_[1, 2] - tol) >= pt[2] 928 | 929 | def translate(self, offset): 930 | """ Return a new Extents object that is translated by the given offset 931 | """ 932 | if self.box_ is None: 933 | raise ValueError("Self is empty") 934 | elif isinstance(offset, Vector3): 935 | offset = tuple(offset) 936 | 937 | return Extents([np.add(self.box_[0], tuple(offset)), 938 | np.add(self.box_[1], tuple(offset))]) 939 | 940 | def rotate(self, quat): 941 | """ Return a new Extents object that is rotated by the given Quaternion 942 | """ 943 | if self.box_ is None: 944 | raise ValueError("Self is empty") 945 | elif not isinstance(quat, Quaternion): 946 | raise ValueError("quat is not a Quaternion") 947 | 948 | xform = quat.asTransform() 949 | result = Extents() 950 | 951 | obox = self.box_ 952 | 953 | for i in (0,1): 954 | for j in (0,1): 955 | for k in (0,1): 956 | p = xform.apply((obox[i, 0], obox[j, 1], obox[k, 2])) 957 | result = result.enclose(p) 958 | 959 | return result 960 | 961 | def center(self): 962 | """ Return the center point of the extent box 963 | """ 964 | if self.box_ is None: 965 | raise ValueError("Self is empty") 966 | return (self.box_[0] / 2.0) + (self.box_[1] / 2.0) 967 | 968 | def minimumSide(self): 969 | """ Return the length of the shortest side of the box 970 | """ 971 | if self.box_ is None: 972 | raise ValueError("Self is empty") 973 | return min(self.box_[0, 0]-self.box_[1, 0], 974 | self.box_[0, 1]-self.box_[1, 1], 975 | self.box_[0, 2], self.box_[1, 2]) 976 | 977 | def maximumSide(self): 978 | """ Return the length of the longest side of the box 979 | """ 980 | if self.box_ is None: 981 | raise ValueError("Self is empty") 982 | return max(self.box_[0, 0]-self.box_[1, 0], 983 | self.box_[0, 1]-self.box_[1, 1], 984 | self.box_[0, 2], self.box_[1, 2]) 985 | 986 | 987 | class Transform(object): 988 | """ Utility functions for transform matrices, which are represented 989 | as a list of sixteen real values. The matrix is represented 990 | in a column-first order, which matches the order used in Glyph. 991 | """ 992 | @staticmethod 993 | def identity(): 994 | """ Return an identity Transform """ 995 | return Transform(list(np.eye(4))) 996 | 997 | def __init__(self, matrix=None): 998 | """ Construct a Transform from an optional set of 16 real values. 999 | If an argument is not supplied, set to the identity matrix. 1000 | """ 1001 | if matrix is None: 1002 | self.xform_ = Transform.identity().xform_ 1003 | return 1004 | elif isinstance(matrix, Quaternion): 1005 | self.xform_ = matrix.asTransform().xform_ 1006 | return 1007 | elif isinstance(matrix, Transform): 1008 | self.xform_ = matrix.xform_ 1009 | return 1010 | elif isinstance(matrix, np.matrix): 1011 | self.xform_ = matrix.A 1012 | return 1013 | elif isinstance(matrix, np.ndarray): 1014 | self.xform_ = matrix 1015 | if matrix.size == 16: 1016 | self.xform_ = self.xform_.reshape((4,4)) 1017 | return 1018 | elif not isinstance(matrix, (list, tuple)): 1019 | raise ValueError("Invalid argument") 1020 | 1021 | # Assume that the incoming transformation matrix is in Glyph order 1022 | # (column-first), and transpose to row-first for mathematical 1023 | # operations 1024 | if len(matrix) == 4: 1025 | # Assume a list/tuple of 4 lists/tuples 1026 | self.xform_ = np.array(matrix).transpose() 1027 | elif len(matrix) == 16: 1028 | self.xform_ = np.array(matrix).reshape((4,4)).transpose() 1029 | else: 1030 | raise ValueError("Invalid xform matrix") 1031 | 1032 | def __eq__(self, other): 1033 | """ Compare equality of two transforms """ 1034 | if isinstance(other, Transform): 1035 | return np.array_equal(self.xform_, other.xform_) 1036 | elif isinstance(other, (list, tuple, np.matrix, np.ndarray)): 1037 | return np.array_equal(self.xform_, Transform(other).xform_) 1038 | else: 1039 | raise ValueError("Invalid argument") 1040 | 1041 | def __str__(self): 1042 | """ Return the string representation of a Transform, as a one- 1043 | dimensional array of floats in column-wise order 1044 | """ 1045 | return str(tuple(self)) 1046 | 1047 | def __iter__(self): 1048 | # account for transposed glyph notation 1049 | return (e for e in self.xform_.transpose().flatten()) 1050 | 1051 | @property 1052 | def matrix(self): 1053 | return list(self) 1054 | 1055 | def element(self, i, j): 1056 | """ Get an element of a transform matrix with i, j in Glyph order """ 1057 | return self.xform_.transpose()[i, j] 1058 | 1059 | @staticmethod 1060 | def translation(offset): 1061 | """ Return a new Transform that is a translation by the given offset """ 1062 | return Transform.identity().translate(offset) 1063 | 1064 | def translate(self, offset): 1065 | """ Return a new Transform that adds translation to an existing 1066 | Transform 1067 | """ 1068 | if not isinstance(offset, Vector3): 1069 | offset = Vector3(offset) 1070 | col = np.dot(self.xform_, np.array(tuple(offset.vector_) + (1.0,))) 1071 | xf = np.array(self.xform_) 1072 | np.put(xf, [3, 7, 11, 15], col) 1073 | return Transform(xf) 1074 | 1075 | @staticmethod 1076 | def rotation(axis, angle, anchor=None): 1077 | """ Return a new Transform that is a rotation by the given angle 1078 | about the given axis at the (optional) given anchor point 1079 | """ 1080 | return Transform.identity().rotate(axis, angle, anchor) 1081 | 1082 | def rotate(self, axis, angle, anchor=None): 1083 | """ Return a new Transform that adds rotation to a Transform by 1084 | the given angle about the given axis at the (optional) given 1085 | anchor point 1086 | """ 1087 | if not isinstance(axis, Vector3): 1088 | axis = Vector3(axis) 1089 | if anchor is not None: 1090 | if not isinstance(anchor, Vector3): 1091 | anchor = Vector3(anchor) 1092 | axform = Transform.translation(anchor) 1093 | axform = axform.rotate(axis, angle) 1094 | axform = axform.translate(Vector3()-anchor) 1095 | return Transform(np.dot(self.xform_, axform.xform_)) 1096 | 1097 | # handle Cartesian rotations 1098 | cartesian = False 1099 | if axis.x == 0.0 and axis.y == 0.0: 1100 | # Axis is 0 0 Z 1101 | cartesian = True 1102 | ct1 = 0 1103 | ct2 = 5 1104 | pst = 1 1105 | nst = 4 1106 | if axis.z > 0.0: 1107 | angle = -angle 1108 | elif axis.x == 0.0 and axis.z == 0.0: 1109 | # Axis is 0 Y 0 1110 | cartesian = True 1111 | ct1 = 0 1112 | ct2 = 10 1113 | pst = 2 1114 | nst = 8 1115 | if axis.y < 0.0: 1116 | angle = -angle 1117 | elif axis.y == 0.0 and axis.z == 0.0: 1118 | # Axis X 0 0 1119 | cartesian = True 1120 | ct1 = 5 1121 | ct2 = 10 1122 | pst = 9 1123 | nst = 6 1124 | if axis.x < 0.0: 1125 | angle = -angle 1126 | 1127 | if cartesian: 1128 | absAngle = math.fmod(math.fabs(angle), 360.0) 1129 | 1130 | if absAngle == 90.0: 1131 | ca = 0.0 1132 | sa = 1.0 1133 | elif absAngle == 180.0: 1134 | ca = -1.0 1135 | sa = 0.0 1136 | elif absAngle == 270.0: 1137 | ca = 0.0 1138 | sa = -1.0 1139 | else: 1140 | ca = math.cos(math.radians(absAngle)) 1141 | sa = math.sin(math.radians(absAngle)) 1142 | 1143 | if angle < 0.0: 1144 | sa = -sa 1145 | 1146 | mat = np.eye(4).flatten() 1147 | mat[ct1] = ca 1148 | mat[nst] = -sa 1149 | mat[pst] = sa 1150 | mat[ct2] = ca 1151 | 1152 | rxform = Transform(mat) 1153 | else: 1154 | rxform = Quaternion(axis, angle).asTransform() 1155 | 1156 | return Transform(np.dot(self.xform_, rxform.xform_)) 1157 | 1158 | @staticmethod 1159 | def scaling(scale, anchor=None): 1160 | """ Return a new Transform that is a scale by the given factor 1161 | (which can be a scalar or a three-dimensional vector) about 1162 | an optional anchor point 1163 | """ 1164 | return Transform.identity().scale(scale, anchor) 1165 | 1166 | def scale(self, scale, anchor=None): 1167 | """ Return a new Transform that adds scaling to a Transform by 1168 | the given factor (which can be a scalar or a three- 1169 | dimensional vector) about an optional anchor point 1170 | """ 1171 | if isinstance(scale, (float, int)): 1172 | scale = ((float(scale), float(scale), float(scale))) 1173 | else: 1174 | scale = tuple(scale) 1175 | 1176 | if anchor is not None: 1177 | if not isinstance(anchor, Vector3): 1178 | anchor = Vector3(anchor) 1179 | axform = Transform.translation(anchor) 1180 | axform = axform.scale(scale) 1181 | axform = axform.translate(Vector3()-anchor) 1182 | return Transform(np.dot(self.xform_, axform.xform_)) 1183 | 1184 | # transpose to make slicing easier 1185 | xf = self.xform_.transpose() 1186 | xf[0][0:3] = np.multiply(xf[0][0:3], scale) 1187 | xf[1][0:3] = np.multiply(xf[1][0:3], scale) 1188 | xf[2][0:3] = np.multiply(xf[2][0:3], scale) 1189 | xf = xf.transpose() 1190 | 1191 | return Transform(xf) 1192 | 1193 | @staticmethod 1194 | def calculatedScaling(anchor, start, end, tol=0.0): 1195 | """ Return a transform matrix that scales a given point from 1196 | one location to another anchored at a third point 1197 | """ 1198 | if isinstance(anchor, (list, tuple)): 1199 | anchor = Vector3(anchor) 1200 | if isinstance(start, (list, tuple)): 1201 | start = Vector3(start) 1202 | if isinstance(end, (list, tuple)): 1203 | end = Vector3(end) 1204 | 1205 | fac = Vector3() 1206 | 1207 | da0 = start - anchor 1208 | da1 = end - anchor 1209 | fac.x = 1.0 if math.fabs(da0.x) < tol else (da1.x / da0.x) 1210 | fac.y = 1.0 if math.fabs(da0.y) < tol else (da1.y / da0.y) 1211 | fac.z = 1.0 if math.fabs(da0.z) < tol else (da1.z / da0.z) 1212 | 1213 | end1 = ((start - anchor) * fac) + anchor 1214 | 1215 | da1 = end1 - anchor 1216 | fac.x = 1.0 if math.fabs(da0.x) < tol else (da1.x / da0.x) 1217 | fac.y = 1.0 if math.fabs(da0.y) < tol else (da1.y / da0.y) 1218 | fac.z = 1.0 if math.fabs(da0.z) < tol else (da1.z / da0.z) 1219 | 1220 | return Transform.scaling(fac, anchor) 1221 | 1222 | @staticmethod 1223 | def ortho(left, right, bottom, top, near, far): 1224 | """ Return an orthonormal view Transform from a view frustum """ 1225 | if (left == right): 1226 | raise ValueError("left and right plane constants cannot be equal") 1227 | if (bottom == top): 1228 | raise ValueError("bottom and top plane constants cannot be equal") 1229 | if (near == far): 1230 | raise ValueError("near and far plane constants cannot be equal") 1231 | 1232 | irml = 1.0 / (right - left) 1233 | itmb = 1.0 / (top - bottom) 1234 | ifmn = 1.0 / (far - near) 1235 | 1236 | mat = np.eye(4).flatten() 1237 | 1238 | # these are in Python order 1239 | mat[0] = 2.0 * irml 1240 | mat[5] = 2.0 * itmb 1241 | mat[10] = -2.0 * ifmn 1242 | mat[3] = -(right + left) * irml 1243 | mat[7] = -(top + bottom) * itmb 1244 | mat[11] = -(far + near) * ifmn 1245 | 1246 | return Transform(mat) 1247 | 1248 | @staticmethod 1249 | def perspective(left, right, bottom, top, near, far): 1250 | """ Return a perspective view Transform from a view frustum """ 1251 | if (left == right): 1252 | raise ValueError("left and right plane constants cannot be equal") 1253 | if (bottom == top): 1254 | raise ValueError("bottom and top plane constants cannot be equal") 1255 | if (near == far): 1256 | raise ValueError("near and far plane constants cannot be equal") 1257 | 1258 | irml = 1.0 / (right - left) 1259 | itmb = 1.0 / (top - bottom) 1260 | ifmn = 1.0 / (far - near) 1261 | 1262 | mat = np.zeros([16]) 1263 | 1264 | # these are in Glyph order 1265 | mat[0] = 2.0 * near * irml 1266 | mat[5] = 2.0 * near * itmb 1267 | mat[8] = (right + left) * irml 1268 | mat[9] = (top + bottom) * itmb 1269 | mat[10] = -(far + near) * ifmn 1270 | mat[11] = -1.0 1271 | mat[14] = -2.0 * far * near * ifmn 1272 | 1273 | # Python order 1274 | mat = mat.reshape((4,4)).transpose() 1275 | 1276 | return Transform(mat) 1277 | 1278 | @staticmethod 1279 | def mirroring(normal, distance): 1280 | """ Return a new Transform that mirrors about a plane given 1281 | by a normal vector and a scalar distance 1282 | """ 1283 | return Transform.identity().mirror(normal, distance) 1284 | 1285 | @staticmethod 1286 | def mirrorPlane(plane): 1287 | """ Return a new Transform that mirrors about a given plane """ 1288 | if isinstance(plane,list): 1289 | if len(plane) == 2: 1290 | plane = Plane(normal=plane[0], origin=plane[1]) 1291 | elif len(plane) == 4: 1292 | plane = Plane(coeffs=plane) 1293 | if not isinstance(plane,Plane): 1294 | raise ValueError("Invalid argument") 1295 | 1296 | return Transform.identity().mirror(plane.normal_, plane.d_) 1297 | 1298 | def mirror(self, normal, distance): 1299 | """ Return a new Transform that adds mirroring about a plane 1300 | given by a normal vector and a scalar distance 1301 | """ 1302 | if not isinstance(normal, Vector3): 1303 | normal = Vector3(normal) 1304 | normal = normal.normalize() 1305 | 1306 | # These are in Glyph order 1307 | mat = np.zeros([16]) 1308 | mat[ 0] = 1.0 - 2.0 * normal.x * normal.x 1309 | mat[ 1] = -2.0 * normal.y * normal.x 1310 | mat[ 2] = -2.0 * normal.z * normal.x 1311 | mat[ 3] = 0.0 1312 | mat[ 4] = -2.0 * normal.x * normal.y 1313 | mat[ 5] = 1.0 - 2.0 * normal.y * normal.y 1314 | mat[ 6] = -2.0 * normal.z * normal.y 1315 | mat[ 7] = 0.0 1316 | mat[ 8] = -2.0 * normal.x * normal.z 1317 | mat[ 9] = -2.0 * normal.y * normal.z 1318 | mat[10] = 1.0 - 2.0 * normal.z * normal.z 1319 | mat[11] = 0.0 1320 | mat[12] = -2.0 * normal.x * distance 1321 | mat[13] = -2.0 * normal.y * distance 1322 | mat[14] = -2.0 * normal.z * distance 1323 | mat[15] = 1.0 1324 | 1325 | # transpose back to Python order 1326 | mat = mat.reshape((4,4)).transpose() 1327 | 1328 | return Transform(np.dot(self.xform_, mat)) 1329 | 1330 | @staticmethod 1331 | def stretching(anchor, start, end): 1332 | """ Return a new Transform that is a stretching transform. 1333 | If the vector defined by the start and end points is 1334 | orthogonal to the vector defined by the start and anchor 1335 | points, the transform is undefined and the matrix will 1336 | be set to the identity matrix. 1337 | """ 1338 | return Transform.identity().stretch(anchor, start, end) 1339 | 1340 | def stretch(self, anchor, start, end): 1341 | """ Return a new Transform that adds stretching to a Transform. 1342 | If the vector defined by the start and end points is 1343 | orthogonal to the vector defined by the start and anchor 1344 | points, the transform is undefined and the matrix will 1345 | be set to the identity matrix. 1346 | """ 1347 | if not isinstance(anchor, Vector3): 1348 | anchor = Vector3(anchor) 1349 | if not isinstance(start, Vector3): 1350 | start = Vector3(start) 1351 | if not isinstance(end, Vector3): 1352 | end = Vector3(end) 1353 | 1354 | aToStart = start - anchor 1355 | aToEnd = end - anchor 1356 | 1357 | sDir = (end - start).normalize() 1358 | 1359 | if math.fabs(aToStart.normalize().dot(sDir)) >= 0.01: 1360 | factor = (aToEnd.dot(sDir) / aToStart.dot(sDir)) - 1.0 1361 | # These are in Glyph order 1362 | mat = np.eye(4).flatten() 1363 | mat[ 0] = factor * sDir.x * sDir.x 1364 | mat[ 1] = factor * sDir.y * sDir.x 1365 | mat[ 2] = factor * sDir.z * sDir.x 1366 | mat[ 4] = factor * sDir.x * sDir.y 1367 | mat[ 5] = factor * sDir.y * sDir.y 1368 | mat[ 6] = factor * sDir.z * sDir.y 1369 | mat[ 8] = factor * sDir.x * sDir.z 1370 | mat[ 9] = factor * sDir.y * sDir.z 1371 | mat[10] = factor * sDir.z * sDir.z 1372 | 1373 | # transpose back to Python order to apply translation 1374 | mat = mat.reshape((4,4)).transpose() 1375 | 1376 | axform = Transform.translation(Vector3()-anchor) 1377 | mat = np.dot(mat, axform.xform_) 1378 | axform = Transform.translation(anchor) 1379 | mat = np.dot(axform.xform_, mat) 1380 | 1381 | # Glyph order 1382 | mat = mat.transpose().flatten() 1383 | # mat = np.ravel(mat) 1384 | mat[ 0] += 1.0 1385 | mat[ 5] += 1.0 1386 | mat[10] += 1.0 1387 | mat[12] -= anchor.x 1388 | mat[13] -= anchor.y 1389 | mat[14] -= anchor.z 1390 | 1391 | # Python order 1392 | mat = mat.reshape((4,4)).transpose() 1393 | 1394 | return Transform(np.dot(self.xform_, mat)) 1395 | else: 1396 | return Transform(self.xform_) 1397 | 1398 | def apply(self, vec): 1399 | """ Return a new Vector3 that is transformed from a given point """ 1400 | # apply transform to a point 1401 | if not isinstance(vec, Vector3): 1402 | vec = Vector3(vec) 1403 | rw = np.array(tuple(vec.vector_) + (1.0,)) 1404 | rw = np.dot(self.xform_, rw) 1405 | vec = np.array(rw[0:3]) 1406 | if rw[3] != 0.0: 1407 | vec = vec / rw[3] 1408 | return Vector3(vec) 1409 | 1410 | def applyToDirection(self, direct): 1411 | """ Return a a new Vector3 that is a transformed direction vector. 1412 | 1413 | This differs from apply as follows: 1414 | 1415 | When transforming a point by a 4x4 matrix, the point is 1416 | represented by a vector with X, Y, and Z as the first 3 1417 | components and 1 as the fourth component. This allows 1418 | the point to pick up any translation component in the matrix. 1419 | This method represents the direction as a vector with 0 as 1420 | the fourth component. Since a direction can be thought of 1421 | as the difference between two points, a zero fourth component 1422 | is the difference between two points that have 1 as the fourth 1423 | component. 1424 | """ 1425 | # apply transform to a direction vector, as opposed to a point 1426 | if not isinstance(direct, Vector3): 1427 | direct = Vector3(direct) 1428 | rw = np.array(tuple(direct.vector_) + (0.0,)) 1429 | rw = np.dot(self.xform_, rw) 1430 | direct = np.array(rw[0:3]) 1431 | if rw[3] != 0.0: 1432 | direct = direct / rw[3] 1433 | return Vector3(direct) 1434 | 1435 | def applyToNormal(self, normal): 1436 | """ Return a new Vector3 that is a transformed normal vector. 1437 | A normal vector is transformed by multiplying the normal 1438 | by the transposed inverse matrix. 1439 | """ 1440 | if not isinstance(normal, Vector3): 1441 | normal = Vector3(normal) 1442 | rw = np.array(tuple(normal.vector_) + (0.0,)) 1443 | rw = np.dot(np.linalg.inv(self.xform_).transpose(), rw) 1444 | normal = np.array(rw[0:3]) 1445 | return Vector3(normal).normalize() 1446 | 1447 | def applyToPlane(self, plane): 1448 | """ Return a new Plane that is transformed plane """ 1449 | o = self.apply(plane.project((0, 0, 0))) 1450 | n = self.applyToNormal(plane.normal_) 1451 | return Plane(origin=o, normal=n) 1452 | 1453 | def __mul__(self, other): 1454 | """ Return a the multiplication of self and either another Transform 1455 | (a new Transform) or a Vector3 (a new, transformed Vector3) 1456 | """ 1457 | if isinstance(other, Vector3): 1458 | return self.apply(other) 1459 | elif isinstance(other, Transform): 1460 | return Transform(np.dot(self.xform_, other.xform_)) 1461 | else: 1462 | raise ValueError("Invalid argument") 1463 | 1464 | def multiply(self, other): 1465 | """ Return a new Transform that is the multiplication of self 1466 | and another Transform. This is equivalent to 'self * other'. 1467 | """ 1468 | if isinstance(other, Transform): 1469 | return self * other 1470 | else: 1471 | raise ValueError("Invalid argument") 1472 | 1473 | def determinant(self): 1474 | """ Return the scalar determinant of the transformation matrix """ 1475 | return np.linalg.det(self.xform_) 1476 | 1477 | def transpose(self): 1478 | return Transform(self.xform_.transpose()) 1479 | 1480 | def inverse(self): 1481 | return Transform(np.linalg.inv(self.xform_)) 1482 | 1483 | def _clamp(v, high, low, tol=0.0): 1484 | """ Clamp a value to a range, or zero if within tolerance """ 1485 | if v > high-tol: 1486 | return high 1487 | elif v < low+tol: 1488 | return low 1489 | elif v > -tol and v < tol: 1490 | return 0.0 1491 | else: 1492 | return v 1493 | 1494 | ############################################################################# 1495 | # 1496 | # This file is licensed under the Cadence Public License Version 1.0 (the 1497 | # "License"), a copy of which is found in the included file named "LICENSE", 1498 | # and is distributed "AS IS." TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE 1499 | # LAW, CADENCE DISCLAIMS ALL WARRANTIES AND IN NO EVENT SHALL BE LIABLE TO 1500 | # ANY PARTY FOR ANY DAMAGES ARISING OUT OF OR RELATING TO USE OF THIS FILE. 1501 | # Please see the License for the full text of applicable terms. 1502 | # 1503 | ############################################################################# 1504 | --------------------------------------------------------------------------------