--- /dev/null
+comment: no
--- /dev/null
+[flake8]
+ignore = E203, E266, E501, W503
+max-line-length = 80
+max-complexity = 18
+select = B,C,E,F,W,T4,B9
--- /dev/null
+*.inr filter=lfs diff=lfs merge=lfs -text
+*.off filter=lfs diff=lfs merge=lfs -text
+*.vtu filter=lfs diff=lfs merge=lfs -text
--- /dev/null
+name: ci
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repo
+ uses: actions/checkout@v2
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ - name: Run pre-commit
+ uses: pre-commit/action@v2.0.3
+
+ linux:
+ runs-on: ubuntu-20.04
+ steps:
+ - uses: actions/setup-python@v2
+ with:
+ python-version: "3.x"
+
+ - name: Checkout code
+ uses: nschloe/action-cached-lfs-checkout@v1
+
+ - name: Install CGAL 5
+ run: |
+ # Leave that here in case we ever need a PPA again
+ # sudo apt-get install -y software-properties-common
+ # sudo apt-add-repository -y ppa:nschloe/cgal-backports
+ # sudo apt update
+ sudo apt install -y libcgal-dev
+ - name: Install other dependencies
+ run: |
+ sudo apt-get install -y libeigen3-dev python3-pip clang
+ - name: Test with tox
+ run: |
+ pip install tox
+ CC="clang" tox -- --cov pygalmesh --cov-report xml --cov-report term
+ - uses: codecov/codecov-action@v1
+
+ macos:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/setup-python@v2
+ with:
+ python-version: "3.x"
+
+ - name: Checkout code
+ uses: nschloe/action-cached-lfs-checkout@v1
+
+ - name: Install system dependencies
+ run: |
+ brew install cgal eigen
+ - name: Test with tox
+ run: |
+ pip install tox
+ CC="clang" tox -- --cov pygalmesh --cov-report xml --cov-report term
--- /dev/null
+*.mesh
+.cache/
+build/
+dist/
+MANIFEST
+README.rst
+do-configure.sh
+.pytest_cache/
+*.egg-info/
+.tox/
--- /dev/null
+repos:
+ - repo: https://github.com/PyCQA/isort
+ rev: 5.9.3
+ hooks:
+ - id: isort
+
+ - repo: https://github.com/psf/black
+ rev: 21.9b0
+ hooks:
+ - id: black
+ language_version: python3
+
+ - repo: https://github.com/PyCQA/flake8
+ rev: 3.9.2
+ hooks:
+ - id: flake8
--- /dev/null
+cff-version: 1.2.0
+message: "If you use this software, please cite it as below."
+authors:
+- family-names: "Schlömer"
+ given-names: "Nico"
+ orcid: "https://orcid.org/0000-0001-5228-0946"
+title: "pygalmesh: Python interface for CGAL's meshing tools"
+doi: 10.5281/zenodo.5564818
+url: https://github.com/nschloe/pygalmesh
+license: GPL-3.0
--- /dev/null
+# CMake is used for debugging in pygalmesh. Like every other Python package, the
+# production build system is setuptools.
+cmake_minimum_required(VERSION 3.0)
+
+project(pygalmesh CXX)
+
+add_subdirectory(src)
--- /dev/null
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
--- /dev/null
+include src/domain.hpp
+include src/generate.hpp
+include src/generate_2d.hpp
+include src/generate_from_inr.hpp
+include src/generate_from_off.hpp
+include src/generate_periodic.hpp
+include src/generate_surface_mesh.hpp
+include src/polygon2d.hpp
+include src/primitives.hpp
+include src/remesh_surface.hpp
+include src/sizing_field.hpp
+
+include tests/*.py
+recursive-include tests/meshes *
--- /dev/null
+<p align="center">
+ <a href="https://github.com/nschloe/pygalmesh"><img alt="pygalmesh" src="https://nschloe.github.io/pygalmesh/pygalmesh-logo.svg" width="60%"></a>
+ <p align="center">Create high-quality meshes with ease.</p>
+</p>
+
+[](https://pypi.org/project/pygalmesh)
+[](https://anaconda.org/conda-forge/pygalmesh/)
+[](https://repology.org/project/pygalmesh/versions)
+[](https://pypi.org/pypi/pygalmesh/)
+[](https://doi.org/10.5281/zenodo.5564818)
+[](https://github.com/nschloe/pygalmesh)
+[](https://pepy.tech/project/pygalmesh)
+<!--[](https://pypistats.org/packages/pygalmesh)-->
+
+[](https://discord.gg/Z6DMsJh4Hr)
+
+[](https://github.com/nschloe/pygalmesh/actions?query=workflow%3Aci)
+[](https://codecov.io/gh/nschloe/pygalmesh)
+[](https://lgtm.com/projects/g/nschloe/pygalmesh)
+[](https://github.com/psf/black)
+
+pygalmesh is a Python frontend to [CGAL](https://www.cgal.org/)'s
+[2D](https://doc.cgal.org/latest/Mesh_2/index.html) and [3D mesh generation
+capabilities](https://doc.cgal.org/latest/Mesh_3/index.html). pygalmesh makes it easy
+to create high-quality 2D, 3D volume meshes, periodic volume meshes, and surface meshes.
+
+### Examples
+
+#### 2D meshes
+
+<img src="https://nschloe.github.io/pygalmesh/rect.svg" width="30%">
+
+CGAL generates 2D meshes from linear constraints.
+
+```python
+import numpy as np
+import pygalmesh
+
+points = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]])
+constraints = [[0, 1], [1, 2], [2, 3], [3, 0]]
+
+mesh = pygalmesh.generate_2d(
+ points,
+ constraints,
+ max_edge_size=1.0e-1,
+ num_lloyd_steps=10,
+)
+# mesh.points, mesh.cells
+```
+
+The quality of the mesh isn't very good, but can be improved with
+[optimesh](https://github.com/nschloe/optimesh).
+
+#### A simple ball
+
+<img src="https://nschloe.github.io/pygalmesh/ball.png" width="30%">
+
+```python
+import pygalmesh
+
+s = pygalmesh.Ball([0, 0, 0], 1.0)
+mesh = pygalmesh.generate_mesh(s, max_cell_circumradius=0.2)
+
+# mesh.points, mesh.cells, ...
+```
+
+You can write the mesh with
+
+<!--pytest-codeblocks:skip-->
+
+```python
+mesh.write("out.vtk")
+```
+
+You can use any format supported by [meshio](https://github.com/nschloe/meshio).
+
+The mesh generation comes with many more options, described
+[here](https://doc.cgal.org/latest/Mesh_3/). Try, for example,
+
+<!--pytest-codeblocks:skip-->
+
+```python
+mesh = pygalmesh.generate_mesh(
+ s, max_cell_circumradius=0.2, odt=True, lloyd=True, verbose=False
+)
+```
+
+#### Other primitive shapes
+
+<img src="https://nschloe.github.io/pygalmesh/tetra.png" width="30%">
+
+pygalmesh provides out-of-the-box support for balls, cuboids, ellipsoids, tori, cones,
+cylinders, and tetrahedra. Try for example
+
+```python
+import pygalmesh
+
+s0 = pygalmesh.Tetrahedron(
+ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]
+)
+mesh = pygalmesh.generate_mesh(
+ s0,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=0.1,
+)
+```
+
+#### Domain combinations
+
+<img src="https://nschloe.github.io/pygalmesh/ball-difference.png" width="30%">
+
+Supported are unions, intersections, and differences of all domains. As mentioned above,
+however, the sharp intersections between two domains are not automatically handled. Try
+for example
+
+```python
+import pygalmesh
+
+radius = 1.0
+displacement = 0.5
+s0 = pygalmesh.Ball([displacement, 0, 0], radius)
+s1 = pygalmesh.Ball([-displacement, 0, 0], radius)
+u = pygalmesh.Difference(s0, s1)
+```
+
+To sharpen the intersection circle, add it as a feature edge polygon line, e.g.,
+
+```python
+import numpy as np
+import pygalmesh
+
+radius = 1.0
+displacement = 0.5
+s0 = pygalmesh.Ball([displacement, 0, 0], radius)
+s1 = pygalmesh.Ball([-displacement, 0, 0], radius)
+u = pygalmesh.Difference(s0, s1)
+
+# add circle
+a = np.sqrt(radius ** 2 - displacement ** 2)
+max_edge_size_at_feature_edges = 0.15
+n = int(2 * np.pi * a / max_edge_size_at_feature_edges)
+alpha = np.linspace(0.0, 2 * np.pi, n + 1)
+circ = a * np.column_stack([np.zeros(n + 1), np.cos(alpha), np.sin(alpha)])
+
+mesh = pygalmesh.generate_mesh(
+ u,
+ extra_feature_edges=[circ],
+ max_cell_circumradius=0.15,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ min_facet_angle=25,
+ max_radius_surface_delaunay_ball=0.15,
+ max_circumradius_edge_ratio=2.0,
+)
+```
+
+Note that the length of the polygon legs are kept in sync with
+`max_edge_size_at_feature_edges` of the mesh generation. This makes sure that it fits in
+nicely with the rest of the mesh.
+
+#### Domain deformations
+
+<img src="https://nschloe.github.io/pygalmesh/egg.png" width="30%">
+
+You can of course translate, rotate, scale, and stretch any domain. Try, for example,
+
+```python
+import pygalmesh
+
+s = pygalmesh.Stretch(pygalmesh.Ball([0, 0, 0], 1.0), [1.0, 2.0, 0.0])
+
+mesh = pygalmesh.generate_mesh(s, max_cell_circumradius=0.1)
+```
+
+#### Extrusion of 2D polygons
+
+<img src="https://nschloe.github.io/pygalmesh/triangle-rotated.png" width="30%">
+
+pygalmesh lets you extrude any polygon into a 3D body. It even supports rotation
+alongside!
+
+```python
+import pygalmesh
+
+p = pygalmesh.Polygon2D([[-0.5, -0.3], [0.5, -0.3], [0.0, 0.5]])
+max_edge_size_at_feature_edges = 0.1
+domain = pygalmesh.Extrude(
+ p,
+ [0.0, 0.0, 1.0],
+ 0.5 * 3.14159265359,
+ max_edge_size_at_feature_edges,
+)
+mesh = pygalmesh.generate_mesh(
+ domain,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ verbose=False,
+)
+```
+
+Feature edges are automatically preserved here, which is why an edge length needs to be
+given to `pygalmesh.Extrude`.
+
+#### Rotation bodies
+
+<img src="https://nschloe.github.io/pygalmesh/circle-rotate-extr.png" width="30%">
+
+Polygons in the x-z-plane can also be rotated around the z-axis to yield a rotation
+body.
+
+```python
+import pygalmesh
+
+p = pygalmesh.Polygon2D([[0.5, -0.3], [1.5, -0.3], [1.0, 0.5]])
+max_edge_size_at_feature_edges = 0.1
+domain = pygalmesh.RingExtrude(p, max_edge_size_at_feature_edges)
+mesh = pygalmesh.generate_mesh(
+ domain,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ verbose=False,
+)
+```
+
+#### Your own custom level set function
+
+<img src="https://nschloe.github.io/pygalmesh/heart.png" width="30%">
+
+If all of the variety is not enough for you, you can define your own custom level set
+function. You simply need to subclass `pygalmesh.DomainBase` and specify a function,
+e.g.,
+
+```python
+import pygalmesh
+
+
+class Heart(pygalmesh.DomainBase):
+ def __init__(self):
+ super().__init__()
+
+ def eval(self, x):
+ return (
+ (x[0] ** 2 + 9.0 / 4.0 * x[1] ** 2 + x[2] ** 2 - 1) ** 3
+ - x[0] ** 2 * x[2] ** 3
+ - 9.0 / 80.0 * x[1] ** 2 * x[2] ** 3
+ )
+
+ def get_bounding_sphere_squared_radius(self):
+ return 10.0
+
+
+d = Heart()
+mesh = pygalmesh.generate_mesh(d, max_cell_circumradius=0.1)
+```
+
+Note that you need to specify the square of a bounding sphere radius, used as an input
+to CGAL's mesh generator.
+
+#### Local refinement
+
+<img src="https://nschloe.github.io/pygalmesh/ball-local-refinement.png" width="30%">
+
+Use `generate_mesh` with a function (regular or lambda) as `max_cell_circumradius`. The
+same goes for `max_edge_size_at_feature_edges`, `max_radius_surface_delaunay_ball`, and
+`max_facet_distance`.
+
+```python
+import numpy as np
+import pygalmesh
+
+mesh = pygalmesh.generate_mesh(
+ pygalmesh.Ball([0.0, 0.0, 0.0], 1.0),
+ min_facet_angle=30.0,
+ max_radius_surface_delaunay_ball=0.1,
+ max_facet_distance=0.025,
+ max_circumradius_edge_ratio=2.0,
+ max_cell_circumradius=lambda x: abs(np.sqrt(np.dot(x, x)) - 0.5) / 5 + 0.025,
+)
+```
+
+#### Surface meshes
+
+If you're only after the surface of a body, pygalmesh has `generate_surface_mesh` for
+you. It offers fewer options (obviously, `max_cell_circumradius` is gone), but otherwise
+works the same way:
+
+```python
+import pygalmesh
+
+s = pygalmesh.Ball([0, 0, 0], 1.0)
+mesh = pygalmesh.generate_surface_mesh(
+ s,
+ min_facet_angle=30.0,
+ max_radius_surface_delaunay_ball=0.1,
+ max_facet_distance=0.1,
+)
+```
+
+Refer to [CGAL's
+documentation](https://doc.cgal.org/latest/Surface_mesher/index.html) for the
+options.
+
+#### Periodic volume meshes
+
+<img src="https://nschloe.github.io/pygalmesh/periodic.png" width="30%">
+
+pygalmesh also interfaces CGAL's [3D periodic
+mesh generation](https://doc.cgal.org/latest/Periodic_3_mesh_3/index.html). Besides a
+domain, one needs to specify a bounding box, and optionally the number of copies in the
+output (1, 2, 4, or 8). Example:
+
+```python
+import numpy as np
+import pygalmesh
+
+
+class Schwarz(pygalmesh.DomainBase):
+ def __init__(self):
+ super().__init__()
+
+ def eval(self, x):
+ x2 = np.cos(x[0] * 2 * np.pi)
+ y2 = np.cos(x[1] * 2 * np.pi)
+ z2 = np.cos(x[2] * 2 * np.pi)
+ return x2 + y2 + z2
+
+
+mesh = pygalmesh.generate_periodic_mesh(
+ Schwarz(),
+ [0, 0, 0, 1, 1, 1],
+ max_cell_circumradius=0.05,
+ min_facet_angle=30,
+ max_radius_surface_delaunay_ball=0.05,
+ max_facet_distance=0.025,
+ max_circumradius_edge_ratio=2.0,
+ number_of_copies_in_output=4,
+ # odt=True,
+ # lloyd=True,
+ verbose=False,
+)
+```
+
+#### Volume meshes from surface meshes
+
+<img src="https://nschloe.github.io/pygalmesh/elephant.png" width="30%">
+
+If you have a surface mesh at hand (like
+[elephant.vtu](http://nschloe.github.io/pygalmesh/elephant.vtu)), pygalmesh generates a
+volume mesh on the command line via
+
+```
+pygalmesh volume-from-surface elephant.vtu out.vtk --cell-size 1.0 --odt
+```
+
+(See `pygalmesh volume-from-surface -h` for all options.)
+
+In Python, do
+
+<!--pytest-codeblocks:skip-->
+
+```python
+import pygalmesh
+
+mesh = pygalmesh.generate_volume_mesh_from_surface_mesh(
+ "elephant.vtu",
+ min_facet_angle=25.0,
+ max_radius_surface_delaunay_ball=0.15,
+ max_facet_distance=0.008,
+ max_circumradius_edge_ratio=3.0,
+ verbose=False,
+)
+```
+
+#### Meshes from INR voxel files
+
+<img src="https://nschloe.github.io/pygalmesh/liver.png" width="30%">
+
+It is also possible to generate meshes from INR voxel files, e.g.,
+[here](https://github.com/CGAL/cgal/tree/master/Mesh_3/examples/Mesh_3/data) either on
+the command line
+
+```
+pygalmesh from-inr skull_2.9.inr out.vtu --cell-size 5.0 --odt
+```
+
+(see `pygalmesh from-inr -h` for all options) or from Python
+
+<!--pytest-codeblocks:skip-->
+
+```python
+import pygalmesh
+
+mesh = pygalmesh.generate_from_inr(
+ "skull_2.9.inr",
+ max_cell_circumradius=5.0,
+ verbose=False,
+)
+```
+
+#### Meshes from numpy arrays representing 3D images
+
+| <img src="https://nschloe.github.io/pygalmesh/voxel-ball.png" width="70%"> | <img src="https://nschloe.github.io/pygalmesh/phantom.png" width="70%"> |
+| :------------------------------------------------------------------------: | :---------------------------------------------------------------------: |
+
+pygalmesh can help generating unstructed meshes from 3D numpy int arrays specifying the
+subdomains. Subdomains with key `0` are not meshed.
+
+```python
+import pygalmesh
+import numpy as np
+
+x_ = np.linspace(-1.0, 1.0, 50)
+y_ = np.linspace(-1.0, 1.0, 50)
+z_ = np.linspace(-1.0, 1.0, 50)
+x, y, z = np.meshgrid(x_, y_, z_)
+
+vol = np.empty((50, 50, 50), dtype=np.uint8)
+idx = x ** 2 + y ** 2 + z ** 2 < 0.5 ** 2
+vol[idx] = 1
+vol[~idx] = 0
+
+voxel_size = (0.1, 0.1, 0.1)
+
+mesh = pygalmesh.generate_from_array(
+ vol, voxel_size, max_facet_distance=0.2, max_cell_circumradius=0.1
+)
+mesh.write("ball.vtk")
+```
+
+The code below creates a mesh from the 3D breast phantom from [Lou et
+al](http://biomedicaloptics.spiedigitallibrary.org/article.aspx?articleid=2600985)
+available
+[here](https://wustl.app.box.com/s/rqivtin0xcofjwlkz43acou8jknsbfx8/file/127108205145).
+The phantom comprises four tissue types (background, fat, fibrograndular, skin, vascular
+tissues). The generated mesh conforms to tissues interfaces.
+
+<!--pytest-codeblocks:skip-->
+
+```python
+import pygalmesh
+import meshio
+
+with open("MergedPhantom.DAT", "rb") as fid:
+ vol = np.fromfile(fid, dtype=np.uint8)
+
+vol = vol.reshape((722, 411, 284))
+voxel_size = (0.2, 0.2, 0.2)
+
+mesh = pygalmesh.generate_from_array(
+ vol, voxel_size, max_facet_distance=0.2, max_cell_circumradius=1.0
+)
+mesh.write("breast.vtk")
+```
+
+In addition, we can specify different mesh sizes for each tissue type. The code below
+sets the mesh size to _1 mm_ for the skin tissue (label `4`), _0.5 mm_ for the vascular
+tissue (label `5`), and _2 mm_ for all other tissues (`default`).
+
+<!--pytest-codeblocks:skip-->
+
+```python
+mesh = pygalmesh.generate_from_array(
+ vol,
+ voxel_size,
+ max_facet_distance=0.2,
+ max_cell_circumradius={"default": 2.0, 4: 1.0, 5: 0.5},
+)
+mesh.write("breast_adapted.vtk")
+```
+
+#### Surface remeshing
+
+| <img src="https://nschloe.github.io/pygalmesh/lion-head0.png" width="100%"> | <img src="https://nschloe.github.io/pygalmesh/lion-head1.png" width="100%"> |
+| :-------------------------------------------------------------------------: | :-------------------------------------------------------------------------: |
+
+pygalmesh can help remeshing an existing surface mesh, e.g.,
+[`lion-head.off`](https://github.com/nschloe/pygalmesh/raw/gh-pages/lion-head.off). On
+the command line, use
+
+```
+pygalmesh remesh-surface lion-head.off out.vtu -e 0.025 -a 25 -s 0.1 -d 0.001
+```
+
+(see `pygalmesh remesh-surface -h` for all options) or from Python
+
+<!--pytest-codeblocks:skip-->
+
+```python
+import pygalmesh
+
+mesh = pygalmesh.remesh_surface(
+ "lion-head.off",
+ max_edge_size_at_feature_edges=0.025,
+ min_facet_angle=25,
+ max_radius_surface_delaunay_ball=0.1,
+ max_facet_distance=0.001,
+ verbose=False,
+)
+```
+
+### Installation
+
+For installation, pygalmesh needs [CGAL](https://www.cgal.org/) and
+[Eigen](http://eigen.tuxfamily.org/index.php?title=Main_Page) installed on your
+system. They are typically available on your Linux distribution, e.g., on
+Ubuntu
+
+```
+sudo apt install libcgal-dev libeigen3-dev
+```
+
+On MacOS with homebrew,
+
+```
+brew install cgal eigen
+```
+
+After that, pygalmesh can be [installed from the Python Package
+Index](https://pypi.org/project/pygalmesh/), so with
+
+```
+pip install -U pygalmesh
+```
+
+you can install/upgrade.
+
+#### Troubleshooting
+
+If pygalmesh fails to build due to `fatal error: 'Eigen/Dense' file not found`
+you will need to create a symbolic link for Eigen to be detected, e.g.
+
+```
+cd /usr/local/include
+sudo ln -sf eigen3/Eigen Eigen
+```
+
+It's possible that `eigen3` could be in `/usr/include` instead of
+`/usr/local/install`.
+
+#### Manual installation
+
+For manual installation (if you're a developer or just really keen on getting
+the bleeding edge version of pygalmesh), there are two possibilities:
+
+- Get the sources, type `python3 setup.py install`. This does the trick
+ most the time.
+- As a fallback, there's a CMake-based installation. Simply go `cmake /path/to/sources/` and `make`.
+
+### Testing
+
+To run the pygalmesh unit tests, check out this repository and type
+
+```
+pytest
+```
+
+### Background
+
+CGAL offers two different approaches for mesh generation:
+
+1. Meshes defined implicitly by level sets of functions.
+2. Meshes defined by a set of bounding planes.
+
+pygalmesh provides a front-end to the first approach, which has the following advantages
+and disadvantages:
+
+- All boundary points are guaranteed to be in the level set within any specified
+ residual. This results in smooth curved surfaces.
+- Sharp intersections of subdomains (e.g., in unions or differences of sets) need to be
+ specified manually (via feature edges, see below), which can be tedious.
+
+On the other hand, the bounding-plane approach (realized by
+[mshr](https://bitbucket.org/fenics-project/mshr)), has the following properties:
+
+- Smooth, curved domains are approximated by a set of bounding planes, resulting in more
+ of less visible edges.
+- Intersections of domains can be computed automatically, so domain unions etc. have
+ sharp edges where they belong.
+
+See [here](https://github.com/nschloe/awesome-scientific-computing#meshing) for other
+mesh generation tools.
+
+### License
+
+pygalmesh is published under the [GPLv3 license](https://www.gnu.org/licenses/gpl-3.0.en.html).
--- /dev/null
+version := `python3 -c "from configparser import ConfigParser; p = ConfigParser(); p.read('setup.cfg'); print(p['metadata']['version'])"`
+
+default:
+ @echo "\"make publish\"?"
+
+tag:
+ @if [ "$(git rev-parse --abbrev-ref HEAD)" != "main" ]; then exit 1; fi
+ curl -H "Authorization: token `cat ~/.github-access-token`" -d '{"tag_name": "v{{version}}"}' https://api.github.com/repos/nschloe/pygalmesh/releases
+
+upload: clean
+ @if [ "$(git rev-parse --abbrev-ref HEAD)" != "main" ]; then exit 1; fi
+ python3 -m build --sdist .
+ twine upload dist/*.tar.gz
+
+publish: tag upload
+
+clean:
+ @find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf
+ @rm -rf src/*.egg-info/ build/ dist/ .tox/ pygalmesh.egg-info//
+
+format:
+ isort .
+ black .
+ blacken-docs README.md
+
+lint:
+ black --check .
+ flake8 setup.py pygalmesh/ test/*.py
--- /dev/null
+from _pygalmesh import _CGAL_VERSION_STR
+
+try:
+ # Python 3.8
+ from importlib import metadata
+except ImportError:
+ import importlib_metadata as metadata
+
+try:
+ __version__ = metadata.version("pygalmesh")
+except Exception:
+ __version__ = "unknown"
+
+__cgal_version__ = _CGAL_VERSION_STR
--- /dev/null
+# https://github.com/pybind/pybind11/issues/1004
+from _pygalmesh import (
+ Ball,
+ Cone,
+ Cuboid,
+ Cylinder,
+ Difference,
+ DomainBase,
+ Ellipsoid,
+ Extrude,
+ HalfSpace,
+ Intersection,
+ Polygon2D,
+ RingExtrude,
+ Rotate,
+ Scale,
+ Stretch,
+ Tetrahedron,
+ Torus,
+ Translate,
+ Union,
+)
+
+from . import _cli
+from .__about__ import __cgal_version__, __version__
+from .main import (
+ generate_2d,
+ generate_from_array,
+ generate_from_inr,
+ generate_mesh,
+ generate_periodic_mesh,
+ generate_surface_mesh,
+ generate_volume_mesh_from_surface_mesh,
+ remesh_surface,
+ save_inr,
+)
+
+__all__ = [
+ "__version__",
+ "__cgal_version__",
+ #
+ "_cli",
+ #
+ "DomainBase",
+ "Translate",
+ "Rotate",
+ "Scale",
+ "Stretch",
+ "Intersection",
+ "Union",
+ "Difference",
+ "Extrude",
+ "Ball",
+ "Cuboid",
+ "Ellipsoid",
+ "Tetrahedron",
+ "Cone",
+ "Cylinder",
+ "Torus",
+ "HalfSpace",
+ "Polygon2D",
+ "RingExtrude",
+ #
+ "generate_mesh",
+ "generate_2d",
+ "generate_periodic_mesh",
+ "generate_surface_mesh",
+ "generate_volume_mesh_from_surface_mesh",
+ "generate_from_array",
+ "generate_from_inr",
+ "remesh_surface",
+ "save_inr",
+]
--- /dev/null
+import argparse
+from sys import version_info
+
+import meshio
+from _pygalmesh import _CGAL_VERSION_STR
+
+from .__about__ import __version__
+from .main import generate_from_inr, generate_volume_mesh_from_surface_mesh
+from .main import remesh_surface as rms
+
+
+def cli(argv=None):
+ parser = argparse.ArgumentParser(
+ description="pygalmesh command-line tools",
+ formatter_class=argparse.RawTextHelpFormatter,
+ )
+
+ parser.add_argument(
+ "--version",
+ "-v",
+ action="version",
+ version=get_version_text(),
+ help="display version information",
+ )
+
+ subparsers = parser.add_subparsers(title="subcommands")
+
+ parser_inr = subparsers.add_parser(
+ "from-inr", help="Create volume mesh from INR voxel files"
+ )
+ _cli_inr(parser_inr)
+ parser_inr.set_defaults(func=inr)
+
+ parser_remesh = subparsers.add_parser("remesh-surface", help="Remesh surface mesh")
+ _cli_remesh(parser_remesh)
+ parser_remesh.set_defaults(func=remesh_surface)
+
+ parser_volume_from_surface = subparsers.add_parser(
+ "volume-from-surface", help="Generate volume mesh from surface mesh"
+ )
+ _cli_volume_from_surface(parser_volume_from_surface)
+ parser_volume_from_surface.set_defaults(func=volume_from_surface)
+
+ args = parser.parse_args(argv)
+ return args.func(args)
+
+
+def get_version_text():
+ python_ver = f"{version_info.major}.{version_info.minor}.{version_info.micro}"
+ return "\n".join(
+ [
+ f"pygalmesh {__version__} [Python {python_ver}, CGAL {_CGAL_VERSION_STR}]"
+ + "Copyright (c) 2016-2021, Nico Schlömer <nico.schloemer@gmail.com>",
+ ]
+ )
+
+
+def inr(args):
+ mesh = generate_from_inr(
+ args.infile,
+ lloyd=args.lloyd,
+ odt=args.odt,
+ perturb=args.perturb,
+ exude=args.exude,
+ max_edge_size_at_feature_edges=args.max_edge_size_at_feature_edges,
+ min_facet_angle=args.min_facet_angle,
+ max_radius_surface_delaunay_ball=args.max_radius_surface_delaunay_ball,
+ max_facet_distance=args.max_facet_distance,
+ max_circumradius_edge_ratio=args.max_circumradius_edge_ratio,
+ max_cell_circumradius=args.max_cell_circumradius,
+ verbose=not args.quiet,
+ )
+ meshio.write(args.outfile, mesh)
+
+
+def _cli_inr(parser):
+ parser.add_argument("infile", type=str, help="input INR file")
+
+ parser.add_argument("outfile", type=str, help="output mesh file")
+
+ parser.add_argument(
+ "--lloyd",
+ "-l",
+ type=bool,
+ default=False,
+ help="Lloyd smoothing (default: false)",
+ )
+
+ parser.add_argument(
+ "--odt",
+ "-o",
+ action="store_true",
+ default=False,
+ help="ODT smoothing (default: false)",
+ )
+
+ parser.add_argument(
+ "--perturb",
+ "-p",
+ action="store_true",
+ default=False,
+ help="perturb (default: false)",
+ )
+
+ parser.add_argument(
+ "--exude",
+ "-x",
+ action="store_true",
+ default=False,
+ help="exude (default: false)",
+ )
+
+ parser.add_argument(
+ "--max-edge-size-at-feature-edges",
+ "-e",
+ type=float,
+ default=0.0,
+ help="maximum edge size at feature edges (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--min-facet-angle",
+ "-a",
+ type=float,
+ default=0.0,
+ help="minimum facet angle (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--max-radius-surface-delaunay-ball",
+ "-s",
+ type=float,
+ default=0.0,
+ help="maximum radius of the surface facet Delaunay ball (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--max-facet-distance",
+ "-d",
+ type=float,
+ default=0.0,
+ help="maximum facet distance (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--max-circumradius-edge-ratio",
+ "-r",
+ type=float,
+ default=0.0,
+ help="cell radius/edge ratio (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--max-cell-circumradius",
+ "-c",
+ type=float,
+ default=0.0,
+ help="maximum cell circumradius (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--quiet",
+ "-q",
+ action="store_true",
+ default=False,
+ help="quiet mode (default: False)",
+ )
+ return parser
+
+
+def remesh_surface(args):
+ mesh = rms(
+ args.infile,
+ # lloyd=args.lloyd,
+ # odt=args.odt,
+ # perturb=args.perturb,
+ # exude=args.exude,
+ max_edge_size_at_feature_edges=args.max_edge_size_at_feature_edges,
+ min_facet_angle=args.min_facet_angle,
+ max_radius_surface_delaunay_ball=args.max_radius_surface_delaunay_ball,
+ max_facet_distance=args.max_facet_distance,
+ verbose=not args.quiet,
+ )
+ meshio.write(args.outfile, mesh)
+
+
+def _cli_remesh(parser):
+ parser.add_argument("infile", type=str, help="input mesh file")
+ parser.add_argument("outfile", type=str, help="output mesh file")
+
+ parser.add_argument(
+ "--max-edge-size-at-feature-edges",
+ "-e",
+ type=float,
+ default=0.0,
+ help="maximum edge size at feature edges (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--min-facet-angle",
+ "-a",
+ type=float,
+ default=0.0,
+ help="minimum facet angle (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--max-radius-surface-delaunay-ball",
+ "-s",
+ type=float,
+ default=0.0,
+ help="maximum radius of the surface facet Delaunay ball (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--max-facet-distance",
+ "-d",
+ type=float,
+ default=0.0,
+ help="maximum facet distance (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--quiet",
+ "-q",
+ action="store_true",
+ default=False,
+ help="quiet mode (default: False)",
+ )
+ return parser
+
+
+def volume_from_surface(args):
+ mesh = generate_volume_mesh_from_surface_mesh(
+ args.infile,
+ lloyd=args.lloyd,
+ odt=args.odt,
+ perturb=args.perturb,
+ exude=args.exude,
+ max_edge_size_at_feature_edges=args.max_edge_size_at_feature_edges,
+ min_facet_angle=args.min_facet_angle,
+ max_radius_surface_delaunay_ball=args.max_radius_surface_delaunay_ball,
+ max_facet_distance=args.max_facet_distance,
+ max_circumradius_edge_ratio=args.max_circumradius_edge_ratio,
+ max_cell_circumradius=args.max_cell_circumradius,
+ reorient=args.reorient,
+ verbose=not args.quiet,
+ )
+ meshio.write(args.outfile, mesh)
+
+
+def _cli_volume_from_surface(parser):
+ parser.add_argument("infile", type=str, help="input mesh file")
+
+ parser.add_argument("outfile", type=str, help="output mesh file")
+
+ parser.add_argument(
+ "--lloyd",
+ "-l",
+ type=bool,
+ default=False,
+ help="Lloyd smoothing (default: false)",
+ )
+
+ parser.add_argument(
+ "--odt",
+ "-o",
+ action="store_true",
+ default=False,
+ help="ODT smoothing (default: false)",
+ )
+
+ parser.add_argument(
+ "--perturb",
+ "-p",
+ action="store_true",
+ default=False,
+ help="perturb (default: false)",
+ )
+
+ parser.add_argument(
+ "--exude",
+ "-x",
+ action="store_true",
+ default=False,
+ help="exude (default: false)",
+ )
+
+ parser.add_argument(
+ "--max-edge-size-at-feature-edges",
+ "-e",
+ type=float,
+ default=0.0,
+ help="maximum edge size at feature edges (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--min-facet-angle",
+ "-a",
+ type=float,
+ default=0.0,
+ help="minimum facet angle (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--max-radius-surface-delaunay-ball",
+ "-s",
+ type=float,
+ default=0.0,
+ help="maximum radius of the surface facet Delaunay ball (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--max-facet-distance",
+ "-d",
+ type=float,
+ default=0.0,
+ help="maximum facet distance (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--max-circumradius-edge-ratio",
+ "-r",
+ type=float,
+ default=0.0,
+ help="cell radius/edge ratio (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--max-cell-circumradius",
+ "-c",
+ type=float,
+ default=0.0,
+ help="maximum cell circumradius (default: 0.0)",
+ )
+
+ parser.add_argument(
+ "--reorient",
+ "-t",
+ action="store_true",
+ default=False,
+ help="automatically fix face orientation (default: False)",
+ )
+
+ parser.add_argument(
+ "--quiet",
+ "-q",
+ action="store_true",
+ default=False,
+ help="quiet mode (default: False)",
+ )
+ return parser
--- /dev/null
+from __future__ import annotations
+
+import math
+import os
+import tempfile
+from typing import Callable
+
+import meshio
+import numpy
+from _pygalmesh import (
+ SizingFieldBase,
+ _generate_2d,
+ _generate_from_inr,
+ _generate_from_inr_with_subdomain_sizing,
+ _generate_from_off,
+ _generate_mesh,
+ _generate_periodic_mesh,
+ _generate_surface_mesh,
+ _remesh_surface,
+)
+
+
+class Wrapper(SizingFieldBase):
+ def __init__(self, f):
+ self.f = f
+ super().__init__()
+
+ def eval(self, x):
+ return self.f(x)
+
+
+def generate_mesh(
+ domain,
+ extra_feature_edges: list | None = None,
+ bounding_sphere_radius: float = 0.0,
+ lloyd: bool = False,
+ odt: bool = False,
+ perturb: bool = True,
+ exude: bool = True,
+ max_edge_size_at_feature_edges: float = 0.0,
+ min_facet_angle: float = 0.0,
+ max_radius_surface_delaunay_ball: float | Callable[..., float] = 0.0,
+ max_facet_distance: float = 0.0,
+ max_circumradius_edge_ratio: float = 0.0,
+ max_cell_circumradius: float | Callable[..., float] = 0.0,
+ exude_time_limit: float = 0.0,
+ exude_sliver_bound: float = 0.0,
+ verbose: bool = True,
+ seed: int = 0,
+):
+ """
+ From <https://doc.cgal.org/latest/Mesh_3/classCGAL_1_1Mesh__criteria__3.html>:
+
+ max_edge_size_at_feature_edges:
+ a scalar field (resp. a constant) providing a space varying (resp. a uniform)
+ upper bound for the lengths of curve edges. This parameter has to be set to a
+ positive value when 1-dimensional features protection is used.
+ min_facet_angle:
+ a lower bound for the angles (in degrees) of the surface mesh facets.
+ max_radius_surface_delaunay_ball:
+ a scalar field (resp. a constant) describing a space varying (resp. a uniform)
+ upper-bound or for the radii of the surface Delaunay balls.
+ max_facet_distance:
+ a scalar field (resp. a constant) describing a space varying (resp. a uniform)
+ upper bound for the distance between the facet circumcenter and the center of
+ its surface Delaunay ball.
+ max_circumradius_edge_ratio:
+ an upper bound for the radius-edge ratio of the mesh tetrahedra.
+ max_cell_circumradius:
+ a scalar field (resp. a constant) describing a space varying (resp. a uniform)
+ upper-bound for the circumradii of the mesh tetrahedra.
+ """
+ extra_feature_edges = [] if extra_feature_edges is None else extra_feature_edges
+
+ fh, outfile = tempfile.mkstemp(suffix=".mesh")
+ os.close(fh)
+
+ def _select(obj):
+ if isinstance(obj, float):
+ return obj, None
+ assert callable(obj)
+ return -1.0, Wrapper(obj)
+
+ (
+ max_edge_size_at_feature_edges_value,
+ max_edge_size_at_feature_edges_field,
+ ) = _select(max_edge_size_at_feature_edges)
+ max_cell_circumradius_value, max_cell_circumradius_field = _select(
+ max_cell_circumradius
+ )
+ (
+ max_radius_surface_delaunay_ball_value,
+ max_radius_surface_delaunay_ball_field,
+ ) = _select(max_radius_surface_delaunay_ball)
+ max_facet_distance_value, max_facet_distance_field = _select(max_facet_distance)
+
+ # if feature_edges:
+ # if max_edge_size_at_feature_edges == 0.0:
+ # raise ValueError(
+ # "Need a positive max_edge_size_at_feature_edges bound if feature_edges are present."
+ # )
+ # elif max_edge_size_at_feature_edges != 0.0:
+ # warnings.warn(
+ # "No feature edges. The max_edge_size_at_feature_edges argument has no effect."
+ # )
+
+ _generate_mesh(
+ domain,
+ outfile,
+ extra_feature_edges=extra_feature_edges,
+ bounding_sphere_radius=bounding_sphere_radius,
+ lloyd=lloyd,
+ odt=odt,
+ perturb=perturb,
+ exude=exude,
+ max_edge_size_at_feature_edges_value=max_edge_size_at_feature_edges_value,
+ max_edge_size_at_feature_edges_field=max_edge_size_at_feature_edges_field,
+ min_facet_angle=min_facet_angle,
+ max_radius_surface_delaunay_ball_value=max_radius_surface_delaunay_ball_value,
+ max_radius_surface_delaunay_ball_field=max_radius_surface_delaunay_ball_field,
+ max_facet_distance_value=max_facet_distance_value,
+ max_facet_distance_field=max_facet_distance_field,
+ max_circumradius_edge_ratio=max_circumradius_edge_ratio,
+ max_cell_circumradius_value=max_cell_circumradius_value,
+ max_cell_circumradius_field=max_cell_circumradius_field,
+ exude_time_limit=exude_time_limit,
+ exude_sliver_bound=exude_sliver_bound,
+ verbose=verbose,
+ seed=seed,
+ )
+
+ mesh = meshio.read(outfile)
+ os.remove(outfile)
+ return mesh
+
+
+def generate_2d(
+ points,
+ constraints,
+ B: float = math.sqrt(2),
+ max_edge_size: float = 0.0,
+ num_lloyd_steps: int = 0,
+):
+ # some sanity checks
+ points = numpy.asarray(points)
+ constraints = numpy.asarray(constraints)
+ assert numpy.all(constraints >= 0)
+ assert numpy.all(constraints < len(points))
+ # make sure there are no edges of 0 length
+ edges = points[constraints[:, 0]] - points[constraints[:, 1]]
+ length2 = numpy.einsum("ij,ij->i", edges, edges)
+ if numpy.any(length2 < 1.0e-15):
+ raise RuntimeError("Constraint of (near)-zero length.")
+
+ points, cells = _generate_2d(
+ points,
+ constraints,
+ B,
+ max_edge_size,
+ num_lloyd_steps,
+ )
+ return meshio.Mesh(numpy.array(points), {"triangle": numpy.array(cells)})
+
+
+def generate_periodic_mesh(
+ domain,
+ bounding_cuboid,
+ lloyd: bool = False,
+ odt: bool = False,
+ perturb: bool = True,
+ exude: bool = True,
+ max_edge_size_at_feature_edges: float = 0.0,
+ min_facet_angle: float = 0.0,
+ max_radius_surface_delaunay_ball: float = 0.0,
+ max_facet_distance: float = 0.0,
+ max_circumradius_edge_ratio: float = 0.0,
+ max_cell_circumradius: float = 0.0,
+ number_of_copies_in_output: int = 1,
+ verbose: bool = True,
+ seed: int = 0,
+):
+ fh, outfile = tempfile.mkstemp(suffix=".mesh")
+ os.close(fh)
+
+ assert number_of_copies_in_output in [1, 2, 4, 8]
+
+ _generate_periodic_mesh(
+ domain,
+ outfile,
+ bounding_cuboid,
+ lloyd=lloyd,
+ odt=odt,
+ perturb=perturb,
+ exude=exude,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ min_facet_angle=min_facet_angle,
+ max_radius_surface_delaunay_ball=max_radius_surface_delaunay_ball,
+ max_facet_distance=max_facet_distance,
+ max_circumradius_edge_ratio=max_circumradius_edge_ratio,
+ max_cell_circumradius=max_cell_circumradius,
+ number_of_copies_in_output=number_of_copies_in_output,
+ verbose=verbose,
+ seed=seed,
+ )
+
+ mesh = meshio.read(outfile)
+ os.remove(outfile)
+ return mesh
+
+
+def generate_surface_mesh(
+ domain,
+ bounding_sphere_radius: float = 0.0,
+ min_facet_angle: float = 0.0,
+ max_radius_surface_delaunay_ball: float = 0.0,
+ max_facet_distance: float = 0.0,
+ verbose: bool = True,
+ seed: int = 0,
+):
+ fh, outfile = tempfile.mkstemp(suffix=".off")
+ os.close(fh)
+
+ _generate_surface_mesh(
+ domain,
+ outfile,
+ bounding_sphere_radius=bounding_sphere_radius,
+ min_facet_angle=min_facet_angle,
+ max_radius_surface_delaunay_ball=max_radius_surface_delaunay_ball,
+ max_facet_distance=max_facet_distance,
+ verbose=verbose,
+ seed=seed,
+ )
+
+ mesh = meshio.read(outfile)
+ os.remove(outfile)
+ return mesh
+
+
+def generate_volume_mesh_from_surface_mesh(
+ filename: str,
+ lloyd: bool = False,
+ odt: bool = False,
+ perturb: bool = True,
+ exude: bool = True,
+ max_edge_size_at_feature_edges: float = 0.0,
+ min_facet_angle: float = 0.0,
+ max_radius_surface_delaunay_ball: float = 0.0,
+ max_facet_distance: float = 0.0,
+ max_circumradius_edge_ratio: float = 0.0,
+ max_cell_circumradius: float = 0.0,
+ exude_time_limit: float = 0.0,
+ exude_sliver_bound: float = 0.0,
+ verbose: bool = True,
+ reorient: bool = False,
+ seed: int = 0,
+):
+ mesh = meshio.read(filename)
+
+ fh, off_file = tempfile.mkstemp(suffix=".off")
+ os.close(fh)
+ meshio.write(off_file, mesh)
+
+ fh, outfile = tempfile.mkstemp(suffix=".mesh")
+ os.close(fh)
+
+ _generate_from_off(
+ off_file,
+ outfile,
+ lloyd=lloyd,
+ odt=odt,
+ perturb=perturb,
+ exude=exude,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ min_facet_angle=min_facet_angle,
+ max_radius_surface_delaunay_ball=max_radius_surface_delaunay_ball,
+ max_facet_distance=max_facet_distance,
+ max_circumradius_edge_ratio=max_circumradius_edge_ratio,
+ max_cell_circumradius=max_cell_circumradius,
+ exude_time_limit=exude_time_limit,
+ exude_sliver_bound=exude_sliver_bound,
+ verbose=verbose,
+ reorient=reorient,
+ seed=seed,
+ )
+
+ mesh = meshio.read(outfile)
+ os.remove(off_file)
+ os.remove(outfile)
+ return mesh
+
+
+def generate_from_inr(
+ inr_filename: str,
+ lloyd: bool = False,
+ odt: bool = False,
+ perturb: bool = True,
+ exude: bool = True,
+ max_edge_size_at_feature_edges: float = 0.0,
+ min_facet_angle: float = 0.0,
+ max_radius_surface_delaunay_ball: float = 0.0,
+ max_facet_distance: float = 0.0,
+ max_circumradius_edge_ratio: float = 0.0,
+ max_cell_circumradius: float | dict[int | str, float] = 0.0,
+ exude_time_limit: float = 0.0,
+ exude_sliver_bound: float = 0.0,
+ verbose: bool = True,
+ seed: int = 0,
+):
+ fh, outfile = tempfile.mkstemp(suffix=".mesh")
+ os.close(fh)
+
+ if isinstance(max_cell_circumradius, float):
+ _generate_from_inr(
+ inr_filename,
+ outfile,
+ lloyd=lloyd,
+ odt=odt,
+ perturb=perturb,
+ exude=exude,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ min_facet_angle=min_facet_angle,
+ max_radius_surface_delaunay_ball=max_radius_surface_delaunay_ball,
+ max_facet_distance=max_facet_distance,
+ max_circumradius_edge_ratio=max_circumradius_edge_ratio,
+ max_cell_circumradius=max_cell_circumradius,
+ exude_time_limit=exude_time_limit,
+ exude_sliver_bound=exude_sliver_bound,
+ verbose=verbose,
+ seed=seed,
+ )
+ else:
+ assert isinstance(max_cell_circumradius, dict)
+ if "default" in max_cell_circumradius.keys():
+ default_max_cell_circumradius = max_cell_circumradius.pop("default")
+ else:
+ default_max_cell_circumradius = 0.0
+
+ max_cell_circumradiuss = list(max_cell_circumradius.values())
+ subdomain_labels = list(max_cell_circumradius.keys())
+
+ _generate_from_inr_with_subdomain_sizing(
+ inr_filename,
+ outfile,
+ default_max_cell_circumradius,
+ max_cell_circumradiuss,
+ subdomain_labels,
+ lloyd=lloyd,
+ odt=odt,
+ perturb=perturb,
+ exude=exude,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ min_facet_angle=min_facet_angle,
+ max_radius_surface_delaunay_ball=max_radius_surface_delaunay_ball,
+ max_facet_distance=max_facet_distance,
+ max_circumradius_edge_ratio=max_circumradius_edge_ratio,
+ verbose=verbose,
+ seed=seed,
+ )
+
+ mesh = meshio.read(outfile)
+ os.remove(outfile)
+ return mesh
+
+
+def remesh_surface(
+ filename: str,
+ max_edge_size_at_feature_edges: float = 0.0,
+ min_facet_angle: float = 0.0,
+ max_radius_surface_delaunay_ball: float = 0.0,
+ max_facet_distance: float = 0.0,
+ verbose: bool = True,
+ seed: int = 0,
+):
+ mesh = meshio.read(filename)
+
+ fh, off_file = tempfile.mkstemp(suffix=".off")
+ os.close(fh)
+ meshio.write(off_file, mesh)
+
+ fh, outfile = tempfile.mkstemp(suffix=".off")
+ os.close(fh)
+
+ _remesh_surface(
+ off_file,
+ outfile,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ min_facet_angle=min_facet_angle,
+ max_radius_surface_delaunay_ball=max_radius_surface_delaunay_ball,
+ max_facet_distance=max_facet_distance,
+ verbose=verbose,
+ seed=seed,
+ )
+
+ mesh = meshio.read(outfile)
+ os.remove(off_file)
+ os.remove(outfile)
+ return mesh
+
+
+def save_inr(vol, voxel_size: tuple[float, float, float], fname: str):
+ """
+ Save a volume (described as a numpy array) to INR format.
+ Code inspired by iso2mesh (http://iso2mesh.sf.net) by Q. Fang
+ INPUTS:
+ - vol: volume as a 3D numpy array
+ - h: voxel sizes
+ - fname: filename for saving the inr file
+ """
+ fid = open(fname, "wb")
+
+ btype, bitlen = {
+ "uint8": ("unsigned fixed", 8),
+ "uint16": ("unsigned fixed", 16),
+ "float32": ("float", 32),
+ "float64": ("float", 64),
+ }[vol.dtype.name]
+
+ xdim, ydim, zdim = vol.shape
+ header = "\n".join(
+ [
+ "#INRIMAGE-4#{",
+ f"XDIM={xdim}",
+ f"YDIM={ydim}",
+ f"ZDIM={zdim}",
+ "VDIM=1",
+ f"TYPE={btype}",
+ f"PIXSIZE={bitlen} bits",
+ "CPU=decm",
+ f"VX={voxel_size[0]:f}",
+ f"VY={voxel_size[1]:f}",
+ f"VZ={voxel_size[2]:f}",
+ ]
+ )
+ header += "\n"
+
+ header = header + "\n" * (256 - 4 - len(header)) + "##}\n"
+
+ fid.write(header.encode("ascii"))
+ fid.write(vol.tobytes(order="F"))
+
+
+def generate_from_array(
+ vol,
+ voxel_size: tuple[float, float, float],
+ lloyd: bool = False,
+ odt: bool = False,
+ perturb: bool = True,
+ exude: bool = True,
+ max_edge_size_at_feature_edges: float = 0.0,
+ min_facet_angle: float = 0.0,
+ max_radius_surface_delaunay_ball: float = 0.0,
+ max_cell_circumradius: float | dict[int | str, float] = 0.0,
+ max_facet_distance: float = 0.0,
+ max_circumradius_edge_ratio: float = 0.0,
+ verbose: bool = True,
+ seed: int = 0,
+):
+ assert vol.dtype in ["uint8", "uint16"]
+ fh, inr_filename = tempfile.mkstemp(suffix=".inr")
+ os.close(fh)
+ save_inr(vol, voxel_size, inr_filename)
+ mesh = generate_from_inr(
+ inr_filename,
+ lloyd,
+ odt,
+ perturb,
+ exude,
+ max_edge_size_at_feature_edges,
+ min_facet_angle,
+ max_radius_surface_delaunay_ball,
+ max_facet_distance,
+ max_circumradius_edge_ratio,
+ max_cell_circumradius,
+ verbose,
+ seed,
+ )
+ os.remove(inr_filename)
+ return mesh
--- /dev/null
+[build-system]
+requires = ["setuptools>=42", "wheel", "pybind11>=2.6.0"]
+build-backend = "setuptools.build_meta"
+
+[tool.isort]
+profile = "black"
--- /dev/null
+[metadata]
+name = pygalmesh
+version = 0.10.6
+author = Nico Schlömer
+author_email = nico.schloemer@gmail.com
+description = Python frontend to CGAL's mesh generation capabilities
+url = https://github.com/nschloe/pygalmesh
+project_urls =
+ Code=https://github.com/nschloe/pygalmesh
+ Issues=https://github.com/nschloe/pygalmesh/issues
+ Funding=https://github.com/sponsors/nschloe
+long_description = file: README.md
+long_description_content_type = text/markdown
+license = GPL-3.0-or-later
+classifiers =
+ Development Status :: 4 - Beta
+ License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
+ Operating System :: OS Independent
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.7
+ Programming Language :: Python :: 3.8
+ Programming Language :: Python :: 3.9
+ Programming Language :: Python :: 3.10
+ Topic :: Scientific/Engineering
+ Topic :: Scientific/Engineering :: Mathematics
+ Topic :: Scientific/Engineering :: Physics
+ Topic :: Scientific/Engineering :: Visualization
+keywords =
+ mathematics
+ physics
+ engineering
+ cgal
+ mesh
+ mesh generation
+
+[options]
+packages = find:
+install_requires =
+ importlib_metadata;python_version<"3.8"
+ meshio >= 4.0.0, < 6.0.0
+ numpy
+python_requires = >=3.7
+
+[options.entry_points]
+console_scripts =
+ pygalmesh = pygalmesh._cli:cli
--- /dev/null
+import os
+
+from pybind11.setup_helpers import Pybind11Extension, build_ext
+from setuptools import setup
+
+# https://github.com/pybind/python_example/
+ext_modules = [
+ Pybind11Extension(
+ "_pygalmesh",
+ # Sort input source files to ensure bit-for-bit reproducible builds
+ # (https://github.com/pybind/python_example/pull/53)
+ sorted(
+ [
+ "src/generate.cpp",
+ "src/generate_2d.cpp",
+ "src/generate_from_inr.cpp",
+ "src/generate_from_off.cpp",
+ "src/generate_periodic.cpp",
+ "src/generate_surface_mesh.cpp",
+ "src/remesh_surface.cpp",
+ "src/pybind11.cpp",
+ ]
+ ),
+ include_dirs=[
+ os.environ.get("EIGEN_INCLUDE_DIR", "/usr/include/eigen3/"),
+ # macos/brew:
+ "/usr/local/include/eigen3",
+ ],
+ # no CGAL libraries necessary from CGAL 5.0 onwards
+ libraries=["gmp", "mpfr"],
+ )
+]
+
+if __name__ == "__main__":
+ setup(
+ cmdclass={"build_ext": build_ext},
+ ext_modules=ext_modules,
+ zip_safe=False,
+ )
--- /dev/null
+FIND_PACKAGE(pybind11 REQUIRED)
+# include_directories(${PYBIND11_INCLUDE_DIR})
+
+FIND_PACKAGE(Eigen3 REQUIRED)
+include_directories(${EIGEN3_INCLUDE_DIR})
+
+FIND_PACKAGE(CGAL REQUIRED)
+include(${CGAL_USE_FILE})
+
+FILE(GLOB pygalmesh_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp")
+# FILE(GLOB pygalmesh_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/*.hpp")
+
+pybind11_add_module(pygalmesh ${pygalmesh_SRCS})
+
+# ADD_LIBRARY(pygalmesh ${pygalmesh_SRCS})
+target_link_libraries(pygalmesh PRIVATE ${CGAL_LIBRARIES})
+
+# https://github.com/CGAL/cgal/issues/6002
+# find_program(iwyu_path NAMES include-what-you-use iwyu REQUIRED)
+# set_property(TARGET pygalmesh PROPERTY CXX_INCLUDE_WHAT_YOU_USE ${iwyu_path})
+
+# execute_process(
+# COMMAND python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()"
+# OUTPUT_VARIABLE PYTHON_SITE_PACKAGES
+# OUTPUT_STRIP_TRAILING_WHITESPACE
+# )
+# install(TARGETS _pygalmesh DESTINATION ${PYTHON_SITE_PACKAGES})
+# install(
+# FILES ${CMAKE_BINARY_DIR}/src/pygalmesh.py
+# DESTINATION ${PYTHON_SITE_PACKAGES}
+# )
--- /dev/null
+#ifndef DOMAIN_HPP
+#define DOMAIN_HPP
+
+#include <Eigen/Dense>
+#include <array>
+#include <limits>
+#include <memory>
+#include <vector>
+
+namespace pygalmesh {
+
+class DomainBase
+{
+ public:
+
+ virtual ~DomainBase() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const = 0;
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const = 0;
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ return {};
+ };
+};
+
+class Translate: public pygalmesh::DomainBase
+{
+ public:
+ Translate(
+ const std::shared_ptr<const pygalmesh::DomainBase> & domain,
+ const std::array<double, 3> & direction
+ ):
+ domain_(domain),
+ direction_(Eigen::Vector3d(direction.data())),
+ translated_features_(translate_features(domain->get_features(), direction_))
+ {
+ }
+
+ virtual ~Translate() = default;
+
+ std::vector<std::vector<std::array<double, 3>>>
+ translate_features(
+ const std::vector<std::vector<std::array<double, 3>>> & features,
+ const Eigen::Vector3d & direction
+ ) const
+ {
+ std::vector<std::vector<std::array<double, 3>>> translated_features;
+ for (const auto & feature: features) {
+ std::vector<std::array<double, 3>> translated_feature;
+ for (const auto & point: feature) {
+ const std::array<double, 3> translated_point = {
+ point[0] + direction[0],
+ point[1] + direction[1],
+ point[2] + direction[2]
+ };
+ translated_feature.push_back(translated_point);
+ }
+ translated_features.push_back(translated_feature);
+ }
+ return translated_features;
+ }
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ const std::array<double, 3> d = {
+ x[0] - direction_[0],
+ x[1] - direction_[1],
+ x[2] - direction_[2]
+ };
+ return domain_->eval(d);
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ const double radius = sqrt(domain_->get_bounding_sphere_squared_radius());
+ const double dir_norm = direction_.norm();
+ return (radius + dir_norm)*(radius + dir_norm);
+ }
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ return translated_features_;
+ };
+
+ private:
+ const std::shared_ptr<const pygalmesh::DomainBase> domain_;
+ const Eigen::Vector3d direction_;
+ const std::vector<std::vector<std::array<double, 3>>> translated_features_;
+};
+
+class Rotate: public pygalmesh::DomainBase
+{
+ public:
+ Rotate(
+ const std::shared_ptr<const pygalmesh::DomainBase> & domain,
+ const std::array<double, 3> & axis,
+ const double angle
+ ):
+ domain_(domain),
+ normalized_axis_(Eigen::Vector3d(axis.data()).normalized()),
+ sinAngle_(sin(angle)),
+ cosAngle_(cos(angle)),
+ rotated_features_(rotate_features(domain_->get_features()))
+ {
+ }
+
+ virtual ~Rotate() = default;
+
+ Eigen::Vector3d
+ rotate(
+ const Eigen::Vector3d & vec,
+ const Eigen::Vector3d & axis,
+ const double sinAngle,
+ const double cosAngle
+ ) const
+ {
+ // Rotate a vector `v` by the angle `theta` in the plane perpendicular
+ // to the axis given by `u`.
+ // Refer to
+ // http://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle
+ //
+ // cos(theta) * I * v
+ // + sin(theta) u\cross v
+ // + (1-cos(theta)) (u*u^T) * v
+ return cosAngle * vec
+ + sinAngle * axis.cross(vec)
+ + (1.0-cosAngle) * axis.dot(vec) * axis;
+ }
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ // rotate with negative angle
+ const auto p2 = rotate(
+ Eigen::Vector3d(x.data()),
+ normalized_axis_,
+ -sinAngle_,
+ cosAngle_
+ );
+ return domain_->eval({p2[0], p2[1], p2[2]});
+ }
+
+ std::vector<std::vector<std::array<double, 3>>>
+ rotate_features(
+ const std::vector<std::vector<std::array<double, 3>>> & features
+ ) const
+ {
+ std::vector<std::vector<std::array<double, 3>>> rotated_features;
+ for (const auto & feature: features) {
+ std::vector<std::array<double, 3>> rotated_feature;
+ for (const auto & point: feature) {
+ const auto p2 = rotate(
+ Eigen::Vector3d(point.data()),
+ normalized_axis_,
+ sinAngle_,
+ cosAngle_
+ );
+ rotated_feature.push_back({p2[0], p2[1], p2[2]});
+ }
+ rotated_features.push_back(rotated_feature);
+ }
+ return rotated_features;
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ return domain_->get_bounding_sphere_squared_radius();
+ }
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ return rotated_features_;
+ };
+
+ private:
+ const std::shared_ptr<const pygalmesh::DomainBase> domain_;
+ const Eigen::Vector3d normalized_axis_;
+ const double sinAngle_;
+ const double cosAngle_;
+ const std::vector<std::vector<std::array<double, 3>>> rotated_features_;
+};
+
+class Scale: public pygalmesh::DomainBase
+{
+ public:
+ Scale(
+ std::shared_ptr<const pygalmesh::DomainBase> & domain,
+ const double alpha
+ ):
+ domain_(domain),
+ alpha_(alpha),
+ scaled_features_(scale_features(domain_->get_features()))
+ {
+ assert(alpha_ > 0.0);
+ }
+
+ virtual ~Scale() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ return domain_->eval({x[0]/alpha_, x[1]/alpha_, x[2]/alpha_});
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ return alpha_*alpha_ * domain_->get_bounding_sphere_squared_radius();
+ }
+
+ std::vector<std::vector<std::array<double, 3>>>
+ scale_features(
+ const std::vector<std::vector<std::array<double, 3>>> & features
+ ) const
+ {
+ std::vector<std::vector<std::array<double, 3>>> scaled_features;
+ for (const auto & feature: features) {
+ std::vector<std::array<double, 3>> scaled_feature;
+ for (const auto & point: feature) {
+ scaled_feature.push_back({
+ alpha_ * point[0],
+ alpha_ * point[1],
+ alpha_ * point[2]
+ });
+ }
+ scaled_features.push_back(scaled_feature);
+ }
+ return scaled_features;
+ }
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ return scaled_features_;
+ };
+
+ private:
+ std::shared_ptr<const pygalmesh::DomainBase> domain_;
+ const double alpha_;
+ const std::vector<std::vector<std::array<double, 3>>> scaled_features_;
+};
+
+class Stretch: public pygalmesh::DomainBase
+{
+ public:
+ Stretch(
+ std::shared_ptr<const pygalmesh::DomainBase> & domain,
+ const std::array<double, 3> & direction
+ ):
+ domain_(domain),
+ normalized_direction_(Eigen::Vector3d(direction.data()).normalized()),
+ alpha_(Eigen::Vector3d(direction.data()).norm()),
+ stretched_features_(stretch_features(domain_->get_features()))
+ {
+ assert(alpha_ > 0.0);
+ }
+
+ virtual ~Stretch() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ const Eigen::Vector3d v(x.data());
+ const double beta = normalized_direction_.dot(v);
+ // scale the component of normalized_direction_ by 1/alpha_
+ const auto v2 = beta/alpha_ * normalized_direction_
+ + (v - beta * normalized_direction_);
+ return domain_->eval({v2[0], v2[1], v2[2]});
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ return alpha_*alpha_ * domain_->get_bounding_sphere_squared_radius();
+ }
+
+ std::vector<std::vector<std::array<double, 3>>>
+ stretch_features(
+ const std::vector<std::vector<std::array<double, 3>>> & features
+ ) const
+ {
+ std::vector<std::vector<std::array<double, 3>>> stretched_features;
+ for (const auto & feature: features) {
+ std::vector<std::array<double, 3>> stretched_feature;
+ for (const auto & point: feature) {
+ // scale the component of normalized_direction_ by alpha_
+ const Eigen::Vector3d v(point.data());
+ const double beta = normalized_direction_.dot(v);
+ const auto v2 = beta * alpha_ * normalized_direction_
+ + (v - beta * normalized_direction_);
+ stretched_feature.push_back({v2[0], v2[1], v2[2]});
+ }
+ stretched_features.push_back(stretched_feature);
+ }
+ return stretched_features;
+ }
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ return stretched_features_;
+ };
+
+ private:
+ std::shared_ptr<const pygalmesh::DomainBase> domain_;
+ const Eigen::Vector3d normalized_direction_;
+ const double alpha_;
+ const std::vector<std::vector<std::array<double, 3>>> stretched_features_;
+};
+
+class Intersection: public pygalmesh::DomainBase
+{
+ public:
+ explicit Intersection(
+ std::vector<std::shared_ptr<const pygalmesh::DomainBase>> & domains
+ ):
+ domains_(domains)
+ {
+ }
+
+ virtual ~Intersection() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ // TODO find a differentiable expression
+ double maxval = std::numeric_limits<double>::lowest();
+ for (const auto & domain: domains_) {
+ maxval = std::max(maxval, domain->eval(x));
+ }
+ return maxval;
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ double min = std::numeric_limits<double>::max();
+ for (const auto & domain: domains_) {
+ min = std::min(min, domain->get_bounding_sphere_squared_radius());
+ }
+ return min;
+ }
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ std::vector<std::vector<std::array<double, 3>>> features;
+ for (const auto & domain: domains_) {
+ const auto f = domain->get_features();
+ features.insert(std::end(features), std::begin(f), std::end(f));
+ }
+ return features;
+ };
+
+ private:
+ std::vector<std::shared_ptr<const pygalmesh::DomainBase>> domains_;
+};
+
+class Union: public pygalmesh::DomainBase
+{
+ public:
+ explicit Union(
+ std::vector<std::shared_ptr<const pygalmesh::DomainBase>> & domains
+ ):
+ domains_(domains)
+ {
+ }
+
+ virtual ~Union() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ // TODO find a differentiable expression
+ double minval = std::numeric_limits<double>::max();
+ for (const auto & domain: domains_) {
+ minval = std::min(minval, domain->eval(x));
+ }
+ return minval;
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ double max = 0.0;
+ for (const auto & domain: domains_) {
+ max = std::max(max, domain->get_bounding_sphere_squared_radius());
+ }
+ return max;
+ }
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ std::vector<std::vector<std::array<double, 3>>> features;
+ for (const auto & domain: domains_) {
+ const auto f = domain->get_features();
+ features.insert(std::end(features), std::begin(f), std::end(f));
+ }
+ return features;
+ };
+
+ private:
+ std::vector<std::shared_ptr<const pygalmesh::DomainBase>> domains_;
+};
+
+class Difference: public pygalmesh::DomainBase
+{
+ public:
+ Difference(
+ std::shared_ptr<const pygalmesh::DomainBase> & domain0,
+ std::shared_ptr<const pygalmesh::DomainBase> & domain1
+ ):
+ domain0_(domain0),
+ domain1_(domain1)
+ {
+ }
+
+ virtual ~Difference() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ // TODO find a continuous (perhaps even differentiable) expression
+ const double val0 = domain0_->eval(x);
+ const double val1 = domain1_->eval(x);
+ return (val0 < 0.0 && val1 >= 0.0) ? val0 : std::max(val0, -val1);
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ return domain0_->get_bounding_sphere_squared_radius();
+ }
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ std::vector<std::vector<std::array<double, 3>>> features;
+
+ const auto f0 = domain0_->get_features();
+ features.insert(std::end(features), std::begin(f0), std::end(f0));
+
+ const auto f1 = domain1_->get_features();
+ features.insert(std::end(features), std::begin(f1), std::end(f1));
+
+ return features;
+ };
+
+ private:
+ std::shared_ptr<const pygalmesh::DomainBase> domain0_;
+ std::shared_ptr<const pygalmesh::DomainBase> domain1_;
+};
+
+} // namespace pygalmesh
+#endif // DOMAIN_HPP
--- /dev/null
+#define CGAL_MESH_3_VERBOSE 1
+
+#include "generate.hpp"
+
+#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
+
+#include <CGAL/Mesh_triangulation_3.h>
+#include <CGAL/Mesh_complex_3_in_triangulation_3.h>
+#include <CGAL/Mesh_criteria_3.h>
+
+#include <CGAL/Implicit_mesh_domain_3.h>
+#include <CGAL/Mesh_domain_with_polyline_features_3.h>
+#include <CGAL/make_mesh_3.h>
+
+namespace pygalmesh {
+
+typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
+
+typedef CGAL::Mesh_domain_with_polyline_features_3<CGAL::Labeled_mesh_domain_3<K>> Mesh_domain;
+
+// Triangulation
+typedef CGAL::Mesh_triangulation_3<Mesh_domain>::type Tr;
+typedef CGAL::Mesh_complex_3_in_triangulation_3<Tr> C3t3;
+
+// Mesh Criteria
+typedef CGAL::Mesh_criteria_3<Tr> Mesh_criteria;
+typedef Mesh_criteria::Edge_criteria Edge_criteria;
+typedef Mesh_criteria::Facet_criteria Facet_criteria;
+typedef Mesh_criteria::Cell_criteria Cell_criteria;
+
+// translate vector<vector<array<double, 3>> to list<vector<Point_3>>
+std::list<std::vector<K::Point_3>>
+translate_feature_edges(
+ const std::vector<std::vector<std::array<double, 3>>> & feature_edges
+ )
+{
+ std::list<std::vector<K::Point_3>> polylines;
+ for (const auto & feature_edge: feature_edges) {
+ std::vector<K::Point_3> polyline;
+ for (const auto & point: feature_edge) {
+ polyline.push_back(K::Point_3(point[0], point[1], point[2]));
+ }
+ polylines.push_back(polyline);
+ }
+ return polylines;
+}
+
+void
+generate_mesh(
+ const std::shared_ptr<pygalmesh::DomainBase> & domain,
+ const std::string & outfile,
+ const std::vector<std::vector<std::array<double, 3>>> & extra_feature_edges,
+ const double bounding_sphere_radius,
+ const bool lloyd,
+ const bool odt,
+ const bool perturb,
+ const bool exude,
+ //
+ const double max_edge_size_at_feature_edges_value,
+ const std::shared_ptr<pygalmesh::SizingFieldBase> & max_edge_size_at_feature_edges_field,
+ //
+ const double min_facet_angle,
+ //
+ const double max_radius_surface_delaunay_ball_value,
+ const std::shared_ptr<pygalmesh::SizingFieldBase> & max_radius_surface_delaunay_ball_field,
+ //
+ const double max_facet_distance_value,
+ const std::shared_ptr<pygalmesh::SizingFieldBase> & max_facet_distance_field,
+ //
+ const double max_circumradius_edge_ratio,
+ //
+ const double max_cell_circumradius_value,
+ const std::shared_ptr<pygalmesh::SizingFieldBase> & max_cell_circumradius_field,
+ //
+ const double exude_time_limit,
+ const double exude_sliver_bound,
+ //
+ const bool verbose,
+ const int seed
+ )
+{
+ CGAL::get_default_random() = CGAL::Random(seed);
+
+ const double bounding_sphere_radius2 = bounding_sphere_radius > 0 ?
+ bounding_sphere_radius*bounding_sphere_radius :
+ // some wiggle room
+ 1.01 * domain->get_bounding_sphere_squared_radius();
+
+ // wrap domain
+ const auto d = [&](K::Point_3 p) {
+ return domain->eval({p.x(), p.y(), p.z()});
+ };
+
+ Mesh_domain cgal_domain = Mesh_domain::create_implicit_mesh_domain(
+ d,
+ K::Sphere_3(CGAL::ORIGIN, bounding_sphere_radius2)
+ );
+
+ // cgal_domain.detect_features();
+
+ const auto native_features = translate_feature_edges(domain->get_features());
+ cgal_domain.add_features(native_features.begin(), native_features.end());
+
+ const auto polylines = translate_feature_edges(extra_feature_edges);
+ cgal_domain.add_features(polylines.begin(), polylines.end());
+
+ // perhaps there's a more elegant solution here
+ // see <https://github.com/CGAL/cgal/issues/1286>
+ if (!verbose) {
+ // suppress output
+ std::cerr.setstate(std::ios_base::failbit);
+ }
+
+ // Build the float/field values according to
+ // <https://github.com/CGAL/cgal/issues/5044#issuecomment-705526982>.
+
+ // nested ternary operator
+ const auto facet_criteria = max_radius_surface_delaunay_ball_field ? (
+ max_facet_distance_field ?
+ Facet_criteria(
+ min_facet_angle,
+ [&](K::Point_3 p, const int, const Mesh_domain::Index&) {
+ return max_radius_surface_delaunay_ball_field->eval({p.x(), p.y(), p.z()});
+ },
+ [&](K::Point_3 p, const int, const Mesh_domain::Index&) {
+ return max_facet_distance_field->eval({p.x(), p.y(), p.z()});
+ }
+ ) : Facet_criteria(
+ min_facet_angle,
+ [&](K::Point_3 p, const int, const Mesh_domain::Index&) {
+ return max_radius_surface_delaunay_ball_field->eval({p.x(), p.y(), p.z()});
+ },
+ max_facet_distance_value
+ )
+ ) : (
+ max_facet_distance_field ?
+ Facet_criteria(
+ min_facet_angle,
+ max_radius_surface_delaunay_ball_value,
+ [&](K::Point_3 p, const int, const Mesh_domain::Index&) {
+ return max_facet_distance_field->eval({p.x(), p.y(), p.z()});
+ }
+ ) : Facet_criteria(
+ min_facet_angle,
+ max_radius_surface_delaunay_ball_value,
+ max_facet_distance_value
+ )
+ );
+
+ const auto edge_criteria = max_edge_size_at_feature_edges_field ?
+ Edge_criteria(
+ [&](K::Point_3 p, const int, const Mesh_domain::Index&) {
+ return max_edge_size_at_feature_edges_field->eval({p.x(), p.y(), p.z()});
+ }) : Edge_criteria(max_edge_size_at_feature_edges_value);
+
+ const auto cell_criteria = max_cell_circumradius_field ?
+ Cell_criteria(
+ max_circumradius_edge_ratio,
+ [&](K::Point_3 p, const int, const Mesh_domain::Index&) {
+ return max_cell_circumradius_field->eval({p.x(), p.y(), p.z()});
+ }) : Cell_criteria(max_circumradius_edge_ratio, max_cell_circumradius_value);
+
+ const auto criteria = Mesh_criteria(edge_criteria, facet_criteria, cell_criteria);
+
+ // Mesh generation
+ C3t3 c3t3 = CGAL::make_mesh_3<C3t3>(
+ cgal_domain,
+ criteria,
+ lloyd ? CGAL::parameters::lloyd() : CGAL::parameters::no_lloyd(),
+ odt ? CGAL::parameters::odt() : CGAL::parameters::no_odt(),
+ perturb ? CGAL::parameters::perturb() : CGAL::parameters::no_perturb(),
+ exude ?
+ CGAL::parameters::exude(
+ CGAL::parameters::time_limit = exude_time_limit,
+ CGAL::parameters::sliver_bound = exude_sliver_bound
+ ) :
+ CGAL::parameters::no_exude()
+ );
+ if (!verbose) {
+ std::cerr.clear();
+ }
+
+ // Output
+ std::ofstream medit_file(outfile);
+ c3t3.output_to_medit(medit_file);
+ medit_file.close();
+
+ return;
+}
+
+} // namespace pygalmesh
--- /dev/null
+#ifndef GENERATE_HPP
+#define GENERATE_HPP
+
+#include "domain.hpp"
+#include "sizing_field.hpp"
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace pygalmesh {
+
+void generate_mesh(
+ const std::shared_ptr<pygalmesh::DomainBase> & domain,
+ const std::string & outfile,
+ const std::vector<std::vector<std::array<double, 3>>> & extra_feature_edges = {},
+ const double bounding_sphere_radius = 0.0,
+ const bool lloyd = false,
+ const bool odt = false,
+ const bool perturb = true,
+ const bool exude = true,
+ //
+ const double max_edge_size_at_feature_edges_value = 0.0, // std::numeric_limits<double>::max(),
+ const std::shared_ptr<pygalmesh::SizingFieldBase> & max_edge_size_at_feature_edges_field = nullptr,
+ //
+ const double min_facet_angle = 0.0,
+ //
+ const double max_radius_surface_delaunay_ball_value = 0.0,
+ const std::shared_ptr<pygalmesh::SizingFieldBase> & max_radius_surface_delaunay_ball_field = nullptr,
+ //
+ const double max_facet_distance_value = 0.0,
+ const std::shared_ptr<pygalmesh::SizingFieldBase> & max_facet_distance_field = nullptr,
+ //
+ const double max_circumradius_edge_ratio = 0.0,
+ //
+ const double max_cell_circumradius_value = 0.0,
+ const std::shared_ptr<pygalmesh::SizingFieldBase> & max_cell_circumradius_field = nullptr,
+ //
+ const double exude_time_limit = 0.0,
+ const double exude_sliver_bound = 0.0,
+ //
+ const bool verbose = true,
+ const int seed = 0
+ );
+
+} // namespace pygalmesh
+
+#endif // GENERATE_HPP
--- /dev/null
+#define CGAL_MESH_3_VERBOSE 1
+
+#include "generate_2d.hpp"
+
+#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
+#include <CGAL/Constrained_Delaunay_triangulation_2.h>
+#include <CGAL/Delaunay_mesher_2.h>
+#include <CGAL/Delaunay_mesh_face_base_2.h>
+#include <CGAL/Delaunay_mesh_vertex_base_2.h>
+#include <CGAL/Delaunay_mesh_size_criteria_2.h>
+#include <CGAL/lloyd_optimize_mesh_2.h>
+
+namespace pygalmesh {
+
+typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
+typedef CGAL::Delaunay_mesh_vertex_base_2<K> Vb;
+typedef CGAL::Delaunay_mesh_face_base_2<K> Fb;
+typedef CGAL::Triangulation_data_structure_2<Vb, Fb> Tds;
+typedef CGAL::Constrained_Delaunay_triangulation_2<K, Tds> CDT;
+typedef CGAL::Delaunay_mesh_size_criteria_2<CDT> Criteria;
+typedef CDT::Vertex_handle Vertex_handle;
+typedef CDT::Point Point;
+
+std::tuple<std::vector<std::array<double, 2>>, std::vector<std::array<int, 3>>>
+generate_2d(
+ const std::vector<std::array<double, 2>> & points,
+ const std::vector<std::array<int, 2>> & constraints,
+ // See
+ // https://doc.cgal.org/latest/Mesh_2/classCGAL_1_1Delaunay__mesh__size__criteria__2.html#a58b0186eae407ba76b8f4a3d0aa85a1a
+ // for what the bounds mean. Spoiler:
+ // B = circumradius / shortest_edge,
+ // relates to the smallest angle via sin(alpha_min) = 1 / (2B)
+ // edge_size S: "all segments of all triangles must be shorter than a bound S."
+ const double max_circumradius_shortest_edge_ratio,
+ const double max_edge_size,
+ const int num_lloyd_steps
+)
+{
+ CDT cdt;
+ // construct a constrained triangulation
+ std::vector<Vertex_handle> vertices(points.size());
+ int k = 0;
+ for (auto pt: points) {
+ vertices[k] = cdt.insert(Point(pt[0], pt[1]));
+ k++;
+ }
+ for (auto c: constraints) {
+ cdt.insert_constraint(vertices[c[0]], vertices[c[1]]);
+ }
+
+ // create proper mesh
+ CGAL::refine_Delaunay_mesh_2(
+ cdt,
+ Criteria(
+ 0.25 / (max_circumradius_shortest_edge_ratio * max_circumradius_shortest_edge_ratio),
+ max_edge_size
+ )
+ );
+
+ if (num_lloyd_steps > 0) {
+ CGAL::lloyd_optimize_mesh_2(
+ cdt,
+ CGAL::parameters::max_iteration_number = num_lloyd_steps
+ );
+ }
+
+ // convert points to vector of arrays
+ std::map<Vertex_handle, int> vertex_index;
+ std::vector<std::array<double, 2>> out_points(cdt.number_of_vertices());
+ k = 0;
+ for (auto vit: cdt.finite_vertex_handles()) {
+ out_points[k][0] = vit->point()[0];
+ out_points[k][1] = vit->point()[1];
+ vertex_index[vit] = k;
+ k++;
+ }
+
+ // https://github.com/CGAL/cgal/issues/5068#issuecomment-706213606
+ auto nb_faces = 0u;
+ for (auto fit: cdt.finite_face_handles()) {
+ if(fit->is_in_domain()) ++nb_faces;
+ }
+ std::vector<std::array<int, 3>> out_cells(nb_faces);
+ k = 0;
+ for (auto fit: cdt.finite_face_handles()) {
+ if(!fit->is_in_domain()) continue;
+ out_cells[k][0] = vertex_index[fit->vertex(0)];
+ out_cells[k][1] = vertex_index[fit->vertex(1)];
+ out_cells[k][2] = vertex_index[fit->vertex(2)];
+ k++;
+ }
+
+ return std::make_tuple(out_points, out_cells);
+}
+
+} // namespace pygalmesh
--- /dev/null
+#ifndef GENERATE_2D_HPP
+#define GENERATE_2D_HPP
+
+#include <memory>
+#include <vector>
+
+namespace pygalmesh {
+
+std::tuple<std::vector<std::array<double, 2>>, std::vector<std::array<int, 3>>>
+generate_2d(
+ const std::vector<std::array<double, 2>> & points,
+ const std::vector<std::array<int, 2>> & constraints,
+ const double max_circumradius_shortest_edge_ratio,
+ // https://github.com/CGAL/cgal/issues/5061#issuecomment-705520984
+ // the "default" size criterion for a triangle in the 2D mesh generator refers to its
+ // edge lengths. In the output mesh, all segments of all triangles must be shorter
+ // than the given bound.
+ const double max_edge_size,
+ const int num_lloyd_steps
+);
+
+} // namespace pygalmesh
+
+#endif // GENERATE_2D_HPP
--- /dev/null
+#define CGAL_MESH_3_VERBOSE 1
+
+#include "generate_from_inr.hpp"
+
+#include <cassert>
+
+#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
+#include <CGAL/Image_3.h>
+
+#include <CGAL/Mesh_triangulation_3.h>
+#include <CGAL/Mesh_complex_3_in_triangulation_3.h>
+#include <CGAL/Mesh_criteria_3.h>
+
+#include <CGAL/Implicit_mesh_domain_3.h>
+#include <CGAL/Mesh_domain_with_polyline_features_3.h>
+#include <CGAL/make_mesh_3.h>
+
+namespace pygalmesh {
+
+typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
+
+typedef CGAL::Labeled_mesh_domain_3<K> Mesh_domain;
+
+// Triangulation
+typedef CGAL::Mesh_triangulation_3<Mesh_domain>::type Tr;
+typedef CGAL::Mesh_complex_3_in_triangulation_3<Tr> C3t3;
+
+// Mesh Criteria
+typedef CGAL::Mesh_criteria_3<Tr> Mesh_criteria;
+typedef Mesh_criteria::Facet_criteria Facet_criteria;
+typedef Mesh_criteria::Cell_criteria Cell_criteria;
+
+typedef CGAL::Mesh_constant_domain_field_3<Mesh_domain::R,
+ Mesh_domain::Index> Sizing_field_cell;
+
+void
+generate_from_inr(
+ const std::string & inr_filename,
+ const std::string & outfile,
+ const bool lloyd,
+ const bool odt,
+ const bool perturb,
+ const bool exude,
+ const double max_edge_size_at_feature_edges,
+ const double min_facet_angle,
+ const double max_radius_surface_delaunay_ball,
+ const double max_facet_distance,
+ const double max_circumradius_edge_ratio,
+ const double max_cell_circumradius,
+ const double exude_time_limit,
+ const double exude_sliver_bound,
+ const bool verbose,
+ const int seed
+ )
+{
+ CGAL::get_default_random() = CGAL::Random(seed);
+
+ CGAL::Image_3 image;
+ const bool success = image.read(inr_filename.c_str());
+ if (!success) {
+ throw "Could not read image file";
+ }
+ Mesh_domain cgal_domain = Mesh_domain::create_labeled_image_mesh_domain(image);
+
+ Mesh_criteria criteria(
+ CGAL::parameters::edge_size=max_edge_size_at_feature_edges,
+ CGAL::parameters::facet_angle=min_facet_angle,
+ CGAL::parameters::facet_size=max_radius_surface_delaunay_ball,
+ CGAL::parameters::facet_distance=max_facet_distance,
+ CGAL::parameters::cell_radius_edge_ratio=max_circumradius_edge_ratio,
+ CGAL::parameters::cell_size=max_cell_circumradius
+ );
+
+ // Mesh generation
+ if (!verbose) {
+ // suppress output
+ std::cerr.setstate(std::ios_base::failbit);
+ }
+ C3t3 c3t3 = CGAL::make_mesh_3<C3t3>(
+ cgal_domain,
+ criteria,
+ lloyd ? CGAL::parameters::lloyd() : CGAL::parameters::no_lloyd(),
+ odt ? CGAL::parameters::odt() : CGAL::parameters::no_odt(),
+ perturb ? CGAL::parameters::perturb() : CGAL::parameters::no_perturb(),
+ exude ?
+ CGAL::parameters::exude(
+ CGAL::parameters::time_limit = exude_time_limit,
+ CGAL::parameters::sliver_bound = exude_sliver_bound
+ ) :
+ CGAL::parameters::no_exude()
+ );
+ if (!verbose) {
+ std::cerr.clear();
+ }
+
+ // Output
+ std::ofstream medit_file(outfile);
+ c3t3.output_to_medit(medit_file);
+ medit_file.close();
+ return;
+}
+
+
+void
+generate_from_inr_with_subdomain_sizing(
+ const std::string & inr_filename,
+ const std::string & outfile,
+ const double default_max_cell_circumradius,
+ const std::vector<double> & max_cell_circumradiuss,
+ const std::vector<int> & cell_labels,
+ const bool lloyd,
+ const bool odt,
+ const bool perturb,
+ const bool exude,
+ const double max_edge_size_at_feature_edges,
+ const double min_facet_angle,
+ const double max_radius_surface_delaunay_ball,
+ const double max_facet_distance,
+ const double max_circumradius_edge_ratio,
+ const double exude_time_limit,
+ const double exude_sliver_bound,
+ const bool verbose,
+ const int seed
+ )
+{
+ CGAL::get_default_random() = CGAL::Random(seed);
+
+ CGAL::Image_3 image;
+ const bool success = image.read(inr_filename.c_str());
+ if (!success) {
+ throw "Could not read image file";
+ }
+ Mesh_domain cgal_domain = Mesh_domain::create_labeled_image_mesh_domain(image);
+
+ Sizing_field_cell max_cell_circumradius(default_max_cell_circumradius);
+ const int ndimensions = 3;
+ for(std::vector<double>::size_type i(0); i < max_cell_circumradiuss.size(); ++i)
+ max_cell_circumradius.set_size(max_cell_circumradiuss[i], ndimensions, cgal_domain.index_from_subdomain_index(cell_labels[i]));
+
+ Mesh_criteria criteria(
+ CGAL::parameters::edge_size=max_edge_size_at_feature_edges,
+ CGAL::parameters::facet_angle=min_facet_angle,
+ CGAL::parameters::facet_size=max_radius_surface_delaunay_ball,
+ CGAL::parameters::facet_distance=max_facet_distance,
+ CGAL::parameters::cell_radius_edge_ratio=max_circumradius_edge_ratio,
+ CGAL::parameters::cell_size=max_cell_circumradius
+ );
+
+ // Mesh generation
+ if (!verbose) {
+ // suppress output
+ std::cerr.setstate(std::ios_base::failbit);
+ }
+ C3t3 c3t3 = CGAL::make_mesh_3<C3t3>(
+ cgal_domain,
+ criteria,
+ lloyd ? CGAL::parameters::lloyd() : CGAL::parameters::no_lloyd(),
+ odt ? CGAL::parameters::odt() : CGAL::parameters::no_odt(),
+ perturb ? CGAL::parameters::perturb() : CGAL::parameters::no_perturb(),
+ exude ?
+ CGAL::parameters::exude(
+ CGAL::parameters::time_limit = exude_time_limit,
+ CGAL::parameters::sliver_bound = exude_sliver_bound
+ ) :
+ CGAL::parameters::no_exude()
+ );
+ if (!verbose) {
+ std::cerr.clear();
+ }
+
+ // Output
+ std::ofstream medit_file(outfile);
+ c3t3.output_to_medit(medit_file);
+ medit_file.close();
+ return;
+}
+
+} // namespace pygalmesh
--- /dev/null
+#ifndef GENERATE_FROM_INR_HPP
+#define GENERATE_FROM_INR_HPP
+
+#include <string>
+#include <vector>
+
+namespace pygalmesh {
+
+void generate_from_inr(
+ const std::string & inr_filename,
+ const std::string & outfile,
+ const bool lloyd = false,
+ const bool odt = false,
+ const bool perturb = true,
+ const bool exude = true,
+ const double max_edge_size_at_feature_edges = 0.0, // std::numeric_limits<double>::max(),
+ const double min_facet_angle = 0.0,
+ const double max_radius_surface_delaunay_ball = 0.0,
+ const double max_facet_distance = 0.0,
+ const double max_circumradius_edge_ratio = 0.0,
+ const double max_cell_circumradius = 0.0,
+ const double exude_time_limit = 0.0,
+ const double exude_sliver_bound = 0.0,
+ const bool verbose = true,
+ const int seed = 0
+ );
+
+void
+generate_from_inr_with_subdomain_sizing(
+ const std::string & inr_filename,
+ const std::string & outfile,
+ const double default_max_cell_circumradius,
+ const std::vector<double> & max_cell_circumradiuss,
+ const std::vector<int> & cell_labels,
+ const bool lloyd = false,
+ const bool odt = false,
+ const bool perturb = true,
+ const bool exude = true,
+ const double max_edge_size_at_feature_edges = 0.0,
+ const double min_facet_angle = 0.0,
+ const double max_radius_surface_delaunay_ball = 0.0,
+ const double max_facet_distance = 0.0,
+ const double max_circumradius_edge_ratio = 0.0,
+ const double exude_time_limit = 0.0,
+ const double exude_sliver_bound = 0.0,
+ const bool verbose = true,
+ const int seed = 0
+ );
+
+} // namespace pygalmesh
+
+#endif // GENERATE_FROM_INR_HPP
--- /dev/null
+#include "generate_from_off.hpp"
+
+#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
+#include <CGAL/Mesh_complex_3_in_triangulation_3.h>
+#include <CGAL/Mesh_criteria_3.h>
+#include <CGAL/Mesh_triangulation_3.h>
+#include <CGAL/Polyhedral_mesh_domain_3.h>
+#include <CGAL/make_mesh_3.h>
+#include <CGAL/refine_mesh_3.h>
+
+// IO
+#include <CGAL/IO/Polyhedron_iostream.h>
+
+// for re-orientation
+#include <CGAL/Polygon_mesh_processing/orient_polygon_soup.h>
+#include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h>
+#include <CGAL/Polygon_mesh_processing/orientation.h>
+
+#include <CGAL/version_macros.h>
+
+#if CGAL_VERSION_MAJOR >= 5 && CGAL_VERSION_MINOR < 3
+ #include <CGAL/IO/OFF_reader.h>
+#endif
+
+// for sharp features
+//#include <CGAL/Polyhedral_mesh_domain_with_features_3.h>
+
+namespace pygalmesh {
+
+// Domain
+typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
+typedef CGAL::Polyhedron_3<K> Polyhedron;
+typedef CGAL::Polyhedral_mesh_domain_3<Polyhedron, K> Mesh_domain;
+// for sharp features
+//typedef CGAL::Polyhedral_mesh_domain_with_features_3<K> Mesh_domain;
+//typedef CGAL::Mesh_polyhedron_3<K>::type Polyhedron;
+
+// Triangulation
+typedef CGAL::Mesh_triangulation_3<Mesh_domain>::type Tr;
+
+typedef CGAL::Mesh_complex_3_in_triangulation_3<Tr> C3t3;
+
+// Criteria
+typedef CGAL::Mesh_criteria_3<Tr> Mesh_criteria;
+
+// To avoid verbose function and named parameters call
+using namespace CGAL::parameters;
+
+void generate_from_off(
+ const std::string& infile,
+ const std::string& outfile,
+ const bool lloyd,
+ const bool odt,
+ const bool perturb,
+ const bool exude,
+ const double max_edge_size_at_feature_edges,
+ const double min_facet_angle,
+ const double max_radius_surface_delaunay_ball,
+ const double max_facet_distance,
+ const double max_circumradius_edge_ratio,
+ const double max_cell_circumradius,
+ const double exude_time_limit,
+ const double exude_sliver_bound,
+ const bool verbose,
+ const bool reorient,
+ const int seed
+) {
+ CGAL::get_default_random() = CGAL::Random(seed);
+
+ std::ifstream input(infile);
+ Polyhedron polyhedron;
+ // fix the orientation of the faces of the input file
+ if (reorient) {
+ std::stringstream msg;
+ msg << "fixing face orientation for \"" << infile <<"\""<< std::endl;
+ std::vector<K::Point_3> points;
+ std::vector<std::vector<std::size_t> > polygons;
+
+ if(
+ !input ||
+#if CGAL_VERSION_MAJOR >= 5 && CGAL_VERSION_MINOR >= 3
+ !CGAL::IO::read_OFF(input, points, polygons) ||
+#else
+ !CGAL::read_OFF(input, points, polygons) ||
+#endif
+ points.empty()
+ )
+ {
+ std::stringstream msg;
+ msg << "Cannot read .off file \"" << infile <<"\""<< std::endl;
+ throw std::runtime_error(msg.str());
+ }
+ // orient the polygons
+ CGAL::Polygon_mesh_processing::orient_polygon_soup(points, polygons);
+ // create the polyhedron
+ CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh(points, polygons, polyhedron);
+
+ } else {
+
+ // Create input polyhedron
+ input >> polyhedron;
+ if (!input.good()) {
+ // Even if the mesh exists, it may not be valid, see
+ // <https://github.com/CGAL/cgal/issues/4632>
+ std::stringstream msg;
+ msg << "Invalid input file \"" << infile << "\"" << std::endl;
+ msg << "If this is due to wrong face orientation, retry with the option --reorient \"" << std::endl;
+ throw std::runtime_error(msg.str());
+ }
+ }
+
+ input.close();
+
+ // Create domain
+ Mesh_domain cgal_domain(polyhedron);
+
+ // Get sharp features
+ // cgal_domain.detect_features();
+
+
+ // Mesh criteria
+ Mesh_criteria criteria(
+ CGAL::parameters::edge_size = max_edge_size_at_feature_edges,
+ CGAL::parameters::facet_angle = min_facet_angle,
+ CGAL::parameters::facet_size = max_radius_surface_delaunay_ball,
+ CGAL::parameters::facet_distance = max_facet_distance,
+ CGAL::parameters::cell_radius_edge_ratio = max_circumradius_edge_ratio,
+ CGAL::parameters::cell_size = max_cell_circumradius);
+
+ // Mesh generation
+ if (!verbose) {
+ // suppress output
+ std::cerr.setstate(std::ios_base::failbit);
+ }
+ C3t3 c3t3 = CGAL::make_mesh_3<C3t3>(
+ cgal_domain, criteria,
+ lloyd ? CGAL::parameters::lloyd() : CGAL::parameters::no_lloyd(),
+ odt ? CGAL::parameters::odt() : CGAL::parameters::no_odt(),
+ perturb ? CGAL::parameters::perturb() : CGAL::parameters::no_perturb(),
+ exude ?
+ CGAL::parameters::exude(
+ CGAL::parameters::time_limit = exude_time_limit,
+ CGAL::parameters::sliver_bound = exude_sliver_bound
+ ) :
+ CGAL::parameters::no_exude()
+ );
+ if (!verbose) {
+ std::cerr.clear();
+ }
+
+ // Output
+ std::ofstream medit_file(outfile);
+ c3t3.output_to_medit(medit_file);
+ medit_file.close();
+
+ return;
+}
+
+} // namespace pygalmesh
--- /dev/null
+#ifndef GENERATE_FROM_OFF_HPP
+#define GENERATE_FROM_OFF_HPP
+
+#include <string>
+#include <vector>
+
+namespace pygalmesh {
+
+void
+generate_from_off(
+ const std::string & infile,
+ const std::string & outfile,
+ const bool lloyd = false,
+ const bool odt = false,
+ const bool perturb = true,
+ const bool exude = true,
+ const double max_edge_size_at_feature_edges = 0.0, // std::numeric_limits<double>::max(),
+ const double min_facet_angle = 0.0,
+ const double max_radius_surface_delaunay_ball = 0.0,
+ const double max_facet_distance = 0.0,
+ const double max_circumradius_edge_ratio = 0.0,
+ const double max_cell_circumradius = 0.0,
+ const double exude_time_limit = 0.0,
+ const double exude_sliver_bound = 0.0,
+ const bool verbose = true,
+ const bool reorient = false,
+ const int seed = 0
+ );
+
+} // namespace pygalmesh
+
+#endif // GENERATE_FROM_OFF_HPP
--- /dev/null
+#define CGAL_MESH_3_VERBOSE 1
+
+#include "generate_periodic.hpp"
+
+#include <CGAL/Periodic_3_mesh_3/config.h>
+#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
+#include <CGAL/make_periodic_3_mesh_3.h>
+#include <CGAL/optimize_periodic_3_mesh_3.h>
+#include <CGAL/Periodic_3_mesh_3/IO/File_medit.h>
+#include <CGAL/Periodic_3_mesh_triangulation_3.h>
+#include <CGAL/Labeled_mesh_domain_3.h>
+#include <CGAL/Mesh_complex_3_in_triangulation_3.h>
+#include <CGAL/Mesh_criteria_3.h>
+#include <CGAL/number_type_config.h> // CGAL_PI
+#include <cmath>
+#include <iostream>
+#include <fstream>
+
+
+namespace pygalmesh {
+
+typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
+
+typedef CGAL::Labeled_mesh_domain_3<K> Periodic_mesh_domain;
+
+// Triangulation
+typedef CGAL::Periodic_3_mesh_triangulation_3<Periodic_mesh_domain>::type Tr;
+typedef CGAL::Mesh_complex_3_in_triangulation_3<Tr> C3t3;
+
+// Mesh Criteria
+typedef CGAL::Mesh_criteria_3<Tr> Mesh_criteria;
+typedef Mesh_criteria::Facet_criteria Facet_criteria;
+typedef Mesh_criteria::Cell_criteria Cell_criteria;
+
+
+typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
+typedef K::FT FT;
+typedef K::Point_3 Point;
+typedef K::Iso_cuboid_3 Iso_cuboid;
+// Domain
+typedef FT (Function)(const Point&);
+typedef CGAL::Labeled_mesh_domain_3<K> Periodic_mesh_domain;
+// Triangulation
+typedef CGAL::Periodic_3_mesh_triangulation_3<Periodic_mesh_domain>::type Tr;
+typedef CGAL::Mesh_complex_3_in_triangulation_3<Tr> C3t3;
+// Criteria
+typedef CGAL::Mesh_criteria_3<Tr> Periodic_mesh_criteria;
+// To avoid verbose function and named parameters call
+using namespace CGAL::parameters;
+
+void
+generate_periodic_mesh(
+ const std::shared_ptr<pygalmesh::DomainBase> & domain,
+ const std::string & outfile,
+ const std::array<double, 6> bounding_cuboid,
+ const bool lloyd,
+ const bool odt,
+ const bool perturb,
+ const bool exude,
+ const double max_edge_size_at_feature_edges,
+ const double min_facet_angle,
+ const double max_radius_surface_delaunay_ball,
+ const double max_facet_distance,
+ const double max_circumradius_edge_ratio,
+ const double max_cell_circumradius,
+ const int number_of_copies_in_output,
+ const bool verbose,
+ const int seed
+ )
+{
+ CGAL::get_default_random() = CGAL::Random(seed);
+
+ K::Iso_cuboid_3 cuboid(
+ bounding_cuboid[0],
+ bounding_cuboid[1],
+ bounding_cuboid[2],
+ bounding_cuboid[3],
+ bounding_cuboid[4],
+ bounding_cuboid[5]
+ );
+
+ // wrap domain
+ const auto d = [&](K::Point_3 p) {
+ return domain->eval({p.x(), p.y(), p.z()});
+ };
+ Periodic_mesh_domain cgal_domain =
+ Periodic_mesh_domain::create_implicit_mesh_domain(d, cuboid);
+
+ Mesh_criteria criteria(
+ CGAL::parameters::edge_size=max_edge_size_at_feature_edges,
+ CGAL::parameters::facet_angle=min_facet_angle,
+ CGAL::parameters::facet_size=max_radius_surface_delaunay_ball,
+ CGAL::parameters::facet_distance=max_facet_distance,
+ CGAL::parameters::cell_radius_edge_ratio=max_circumradius_edge_ratio,
+ CGAL::parameters::cell_size=max_cell_circumradius
+ );
+
+ // Mesh generation
+ if (!verbose) {
+ // suppress output
+ std::cerr.setstate(std::ios_base::failbit);
+ }
+ C3t3 c3t3 = CGAL::make_periodic_3_mesh_3<C3t3>(
+ cgal_domain,
+ criteria,
+ lloyd ? CGAL::parameters::lloyd() : CGAL::parameters::no_lloyd(),
+ odt ? CGAL::parameters::odt() : CGAL::parameters::no_odt(),
+ perturb ? CGAL::parameters::perturb() : CGAL::parameters::no_perturb(),
+ exude ? CGAL::parameters::exude() : CGAL::parameters::no_exude()
+ );
+ if (!verbose) {
+ std::cerr.clear();
+ }
+
+ // Output
+ std::ofstream medit_file(outfile);
+ CGAL::output_periodic_mesh_to_medit(medit_file, c3t3, number_of_copies_in_output);
+ medit_file.close();
+
+ return;
+}
+
+} // namespace pygalmesh
--- /dev/null
+#ifndef GENERATE_PERIODIC_HPP
+#define GENERATE_PERIODIC_HPP
+
+#include "domain.hpp"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace pygalmesh {
+
+void generate_periodic_mesh(
+ const std::shared_ptr<pygalmesh::DomainBase> & domain,
+ const std::string & outfile,
+ const std::array<double, 6> bounding_cuboid,
+ const bool lloyd = false,
+ const bool odt = false,
+ const bool perturb = true,
+ const bool exude = true,
+ const double max_edge_size_at_feature_edges = 0.0, // std::numeric_limits<double>::max(),
+ const double min_facet_angle = 0.0,
+ const double max_radius_surface_delaunay_ball = 0.0,
+ const double max_facet_distance = 0.0,
+ const double max_circumradius_edge_ratio = 0.0,
+ const double max_cell_circumradius = 0.0,
+ const int number_of_copies_in_output = 1,
+ const bool verbose = true,
+ const int seed = 0
+ );
+
+} // namespace pygalmesh
+
+#endif // GENERATE_PERIODIC_HPP
--- /dev/null
+#define CGAL_SURFACE_MESHER_VERBOSE 1
+
+#include "generate_surface_mesh.hpp"
+
+#include <CGAL/Surface_mesh_default_triangulation_3.h>
+#include <CGAL/Complex_2_in_triangulation_3.h>
+#include <CGAL/make_surface_mesh.h>
+#include <fstream>
+#include <CGAL/IO/Complex_2_in_triangulation_3_file_writer.h>
+#include <CGAL/Implicit_surface_3.h>
+
+namespace pygalmesh {
+
+// default triangulation for Surface_mesher
+typedef CGAL::Surface_mesh_default_triangulation_3 Tr;
+// c2t3
+typedef CGAL::Complex_2_in_triangulation_3<Tr> C2t3;
+typedef Tr::Geom_traits GT;
+
+// Wrapper for DomainBase for translating to GT.
+class CgalDomainWrapper
+{
+ public:
+ explicit CgalDomainWrapper(const std::shared_ptr<DomainBase> & domain):
+ domain_(domain)
+ {
+ }
+
+ virtual ~CgalDomainWrapper() = default;
+
+ virtual
+ GT::FT
+ operator()(GT::Point_3 p) const
+ {
+ return domain_->eval({p.x(), p.y(), p.z()});
+ }
+
+ private:
+ const std::shared_ptr<DomainBase> domain_;
+};
+
+typedef CGAL::Implicit_surface_3<GT, CgalDomainWrapper> Surface_3;
+
+void
+generate_surface_mesh(
+ const std::shared_ptr<pygalmesh::DomainBase> & domain,
+ const std::string & outfile,
+ const double bounding_sphere_radius,
+ const double min_facet_angle,
+ const double max_radius_surface_delaunay_ball,
+ const double max_facet_distance,
+ const bool verbose,
+ const int seed
+ )
+{
+ CGAL::get_default_random() = CGAL::Random(seed);
+
+ const double bounding_sphere_radius2 = bounding_sphere_radius > 0 ?
+ bounding_sphere_radius*bounding_sphere_radius :
+ // add a little wiggle room
+ 1.01 * domain->get_bounding_sphere_squared_radius();
+
+ Tr tr; // 3D-Delaunay triangulation
+ C2t3 c2t3 (tr); // 2D-complex in 3D-Delaunay triangulation
+
+ const auto d = CgalDomainWrapper(domain);
+ Surface_3 surface(
+ d,
+ GT::Sphere_3(CGAL::ORIGIN, bounding_sphere_radius2)
+ );
+
+ CGAL::Surface_mesh_default_criteria_3<Tr> criteria(
+ min_facet_angle,
+ max_radius_surface_delaunay_ball,
+ max_facet_distance
+ );
+
+ if (!verbose) {
+ // suppress output
+ std::cout.setstate(std::ios_base::failbit);
+ std::cerr.setstate(std::ios_base::failbit);
+ }
+ CGAL::make_surface_mesh(
+ c2t3,
+ surface,
+ criteria,
+ CGAL::Non_manifold_tag()
+ );
+ if (!verbose) {
+ std::cout.clear();
+ std::cerr.clear();
+ }
+
+ // Output
+ std::ofstream off_file(outfile);
+ CGAL::output_surface_facets_to_off(off_file, c2t3);
+ off_file.close();
+
+ return;
+}
+
+} // namespace pygalmesh
--- /dev/null
+#ifndef GENERATE_SURFACE_MESH_HPP
+#define GENERATE_SURFACE_MESH_HPP
+
+#include "domain.hpp"
+
+#include <memory>
+#include <string>
+
+namespace pygalmesh {
+
+void generate_surface_mesh(
+ const std::shared_ptr<pygalmesh::DomainBase> & domain,
+ const std::string & outfile,
+ const double bounding_sphere_radius = 0.0,
+ const double min_facet_angle = 0.0,
+ const double max_radius_surface_delaunay_ball = 0.0,
+ const double max_facet_distance = 0.0,
+ const bool verbose = true,
+ const int seed = 0
+ );
+
+} // namespace pygalmesh
+
+#endif // GENERATE_SURFACE_MESH_HPP
--- /dev/null
+#ifndef POLYGON2D_HPP
+#define POLYGON2D_HPP
+
+#include "domain.hpp"
+
+#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
+#include <CGAL/Polygon_2_algorithms.h>
+#include <array>
+#include <memory>
+#include <vector>
+
+typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
+
+namespace pygalmesh {
+
+class Polygon2D {
+ public:
+ explicit Polygon2D(const std::vector<std::array<double, 2>> & _points):
+ points(vector_to_cgal_points(_points))
+ {
+ }
+
+ virtual ~Polygon2D() = default;
+
+ std::vector<K::Point_2>
+ vector_to_cgal_points(const std::vector<std::array<double, 2>> & _points) const
+ {
+ std::vector<K::Point_2> points2(_points.size());
+ for (size_t i = 0; i < _points.size(); i++) {
+ assert(_points[i].size() == 2);
+ points2[i] = K::Point_2(_points[i][0], _points[i][1]);
+ }
+ return points2;
+ }
+
+ bool
+ is_inside(const std::array<double, 2> & point)
+ {
+ K::Point_2 pt(point[0], point[1]);
+ switch(CGAL::bounded_side_2(this->points.begin(), this->points.end(), pt, K())) {
+ case CGAL::ON_BOUNDED_SIDE:
+ return true;
+ case CGAL::ON_BOUNDARY:
+ return true;
+ case CGAL::ON_UNBOUNDED_SIDE:
+ return false;
+ default:
+ return false;
+ }
+ return false;
+ }
+
+ public:
+ const std::vector<K::Point_2> points;
+};
+
+
+class Extrude: public pygalmesh::DomainBase {
+ public:
+ Extrude(
+ const std::shared_ptr<pygalmesh::Polygon2D> & poly,
+ const std::array<double, 3> & direction,
+ const double alpha = 0.0,
+ const double max_edge_size_at_feature_edges = 0.0
+ ):
+ poly_(poly),
+ direction_(direction),
+ alpha_(alpha),
+ max_edge_size_at_feature_edges_(max_edge_size_at_feature_edges)
+ {
+ }
+
+ virtual ~Extrude() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ if (x[2] < 0.0 || x[2] > direction_[2]) {
+ return 1.0;
+ }
+
+ const double beta = x[2] / direction_[2];
+
+ std::array<double, 2> x2 = {
+ x[0] - beta * direction_[0],
+ x[1] - beta * direction_[1]
+ };
+
+ if (alpha_ != 0.0) {
+ std::array<double, 2> x3;
+ // turn by -beta*alpha
+ const double sinAlpha = sin(beta*alpha_);
+ const double cosAlpha = cos(beta*alpha_);
+ x3[0] = cosAlpha * x2[0] + sinAlpha * x2[1];
+ x3[1] = -sinAlpha * x2[0] + cosAlpha * x2[1];
+ x2 = x3;
+ }
+
+ return poly_->is_inside(x2) ? -1.0 : 1.0;
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ double max = 0.0;
+ for (const auto & pt: poly_->points) {
+ // bottom polygon
+ const double nrm0 = pt.x()*pt.x() + pt.y()*pt.y();
+ if (nrm0 > max) {
+ max = nrm0;
+ }
+
+ // TODO rotation
+
+ // top polygon
+ const double x = pt.x() + direction_[0];
+ const double y = pt.y() + direction_[1];
+ const double z = direction_[2];
+ const double nrm1 = x*x + y*y + z*z;
+ if (nrm1 > max) {
+ max = nrm1;
+ }
+ }
+ return max;
+ }
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ std::vector<std::vector<std::array<double, 3>>> features = {};
+
+ size_t n;
+
+ // bottom polygon
+ n = poly_->points.size();
+ for (size_t i=0; i < n-1; i++) {
+ features.push_back({
+ {poly_->points[i].x(), poly_->points[i].y(), 0.0},
+ {poly_->points[i+1].x(), poly_->points[i+1].y(), 0.0}
+ });
+ }
+ features.push_back({
+ {poly_->points[n-1].x(), poly_->points[n-1].y(), 0.0},
+ {poly_->points[0].x(), poly_->points[0].y(), 0.0}
+ });
+
+ // top polygon, R*x + d
+ n = poly_->points.size();
+ const double sinAlpha = sin(alpha_);
+ const double cosAlpha = cos(alpha_);
+ for (size_t i=0; i < n-1; i++) {
+ features.push_back({
+ {
+ cosAlpha * poly_->points[i].x() - sinAlpha * poly_->points[i].y() + direction_[0],
+ sinAlpha * poly_->points[i].x() + cosAlpha * poly_->points[i].y() + direction_[1],
+ direction_[2]
+ },
+ {
+ cosAlpha * poly_->points[i+1].x() - sinAlpha * poly_->points[i+1].y() + direction_[0],
+ sinAlpha * poly_->points[i+1].x() + cosAlpha * poly_->points[i+1].y() + direction_[1],
+ direction_[2]
+ }
+ });
+ }
+ features.push_back({
+ {
+ cosAlpha * poly_->points[n-1].x() - sinAlpha * poly_->points[n-1].y() + direction_[0],
+ sinAlpha * poly_->points[n-1].x() + cosAlpha * poly_->points[n-1].y() + direction_[1],
+ direction_[2]
+ },
+ {
+ cosAlpha * poly_->points[0].x() - sinAlpha * poly_->points[0].y() + direction_[0],
+ sinAlpha * poly_->points[0].x() + cosAlpha * poly_->points[0].y() + direction_[1],
+ direction_[2]
+ }
+ });
+
+ // features connecting the top and bottom
+ if (alpha_ == 0) {
+ for (const auto & pt: poly_->points) {
+ std::vector<std::array<double, 3>> line = {
+ {pt.x(), pt.y(), 0.0},
+ {pt.x() + direction_[0], pt.y() + direction_[1], direction_[2]}
+ };
+ features.push_back(line);
+ }
+ } else {
+ // Alright, we need to chop the lines on which the polygon corners are
+ // sitting into pieces. How long? About max_edge_size_at_feature_edges. For the starting point
+ // (x0, y0, z0) height h and angle alpha, the lines are given by
+ //
+ // f(beta) = (
+ // cos(alpha*beta) x0 - sin(alpha*beta) y0,
+ // sin(alpha*beta) x0 + cos(alpha*beta) y0,
+ // z0 + beta * h
+ // )
+ //
+ // with beta in [0, 1]. The length from beta0 till beta1 is then
+ //
+ // l = sqrt(alpha^2 (x0^2 + y0^2) + h^2) * (beta1 - beta0).
+ //
+ const double height = direction_[2];
+ for (const auto & pt: poly_->points) {
+ const double l = sqrt(alpha_*alpha_ * (pt.x()*pt.x() + pt.y()*pt.y()) + height*height);
+ assert(max_edge_size_at_feature_edges_ > 0.0);
+ const size_t n = int(l / max_edge_size_at_feature_edges_ - 0.5) + 1;
+ std::vector<std::array<double, 3>> line = {
+ {pt.x(), pt.y(), 0.0},
+ };
+ for (size_t i=0; i < n; i++) {
+ const double beta = double(i+1) / n;
+ const double sinAB = sin(alpha_*beta);
+ const double cosAB = cos(alpha_*beta);
+ line.push_back({
+ cosAB * pt.x() - sinAB * pt.y(),
+ sinAB * pt.x() + cosAB * pt.y(),
+ beta * height
+ });
+ }
+ features.push_back(line);
+ }
+ }
+
+ return features;
+ };
+
+ private:
+ const std::shared_ptr<pygalmesh::Polygon2D> poly_;
+ const std::array<double, 3> direction_;
+ const double alpha_;
+ const double max_edge_size_at_feature_edges_;
+};
+
+
+class ring_extrude: public pygalmesh::DomainBase {
+ public:
+ ring_extrude(
+ const std::shared_ptr<pygalmesh::Polygon2D> & poly,
+ const double max_edge_size_at_feature_edges
+ ):
+ poly_(poly),
+ max_edge_size_at_feature_edges_(max_edge_size_at_feature_edges)
+ {
+ assert(max_edge_size_at_feature_edges > 0.0);
+ }
+
+ virtual ~ring_extrude() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ const double r = sqrt(x[0]*x[0] + x[1]*x[1]);
+ const double z = x[2];
+
+ return poly_->is_inside({r, z}) ? -1.0 : 1.0;
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ double max = 0.0;
+ for (const auto & pt: poly_->points) {
+ const double nrm1 = pt.x()*pt.x() + pt.y()*pt.y();
+ if (nrm1 > max) {
+ max = nrm1;
+ }
+ }
+ return max;
+ }
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ std::vector<std::vector<std::array<double, 3>>> features = {};
+
+ for (const auto & pt: poly_->points) {
+ const double r = pt.x();
+ const double circ = 2 * 3.14159265359 * r;
+ const size_t n = int(circ / max_edge_size_at_feature_edges_ - 0.5) + 1;
+ std::vector<std::array<double, 3>> line;
+ for (size_t i=0; i < n; i++) {
+ const double alpha = (2 * 3.14159265359 * i) / n;
+ line.push_back({
+ r * cos(alpha),
+ r * sin(alpha),
+ pt.y()
+ });
+ }
+ line.push_back(line.front());
+ features.push_back(line);
+ }
+ return features;
+ }
+
+ private:
+ const std::shared_ptr<pygalmesh::Polygon2D> poly_;
+ const double max_edge_size_at_feature_edges_;
+};
+
+} // namespace pygalmesh
+
+#endif // POLYGON2D_HPP
--- /dev/null
+// Note:
+// One could also implement the primitives as Python classes and have much more readable
+// code. Unfortunately, this approach would be a lot slower. The reason is that CGAL
+// calls the eval() method with individual points, and this many CPP-to-Python calls are
+// very expensive. It would be a lot better if CGAL called the method with batches of X
+// at once, which would also enable users to employ some optimization, but this isn't
+// the case yet. It's not clear if the mesh building algorithm allows for such
+// optimziation at all.
+// The corresponding issue hasn't gained much traction
+// <https://github.com/CGAL/cgal/issues/3874>.
+//
+#ifndef PRIMITIVES_HPP
+#define PRIMITIVES_HPP
+
+#include "domain.hpp"
+
+#include <memory>
+#include <vector>
+
+namespace pygalmesh {
+
+class Ball: public pygalmesh::DomainBase
+{
+ public:
+ Ball(
+ const std::array<double, 3> & x0,
+ const double radius
+ ):
+ x0_(x0),
+ radius_(radius)
+ {
+ assert(x0_.size() == 3);
+ }
+
+ virtual ~Ball() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ const double xx0 = x[0] - x0_[0];
+ const double yy0 = x[1] - x0_[1];
+ const double zz0 = x[2] - x0_[2];
+ return xx0*xx0 + yy0*yy0 + zz0*zz0 - radius_*radius_;
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ const double x0_nrm = sqrt(x0_[0]*x0_[0] + x0_[1]*x0_[1] + x0_[2]*x0_[2]);
+ return (x0_nrm + radius_) * (x0_nrm + radius_);
+ }
+
+ private:
+ const std::array<double, 3> x0_;
+ const double radius_;
+};
+
+
+class Cuboid: public pygalmesh::DomainBase
+{
+ public:
+ Cuboid(
+ const std::array<double, 3> & x0,
+ const std::array<double, 3> & x1
+ ):
+ x0_(x0),
+ x1_(x1)
+ {
+ }
+
+ virtual ~Cuboid() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ // TODO differentiable expression?
+ return std::max(std::max(
+ (x[0] - x0_[0]) * (x[0] - x1_[0]),
+ (x[1] - x0_[1]) * (x[1] - x1_[1])
+ ),
+ (x[2] - x0_[2]) * (x[2] - x1_[2])
+ );
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ const double x0_nrm2 = x0_[0]*x0_[0] + x0_[1]*x0_[1] + x0_[2]*x0_[2];
+ const double x1_nrm2 = x1_[0]*x1_[0] + x1_[1]*x1_[1] + x1_[2]*x1_[2];
+ return std::max({x0_nrm2, x1_nrm2});
+ }
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ std::vector<std::array<double, 3>> corners = {
+ {x0_[0], x0_[1], x0_[2]},
+ {x1_[0], x0_[1], x0_[2]},
+ {x0_[0], x1_[1], x0_[2]},
+ {x0_[0], x0_[1], x1_[2]},
+ {x1_[0], x1_[1], x0_[2]},
+ {x1_[0], x0_[1], x1_[2]},
+ {x0_[0], x1_[1], x1_[2]},
+ {x1_[0], x1_[1], x1_[2]}
+ };
+ return {
+ {corners[0], corners[1]},
+ {corners[0], corners[2]},
+ {corners[0], corners[3]},
+ {corners[1], corners[4]},
+ {corners[1], corners[5]},
+ {corners[2], corners[4]},
+ {corners[2], corners[6]},
+ {corners[3], corners[5]},
+ {corners[3], corners[6]},
+ {corners[4], corners[7]},
+ {corners[5], corners[7]},
+ {corners[6], corners[7]}
+ };
+ };
+
+ private:
+ const std::array<double, 3> x0_;
+ const std::array<double, 3> x1_;
+};
+
+
+class Ellipsoid: public pygalmesh::DomainBase
+{
+ public:
+ Ellipsoid(
+ const std::array<double, 3> & x0,
+ const double a0,
+ const double a1,
+ const double a2
+ ):
+ x0_(x0),
+ a0_2_(a0*a0),
+ a1_2_(a1*a1),
+ a2_2_(a2*a1)
+ {
+ }
+
+ virtual ~Ellipsoid() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ const double xx0 = x[0] - x0_[0];
+ const double yy0 = x[1] - x0_[1];
+ const double zz0 = x[2] - x0_[2];
+ return xx0*xx0/a0_2_ + yy0*yy0/a1_2_ + zz0*zz0/a2_2_ - 1.0;
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ return std::max({a0_2_, a1_2_, a2_2_});
+ }
+
+ private:
+ const std::array<double, 3> x0_;
+ const double a0_2_;
+ const double a1_2_;
+ const double a2_2_;
+};
+
+
+class Cylinder: public pygalmesh::DomainBase
+{
+ public:
+ Cylinder(
+ const double z0,
+ const double z1,
+ const double radius,
+ const double feature_edge_length
+ ):
+ z0_(z0),
+ z1_(z1),
+ radius_(radius),
+ feature_edge_length_(feature_edge_length)
+ {
+ assert(z1_ > z0_);
+ }
+
+ virtual ~Cylinder() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ return (z0_ < x[2] && x[2] < z1_) ?
+ x[0]*x[0] + x[1]*x[1] - radius_*radius_ :
+ 1.0;
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ const double zmax = std::max({abs(z0_), abs(z1_)});
+ return zmax*zmax + radius_*radius_;
+ }
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ const double pi = 3.1415926535897932384;
+ const size_t n = 2 * pi * radius_ / feature_edge_length_;
+ std::vector<std::array<double, 3>> circ0(n+1);
+ std::vector<std::array<double, 3>> circ1(n+1);
+ for (size_t i=0; i < n; i++) {
+ const double c = radius_ * cos((2*pi * i) / n);
+ const double s = radius_ * sin((2*pi * i) / n);
+ circ0[i] = {c, s, z0_};
+ circ1[i] = {c, s, z1_};
+ }
+ // close the circles
+ circ0[n] = circ0[0];
+ circ1[n] = circ1[0];
+ return {circ0, circ1};
+ };
+
+ private:
+ const double z0_;
+ const double z1_;
+ const double radius_;
+ const double feature_edge_length_;
+};
+
+
+class Cone: public pygalmesh::DomainBase
+{
+ public:
+ Cone(
+ const double radius,
+ const double height,
+ const double feature_edge_length
+ ):
+ radius_(radius),
+ height_(height),
+ feature_edge_length_(feature_edge_length)
+ {
+ assert(radius_ > 0.0);
+ assert(height_ > 0.0);
+ }
+
+ virtual ~Cone() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ const double rad = radius_ * (1.0 - x[2] / height_);
+
+ return (0.0 < x[2] && x[2] < height_) ?
+ x[0]*x[0] + x[1]*x[1] - rad*rad :
+ 1.0;
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ const double max = std::max({radius_, height_});
+ return max*max;
+ }
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ const double pi = 3.1415926535897932384;
+ const size_t n = 2 * pi * radius_ / feature_edge_length_;
+ std::vector<std::array<double, 3>> circ0(n+1);
+ for (size_t i=0; i < n; i++) {
+ const double c = radius_ * cos((2*pi * i) / n);
+ const double s = radius_ * sin((2*pi * i) / n);
+ circ0[i] = {c, s, 0.0};
+ }
+ circ0[n] = circ0[0];
+ return {circ0};
+ };
+
+ private:
+ const double radius_;
+ const double height_;
+ const double feature_edge_length_;
+};
+
+
+class Tetrahedron: public pygalmesh::DomainBase
+{
+ public:
+ Tetrahedron(
+ const std::array<double, 3> & x0,
+ const std::array<double, 3> & x1,
+ const std::array<double, 3> & x2,
+ const std::array<double, 3> & x3
+ ):
+ x0_(Eigen::Vector3d(x0.data())),
+ x1_(Eigen::Vector3d(x1.data())),
+ x2_(Eigen::Vector3d(x2.data())),
+ x3_(Eigen::Vector3d(x3.data())),
+ A_(constructA(x0, x1, x2, x3))
+ {
+ }
+
+ Eigen::Matrix4d constructA(
+ const std::array<double, 3> & x0,
+ const std::array<double, 3> & x1,
+ const std::array<double, 3> & x2,
+ const std::array<double, 3> & x3
+ ) {
+ Eigen::Matrix4d A;
+ A << x0[0], x1[0], x2[0], x3[0],
+ x0[1], x1[1], x2[1], x3[1],
+ x0[2], x1[2], x2[2], x3[2],
+ 1.0, 1.0, 1.0, 1.0;
+ return A;
+ }
+
+ virtual ~Tetrahedron() = default;
+
+ // bool isOnSameSide(
+ // const Eigen::Vector3d & v0,
+ // const Eigen::Vector3d & v1,
+ // const Eigen::Vector3d & v2,
+ // const Eigen::Vector3d & v3,
+ // const Eigen::Vector3d & p
+ // ) const
+ // {
+ // const auto normal = (v1 - v0).cross(v2 - v0);
+ // const double dot_v3 = normal.dot(v3 - v0);
+ // const double dot_p = normal.dot(p - v0);
+ // return (
+ // (dot_v3 > 0 && dot_p > 0) || (dot_v3 < 0 && dot_p < 0)
+ // );
+ // }
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ Eigen::Vector4d b;
+ b << x[0], x[1], x[2], 1.0;
+ Eigen::Vector4d bary = A_.partialPivLu().solve(b);
+ return -bary.minCoeff();
+
+ // Eigen::Vector3d pvec(x.data());
+ // const bool a =
+ // isOnSameSide(x0_, x1_, x2_, x3_, pvec) &&
+ // isOnSameSide(x1_, x2_, x3_, x0_, pvec) &&
+ // isOnSameSide(x2_, x3_, x0_, x1_, pvec) &&
+ // isOnSameSide(x3_, x0_, x1_, x2_, pvec);
+ // return a ? -1.0 : 1.0;
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ return std::max({
+ x0_.dot(x0_),
+ x1_.dot(x1_),
+ x2_.dot(x2_),
+ x3_.dot(x3_)
+ });
+ }
+
+ virtual
+ std::vector<std::vector<std::array<double, 3>>>
+ get_features() const
+ {
+ std::vector<std::array<double, 3>> pts = {
+ {x0_[0], x0_[1], x0_[2]},
+ {x1_[0], x1_[1], x1_[2]},
+ {x2_[0], x2_[1], x2_[2]},
+ {x3_[0], x3_[1], x3_[2]}
+ };
+ return {
+ {pts[0], pts[1]},
+ {pts[0], pts[2]},
+ {pts[0], pts[3]},
+ {pts[1], pts[2]},
+ {pts[1], pts[3]},
+ {pts[2], pts[3]}
+ };
+ };
+
+ private:
+ const Eigen::Vector3d x0_;
+ const Eigen::Vector3d x1_;
+ const Eigen::Vector3d x2_;
+ const Eigen::Vector3d x3_;
+ const Eigen::Matrix4d A_;
+};
+
+
+class Torus: public pygalmesh::DomainBase
+{
+ public:
+ Torus(
+ const double major_radius,
+ const double minor_radius
+ ):
+ major_radius_(major_radius),
+ minor_radius_(minor_radius)
+ {
+ }
+
+ virtual ~Torus() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ const double r = sqrt(x[0]*x[0] + x[1]*x[1]);
+ return (
+ (r - major_radius_)*(r - major_radius_) + x[2]*x[2]
+ - minor_radius_*minor_radius_
+ );
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ return (major_radius_ + minor_radius_)*(major_radius_ + minor_radius_);
+ }
+
+ private:
+ const double major_radius_;
+ const double minor_radius_;
+};
+
+
+class HalfSpace: public pygalmesh::DomainBase
+{
+ public:
+ HalfSpace(
+ const std::array<double, 3> & n,
+ const double alpha,
+ const double bounding_sphere_squared_radius
+ ):
+ n_(n),
+ alpha_(alpha),
+ bounding_sphere_squared_radius_(bounding_sphere_squared_radius)
+ {
+ }
+
+ virtual ~HalfSpace() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const
+ {
+ return n_[0]*x[0] + n_[1]*x[1] + n_[2]*x[2] - alpha_;
+ }
+
+ virtual
+ double
+ get_bounding_sphere_squared_radius() const
+ {
+ return bounding_sphere_squared_radius_;
+ }
+
+ private:
+ const std::array<double, 3> n_;
+ const double alpha_;
+ const double bounding_sphere_squared_radius_;
+};
+
+} // namespace pygalmesh
+
+#endif // PRIMITIVES_HPP
--- /dev/null
+#include "domain.hpp"
+#include "generate.hpp"
+#include "generate_2d.hpp"
+#include "generate_from_off.hpp"
+#include "generate_from_inr.hpp"
+#include "remesh_surface.hpp"
+#include "generate_periodic.hpp"
+#include "generate_surface_mesh.hpp"
+#include "polygon2d.hpp"
+#include "primitives.hpp"
+#include "sizing_field.hpp"
+
+#include <CGAL/version.h>
+
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+
+namespace py = pybind11;
+using namespace pygalmesh;
+
+
+// https://pybind11.readthedocs.io/en/stable/advanced/classes.html#overriding-virtual-functions-in-python
+class PyDomainBase: public DomainBase {
+public:
+ using DomainBase::DomainBase;
+
+ double
+ eval(const std::array<double, 3> & x) const override {
+ PYBIND11_OVERLOAD_PURE(double, DomainBase, eval, x);
+ }
+
+ double
+ get_bounding_sphere_squared_radius() const override {
+ PYBIND11_OVERLOAD_PURE(double, DomainBase, get_bounding_sphere_squared_radius);
+ }
+
+ // std::vector<std::vector<std::array<double, 3>>>
+ // get_features() const override {
+ // PYBIND11_OVERLOAD(
+ // std::vector<std::vector<std::array<double, 3>>>,
+ // DomainBase,
+ // get_features,
+ // 0.0
+ // );
+ // }
+};
+
+
+// https://pybind11.readthedocs.io/en/stable/advanced/classes.html#overriding-virtual-functions-in-python
+class PySizingFieldBase: public SizingFieldBase {
+public:
+ using SizingFieldBase::SizingFieldBase;
+
+ double
+ eval(const std::array<double, 3> & x) const override {
+ PYBIND11_OVERLOAD_PURE(double, SizingFieldBase, eval, x);
+ }
+};
+
+
+PYBIND11_MODULE(_pygalmesh, m) {
+ // m.doc() = "documentation string";
+
+ // Domain base.
+ // shared_ptr b/c of
+ // <https://github.com/pybind/pybind11/issues/956#issuecomment-317022720>
+ py::class_<DomainBase, PyDomainBase, std::shared_ptr<DomainBase>>(m, "DomainBase")
+ .def(py::init<>())
+ .def("eval", &DomainBase::eval)
+ .def("get_bounding_sphere_squared_radius", &DomainBase::get_bounding_sphere_squared_radius)
+ .def("get_features", &DomainBase::get_features);
+
+ // Sizing field base.
+ // shared_ptr b/c of
+ // <https://github.com/pybind/pybind11/issues/956#issuecomment-317022720>
+ py::class_<SizingFieldBase, PySizingFieldBase, std::shared_ptr<SizingFieldBase>>(m, "SizingFieldBase")
+ .def(py::init<>())
+ .def("eval", &SizingFieldBase::eval);
+
+ // Domain transformations
+ py::class_<Translate, DomainBase, std::shared_ptr<Translate>>(m, "Translate")
+ .def(py::init<
+ const std::shared_ptr<const DomainBase> &,
+ const std::array<double, 3> &
+ >())
+ .def("eval", &Translate::eval)
+ .def("translate_features", &Translate::translate_features)
+ .def("get_bounding_sphere_squared_radius", &Translate::get_bounding_sphere_squared_radius)
+ .def("get_features", &Translate::get_features);
+
+ py::class_<Rotate, DomainBase, std::shared_ptr<Rotate>>(m, "Rotate")
+ .def(py::init<
+ const std::shared_ptr<const pygalmesh::DomainBase> &,
+ const std::array<double, 3> &,
+ const double
+ >())
+ .def("eval", &Rotate::eval)
+ .def("rotate", &Rotate::rotate)
+ .def("rotate_features", &Rotate::rotate_features)
+ .def("get_bounding_sphere_squared_radius", &Rotate::get_bounding_sphere_squared_radius)
+ .def("get_features", &Rotate::get_features);
+
+ py::class_<Scale, DomainBase, std::shared_ptr<Scale>>(m, "Scale")
+ .def(py::init<
+ std::shared_ptr<const pygalmesh::DomainBase> &,
+ const double
+ >())
+ .def("eval", &Scale::eval)
+ .def("scale_features", &Scale::scale_features)
+ .def("get_bounding_sphere_squared_radius", &Scale::get_bounding_sphere_squared_radius)
+ .def("get_features", &Scale::get_features);
+
+ py::class_<Stretch, DomainBase, std::shared_ptr<Stretch>>(m, "Stretch")
+ .def(py::init<
+ std::shared_ptr<const pygalmesh::DomainBase> &,
+ const std::array<double, 3> &
+ >())
+ .def("eval", &Stretch::eval)
+ .def("stretch_features", &Stretch::stretch_features)
+ .def("get_bounding_sphere_squared_radius", &Stretch::get_bounding_sphere_squared_radius)
+ .def("get_features", &Stretch::get_features);
+
+ py::class_<Intersection, DomainBase, std::shared_ptr<Intersection>>(m, "Intersection")
+ .def(py::init<
+ std::vector<std::shared_ptr<const pygalmesh::DomainBase>> &
+ >())
+ .def("eval", &Intersection::eval)
+ .def("get_bounding_sphere_squared_radius", &Intersection::get_bounding_sphere_squared_radius)
+ .def("get_features", &Intersection::get_features);
+
+ py::class_<Union, DomainBase, std::shared_ptr<Union>>(m, "Union")
+ .def(py::init<
+ std::vector<std::shared_ptr<const pygalmesh::DomainBase>> &
+ >())
+ .def("eval", &Union::eval)
+ .def("get_bounding_sphere_squared_radius", &Union::get_bounding_sphere_squared_radius)
+ .def("get_features", &Union::get_features);
+
+ py::class_<Difference, DomainBase, std::shared_ptr<Difference>>(m, "Difference")
+ .def(py::init<
+ std::shared_ptr<const pygalmesh::DomainBase> &,
+ std::shared_ptr<const pygalmesh::DomainBase> &
+ >())
+ .def("eval", &Difference::eval)
+ .def("get_bounding_sphere_squared_radius", &Difference::get_bounding_sphere_squared_radius)
+ .def("get_features", &Difference::get_features);
+
+ // Primitives
+ py::class_<Ball, DomainBase, std::shared_ptr<Ball>>(m, "Ball")
+ .def(py::init<
+ const std::array<double, 3> &,
+ const double
+ >())
+ .def("eval", &Ball::eval)
+ .def("get_bounding_sphere_squared_radius", &Ball::get_bounding_sphere_squared_radius);
+
+ py::class_<Cuboid, DomainBase, std::shared_ptr<Cuboid>>(m, "Cuboid")
+ .def(py::init<
+ const std::array<double, 3> &,
+ const std::array<double, 3> &
+ >())
+ .def("eval", &Cuboid::eval)
+ .def("get_bounding_sphere_squared_radius", &Cuboid::get_bounding_sphere_squared_radius)
+ .def("get_features", &Cuboid::get_features);
+
+ py::class_<Ellipsoid, DomainBase, std::shared_ptr<Ellipsoid>>(m, "Ellipsoid")
+ .def(py::init<
+ const std::array<double, 3> &,
+ const double,
+ const double,
+ const double
+ >())
+ .def("eval", &Ellipsoid::eval)
+ .def("get_bounding_sphere_squared_radius", &Ellipsoid::get_bounding_sphere_squared_radius)
+ .def("get_features", &Ellipsoid::get_features);
+
+ py::class_<Cylinder, DomainBase, std::shared_ptr<Cylinder>>(m, "Cylinder")
+ .def(py::init<
+ const double,
+ const double,
+ const double,
+ const double
+ >())
+ .def("eval", &Cylinder::eval)
+ .def("get_bounding_sphere_squared_radius", &Cylinder::get_bounding_sphere_squared_radius)
+ .def("get_features", &Cylinder::get_features);
+
+ py::class_<Cone, DomainBase, std::shared_ptr<Cone>>(m, "Cone")
+ .def(py::init<
+ const double,
+ const double,
+ const double
+ >())
+ .def("eval", &Cone::eval)
+ .def("get_bounding_sphere_squared_radius", &Cone::get_bounding_sphere_squared_radius)
+ .def("get_features", &Cone::get_features);
+
+ py::class_<Tetrahedron, DomainBase, std::shared_ptr<Tetrahedron>>(m, "Tetrahedron")
+ .def(py::init<
+ const std::array<double, 3> &,
+ const std::array<double, 3> &,
+ const std::array<double, 3> &,
+ const std::array<double, 3> &
+ >())
+ .def("eval", &Tetrahedron::eval)
+ .def("get_bounding_sphere_squared_radius", &Tetrahedron::get_bounding_sphere_squared_radius)
+ .def("get_features", &Tetrahedron::get_features);
+
+ py::class_<Torus, DomainBase, std::shared_ptr<Torus>>(m, "Torus")
+ .def(py::init<
+ const double,
+ const double
+ >())
+ .def("eval", &Torus::eval)
+ .def("get_bounding_sphere_squared_radius", &Torus::get_bounding_sphere_squared_radius)
+ .def("get_features", &Torus::get_features);
+
+ py::class_<HalfSpace, DomainBase, std::shared_ptr<HalfSpace>>(m, "HalfSpace")
+ .def(py::init<
+ const std::array<double, 3> &,
+ const double,
+ const double
+ >())
+ .def("eval", &HalfSpace::eval)
+ .def("get_bounding_sphere_squared_radius", &HalfSpace::get_bounding_sphere_squared_radius);
+
+ // polygon2d
+ py::class_<Polygon2D, std::shared_ptr<Polygon2D>>(m, "Polygon2D")
+ .def(py::init<
+ const std::vector<std::array<double, 2>> &
+ >())
+ .def("vector_to_cgal_points", &Polygon2D::vector_to_cgal_points)
+ .def("is_inside", &Polygon2D::is_inside);
+
+ py::class_<Extrude, DomainBase, std::shared_ptr<Extrude>>(m, "Extrude")
+ .def(py::init<
+ const std::shared_ptr<pygalmesh::Polygon2D> &,
+ const std::array<double, 3> &,
+ const double,
+ const double
+ >(),
+ py::arg("poly"),
+ py::arg("direction"),
+ py::arg("alpha") = 0.0,
+ py::arg("max_edge_size_at_feature_edges") = 0.0
+ )
+ .def("eval", &Extrude::eval)
+ .def("get_bounding_sphere_squared_radius", &Extrude::get_bounding_sphere_squared_radius)
+ .def("get_features", &Extrude::get_features);
+
+ py::class_<ring_extrude, DomainBase, std::shared_ptr<ring_extrude>>(m, "RingExtrude")
+ .def(py::init<
+ const std::shared_ptr<pygalmesh::Polygon2D> &,
+ const double
+ >())
+ .def("eval", &ring_extrude::eval)
+ .def("get_bounding_sphere_squared_radius", &ring_extrude::get_bounding_sphere_squared_radius)
+ .def("get_features", &ring_extrude::get_features);
+
+ // functions
+ m.def(
+ "_generate_2d", &generate_2d,
+ py::arg("points"),
+ py::arg("constraints"),
+ py::arg("max_circumradius_shortest_edge_ratio") = 1.41421356237,
+ py::arg("max_edge_size") = 0.0,
+ py::arg("num_lloyd_steps") = 0
+ );
+ m.def(
+ "_generate_mesh", &generate_mesh,
+ py::arg("domain"),
+ py::arg("outfile"),
+ py::arg("extra_feature_edges") = std::vector<std::vector<std::array<double, 3>>>(),
+ py::arg("bounding_sphere_radius") = 0.0,
+ py::arg("lloyd") = false,
+ py::arg("odt") = false,
+ py::arg("perturb") = true,
+ py::arg("exude") = true,
+ py::arg("max_edge_size_at_feature_edges_value") = 0.0,
+ py::arg("max_edge_size_at_feature_edges_field") = nullptr,
+ py::arg("min_facet_angle") = 0.0,
+ py::arg("max_radius_surface_delaunay_ball_value") = 0.0,
+ py::arg("max_radius_surface_delaunay_ball_field") = nullptr,
+ py::arg("max_facet_distance_value") = 0.0,
+ py::arg("max_facet_distance_field") = nullptr,
+ py::arg("max_circumradius_edge_ratio") = 0.0,
+ py::arg("max_cell_circumradius_value") = 0.0,
+ py::arg("max_cell_circumradius_field") = nullptr,
+ py::arg("exude_time_limit") = 0.0,
+ py::arg("exude_sliver_bound") = 0.0,
+ py::arg("verbose") = true,
+ py::arg("seed") = 0
+ );
+ m.def(
+ "_generate_periodic_mesh", &generate_periodic_mesh,
+ py::arg("domain"),
+ py::arg("outfile"),
+ py::arg("bounding_cuboid"),
+ py::arg("lloyd") = false,
+ py::arg("odt") = false,
+ py::arg("perturb") = true,
+ py::arg("exude") = true,
+ py::arg("max_edge_size_at_feature_edges") = 0.0,
+ py::arg("min_facet_angle") = 0.0,
+ py::arg("max_radius_surface_delaunay_ball") = 0.0,
+ py::arg("max_facet_distance") = 0.0,
+ py::arg("max_circumradius_edge_ratio") = 0.0,
+ py::arg("max_cell_circumradius") = 0.0,
+ py::arg("number_of_copies_in_output") = 1,
+ py::arg("verbose") = true,
+ py::arg("seed") = 0
+ );
+ m.def(
+ "_generate_surface_mesh", &generate_surface_mesh,
+ py::arg("domain"),
+ py::arg("outfile"),
+ py::arg("bounding_sphere_radius") = 0.0,
+ py::arg("min_facet_angle") = 0.0,
+ py::arg("max_radius_surface_delaunay_ball") = 0.0,
+ py::arg("max_facet_distance") = 0.0,
+ py::arg("verbose") = true,
+ py::arg("seed") = 0
+ );
+ m.def(
+ "_generate_from_off", &generate_from_off,
+ py::arg("infile"),
+ py::arg("outfile"),
+ py::arg("lloyd") = false,
+ py::arg("odt") = false,
+ py::arg("perturb") = true,
+ py::arg("exude") = true,
+ py::arg("max_edge_size_at_feature_edges") = 0.0, // std::numeric_limits<double>::max(),
+ py::arg("min_facet_angle") = 0.0,
+ py::arg("max_radius_surface_delaunay_ball") = 0.0,
+ py::arg("max_facet_distance") = 0.0,
+ py::arg("max_circumradius_edge_ratio") = 0.0,
+ py::arg("max_cell_circumradius") = 0.0,
+ py::arg("exude_time_limit") = 0.0,
+ py::arg("exude_sliver_bound") = 0.0,
+ py::arg("verbose") = true,
+ py::arg("reorient") = false,
+ py::arg("seed") = 0
+ );
+ m.def(
+ "_generate_from_inr", &generate_from_inr,
+ py::arg("inr_filename"),
+ py::arg("outfile"),
+ py::arg("lloyd") = false,
+ py::arg("odt") = false,
+ py::arg("perturb") = true,
+ py::arg("exude") = true,
+ py::arg("max_edge_size_at_feature_edges") = 0.0,
+ py::arg("min_facet_angle") = 0.0,
+ py::arg("max_radius_surface_delaunay_ball") = 0.0,
+ py::arg("max_facet_distance") = 0.0,
+ py::arg("max_circumradius_edge_ratio") = 0.0,
+ py::arg("max_cell_circumradius") = 0.0,
+ py::arg("exude_time_limit") = 0.0,
+ py::arg("exude_sliver_bound") = 0.0,
+ py::arg("verbose") = true,
+ py::arg("seed") = 0
+ );
+ m.def(
+ "_generate_from_inr_with_subdomain_sizing", &generate_from_inr_with_subdomain_sizing,
+ py::arg("inr_filename"),
+ py::arg("outfile"),
+ py::arg("default_max_cell_circumradius"),
+ py::arg("max_cell_circumradiuss"),
+ py::arg("cell_labels"),
+ py::arg("lloyd") = false,
+ py::arg("odt") = false,
+ py::arg("perturb") = true,
+ py::arg("exude") = true,
+ py::arg("max_edge_size_at_feature_edges") = 0.0,
+ py::arg("min_facet_angle") = 0.0,
+ py::arg("max_radius_surface_delaunay_ball") = 0.0,
+ py::arg("max_facet_distance") = 0.0,
+ py::arg("max_circumradius_edge_ratio") = 0.0,
+ py::arg("exude_time_limit") = 0.0,
+ py::arg("exude_sliver_bound") = 0.0,
+ py::arg("verbose") = true,
+ py::arg("seed") = 0
+ );
+ m.def(
+ "_remesh_surface", &remesh_surface,
+ py::arg("infile"),
+ py::arg("outfile"),
+ py::arg("max_edge_size_at_feature_edges") = 0.0,
+ py::arg("min_facet_angle") = 0.0,
+ py::arg("max_radius_surface_delaunay_ball") = 0.0,
+ py::arg("max_facet_distance") = 0.0,
+ py::arg("verbose") = true,
+ py::arg("seed") = 0
+ );
+ m.attr("_CGAL_VERSION_STR") = CGAL_VERSION_STR;
+}
--- /dev/null
+#define CGAL_MESH_3_VERBOSE 1
+
+#include "remesh_surface.hpp"
+
+#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
+#include <CGAL/Mesh_triangulation_3.h>
+#include <CGAL/Mesh_complex_3_in_triangulation_3.h>
+#include <CGAL/Mesh_criteria_3.h>
+#include <CGAL/Polyhedral_mesh_domain_with_features_3.h>
+#include <CGAL/make_mesh_3.h>
+
+namespace pygalmesh {
+// Domain
+typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
+typedef CGAL::Polyhedral_mesh_domain_with_features_3<K> Mesh_domain;
+// Polyhedron type
+typedef CGAL::Mesh_polyhedron_3<K>::type Polyhedron;
+// Triangulation
+typedef CGAL::Mesh_triangulation_3<Mesh_domain>::type Tr;
+typedef CGAL::Mesh_complex_3_in_triangulation_3<
+ Tr,Mesh_domain::Corner_index,Mesh_domain::Curve_index> C3t3;
+// Criteria
+typedef CGAL::Mesh_criteria_3<Tr> Mesh_criteria;
+
+// <https://doc.cgal.org/latest/Mesh_3/#title24>
+void
+remesh_surface(
+ const std::string & infile,
+ const std::string & outfile,
+ const double max_edge_size_at_feature_edges,
+ const double min_facet_angle,
+ const double max_radius_surface_delaunay_ball,
+ const double max_facet_distance,
+ const bool verbose,
+ const int seed
+ )
+{
+ CGAL::get_default_random() = CGAL::Random(seed);
+
+ // Load a polyhedron
+ Polyhedron poly;
+ std::ifstream input(infile.c_str());
+ input >> poly;
+ if (!CGAL::is_triangle_mesh(poly)){
+ throw "Input geometry is not triangulated.";
+ }
+ // Create a vector with only one element: the pointer to the polyhedron.
+ std::vector<Polyhedron*> poly_ptrs_vector(1, &poly);
+ // Create a polyhedral domain with only one polyhedron and no "bounding polyhedron"
+ // so the volumetric part of the domain will be empty.
+ Mesh_domain domain(poly_ptrs_vector.begin(), poly_ptrs_vector.end());
+
+ // Get sharp features
+ domain.detect_features(); //includes detection of borders
+ // Mesh criteria
+ Mesh_criteria criteria(
+ CGAL::parameters::edge_size=max_edge_size_at_feature_edges,
+ CGAL::parameters::facet_angle=min_facet_angle,
+ CGAL::parameters::facet_size=max_radius_surface_delaunay_ball,
+ CGAL::parameters::facet_distance=max_facet_distance
+ );
+
+ // Mesh generation
+ if (!verbose) {
+ // suppress output
+ std::cerr.setstate(std::ios_base::failbit);
+ }
+ C3t3 c3t3 = CGAL::make_mesh_3<C3t3>(
+ domain,
+ criteria,
+ CGAL::parameters::no_perturb(),
+ CGAL::parameters::no_exude()
+ );
+ if (!verbose) {
+ std::cerr.clear();
+ }
+ // Output the facets of the c3t3 to an OFF file. The facets will not be
+ // oriented.
+ std::ofstream off_file(outfile.c_str());
+ c3t3.output_boundary_to_off(off_file);
+ if (off_file.fail()) {
+ throw "Failed to write OFF.";
+ }
+ return;
+}
+
+} // namespace pygalmesh
--- /dev/null
+#ifndef REMESH_SURFACE_HPP
+#define REMESH_SURFACE_HPP
+
+#include <string>
+#include <vector>
+
+namespace pygalmesh {
+
+void remesh_surface(
+ const std::string & infilen,
+ const std::string & outfile,
+ const double max_edge_size_at_feature_edges = 0.0, // std::numeric_limits<double>::max(),
+ const double min_facet_angle = 0.0,
+ const double max_radius_surface_delaunay_ball = 0.0,
+ const double max_facet_distance = 0.0,
+ const bool verbose = true,
+ const int seed = 0
+ );
+
+} // namespace pygalmesh
+
+#endif // REMESH_SURFACE_HPP
--- /dev/null
+#ifndef SIZING_FIELD_HPP
+#define SIZING_FIELD_HPP
+
+#include <array>
+#include <memory>
+
+namespace pygalmesh {
+
+class SizingFieldBase
+{
+ public:
+
+ virtual ~SizingFieldBase() = default;
+
+ virtual
+ double
+ eval(const std::array<double, 3> & x) const = 0;
+
+ double val = -1.0;
+};
+
+} // namespace pygalmesh
+#endif // SIZING_FIELD_HPP
--- /dev/null
+import numpy
+
+
+def _row_dot(a, b):
+ # http://stackoverflow.com/a/26168677/353337
+ return numpy.einsum("ij, ij->i", a, b)
+
+
+def compute_volumes(vertices, tets):
+ cell_coords = vertices[tets]
+
+ a = cell_coords[:, 1, :] - cell_coords[:, 0, :]
+ b = cell_coords[:, 2, :] - cell_coords[:, 0, :]
+ c = cell_coords[:, 3, :] - cell_coords[:, 0, :]
+
+ # omega = <a, b x c>
+ omega = _row_dot(a, numpy.cross(b, c))
+
+ # https://en.wikipedia.org/wiki/Tetrahedron#Volume
+ return abs(omega) / 6.0
+
+
+def compute_triangle_areas(vertices, triangles):
+ e0 = vertices[triangles[:, 1]] - vertices[triangles[:, 0]]
+ e1 = vertices[triangles[:, 2]] - vertices[triangles[:, 1]]
+
+ assert e0.shape == e1.shape
+ if e0.shape[1] == 2:
+ z_component_of_e0_cross_e1 = numpy.cross(e0, e1)
+ cross_magnitude = z_component_of_e0_cross_e1
+ else:
+ assert e0.shape[1] == 3
+ e0_cross_e1 = numpy.cross(e0, e1)
+ cross_magnitude = numpy.sqrt(_row_dot(e0_cross_e1, e0_cross_e1))
+
+ return 0.5 * cross_magnitude
--- /dev/null
+<VTKFile byte_order="LittleEndian" compressor="vtkZLibDataCompressor" header_type="UInt32" type="UnstructuredGrid" version="0.1">
+ <!--This file was created by meshio v2.3.6-->
+ <UnstructuredGrid>
+ <Piece NumberOfCells="5558" NumberOfPoints="2775">
+ <Points>
+ <DataArray Name="Points" type="Float64" NumberOfComponents="3" format="binary">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</DataArray>
+ </Points>
+ <Cells>
+ <DataArray Name="connectivity" type="Int64" format="binary">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</DataArray>
+ <DataArray Name="offsets" type="Int64" format="binary">AgAAAACAAACwLQAAlhgAAOQIAAA=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</DataArray>
+ <DataArray Name="types" type="Int64" format="binary">AgAAAACAAACwLQAASgAAACwAAAA=eJztxaEBAAAMAqAV/395wTOEQq5i27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt27Zt2/bwD+weUAF4nO3FoQEAAAwCoBX/f3nBF4xQyFVs27Zt27Zt27Zt27Zt27Zt29MfEfscjw==</DataArray>
+ </Cells>
+ </Piece>
+ </UnstructuredGrid>
+</VTKFile>
--- /dev/null
+#INRIMAGE-4#{
+XDIM=10
+YDIM=10
+ZDIM=10
+VDIM=1
+TYPE=unsigned fixed
+PIXSIZE=8 bits
+SCALE=2**0
+CPU=decm
+VX=1
+VY=1
+VZ=1
+#GEOMETRY=CARTESIAN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+##}
+\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 1\ 1\ 1\ 1\ 3\ 3\ 1\ 2\ 2\ 2\ 1\ 1\ 1\ 3\ 3\ 3\ 3\ 2\ 2\ 2\ 1\ 1\ 1\ 3\ 3\ 3\ 3\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 3\ 3\ 1\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 3\ 3\ 3\ 3\ 2\ 2\ 2\ 1\ 1\ 1\ 3\ 3\ 3\ 3\ 2\ 2\ 2\ 1\ 1\ 1\ 3\ 3\ 3\ 3\ 2\ 2\ 2\ 1\ 1\ 1\ 3\ 3\ 3\ 3\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 3\ 3\ 3\ 3\ 2\ 2\ 2\ 1\ 1\ 1\ 3\ 3\ 3\ 3\ 2\ 2\ 2\ 1\ 1\ 1\ 3\ 3\ 3\ 3\ 2\ 2\ 2\ 1\ 1\ 1\ 3\ 3\ 3\ 3\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 2\ 3\ 3\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 3\ 3\ 3\ 3\ 2\ 2\ 2\ 1\ 1\ 1\ 3\ 3\ 3\ 3\ 2\ 2\ 2\ 1\ 1\ 1\ 2\ 3\ 3\ 2\ 2\ 2\ 2\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 1\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2\ 2
\ No newline at end of file
--- /dev/null
+import numpy as np
+from helpers import compute_triangle_areas
+
+import pygalmesh
+
+
+def test_rectangle():
+ points = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]])
+ constraints = [[0, 1], [1, 2], [2, 3], [3, 0]]
+
+ mesh = pygalmesh.generate_2d(
+ points, constraints, max_edge_size=1.0e-1, num_lloyd_steps=10
+ )
+
+ assert mesh.points.shape == (276, 2)
+ assert mesh.get_cells_type("triangle").shape == (486, 3)
+
+ # # show mesh
+ # import matplotlib.pyplot as plt
+ # pts = points[cells]
+ # for pt in pts:
+ # plt.plot([pt[0][0], pt[1][0]], [pt[0][1], pt[1][1]], "-k")
+ # plt.plot([pt[1][0], pt[2][0]], [pt[1][1], pt[2][1]], "-k")
+ # plt.plot([pt[2][0], pt[0][0]], [pt[2][1], pt[0][1]], "-k")
+ # # for pt in points:
+ # # plt.plot(pt[0], pt[1], "or")
+ # plt.gca().set_aspect("equal")
+ # plt.show()
+
+ # mesh.points *= 100
+ # mesh.write("rect.svg")
+
+
+def test_disk():
+ h = 0.1
+ n = int(2 * np.pi / h)
+ alpha = np.linspace(0.0, 2 * np.pi, n + 1, endpoint=False)
+ points = np.column_stack([np.cos(alpha), np.sin(alpha)])
+
+ constraints = [[k, k + 1] for k in range(n)] + [[n, 0]]
+ mesh = pygalmesh.generate_2d(
+ points, constraints, max_edge_size=h, num_lloyd_steps=0
+ )
+ areas = compute_triangle_areas(mesh.points, mesh.get_cells_type("triangle"))
+ assert np.all(areas > 1.0e-5)
+
+
+if __name__ == "__main__":
+ test_disk()
--- /dev/null
+import helpers
+import numpy as np
+
+import pygalmesh
+
+
+def test_from_array():
+ n = 200
+ shape = (n, n, n)
+ h = (1.0 / shape[0], 1.0 / shape[1], 1.0 / shape[2])
+ vol = np.zeros(shape, dtype=np.uint16)
+ i, j, k = np.arange(shape[0]), np.arange(shape[1]), np.arange(shape[2])
+ ii, jj, kk = np.meshgrid(i, j, k)
+ vol[ii * ii + jj * jj + kk * kk < n ** 2] = 1
+ vol[ii * ii + jj * jj + kk * kk < (0.5 * n) ** 2] = 2
+
+ mesh = pygalmesh.generate_from_array(
+ vol,
+ h,
+ max_cell_circumradius=100 * min(h),
+ max_facet_distance=min(h),
+ verbose=False,
+ )
+
+ tol = min(h)
+ ref = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0]
+ assert abs(max(mesh.points[:, 0]) - ref[0]) < (1.0 + ref[0]) * tol
+ assert abs(min(mesh.points[:, 0]) - ref[1]) < (1.0 + ref[1]) * tol
+ assert abs(max(mesh.points[:, 1]) - ref[2]) < (1.0 + ref[2]) * tol
+ assert abs(min(mesh.points[:, 1]) - ref[3]) < (1.0 + ref[3]) * tol
+ assert abs(max(mesh.points[:, 2]) - ref[4]) < (1.0 + ref[4]) * tol
+ assert abs(min(mesh.points[:, 2]) - ref[5]) < (1.0 + ref[5]) * tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ ref = 1.0 / 6.0 * np.pi
+ # Debian needs 2.0e-2 here.
+ # <https://github.com/nschloe/pygalmesh/issues/60>
+ assert abs(vol - ref) < ref * 2.0e-2
+
+
+def test_from_array_with_subdomain_sizing():
+ n = 200
+ shape = (n, n, n)
+ h = (1.0 / shape[0], 1.0 / shape[1], 1.0 / shape[2])
+ vol = np.zeros(shape, dtype=np.uint16)
+ i, j, k = np.arange(shape[0]), np.arange(shape[1]), np.arange(shape[2])
+ ii, jj, kk = np.meshgrid(i, j, k)
+ vol[ii * ii + jj * jj + kk * kk < n ** 2] = 1
+ vol[ii * ii + jj * jj + kk * kk < (0.5 * n) ** 2] = 2
+
+ mesh = pygalmesh.generate_from_array(
+ vol,
+ h,
+ max_cell_circumradius={1: 100 * min(h), 2: 10 * min(h)},
+ max_facet_distance=min(h),
+ verbose=False,
+ )
+
+ tol = min(h)
+ ref = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0]
+ assert abs(max(mesh.points[:, 0]) - ref[0]) < tol
+ assert abs(min(mesh.points[:, 0]) - ref[1]) < tol
+ assert abs(max(mesh.points[:, 1]) - ref[2]) < tol
+ assert abs(min(mesh.points[:, 1]) - ref[3]) < tol
+ assert abs(max(mesh.points[:, 2]) - ref[4]) < tol
+ assert abs(min(mesh.points[:, 2]) - ref[5]) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ ref = 1.0 / 6.0 * np.pi
+ # Debian needs 2.0e-2 here.
+ # <https://github.com/nschloe/pygalmesh/issues/60>
+ assert abs(vol - ref) < ref * 2.0e-2
--- /dev/null
+import pathlib
+import tempfile
+
+import helpers
+import meshio
+
+import pygalmesh
+
+
+def test_inr():
+ this_dir = pathlib.Path(__file__).resolve().parent
+ # mesh = pygalmesh.generate_from_inr(
+ # this_dir / "meshes" / "skull_2.9.inr", max_cell_circumradius=5.0, verbose=False
+ # )
+ with tempfile.TemporaryDirectory() as tmp:
+ out_filename = str(pathlib.Path(tmp) / "out.vtk")
+ pygalmesh._cli.cli(
+ [
+ "from-inr",
+ str(this_dir / "meshes" / "sphere.inr"),
+ out_filename,
+ "--max-cell-circumradius",
+ "1.0",
+ "--quiet",
+ ]
+ )
+ mesh = meshio.read(out_filename)
+
+ vals_refs = [
+ (max(mesh.points[:, 0]), 9.00478554e00),
+ (min(mesh.points[:, 0]), -4.25843196e-03),
+ (max(mesh.points[:, 1]), 9.00332642e00),
+ (min(mesh.points[:, 1]), -4.41271299e-03),
+ (max(mesh.points[:, 2]), 9.00407982e00),
+ (min(mesh.points[:, 2]), -3.98639357e-03),
+ ]
+ for val, ref in vals_refs:
+ assert abs(val - ref) < 1.0e-3 * (1.0 + abs(ref)), f"{val:.8e} != {ref:.8e}"
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ ref = 6.95558790e02
+ # Debian needs 2.0e-2 here.
+ # <https://github.com/nschloe/pygalmesh/issues/60>
+ assert abs(vol - ref) < ref * 2.0e-2, f"{vol:.8e}"
--- /dev/null
+import numpy
+
+import pygalmesh
+
+
+def test_schwarz():
+ class Schwarz(pygalmesh.DomainBase):
+ def __init__(self):
+ super().__init__()
+
+ def eval(self, x):
+ x2 = numpy.cos(x[0] * 2 * numpy.pi)
+ y2 = numpy.cos(x[1] * 2 * numpy.pi)
+ z2 = numpy.cos(x[2] * 2 * numpy.pi)
+ return x2 + y2 + z2
+
+ mesh = pygalmesh.generate_periodic_mesh(
+ Schwarz(),
+ [0, 0, 0, 1, 1, 1],
+ max_cell_circumradius=0.05,
+ min_facet_angle=30,
+ max_radius_surface_delaunay_ball=0.05,
+ max_facet_distance=0.025,
+ max_circumradius_edge_ratio=2.0,
+ number_of_copies_in_output=4,
+ # odt=True,
+ # lloyd=True,
+ verbose=False,
+ )
+
+ # The RNG in CGAL makes the following assertions fail sometimes.
+ # assert len(mesh.cells["triangle"]) == 12784
+ # assert len(mesh.cells["tetra"]) == 67120
+
+ return mesh
+
+
+if __name__ == "__main__":
+ import meshio
+
+ mesh = test_schwarz()
+ meshio.write("out.vtk", mesh)
--- /dev/null
+import pathlib
+import tempfile
+
+import helpers
+import meshio
+
+import pygalmesh
+
+
+def test_remesh_surface():
+ this_dir = pathlib.Path(__file__).resolve().parent
+ # mesh = pygalmesh.remesh_surface(
+ # this_dir / "meshes" / "lion-head.vtu",
+ # max_edge_size_at_feature_edges=0.025,
+ # min_facet_angle=25,
+ # max_radius_surface_delaunay_ball=0.1,
+ # max_facet_distance=0.001,
+ # verbose=False,
+ # )
+ with tempfile.TemporaryDirectory() as tmp:
+ out_filename = str(pathlib.Path(tmp) / "out.vtk")
+ pygalmesh._cli.cli(
+ [
+ "remesh-surface",
+ str(this_dir / "meshes" / "elephant.vtu"),
+ out_filename,
+ "--max-edge-size-at-feature-edges",
+ "0.025",
+ "--min-facet-angle",
+ "25",
+ "--max-radius-surface-delaunay-ball",
+ "0.1",
+ "--max-facet-distance",
+ "0.001",
+ "--quiet",
+ ]
+ )
+ mesh = meshio.read(out_filename)
+
+ vals_refs = [
+ (max(mesh.points[:, 0]), 3.60217000e-01),
+ (min(mesh.points[:, 0]), -3.60140000e-01),
+ (max(mesh.points[:, 1]), 4.98948000e-01),
+ (min(mesh.points[:, 1]), -4.99336000e-01),
+ (max(mesh.points[:, 2]), 3.00977000e-01),
+ (min(mesh.points[:, 2]), -3.01316000e-01),
+ ]
+ for val, ref in vals_refs:
+ assert abs(val - ref) < 1.0e-3 * (1.0 + abs(ref)), f"{val:.8e} != {ref:.8e}"
+
+ triangle_areas = helpers.compute_triangle_areas(
+ mesh.points, mesh.get_cells_type("triangle")
+ )
+ vol = sum(triangle_areas)
+ ref = 1.2357989593759846
+ assert abs(vol - ref) < ref * 1.0e-3, vol
--- /dev/null
+import helpers
+import numpy
+
+import pygalmesh
+
+
+def test_sphere():
+ radius = 1.0
+ s = pygalmesh.Ball([0.0, 0.0, 0.0], radius)
+ mesh = pygalmesh.generate_surface_mesh(
+ s,
+ min_facet_angle=30.0,
+ max_radius_surface_delaunay_ball=0.1,
+ max_facet_distance=0.1,
+ verbose=False,
+ )
+
+ tol = 1.0e-2
+ assert abs(max(mesh.points[:, 0]) - radius) < (1.0 + radius) * tol
+ assert abs(min(mesh.points[:, 0]) + radius) < (1.0 + radius) * tol
+ assert abs(max(mesh.points[:, 1]) - radius) < (1.0 + radius) * tol
+ assert abs(min(mesh.points[:, 1]) + radius) < (1.0 + radius) * tol
+ assert abs(max(mesh.points[:, 2]) - radius) < (1.0 + radius) * tol
+ assert abs(min(mesh.points[:, 2]) + radius) < (1.0 + radius) * tol
+
+ areas = helpers.compute_triangle_areas(mesh.points, mesh.get_cells_type("triangle"))
+ surface_area = sum(areas)
+ assert abs(surface_area - 4 * numpy.pi * radius ** 2) < 0.1
--- /dev/null
+import pathlib
+import tempfile
+
+import helpers
+import meshio
+
+import pygalmesh
+
+
+def test_volume_from_surface():
+ this_dir = pathlib.Path(__file__).resolve().parent
+ # mesh = pygalmesh.generate_volume_mesh_from_surface_mesh(
+ # this_dir / "meshes" / "elephant.vtu",
+ # min_facet_angle=25.0,
+ # max_radius_surface_delaunay_ball=0.15,
+ # max_facet_distance=0.008,
+ # max_circumradius_edge_ratio=3.0,
+ # verbose=False,
+ # )
+ with tempfile.TemporaryDirectory() as tmp:
+ out_filename = str(pathlib.Path(tmp) / "out.vtk")
+ pygalmesh._cli.cli(
+ [
+ "volume-from-surface",
+ str(this_dir / "meshes" / "elephant.vtu"),
+ out_filename,
+ "--min-facet-angle",
+ "0.5",
+ "--max-radius-surface-delaunay-ball",
+ "0.15",
+ "--max-facet-distance",
+ "0.008",
+ "--max-circumradius-edge-ratio",
+ "3.0",
+ "--quiet",
+ ]
+ )
+ mesh = meshio.read(out_filename)
+
+ tol = 2.0e-2
+ vals_refs = [
+ (max(mesh.points[:, 0]), +0.357612477657),
+ (min(mesh.points[:, 0]), -0.358747130015),
+ (max(mesh.points[:, 1]), +0.496137874959),
+ (min(mesh.points[:, 1]), -0.495301051456),
+ (max(mesh.points[:, 2]), +0.298780230629),
+ (min(mesh.points[:, 2]), -0.300472866512),
+ ]
+ for val, ref in vals_refs:
+ assert abs(val - ref) < (1.0 + ref) * tol, f"{val:.15e} != {ref:.15e}"
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 0.044164693065) < (1.0 + vol) * tol
--- /dev/null
+import helpers
+import numpy
+
+import pygalmesh
+
+
+def test_ball():
+ s = pygalmesh.Ball([0.0, 0.0, 0.0], 1.0)
+ mesh = pygalmesh.generate_mesh(s, max_cell_circumradius=0.2, verbose=False)
+
+ assert abs(max(mesh.points[:, 0]) - 1.0) < 0.02
+ assert abs(min(mesh.points[:, 0]) + 1.0) < 0.02
+ assert abs(max(mesh.points[:, 1]) - 1.0) < 0.02
+ assert abs(min(mesh.points[:, 1]) + 1.0) < 0.02
+ assert abs(max(mesh.points[:, 2]) - 1.0) < 0.02
+ assert abs(min(mesh.points[:, 2]) + 1.0) < 0.02
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 4.0 / 3.0 * numpy.pi) < 0.15
+
+
+def test_balls_union():
+ radius = 1.0
+ displacement = 0.5
+ s0 = pygalmesh.Ball([displacement, 0, 0], radius)
+ s1 = pygalmesh.Ball([-displacement, 0, 0], radius)
+ u = pygalmesh.Union([s0, s1])
+
+ a = numpy.sqrt(radius ** 2 - displacement ** 2)
+ max_edge_size_at_feature_edges = 0.1
+ n = int(2 * numpy.pi * a / max_edge_size_at_feature_edges)
+ circ = [
+ [0.0, a * numpy.cos(i * 2 * numpy.pi / n), a * numpy.sin(i * 2 * numpy.pi / n)]
+ for i in range(n)
+ ]
+ circ.append(circ[0])
+
+ mesh = pygalmesh.generate_mesh(
+ u,
+ extra_feature_edges=[circ],
+ max_cell_circumradius=0.15,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ verbose=False,
+ )
+
+ assert abs(max(mesh.points[:, 0]) - (radius + displacement)) < 0.02
+ assert abs(min(mesh.points[:, 0]) + (radius + displacement)) < 0.02
+ assert abs(max(mesh.points[:, 1]) - radius) < 0.02
+ assert abs(min(mesh.points[:, 1]) + radius) < 0.02
+ assert abs(max(mesh.points[:, 2]) - radius) < 0.02
+ assert abs(min(mesh.points[:, 2]) + radius) < 0.02
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ h = radius - displacement
+ ref_vol = 2 * (
+ 4.0 / 3.0 * numpy.pi * radius ** 3 - h * numpy.pi / 6.0 * (3 * a ** 2 + h ** 2)
+ )
+
+ assert abs(vol - ref_vol) < 0.1
+
+
+def test_balls_intersection():
+ radius = 1.0
+ displacement = 0.5
+ s0 = pygalmesh.Ball([displacement, 0, 0], radius)
+ s1 = pygalmesh.Ball([-displacement, 0, 0], radius)
+ u = pygalmesh.Intersection([s0, s1])
+
+ a = numpy.sqrt(radius ** 2 - displacement ** 2)
+ max_edge_size_at_feature_edges = 0.1
+ n = int(2 * numpy.pi * a / max_edge_size_at_feature_edges)
+ circ = [
+ [0.0, a * numpy.cos(i * 2 * numpy.pi / n), a * numpy.sin(i * 2 * numpy.pi / n)]
+ for i in range(n)
+ ]
+ circ.append(circ[0])
+
+ mesh = pygalmesh.generate_mesh(
+ u,
+ extra_feature_edges=[circ],
+ max_cell_circumradius=0.15,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ verbose=False,
+ )
+
+ assert abs(max(mesh.points[:, 0]) - (radius - displacement)) < 0.02
+ assert abs(min(mesh.points[:, 0]) + (radius - displacement)) < 0.02
+ assert abs(max(mesh.points[:, 1]) - a) < 0.02
+ assert abs(min(mesh.points[:, 1]) + a) < 0.02
+ assert abs(max(mesh.points[:, 2]) - a) < 0.02
+ assert abs(min(mesh.points[:, 2]) + a) < 0.02
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ h = radius - displacement
+ ref_vol = 2 * (h * numpy.pi / 6.0 * (3 * a ** 2 + h ** 2))
+
+ assert abs(vol - ref_vol) < 0.1
+
+
+def test_balls_difference():
+ radius = 1.0
+ displacement = 0.5
+ s0 = pygalmesh.Ball([displacement, 0, 0], radius)
+ s1 = pygalmesh.Ball([-displacement, 0, 0], radius)
+ u = pygalmesh.Difference(s0, s1)
+
+ a = numpy.sqrt(radius ** 2 - displacement ** 2)
+ max_edge_size_at_feature_edges = 0.15
+ n = int(2 * numpy.pi * a / max_edge_size_at_feature_edges)
+ circ = [
+ [0.0, a * numpy.cos(i * 2 * numpy.pi / n), a * numpy.sin(i * 2 * numpy.pi / n)]
+ for i in range(n)
+ ]
+ circ.append(circ[0])
+
+ mesh = pygalmesh.generate_mesh(
+ u,
+ extra_feature_edges=[circ],
+ max_cell_circumradius=0.15,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ min_facet_angle=25,
+ max_radius_surface_delaunay_ball=0.15,
+ max_circumradius_edge_ratio=2.0,
+ verbose=False,
+ )
+
+ tol = 0.02
+ assert abs(max(mesh.points[:, 0]) - (radius + displacement)) < tol
+ assert abs(min(mesh.points[:, 0]) - 0.0) < tol
+ assert abs(max(mesh.points[:, 1]) - radius) < tol
+ assert abs(min(mesh.points[:, 1]) + radius) < tol
+ assert abs(max(mesh.points[:, 2]) - radius) < tol
+ assert abs(min(mesh.points[:, 2]) + radius) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ h = radius - displacement
+ ref_vol = 4.0 / 3.0 * numpy.pi * radius ** 3 - 2 * h * numpy.pi / 6.0 * (
+ 3 * a ** 2 + h ** 2
+ )
+
+ assert abs(vol - ref_vol) < 0.05
+
+
+def test_cuboids_intersection():
+ c0 = pygalmesh.Cuboid([0, 0, -0.5], [3, 3, 0.5])
+ c1 = pygalmesh.Cuboid([1, 1, -2], [2, 2, 2])
+ u = pygalmesh.Intersection([c0, c1])
+
+ # In CGAL, feature edges must not intersect, and that's a problem here: The
+ # intersection edges of the cuboids share eight points with the edges of
+ # the tall and skinny cuboid.
+ # eps = 1.0e-2
+ # extra_features = [
+ # [[1.0, 1.0 + eps, 0.5], [1.0, 2.0 - eps, 0.5]],
+ # [[1.0 + eps, 2.0, 0.5], [2.0 - eps, 2.0, 0.5]],
+ # [[2.0, 2.0 - eps, 0.5], [2.0, 1.0 + eps, 0.5]],
+ # [[2.0 - eps, 1.0, 0.5], [1.0 + eps, 1.0, 0.5]],
+ # ]
+
+ mesh = pygalmesh.generate_mesh(
+ u,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=0.1,
+ verbose=False,
+ )
+
+ # filter the vertices that belong to cells
+ verts = mesh.points[numpy.unique(mesh.get_cells_type("tetra"))]
+
+ tol = 1.0e-2
+ assert abs(max(verts[:, 0]) - 2.0) < tol
+ assert abs(min(verts[:, 0]) - 1.0) < tol
+ assert abs(max(verts[:, 1]) - 2.0) < tol
+ assert abs(min(verts[:, 1]) - 1.0) < tol
+ assert abs(max(verts[:, 2]) - 0.5) < 0.05
+ assert abs(min(verts[:, 2]) + 0.5) < 0.05
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 1.0) < 0.05
+
+
+def test_cuboids_union():
+ c0 = pygalmesh.Cuboid([0, 0, -0.5], [3, 3, 0.5])
+ c1 = pygalmesh.Cuboid([1, 1, -2], [2, 2, 2])
+ u = pygalmesh.Union([c0, c1])
+
+ mesh = pygalmesh.generate_mesh(
+ u,
+ max_cell_circumradius=0.2,
+ max_edge_size_at_feature_edges=0.2,
+ verbose=False,
+ )
+
+ # filter the vertices that belong to cells
+ verts = mesh.points[numpy.unique(mesh.get_cells_type("tetra"))]
+
+ tol = 1.0e-2
+ assert abs(max(verts[:, 0]) - 3.0) < tol
+ assert abs(min(verts[:, 0]) - 0.0) < tol
+ assert abs(max(verts[:, 1]) - 3.0) < tol
+ assert abs(min(verts[:, 1]) - 0.0) < tol
+ assert abs(max(verts[:, 2]) - 2.0) < tol
+ assert abs(min(verts[:, 2]) + 2.0) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 12.0) < 0.1
+
+
+def test_cuboid():
+ s0 = pygalmesh.Cuboid([0, 0, 0], [1, 2, 3])
+ mesh = pygalmesh.generate_mesh(
+ s0, max_edge_size_at_feature_edges=0.1, verbose=False
+ )
+
+ tol = 1.0e-3
+ assert abs(max(mesh.points[:, 0]) - 1.0) < tol
+ assert abs(min(mesh.points[:, 0]) + 0.0) < tol
+ assert abs(max(mesh.points[:, 1]) - 2.0) < tol
+ assert abs(min(mesh.points[:, 1]) + 0.0) < tol
+ assert abs(max(mesh.points[:, 2]) - 3.0) < tol
+ assert abs(min(mesh.points[:, 2]) + 0.0) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 6.0) < tol
+
+
+def test_cone():
+ base_radius = 1.0
+ height = 2.0
+ max_edge_size_at_feature_edges = 0.1
+ s0 = pygalmesh.Cone(base_radius, height, max_edge_size_at_feature_edges)
+ mesh = pygalmesh.generate_mesh(
+ s0,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ verbose=False,
+ )
+
+ tol = 2.0e-1
+ assert abs(max(mesh.points[:, 0]) - base_radius) < tol
+ assert abs(min(mesh.points[:, 0]) + base_radius) < tol
+ assert abs(max(mesh.points[:, 1]) - base_radius) < tol
+ assert abs(min(mesh.points[:, 1]) + base_radius) < tol
+ assert abs(max(mesh.points[:, 2]) - height) < tol
+ assert abs(min(mesh.points[:, 2]) + 0.0) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ ref_vol = numpy.pi * base_radius * base_radius / 3.0 * height
+ assert abs(vol - ref_vol) < tol
+
+
+def test_cylinder():
+ radius = 1.0
+ z0 = 0.0
+ z1 = 1.0
+ edge_length = 0.1
+ s0 = pygalmesh.Cylinder(z0, z1, radius, edge_length)
+ mesh = pygalmesh.generate_mesh(
+ s0,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=edge_length,
+ verbose=False,
+ )
+
+ tol = 1.0e-1
+ assert abs(max(mesh.points[:, 0]) - radius) < tol
+ assert abs(min(mesh.points[:, 0]) + radius) < tol
+ assert abs(max(mesh.points[:, 1]) - radius) < tol
+ assert abs(min(mesh.points[:, 1]) + radius) < tol
+ assert abs(max(mesh.points[:, 2]) - z1) < tol
+ assert abs(min(mesh.points[:, 2]) + z0) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ ref_vol = numpy.pi * radius * radius * (z1 - z0)
+ assert abs(vol - ref_vol) < tol
+
+
+def test_tetrahedron():
+ s0 = pygalmesh.Tetrahedron(
+ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]
+ )
+ mesh = pygalmesh.generate_mesh(
+ s0,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=0.1,
+ verbose=False,
+ )
+
+ tol = 1.0e-3
+ assert abs(max(mesh.points[:, 0]) - 1.0) < tol
+ assert abs(min(mesh.points[:, 0]) + 0.0) < tol
+ assert abs(max(mesh.points[:, 1]) - 1.0) < tol
+ assert abs(min(mesh.points[:, 1]) + 0.0) < tol
+ assert abs(max(mesh.points[:, 2]) - 1.0) < tol
+ assert abs(min(mesh.points[:, 2]) + 0.0) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 1.0 / 6.0) < tol
+
+
+def test_torus():
+ major_radius = 1.0
+ minor_radius = 0.5
+ s0 = pygalmesh.Torus(major_radius, minor_radius)
+ mesh = pygalmesh.generate_mesh(s0, max_cell_circumradius=0.1, verbose=False)
+
+ tol = 1.0e-2
+ radii_sum = major_radius + minor_radius
+ assert abs(max(mesh.points[:, 0]) - radii_sum) < tol
+ assert abs(min(mesh.points[:, 0]) + radii_sum) < tol
+ assert abs(max(mesh.points[:, 1]) - radii_sum) < tol
+ assert abs(min(mesh.points[:, 1]) + radii_sum) < tol
+ assert abs(max(mesh.points[:, 2]) - minor_radius) < tol
+ assert abs(min(mesh.points[:, 2]) + minor_radius) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ ref_vol = (numpy.pi * minor_radius * minor_radius) * (2 * numpy.pi * major_radius)
+ assert abs(vol - ref_vol) < 1.0e-1
+
+
+def test_custom_function():
+ class Hyperboloid(pygalmesh.DomainBase):
+ def __init__(self, max_edge_size_at_feature_edges):
+ super().__init__()
+ self.z0 = -1.0
+ self.z1 = 1.0
+ self.waist_radius = 0.5
+ self.max_edge_size_at_feature_edges = max_edge_size_at_feature_edges
+
+ def eval(self, x):
+ if self.z0 < x[2] and x[2] < self.z1:
+ return x[0] ** 2 + x[1] ** 2 - (x[2] ** 2 + self.waist_radius) ** 2
+ return 1.0
+
+ def get_bounding_sphere_squared_radius(self):
+ z_max = max(abs(self.z0), abs(self.z1))
+ r_max = z_max ** 2 + self.waist_radius
+ return r_max * r_max + z_max * z_max
+
+ def get_features(self):
+ radius0 = self.z0 ** 2 + self.waist_radius
+ n0 = int(2 * numpy.pi * radius0 / self.max_edge_size_at_feature_edges)
+ circ0 = [
+ [
+ radius0 * numpy.cos((2 * numpy.pi * k) / n0),
+ radius0 * numpy.sin((2 * numpy.pi * k) / n0),
+ self.z0,
+ ]
+ for k in range(n0)
+ ]
+ circ0.append(circ0[0])
+
+ radius1 = self.z1 ** 2 + self.waist_radius
+ n1 = int(2 * numpy.pi * radius1 / self.max_edge_size_at_feature_edges)
+ circ1 = [
+ [
+ radius1 * numpy.cos((2 * numpy.pi * k) / n1),
+ radius1 * numpy.sin((2 * numpy.pi * k) / n1),
+ self.z1,
+ ]
+ for k in range(n1)
+ ]
+ circ1.append(circ1[0])
+ return [circ0, circ1]
+
+ max_edge_size_at_feature_edges = 0.12
+ d = Hyperboloid(max_edge_size_at_feature_edges)
+
+ mesh = pygalmesh.generate_mesh(
+ d,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ verbose=False,
+ )
+
+ # TODO check the reference values
+ tol = 1.0e-1
+ assert abs(max(mesh.points[:, 0]) - 1.4) < tol
+ assert abs(min(mesh.points[:, 0]) + 1.4) < tol
+ assert abs(max(mesh.points[:, 1]) - 1.4) < tol
+ assert abs(min(mesh.points[:, 1]) + 1.4) < tol
+ assert abs(max(mesh.points[:, 2]) - 1.0) < tol
+ assert abs(min(mesh.points[:, 2]) + 1.0) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 2 * numpy.pi * 47.0 / 60.0) < 0.16
+
+
+def test_scaling():
+ alpha = 1.3
+ s = pygalmesh.Scale(pygalmesh.Cuboid([0, 0, 0], [1, 2, 3]), alpha)
+ mesh = pygalmesh.generate_mesh(
+ s,
+ max_cell_circumradius=0.2,
+ max_edge_size_at_feature_edges=0.1,
+ verbose=False,
+ )
+
+ tol = 1.0e-2
+ assert abs(max(mesh.points[:, 0]) - 1 * alpha) < tol
+ assert abs(min(mesh.points[:, 0]) + 0.0) < tol
+ assert abs(max(mesh.points[:, 1]) - 2 * alpha) < tol
+ assert abs(min(mesh.points[:, 1]) + 0.0) < tol
+ assert abs(max(mesh.points[:, 2]) - 3 * alpha) < tol
+ assert abs(min(mesh.points[:, 2]) + 0.0) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 6.0 * alpha ** 3) < tol
+
+
+def test_stretch():
+ alpha = 2.0
+ s = pygalmesh.Stretch(pygalmesh.Cuboid([0, 0, 0], [1, 2, 3]), [alpha, 0.0, 0.0])
+ mesh = pygalmesh.generate_mesh(
+ s,
+ max_cell_circumradius=0.2,
+ max_edge_size_at_feature_edges=0.2,
+ verbose=False,
+ )
+
+ tol = 1.0e-2
+ assert abs(max(mesh.points[:, 0]) - alpha) < tol
+ assert abs(min(mesh.points[:, 0]) + 0.0) < tol
+ assert abs(max(mesh.points[:, 1]) - 2.0) < tol
+ assert abs(min(mesh.points[:, 1]) + 0.0) < tol
+ assert abs(max(mesh.points[:, 2]) - 3.0) < tol
+ assert abs(min(mesh.points[:, 2]) + 0.0) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 12.0) < tol
+
+
+def test_rotation():
+ s0 = pygalmesh.Rotate(
+ pygalmesh.Cuboid([0, 0, 0], [1, 2, 3]), [1.0, 0.0, 0.0], numpy.pi / 12.0
+ )
+ mesh = pygalmesh.generate_mesh(
+ s0,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=0.1,
+ verbose=False,
+ )
+
+ tol = 1.0e-2
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 6.0) < tol
+
+
+def test_translation():
+ s0 = pygalmesh.Translate(pygalmesh.Cuboid([0, 0, 0], [1, 2, 3]), [1.0, 0.0, 0.0])
+ mesh = pygalmesh.generate_mesh(
+ s0,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=0.1,
+ verbose=False,
+ )
+
+ tol = 1.0e-2
+ assert abs(max(mesh.points[:, 0]) - 2.0) < tol
+ assert abs(min(mesh.points[:, 0]) - 1.0) < tol
+ assert abs(max(mesh.points[:, 1]) - 2.0) < tol
+ assert abs(min(mesh.points[:, 1]) + 0.0) < tol
+ assert abs(max(mesh.points[:, 2]) - 3.0) < tol
+ assert abs(min(mesh.points[:, 2]) + 0.0) < tol
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 6.0) < tol
+
+
+def test_extrude():
+ p = pygalmesh.Polygon2D([[-0.5, -0.3], [0.5, -0.3], [0.0, 0.5]])
+ domain = pygalmesh.Extrude(p, [0.0, 0.3, 1.0])
+ mesh = pygalmesh.generate_mesh(
+ domain,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=0.1,
+ verbose=False,
+ )
+
+ tol = 1.0e-3
+ assert abs(max(mesh.points[:, 0]) - 0.5) < tol
+ assert abs(min(mesh.points[:, 0]) + 0.5) < tol
+ assert abs(max(mesh.points[:, 1]) - 0.8) < tol
+ assert abs(min(mesh.points[:, 1]) + 0.3) < tol
+ # Relax tolerance for debian, see <https://github.com/nschloe/pygalmesh/pull/47>
+ assert abs(max(mesh.points[:, 2]) - 1.0) < 1.1e-3
+ assert abs(min(mesh.points[:, 2]) + 0.0) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 0.4) < tol
+
+
+def test_extrude_rotate():
+ p = pygalmesh.Polygon2D([[-0.5, -0.3], [0.5, -0.3], [0.0, 0.5]])
+ max_edge_size_at_feature_edges = 0.1
+ domain = pygalmesh.Extrude(
+ p,
+ [0.0, 0.0, 1.0],
+ 0.5 * 3.14159265359,
+ max_edge_size_at_feature_edges,
+ )
+ mesh = pygalmesh.generate_mesh(
+ domain,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ verbose=False,
+ )
+
+ tol = 1.0e-3
+ assert abs(max(mesh.points[:, 0]) - 0.583012701892) < tol
+ assert abs(min(mesh.points[:, 0]) + 0.5) < tol
+ assert abs(max(mesh.points[:, 1]) - 0.5) < tol
+ assert abs(min(mesh.points[:, 1]) + 0.583012701892) < tol
+ assert abs(max(mesh.points[:, 2]) - 1.0) < tol
+ assert abs(min(mesh.points[:, 2]) + 0.0) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 0.4) < 0.05
+
+
+def test_ring_extrude():
+ p = pygalmesh.Polygon2D([[0.5, -0.3], [1.5, -0.3], [1.0, 0.5]])
+ max_edge_size_at_feature_edges = 0.1
+ domain = pygalmesh.RingExtrude(p, max_edge_size_at_feature_edges)
+ mesh = pygalmesh.generate_mesh(
+ domain,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ verbose=False,
+ )
+
+ tol = 1.0e-2
+ assert abs(max(mesh.points[:, 0]) - 1.5) < tol
+ assert abs(min(mesh.points[:, 0]) + 1.5) < tol
+ assert abs(max(mesh.points[:, 1]) - 1.5) < tol
+ assert abs(min(mesh.points[:, 1]) + 1.5) < tol
+ assert abs(max(mesh.points[:, 2]) - 0.5) < tol
+ assert abs(min(mesh.points[:, 2]) + 0.3) < tol
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 2 * numpy.pi * 0.4) < 0.05
+
+
+def test_heart():
+ class Heart(pygalmesh.DomainBase):
+ def __init__(self, max_edge_size_at_feature_edges):
+ super().__init__()
+
+ def eval(self, x):
+ return (
+ (x[0] ** 2 + 9.0 / 4.0 * x[1] ** 2 + x[2] ** 2 - 1) ** 3
+ - x[0] ** 2 * x[2] ** 3
+ - 9.0 / 80.0 * x[1] ** 2 * x[2] ** 3
+ )
+
+ def get_bounding_sphere_squared_radius(self):
+ return 10.0
+
+ max_edge_size_at_feature_edges = 0.1
+ d = Heart(max_edge_size_at_feature_edges)
+
+ mesh = pygalmesh.generate_mesh(
+ d,
+ max_cell_circumradius=0.1,
+ max_edge_size_at_feature_edges=max_edge_size_at_feature_edges,
+ # odt=True,
+ # lloyd=True,
+ # verbose=True
+ )
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ ref = 3.3180194961823872
+ assert abs(vol - ref) < 1.0e-3 * ref
+
+
+def test_halfspace():
+ c = pygalmesh.Cuboid([0, 0, 0], [1, 1, 1])
+ s = pygalmesh.HalfSpace([1.0, 2.0, 3.0], 1.0, 2.0)
+ u = pygalmesh.Intersection([c, s])
+
+ mesh = pygalmesh.generate_mesh(
+ u,
+ max_cell_circumradius=0.2,
+ max_edge_size_at_feature_edges=0.2,
+ verbose=False,
+ )
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 1 / 750) < 1.0e-3
+
+
+def test_ball_with_sizing_field():
+ mesh = pygalmesh.generate_mesh(
+ pygalmesh.Ball([0.0, 0.0, 0.0], 1.0),
+ min_facet_angle=30.0,
+ max_radius_surface_delaunay_ball=0.1,
+ max_facet_distance=0.025,
+ max_circumradius_edge_ratio=2.0,
+ max_cell_circumradius=lambda x: abs(numpy.sqrt(numpy.dot(x, x)) - 0.5) / 5
+ + 0.025,
+ verbose=False,
+ )
+
+ assert abs(max(mesh.points[:, 0]) - 1.0) < 0.02
+ assert abs(min(mesh.points[:, 0]) + 1.0) < 0.02
+ assert abs(max(mesh.points[:, 1]) - 1.0) < 0.02
+ assert abs(min(mesh.points[:, 1]) + 1.0) < 0.02
+ assert abs(max(mesh.points[:, 2]) - 1.0) < 0.02
+ assert abs(min(mesh.points[:, 2]) + 1.0) < 0.02
+
+ vol = sum(helpers.compute_volumes(mesh.points, mesh.get_cells_type("tetra")))
+ assert abs(vol - 4.0 / 3.0 * numpy.pi) < 0.15
+
+
+if __name__ == "__main__":
+ test_ball()
+ # test_ball_with_sizing_field()
--- /dev/null
+[tox]
+envlist = py3
+isolated_build = True
+
+[testenv]
+deps =
+ pytest
+ pytest-codeblocks
+ pytest-cov
+ pytest-randomly
+commands =
+ pytest {posargs} --codeblocks