diff --git a/CHANGELOG.md b/CHANGELOG.md index d792ec2..5518d3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +**v2.9.0** No-one helped with think_open branch, that was trying to get ready for jdk9 (as compilable), so I'm abandoning 2.8.0 series, the 2.9.0 version will create an integrated jar and concentrate on compatibility, rather be future looking (but I'm not sure that think_different is any use or will work in future). + **v2.7.2** Slight re-factor of control_panel, fix virgin install of libraries folder, add grafica library examples suggest upgrade to jruby-9.1.17.0 **v2.7.1** Avoid calling protected method in control_panel (ready for jdk9) diff --git a/README.md b/README.md index 19f4421..1e50036 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ adjust above for your OS/distro setup. ## Requirements -- jdk8+ (jdk9 mostly works, see changelog, but is noisy) -- jruby-9.1.16.0 -- mvn-3.5.0+ -- processing-core.jar (_build only_) see [propane-core](https://github.com/ruby-processing/processing-core) +- `jdk8_u172` +- `jruby-9.1.16.0` +- `mvn-3.5.0+` +- `apple.jar` (_build only_) see [vanilla-processing](https://github.com/processing/processing/blob/master/core/apple.jar) ## Building and testing @@ -23,7 +23,7 @@ rake javadoc ## Installation ```bash jgem install propane # from rubygems -jgem install propane-2.7.1-java.gem # local install requires a custom processing-core +jgem install propane-2.9.0-java.gem # local install requires a custom processing-core ``` ## Usage diff --git a/Rakefile b/Rakefile index f01bde5..e84abe6 100644 --- a/Rakefile +++ b/Rakefile @@ -7,7 +7,7 @@ def create_manifest File.open('MANIFEST.MF', 'w') do |f| f.puts(title) f.puts(version) - f.puts('Class-Path: processing-core.jar gluegen-rt-2.3.2.jar jog-all-2.3.2.jar') + f.puts('Class-Path: gluegen-rt-2.3.2.jar jog-all-2.3.2.jar') end end @@ -21,7 +21,7 @@ end desc 'Install' task :install do sh 'mvn dependency:copy' - sh 'mv target/propane.jar lib' + sh 'mv target/propane-2.9.0.jar lib' end desc 'Gem' diff --git a/lib/propane/version.rb b/lib/propane/version.rb index d9306d9..9758341 100644 --- a/lib/propane/version.rb +++ b/lib/propane/version.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true module Propane - VERSION = '2.7.2'.freeze + VERSION = '2.9.0'.freeze end diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..f9b24c5 --- /dev/null +++ b/license.txt @@ -0,0 +1,508 @@ +We use GPL v2 for the parts of the project that we've developed ourselves. +For the 'core' library, it's LGPL, for everything else, it's GPL. + +Over the course of many years of development, files being moved around, +and other code being added and removed, the license information has become +quite a mess. Please help us clean this up so that others are properly +credited and licenses are consistently/correctly noted: +https://github.com/processing/processing/issues/224 + + +..................................................................... + + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + +..................................................................... + + +the original document can be found at: +http://oss.software.ibm.com/developerworks/opensource/license10.html + + +IBM Public License Version 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS IBM +PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF +THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + + +1. DEFINITIONS + +"Contribution" means: + +a) in the case of International Business Machines Corporation ("IBM"), +the Original Program, and + +b) in the case of each Contributor, + +i) changes to the Program, and + +ii) additions to the Program; +where such changes and/or additions to the Program originate from and +are distributed by that particular Contributor. A Contribution +'originates' from a Contributor if it was added to the Program by such +Contributor itself or anyone acting on such Contributor's +behalf. Contributions do not include additions to the Program which: +(i) are separate modules of software distributed in conjunction with +the Program under their own license agreement, and (ii) are not +derivative works of the Program. + +"Contributor" means IBM and any other entity that distributes the +Program. + +"Licensed Patents " mean patent claims licensable by a Contributor +which are necessarily infringed by the use or sale of its Contribution +alone or when combined with the Program. + + +"Original Program" means the original version of the software +accompanying this Agreement as released by IBM, including source code, +object code and documentation, if any. + +"Program" means the Original Program and Contributions. + +"Recipient" means anyone who receives the Program under this +Agreement, including all Contributors. + + +2. GRANT OF RIGHTS + +a) Subject to the terms of this Agreement, each Contributor hereby +grants Recipient a non-exclusive, worldwide, royalty-free copyright +license to reproduce, prepare derivative works of, publicly display, +publicly perform, distribute and sublicense the Contribution of such +Contributor, if any, and such derivative works, in source code and +object code form. + +b) Subject to the terms of this Agreement, each Contributor hereby +grants Recipient a non-exclusive, worldwide, royalty-free patent +license under Licensed Patents to make, use, sell, offer to sell, +import and otherwise transfer the Contribution of such Contributor, if +any, in source code and object code form. This patent license shall +apply to the combination of the Contribution and the Program if, at +the time the Contribution is added by the Contributor, such addition +of the Contribution causes such combination to be covered by the +Licensed Patents. The patent license shall not apply to any other +combinations which include the Contribution. No hardware per se is +licensed hereunder. + +c) Recipient understands that although each Contributor grants the +licenses to its Contributions set forth herein, no assurances are +provided by any Contributor that the Program does not infringe the +patent or other intellectual property rights of any other entity. Each +Contributor disclaims any liability to Recipient for claims brought by +any other entity based on infringement of intellectual property rights +or otherwise. As a condition to exercising the rights and licenses +granted hereunder, each Recipient hereby assumes sole responsibility +to secure any other intellectual property rights needed, if any. For +example, if a third party patent license is required to allow +Recipient to distribute the Program, it is Recipient's responsibility +to acquire that license before distributing the Program. + +d) Each Contributor represents that to its knowledge it has sufficient +copyright rights in its Contribution, if any, to grant the copyright +license set forth in this Agreement. + + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form +under its own license agreement, provided that: + +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: + +i) effectively disclaims on behalf of all Contributors all warranties +and conditions, express and implied, including warranties or +conditions of title and non-infringement, and implied warranties or +conditions of merchantability and fitness for a particular purpose; + +ii) effectively excludes on behalf of all Contributors all liability +for damages, including direct, indirect, special, incidental and +consequential damages, such as lost profits; + +iii) states that any provisions which differ from this Agreement are +offered by that Contributor alone and not by any other party; and + +iv) states that source code for the Program is available from such +Contributor, and informs licensees how to obtain it in a reasonable +manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: +a) it must be made available under this Agreement; and +b) a copy of this Agreement must be included with each copy of the +Program. + +Each Contributor must include the following in a conspicuous location +in the Program: + +Copyright 2003, International Business Machines Corporation and +others. All Rights Reserved. + +In addition, each Contributor must identify itself as the originator +of its Contribution, if any, in a manner that reasonably allows +subsequent Recipients to identify the originator of the Contribution. + + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain +responsibilities with respect to end users, business partners and the +like. While this license is intended to facilitate the commercial use +of the Program, the Contributor who includes the Program in a +commercial product offering should do so in a manner which does not +create potential liability for other Contributors. Therefore, if a +Contributor includes the Program in a commercial product offering, +such Contributor ("Commercial Contributor") hereby agrees to defend +and indemnify every other Contributor ("Indemnified Contributor") +against any losses, damages and costs (collectively "Losses") arising +from claims, lawsuits and other legal actions brought by a third party +against the Indemnified Contributor to the extent caused by the acts +or omissions of such Commercial Contributor in connection with its +distribution of the Program in a commercial product offering. The +obligations in this section do not apply to any claims or Losses +relating to any actual or alleged intellectual property +infringement. In order to qualify, an Indemnified Contributor must: a) +promptly notify the Commercial Contributor in writing of such claim, +and b) allow the Commercial Contributor to control, and cooperate with +the Commercial Contributor in, the defense and any related settlement +negotiations. The Indemnified Contributor may participate in any such +claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those +performance claims and warranties, and if a court requires any other +Contributor to pay any damages as a result, the Commercial Contributor +must pay those damages. + + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS +PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY +WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY +OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely +responsible for determining the appropriateness of using and +distributing the Program and assumes all risks associated with its +exercise of rights under this Agreement, including but not limited to +the risks and costs of program errors, compliance with applicable +laws, damage to or loss of data, programs or equipment, and +unavailability or interruption of operations. + + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR +ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR +DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED +HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and +enforceable. + +If Recipient institutes patent litigation against a Contributor with +respect to a patent applicable to software (including a cross-claim or +counterclaim in a lawsuit), then any patent licenses granted by that +Contributor to such Recipient under this Agreement shall terminate as +of the date such litigation is filed. In addition, If Recipient +institutes patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Program +itself (excluding combinations of the Program with other software or +hardware diff --git a/pom.rb b/pom.rb index fca2c78..3891043 100644 --- a/pom.rb +++ b/pom.rb @@ -1,17 +1,23 @@ require 'fileutils' -project 'rp5extras', 'https://github.com/monkstone/propane' do +project 'propane', 'https://github.com/monkstone/propane' do model_version '4.0.0' - id 'propane:propane', '2.7.2' + id 'propane:propane:2.9.0' packaging 'jar' - description 'rp5extras for propane' + description 'An integrated processing-core (somewhat hacked), with additional java code for a jruby version of processing.' + organization 'ruby-processing', 'https://ruby-processing.github.io' - { 'monkstone' => 'Martin Prout' }.each do |key, value| + + { + 'monkstone' => 'Martin Prout', 'benfry' => 'Ben Fry', + 'REAS' => 'Casey Reas', 'codeanticode' => 'Andres Colubri' + }.each do |key, value| developer key do name value roles 'developer' end end license 'GPL 3', 'http://www.gnu.org/licenses/gpl-3.0-standalone.html' + license 'LGPL 2', 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html' issue_management 'https://github.com/ruby-processing/propane/issues', 'Github' source_control( @@ -100,8 +106,14 @@ end build do - default_goal 'package' - source_directory 'src' - final_name 'propane' + resource do + directory '${source.directory}/main/java' + includes ['**/**/*.glsl', '**/*.jnilib'] + excludes '**/**/*.java' + end + resource do + directory '${source.directory}/main/resources' + includes ['**/*.png', '*.txt'] + end end end diff --git a/pom.xml b/pom.xml index 615d619..b79fdf1 100644 --- a/pom.xml +++ b/pom.xml @@ -11,9 +11,9 @@ DO NOT MODIFIY - GENERATED CODE 4.0.0 propane propane - 2.7.2 - rp5extras - rp5extras for propane + 2.9.0 + propane + An integrated processing-core (somewhat hacked), with additional java code for a jruby version of processing. https://github.com/monkstone/propane ruby-processing @@ -24,6 +24,10 @@ DO NOT MODIFIY - GENERATED CODE GPL 3 http://www.gnu.org/licenses/gpl-3.0-standalone.html + + LGPL 2 + https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html + @@ -33,6 +37,27 @@ DO NOT MODIFIY - GENERATED CODE developer + + benfry + Ben Fry + + developer + + + + REAS + Casey Reas + + developer + + + + codeanticode + Andres Colubri + + developer + + scm:git:git://github.com/ruby-processing/propane.git @@ -62,9 +87,9 @@ DO NOT MODIFIY - GENERATED CODE pom - org.processing - core - 3.3.7 + com.apple.eawt + apple + 1.0 org.processing @@ -83,9 +108,25 @@ DO NOT MODIFIY - GENERATED CODE - src - package - propane + + + ${source.directory}/main/java + + **/**/*.glsl + **/*.jnilib + + + **/**/*.java + + + + ${source.directory}/main/resources + + **/*.png + *.txt + + + diff --git a/propane.gemspec b/propane.gemspec index d886643..e292711 100644 --- a/propane.gemspec +++ b/propane.gemspec @@ -15,8 +15,7 @@ Gem::Specification.new do |gem| gem.summary = %q{ruby wrapper for processing-3.4 on MacOS and linux64 bit only for opengl} gem.homepage = 'https://ruby-processing.github.io/propane/' gem.files = `git ls-files`.split($/) - gem.files << 'lib/propane.jar' - gem.files << 'lib/processing-core.jar' + gem.files << 'lib/propane-2.9.0.jar' gem.files << 'lib/gluegen-rt-2.3.2.jar' gem.files << 'lib/jogl-all-2.3.2.jar' gem.files << 'lib/gluegen-rt-2.3.2-natives-linux-amd64.jar' @@ -30,5 +29,5 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency 'arcball', '~> 1.0', '>= 1.0.0' gem.require_paths = ['lib'] gem.platform = 'java' - gem.requirements << 'java runtime >= 1.8.0_151+' + gem.requirements << 'java runtime >= 1.8.0_171+' end diff --git a/src/main/java/japplemenubar/JAppleMenuBar.java b/src/main/java/japplemenubar/JAppleMenuBar.java new file mode 100644 index 0000000..148eed9 --- /dev/null +++ b/src/main/java/japplemenubar/JAppleMenuBar.java @@ -0,0 +1,88 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2011-12 hansi raber, released under LGPL under agreement + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ +package japplemenubar; + +import java.io.*; + +import processing.core.PApplet; + + +/** + * Starting point for the application. General initialization should be done + * inside the ApplicationController's init() method. If certain kinds of + * non-Swing initialization takes too long, it should happen in a new Thread + * and off the Swing event dispatch thread (EDT). + * + * @author hansi + */ +public class JAppleMenuBar { + static JAppleMenuBar instance; + static final String FILENAME = "libjAppleMenuBar.jnilib"; + + static { + try { + File temp = File.createTempFile("processing", "menubar"); + temp.delete(); // remove the file itself + temp.mkdirs(); // create a directory out of it + temp.deleteOnExit(); + + File jnilibFile = new File(temp, FILENAME); + InputStream input = JAppleMenuBar.class.getResourceAsStream(FILENAME); + if (input != null) { + if (PApplet.saveStream(jnilibFile, input)) { + System.load(jnilibFile.getAbsolutePath()); + instance = new JAppleMenuBar(); + + } else { + sadness("Problem saving " + FILENAME + " for full screen use."); + } + } else { + sadness("Could not load " + FILENAME + " from core.jar"); + } + } catch (IOException e) { + sadness("Unknown error, here's the stack trace."); + e.printStackTrace(); + } + } + + + static void sadness(String msg) { + System.err.println("Full screen mode disabled. " + msg); + } + + +// static public void show() { +// instance.setVisible(true); +// } + + + static public void hide() { + instance.setVisible(false, false); + } + + + public native void setVisible(boolean visibility, boolean kioskMode); + + +// public void setVisible(boolean visibility) { +// // Keep original API in-tact. Default kiosk-mode to off. +// setVisible(visibility, false); +// } +} diff --git a/src/main/java/japplemenubar/libjAppleMenuBar.jnilib b/src/main/java/japplemenubar/libjAppleMenuBar.jnilib new file mode 100755 index 0000000..2c57f64 Binary files /dev/null and b/src/main/java/japplemenubar/libjAppleMenuBar.jnilib differ diff --git a/src/monkstone/ColorUtil.java b/src/main/java/monkstone/ColorUtil.java similarity index 100% rename from src/monkstone/ColorUtil.java rename to src/main/java/monkstone/ColorUtil.java diff --git a/src/monkstone/MathToolModule.java b/src/main/java/monkstone/MathToolModule.java similarity index 100% rename from src/monkstone/MathToolModule.java rename to src/main/java/monkstone/MathToolModule.java diff --git a/src/monkstone/PropaneLibrary.java b/src/main/java/monkstone/PropaneLibrary.java similarity index 100% rename from src/monkstone/PropaneLibrary.java rename to src/main/java/monkstone/PropaneLibrary.java diff --git a/src/monkstone/core/LibraryProxy.java b/src/main/java/monkstone/core/LibraryProxy.java similarity index 100% rename from src/monkstone/core/LibraryProxy.java rename to src/main/java/monkstone/core/LibraryProxy.java diff --git a/src/monkstone/fastmath/Deglut.java b/src/main/java/monkstone/fastmath/Deglut.java similarity index 100% rename from src/monkstone/fastmath/Deglut.java rename to src/main/java/monkstone/fastmath/Deglut.java diff --git a/src/monkstone/fastmath/package-info.java b/src/main/java/monkstone/fastmath/package-info.java similarity index 100% rename from src/monkstone/fastmath/package-info.java rename to src/main/java/monkstone/fastmath/package-info.java diff --git a/src/monkstone/filechooser/Chooser.java b/src/main/java/monkstone/filechooser/Chooser.java similarity index 100% rename from src/monkstone/filechooser/Chooser.java rename to src/main/java/monkstone/filechooser/Chooser.java diff --git a/src/monkstone/noise/SimplexNoise.java b/src/main/java/monkstone/noise/SimplexNoise.java similarity index 100% rename from src/monkstone/noise/SimplexNoise.java rename to src/main/java/monkstone/noise/SimplexNoise.java diff --git a/src/monkstone/slider/CustomHorizontalSlider.java b/src/main/java/monkstone/slider/CustomHorizontalSlider.java similarity index 100% rename from src/monkstone/slider/CustomHorizontalSlider.java rename to src/main/java/monkstone/slider/CustomHorizontalSlider.java diff --git a/src/monkstone/slider/CustomVerticalSlider.java b/src/main/java/monkstone/slider/CustomVerticalSlider.java similarity index 100% rename from src/monkstone/slider/CustomVerticalSlider.java rename to src/main/java/monkstone/slider/CustomVerticalSlider.java diff --git a/src/monkstone/slider/SimpleHorizontalSlider.java b/src/main/java/monkstone/slider/SimpleHorizontalSlider.java similarity index 100% rename from src/monkstone/slider/SimpleHorizontalSlider.java rename to src/main/java/monkstone/slider/SimpleHorizontalSlider.java diff --git a/src/monkstone/slider/SimpleSlider.java b/src/main/java/monkstone/slider/SimpleSlider.java similarity index 100% rename from src/monkstone/slider/SimpleSlider.java rename to src/main/java/monkstone/slider/SimpleSlider.java diff --git a/src/monkstone/slider/SimpleVerticalSlider.java b/src/main/java/monkstone/slider/SimpleVerticalSlider.java similarity index 100% rename from src/monkstone/slider/SimpleVerticalSlider.java rename to src/main/java/monkstone/slider/SimpleVerticalSlider.java diff --git a/src/monkstone/slider/Slider.java b/src/main/java/monkstone/slider/Slider.java similarity index 100% rename from src/monkstone/slider/Slider.java rename to src/main/java/monkstone/slider/Slider.java diff --git a/src/monkstone/slider/SliderBar.java b/src/main/java/monkstone/slider/SliderBar.java similarity index 100% rename from src/monkstone/slider/SliderBar.java rename to src/main/java/monkstone/slider/SliderBar.java diff --git a/src/monkstone/slider/SliderGroup.java b/src/main/java/monkstone/slider/SliderGroup.java similarity index 100% rename from src/monkstone/slider/SliderGroup.java rename to src/main/java/monkstone/slider/SliderGroup.java diff --git a/src/monkstone/slider/WheelHandler.java b/src/main/java/monkstone/slider/WheelHandler.java similarity index 100% rename from src/monkstone/slider/WheelHandler.java rename to src/main/java/monkstone/slider/WheelHandler.java diff --git a/src/monkstone/vecmath/AppRender.java b/src/main/java/monkstone/vecmath/AppRender.java similarity index 100% rename from src/monkstone/vecmath/AppRender.java rename to src/main/java/monkstone/vecmath/AppRender.java diff --git a/src/monkstone/vecmath/JRender.java b/src/main/java/monkstone/vecmath/JRender.java similarity index 100% rename from src/monkstone/vecmath/JRender.java rename to src/main/java/monkstone/vecmath/JRender.java diff --git a/src/monkstone/vecmath/ShapeRender.java b/src/main/java/monkstone/vecmath/ShapeRender.java similarity index 100% rename from src/monkstone/vecmath/ShapeRender.java rename to src/main/java/monkstone/vecmath/ShapeRender.java diff --git a/src/monkstone/vecmath/package-info.java b/src/main/java/monkstone/vecmath/package-info.java similarity index 100% rename from src/monkstone/vecmath/package-info.java rename to src/main/java/monkstone/vecmath/package-info.java diff --git a/src/monkstone/vecmath/vec2/Vec2.java b/src/main/java/monkstone/vecmath/vec2/Vec2.java similarity index 100% rename from src/monkstone/vecmath/vec2/Vec2.java rename to src/main/java/monkstone/vecmath/vec2/Vec2.java diff --git a/src/monkstone/vecmath/vec2/package-info.java b/src/main/java/monkstone/vecmath/vec2/package-info.java similarity index 100% rename from src/monkstone/vecmath/vec2/package-info.java rename to src/main/java/monkstone/vecmath/vec2/package-info.java diff --git a/src/monkstone/vecmath/vec3/Vec3.java b/src/main/java/monkstone/vecmath/vec3/Vec3.java similarity index 100% rename from src/monkstone/vecmath/vec3/Vec3.java rename to src/main/java/monkstone/vecmath/vec3/Vec3.java diff --git a/src/monkstone/vecmath/vec3/package-info.java b/src/main/java/monkstone/vecmath/vec3/package-info.java similarity index 100% rename from src/monkstone/vecmath/vec3/package-info.java rename to src/main/java/monkstone/vecmath/vec3/package-info.java diff --git a/src/monkstone/videoevent/VideoInterface.java b/src/main/java/monkstone/videoevent/VideoInterface.java similarity index 100% rename from src/monkstone/videoevent/VideoInterface.java rename to src/main/java/monkstone/videoevent/VideoInterface.java diff --git a/src/monkstone/videoevent/package-info.java b/src/main/java/monkstone/videoevent/package-info.java similarity index 100% rename from src/monkstone/videoevent/package-info.java rename to src/main/java/monkstone/videoevent/package-info.java diff --git a/src/main/java/processing/awt/PGraphicsJava2D.java b/src/main/java/processing/awt/PGraphicsJava2D.java new file mode 100644 index 0000000..1fc3beb --- /dev/null +++ b/src/main/java/processing/awt/PGraphicsJava2D.java @@ -0,0 +1,3029 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2013-15 The Processing Foundation + Copyright (c) 2005-13 Ben Fry and Casey Reas + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.awt; + +import java.awt.*; +import java.awt.font.TextAttribute; +import java.awt.geom.*; +import java.awt.image.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import processing.core.*; + + +/** + * Subclass for PGraphics that implements the graphics API using Java2D. + *

+ * To get access to the Java 2D "Graphics2D" object for the default + * renderer, use: + *

+ * Graphics2D g2 = (Graphics2D) g.getNative();
+ * 
+ * This will let you do Graphics2D calls directly, but is not supported + * in any way shape or form. Which just means "have fun, but don't complain + * if it breaks." + *

+ * Advanced debugging notes for Java2D. + */ +public class PGraphicsJava2D extends PGraphics { +//// BufferStrategy strategy; +//// BufferedImage bimage; +//// VolatileImage vimage; +// Canvas canvas; +//// boolean useCanvas = true; +// boolean useCanvas = false; +//// boolean useRetina = true; +//// boolean useOffscreen = true; // ~40fps +// boolean useOffscreen = false; + + public Graphics2D g2; +// protected BufferedImage offscreen; + + Composite defaultComposite; + + GeneralPath gpath; + + // path for contours so gpath can be closed + GeneralPath auxPath; + + boolean openContour; + + /// break the shape at the next vertex (next vertex() call is a moveto()) + boolean breakShape; + + /// coordinates for internal curve calculation + float[] curveCoordX; + float[] curveCoordY; + float[] curveDrawX; + float[] curveDrawY; + + int transformCount; + AffineTransform transformStack[] = + new AffineTransform[MATRIX_STACK_DEPTH]; + double[] transform = new double[6]; + + Line2D.Float line = new Line2D.Float(); + Ellipse2D.Float ellipse = new Ellipse2D.Float(); + Rectangle2D.Float rect = new Rectangle2D.Float(); + Arc2D.Float arc = new Arc2D.Float(); + + protected Color tintColorObject; + + protected Color fillColorObject; + public boolean fillGradient; + public Paint fillGradientObject; + + protected Stroke strokeObject; + protected Color strokeColorObject; + public boolean strokeGradient; + public Paint strokeGradientObject; + + Font fontObject; + + + + ////////////////////////////////////////////////////////////// + + // INTERNAL + + + public PGraphicsJava2D() { } + + + //public void setParent(PApplet parent) + + + //public void setPrimary(boolean primary) + + + //public void setPath(String path) + + +// /** +// * Called in response to a resize event, handles setting the +// * new width and height internally, as well as re-allocating +// * the pixel buffer for the new size. +// * +// * Note that this will nuke any cameraMode() settings. +// */ +// @Override +// public void setSize(int iwidth, int iheight) { // ignore +// width = iwidth; +// height = iheight; +// +// allocate(); +// reapplySettings(); +// } + + +// @Override +// protected void allocate() { +// //surface.initImage(this, width, height); +// surface.initImage(this); +// } + + + /* + @Override + protected void allocate() { + // Tried this with RGB instead of ARGB for the primarySurface version, + // but didn't see any performance difference (OS X 10.6, Java 6u24). + // For 0196, also attempted RGB instead of ARGB, but that causes + // strange things to happen with blending. +// image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + if (primarySurface) { + if (useCanvas) { + if (canvas != null) { + parent.removeListeners(canvas); + parent.remove(canvas); + } + canvas = new Canvas(); + canvas.setIgnoreRepaint(true); + +// parent.setLayout(new BorderLayout()); +// parent.add(canvas, BorderLayout.CENTER); + parent.add(canvas); +// canvas.validate(); +// parent.doLayout(); + + if (canvas.getWidth() != width || canvas.getHeight() != height) { + PApplet.debug("PGraphicsJava2D comp size being set to " + width + "x" + height); + canvas.setSize(width, height); + } else { + PApplet.debug("PGraphicsJava2D comp size already " + width + "x" + height); + } + + parent.addListeners(canvas); +// canvas.createBufferStrategy(1); +// g2 = (Graphics2D) canvas.getGraphics(); + + } else { + parent.updateListeners(parent); // in case they're already there + + // using a compatible image here doesn't seem to provide any performance boost + + if (useOffscreen) { + // Needs to be RGB otherwise there's a major performance hit [0204] + // http://code.google.com/p/processing/issues/detail?id=729 + image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); +// GraphicsConfiguration gc = parent.getGraphicsConfiguration(); +// image = gc.createCompatibleImage(width, height); + offscreen = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); +// offscreen = gc.createCompatibleImage(width, height); + g2 = (Graphics2D) offscreen.getGraphics(); + + } else { +// System.out.println("hopefully faster " + width + " " + height); +// new Exception().printStackTrace(System.out); + + GraphicsConfiguration gc = canvas.getGraphicsConfiguration(); + // If not realized (off-screen, i.e the Color Selector Tool), + // gc will be null. + if (gc == null) { + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + gc = ge.getDefaultScreenDevice().getDefaultConfiguration(); + } + + image = gc.createCompatibleImage(width, height); + g2 = (Graphics2D) image.getGraphics(); + } + } + } else { // not the primary surface + // Since this buffer's offscreen anyway, no need for the extra offscreen + // buffer. However, unlike the primary surface, this feller needs to be + // ARGB so that blending ("alpha" compositing) will work properly. + image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + g2 = (Graphics2D) image.getGraphics(); + } + */ + + /* + if (primarySurface) { + Canvas canvas = ((PSurfaceAWT) surface).canvas; + + GraphicsConfiguration gc = canvas.getGraphicsConfiguration(); + // If not realized (off-screen, i.e the Color Selector Tool), + // gc will be null. + if (gc == null) { + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + gc = ge.getDefaultScreenDevice().getDefaultConfiguration(); + } + + image = gc.createCompatibleImage(width, height); + g2 = (Graphics2D) image.getGraphics(); + + } else { + + } + g2 = (Graphics2D) image.getGraphics(); + } + */ + + + //public void dispose() + + + @Override + public PSurface createSurface() { + return surface = new PSurfaceAWT(this); + } + + + /** + * Still need a means to get the java.awt.Image object, since getNative() + * is going to return the {@link Graphics2D} object. + */ + @Override + public Image getImage() { + return image; + } + + + /** Returns the java.awt.Graphics2D object used by this renderer. */ + @Override + public Object getNative() { + return g2; + } + + + ////////////////////////////////////////////////////////////// + + // FRAME + + +// @Override +// public boolean canDraw() { +// return true; +// } + + +// @Override +// public void requestDraw() { +//// EventQueue.invokeLater(new Runnable() { +//// public void run() { +// parent.handleDraw(); +//// } +//// }); +// } + + +// Graphics2D g2old; + + public Graphics2D checkImage() { + if (image == null || + ((BufferedImage) image).getWidth() != width*pixelDensity || + ((BufferedImage) image).getHeight() != height*pixelDensity) { +// ((VolatileImage) image).getWidth() != width || +// ((VolatileImage) image).getHeight() != height) { +// image = new BufferedImage(width * pixelFactor, height * pixelFactor +// format == RGB ? BufferedImage.TYPE_INT_ARGB); + + GraphicsConfiguration gc = null; + if (surface != null) { + Component comp = null; //surface.getComponent(); + if (comp == null) { +// System.out.println("component null, but parent.frame is " + parent.frame); + comp = parent.frame; + } + if (comp != null) { + gc = comp.getGraphicsConfiguration(); + } + } + // If not realized (off-screen, i.e the Color Selector Tool), gc will be null. + if (gc == null) { + //System.err.println("GraphicsConfiguration null in initImage()"); + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + gc = ge.getDefaultScreenDevice().getDefaultConfiguration(); + } + + // Formerly this was broken into separate versions based on offscreen or + // not, but we may as well create a compatible image; it won't hurt, right? + int wide = width * pixelDensity; + int high = height * pixelDensity; +// System.out.println("re-creating image"); + image = gc.createCompatibleImage(wide, high, Transparency.TRANSLUCENT); +// image = gc.createCompatibleVolatileImage(wide, high); + //image = surface.getComponent().createImage(width, height); + } + return (Graphics2D) image.getGraphics(); + } + + + @Override + public void beginDraw() { + g2 = checkImage(); + + // Calling getGraphics() seems to nuke several settings. + // It seems to be re-creating a new Graphics2D object each time. + // https://github.com/processing/processing/issues/3331 + if (strokeObject != null) { + g2.setStroke(strokeObject); + } + // https://github.com/processing/processing/issues/2617 + if (fontObject != null) { + g2.setFont(fontObject); + } + // https://github.com/processing/processing/issues/4019 + if (blendMode != 0) { + blendMode(blendMode); + } + handleSmooth(); + + /* + // NOTE: Calling image.getGraphics() will create a new Graphics context, + // even if it's for the same image that's already had a context created. + // Seems like a speed/memory issue, and also requires that all smoothing, + // stroke, font and other props be reset. Can't find a good answer about + // whether getGraphics() and dispose() on each frame is 1) better practice + // and 2) minimal overhead, however. Instinct suggests #1 may be true, + // but #2 seems a problem. + if (primarySurface && !useOffscreen) { + GraphicsConfiguration gc = canvas.getGraphicsConfiguration(); + if (false) { + if (image == null || ((VolatileImage) image).validate(gc) == VolatileImage.IMAGE_INCOMPATIBLE) { + image = gc.createCompatibleVolatileImage(width, height); + g2 = (Graphics2D) image.getGraphics(); + reapplySettings = true; + } + } else { + if (image == null) { + image = gc.createCompatibleImage(width, height); + PApplet.debug("created new image, type is " + image); + g2 = (Graphics2D) image.getGraphics(); + reapplySettings = true; + } + } + } + + if (useCanvas && primarySurface) { + if (parent.frameCount == 0) { + canvas.createBufferStrategy(2); + strategy = canvas.getBufferStrategy(); + PApplet.debug("PGraphicsJava2D.beginDraw() strategy is " + strategy); + BufferCapabilities caps = strategy.getCapabilities(); + caps = strategy.getCapabilities(); + PApplet.debug("PGraphicsJava2D.beginDraw() caps are " + + " flipping: " + caps.isPageFlipping() + + " front/back accel: " + caps.getFrontBufferCapabilities().isAccelerated() + " " + + "/" + caps.getBackBufferCapabilities().isAccelerated()); + } + GraphicsConfiguration gc = canvas.getGraphicsConfiguration(); + + if (bimage == null || + bimage.getWidth() != width || + bimage.getHeight() != height) { + PApplet.debug("PGraphicsJava2D creating new image"); + bimage = gc.createCompatibleImage(width, height); +// image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + g2 = bimage.createGraphics(); + defaultComposite = g2.getComposite(); + reapplySettings = true; + } + } + */ + + checkSettings(); + resetMatrix(); // reset model matrix + vertexCount = 0; + } + + + /** + * Smoothing for Java2D is 2 for bilinear, and 3 for bicubic (the default). + * Internally, smooth(1) is the default, smooth(0) is noSmooth(). + */ + protected void handleSmooth() { + if (smooth == 0) { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_OFF); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); + + } else { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + + if (smooth == 1 || smooth == 3) { // default is bicubic + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BICUBIC); + } else if (smooth == 2) { + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + } + + // http://docs.oracle.com/javase/tutorial/2d/text/renderinghints.html + // Oracle Java text anti-aliasing on OS X looks like s*t compared to the + // text rendering with Apple's old Java 6. Below, several attempts to fix: + g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + // Turns out this is the one that actually makes things work. + // Kerning is still screwed up, however. + g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, + RenderingHints.VALUE_FRACTIONALMETRICS_ON); +// g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, +// RenderingHints.VALUE_TEXT_ANTIALIAS_GASP); +// g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, +// RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); + +// g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, +// RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + } + } + + + @Override + public void endDraw() { + // hm, mark pixels as changed, because this will instantly do a full + // copy of all the pixels to the surface.. so that's kind of a mess. + //updatePixels(); + + if (primaryGraphics) { + /* + //if (canvas != null) { + if (useCanvas) { + //System.out.println(canvas); + + // alternate version + //canvas.repaint(); // ?? what to do for swapping buffers + +// System.out.println("endDraw() frameCount is " + parent.frameCount); +// if (parent.frameCount != 0) { + redraw(); +// } + + } else if (useOffscreen) { + // don't copy the pixels/data elements of the buffered image directly, + // since it'll disable the nice speedy pipeline stuff, sending all drawing + // into a world of suck that's rough 6 trillion times slower. + synchronized (image) { + //System.out.println("inside j2d sync"); + image.getGraphics().drawImage(offscreen, 0, 0, null); + } + + } else { + // changed to not dispose and get on each frame, + // otherwise a new Graphics context is used on each frame +// g2.dispose(); +// System.out.println("not doing anything special in endDraw()"); + } + */ + } else { + // TODO this is probably overkill for most tasks... + loadPixels(); + } + +// // Marking as modified, and then calling updatePixels() in +// // the super class, which just sets the mx1, my1, mx2, my2 +// // coordinates of the modified area. This avoids doing the +// // full copy of the pixels to the surface in this.updatePixels(). +// setModified(); +// super.updatePixels(); + + // Marks pixels as modified so that the pixels will be updated. + // Also sets mx1/y1/x2/y2 so that OpenGL will pick it up. + setModified(); + + g2.dispose(); + } + + + /* + private void redraw() { + // only need this check if the validate() call will use redraw() +// if (strategy == null) return; + do { + PApplet.debug("PGraphicsJava2D.redraw() top of outer do { } block"); + do { + PApplet.debug("PGraphicsJava2D.redraw() top of inner do { } block"); + PApplet.debug("strategy is " + strategy); + Graphics bsg = strategy.getDrawGraphics(); +// if (vimage != null) { +// bsg.drawImage(vimage, 0, 0, null); +// } else { + bsg.drawImage(bimage, 0, 0, null); +// if (parent.frameCount == 0) { +// try { +// ImageIO.write(image, "jpg", new java.io.File("/Users/fry/Desktop/buff.jpg")); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } +// } + bsg.dispose(); + + // the strategy version +// g2.dispose(); +// if (!strategy.contentsLost()) { +// if (parent.frameCount != 0) { +// Toolkit.getDefaultToolkit().sync(); +// } +// } else { +// System.out.println("XXXXX strategy contents lost"); +// } +// } +// } + } while (strategy.contentsRestored()); + + PApplet.debug("PGraphicsJava2D.redraw() showing strategy"); + strategy.show(); + + } while (strategy.contentsLost()); + PApplet.debug("PGraphicsJava2D.redraw() out of do { } block"); + } + */ + + + + ////////////////////////////////////////////////////////////// + + // SETTINGS + + + //protected void checkSettings() + + + @Override + protected void defaultSettings() { +// if (!useCanvas) { +// // Papered over another threading issue... +// // See if this comes back now that the other issue is fixed. +//// while (g2 == null) { +//// try { +//// System.out.println("sleeping until g2 is available"); +//// Thread.sleep(5); +//// } catch (InterruptedException e) { } +//// } + defaultComposite = g2.getComposite(); +// } + super.defaultSettings(); + } + + + //protected void reapplySettings() + + + + ////////////////////////////////////////////////////////////// + + // HINT + + + @Override + public void hint(int which) { + // take care of setting the hint + super.hint(which); + + // Avoid badness when drawing shorter strokes. + // http://code.google.com/p/processing/issues/detail?id=1068 + // Unfortunately cannot always be enabled, because it makes the + // stroke in many standard Processing examples really gross. + if (which == ENABLE_STROKE_PURE) { + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + RenderingHints.VALUE_STROKE_PURE); + } else if (which == DISABLE_STROKE_PURE) { + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + RenderingHints.VALUE_STROKE_DEFAULT); + } + } + + + + ////////////////////////////////////////////////////////////// + + // SHAPE CREATION + + + @Override + protected PShape createShapeFamily(int type) { + return new PShape(this, type); + } + + + @Override + protected PShape createShapePrimitive(int kind, float... p) { + return new PShape(this, kind, p); + } + + +// @Override +// public PShape createShape(PShape source) { +// return PShapeOpenGL.createShape2D(this, source); +// } + + + /* + protected PShape createShapeImpl(PGraphicsJava2D pg, int type) { + PShape shape = null; + if (type == PConstants.GROUP) { + shape = new PShape(pg, PConstants.GROUP); + } else if (type == PShape.PATH) { + shape = new PShape(pg, PShape.PATH); + } else if (type == PShape.GEOMETRY) { + shape = new PShape(pg, PShape.GEOMETRY); + } + // defaults to false, don't assign it and make complexity for overrides + //shape.set3D(false); + return shape; + } + */ + + + /* + static protected PShape createShapeImpl(PGraphicsJava2D pg, + int kind, float... p) { + PShape shape = null; + int len = p.length; + + if (kind == POINT) { + if (len != 2) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShape(pg, PShape.PRIMITIVE); + shape.setKind(POINT); + } else if (kind == LINE) { + if (len != 4) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShape(pg, PShape.PRIMITIVE); + shape.setKind(LINE); + } else if (kind == TRIANGLE) { + if (len != 6) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShape(pg, PShape.PRIMITIVE); + shape.setKind(TRIANGLE); + } else if (kind == QUAD) { + if (len != 8) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShape(pg, PShape.PRIMITIVE); + shape.setKind(QUAD); + } else if (kind == RECT) { + if (len != 4 && len != 5 && len != 8 && len != 9) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShape(pg, PShape.PRIMITIVE); + shape.setKind(RECT); + } else if (kind == ELLIPSE) { + if (len != 4 && len != 5) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShape(pg, PShape.PRIMITIVE); + shape.setKind(ELLIPSE); + } else if (kind == ARC) { + if (len != 6 && len != 7) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShape(pg, PShape.PRIMITIVE); + shape.setKind(ARC); + } else if (kind == BOX) { + showWarning("Primitive not supported in 2D"); + } else if (kind == SPHERE) { + showWarning("Primitive not supported in 2D"); + } else { + showWarning("Unrecognized primitive type"); + } + + if (shape != null) { + shape.setParams(p); + } + + // defaults to false, don't assign it and make complexity for overrides + //shape.set3D(false); + + return shape; + } + */ + + + + ////////////////////////////////////////////////////////////// + + // SHAPES + + + @Override + public void beginShape(int kind) { + //super.beginShape(kind); + shape = kind; + vertexCount = 0; + curveVertexCount = 0; + + // set gpath to null, because when mixing curves and straight + // lines, vertexCount will be set back to zero, so vertexCount == 1 + // is no longer a good indicator of whether the shape is new. + // this way, just check to see if gpath is null, and if it isn't + // then just use it to continue the shape. + gpath = null; + auxPath = null; + } + + + //public boolean edge(boolean e) + + + //public void normal(float nx, float ny, float nz) { + + + //public void textureMode(int mode) + + + @Override + public void texture(PImage image) { + showMethodWarning("texture"); + } + + + @Override + public void vertex(float x, float y) { + curveVertexCount = 0; + //float vertex[]; + + if (vertexCount == vertices.length) { + float temp[][] = new float[vertexCount<<1][VERTEX_FIELD_COUNT]; + System.arraycopy(vertices, 0, temp, 0, vertexCount); + vertices = temp; + //message(CHATTER, "allocating more vertices " + vertices.length); + } + // not everyone needs this, but just easier to store rather + // than adding another moving part to the code... + vertices[vertexCount][X] = x; + vertices[vertexCount][Y] = y; + vertexCount++; + + switch (shape) { + + case POINTS: + point(x, y); + break; + + case LINES: + if ((vertexCount % 2) == 0) { + line(vertices[vertexCount-2][X], + vertices[vertexCount-2][Y], x, y); + } + break; + + case TRIANGLES: + if ((vertexCount % 3) == 0) { + triangle(vertices[vertexCount - 3][X], + vertices[vertexCount - 3][Y], + vertices[vertexCount - 2][X], + vertices[vertexCount - 2][Y], + x, y); + } + break; + + case TRIANGLE_STRIP: + if (vertexCount >= 3) { + triangle(vertices[vertexCount - 2][X], + vertices[vertexCount - 2][Y], + vertices[vertexCount - 1][X], + vertices[vertexCount - 1][Y], + vertices[vertexCount - 3][X], + vertices[vertexCount - 3][Y]); + } + break; + + case TRIANGLE_FAN: + if (vertexCount >= 3) { + // This is an unfortunate implementation because the stroke for an + // adjacent triangle will be repeated. However, if the stroke is not + // redrawn, it will replace the adjacent line (when it lines up + // perfectly) or show a faint line (when off by a small amount). + // The alternative would be to wait, then draw the shape as a + // polygon fill, followed by a series of vertices. But that's a + // poor method when used with PDF, DXF, or other recording objects, + // since discrete triangles would likely be preferred. + triangle(vertices[0][X], + vertices[0][Y], + vertices[vertexCount - 2][X], + vertices[vertexCount - 2][Y], + x, y); + } + break; + + case QUAD: + case QUADS: + if ((vertexCount % 4) == 0) { + quad(vertices[vertexCount - 4][X], + vertices[vertexCount - 4][Y], + vertices[vertexCount - 3][X], + vertices[vertexCount - 3][Y], + vertices[vertexCount - 2][X], + vertices[vertexCount - 2][Y], + x, y); + } + break; + + case QUAD_STRIP: + // 0---2---4 + // | | | + // 1---3---5 + if ((vertexCount >= 4) && ((vertexCount % 2) == 0)) { + quad(vertices[vertexCount - 4][X], + vertices[vertexCount - 4][Y], + vertices[vertexCount - 2][X], + vertices[vertexCount - 2][Y], + x, y, + vertices[vertexCount - 3][X], + vertices[vertexCount - 3][Y]); + } + break; + + case POLYGON: + if (gpath == null) { + gpath = new GeneralPath(); + gpath.moveTo(x, y); + } else if (breakShape) { + gpath.moveTo(x, y); + breakShape = false; + } else { + gpath.lineTo(x, y); + } + break; + } + } + + + @Override + public void vertex(float x, float y, float z) { + showDepthWarningXYZ("vertex"); + } + + @Override + public void vertex(float[] v) { + vertex(v[X], v[Y]); + } + + + @Override + public void vertex(float x, float y, float u, float v) { + showVariationWarning("vertex(x, y, u, v)"); + } + + + @Override + public void vertex(float x, float y, float z, float u, float v) { + showDepthWarningXYZ("vertex"); + } + + + @Override + public void beginContour() { + if (openContour) { + PGraphics.showWarning("Already called beginContour()"); + return; + } + + // draw contours to auxiliary path so main path can be closed later + GeneralPath contourPath = auxPath; + auxPath = gpath; + gpath = contourPath; + + if (contourPath != null) { // first contour does not break + breakShape = true; + } + + openContour = true; + } + + + @Override + public void endContour() { + if (!openContour) { + PGraphics.showWarning("Need to call beginContour() first"); + return; + } + + // close this contour + if (gpath != null) gpath.closePath(); + + // switch back to main path + GeneralPath contourPath = gpath; + gpath = auxPath; + auxPath = contourPath; + + openContour = false; + } + + + @Override + public void endShape(int mode) { + if (openContour) { // correct automagically, notify user + endContour(); + PGraphics.showWarning("Missing endContour() before endShape()"); + } + if (gpath != null) { // make sure something has been drawn + if (shape == POLYGON) { + if (mode == CLOSE) { + gpath.closePath(); + } + if (auxPath != null) { + gpath.append(auxPath, false); + } + drawShape(gpath); + } + } + shape = 0; + } + + ////////////////////////////////////////////////////////////// + + // CLIPPING + + + @Override + protected void clipImpl(float x1, float y1, float x2, float y2) { + g2.setClip(new Rectangle2D.Float(x1, y1, x2 - x1, y2 - y1)); + } + + + @Override + public void noClip() { + g2.setClip(null); + } + + + + ////////////////////////////////////////////////////////////// + + // BLEND + + /** + * ( begin auto-generated from blendMode.xml ) + * + * This is a new reference entry for Processing 2.0. It will be updated shortly. + * + * ( end auto-generated ) + * + * @webref Rendering + * @param mode the blending mode to use + */ + @Override + protected void blendModeImpl() { + if (blendMode == BLEND) { + g2.setComposite(defaultComposite); + + } else { + g2.setComposite(new Composite() { + + @Override + public CompositeContext createContext(ColorModel srcColorModel, + ColorModel dstColorModel, + RenderingHints hints) { + return new BlendingContext(blendMode); + } + }); + } + } + + + // Blending implementation cribbed from portions of Romain Guy's + // demo and terrific writeup on blending modes in Java 2D. + // http://www.curious-creature.org/2006/09/20/new-blendings-modes-for-java2d/ + private static final class BlendingContext implements CompositeContext { + private int mode; + + private BlendingContext(int mode) { + this.mode = mode; + } + + public void dispose() { } + + public void compose(Raster src, Raster dstIn, WritableRaster dstOut) { + // not sure if this is really necessary, since we control our buffers + if (src.getSampleModel().getDataType() != DataBuffer.TYPE_INT || + dstIn.getSampleModel().getDataType() != DataBuffer.TYPE_INT || + dstOut.getSampleModel().getDataType() != DataBuffer.TYPE_INT) { + throw new IllegalStateException("Source and destination must store pixels as INT."); + } + + int width = Math.min(src.getWidth(), dstIn.getWidth()); + int height = Math.min(src.getHeight(), dstIn.getHeight()); + + int[] srcPixels = new int[width]; + int[] dstPixels = new int[width]; + + for (int y = 0; y < height; y++) { + src.getDataElements(0, y, width, 1, srcPixels); + dstIn.getDataElements(0, y, width, 1, dstPixels); + for (int x = 0; x < width; x++) { + dstPixels[x] = blendColor(dstPixels[x], srcPixels[x], mode); + } + dstOut.setDataElements(0, y, width, 1, dstPixels); + } + } + } + + + + ////////////////////////////////////////////////////////////// + + // BEZIER VERTICES + + + @Override + public void bezierVertex(float x1, float y1, + float x2, float y2, + float x3, float y3) { + bezierVertexCheck(); + gpath.curveTo(x1, y1, x2, y2, x3, y3); + } + + + @Override + public void bezierVertex(float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + showDepthWarningXYZ("bezierVertex"); + } + + + + ////////////////////////////////////////////////////////////// + + // QUADRATIC BEZIER VERTICES + + + @Override + public void quadraticVertex(float ctrlX, float ctrlY, + float endX, float endY) { + bezierVertexCheck(); + Point2D cur = gpath.getCurrentPoint(); + + float x1 = (float) cur.getX(); + float y1 = (float) cur.getY(); + + bezierVertex(x1 + ((ctrlX-x1)*2/3.0f), y1 + ((ctrlY-y1)*2/3.0f), + endX + ((ctrlX-endX)*2/3.0f), endY + ((ctrlY-endY)*2/3.0f), + endX, endY); + } + + + @Override + public void quadraticVertex(float x2, float y2, float z2, + float x4, float y4, float z4) { + showDepthWarningXYZ("quadVertex"); + } + + + + ////////////////////////////////////////////////////////////// + + // CURVE VERTICES + + + @Override + protected void curveVertexCheck() { + super.curveVertexCheck(); + + if (curveCoordX == null) { + curveCoordX = new float[4]; + curveCoordY = new float[4]; + curveDrawX = new float[4]; + curveDrawY = new float[4]; + } + } + + + @Override + protected void curveVertexSegment(float x1, float y1, + float x2, float y2, + float x3, float y3, + float x4, float y4) { + curveCoordX[0] = x1; + curveCoordY[0] = y1; + + curveCoordX[1] = x2; + curveCoordY[1] = y2; + + curveCoordX[2] = x3; + curveCoordY[2] = y3; + + curveCoordX[3] = x4; + curveCoordY[3] = y4; + + curveToBezierMatrix.mult(curveCoordX, curveDrawX); + curveToBezierMatrix.mult(curveCoordY, curveDrawY); + + // since the paths are continuous, + // only the first point needs the actual moveto + if (gpath == null) { + gpath = new GeneralPath(); + gpath.moveTo(curveDrawX[0], curveDrawY[0]); + } + + gpath.curveTo(curveDrawX[1], curveDrawY[1], + curveDrawX[2], curveDrawY[2], + curveDrawX[3], curveDrawY[3]); + } + + + @Override + public void curveVertex(float x, float y, float z) { + showDepthWarningXYZ("curveVertex"); + } + + + + ////////////////////////////////////////////////////////////// + + // RENDERER + + + //public void flush() + + + + ////////////////////////////////////////////////////////////// + + // POINT, LINE, TRIANGLE, QUAD + + + @Override + public void point(float x, float y) { + if (stroke) { +// if (strokeWeight > 1) { + line(x, y, x + EPSILON, y + EPSILON); +// } else { +// set((int) screenX(x, y), (int) screenY(x, y), strokeColor); +// } + } + } + + + @Override + public void line(float x1, float y1, float x2, float y2) { + line.setLine(x1, y1, x2, y2); + strokeShape(line); + } + + + @Override + public void triangle(float x1, float y1, float x2, float y2, + float x3, float y3) { + gpath = new GeneralPath(); + gpath.moveTo(x1, y1); + gpath.lineTo(x2, y2); + gpath.lineTo(x3, y3); + gpath.closePath(); + drawShape(gpath); + } + + + @Override + public void quad(float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4) { + GeneralPath gp = new GeneralPath(); + gp.moveTo(x1, y1); + gp.lineTo(x2, y2); + gp.lineTo(x3, y3); + gp.lineTo(x4, y4); + gp.closePath(); + drawShape(gp); + } + + + + ////////////////////////////////////////////////////////////// + + // RECT + + + //public void rectMode(int mode) + + + //public void rect(float a, float b, float c, float d) + + + @Override + protected void rectImpl(float x1, float y1, float x2, float y2) { + rect.setFrame(x1, y1, x2-x1, y2-y1); + drawShape(rect); + } + + + + ////////////////////////////////////////////////////////////// + + // ELLIPSE + + + //public void ellipseMode(int mode) + + + //public void ellipse(float a, float b, float c, float d) + + + @Override + protected void ellipseImpl(float x, float y, float w, float h) { + ellipse.setFrame(x, y, w, h); + drawShape(ellipse); + } + + + + ////////////////////////////////////////////////////////////// + + // ARC + + + //public void arc(float a, float b, float c, float d, + // float start, float stop) + + + @Override + protected void arcImpl(float x, float y, float w, float h, + float start, float stop, int mode) { + // 0 to 90 in java would be 0 to -90 for p5 renderer + // but that won't work, so -90 to 0? + + start = -start * RAD_TO_DEG; + stop = -stop * RAD_TO_DEG; + + // ok to do this because already checked for NaN +// while (start < 0) { +// start += 360; +// stop += 360; +// } +// if (start > stop) { +// float temp = start; +// start = stop; +// stop = temp; +// } + float sweep = stop - start; + + // The defaults, before 2.0b7, were to stroke as Arc2D.OPEN, and then fill + // using Arc2D.PIE. That's a little wonky, but it's here for compatability. + int fillMode = Arc2D.PIE; + int strokeMode = Arc2D.OPEN; + + if (mode == OPEN) { + fillMode = Arc2D.OPEN; + //strokeMode = Arc2D.OPEN; + + } else if (mode == PIE) { + //fillMode = Arc2D.PIE; + strokeMode = Arc2D.PIE; + + } else if (mode == CHORD) { + fillMode = Arc2D.CHORD; + strokeMode = Arc2D.CHORD; + } + + if (fill) { + //System.out.println("filla"); + arc.setArc(x, y, w, h, start, sweep, fillMode); + fillShape(arc); + } + if (stroke) { + //System.out.println("strokey"); + arc.setArc(x, y, w, h, start, sweep, strokeMode); + strokeShape(arc); + } + } + + + + ////////////////////////////////////////////////////////////// + + // JAVA2D SHAPE/PATH HANDLING + + + protected void fillShape(Shape s) { + if (fillGradient) { + g2.setPaint(fillGradientObject); + g2.fill(s); + } else if (fill) { + g2.setColor(fillColorObject); + g2.fill(s); + } + } + + + protected void strokeShape(Shape s) { + if (strokeGradient) { + g2.setPaint(strokeGradientObject); + g2.draw(s); + } else if (stroke) { + g2.setColor(strokeColorObject); + g2.draw(s); + } + } + + + protected void drawShape(Shape s) { + if (fillGradient) { + g2.setPaint(fillGradientObject); + g2.fill(s); + } else if (fill) { + g2.setColor(fillColorObject); + g2.fill(s); + } + if (strokeGradient) { + g2.setPaint(strokeGradientObject); + g2.draw(s); + } else if (stroke) { + g2.setColor(strokeColorObject); + g2.draw(s); + } + } + + + + ////////////////////////////////////////////////////////////// + + // BOX + + + //public void box(float size) + + + @Override + public void box(float w, float h, float d) { + showMethodWarning("box"); + } + + + + ////////////////////////////////////////////////////////////// + + // SPHERE + + + //public void sphereDetail(int res) + + + //public void sphereDetail(int ures, int vres) + + + @Override + public void sphere(float r) { + showMethodWarning("sphere"); + } + + + + ////////////////////////////////////////////////////////////// + + // BEZIER + + + //public float bezierPoint(float a, float b, float c, float d, float t) + + + //public float bezierTangent(float a, float b, float c, float d, float t) + + + //protected void bezierInitCheck() + + + //protected void bezierInit() + + + /** Ignored (not needed) in Java 2D. */ + @Override + public void bezierDetail(int detail) { + } + + + //public void bezier(float x1, float y1, + // float x2, float y2, + // float x3, float y3, + // float x4, float y4) + + + //public void bezier(float x1, float y1, float z1, + // float x2, float y2, float z2, + // float x3, float y3, float z3, + // float x4, float y4, float z4) + + + + ////////////////////////////////////////////////////////////// + + // CURVE + + + //public float curvePoint(float a, float b, float c, float d, float t) + + + //public float curveTangent(float a, float b, float c, float d, float t) + + + /** Ignored (not needed) in Java 2D. */ + @Override + public void curveDetail(int detail) { + } + + //public void curveTightness(float tightness) + + + //protected void curveInitCheck() + + + //protected void curveInit() + + + //public void curve(float x1, float y1, + // float x2, float y2, + // float x3, float y3, + // float x4, float y4) + + + //public void curve(float x1, float y1, float z1, + // float x2, float y2, float z2, + // float x3, float y3, float z3, + // float x4, float y4, float z4) + + + +// ////////////////////////////////////////////////////////////// +// +// // SMOOTH +// +// +// @Override +// public void smooth() { +// smooth = true; +// +// if (quality == 0) { +// quality = 4; // change back to bicubic +// } +// +// g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, +// RenderingHints.VALUE_ANTIALIAS_ON); +// +// g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, +// quality == 4 ? +// RenderingHints.VALUE_INTERPOLATION_BICUBIC : +// RenderingHints.VALUE_INTERPOLATION_BILINEAR); +// +// // http://docs.oracle.com/javase/tutorial/2d/text/renderinghints.html +// // Oracle Java text anti-aliasing on OS X looks like s*t compared to the +// // text rendering with Apple's old Java 6. Below, several attempts to fix: +// g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, +// RenderingHints.VALUE_TEXT_ANTIALIAS_ON); +// // Turns out this is the one that actually makes things work. +// // Kerning is still screwed up, however. +// g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, +// RenderingHints.VALUE_FRACTIONALMETRICS_ON); +//// g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, +//// RenderingHints.VALUE_TEXT_ANTIALIAS_GASP); +//// g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, +//// RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); +// +//// g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, +//// RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); +// +// } +// +// +// @Override +// public void smooth(int quality) { +// this.quality = quality; +// if (quality == 0) { +// noSmooth(); +// } else { +// smooth(); +// } +// } +// +// +// @Override +// public void noSmooth() { +// smooth = false; +// quality = 0; // https://github.com/processing/processing/issues/3113 +// g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, +// RenderingHints.VALUE_ANTIALIAS_OFF); +// g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, +// RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); +// g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, +// RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); +// } + + + + ////////////////////////////////////////////////////////////// + + // IMAGE + + + //public void imageMode(int mode) + + + //public void image(PImage image, float x, float y) + + + //public void image(PImage image, float x, float y, float c, float d) + + + //public void image(PImage image, + // float a, float b, float c, float d, + // int u1, int v1, int u2, int v2) + + + /** + * Handle renderer-specific image drawing. + */ + @Override + protected void imageImpl(PImage who, + float x1, float y1, float x2, float y2, + int u1, int v1, int u2, int v2) { + // Image not ready yet, or an error + if (who.width <= 0 || who.height <= 0) return; + + ImageCache cash = (ImageCache) getCache(who); + + // Nuke the cache if the image was resized + if (cash != null) { + if (who.pixelWidth != cash.image.getWidth() || + who.pixelHeight != cash.image.getHeight()) { + cash = null; + } + } + + if (cash == null) { + //System.out.println("making new image cache"); + cash = new ImageCache(); //who); + setCache(who, cash); + who.updatePixels(); // mark the whole thing for update + who.setModified(); + } + + // If image previously was tinted, or the color changed + // or the image was tinted, and tint is now disabled + if ((tint && !cash.tinted) || + (tint && (cash.tintedColor != tintColor)) || + (!tint && cash.tinted)) { + // For tint change, mark all pixels as needing update. + who.updatePixels(); + } + + if (who.isModified()) { + if (who.pixels == null) { + // This might be a PGraphics that hasn't been drawn to yet. + // Can't just bail because the cache has been created above. + // https://github.com/processing/processing/issues/2208 + who.pixels = new int[who.pixelWidth * who.pixelHeight]; + } + cash.update(who, tint, tintColor); + who.setModified(false); + } + + u1 *= who.pixelDensity; + v1 *= who.pixelDensity; + u2 *= who.pixelDensity; + v2 *= who.pixelDensity; + + g2.drawImage(((ImageCache) getCache(who)).image, + (int) x1, (int) y1, (int) x2, (int) y2, + u1, v1, u2, v2, null); + + // Every few years I think "nah, Java2D couldn't possibly be that f*king + // slow, why are we doing this by hand?" then comes the affirmation: +// Composite oldComp = null; +// if (false && tint) { +// oldComp = g2.getComposite(); +// int alpha = (tintColor >> 24) & 0xff; +// System.out.println("using alpha composite"); +// Composite alphaComp = +// AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha / 255f); +// g2.setComposite(alphaComp); +// } +// +// long t = System.currentTimeMillis(); +// g2.drawImage(who.getImage(), +// (int) x1, (int) y1, (int) x2, (int) y2, +// u1, v1, u2, v2, null); +// System.out.println(System.currentTimeMillis() - t); +// +// if (oldComp != null) { +// g2.setComposite(oldComp); +// } + } + + + static class ImageCache { + boolean tinted; + int tintedColor; + int[] tintedTemp; // one row of tinted pixels + BufferedImage image; +// BufferedImage compat; + +// public ImageCache(PImage source) { +//// this.source = source; +// // even if RGB, set the image type to ARGB, because the +// // image may have an alpha value for its tint(). +//// int type = BufferedImage.TYPE_INT_ARGB; +// //System.out.println("making new buffered image"); +//// image = new BufferedImage(source.width, source.height, type); +// } + + /** + * Update the pixels of the cache image. Already determined that the tint + * has changed, or the pixels have changed, so should just go through + * with the update without further checks. + */ + public void update(PImage source, boolean tint, int tintColor) { + //int bufferType = BufferedImage.TYPE_INT_ARGB; + int targetType = ARGB; + boolean opaque = (tintColor & 0xFF000000) == 0xFF000000; + if (source.format == RGB) { + if (!tint || (tint && opaque)) { + //bufferType = BufferedImage.TYPE_INT_RGB; + targetType = RGB; + } + } +// boolean wrongType = (image != null) && (image.getType() != bufferType); +// if ((image == null) || wrongType) { +// image = new BufferedImage(source.width, source.height, bufferType); +// } + // Must always use an ARGB image, otherwise will write zeros + // in the alpha channel when drawn to the screen. + // https://github.com/processing/processing/issues/2030 + if (image == null) { + image = new BufferedImage(source.pixelWidth, source.pixelHeight, + BufferedImage.TYPE_INT_ARGB); + } + + WritableRaster wr = image.getRaster(); + if (tint) { + if (tintedTemp == null || tintedTemp.length != source.pixelWidth) { + tintedTemp = new int[source.pixelWidth]; + } + int a2 = (tintColor >> 24) & 0xff; +// System.out.println("tint color is " + a2); +// System.out.println("source.pixels[0] alpha is " + (source.pixels[0] >>> 24)); + int r2 = (tintColor >> 16) & 0xff; + int g2 = (tintColor >> 8) & 0xff; + int b2 = (tintColor) & 0xff; + + //if (bufferType == BufferedImage.TYPE_INT_RGB) { + if (targetType == RGB) { + // The target image is opaque, meaning that the source image has no + // alpha (is not ARGB), and the tint has no alpha. + int index = 0; + for (int y = 0; y < source.pixelHeight; y++) { + for (int x = 0; x < source.pixelWidth; x++) { + int argb1 = source.pixels[index++]; + int r1 = (argb1 >> 16) & 0xff; + int g1 = (argb1 >> 8) & 0xff; + int b1 = (argb1) & 0xff; + + // Prior to 2.1, the alpha channel was commented out here, + // but can't remember why (just thought unnecessary b/c of RGB?) + // https://github.com/processing/processing/issues/2030 + tintedTemp[x] = 0xFF000000 | + (((r2 * r1) & 0xff00) << 8) | + ((g2 * g1) & 0xff00) | + (((b2 * b1) & 0xff00) >> 8); + } + wr.setDataElements(0, y, source.pixelWidth, 1, tintedTemp); + } + // could this be any slower? +// float[] scales = { tintR, tintG, tintB }; +// float[] offsets = new float[3]; +// RescaleOp op = new RescaleOp(scales, offsets, null); +// op.filter(image, image); + + //} else if (bufferType == BufferedImage.TYPE_INT_ARGB) { + } else if (targetType == ARGB) { + if (source.format == RGB && + (tintColor & 0xffffff) == 0xffffff) { + int hi = tintColor & 0xff000000; + int index = 0; + for (int y = 0; y < source.pixelHeight; y++) { + for (int x = 0; x < source.pixelWidth; x++) { + tintedTemp[x] = hi | (source.pixels[index++] & 0xFFFFFF); + } + wr.setDataElements(0, y, source.pixelWidth, 1, tintedTemp); + } + } else { + int index = 0; + for (int y = 0; y < source.pixelHeight; y++) { + if (source.format == RGB) { + int alpha = tintColor & 0xFF000000; + for (int x = 0; x < source.pixelWidth; x++) { + int argb1 = source.pixels[index++]; + int r1 = (argb1 >> 16) & 0xff; + int g1 = (argb1 >> 8) & 0xff; + int b1 = (argb1) & 0xff; + tintedTemp[x] = alpha | + (((r2 * r1) & 0xff00) << 8) | + ((g2 * g1) & 0xff00) | + (((b2 * b1) & 0xff00) >> 8); + } + } else if (source.format == ARGB) { + for (int x = 0; x < source.pixelWidth; x++) { + int argb1 = source.pixels[index++]; + int a1 = (argb1 >> 24) & 0xff; + int r1 = (argb1 >> 16) & 0xff; + int g1 = (argb1 >> 8) & 0xff; + int b1 = (argb1) & 0xff; + tintedTemp[x] = + (((a2 * a1) & 0xff00) << 16) | + (((r2 * r1) & 0xff00) << 8) | + ((g2 * g1) & 0xff00) | + (((b2 * b1) & 0xff00) >> 8); + } + } else if (source.format == ALPHA) { + int lower = tintColor & 0xFFFFFF; + for (int x = 0; x < source.pixelWidth; x++) { + int a1 = source.pixels[index++]; + tintedTemp[x] = + (((a2 * a1) & 0xff00) << 16) | lower; + } + } + wr.setDataElements(0, y, source.pixelWidth, 1, tintedTemp); + } + } + // Not sure why ARGB images take the scales in this order... +// float[] scales = { tintR, tintG, tintB, tintA }; +// float[] offsets = new float[4]; +// RescaleOp op = new RescaleOp(scales, offsets, null); +// op.filter(image, image); + } + } else { // !tint + if (targetType == RGB && (source.pixels[0] >> 24 == 0)) { + // If it's an RGB image and the high bits aren't set, need to set + // the high bits to opaque because we're drawing ARGB images. + source.filter(OPAQUE); + // Opting to just manipulate the image here, since it shouldn't + // affect anything else (and alpha(get(x, y)) should return 0xff). + // Wel also make no guarantees about the values of the pixels array + // in a PImage and how the high bits will be set. + } + // If no tint, just shove the pixels on in there verbatim + wr.setDataElements(0, 0, source.pixelWidth, source.pixelHeight, source.pixels); + } + this.tinted = tint; + this.tintedColor = tintColor; + +// GraphicsConfiguration gc = parent.getGraphicsConfiguration(); +// compat = gc.createCompatibleImage(image.getWidth(), +// image.getHeight(), +// Transparency.TRANSLUCENT); +// +// Graphics2D g = compat.createGraphics(); +// g.drawImage(image, 0, 0, null); +// g.dispose(); + } + } + + + + ////////////////////////////////////////////////////////////// + + // SHAPE + + + //public void shapeMode(int mode) + + + //public void shape(PShape shape) + + + //public void shape(PShape shape, float x, float y) + + + //public void shape(PShape shape, float x, float y, float c, float d) + + + ////////////////////////////////////////////////////////////// + + // SHAPE I/O + + + //public PShape loadShape(String filename) + + + @Override + public PShape loadShape(String filename, String options) { + String extension = PApplet.getExtension(filename); + if (extension.equals("svg") || extension.equals("svgz")) { + return new PShapeJava2D(parent.loadXML(filename)); + } + PGraphics.showWarning("Unsupported format: " + filename); + return null; + } + + + + ////////////////////////////////////////////////////////////// + + // TEXT ATTRIBTUES + + + //public void textAlign(int align) + + + //public void textAlign(int alignX, int alignY) + + + @Override + public float textAscent() { + if (textFont == null) { + defaultFontOrDeath("textAscent"); + } + + Font font = (Font) textFont.getNative(); + if (font != null) { + //return getFontMetrics(font).getAscent(); + return g2.getFontMetrics(font).getAscent(); + } + return super.textAscent(); + } + + + @Override + public float textDescent() { + if (textFont == null) { + defaultFontOrDeath("textDescent"); + } + Font font = (Font) textFont.getNative(); + if (font != null) { + //return getFontMetrics(font).getDescent(); + return g2.getFontMetrics(font).getDescent(); + } + return super.textDescent(); + } + + + //public void textFont(PFont which) + + + //public void textFont(PFont which, float size) + + + //public void textLeading(float leading) + + + //public void textMode(int mode) + + + @Override + protected boolean textModeCheck(int mode) { + return mode == MODEL; + } + + + /** + * Same as parent, but override for native version of the font. + *

+ * Called from textFontImpl and textSizeImpl, so the metrics + * will get recorded properly. + */ + @Override + protected void handleTextSize(float size) { + // if a native version available, derive this font + Font font = (Font) textFont.getNative(); + // don't derive again if the font size has not changed + if (font != null) { + if (font.getSize2D() != size) { + Map map = + new HashMap<>(); + map.put(TextAttribute.SIZE, size); + map.put(TextAttribute.KERNING, + TextAttribute.KERNING_ON); +// map.put(TextAttribute.TRACKING, +// TextAttribute.TRACKING_TIGHT); + font = font.deriveFont(map); + } + g2.setFont(font); + textFont.setNative(font); + fontObject = font; + + /* + Map attrs = font.getAttributes(); + for (TextAttribute ta : attrs.keySet()) { + System.out.println(ta + " -> " + attrs.get(ta)); + } + */ + } + + // take care of setting the textSize and textLeading vars + // this has to happen second, because it calls textAscent() + // (which requires the native font metrics to be set) + super.handleTextSize(size); + } + + + //public float textWidth(char c) + + + //public float textWidth(String str) + + + @Override + protected float textWidthImpl(char buffer[], int start, int stop) { + if (textFont == null) { + defaultFontOrDeath("textWidth"); + } + // Avoid "Zero length string passed to TextLayout constructor" error + if (start == stop) { + return 0; + } + + Font font = (Font) textFont.getNative(); +// System.out.println(font); + //if (font != null && (textFont.isStream() || hints[ENABLE_NATIVE_FONTS])) { + if (font != null) { +// System.out.println("using charswidth for " + new String(buffer, start, stop-start)); + // maybe should use one of the newer/fancier functions for this? +// int length = stop - start; +// FontMetrics metrics = getFontMetrics(font); + FontMetrics metrics = g2.getFontMetrics(font); + // Using fractional metrics makes the measurement worse, not better, + // at least on OS X 10.6 (November, 2010). + // TextLayout returns the same value as charsWidth(). +// System.err.println("using native"); +// g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, +// RenderingHints.VALUE_FRACTIONALMETRICS_ON); +// float m1 = metrics.charsWidth(buffer, start, length); +// float m2 = (float) metrics.getStringBounds(buffer, start, stop, g2).getWidth(); +// TextLayout tl = new TextLayout(new String(buffer, start, length), font, g2.getFontRenderContext()); +// float m3 = (float) tl.getBounds().getWidth(); +// System.err.println(m1 + " " + m2 + " " + m3); +////// return m1; +//// return m2; +//// return metrics.charsWidth(buffer, start, length); +// return m2; + return (float) + metrics.getStringBounds(buffer, start, stop, g2).getWidth(); + } +// System.err.println("not native"); + return super.textWidthImpl(buffer, start, stop); + } + + +// protected void beginTextScreenMode() { +// loadPixels(); +// } + + +// protected void endTextScreenMode() { +// updatePixels(); +// } + + + ////////////////////////////////////////////////////////////// + + // TEXT + + // None of the variations of text() are overridden from PGraphics. + + + + ////////////////////////////////////////////////////////////// + + // TEXT IMPL + + + //protected void textLineAlignImpl(char buffer[], int start, int stop, + // float x, float y) + + + @Override + protected void textLineImpl(char buffer[], int start, int stop, + float x, float y) { + Font font = (Font) textFont.getNative(); +// if (font != null && (textFont.isStream() || hints[ENABLE_NATIVE_FONTS])) { + if (font != null) { + /* + // save the current setting for text smoothing. note that this is + // different from the smooth() function, because the font smoothing + // is controlled when the font is created, not now as it's drawn. + // fixed a bug in 0116 that handled this incorrectly. + Object textAntialias = + g2.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING); + + // override the current text smoothing setting based on the font + // (don't change the global smoothing settings) + g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + textFont.smooth ? + RenderingHints.VALUE_ANTIALIAS_ON : + RenderingHints.VALUE_ANTIALIAS_OFF); + */ + Object antialias = + g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING); + if (antialias == null) { + // if smooth() and noSmooth() not called, this will be null (0120) + antialias = RenderingHints.VALUE_ANTIALIAS_DEFAULT; + } + + // override the current smoothing setting based on the font + // also changes global setting for antialiasing, but this is because it's + // not possible to enable/disable them independently in some situations. + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + textFont.isSmooth() ? + RenderingHints.VALUE_ANTIALIAS_ON : + RenderingHints.VALUE_ANTIALIAS_OFF); + + g2.setColor(fillColorObject); + + int length = stop - start; + if (length != 0) { + g2.drawChars(buffer, start, length, (int) (x + 0.5f), (int) (y + 0.5f)); + // better to use round here? also, drawChars now just calls drawString +// g2.drawString(new String(buffer, start, stop - start), Math.round(x), Math.round(y)); + + // better to use drawString() with floats? (nope, draws the same) + //g2.drawString(new String(buffer, start, length), x, y); + + // this didn't seem to help the scaling issue, and creates garbage + // because of a fairly heavyweight new temporary object +// java.awt.font.GlyphVector gv = +// font.createGlyphVector(g2.getFontRenderContext(), new String(buffer, start, stop - start)); +// g2.drawGlyphVector(gv, x, y); + } + + // return to previous smoothing state if it was changed + //g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, textAntialias); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialias); + + } else { // otherwise just do the default + super.textLineImpl(buffer, start, stop, x, y); + } + } + + +// /** +// * Convenience method to get a legit FontMetrics object. Where possible, +// * override this any renderer subclass so that you're not using what's +// * returned by getDefaultToolkit() to get your metrics. +// */ +// @SuppressWarnings("deprecation") +// public FontMetrics getFontMetrics(Font font) { // ignore +// Frame frame = parent.frame; +// if (frame != null) { +// return frame.getToolkit().getFontMetrics(font); +// } +// return Toolkit.getDefaultToolkit().getFontMetrics(font); +// } +// +// +// /** +// * Convenience method to jump through some Java2D hoops and get an FRC. +// */ +// public FontRenderContext getFontRenderContext(Font font) { // ignore +// return getFontMetrics(font).getFontRenderContext(); +// } + + /* + Toolkit toolkit; + + @SuppressWarnings("deprecation") + protected FontMetrics getFontMetrics(Font font) { + if (toolkit == null) { + try { + Canvas canvas = (Canvas) surface.getNative(); + toolkit = canvas.getToolkit(); + } catch (Exception e) { + // May error out if it's a PSurfaceNone or similar + toolkit = Toolkit.getDefaultToolkit(); + } + } + return toolkit.getFontMetrics(font); + //return (g2 != null) ? g2.getFontMetrics(font) : super.getFontMetrics(font); + } + */ + + + ////////////////////////////////////////////////////////////// + + // MATRIX STACK + + + @Override + public void pushMatrix() { + if (transformCount == transformStack.length) { + throw new RuntimeException("pushMatrix() cannot use push more than " + + transformStack.length + " times"); + } + transformStack[transformCount] = g2.getTransform(); + transformCount++; + } + + + @Override + public void popMatrix() { + if (transformCount == 0) { + throw new RuntimeException("missing a pushMatrix() " + + "to go with that popMatrix()"); + } + transformCount--; + g2.setTransform(transformStack[transformCount]); + } + + + + ////////////////////////////////////////////////////////////// + + // MATRIX TRANSFORMS + + + @Override + public void translate(float tx, float ty) { + g2.translate(tx, ty); + } + + + //public void translate(float tx, float ty, float tz) + + + @Override + public void rotate(float angle) { + g2.rotate(angle); + } + + + @Override + public void rotateX(float angle) { + showDepthWarning("rotateX"); + } + + + @Override + public void rotateY(float angle) { + showDepthWarning("rotateY"); + } + + + @Override + public void rotateZ(float angle) { + showDepthWarning("rotateZ"); + } + + + @Override + public void rotate(float angle, float vx, float vy, float vz) { + showVariationWarning("rotate"); + } + + + @Override + public void scale(float s) { + g2.scale(s, s); + } + + + @Override + public void scale(float sx, float sy) { + g2.scale(sx, sy); + } + + + @Override + public void scale(float sx, float sy, float sz) { + showDepthWarningXYZ("scale"); + } + + + @Override + public void shearX(float angle) { + g2.shear(Math.tan(angle), 0); + } + + + @Override + public void shearY(float angle) { + g2.shear(0, Math.tan(angle)); + } + + + + ////////////////////////////////////////////////////////////// + + // MATRIX MORE + + + @Override + public void resetMatrix() { + g2.setTransform(new AffineTransform()); + g2.scale(pixelDensity, pixelDensity); + } + + + //public void applyMatrix(PMatrix2D source) + + + @Override + public void applyMatrix(float n00, float n01, float n02, + float n10, float n11, float n12) { + //System.out.println("PGraphicsJava2D.applyMatrix()"); + //System.out.println(new AffineTransform(n00, n10, n01, n11, n02, n12)); + g2.transform(new AffineTransform(n00, n10, n01, n11, n02, n12)); + //g2.transform(new AffineTransform(n00, n01, n02, n10, n11, n12)); + } + + + //public void applyMatrix(PMatrix3D source) + + + @Override + public void applyMatrix(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + showVariationWarning("applyMatrix"); + } + + + + ////////////////////////////////////////////////////////////// + + // MATRIX GET/SET + + + @Override + public PMatrix getMatrix() { + return getMatrix((PMatrix2D) null); + } + + + @Override + public PMatrix2D getMatrix(PMatrix2D target) { + if (target == null) { + target = new PMatrix2D(); + } + g2.getTransform().getMatrix(transform); + target.set((float) transform[0], (float) transform[2], (float) transform[4], + (float) transform[1], (float) transform[3], (float) transform[5]); + return target; + } + + + @Override + public PMatrix3D getMatrix(PMatrix3D target) { + showVariationWarning("getMatrix"); + return target; + } + + + //public void setMatrix(PMatrix source) + + + @Override + public void setMatrix(PMatrix2D source) { + g2.setTransform(new AffineTransform(source.m00, source.m10, + source.m01, source.m11, + source.m02, source.m12)); + } + + + @Override + public void setMatrix(PMatrix3D source) { + showVariationWarning("setMatrix"); + } + + + @Override + public void printMatrix() { + getMatrix((PMatrix2D) null).print(); + } + + + + ////////////////////////////////////////////////////////////// + + // CAMERA and PROJECTION + + // Inherit the plaintive warnings from PGraphics + + + //public void beginCamera() + //public void endCamera() + //public void camera() + //public void camera(float eyeX, float eyeY, float eyeZ, + // float centerX, float centerY, float centerZ, + // float upX, float upY, float upZ) + //public void printCamera() + + //public void ortho() + //public void ortho(float left, float right, + // float bottom, float top, + // float near, float far) + //public void perspective() + //public void perspective(float fov, float aspect, float near, float far) + //public void frustum(float left, float right, + // float bottom, float top, + // float near, float far) + //public void printProjection() + + + + ////////////////////////////////////////////////////////////// + + // SCREEN and MODEL transforms + + + @Override + public float screenX(float x, float y) { + g2.getTransform().getMatrix(transform); + return (float)transform[0]*x + (float)transform[2]*y + (float)transform[4]; + } + + + @Override + public float screenY(float x, float y) { + g2.getTransform().getMatrix(transform); + return (float)transform[1]*x + (float)transform[3]*y + (float)transform[5]; + } + + + @Override + public float screenX(float x, float y, float z) { + showDepthWarningXYZ("screenX"); + return 0; + } + + + @Override + public float screenY(float x, float y, float z) { + showDepthWarningXYZ("screenY"); + return 0; + } + + + @Override + public float screenZ(float x, float y, float z) { + showDepthWarningXYZ("screenZ"); + return 0; + } + + + //public float modelX(float x, float y, float z) + + + //public float modelY(float x, float y, float z) + + + //public float modelZ(float x, float y, float z) + + + + ////////////////////////////////////////////////////////////// + + // STYLE + + // pushStyle(), popStyle(), style() and getStyle() inherited. + + + + ////////////////////////////////////////////////////////////// + + // STROKE CAP/JOIN/WEIGHT + + + @Override + public void strokeCap(int cap) { + super.strokeCap(cap); + strokeImpl(); + } + + + @Override + public void strokeJoin(int join) { + super.strokeJoin(join); + strokeImpl(); + } + + + @Override + public void strokeWeight(float weight) { + super.strokeWeight(weight); + strokeImpl(); + } + + + protected void strokeImpl() { + int cap = BasicStroke.CAP_BUTT; + if (strokeCap == ROUND) { + cap = BasicStroke.CAP_ROUND; + } else if (strokeCap == PROJECT) { + cap = BasicStroke.CAP_SQUARE; + } + + int join = BasicStroke.JOIN_BEVEL; + if (strokeJoin == MITER) { + join = BasicStroke.JOIN_MITER; + } else if (strokeJoin == ROUND) { + join = BasicStroke.JOIN_ROUND; + } + + strokeObject = new BasicStroke(strokeWeight, cap, join); + g2.setStroke(strokeObject); + } + + + + ////////////////////////////////////////////////////////////// + + // STROKE + + // noStroke() and stroke() inherited from PGraphics. + + + @Override + protected void strokeFromCalc() { + super.strokeFromCalc(); + strokeColorObject = new Color(strokeColor, true); + strokeGradient = false; + } + + + + ////////////////////////////////////////////////////////////// + + // TINT + + // noTint() and tint() inherited from PGraphics. + + + @Override + protected void tintFromCalc() { + super.tintFromCalc(); + // TODO actually implement tinted images + tintColorObject = new Color(tintColor, true); + } + + + + ////////////////////////////////////////////////////////////// + + // FILL + + // noFill() and fill() inherited from PGraphics. + + + @Override + protected void fillFromCalc() { + super.fillFromCalc(); + fillColorObject = new Color(fillColor, true); + fillGradient = false; + } + + + + ////////////////////////////////////////////////////////////// + + // MATERIAL PROPERTIES + + + //public void ambient(int rgb) + //public void ambient(float gray) + //public void ambient(float x, float y, float z) + //protected void ambientFromCalc() + //public void specular(int rgb) + //public void specular(float gray) + //public void specular(float x, float y, float z) + //protected void specularFromCalc() + //public void shininess(float shine) + //public void emissive(int rgb) + //public void emissive(float gray) + //public void emissive(float x, float y, float z ) + //protected void emissiveFromCalc() + + + + ////////////////////////////////////////////////////////////// + + // LIGHTS + + + //public void lights() + //public void noLights() + //public void ambientLight(float red, float green, float blue) + //public void ambientLight(float red, float green, float blue, + // float x, float y, float z) + //public void directionalLight(float red, float green, float blue, + // float nx, float ny, float nz) + //public void pointLight(float red, float green, float blue, + // float x, float y, float z) + //public void spotLight(float red, float green, float blue, + // float x, float y, float z, + // float nx, float ny, float nz, + // float angle, float concentration) + //public void lightFalloff(float constant, float linear, float quadratic) + //public void lightSpecular(float x, float y, float z) + //protected void lightPosition(int num, float x, float y, float z) + //protected void lightDirection(int num, float x, float y, float z) + + + + ////////////////////////////////////////////////////////////// + + // BACKGROUND + + + int[] clearPixels; + + protected void clearPixels(int color) { + // On a hi-res display, image may be larger than width/height + int imageWidth = image.getWidth(null); + int imageHeight = image.getHeight(null); + + // Create a small array that can be used to set the pixels several times. + // Using a single-pixel line of length 'width' is a tradeoff between + // speed (setting each pixel individually is too slow) and memory + // (an array for width*height would waste lots of memory if it stayed + // resident, and would terrify the gc if it were re-created on each trip + // to background(). +// WritableRaster raster = ((BufferedImage) image).getRaster(); +// WritableRaster raster = image.getRaster(); + WritableRaster raster = getRaster(); + if ((clearPixels == null) || (clearPixels.length < imageWidth)) { + clearPixels = new int[imageWidth]; + } + Arrays.fill(clearPixels, 0, imageWidth, backgroundColor); + for (int i = 0; i < imageHeight; i++) { + raster.setDataElements(0, i, imageWidth, 1, clearPixels); + } + } + + // background() methods inherited from PGraphics, along with the + // PImage version of backgroundImpl(), since it just calls set(). + + + //public void backgroundImpl(PImage image) + + + @Override + public void backgroundImpl() { + if (backgroundAlpha) { + clearPixels(backgroundColor); + + } else { + Color bgColor = new Color(backgroundColor); + // seems to fire an additional event that causes flickering, + // like an extra background erase on OS X +// if (canvas != null) { +// canvas.setBackground(bgColor); +// } + //new Exception().printStackTrace(System.out); + // in case people do transformations before background(), + // need to handle this with a push/reset/pop + Composite oldComposite = g2.getComposite(); + g2.setComposite(defaultComposite); + + pushMatrix(); + resetMatrix(); + g2.setColor(bgColor); //, backgroundAlpha)); +// g2.fillRect(0, 0, width, height); + // On a hi-res display, image may be larger than width/height + if (image != null) { + // image will be null in subclasses (i.e. PDF) + g2.fillRect(0, 0, image.getWidth(null), image.getHeight(null)); + } else { + // hope for the best if image is null + g2.fillRect(0, 0, width, height); + } + popMatrix(); + + g2.setComposite(oldComposite); + } + } + + + + ////////////////////////////////////////////////////////////// + + // COLOR MODE + + // All colorMode() variations are inherited from PGraphics. + + + + ////////////////////////////////////////////////////////////// + + // COLOR CALC + + // colorCalc() and colorCalcARGB() inherited from PGraphics. + + + + ////////////////////////////////////////////////////////////// + + // COLOR DATATYPE STUFFING + + // final color() variations inherited. + + + + ////////////////////////////////////////////////////////////// + + // COLOR DATATYPE EXTRACTION + + // final methods alpha, red, green, blue, + // hue, saturation, and brightness all inherited. + + + + ////////////////////////////////////////////////////////////// + + // COLOR DATATYPE INTERPOLATION + + // both lerpColor variants inherited. + + + + ////////////////////////////////////////////////////////////// + + // BEGIN/END RAW + + + @Override + public void beginRaw(PGraphics recorderRaw) { + showMethodWarning("beginRaw"); + } + + + @Override + public void endRaw() { + showMethodWarning("endRaw"); + } + + + + ////////////////////////////////////////////////////////////// + + // WARNINGS and EXCEPTIONS + + // showWarning and showException inherited. + + + + ////////////////////////////////////////////////////////////// + + // RENDERER SUPPORT QUERIES + + + //public boolean displayable() // true + + + //public boolean is2D() // true + + + //public boolean is3D() // false + + + + ////////////////////////////////////////////////////////////// + + // PIMAGE METHODS + + + // getImage, setCache, getCache, removeCache, isModified, setModified + + + protected WritableRaster getRaster() { + WritableRaster raster = null; + if (primaryGraphics) { + /* + // 'offscreen' will probably be removed in the next release + if (useOffscreen) { + raster = offscreen.getRaster(); + } else*/ if (image instanceof VolatileImage) { + // when possible, we'll try VolatileImage + raster = ((VolatileImage) image).getSnapshot().getRaster(); + } + } + if (raster == null) { + raster = ((BufferedImage) image).getRaster(); + } + + // On Raspberry Pi (and perhaps other platforms, the color buffer won't + // necessarily be the int array that we'd like. Need to convert it here. + // Not that this would probably mean getRaster() would need to work more + // like loadRaster/updateRaster because the pixels will need to be + // temporarily moved to (and later from) a buffer that's understood by + // the rest of the Processing source. + // https://github.com/processing/processing/issues/2010 + if (raster.getTransferType() != DataBuffer.TYPE_INT) { + System.err.println("See https://github.com/processing/processing/issues/2010"); + throw new RuntimeException("Pixel operations are not supported on this device."); + } + return raster; + } + + + @Override + public void loadPixels() { + if (pixels == null || (pixels.length != pixelWidth*pixelHeight)) { + pixels = new int[pixelWidth * pixelHeight]; + } + + WritableRaster raster = getRaster(); + raster.getDataElements(0, 0, pixelWidth, pixelHeight, pixels); + if (raster.getNumBands() == 3) { + // Java won't set the high bits when RGB, returns 0 for alpha + // https://github.com/processing/processing/issues/2030 + for (int i = 0; i < pixels.length; i++) { + pixels[i] = 0xff000000 | pixels[i]; + } + } + //((BufferedImage) image).getRGB(0, 0, width, height, pixels, 0, width); +// WritableRaster raster = ((BufferedImage) (useOffscreen && primarySurface ? offscreen : image)).getRaster(); +// WritableRaster raster = image.getRaster(); + } + + +// /** +// * Update the pixels[] buffer to the PGraphics image. +// *

+// * Unlike in PImage, where updatePixels() only requests that the +// * update happens, in PGraphicsJava2D, this will happen immediately. +// */ +// @Override +// public void updatePixels() { +// //updatePixels(0, 0, width, height); +//// WritableRaster raster = ((BufferedImage) (useOffscreen && primarySurface ? offscreen : image)).getRaster(); +//// WritableRaster raster = image.getRaster(); +// updatePixels(0, 0, width, height); +// } + + + /** + * Update the pixels[] buffer to the PGraphics image. + *

+ * Unlike in PImage, where updatePixels() only requests that the + * update happens, in PGraphicsJava2D, this will happen immediately. + */ + @Override + public void updatePixels(int x, int y, int c, int d) { + //if ((x == 0) && (y == 0) && (c == width) && (d == height)) { +// System.err.format("%d %d %d %d .. w/h = %d %d .. pw/ph = %d %d %n", x, y, c, d, width, height, pixelWidth, pixelHeight); + if ((x != 0) || (y != 0) || (c != pixelWidth) || (d != pixelHeight)) { + // Show a warning message, but continue anyway. + showVariationWarning("updatePixels(x, y, w, h)"); +// new Exception().printStackTrace(System.out); + } +// updatePixels(); + if (pixels != null) { + getRaster().setDataElements(0, 0, pixelWidth, pixelHeight, pixels); + } + modified = true; + } + + +// @Override +// protected void updatePixelsImpl(int x, int y, int w, int h) { +// super.updatePixelsImpl(x, y, w, h); +// +// if ((x != 0) || (y != 0) || (w != width) || (h != height)) { +// // Show a warning message, but continue anyway. +// showVariationWarning("updatePixels(x, y, w, h)"); +// } +// getRaster().setDataElements(0, 0, width, height, pixels); +// } + + + + ////////////////////////////////////////////////////////////// + + // GET/SET + + + static int getset[] = new int[1]; + + + @Override + public int get(int x, int y) { + if ((x < 0) || (y < 0) || (x >= width) || (y >= height)) return 0; + //return ((BufferedImage) image).getRGB(x, y); +// WritableRaster raster = ((BufferedImage) (useOffscreen && primarySurface ? offscreen : image)).getRaster(); + WritableRaster raster = getRaster(); + raster.getDataElements(x, y, getset); + if (raster.getNumBands() == 3) { + // https://github.com/processing/processing/issues/2030 + return getset[0] | 0xff000000; + } + return getset[0]; + } + + + //public PImage get(int x, int y, int w, int h) + + + @Override + protected void getImpl(int sourceX, int sourceY, + int sourceWidth, int sourceHeight, + PImage target, int targetX, int targetY) { + // last parameter to getRGB() is the scan size of the *target* buffer + //((BufferedImage) image).getRGB(x, y, w, h, output.pixels, 0, w); +// WritableRaster raster = +// ((BufferedImage) (useOffscreen && primarySurface ? offscreen : image)).getRaster(); + WritableRaster raster = getRaster(); + + if (sourceWidth == target.pixelWidth && sourceHeight == target.pixelHeight) { + raster.getDataElements(sourceX, sourceY, sourceWidth, sourceHeight, target.pixels); + // https://github.com/processing/processing/issues/2030 + if (raster.getNumBands() == 3) { + target.filter(OPAQUE); + } + + } else { + // TODO optimize, incredibly inefficient to reallocate this much memory + int[] temp = new int[sourceWidth * sourceHeight]; + raster.getDataElements(sourceX, sourceY, sourceWidth, sourceHeight, temp); + + // Copy the temporary output pixels over to the outgoing image + int sourceOffset = 0; + int targetOffset = targetY*target.pixelWidth + targetX; + for (int y = 0; y < sourceHeight; y++) { + if (raster.getNumBands() == 3) { + for (int i = 0; i < sourceWidth; i++) { + // Need to set the high bits for this feller + // https://github.com/processing/processing/issues/2030 + target.pixels[targetOffset + i] = 0xFF000000 | temp[sourceOffset + i]; + } + } else { + System.arraycopy(temp, sourceOffset, target.pixels, targetOffset, sourceWidth); + } + sourceOffset += sourceWidth; + targetOffset += target.pixelWidth; + } + } + } + + + @Override + public void set(int x, int y, int argb) { + if ((x < 0) || (y < 0) || (x >= pixelWidth) || (y >= pixelHeight)) return; +// ((BufferedImage) image).setRGB(x, y, argb); + getset[0] = argb; +// WritableRaster raster = ((BufferedImage) (useOffscreen && primarySurface ? offscreen : image)).getRaster(); +// WritableRaster raster = image.getRaster(); + getRaster().setDataElements(x, y, getset); + } + + + //public void set(int x, int y, PImage img) + + + @Override + protected void setImpl(PImage sourceImage, + int sourceX, int sourceY, + int sourceWidth, int sourceHeight, + int targetX, int targetY) { + WritableRaster raster = getRaster(); +// ((BufferedImage) (useOffscreen && primarySurface ? offscreen : image)).getRaster(); + + if ((sourceX == 0) && (sourceY == 0) && + (sourceWidth == sourceImage.pixelWidth) && + (sourceHeight == sourceImage.pixelHeight)) { +// System.out.format("%d %d %dx%d %d%n", targetX, targetY, +// sourceImage.width, sourceImage.height, +// sourceImage.pixels.length); + raster.setDataElements(targetX, targetY, + sourceImage.pixelWidth, sourceImage.pixelHeight, + sourceImage.pixels); + } else { + // TODO optimize, incredibly inefficient to reallocate this much memory + PImage temp = sourceImage.get(sourceX, sourceY, sourceWidth, sourceHeight); + raster.setDataElements(targetX, targetY, temp.pixelWidth, temp.pixelHeight, temp.pixels); + } + } + + + + ////////////////////////////////////////////////////////////// + + // MASK + + + static final String MASK_WARNING = + "mask() cannot be used on the main drawing surface"; + + + @Override + public void mask(int[] alpha) { + if (primaryGraphics) { + showWarning(MASK_WARNING); + + } else { + super.mask(alpha); + } + } + + + @Override + public void mask(PImage alpha) { + if (primaryGraphics) { + showWarning(MASK_WARNING); + + } else { + super.mask(alpha); + } + } + + + + ////////////////////////////////////////////////////////////// + + // FILTER + + // Because the PImage versions call loadPixels() and + // updatePixels(), no need to override anything here. + + + //public void filter(int kind) + + + //public void filter(int kind, float param) + + + + ////////////////////////////////////////////////////////////// + + // COPY + + + @Override + public void copy(int sx, int sy, int sw, int sh, + int dx, int dy, int dw, int dh) { + if ((sw != dw) || (sh != dh)) { + g2.drawImage(image, dx, dy, dx + dw, dy + dh, sx, sy, sx + sw, sy + sh, null); + + } else { + dx = dx - sx; // java2d's "dx" is the delta, not dest + dy = dy - sy; + g2.copyArea(sx, sy, sw, sh, dx, dy); + } + } + + + @Override + public void copy(PImage src, + int sx, int sy, int sw, int sh, + int dx, int dy, int dw, int dh) { + g2.drawImage((Image) src.getNative(), + dx, dy, dx + dw, dy + dh, + sx, sy, sx + sw, sy + sh, null); + } + + + + ////////////////////////////////////////////////////////////// + + // BLEND + + +// static public int blendColor(int c1, int c2, int mode) + + +// public void blend(int sx, int sy, int sw, int sh, +// int dx, int dy, int dw, int dh, int mode) + + +// public void blend(PImage src, +// int sx, int sy, int sw, int sh, +// int dx, int dy, int dw, int dh, int mode) + + + + ////////////////////////////////////////////////////////////// + + // SAVE + + +// public void save(String filename) { +// loadPixels(); +// super.save(filename); +// } +} diff --git a/src/main/java/processing/awt/PShapeJava2D.java b/src/main/java/processing/awt/PShapeJava2D.java new file mode 100644 index 0000000..2b9b968 --- /dev/null +++ b/src/main/java/processing/awt/PShapeJava2D.java @@ -0,0 +1,377 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2015 The Processing Foundation + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License version 2.1 as published by the Free Software Foundation. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.awt; + +import java.awt.Paint; +import java.awt.PaintContext; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.ColorModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +import processing.core.PApplet; +import processing.core.PGraphics; +import processing.core.PShapeSVG; +import processing.data.*; + + +/** + * Implements features for PShape that are specific to AWT and Java2D. + * At the moment, this is gradients and java.awt.Paint handling. + */ +public class PShapeJava2D extends PShapeSVG { + Paint strokeGradientPaint; + Paint fillGradientPaint; + + + public PShapeJava2D(XML svg) { + super(svg); + } + + + public PShapeJava2D(PShapeSVG parent, XML properties, boolean parseKids) { + super(parent, properties, parseKids); + } + + + @Override + protected void setParent(PShapeSVG parent) { + super.setParent(parent); + + if (parent instanceof PShapeJava2D) { + PShapeJava2D pj = (PShapeJava2D) parent; + fillGradientPaint = pj.fillGradientPaint; + strokeGradientPaint = pj.strokeGradientPaint; + + } else { // parent is null or not Java2D + fillGradientPaint = null; + strokeGradientPaint = null; + } + } + + + /** Factory method for subclasses. */ + @Override + protected PShapeSVG createShape(PShapeSVG parent, XML properties, boolean parseKids) { + return new PShapeJava2D(parent, properties, parseKids); + } + + + /* + @Override + public void setColor(String colorText, boolean isFill) { + super.setColor(colorText, isFill); + + if (fillGradient != null) { + fillGradientPaint = calcGradientPaint(fillGradient); + } + if (strokeGradient != null) { + strokeGradientPaint = calcGradientPaint(strokeGradient); + } + } + */ + + + static class LinearGradientPaint implements Paint { + float x1, y1, x2, y2; + float[] offset; + int[] color; + int count; + float opacity; + + public LinearGradientPaint(float x1, float y1, float x2, float y2, + float[] offset, int[] color, int count, + float opacity) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + this.offset = offset; + this.color = color; + this.count = count; + this.opacity = opacity; + } + + public PaintContext createContext(ColorModel cm, + Rectangle deviceBounds, Rectangle2D userBounds, + AffineTransform xform, RenderingHints hints) { + Point2D t1 = xform.transform(new Point2D.Float(x1, y1), null); + Point2D t2 = xform.transform(new Point2D.Float(x2, y2), null); + return new LinearGradientContext((float) t1.getX(), (float) t1.getY(), + (float) t2.getX(), (float) t2.getY()); + } + + public int getTransparency() { + return TRANSLUCENT; // why not.. rather than checking each color + } + + public class LinearGradientContext implements PaintContext { + int ACCURACY = 2; + float tx1, ty1, tx2, ty2; + + public LinearGradientContext(float tx1, float ty1, float tx2, float ty2) { + this.tx1 = tx1; + this.ty1 = ty1; + this.tx2 = tx2; + this.ty2 = ty2; + } + + public void dispose() { } + + public ColorModel getColorModel() { return ColorModel.getRGBdefault(); } + + public Raster getRaster(int x, int y, int w, int h) { + WritableRaster raster = + getColorModel().createCompatibleWritableRaster(w, h); + + int[] data = new int[w * h * 4]; + + // make normalized version of base vector + float nx = tx2 - tx1; + float ny = ty2 - ty1; + float len = (float) Math.sqrt(nx*nx + ny*ny); + if (len != 0) { + nx /= len; + ny /= len; + } + + int span = (int) PApplet.dist(tx1, ty1, tx2, ty2) * ACCURACY; + if (span <= 0) { + //System.err.println("span is too small"); + // annoying edge case where the gradient isn't legit + int index = 0; + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i++) { + data[index++] = 0; + data[index++] = 0; + data[index++] = 0; + data[index++] = 255; + } + } + + } else { + int[][] interp = new int[span][4]; + int prev = 0; + for (int i = 1; i < count; i++) { + int c0 = color[i-1]; + int c1 = color[i]; + int last = (int) (offset[i] * (span-1)); + //System.out.println("last is " + last); + for (int j = prev; j <= last; j++) { + float btwn = PApplet.norm(j, prev, last); + interp[j][0] = (int) PApplet.lerp((c0 >> 16) & 0xff, (c1 >> 16) & 0xff, btwn); + interp[j][1] = (int) PApplet.lerp((c0 >> 8) & 0xff, (c1 >> 8) & 0xff, btwn); + interp[j][2] = (int) PApplet.lerp(c0 & 0xff, c1 & 0xff, btwn); + interp[j][3] = (int) (PApplet.lerp((c0 >> 24) & 0xff, (c1 >> 24) & 0xff, btwn) * opacity); + //System.out.println(j + " " + interp[j][0] + " " + interp[j][1] + " " + interp[j][2]); + } + prev = last; + } + + int index = 0; + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i++) { + //float distance = 0; //PApplet.dist(cx, cy, x + i, y + j); + //int which = PApplet.min((int) (distance * ACCURACY), interp.length-1); + float px = (x + i) - tx1; + float py = (y + j) - ty1; + // distance up the line is the dot product of the normalized + // vector of the gradient start/stop by the point being tested + int which = (int) ((px*nx + py*ny) * ACCURACY); + if (which < 0) which = 0; + if (which > interp.length-1) which = interp.length-1; + //if (which > 138) System.out.println("grabbing " + which); + + data[index++] = interp[which][0]; + data[index++] = interp[which][1]; + data[index++] = interp[which][2]; + data[index++] = interp[which][3]; + } + } + } + raster.setPixels(0, 0, w, h, data); + + return raster; + } + } + } + + + static class RadialGradientPaint implements Paint { + float cx, cy, radius; + float[] offset; + int[] color; + int count; + float opacity; + + public RadialGradientPaint(float cx, float cy, float radius, + float[] offset, int[] color, int count, + float opacity) { + this.cx = cx; + this.cy = cy; + this.radius = radius; + this.offset = offset; + this.color = color; + this.count = count; + this.opacity = opacity; + } + + public PaintContext createContext(ColorModel cm, + Rectangle deviceBounds, Rectangle2D userBounds, + AffineTransform xform, RenderingHints hints) { + return new RadialGradientContext(); + } + + public int getTransparency() { + return TRANSLUCENT; + } + + public class RadialGradientContext implements PaintContext { + int ACCURACY = 5; + + public void dispose() {} + + public ColorModel getColorModel() { return ColorModel.getRGBdefault(); } + + public Raster getRaster(int x, int y, int w, int h) { + WritableRaster raster = + getColorModel().createCompatibleWritableRaster(w, h); + + int span = (int) radius * ACCURACY; + int[][] interp = new int[span][4]; + int prev = 0; + for (int i = 1; i < count; i++) { + int c0 = color[i-1]; + int c1 = color[i]; + int last = (int) (offset[i] * (span - 1)); + for (int j = prev; j <= last; j++) { + float btwn = PApplet.norm(j, prev, last); + interp[j][0] = (int) PApplet.lerp((c0 >> 16) & 0xff, (c1 >> 16) & 0xff, btwn); + interp[j][1] = (int) PApplet.lerp((c0 >> 8) & 0xff, (c1 >> 8) & 0xff, btwn); + interp[j][2] = (int) PApplet.lerp(c0 & 0xff, c1 & 0xff, btwn); + interp[j][3] = (int) (PApplet.lerp((c0 >> 24) & 0xff, (c1 >> 24) & 0xff, btwn) * opacity); + } + prev = last; + } + + int[] data = new int[w * h * 4]; + int index = 0; + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i++) { + float distance = PApplet.dist(cx, cy, x + i, y + j); + int which = PApplet.min((int) (distance * ACCURACY), interp.length-1); + + data[index++] = interp[which][0]; + data[index++] = interp[which][1]; + data[index++] = interp[which][2]; + data[index++] = interp[which][3]; + } + } + raster.setPixels(0, 0, w, h, data); + + return raster; + } + } + } + + + protected Paint calcGradientPaint(Gradient gradient) { + if (gradient instanceof LinearGradient) { +// System.out.println("creating linear gradient"); + LinearGradient grad = (LinearGradient) gradient; + return new LinearGradientPaint(grad.x1, grad.y1, grad.x2, grad.y2, + grad.offset, grad.color, grad.count, + opacity); + + } else if (gradient instanceof RadialGradient) { +// System.out.println("creating radial gradient"); + RadialGradient grad = (RadialGradient) gradient; + return new RadialGradientPaint(grad.cx, grad.cy, grad.r, + grad.offset, grad.color, grad.count, + opacity); + } + return null; + } + + +// protected Paint calcGradientPaint(Gradient gradient, +// float x1, float y1, float x2, float y2) { +// if (gradient instanceof LinearGradient) { +// LinearGradient grad = (LinearGradient) gradient; +// return new LinearGradientPaint(x1, y1, x2, y2, +// grad.offset, grad.color, grad.count, +// opacity); +// } +// throw new RuntimeException("Not a linear gradient."); +// } + + +// protected Paint calcGradientPaint(Gradient gradient, +// float cx, float cy, float r) { +// if (gradient instanceof RadialGradient) { +// RadialGradient grad = (RadialGradient) gradient; +// return new RadialGradientPaint(cx, cy, r, +// grad.offset, grad.color, grad.count, +// opacity); +// } +// throw new RuntimeException("Not a radial gradient."); +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + @Override + protected void styles(PGraphics g) { + super.styles(g); + + //if (g instanceof PGraphicsJava2D) { + PGraphicsJava2D p2d = (PGraphicsJava2D) g; + + if (strokeGradient != null) { + p2d.strokeGradient = true; + if (strokeGradientPaint == null) { + strokeGradientPaint = calcGradientPaint(strokeGradient); + } + p2d.strokeGradientObject = strokeGradientPaint; + } else { + // need to shut off, in case parent object has a gradient applied + //p2d.strokeGradient = false; + } + if (fillGradient != null) { + p2d.fillGradient = true; + if (fillGradientPaint == null) { + fillGradientPaint = calcGradientPaint(fillGradient); + } + p2d.fillGradientObject = fillGradientPaint; + } else { + // need to shut off, in case parent object has a gradient applied + //p2d.fillGradient = false; + } + //} + } +} \ No newline at end of file diff --git a/src/main/java/processing/awt/PSurfaceAWT.java b/src/main/java/processing/awt/PSurfaceAWT.java new file mode 100644 index 0000000..4cf50e8 --- /dev/null +++ b/src/main/java/processing/awt/PSurfaceAWT.java @@ -0,0 +1,1567 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2014-15 The Processing Foundation + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.awt; + +import java.awt.Canvas; +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Image; +import java.awt.Insets; +import java.awt.Label; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.event.*; +import java.awt.geom.Rectangle2D; +import java.awt.image.*; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JFrame; + +import processing.core.PApplet; +import processing.core.PConstants; +import processing.core.PGraphics; +import processing.core.PImage; +import processing.core.PSurfaceNone; +import processing.event.KeyEvent; +import processing.event.MouseEvent; + + +public class PSurfaceAWT extends PSurfaceNone { + GraphicsDevice displayDevice; + + // used for canvas to determine whether resizable or not +// boolean resizable; // default is false + + // Internally, we know it's always a JFrame (not just a Frame) +// JFrame frame; + // Trying Frame again with a11 to see if this avoids some Swing nastiness. + // In the past, AWT Frames caused some problems on Windows and Linux, + // but those may not be a problem for our reworked PSurfaceAWT class. + Frame frame; + + // Note that x and y may not be zero, depending on the display configuration + Rectangle screenRect; + + // Used for resizing, at least on Windows insets size changes when + // frame.setResizable() is called, and in resize listener we need + // to know what size the window was before. + Insets currentInsets = new Insets(0, 0, 0, 0); + + // 3.0a5 didn't use strategy, and active was shut off during init() w/ retina +// boolean useStrategy = true; + + Canvas canvas; +// Component canvas; + +// PGraphics graphics; // moved to PSurfaceNone + + int sketchWidth; + int sketchHeight; + + int windowScaleFactor; + + + public PSurfaceAWT(PGraphics graphics) { + //this.graphics = graphics; + super(graphics); + + /* + if (checkRetina()) { +// System.out.println("retina in use"); + + // The active-mode rendering seems to be 2x slower, so disable it + // with retina. On a non-retina machine, however, useActive seems + // the only (or best) way to handle the rendering. +// useActive = false; +// canvas = new JPanel(true) { +// @Override +// public void paint(Graphics screen) { +//// if (!sketch.insideDraw) { +// screen.drawImage(PSurfaceAWT.this.graphics.image, 0, 0, sketchWidth, sketchHeight, null); +//// } +// } +// }; + // Under 1.8 and the current 3.0a6 threading regime, active mode w/o + // strategy is far faster, but perhaps only because it's blitting with + // flicker--pushing pixels out before the screen has finished rendering. +// useStrategy = false; + } + */ + canvas = new SmoothCanvas(); +// if (useStrategy) { + //canvas.setIgnoreRepaint(true); +// } + + // Pass tab key to the sketch, rather than moving between components + canvas.setFocusTraversalKeysEnabled(false); + + canvas.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + if (!sketch.isLooping()) { + // make sure this is a real resize event, not just initial setup + // https://github.com/processing/processing/issues/3310 + Dimension canvasSize = canvas.getSize(); + if (canvasSize.width != sketch.sketchWidth() || + canvasSize.height != sketch.sketchHeight()) { + sketch.redraw(); + } + } + } + }); + addListeners(); + } + + +// /** +// * Handle grabbing the focus on startup. Other renderers can override this +// * if handling needs to be different. For the AWT, the request is invoked +// * later on the EDT. Other implementations may not require that, so the +// * invokeLater() happens in here rather than requiring the caller to wrap it. +// */ +// @Override +// void requestFocus() { +//// System.out.println("requesFocus() outer " + EventQueue.isDispatchThread()); +// // for 2.0a6, moving this request to the EDT +// EventQueue.invokeLater(new Runnable() { +// public void run() { +// // Call the request focus event once the image is sure to be on +// // screen and the component is valid. The OpenGL renderer will +// // request focus for its canvas inside beginDraw(). +// // http://java.sun.com/j2se/1.4.2/docs/api/java/awt/doc-files/FocusSpec.html +// // Disabling for 0185, because it causes an assertion failure on OS X +// // http://code.google.com/p/processing/issues/detail?id=258 +// // requestFocus(); +// +// // Changing to this version for 0187 +// // http://code.google.com/p/processing/issues/detail?id=279 +// //requestFocusInWindow(); +// +// // For 3.0, just call this directly on the Canvas object +// if (canvas != null) { +// //System.out.println("requesting focus " + EventQueue.isDispatchThread()); +// //System.out.println("requesting focus " + frame.isVisible()); +// //canvas.requestFocusInWindow(); +// canvas.requestFocus(); +// } +// } +// }); +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public class SmoothCanvas extends Canvas { + private Dimension oldSize = new Dimension(0, 0); + private Dimension newSize = new Dimension(0, 0); + + + // Turns out getParent() returns a JPanel on a JFrame. Yech. + public Frame getFrame() { + return frame; + } + + + @Override + public Dimension getPreferredSize() { + return new Dimension(sketchWidth, sketchHeight); + } + + + @Override + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + + @Override + public Dimension getMaximumSize() { + //return resizable ? super.getMaximumSize() : getPreferredSize(); + return frame.isResizable() ? super.getMaximumSize() : getPreferredSize(); + } + + + @Override + public void validate() { + super.validate(); + newSize.width = getWidth(); + newSize.height = getHeight(); +// if (oldSize.equals(newSize)) { +//// System.out.println("validate() return " + oldSize); +// return; +// } else { + if (!oldSize.equals(newSize)) { +// System.out.println("validate() render old=" + oldSize + " -> new=" + newSize); + oldSize = newSize; + sketch.setSize(newSize.width / windowScaleFactor, newSize.height / windowScaleFactor); +// try { + render(); +// } catch (IllegalStateException ise) { +// System.out.println(ise.getMessage()); +// } + } + } + + + @Override + public void update(Graphics g) { +// System.out.println("updating"); + paint(g); + } + + + @Override + public void paint(Graphics screen) { +// System.out.println("painting"); +// if (useStrategy) { + render(); + /* + if (graphics != null) { + System.out.println("drawing to screen " + canvas); + screen.drawImage(graphics.image, 0, 0, sketchWidth, sketchHeight, null); + } + */ + +// } else { +//// new Exception("painting").printStackTrace(System.out); +//// if (graphics.image != null) { // && !sketch.insideDraw) { +// if (onscreen != null) { +//// synchronized (graphics.image) { +// // Needs the width/height to be set so that retina images are properly scaled down +//// screen.drawImage(graphics.image, 0, 0, sketchWidth, sketchHeight, null); +// synchronized (offscreenLock) { +// screen.drawImage(onscreen, 0, 0, sketchWidth, sketchHeight, null); +// } +// } +// } + } + } + + /* + @Override + public void addNotify() { +// System.out.println("adding notify"); + super.addNotify(); + // prior to Java 7 on OS X, this no longer works [121222] +// createBufferStrategy(2); + } + */ + + + synchronized protected void render() { + if (canvas.isDisplayable() && + graphics.image != null) { + if (canvas.getBufferStrategy() == null) { + canvas.createBufferStrategy(2); + } + BufferStrategy strategy = canvas.getBufferStrategy(); + if (strategy != null) { + // Render single frame +// try { + do { + // The following loop ensures that the contents of the drawing buffer + // are consistent in case the underlying surface was recreated + do { + Graphics2D draw = (Graphics2D) strategy.getDrawGraphics(); + // draw to width/height, since this may be a 2x image + draw.drawImage(graphics.image, 0, 0, sketchWidth, sketchHeight, null); + draw.dispose(); + } while (strategy.contentsRestored()); + + // Display the buffer + strategy.show(); + + // Repeat the rendering if the drawing buffer was lost + } while (strategy.contentsLost()); + } + } + } + + + /* + protected void blit() { + // Other folks that call render() (i.e. paint()) are already on the EDT. + // We need to be using the EDT since we're messing with the Canvas + // object and BufferStrategy and friends. + //EventQueue.invokeLater(new Runnable() { + //public void run() { + //((SmoothCanvas) canvas).render(); + //} + //}); + + if (useStrategy) { + // Not necessary to be on the EDT to update BufferStrategy + //((SmoothCanvas) canvas).render(); + render(); + } else { + if (graphics.image != null) { + BufferedImage graphicsImage = (BufferedImage) graphics.image; + if (offscreen == null || + offscreen.getWidth() != graphicsImage.getWidth() || + offscreen.getHeight() != graphicsImage.getHeight()) { + System.out.println("creating new image"); + offscreen = (BufferedImage) + canvas.createImage(graphicsImage.getWidth(), + graphicsImage.getHeight()); +// off = offscreen.getGraphics(); + } +// synchronized (offscreen) { + Graphics2D off = (Graphics2D) offscreen.getGraphics(); +// off.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1)); + off.drawImage(graphicsImage, 0, 0, null); +// } + off.dispose(); + synchronized (offscreenLock) { + BufferedImage temp = onscreen; + onscreen = offscreen; + offscreen = temp; + } + canvas.repaint(); + } + } + } + */ + + + // what needs to happen here? + @Override + public void initOffscreen(PApplet sketch) { + this.sketch = sketch; + } + + /* + public Frame initOffscreen() { + Frame dummy = new Frame(); + dummy.pack(); // get legit AWT graphics + // but don't show it + return dummy; + } + */ + + /* + @Override + public Component initComponent(PApplet sketch) { + this.sketch = sketch; + + // needed for getPreferredSize() et al + sketchWidth = sketch.sketchWidth(); + sketchHeight = sketch.sketchHeight(); + + return canvas; + } + */ + + + @Override + public void initFrame(final PApplet sketch) {/*, int backgroundColor, + int deviceIndex, boolean fullScreen, boolean spanDisplays) {*/ + this.sketch = sketch; + + GraphicsEnvironment environment = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + + int displayNum = sketch.sketchDisplay(); +// System.out.println("display from sketch is " + displayNum); + if (displayNum > 0) { // if -1, use the default device + GraphicsDevice[] devices = environment.getScreenDevices(); + if (displayNum <= devices.length) { + displayDevice = devices[displayNum - 1]; + } else { + System.err.format("Display %d does not exist, " + + "using the default display instead.%n", displayNum); + for (int i = 0; i < devices.length; i++) { + System.err.format("Display %d is %s%n", (i+1), devices[i]); + } + } + } + if (displayDevice == null) { + displayDevice = environment.getDefaultScreenDevice(); + } + + // Need to save the window bounds at full screen, + // because pack() will cause the bounds to go to zero. + // http://dev.processing.org/bugs/show_bug.cgi?id=923 + boolean spanDisplays = sketch.sketchDisplay() == PConstants.SPAN; + screenRect = spanDisplays ? getDisplaySpan() : + displayDevice.getDefaultConfiguration().getBounds(); + // DisplayMode doesn't work here, because we can't get the upper-left + // corner of the display, which is important for multi-display setups. + + // Set the displayWidth/Height variables inside PApplet, so that they're + // usable and can even be returned by the sketchWidth()/Height() methods. + sketch.displayWidth = screenRect.width; + sketch.displayHeight = screenRect.height; + + windowScaleFactor = PApplet.platform == PConstants.MACOSX ? + 1 : sketch.pixelDensity; + + sketchWidth = sketch.sketchWidth() * windowScaleFactor; + sketchHeight = sketch.sketchHeight() * windowScaleFactor; + + boolean fullScreen = sketch.sketchFullScreen(); + // Removing the section below because sometimes people want to do the + // full screen size in a window, and it also breaks insideSettings(). + // With 3.x, fullScreen() is so easy, that it's just better that way. + // https://github.com/processing/processing/issues/3545 + /* + // Sketch has already requested to be the same as the screen's + // width and height, so let's roll with full screen mode. + if (screenRect.width == sketchWidth && + screenRect.height == sketchHeight) { + fullScreen = true; + sketch.fullScreen(); // won't change the renderer + } + */ + + if (fullScreen || spanDisplays) { + sketchWidth = screenRect.width; + sketchHeight = screenRect.height; + } + + // Using a JFrame fixes a Windows problem with Present mode. This might + // be our error, but usually this is the sort of crap we usually get from + // OS X. It's time for a turnaround: Redmond is thinking different too! + // https://github.com/processing/processing/issues/1955 + frame = new JFrame(displayDevice.getDefaultConfiguration()); +// frame = new Frame(displayDevice.getDefaultConfiguration()); +// // Default Processing gray, which will be replaced below if another +// // color is specified on the command line (i.e. in the prefs). +// ((JFrame) frame).getContentPane().setBackground(WINDOW_BGCOLOR); +// // Cannot call setResizable(false) until later due to OS X (issue #467) + +// // Removed code above, also removed from what's now in the placeXxxx() +// // methods. Not sure why it was being double-set; hopefully anachronistic. +// if (backgroundColor == 0) { +// backgroundColor = WINDOW_BGCOLOR; +// } + final Color windowColor = new Color(sketch.sketchWindowColor(), false); + if (frame instanceof JFrame) { + ((JFrame) frame).getContentPane().setBackground(windowColor); + } else { + frame.setBackground(windowColor); + } + + // Put the p5 logo in the Frame's corner to override the Java coffee cup. + setProcessingIcon(frame); + + // For 0149, moving this code (up to the pack() method) before init(). + // For OpenGL (and perhaps other renderers in the future), a peer is + // needed before a GLDrawable can be created. So pack() needs to be + // called on the Frame before applet.init(), which itself calls size(), + // and launches the Thread that will kick off setup(). + // http://dev.processing.org/bugs/show_bug.cgi?id=891 + // http://dev.processing.org/bugs/show_bug.cgi?id=908 + + frame.add(canvas); + setSize(sketchWidth / windowScaleFactor, sketchHeight / windowScaleFactor); + + /* + if (fullScreen) { + // Called here because the graphics device is needed before we can + // determine whether the sketch wants size(displayWidth, displayHeight), + // and getting the graphics device will be PSurface-specific. + PApplet.hideMenuBar(); + + // Tried to use this to fix the 'present' mode issue. + // Did not help, and the screenRect setup seems to work fine. + //frame.setExtendedState(Frame.MAXIMIZED_BOTH); + + // https://github.com/processing/processing/pull/3162 + frame.dispose(); // release native resources, allows setUndecorated() + frame.setUndecorated(true); + // another duplicate? +// if (backgroundColor != null) { +// frame.getContentPane().setBackground(backgroundColor); +// } + // this may be the bounds of all screens + frame.setBounds(screenRect); + // will be set visible in placeWindow() [3.0a10] + //frame.setVisible(true); // re-add native resources + } + */ + frame.setLayout(null); + //frame.add(applet); + + // Need to pass back our new sketchWidth/Height here, because it may have + // been overridden by numbers we calculated above if fullScreen and/or + // spanScreens was in use. +// pg = sketch.makePrimaryGraphics(sketchWidth, sketchHeight); +// pg = sketch.makePrimaryGraphics(); + + // resize sketch to sketchWidth/sketchHeight here + + if (fullScreen) { + frame.invalidate(); + } else { +// frame.pack(); + } + + // insufficient, places the 100x100 sketches offset strangely + //frame.validate(); + + // disabling resize has to happen after pack() to avoid apparent Apple bug + // http://code.google.com/p/processing/issues/detail?id=467 + frame.setResizable(false); + + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + sketch.exit(); // don't quit, need to just shut everything down (0133) + } + }); + +// sketch.setFrame(frame); + } + + + @Override + public Object getNative() { + return canvas; + } + + +// public Toolkit getToolkit() { +// return canvas.getToolkit(); +// } + + + /** Set the window (and dock, or whatever necessary) title. */ + @Override + public void setTitle(String title) { + frame.setTitle(title); + // Workaround for apparent Java bug on OS X? + // https://github.com/processing/processing/issues/3472 + if (cursorVisible && + (PApplet.platform == PConstants.MACOSX) && + (cursorType != PConstants.ARROW)) { + hideCursor(); + showCursor(); + } + } + + + /** Set true if we want to resize things (default is not resizable) */ + @Override + public void setResizable(boolean resizable) { + //this.resizable = resizable; // really only used for canvas + + if (frame != null) { + frame.setResizable(resizable); + } + } + + + @Override + public void setIcon(PImage image) { + Image awtImage = (Image) image.getNative(); + + if (PApplet.platform != PConstants.MACOSX) { + frame.setIconImage(awtImage); + + } else { + try { + final String td = "processing.core.ThinkDifferent"; + Class thinkDifferent = + Thread.currentThread().getContextClassLoader().loadClass(td); + Method method = + thinkDifferent.getMethod("setIconImage", new Class[] { java.awt.Image.class }); + method.invoke(null, new Object[] { awtImage }); + } catch (Exception e) { + e.printStackTrace(); // That's unfortunate + } + } + } + + + @Override + public void setAlwaysOnTop(boolean always) { + frame.setAlwaysOnTop(always); + } + + + @Override + public void setLocation(int x, int y) { + frame.setLocation(x, y); + } + + + List iconImages; + + protected void setProcessingIcon(Frame frame) { + // On OS X, this only affects what shows up in the dock when minimized. + // So replacing it is actually a step backwards. Brilliant. + if (PApplet.platform != PConstants.MACOSX) { + //Image image = Toolkit.getDefaultToolkit().createImage(ICON_IMAGE); + //frame.setIconImage(image); + try { + if (iconImages == null) { + iconImages = new ArrayList(); + final int[] sizes = { 16, 32, 48, 64, 128, 256, 512 }; + + for (int sz : sizes) { + //URL url = getClass().getResource("/icon/icon-" + sz + ".png"); + URL url = PApplet.class.getResource("/icon/icon-" + sz + ".png"); + Image image = Toolkit.getDefaultToolkit().getImage(url); + iconImages.add(image); + //iconImages.add(Toolkit.getLibImage("icons/pde-" + sz + ".png", frame)); + } + } + frame.setIconImages(iconImages); + + } catch (Exception e) { } // harmless; keep this to ourselves + + } else { // handle OS X differently + if (!dockIconSpecified()) { // don't override existing -Xdock param + // On OS X, set this for AWT surfaces, which handles the dock image + // as well as the cmd-tab image that's shown. Just one size, I guess. + URL url = PApplet.class.getResource("/icon/icon-512.png"); + // Seems dangerous to have this in code instead of using reflection, no? + //ThinkDifferent.setIconImage(Toolkit.getDefaultToolkit().getImage(url)); + try { + final String td = "processing.core.ThinkDifferent"; + Class thinkDifferent = + Thread.currentThread().getContextClassLoader().loadClass(td); + Method method = + thinkDifferent.getMethod("setIconImage", new Class[] { java.awt.Image.class }); + method.invoke(null, new Object[] { Toolkit.getDefaultToolkit().getImage(url) }); + } catch (Exception e) { + e.printStackTrace(); // That's unfortunate + } + } + } + } + + + /** + * @return true if -Xdock:icon was specified on the command line + */ + private boolean dockIconSpecified() { + // TODO This is incomplete... Haven't yet found a way to figure out if + // the app has an icns file specified already. Help? + List jvmArgs = + ManagementFactory.getRuntimeMXBean().getInputArguments(); + for (String arg : jvmArgs) { + if (arg.startsWith("-Xdock:icon")) { + return true; // dock image already set + } + } + return false; + } + + + @Override + public void setVisible(boolean visible) { + frame.setVisible(visible); + + // Generally useful whenever setting the frame visible + if (canvas != null) { + //canvas.requestFocusInWindow(); + canvas.requestFocus(); + } + + // removing per https://github.com/processing/processing/pull/3162 + // can remove the code below once 3.0a6 is tested and behaving +/* + if (visible && PApplet.platform == PConstants.LINUX) { + // Linux doesn't deal with insets the same way. We get fake insets + // earlier, and then the window manager will slap its own insets + // onto things once the frame is realized on the screen. Awzm. + if (PApplet.platform == PConstants.LINUX) { + Insets insets = frame.getInsets(); + frame.setSize(Math.max(sketchWidth, MIN_WINDOW_WIDTH) + + insets.left + insets.right, + Math.max(sketchHeight, MIN_WINDOW_HEIGHT) + + insets.top + insets.bottom); + } + } +*/ + } + + + //public void placeFullScreen(boolean hideStop) { + @Override + public void placePresent(int stopColor) { + setFullFrame(); + + // After the pack(), the screen bounds are gonna be 0s +// frame.setBounds(screenRect); // already called in setFullFrame() + canvas.setBounds((screenRect.width - sketchWidth) / 2, + (screenRect.height - sketchHeight) / 2, + sketchWidth, sketchHeight); + +// if (PApplet.platform == PConstants.MACOSX) { +// macosxFullScreenEnable(frame); +// macosxFullScreenToggle(frame); +// } + + if (stopColor != 0) { + Label label = new Label("stop"); + label.setForeground(new Color(stopColor, false)); + label.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(java.awt.event.MouseEvent e) { + sketch.exit(); + } + }); + frame.add(label); + + Dimension labelSize = label.getPreferredSize(); + // sometimes shows up truncated on mac + //System.out.println("label width is " + labelSize.width); + labelSize = new Dimension(100, labelSize.height); + label.setSize(labelSize); + label.setLocation(20, screenRect.height - labelSize.height - 20); + } + +// if (sketch.getGraphics().displayable()) { +// setVisible(true); +// } + } + + + /* + @Override + public void placeWindow(int[] location) { + setFrameSize(); //sketchWidth, sketchHeight); + + if (location != null) { + // a specific location was received from the Runner + // (applet has been run more than once, user placed window) + frame.setLocation(location[0], location[1]); + + } else { // just center on screen + // Can't use frame.setLocationRelativeTo(null) because it sends the + // frame to the main display, which undermines the --display setting. + frame.setLocation(screenRect.x + (screenRect.width - sketchWidth) / 2, + screenRect.y + (screenRect.height - sketchHeight) / 2); + } + Point frameLoc = frame.getLocation(); + if (frameLoc.y < 0) { + // Windows actually allows you to place frames where they can't be + // closed. Awesome. http://dev.processing.org/bugs/show_bug.cgi?id=1508 + frame.setLocation(frameLoc.x, 30); + } + +// if (backgroundColor != null) { +// ((JFrame) frame).getContentPane().setBackground(backgroundColor); +// } + + setCanvasSize(); //sketchWidth, sketchHeight); + + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + System.exit(0); + } + }); + + // handle frame resizing events + setupFrameResizeListener(); + + // all set for rockin + if (sketch.getGraphics().displayable()) { + frame.setVisible(true); + } + } + */ + + + private void setCanvasSize() { +// System.out.format("setting canvas size %d %d%n", sketchWidth, sketchHeight); +// new Exception().printStackTrace(System.out); + int contentW = Math.max(sketchWidth, MIN_WINDOW_WIDTH); + int contentH = Math.max(sketchHeight, MIN_WINDOW_HEIGHT); + + canvas.setBounds((contentW - sketchWidth)/2, + (contentH - sketchHeight)/2, + sketchWidth, sketchHeight); + } + + + /** Resize frame for these sketch (canvas) dimensions. */ + private Dimension setFrameSize() { //int sketchWidth, int sketchHeight) { + // https://github.com/processing/processing/pull/3162 + frame.addNotify(); // using instead of show() to add the peer [fry] + +// System.out.format("setting frame size %d %d %n", sketchWidth, sketchHeight); +// new Exception().printStackTrace(System.out); + currentInsets = frame.getInsets(); + int windowW = Math.max(sketchWidth, MIN_WINDOW_WIDTH) + + currentInsets.left + currentInsets.right; + int windowH = Math.max(sketchHeight, MIN_WINDOW_HEIGHT) + + currentInsets.top + currentInsets.bottom; + frame.setSize(windowW, windowH); + return new Dimension(windowW, windowH); + } + + + private void setFrameCentered() { + // Can't use frame.setLocationRelativeTo(null) because it sends the + // frame to the main display, which undermines the --display setting. + frame.setLocation(screenRect.x + (screenRect.width - sketchWidth) / 2, + screenRect.y + (screenRect.height - sketchHeight) / 2); + } + + + /** Hide the menu bar, make the Frame undecorated, set it to screenRect. */ + private void setFullFrame() { + // Called here because the graphics device is needed before we can + // determine whether the sketch wants size(displayWidth, displayHeight), + // and getting the graphics device will be PSurface-specific. + PApplet.hideMenuBar(); + + // Tried to use this to fix the 'present' mode issue. + // Did not help, and the screenRect setup seems to work fine. + //frame.setExtendedState(Frame.MAXIMIZED_BOTH); + + // https://github.com/processing/processing/pull/3162 + //frame.dispose(); // release native resources, allows setUndecorated() + frame.removeNotify(); + frame.setUndecorated(true); + frame.addNotify(); + + // this may be the bounds of all screens + frame.setBounds(screenRect); + // will be set visible in placeWindow() [3.0a10] + //frame.setVisible(true); // re-add native resources + } + + + @Override + public void placeWindow(int[] location, int[] editorLocation) { + //Dimension window = setFrameSize(sketchWidth, sketchHeight); + Dimension window = setFrameSize(); //sketchWidth, sketchHeight); + + int contentW = Math.max(sketchWidth, MIN_WINDOW_WIDTH); + int contentH = Math.max(sketchHeight, MIN_WINDOW_HEIGHT); + + if (sketch.sketchFullScreen()) { + setFullFrame(); + } + + // Ignore placement of previous window and editor when full screen + if (!sketch.sketchFullScreen()) { + if (location != null) { + // a specific location was received from the Runner + // (applet has been run more than once, user placed window) + frame.setLocation(location[0], location[1]); + + } else if (editorLocation != null) { + int locationX = editorLocation[0] - 20; + int locationY = editorLocation[1]; + + if (locationX - window.width > 10) { + // if it fits to the left of the window + frame.setLocation(locationX - window.width, locationY); + + } else { // doesn't fit + // if it fits inside the editor window, + // offset slightly from upper lefthand corner + // so that it's plunked inside the text area + //locationX = editorLocation[0] + 66; + //locationY = editorLocation[1] + 66; + locationX = (sketch.displayWidth - window.width) / 2; + locationY = (sketch.displayHeight - window.height) / 2; + + /* + if ((locationX + window.width > sketch.displayWidth - 33) || + (locationY + window.height > sketch.displayHeight - 33)) { + // otherwise center on screen + locationX = (sketch.displayWidth - window.width) / 2; + locationY = (sketch.displayHeight - window.height) / 2; + } + */ + frame.setLocation(locationX, locationY); + } + } else { // just center on screen + setFrameCentered(); + } + Point frameLoc = frame.getLocation(); + if (frameLoc.y < 0) { + // Windows actually allows you to place frames where they can't be + // closed. Awesome. http://dev.processing.org/bugs/show_bug.cgi?id=1508 + frame.setLocation(frameLoc.x, 30); + } + } + + canvas.setBounds((contentW - sketchWidth)/2, + (contentH - sketchHeight)/2, + sketchWidth, sketchHeight); + + // handle frame resizing events + setupFrameResizeListener(); + + /* + // If displayable() is false, then PSurfaceNone should be used, but... + if (sketch.getGraphics().displayable()) { + frame.setVisible(true); +// System.out.println("setting visible on EDT? " + EventQueue.isDispatchThread()); + //requestFocus(); +// if (canvas != null) { +// //canvas.requestFocusInWindow(); +// canvas.requestFocus(); +// } + } + */ +// if (sketch.getGraphics().displayable()) { +// setVisible(true); +// } + } + + + // needs to resize the frame, which will resize the canvas, and so on... + @Override + public void setSize(int wide, int high) { + // When the surface is set to resizable via surface.setResizable(true), + // a crash may occur if the user sets the window to size zero. + // https://github.com/processing/processing/issues/5052 + if (high <= 0) { + high = 1; + } + if (wide <= 0) { + wide = 1; + } + +// if (PApplet.DEBUG) { +// //System.out.format("frame visible %b, setSize(%d, %d) %n", frame.isVisible(), wide, high); +// new Exception(String.format("setSize(%d, %d)", wide, high)).printStackTrace(System.out); +// } + + //if (wide == sketchWidth && high == sketchHeight) { // doesn't work on launch + if (wide == sketch.width && high == sketch.height && + (frame == null || currentInsets.equals(frame.getInsets()))) { +// if (PApplet.DEBUG) { +// new Exception("w/h unchanged " + wide + " " + high).printStackTrace(System.out); +// } + return; // unchanged, don't rebuild everything + } + + sketchWidth = wide * windowScaleFactor; + sketchHeight = high * windowScaleFactor; + +// canvas.setSize(wide, high); +// frame.setSize(wide, high); + if (frame != null) { // skip if just a canvas + setFrameSize(); //wide, high); + } + setCanvasSize(); +// if (frame != null) { +// frame.setLocationRelativeTo(null); +// } + + //initImage(graphics, wide, high); + + //throw new RuntimeException("implement me, see readme.md"); + sketch.setSize(wide, high); +// sketch.width = wide; +// sketch.height = high; + + // set PGraphics variables for width/height/pixelWidth/pixelHeight + graphics.setSize(wide, high); +// System.out.println("out of setSize()"); + } + + + //public void initImage(PGraphics gr, int wide, int high) { + /* + @Override + public void initImage(PGraphics graphics) { + GraphicsConfiguration gc = canvas.getGraphicsConfiguration(); + // If not realized (off-screen, i.e the Color Selector Tool), gc will be null. + if (gc == null) { + System.err.println("GraphicsConfiguration null in initImage()"); + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + gc = ge.getDefaultScreenDevice().getDefaultConfiguration(); + } + + // Formerly this was broken into separate versions based on offscreen or + // not, but we may as well create a compatible image; it won't hurt, right? + int wide = graphics.width * graphics.pixelFactor; + int high = graphics.height * graphics.pixelFactor; + graphics.image = gc.createCompatibleImage(wide, high); + } + */ + + +// @Override +// public Component getComponent() { +// return canvas; +// } + + +// @Override +// public void setSmooth(int level) { +// } + + + /* + private boolean checkRetina() { + if (PApplet.platform == PConstants.MACOSX) { + // This should probably be reset each time there's a display change. + // A 5-minute search didn't turn up any such event in the Java 7 API. + // Also, should we use the Toolkit associated with the editor window? + final String javaVendor = System.getProperty("java.vendor"); + if (javaVendor.contains("Oracle")) { + GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice device = env.getDefaultScreenDevice(); + + try { + Field field = device.getClass().getDeclaredField("scale"); + if (field != null) { + field.setAccessible(true); + Object scale = field.get(device); + + if (scale instanceof Integer && ((Integer)scale).intValue() == 2) { + return true; + } + } + } catch (Exception ignore) { } + } + } + return false; + } + */ + + + /** Get the bounds rectangle for all displays. */ + static Rectangle getDisplaySpan() { + Rectangle bounds = new Rectangle(); + GraphicsEnvironment environment = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + for (GraphicsDevice device : environment.getScreenDevices()) { + for (GraphicsConfiguration config : device.getConfigurations()) { + Rectangle2D.union(bounds, config.getBounds(), bounds); + } + } + return bounds; + } + + + /* + private void checkDisplaySize() { + if (canvas.getGraphicsConfiguration() != null) { + GraphicsDevice displayDevice = getGraphicsConfiguration().getDevice(); + + if (displayDevice != null) { + Rectangle screenRect = + displayDevice.getDefaultConfiguration().getBounds(); + + displayWidth = screenRect.width; + displayHeight = screenRect.height; + } + } + } + */ + + + /** + * Set this sketch to communicate its state back to the PDE. + *

+ * This uses the stderr stream to write positions of the window + * (so that it will be saved by the PDE for the next run) and + * notify on quit. See more notes in the Worker class. + */ + @Override + public void setupExternalMessages() { + frame.addComponentListener(new ComponentAdapter() { + @Override + public void componentMoved(ComponentEvent e) { + Point where = ((Frame) e.getSource()).getLocation(); + sketch.frameMoved(where.x, where.y); + } + }); + } + + + /** + * Set up a listener that will fire proper component resize events + * in cases where frame.setResizable(true) is called. + */ + private void setupFrameResizeListener() { + frame.addWindowStateListener(new WindowStateListener() { + @Override + // Detecting when the frame is resized in order to handle the frame + // maximization bug in OSX: + // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8036935 + public void windowStateChanged(WindowEvent e) { + // This seems to be firing when dragging the window on OS X + // https://github.com/processing/processing/issues/3092 + if (Frame.MAXIMIZED_BOTH == e.getNewState()) { + // Supposedly, sending the frame to back and then front is a + // workaround for this bug: + // http://stackoverflow.com/a/23897602 + // but is not working for me... + //frame.toBack(); + //frame.toFront(); + // Packing the frame works, but that causes the window to collapse + // on OS X when the window is dragged. Changing to addNotify() for + // https://github.com/processing/processing/issues/3092 + //frame.pack(); + frame.addNotify(); + } + } + }); + + frame.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + // Ignore bad resize events fired during setup to fix + // http://dev.processing.org/bugs/show_bug.cgi?id=341 + // This should also fix the blank screen on Linux bug + // http://dev.processing.org/bugs/show_bug.cgi?id=282 + if (frame.isResizable()) { + // might be multiple resize calls before visible (i.e. first + // when pack() is called, then when it's resized for use). + // ignore them because it's not the user resizing things. + Frame farm = (Frame) e.getComponent(); + if (farm.isVisible()) { + Dimension windowSize = farm.getSize(); + int x = farm.getX() + currentInsets.left; + int y = farm.getY() + currentInsets.top; + + // JFrame (unlike java.awt.Frame) doesn't include the left/top + // insets for placement (though it does seem to need them for + // overall size of the window. Perhaps JFrame sets its coord + // system so that (0, 0) is always the upper-left of the content + // area. Which seems nice, but breaks any f*ing AWT-based code. + int w = windowSize.width - currentInsets.left - currentInsets.right; + int h = windowSize.height - currentInsets.top - currentInsets.bottom; + setSize(w / windowScaleFactor, h / windowScaleFactor); + + // correct the location when inset size changes + setLocation(x - currentInsets.left, y - currentInsets.top); + } + } + } + }); + } + + +// /** +// * (No longer in use) Use reflection to call +// * com.apple.eawt.FullScreenUtilities.setWindowCanFullScreen(window, true); +// */ +// static void macosxFullScreenEnable(Window window) { +// try { +// Class util = Class.forName("com.apple.eawt.FullScreenUtilities"); +// Class params[] = new Class[] { Window.class, Boolean.TYPE }; +// Method method = util.getMethod("setWindowCanFullScreen", params); +// method.invoke(util, window, true); +// +// } catch (ClassNotFoundException cnfe) { +// // ignored +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } +// +// +// /** +// * (No longer in use) Use reflection to call +// * com.apple.eawt.Application.getApplication().requestToggleFullScreen(window); +// */ +// static void macosxFullScreenToggle(Window window) { +// try { +// Class appClass = Class.forName("com.apple.eawt.Application"); +// +// Method getAppMethod = appClass.getMethod("getApplication"); +// Object app = getAppMethod.invoke(null, new Object[0]); +// +// Method requestMethod = +// appClass.getMethod("requestToggleFullScreen", Window.class); +// requestMethod.invoke(app, window); +// +// } catch (ClassNotFoundException cnfe) { +// // ignored +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } + + + ////////////////////////////////////////////////////////////// + + + /* + // disabling for now; requires Java 1.7 and "precise" semantics are odd... + // returns 0.1 for tick-by-tick scrolling on OS X, but it's not a matter of + // calling ceil() on the value: 1.5 goes to 1, but 2.3 goes to 2. + // "precise" is a whole different animal, so add later API to shore that up. + static protected Method preciseWheelMethod; + static { + try { + preciseWheelMethod = MouseWheelEvent.class.getMethod("getPreciseWheelRotation", new Class[] { }); + } catch (Exception e) { + // ignored, the method will just be set to null + } + } + */ + + + /** + * Figure out how to process a mouse event. When loop() has been + * called, the events will be queued up until drawing is complete. + * If noLoop() has been called, then events will happen immediately. + */ + protected void nativeMouseEvent(java.awt.event.MouseEvent nativeEvent) { + // the 'amount' is the number of button clicks for a click event, + // or the number of steps/clicks on the wheel for a mouse wheel event. + int peCount = nativeEvent.getClickCount(); + + int peAction = 0; + switch (nativeEvent.getID()) { + case java.awt.event.MouseEvent.MOUSE_PRESSED: + peAction = MouseEvent.PRESS; + break; + case java.awt.event.MouseEvent.MOUSE_RELEASED: + peAction = MouseEvent.RELEASE; + break; + case java.awt.event.MouseEvent.MOUSE_CLICKED: + peAction = MouseEvent.CLICK; + break; + case java.awt.event.MouseEvent.MOUSE_DRAGGED: + peAction = MouseEvent.DRAG; + break; + case java.awt.event.MouseEvent.MOUSE_MOVED: + peAction = MouseEvent.MOVE; + break; + case java.awt.event.MouseEvent.MOUSE_ENTERED: + peAction = MouseEvent.ENTER; + break; + case java.awt.event.MouseEvent.MOUSE_EXITED: + peAction = MouseEvent.EXIT; + break; + //case java.awt.event.MouseWheelEvent.WHEEL_UNIT_SCROLL: + case java.awt.event.MouseEvent.MOUSE_WHEEL: + peAction = MouseEvent.WHEEL; + /* + if (preciseWheelMethod != null) { + try { + peAmount = ((Double) preciseWheelMethod.invoke(nativeEvent, (Object[]) null)).floatValue(); + } catch (Exception e) { + preciseWheelMethod = null; + } + } + */ + peCount = ((MouseWheelEvent) nativeEvent).getWheelRotation(); + break; + } + + //System.out.println(nativeEvent); + //int modifiers = nativeEvent.getModifiersEx(); + // If using getModifiersEx(), the regular modifiers don't set properly. + int modifiers = nativeEvent.getModifiers(); + + int peModifiers = modifiers & + (InputEvent.SHIFT_MASK | + InputEvent.CTRL_MASK | + InputEvent.META_MASK | + InputEvent.ALT_MASK); + + // Windows and OS X seem to disagree on how to handle this. Windows only + // sets BUTTON1_DOWN_MASK, while OS X seems to set BUTTON1_MASK. + // This is an issue in particular with mouse release events: + // http://code.google.com/p/processing/issues/detail?id=1294 + // The fix for which led to a regression (fixed here by checking both): + // http://code.google.com/p/processing/issues/detail?id=1332 + int peButton = 0; +// if ((modifiers & InputEvent.BUTTON1_MASK) != 0 || +// (modifiers & InputEvent.BUTTON1_DOWN_MASK) != 0) { +// peButton = LEFT; +// } else if ((modifiers & InputEvent.BUTTON2_MASK) != 0 || +// (modifiers & InputEvent.BUTTON2_DOWN_MASK) != 0) { +// peButton = CENTER; +// } else if ((modifiers & InputEvent.BUTTON3_MASK) != 0 || +// (modifiers & InputEvent.BUTTON3_DOWN_MASK) != 0) { +// peButton = RIGHT; +// } + if ((modifiers & InputEvent.BUTTON1_MASK) != 0) { + peButton = PConstants.LEFT; + } else if ((modifiers & InputEvent.BUTTON2_MASK) != 0) { + peButton = PConstants.CENTER; + } else if ((modifiers & InputEvent.BUTTON3_MASK) != 0) { + peButton = PConstants.RIGHT; + } + + // If running on Mac OS, allow ctrl-click as right mouse. Prior to 0215, + // this used isPopupTrigger() on the native event, but that doesn't work + // for mouseClicked and mouseReleased (or others). + if (PApplet.platform == PConstants.MACOSX) { + //if (nativeEvent.isPopupTrigger()) { + if ((modifiers & InputEvent.CTRL_MASK) != 0) { + peButton = PConstants.RIGHT; + } + } + + sketch.postEvent(new MouseEvent(nativeEvent, nativeEvent.getWhen(), + peAction, peModifiers, + nativeEvent.getX() / windowScaleFactor, + nativeEvent.getY() / windowScaleFactor, + peButton, + peCount)); + } + + + protected void nativeKeyEvent(java.awt.event.KeyEvent event) { + int peAction = 0; + switch (event.getID()) { + case java.awt.event.KeyEvent.KEY_PRESSED: + peAction = KeyEvent.PRESS; + break; + case java.awt.event.KeyEvent.KEY_RELEASED: + peAction = KeyEvent.RELEASE; + break; + case java.awt.event.KeyEvent.KEY_TYPED: + peAction = KeyEvent.TYPE; + break; + } + +// int peModifiers = event.getModifiersEx() & +// (InputEvent.SHIFT_DOWN_MASK | +// InputEvent.CTRL_DOWN_MASK | +// InputEvent.META_DOWN_MASK | +// InputEvent.ALT_DOWN_MASK); + int peModifiers = event.getModifiers() & + (InputEvent.SHIFT_MASK | + InputEvent.CTRL_MASK | + InputEvent.META_MASK | + InputEvent.ALT_MASK); + + sketch.postEvent(new KeyEvent(event, event.getWhen(), + peAction, peModifiers, + event.getKeyChar(), event.getKeyCode())); + } + + + // listeners, for all my men! + protected void addListeners() { + + canvas.addMouseListener(new MouseListener() { + + public void mousePressed(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + + public void mouseReleased(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + + public void mouseClicked(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + + public void mouseEntered(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + + public void mouseExited(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + }); + + canvas.addMouseMotionListener(new MouseMotionListener() { + + public void mouseDragged(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + + public void mouseMoved(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + }); + + canvas.addMouseWheelListener(new MouseWheelListener() { + + public void mouseWheelMoved(MouseWheelEvent e) { + nativeMouseEvent(e); + } + }); + + canvas.addKeyListener(new KeyListener() { + + public void keyPressed(java.awt.event.KeyEvent e) { + nativeKeyEvent(e); + } + + + public void keyReleased(java.awt.event.KeyEvent e) { + nativeKeyEvent(e); + } + + + public void keyTyped(java.awt.event.KeyEvent e) { + nativeKeyEvent(e); + } + }); + + canvas.addFocusListener(new FocusListener() { + + public void focusGained(FocusEvent e) { + sketch.focused = true; + sketch.focusGained(); + } + + public void focusLost(FocusEvent e) { + sketch.focused = false; + sketch.focusLost(); + } + }); + } + + + /* + public void addListeners(Component comp) { + comp.addMouseListener(this); + comp.addMouseWheelListener(this); + comp.addMouseMotionListener(this); + comp.addKeyListener(this); + comp.addFocusListener(this); + } + + + public void removeListeners(Component comp) { + comp.removeMouseListener(this); + comp.removeMouseWheelListener(this); + comp.removeMouseMotionListener(this); + comp.removeKeyListener(this); + comp.removeFocusListener(this); + } + */ + + +// /** +// * Call to remove, then add, listeners to a component. +// * Avoids issues with double-adding. +// */ +// public void updateListeners(Component comp) { +// removeListeners(comp); +// addListeners(comp); +// } + + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + int cursorType = PConstants.ARROW; + boolean cursorVisible = true; + Cursor invisibleCursor; + + + @Override + public void setCursor(int kind) { + // Swap the HAND cursor because MOVE doesn't seem to be available on OS X + // https://github.com/processing/processing/issues/2358 + if (PApplet.platform == PConstants.MACOSX && kind == PConstants.MOVE) { + kind = PConstants.HAND; + } + canvas.setCursor(Cursor.getPredefinedCursor(kind)); + cursorVisible = true; + this.cursorType = kind; + } + + + @Override + public void setCursor(PImage img, int x, int y) { + // Don't set cursorType, instead use cursorType to save the last + // regular cursor type used for when cursor() is called. + //cursor_type = Cursor.CUSTOM_CURSOR; + + // this is a temporary workaround for the CHIP, will be removed + Dimension cursorSize = Toolkit.getDefaultToolkit().getBestCursorSize(img.width, img.height); + if (cursorSize.width == 0 || cursorSize.height == 0) { + return; + } + + Cursor cursor = + canvas.getToolkit().createCustomCursor((Image) img.getNative(), + new Point(x, y), + "custom"); + canvas.setCursor(cursor); + cursorVisible = true; + } + + + @Override + public void showCursor() { + // Maybe should always set here? Seems dangerous, since it's likely that + // Java will set the cursor to something else on its own, and the sketch + // will be stuck b/c p5 thinks the cursor is set to one particular thing. + if (!cursorVisible) { + cursorVisible = true; + canvas.setCursor(Cursor.getPredefinedCursor(cursorType)); + } + } + + + @Override + public void hideCursor() { + // Because the OS may have shown the cursor on its own, + // don't return if 'cursorVisible' is set to true. [rev 0216] + + if (invisibleCursor == null) { + BufferedImage cursorImg = + new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB); + // this is a temporary workaround for the CHIP, will be removed + Dimension cursorSize = Toolkit.getDefaultToolkit().getBestCursorSize(16, 16); + if (cursorSize.width == 0 || cursorSize.height == 0) { + invisibleCursor = Cursor.getDefaultCursor(); + } else { + invisibleCursor = + canvas.getToolkit().createCustomCursor(cursorImg, new Point(8, 8), "blank"); + } + } + canvas.setCursor(invisibleCursor); + cursorVisible = false; + } + + + @Override + public Thread createThread() { + return new AnimationThread() { + @Override + public void callDraw() { + sketch.handleDraw(); + render(); + } + }; + } + + + void debug(String format, Object ... args) { + System.out.format(format + "%n", args); + } +} diff --git a/src/main/java/processing/core/PApplet.java b/src/main/java/processing/core/PApplet.java new file mode 100644 index 0000000..ea45f20 --- /dev/null +++ b/src/main/java/processing/core/PApplet.java @@ -0,0 +1,15708 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + +// used by link() +import java.awt.Desktop; +import java.awt.DisplayMode; +import java.awt.EventQueue; +import java.awt.FileDialog; +import java.awt.Frame; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.HeadlessException; +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; + +// used by loadImage() functions +import javax.imageio.ImageIO; +// allows us to remove our own MediaTracker code +import javax.swing.ImageIcon; +// used by selectInput(), selectOutput(), selectFolder() +import javax.swing.JFileChooser; +import javax.swing.UIManager; +// used to present the fullScreen() warning about Spaces on OS X +import javax.swing.JOptionPane; +// used by desktopFile() method +import javax.swing.filechooser.FileSystemView; + +// loadXML() error handling +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import java.io.*; +import java.lang.reflect.*; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.text.*; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.regex.*; +import java.util.zip.*; + +import processing.data.*; +import processing.event.*; +import processing.opengl.*; + + +/** + * Base class for all sketches that use processing.core. + *

+ * The + * Window Size and Full Screen page on the Wiki has useful information + * about sizing, multiple displays, full screen, etc. + *

+ * Processing uses active mode rendering. All animation tasks happen on the + * "Processing Animation Thread". The setup() and draw() methods are handled + * by that thread, and events (like mouse movement and key presses, which are + * fired by the event dispatch thread or EDT) are queued to be safely handled + * at the end of draw(). + *

+ * Starting with 3.0a6, blit operations are on the EDT, so as not to cause + * GUI problems with Swing and AWT. In the case of the default renderer, the + * sketch renders to an offscreen image, then the EDT is asked to bring that + * image to the screen. + *

+ * For code that needs to run on the EDT, use EventQueue.invokeLater(). When + * doing so, be careful to synchronize between that code and the Processing + * animation thread. That is, you can't call Processing methods from the EDT + * or at any random time from another thread. Use of a callback function or + * the registerXxx() methods in PApplet can help ensure that your code doesn't + * do something naughty. + *

+ * As of Processing 3.0, we have removed Applet as the base class for PApplet. + * This means that we can remove lots of legacy code, however one downside is + * that it's no longer possible (without extra code) to embed a PApplet into + * another Java application. + *

+ * As of Processing 3.0, we have discontinued support for versions of Java + * prior to 1.8. We don't have enough people to support it, and for a + * project of our (tiny) size, we should be focusing on the future, rather + * than working around legacy Java code. + */ +public class PApplet implements PConstants { + /** Full name of the Java version (i.e. 1.5.0_11). */ + static public final String javaVersionName = + System.getProperty("java.version"); + + static public final int javaPlatform; + static { + String version = javaVersionName; + if (javaVersionName.startsWith("1.")) { + version = version.substring(2); + } + javaPlatform = parseInt(version.substring(0, version.indexOf('.'))); + } + + /** + * Do not use; javaPlatform or javaVersionName are better options. + * For instance, javaPlatform is useful when you need a number for + * comparison, i.e. "if (javaPlatform >= 9)". + * @deprecated + */ + @Deprecated + public static final float javaVersion = 1 + javaPlatform / 10f; + + /** + * Current platform in use, one of the + * PConstants WINDOWS, MACOSX, MACOS9, LINUX or OTHER. + */ + static public int platform; + + static { + String osname = System.getProperty("os.name"); + + if (osname.indexOf("Mac") != -1) { + platform = MACOSX; + + } else if (osname.indexOf("Windows") != -1) { + platform = WINDOWS; + + } else if (osname.equals("Linux")) { // true for the ibm vm + platform = LINUX; + + } else { + platform = OTHER; + } + } + + /** + * Whether to use native (AWT) dialogs for selectInput and selectOutput. + * The native dialogs on some platforms can be ugly, buggy, or missing + * features. For 3.3.5, this defaults to true on all platforms. + */ + static public boolean useNativeSelect = true; + + /** The PGraphics renderer associated with this PApplet */ + public PGraphics g; + + /** + * ( begin auto-generated from displayWidth.xml ) + * + * System variable which stores the width of the computer screen. For + * example, if the current screen resolution is 1024x768, + * displayWidth is 1024 and displayHeight is 768. These + * dimensions are useful when exporting full-screen applications. + *

+ * To ensure that the sketch takes over the entire screen, use "Present" + * instead of "Run". Otherwise the window will still have a frame border + * around it and not be placed in the upper corner of the screen. On Mac OS + * X, the menu bar will remain present unless "Present" mode is used. + * + * ( end auto-generated ) + */ + public int displayWidth; + + /** + * ( begin auto-generated from displayHeight.xml ) + * + * System variable that stores the height of the computer screen. For + * example, if the current screen resolution is 1024x768, + * displayWidth is 1024 and displayHeight is 768. These + * dimensions are useful when exporting full-screen applications. + *

+ * To ensure that the sketch takes over the entire screen, use "Present" + * instead of "Run". Otherwise the window will still have a frame border + * around it and not be placed in the upper corner of the screen. On Mac OS + * X, the menu bar will remain present unless "Present" mode is used. + * + * ( end auto-generated ) + */ + public int displayHeight; + + /** A leech graphics object that is echoing all events. */ + public PGraphics recorder; + + /** + * Command line options passed in from main(). + * This does not include the arguments passed in to PApplet itself. + * @see PApplet#main + */ + public String[] args; + + /** + * Path to sketch folder. Previously undocumented, made private in 3.0a5 + * so that people use the sketchPath() method and it's inited properly. + * Call sketchPath() once to set the default. + */ + private String sketchPath; +// public String sketchPath; + + static final boolean DEBUG = false; +// static final boolean DEBUG = true; + + /** Default width and height for sketch when not specified */ + static public final int DEFAULT_WIDTH = 100; + static public final int DEFAULT_HEIGHT = 100; + +// /** +// * Exception thrown when size() is called the first time. +// *

+// * This is used internally so that setup() is forced to run twice +// * when the renderer is changed. This is the only way for us to handle +// * invoking the new renderer while also in the midst of rendering. +// */ +// static public class RendererChangeException extends RuntimeException { } + + /** + * true if no size() command has been executed. This is used to wait until + * a size has been set before placing in the window and showing it. + */ +// public boolean defaultSize; + +// /** Storage for the current renderer size to avoid re-allocation. */ +// Dimension currentSize = new Dimension(); + + /** + * ( begin auto-generated from pixels.xml ) + * + * Array containing the values for all the pixels in the display window. + * These values are of the color datatype. This array is the size of the + * display window. For example, if the image is 100x100 pixels, there will + * be 10000 values and if the window is 200x300 pixels, there will be 60000 + * values. The index value defines the position of a value within + * the array. For example, the statement color b = pixels[230] will + * set the variable b to be equal to the value at that location in + * the array.
+ *
+ * Before accessing this array, the data must loaded with the + * loadPixels() function. After the array data has been modified, + * the updatePixels() function must be run to update the changes. + * Without loadPixels(), running the code may (or will in future + * releases) result in a NullPointerException. + * + * ( end auto-generated ) + * + * @webref image:pixels + * @see PApplet#loadPixels() + * @see PApplet#updatePixels() + * @see PApplet#get(int, int, int, int) + * @see PApplet#set(int, int, int) + * @see PImage + */ + public int[] pixels; + + /** + * ( begin auto-generated from width.xml ) + * + * System variable which stores the width of the display window. This value + * is set by the first parameter of the size() function. For + * example, the function call size(320, 240) sets the width + * variable to the value 320. The value of width is zero until + * size() is called. + * + * ( end auto-generated ) + * @webref environment + * @see PApplet#height + * @see PApplet#size(int, int) + */ + public int width = DEFAULT_WIDTH; + + /** + * ( begin auto-generated from height.xml ) + * + * System variable which stores the height of the display window. This + * value is set by the second parameter of the size() function. For + * example, the function call size(320, 240) sets the height + * variable to the value 240. The value of height is zero until + * size() is called. + * + * ( end auto-generated ) + * + * @webref environment + * @see PApplet#width + * @see PApplet#size(int, int) + */ + public int height = DEFAULT_HEIGHT; + + /** + * ( begin auto-generated from pixelWidth.xml ) + * + * When pixelDensity(2) is used to make use of a high resolution + * display (called a Retina display on OS X or high-dpi on Windows and + * Linux), the width and height of the sketch do not change, but the + * number of pixels is doubled. As a result, all operations that use pixels + * (like loadPixels(), get(), set(), etc.) happen + * in this doubled space. As a convenience, the variables pixelWidth + * and pixelHeight hold the actual width and height of the sketch + * in pixels. This is useful for any sketch that uses the pixels[] + * array, for instance, because the number of elements in the array will + * be pixelWidth*pixelHeight, not width*height. + * + * ( end auto-generated ) + * + * @webref environment + * @see PApplet#pixelHeight + * @see #pixelDensity(int) + * @see #displayDensity() + */ + public int pixelWidth; + + + /** + * ( begin auto-generated from pixelHeight.xml ) + * + * When pixelDensity(2) is used to make use of a high resolution + * display (called a Retina display on OS X or high-dpi on Windows and + * Linux), the width and height of the sketch do not change, but the + * number of pixels is doubled. As a result, all operations that use pixels + * (like loadPixels(), get(), set(), etc.) happen + * in this doubled space. As a convenience, the variables pixelWidth + * and pixelHeight hold the actual width and height of the sketch + * in pixels. This is useful for any sketch that uses the pixels[] + * array, for instance, because the number of elements in the array will + * be pixelWidth*pixelHeight, not width*height. + * + * ( end auto-generated ) + * + * @webref environment + * @see PApplet#pixelWidth + * @see #pixelDensity(int) + * @see #displayDensity() + */ + public int pixelHeight; + + /** + * Keeps track of ENABLE_KEY_REPEAT hint + */ + protected boolean keyRepeatEnabled = false; + + /** + * ( begin auto-generated from mouseX.xml ) + * + * The system variable mouseX always contains the current horizontal + * coordinate of the mouse. + * + * ( end auto-generated ) + * @webref input:mouse + * @see PApplet#mouseY + * @see PApplet#pmouseX + * @see PApplet#pmouseY + * @see PApplet#mousePressed + * @see PApplet#mousePressed() + * @see PApplet#mouseReleased() + * @see PApplet#mouseClicked() + * @see PApplet#mouseMoved() + * @see PApplet#mouseDragged() + * @see PApplet#mouseButton + * @see PApplet#mouseWheel(MouseEvent) + * + * + */ + public int mouseX; + + /** + * ( begin auto-generated from mouseY.xml ) + * + * The system variable mouseY always contains the current vertical + * coordinate of the mouse. + * + * ( end auto-generated ) + * @webref input:mouse + * @see PApplet#mouseX + * @see PApplet#pmouseX + * @see PApplet#pmouseY + * @see PApplet#mousePressed + * @see PApplet#mousePressed() + * @see PApplet#mouseReleased() + * @see PApplet#mouseClicked() + * @see PApplet#mouseMoved() + * @see PApplet#mouseDragged() + * @see PApplet#mouseButton + * @see PApplet#mouseWheel(MouseEvent) + * + */ + public int mouseY; + + /** + * ( begin auto-generated from pmouseX.xml ) + * + * The system variable pmouseX always contains the horizontal + * position of the mouse in the frame previous to the current frame.
+ *
+ * You may find that pmouseX and pmouseY have different + * values inside draw() and inside events like mousePressed() + * and mouseMoved(). This is because they're used for different + * roles, so don't mix them. Inside draw(), pmouseX and + * pmouseY update only once per frame (once per trip through your + * draw()). But, inside mouse events, they update each time the + * event is called. If they weren't separated, then the mouse would be read + * only once per frame, making response choppy. If the mouse variables were + * always updated multiple times per frame, using line(pmouseX, + * pmouseY, mouseX, mouseY) inside draw() would have lots + * of gaps, because pmouseX may have changed several times in + * between the calls to line(). Use pmouseX and + * pmouseY inside draw() if you want values relative to the + * previous frame. Use pmouseX and pmouseY inside the mouse + * functions if you want continuous response. + * + * ( end auto-generated ) + * @webref input:mouse + * @see PApplet#mouseX + * @see PApplet#mouseY + * @see PApplet#pmouseY + * @see PApplet#mousePressed + * @see PApplet#mousePressed() + * @see PApplet#mouseReleased() + * @see PApplet#mouseClicked() + * @see PApplet#mouseMoved() + * @see PApplet#mouseDragged() + * @see PApplet#mouseButton + * @see PApplet#mouseWheel(MouseEvent) + */ + public int pmouseX; + + /** + * ( begin auto-generated from pmouseY.xml ) + * + * The system variable pmouseY always contains the vertical position + * of the mouse in the frame previous to the current frame. More detailed + * information about how pmouseY is updated inside of draw() + * and mouse events is explained in the reference for pmouseX. + * + * ( end auto-generated ) + * @webref input:mouse + * @see PApplet#mouseX + * @see PApplet#mouseY + * @see PApplet#pmouseX + * @see PApplet#mousePressed + * @see PApplet#mousePressed() + * @see PApplet#mouseReleased() + * @see PApplet#mouseClicked() + * @see PApplet#mouseMoved() + * @see PApplet#mouseDragged() + * @see PApplet#mouseButton + * @see PApplet#mouseWheel(MouseEvent) + */ + public int pmouseY; + + /** + * Previous mouseX/Y for the draw loop, separated out because this is + * separate from the pmouseX/Y when inside the mouse event handlers. + * See emouseX/Y for an explanation. + */ + protected int dmouseX, dmouseY; + + /** + * The pmouseX/Y for the event handlers (mousePressed(), mouseDragged() etc) + * these are different because mouse events are queued to the end of + * draw, so the previous position has to be updated on each event, + * as opposed to the pmouseX/Y that's used inside draw, which is expected + * to be updated once per trip through draw(). + */ + protected int emouseX, emouseY; + + /** + * Used to set pmouseX/Y to mouseX/Y the first time mouseX/Y are used, + * otherwise pmouseX/Y are always zero, causing a nasty jump. + *

+ * Just using (frameCount == 0) won't work since mouseXxxxx() + * may not be called until a couple frames into things. + *

+ * @deprecated Please refrain from using this variable, it will be removed + * from future releases of Processing because it cannot be used consistently + * across platforms and input methods. + */ + @Deprecated + public boolean firstMouse = true; + + /** + * ( begin auto-generated from mouseButton.xml ) + * + * Processing automatically tracks if the mouse button is pressed and which + * button is pressed. The value of the system variable mouseButton + * is either LEFT, RIGHT, or CENTER depending on which + * button is pressed. + * + * ( end auto-generated ) + * + *

Advanced:

+ * + * If running on Mac OS, a ctrl-click will be interpreted as the right-hand + * mouse button (unlike Java, which reports it as the left mouse). + * @webref input:mouse + * @see PApplet#mouseX + * @see PApplet#mouseY + * @see PApplet#pmouseX + * @see PApplet#pmouseY + * @see PApplet#mousePressed + * @see PApplet#mousePressed() + * @see PApplet#mouseReleased() + * @see PApplet#mouseClicked() + * @see PApplet#mouseMoved() + * @see PApplet#mouseDragged() + * @see PApplet#mouseWheel(MouseEvent) + */ + public int mouseButton; + + /** + * ( begin auto-generated from mousePressed_var.xml ) + * + * Variable storing if a mouse button is pressed. The value of the system + * variable mousePressed is true if a mouse button is pressed and + * false if a button is not pressed. + * + * ( end auto-generated ) + * @webref input:mouse + * @see PApplet#mouseX + * @see PApplet#mouseY + * @see PApplet#pmouseX + * @see PApplet#pmouseY + * @see PApplet#mousePressed() + * @see PApplet#mouseReleased() + * @see PApplet#mouseClicked() + * @see PApplet#mouseMoved() + * @see PApplet#mouseDragged() + * @see PApplet#mouseButton + * @see PApplet#mouseWheel(MouseEvent) + */ + public boolean mousePressed; + + + /** @deprecated Use a mouse event handler that passes an event instead. */ + @Deprecated + public MouseEvent mouseEvent; + + /** + * ( begin auto-generated from key.xml ) + * + * The system variable key always contains the value of the most + * recent key on the keyboard that was used (either pressed or released). + *

+ * For non-ASCII keys, use the keyCode variable. The keys included + * in the ASCII specification (BACKSPACE, TAB, ENTER, RETURN, ESC, and + * DELETE) do not require checking to see if they key is coded, and you + * should simply use the key variable instead of keyCode If + * you're making cross-platform projects, note that the ENTER key is + * commonly used on PCs and Unix and the RETURN key is used instead on + * Macintosh. Check for both ENTER and RETURN to make sure your program + * will work for all platforms. + * + * ( end auto-generated ) + * + *

Advanced

+ * + * Last key pressed. + *

+ * If it's a coded key, i.e. UP/DOWN/CTRL/SHIFT/ALT, + * this will be set to CODED (0xffff or 65535). + * + * @webref input:keyboard + * @see PApplet#keyCode + * @see PApplet#keyPressed + * @see PApplet#keyPressed() + * @see PApplet#keyReleased() + */ + public char key; + + /** + * ( begin auto-generated from keyCode.xml ) + * + * The variable keyCode is used to detect special keys such as the + * UP, DOWN, LEFT, RIGHT arrow keys and ALT, CONTROL, SHIFT. When checking + * for these keys, it's first necessary to check and see if the key is + * coded. This is done with the conditional "if (key == CODED)" as shown in + * the example. + *

+ * The keys included in the ASCII specification (BACKSPACE, TAB, ENTER, + * RETURN, ESC, and DELETE) do not require checking to see if they key is + * coded, and you should simply use the key variable instead of + * keyCode If you're making cross-platform projects, note that the + * ENTER key is commonly used on PCs and Unix and the RETURN key is used + * instead on Macintosh. Check for both ENTER and RETURN to make sure your + * program will work for all platforms. + *

+ * For users familiar with Java, the values for UP and DOWN are simply + * shorter versions of Java's KeyEvent.VK_UP and KeyEvent.VK_DOWN. Other + * keyCode values can be found in the Java KeyEvent reference. + * + * ( end auto-generated ) + * + *

Advanced

+ * When "key" is set to CODED, this will contain a Java key code. + *

+ * For the arrow keys, keyCode will be one of UP, DOWN, LEFT and RIGHT. + * Also available are ALT, CONTROL and SHIFT. A full set of constants + * can be obtained from java.awt.event.KeyEvent, from the VK_XXXX variables. + * + * @webref input:keyboard + * @see PApplet#key + * @see PApplet#keyPressed + * @see PApplet#keyPressed() + * @see PApplet#keyReleased() + */ + public int keyCode; + + /** + * ( begin auto-generated from keyPressed_var.xml ) + * + * The boolean system variable keyPressed is true if any key + * is pressed and false if no keys are pressed. + * + * ( end auto-generated ) + * @webref input:keyboard + * @see PApplet#key + * @see PApplet#keyCode + * @see PApplet#keyPressed() + * @see PApplet#keyReleased() + */ + public boolean keyPressed; + List pressedKeys = new ArrayList<>(6); + + /** + * The last KeyEvent object passed into a mouse function. + * @deprecated Use a key event handler that passes an event instead. + */ + @Deprecated + public KeyEvent keyEvent; + + /** + * ( begin auto-generated from focused.xml ) + * + * Confirms if a Processing program is "focused", meaning that it is active + * and will accept input from mouse or keyboard. This variable is "true" if + * it is focused and "false" if not. This variable is often used when you + * want to warn people they need to click on or roll over an applet before + * it will work. + * + * ( end auto-generated ) + * @webref environment + */ + public boolean focused = false; + +// /** +// * Confirms if a Processing program is running inside a web browser. This +// * variable is "true" if the program is online and "false" if not. +// */ +// @Deprecated +// public boolean online = false; +// // This is deprecated because it's poorly named (and even more poorly +// // understood). Further, we'll probably be removing applets soon, in which +// // case this won't work at all. If you want this feature, you can check +// // whether getAppletContext() returns null. + + /** + * Time in milliseconds when the applet was started. + *

+ * Used by the millis() function. + */ + long millisOffset = System.currentTimeMillis(); + + /** + * ( begin auto-generated from frameRate_var.xml ) + * + * The system variable frameRate contains the approximate frame rate + * of the software as it executes. The initial value is 10 fps and is + * updated with each frame. The value is averaged (integrated) over several + * frames. As such, this value won't be valid until after 5-10 frames. + * + * ( end auto-generated ) + * @webref environment + * @see PApplet#frameRate(float) + * @see PApplet#frameCount + */ + public float frameRate = 10; + + protected boolean looping = true; + + /** flag set to true when a redraw is asked for by the user */ + protected boolean redraw = true; + + /** + * ( begin auto-generated from frameCount.xml ) + * + * The system variable frameCount contains the number of frames + * displayed since the program started. Inside setup() the value is + * 0 and and after the first iteration of draw it is 1, etc. + * + * ( end auto-generated ) + * @webref environment + * @see PApplet#frameRate(float) + * @see PApplet#frameRate + */ + public int frameCount; + + /** true if the sketch has stopped permanently. */ + public volatile boolean finished; + + /** used by the UncaughtExceptionHandler, so has to be static */ + static Throwable uncaughtThrowable; + + // public, but undocumented.. removing for 3.0a5 +// /** +// * true if the animation thread is paused. +// */ +// public volatile boolean paused; + + /** + * true if exit() has been called so that things shut down + * once the main thread kicks off. + */ + protected boolean exitCalled; + + // messages to send if attached as an external vm + + /** + * Position of the upper-lefthand corner of the editor window + * that launched this applet. + */ + static public final String ARGS_EDITOR_LOCATION = "--editor-location"; + + static public final String ARGS_EXTERNAL = "--external"; + + /** + * Location for where to position the applet window on screen. + *

+ * This is used by the editor to when saving the previous applet + * location, or could be used by other classes to launch at a + * specific position on-screen. + */ + static public final String ARGS_LOCATION = "--location"; + + /** Used by the PDE to suggest a display (set in prefs, passed on Run) */ + static public final String ARGS_DISPLAY = "--display"; + +// static public final String ARGS_SPAN_DISPLAYS = "--span"; + + static public final String ARGS_WINDOW_COLOR = "--window-color"; + + static public final String ARGS_PRESENT = "--present"; + + static public final String ARGS_STOP_COLOR = "--stop-color"; + + static public final String ARGS_HIDE_STOP = "--hide-stop"; + + /** + * Allows the user or PdeEditor to set a specific sketch folder path. + *

+ * Used by PdeEditor to pass in the location where saveFrame() + * and all that stuff should write things. + */ + static public final String ARGS_SKETCH_FOLDER = "--sketch-path"; + + static public final String ARGS_DENSITY = "--density"; + + /** + * When run externally to a PdeEditor, + * this is sent by the sketch when it quits. + */ + static public final String EXTERNAL_STOP = "__STOP__"; + + /** + * When run externally to a PDE Editor, this is sent by the applet + * whenever the window is moved. + *

+ * This is used so that the editor can re-open the sketch window + * in the same position as the user last left it. + */ + static public final String EXTERNAL_MOVE = "__MOVE__"; + + /** true if this sketch is being run by the PDE */ + boolean external = false; + + static final String ERROR_MIN_MAX = + "Cannot use min() or max() on an empty array."; + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + protected PSurface surface; + + + public PSurface getSurface() { + return surface; + } + + + /** + * A dummy frame to keep compatibility with 2.x code + * and encourage users to update. + */ + public Frame frame; + + +// public Frame getFrame() { +// return frame; +// } +// +// +// public void setFrame(Frame frame) { +// this.frame = frame; +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + +// /** +// * Applet initialization. This can do GUI work because the components have +// * not been 'realized' yet: things aren't visible, displayed, etc. +// */ +// public void init() { +//// println("init() called " + Integer.toHexString(hashCode())); +// // using a local version here since the class variable is deprecated +//// Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); +//// screenWidth = screen.width; +//// screenHeight = screen.height; +// +// defaultSize = true; +// finished = false; // just for clarity +// +// // this will be cleared by draw() if it is not overridden +// looping = true; +// redraw = true; // draw this guy at least once +// firstMouse = true; +// +// // calculated dynamically on first call +//// // Removed in 2.1.2, brought back for 2.1.3. Usually sketchPath is set +//// // inside runSketch(), but if this sketch takes care of calls to init() +//// // when PApplet.main() is not used (i.e. it's in a Java application). +//// // THe path needs to be set here so that loadXxxx() functions work. +//// if (sketchPath == null) { +//// sketchPath = calcSketchPath(); +//// } +// +// // set during Surface.initFrame() +//// // Figure out the available display width and height. +//// // No major problem if this fails, we have to try again anyway in +//// // handleDraw() on the first (== 0) frame. +//// checkDisplaySize(); +// +//// // Set the default size, until the user specifies otherwise +//// int w = sketchWidth(); +//// int h = sketchHeight(); +//// defaultSize = (w == DEFAULT_WIDTH) && (h == DEFAULT_HEIGHT); +//// +//// g = makeGraphics(w, h, sketchRenderer(), null, true); +//// // Fire component resize event +//// setSize(w, h); +//// setPreferredSize(new Dimension(w, h)); +//// +//// width = g.width; +//// height = g.height; +// +// surface.startThread(); +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + boolean insideSettings; + + String renderer = JAVA2D; +// int quality = 2; + int smooth = 1; // default smoothing (whatever that means for the renderer) + + boolean fullScreen; + int display = -1; // use default + GraphicsDevice[] displayDevices; + // Unlike the others above, needs to be public to support + // the pixelWidth and pixelHeight fields. + public int pixelDensity = 1; + int suggestedDensity = -1; + + boolean present; + + String outputPath; + OutputStream outputStream; + + // Background default needs to be different from the default value in + // PGraphics.backgroundColor, otherwise size(100, 100) bg spills over. + // https://github.com/processing/processing/issues/2297 + int windowColor = 0xffDDDDDD; + + + /** + * @param method "size" or "fullScreen" + * @param args parameters passed to the function so we can show the user + * @return true if safely inside the settings() method + */ + boolean insideSettings(String method, Object... args) { + if (insideSettings) { + return true; + } + final String url = "https://processing.org/reference/" + method + "_.html"; + if (!external) { // post a warning for users of Eclipse and other IDEs + StringList argList = new StringList(args); + System.err.println("When not using the PDE, " + method + "() can only be used inside settings()."); + System.err.println("Remove the " + method + "() method from setup(), and add the following:"); + System.err.println("public void settings() {"); + System.err.println(" " + method + "(" + argList.join(", ") + ");"); + System.err.println("}"); + } + throw new IllegalStateException(method + "() cannot be used here, see " + url); + } + + + void handleSettings() { + insideSettings = true; + + // Need the list of display devices to be queried already for usage below. + // https://github.com/processing/processing/issues/3295 + // https://github.com/processing/processing/issues/3296 + // Not doing this from a static initializer because it may cause + // PApplet to cache and the values to stick through subsequent runs. + // Instead make it a runtime thing and a local variable. + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice device = ge.getDefaultScreenDevice(); + displayDevices = ge.getScreenDevices(); + + // Default or unparsed will be -1, spanning will be 0, actual displays will + // be numbered from 1 because it's too weird to say "display 0" in prefs. + if (display > 0 && display <= displayDevices.length) { + device = displayDevices[display-1]; + } + // Set displayWidth and displayHeight for people still using those. + DisplayMode displayMode = device.getDisplayMode(); + displayWidth = displayMode.getWidth(); + displayHeight = displayMode.getHeight(); + + // Here's where size(), fullScreen(), smooth(N) and noSmooth() might + // be called, conjuring up the demons of various rendering configurations. + settings(); + + if (display == SPAN && platform == MACOSX) { + // Make sure "Displays have separate Spaces" is unchecked + // in System Preferences > Mission Control + Process p = exec("defaults", "read", "com.apple.spaces", "spans-displays"); + BufferedReader outReader = createReader(p.getInputStream()); + BufferedReader errReader = createReader(p.getErrorStream()); + StringBuilder stdout = new StringBuilder(); + StringBuilder stderr = new StringBuilder(); + String line = null; + try { + while ((line = outReader.readLine()) != null) { + stdout.append(line); + } + while ((line = errReader.readLine()) != null) { + stderr.append(line); + } + } catch (IOException e) { + printStackTrace(e); + } + + int resultCode = -1; + try { + resultCode = p.waitFor(); + } catch (InterruptedException e) { } + + String result = trim(stdout.toString()); + if ("0".equals(result)) { + EventQueue.invokeLater(new Runnable() { + public void run() { + checkLookAndFeel(); + final String msg = + "To use fullScreen(SPAN), first turn off “Displays have separate spaces”\n" + + "in System Preferences \u2192 Mission Control. Then log out and log back in."; + JOptionPane.showMessageDialog(null, msg, "Apple's Defaults Stink", + JOptionPane.WARNING_MESSAGE); + } + }); + } else if (!"1".equals(result)) { + System.err.println("Could not check the status of “Displays have separate spaces.”"); + System.err.format("Received message '%s' and result code %d.%n", trim(stderr.toString()), resultCode); + } + } + + insideSettings = false; + } + + + /** + * ( begin auto-generated from settings.xml ) + * + * Description to come... + * + * ( end auto-generated ) + * + * Override this method to call size() when not using the PDE. + * + * @webref environment + * @see PApplet#fullScreen() + * @see PApplet#setup() + * @see PApplet#size(int,int) + * @see PApplet#smooth() + */ + public void settings() { + // is this necessary? (doesn't appear to be, so removing) + //size(DEFAULT_WIDTH, DEFAULT_HEIGHT, JAVA2D); + } + + + final public int sketchWidth() { + return width; + } + + + final public int sketchHeight() { + return height; + } + + + final public String sketchRenderer() { + return renderer; + } + + + // Named quality instead of smooth to avoid people trying to set (or get) + // the current smooth level this way. Also that smooth(number) isn't really + // public or well-known API. It's specific to the capabilities of the + // rendering surface, and somewhat independent of whether the sketch is + // smoothing at any given time. It's also a bit like getFill() would return + // true/false for whether fill was enabled, getFillColor() would return the + // color itself. Or at least that's what I can recall at the moment. [fry] +// public int sketchQuality() { +// //return 2; +// return quality; +// } + // smoothing 1 is default.. 0 is none.. 2,4,8 depend on renderer + final public int sketchSmooth() { + return smooth; + } + + + final public boolean sketchFullScreen() { + //return false; + return fullScreen; + } + + +// // Could be named 'screen' instead of display since it's the people using +// // full screen who will be looking for it. On the other hand, screenX/Y/Z +// // makes things confusing, and if 'displayIndex' exists... +// public boolean sketchSpanDisplays() { +// //return false; +// return spanDisplays; +// } + + + // Numbered from 1, SPAN (0) means all displays, -1 means the default display + final public int sketchDisplay() { + return display; + } + + + final public String sketchOutputPath() { + //return null; + return outputPath; + } + + + final public OutputStream sketchOutputStream() { + //return null; + return outputStream; + } + + + final public int sketchWindowColor() { + return windowColor; + } + + + final public int sketchPixelDensity() { + return pixelDensity; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + /** + * ( begin auto-generated from displayDensity.xml ) + * + * This function returns the number "2" if the screen is a high-density + * screen (called a Retina display on OS X or high-dpi on Windows and Linux) + * and a "1" if not. This information is useful for a program to adapt to + * run at double the pixel density on a screen that supports it. + * + * ( end auto-generated ) + * + * @webref environment + * @see PApplet#pixelDensity(int) + * @see PApplet#size(int,int) + */ + public int displayDensity() { + if (display != SPAN && (fullScreen || present)) { + return displayDensity(display); + } + // walk through all displays, use 2 if any display is 2 + for (int i = 0; i < displayDevices.length; i++) { + if (displayDensity(i+1) == 2) { + return 2; + } + } + // If nobody's density is 2 then everyone is 1 + return 1; + } + + /** + * @param display the display number to check + */ + public int displayDensity(int display) { + if (PApplet.platform == PConstants.MACOSX) { + // This should probably be reset each time there's a display change. + // A 5-minute search didn't turn up any such event in the Java 7 API. + // Also, should we use the Toolkit associated with the editor window? + final String javaVendor = System.getProperty("java.vendor"); + if (javaVendor.contains("Oracle")) { + GraphicsDevice device; + GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); + + if (display == -1) { + device = env.getDefaultScreenDevice(); + + } else if (display == SPAN) { + throw new RuntimeException("displayDensity() only works with specific display numbers"); + + } else { + GraphicsDevice[] devices = env.getScreenDevices(); + if (display > 0 && display <= devices.length) { + device = devices[display - 1]; + } else { + if (devices.length == 1) { + System.err.println("Only one display is currently known, use displayDensity(1)."); + } else { + System.err.format("Your displays are numbered %d through %d, " + + "pass one of those numbers to displayDensity()%n", 1, devices.length); + } + throw new RuntimeException("Display " + display + " does not exist."); + } + } + + try { + Field field = device.getClass().getDeclaredField("scale"); + if (field != null) { + field.setAccessible(true); + Object scale = field.get(device); + + if (scale instanceof Integer && ((Integer)scale).intValue() == 2) { + return 2; + } + } + } catch (Exception ignore) { } + } + } else if (PApplet.platform == PConstants.WINDOWS || + PApplet.platform == PConstants.LINUX) { + if (suggestedDensity == -1) { + // TODO: detect and return DPI scaling using JNA; Windows has + // a system-wide value, not sure how it works on Linux + return 1; + } else if (suggestedDensity == 1 || suggestedDensity == 2) { + return suggestedDensity; + } + } + return 1; + } + + + /** + * @webref environment + * @param density 1 or 2 + * + */ + public void pixelDensity(int density) { + //println(density + " " + this.pixelDensity); + if (density != this.pixelDensity) { + if (insideSettings("pixelDensity", density)) { + if (density != 1 && density != 2) { + throw new RuntimeException("pixelDensity() can only be 1 or 2"); + } + if (!FX2D.equals(renderer) && density == 2 && displayDensity() == 1) { + // FX has its own check in PSurfaceFX + // Don't throw exception because the sketch should still work + System.err.println("pixelDensity(2) is not available for this display"); + this.pixelDensity = 1; + } else { + this.pixelDensity = density; + } + } else { + System.err.println("not inside settings"); + // this should only be reachable when not running in the PDE, + // so saying it's a settings()--not just setup()--issue should be ok + throw new RuntimeException("pixelDensity() can only be used inside settings()"); + } + } + } + + + /** + * Called by PSurface objects to set the width and height variables, + * and update the pixelWidth and pixelHeight variables. + */ + public void setSize(int width, int height) { + this.width = width; + this.height = height; + pixelWidth = width * pixelDensity; + pixelHeight = height * pixelDensity; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * @nowebref + */ + public void smooth() { + smooth(1); + } + + /** + * @webref environment + * @param level either 2, 3, 4, or 8 depending on the renderer + */ + public void smooth(int level) { + if (insideSettings) { + this.smooth = level; + + } else if (this.smooth != level) { + smoothWarning("smooth"); + } + } + + /** + * @webref environment + */ + public void noSmooth() { + if (insideSettings) { + this.smooth = 0; + + } else if (this.smooth != 0) { + smoothWarning("noSmooth"); + } + } + + + private void smoothWarning(String method) { + // When running from the PDE, say setup(), otherwise say settings() + final String where = external ? "setup" : "settings"; + PGraphics.showWarning("%s() can only be used inside %s()", method, where); + if (external) { + PGraphics.showWarning("When run from the PDE, %s() is automatically moved from setup() to settings()", method); + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public PGraphics getGraphics() { + return g; + } + + + // TODO should this join the sketchXxxx() functions specific to settings()? + public void orientation(int which) { + // ignore calls to the orientation command + } + + + /** + * Called by the browser or applet viewer to inform this applet that it + * should start its execution. It is called after the init method and + * each time the applet is revisited in a Web page. + *

+ * Called explicitly via the first call to PApplet.paint(), because + * PAppletGL needs to have a usable screen before getting things rolling. + */ + public void start() { +// paused = false; // unpause the thread // removing for 3.0a5, don't think we want this here + + resume(); + handleMethods("resume"); + surface.resumeThread(); + } + + + /** + * Called by the browser or applet viewer to inform + * this applet that it should stop its execution. + *

+ * Unfortunately, there are no guarantees from the Java spec + * when or if stop() will be called (i.e. on browser quit, + * or when moving between web pages), and it's not always called. + */ + public void stop() { + // this used to shut down the sketch, but that code has + // been moved to destroy/dispose() + +// if (paused) { +// synchronized (pauseObject) { +// try { +// pauseObject.wait(); +// } catch (InterruptedException e) { +// // waiting for this interrupt on a start() (resume) call +// } +// } +// } + + //paused = true; // causes animation thread to sleep // 3.0a5 + pause(); + handleMethods("pause"); + // calling this down here, since it's another thread it's safer to call + // pause() and the registered pause methods first. + surface.pauseThread(); + + // actual pause will happen in the run() method + +// synchronized (pauseObject) { +// debug("stop() calling pauseObject.wait()"); +// try { +// pauseObject.wait(); +// } catch (InterruptedException e) { +// // waiting for this interrupt on a start() (resume) call +// } +// } + } + + + /** + * Sketch has been paused. Called when switching tabs in a browser or + * swapping to a different application on Android. Also called just before + * quitting. Use to safely disable things like serial, sound, or sensors. + */ + public void pause() { } + + + /** + * Sketch has resumed. Called when switching tabs in a browser or + * swapping to this application on Android. Also called on startup. + * Use this to safely disable things like serial, sound, or sensors. + */ + public void resume() { } + + +// /** +// * Called by the browser or applet viewer to inform this applet +// * that it is being reclaimed and that it should destroy +// * any resources that it has allocated. +// *

+// * destroy() supposedly gets called as the applet viewer +// * is shutting down the applet. stop() is called +// * first, and then destroy() to really get rid of things. +// * no guarantees on when they're run (on browser quit, or +// * when moving between pages), though. +// */ +// @Override +// public void destroy() { +// this.dispose(); +// } + + + ////////////////////////////////////////////////////////////// + + + /** Map of registered methods, stored by name. */ + HashMap registerMap = + new HashMap<>(); + + /** Lock when un/registering from multiple threads */ + private final Object registerLock = new Object[0]; + + + class RegisteredMethods { + int count; + Object[] objects; + // Because the Method comes from the class being called, + // it will be unique for most, if not all, objects. + Method[] methods; + Object[] emptyArgs = new Object[] { }; + + + void handle() { + handle(emptyArgs); + } + + + void handle(Object[] args) { + for (int i = 0; i < count; i++) { + try { + methods[i].invoke(objects[i], args); + } catch (Exception e) { + // check for wrapped exception, get root exception + Throwable t; + if (e instanceof InvocationTargetException) { + InvocationTargetException ite = (InvocationTargetException) e; + t = ite.getCause(); + } else { + t = e; + } + // check for RuntimeException, and allow to bubble up + if (t instanceof RuntimeException) { + // re-throw exception + throw (RuntimeException) t; + } else { + // trap and print as usual + printStackTrace(t); + } + } + } + } + + + void add(Object object, Method method) { + if (findIndex(object) == -1) { + if (objects == null) { + objects = new Object[5]; + methods = new Method[5]; + + } else if (count == objects.length) { + objects = (Object[]) PApplet.expand(objects); + methods = (Method[]) PApplet.expand(methods); + } + objects[count] = object; + methods[count] = method; + count++; + } else { + die(method.getName() + "() already added for this instance of " + + object.getClass().getName()); + } + } + + + /** + * Removes first object/method pair matched (and only the first, + * must be called multiple times if object is registered multiple times). + * Does not shrink array afterwards, silently returns if method not found. + */ +// public void remove(Object object, Method method) { +// int index = findIndex(object, method); + public void remove(Object object) { + int index = findIndex(object); + if (index != -1) { + // shift remaining methods by one to preserve ordering + count--; + for (int i = index; i < count; i++) { + objects[i] = objects[i+1]; + methods[i] = methods[i+1]; + } + // clean things out for the gc's sake + objects[count] = null; + methods[count] = null; + } + } + + +// protected int findIndex(Object object, Method method) { + protected int findIndex(Object object) { + for (int i = 0; i < count; i++) { + if (objects[i] == object) { +// if (objects[i] == object && methods[i].equals(method)) { + //objects[i].equals() might be overridden, so use == for safety + // since here we do care about actual object identity + //methods[i]==method is never true even for same method, so must use + // equals(), this should be safe because of object identity + return i; + } + } + return -1; + } + } + + + /** + * Register a built-in event so that it can be fired for libraries, etc. + * Supported events include: + *

    + *
  • pre – at the very top of the draw() method (safe to draw) + *
  • draw – at the end of the draw() method (safe to draw) + *
  • post – after draw() has exited (not safe to draw) + *
  • pause – called when the sketch is paused + *
  • resume – called when the sketch is resumed + *
  • dispose – when the sketch is shutting down (definitely not safe to draw) + *
      + * In addition, the new (for 2.0) processing.event classes are passed to + * the following event types: + *
        + *
      • mouseEvent + *
      • keyEvent + *
      • touchEvent + *
      + * The older java.awt events are no longer supported. + * See the Library Wiki page for more details. + * @param methodName name of the method to be called + * @param target the target object that should receive the event + */ + public void registerMethod(String methodName, Object target) { + if (methodName.equals("mouseEvent")) { + registerWithArgs("mouseEvent", target, new Class[] { processing.event.MouseEvent.class }); + + } else if (methodName.equals("keyEvent")) { + registerWithArgs("keyEvent", target, new Class[] { processing.event.KeyEvent.class }); + + } else if (methodName.equals("touchEvent")) { + registerWithArgs("touchEvent", target, new Class[] { processing.event.TouchEvent.class }); + + } else { + registerNoArgs(methodName, target); + } + } + + + private void registerNoArgs(String name, Object o) { + Class c = o.getClass(); + try { + Method method = c.getMethod(name); + synchronized (registerLock) { + RegisteredMethods meth = registerMap.get(name); + if (meth == null) { + meth = new RegisteredMethods(); + registerMap.put(name, meth); + } + meth.add(o, method); + } + } catch (NoSuchMethodException nsme) { + die("There is no public " + name + "() method in the class " + + o.getClass().getName()); + + } catch (Exception e) { + die("Could not register " + name + " + () for " + o, e); + } + } + + + private void registerWithArgs(String name, Object o, Class cargs[]) { + Class c = o.getClass(); + try { + Method method = c.getMethod(name, cargs); + synchronized (registerLock) { + RegisteredMethods meth = registerMap.get(name); + if (meth == null) { + meth = new RegisteredMethods(); + registerMap.put(name, meth); + } + meth.add(o, method); + } + } catch (NoSuchMethodException nsme) { + die("There is no public " + name + "() method in the class " + + o.getClass().getName()); + + } catch (Exception e) { + die("Could not register " + name + " + () for " + o, e); + } + } + + +// public void registerMethod(String methodName, Object target, Object... args) { +// registerWithArgs(methodName, target, args); +// } + + + public void unregisterMethod(String name, Object target) { + synchronized (registerLock) { + RegisteredMethods meth = registerMap.get(name); + if (meth == null) { + die("No registered methods with the name " + name + "() were found."); + } + try { +// Method method = o.getClass().getMethod(name, new Class[] {}); +// meth.remove(o, method); + meth.remove(target); + } catch (Exception e) { + die("Could not unregister " + name + "() for " + target, e); + } + } + } + + + protected void handleMethods(String methodName) { + synchronized (registerLock) { + RegisteredMethods meth = registerMap.get(methodName); + if (meth != null) { + meth.handle(); + } + } + } + + + protected void handleMethods(String methodName, Object[] args) { + synchronized (registerLock) { + RegisteredMethods meth = registerMap.get(methodName); + if (meth != null) { + meth.handle(args); + } + } + } + + + /* + @Deprecated + public void registerSize(Object o) { + System.err.println("The registerSize() command is no longer supported."); +// Class methodArgs[] = new Class[] { Integer.TYPE, Integer.TYPE }; +// registerWithArgs(sizeMethods, "size", o, methodArgs); + } + + + @Deprecated + public void registerPre(Object o) { + registerNoArgs("pre", o); + } + + + @Deprecated + public void registerDraw(Object o) { + registerNoArgs("draw", o); + } + + + @Deprecated + public void registerPost(Object o) { + registerNoArgs("post", o); + } + + + @Deprecated + public void registerDispose(Object o) { + registerNoArgs("dispose", o); + } + + + @Deprecated + public void unregisterSize(Object o) { + System.err.println("The unregisterSize() command is no longer supported."); +// Class methodArgs[] = new Class[] { Integer.TYPE, Integer.TYPE }; +// unregisterWithArgs(sizeMethods, "size", o, methodArgs); + } + + + @Deprecated + public void unregisterPre(Object o) { + unregisterMethod("pre", o); + } + + + @Deprecated + public void unregisterDraw(Object o) { + unregisterMethod("draw", o); + } + + + @Deprecated + public void unregisterPost(Object o) { + unregisterMethod("post", o); + } + + + @Deprecated + public void unregisterDispose(Object o) { + unregisterMethod("dispose", o); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + // Old methods with AWT API that should not be used. + // These were never implemented on Android so they're stored separately. + + RegisteredMethods mouseEventMethods, keyEventMethods; + + + protected void reportDeprecation(Class c, boolean mouse) { + if (g != null) { + PGraphics.showWarning("The class " + c.getName() + + " is incompatible with Processing 2.0."); + PGraphics.showWarning("A library (or other code) is using register" + + (mouse ? "Mouse" : "Key") + "Event() " + + "which is no longer available."); + // This will crash with OpenGL, so quit anyway + if (g instanceof PGraphicsOpenGL) { + PGraphics.showWarning("Stopping the sketch because this code will " + + "not work correctly with OpenGL."); + throw new RuntimeException("This sketch uses a library that " + + "needs to be updated for Processing 2.0."); + } + } + } + + + @Deprecated + public void registerMouseEvent(Object o) { + Class c = o.getClass(); + reportDeprecation(c, true); + try { + Method method = c.getMethod("mouseEvent", new Class[] { java.awt.event.MouseEvent.class }); + if (mouseEventMethods == null) { + mouseEventMethods = new RegisteredMethods(); + } + mouseEventMethods.add(o, method); + } catch (Exception e) { + die("Could not register mouseEvent() for " + o, e); + } + } + + + @Deprecated + public void unregisterMouseEvent(Object o) { + try { +// Method method = o.getClass().getMethod("mouseEvent", new Class[] { MouseEvent.class }); +// mouseEventMethods.remove(o, method); + mouseEventMethods.remove(o); + } catch (Exception e) { + die("Could not unregister mouseEvent() for " + o, e); + } + } + + + @Deprecated + public void registerKeyEvent(Object o) { + Class c = o.getClass(); + reportDeprecation(c, false); + try { + Method method = c.getMethod("keyEvent", new Class[] { java.awt.event.KeyEvent.class }); + if (keyEventMethods == null) { + keyEventMethods = new RegisteredMethods(); + } + keyEventMethods.add(o, method); + } catch (Exception e) { + die("Could not register keyEvent() for " + o, e); + } + } + + + @Deprecated + public void unregisterKeyEvent(Object o) { + try { +// Method method = o.getClass().getMethod("keyEvent", new Class[] { KeyEvent.class }); +// keyEventMethods.remove(o, method); + keyEventMethods.remove(o); + } catch (Exception e) { + die("Could not unregister keyEvent() for " + o, e); + } + } + */ + + + + ////////////////////////////////////////////////////////////// + +/** + * ( begin auto-generated from setup.xml ) + * + * The setup() function is called once when the program starts. It's + * used to define initial + * enviroment properties such as screen size and background color and to + * load media such as images + * and fonts as the program starts. There can only be one setup() + * function for each program and + * it shouldn't be called again after its initial execution. Note: + * Variables declared within + * setup() are not accessible within other functions, including + * draw(). + * + * ( end auto-generated ) + * @webref structure + * @usage web_application + * @see PApplet#size(int, int) + * @see PApplet#loop() + * @see PApplet#noLoop() + * @see PApplet#draw() + */ + public void setup() { + } + +/** + * ( begin auto-generated from draw.xml ) + * + * Called directly after setup() and continuously executes the lines + * of code contained inside its block until the program is stopped or + * noLoop() is called. The draw() function is called + * automatically and should never be called explicitly. It should always be + * controlled with noLoop(), redraw() and loop(). + * After noLoop() stops the code in draw() from executing, + * redraw() causes the code inside draw() to execute once and + * loop() will causes the code inside draw() to execute + * continuously again. The number of times draw() executes in each + * second may be controlled with frameRate() function. + * There can only be one draw() function for each sketch + * and draw() must exist if you want the code to run continuously or + * to process events such as mousePressed(). Sometimes, you might + * have an empty call to draw() in your program as shown in the + * above example. + * + * ( end auto-generated ) + * @webref structure + * @usage web_application + * @see PApplet#setup() + * @see PApplet#loop() + * @see PApplet#noLoop() + * @see PApplet#redraw() + * @see PApplet#frameRate(float) + * @see PGraphics#background(float, float, float, float) + */ + public void draw() { + // if no draw method, then shut things down + //System.out.println("no draw method, goodbye"); + finished = true; + } + + + ////////////////////////////////////////////////////////////// + + + /* + protected void resizeRenderer(int newWidth, int newHeight) { + debug("resizeRenderer request for " + newWidth + " " + newHeight); + if (width != newWidth || height != newHeight) { + debug(" former size was " + width + " " + height); + g.setSize(newWidth, newHeight); + width = newWidth; + height = newHeight; + } + } + */ + + + /** + * Create a full-screen sketch using the default renderer. + */ + public void fullScreen() { + if (!fullScreen) { + if (insideSettings("fullScreen")) { + this.fullScreen = true; + } + } + } + + + public void fullScreen(int display) { + if (!fullScreen || display != this.display) { + if (insideSettings("fullScreen", display)) { + this.fullScreen = true; + this.display = display; + } + } + } + + +/** + * ( begin auto-generated from fullScreen.xml ) + * + * Description to come... + * + * ( end auto-generated ) + * @webref environment + * @param renderer the renderer to use, e.g. P2D, P3D, JAVA2D (default) + * @see PApplet#settings() + * @see PApplet#setup() + * @see PApplet#size(int,int) + * @see PApplet#smooth() + */ + public void fullScreen(String renderer) { + if (!fullScreen || + !renderer.equals(this.renderer)) { + if (insideSettings("fullScreen", renderer)) { + this.fullScreen = true; + this.renderer = renderer; + } + } + } + + + /** + * @param display the screen to run the sketch on (1, 2, 3, etc. or on multiple screens using SPAN) + */ + + public void fullScreen(String renderer, int display) { + if (!fullScreen || + !renderer.equals(this.renderer) || + display != this.display) { + if (insideSettings("fullScreen", renderer, display)) { + this.fullScreen = true; + this.renderer = renderer; + this.display = display; + } + } + } + + + /** + * ( begin auto-generated from size.xml ) + * + * Defines the dimension of the display window in units of pixels. The + * size() function must be the first line in setup(). If + * size() is not used, the default size of the window is 100x100 + * pixels. The system variables width and height are set by + * the parameters passed to this function.
      + *
      + * Do not use variables as the parameters to size() function, + * because it will cause problems when exporting your sketch. When + * variables are used, the dimensions of your sketch cannot be determined + * during export. Instead, employ numeric values in the size() + * statement, and then use the built-in width and height + * variables inside your program when the dimensions of the display window + * are needed.
      + *
      + * The size() function can only be used once inside a sketch, and + * cannot be used for resizing.
      + *
      renderer parameter selects which rendering engine to use. + * For example, if you will be drawing 3D shapes, use P3D, if you + * want to export images from a program as a PDF file use PDF. A + * brief description of the three primary renderers follows:
      + *
      + * P2D (Processing 2D) - The default renderer that supports two + * dimensional drawing.
      + *
      + * P3D (Processing 3D) - 3D graphics renderer that makes use of + * OpenGL-compatible graphics hardware.
      + *
      + * PDF - The PDF renderer draws 2D graphics directly to an Acrobat + * PDF file. This produces excellent results when you need vector shapes + * for high resolution output or printing. You must first use Import + * Library → PDF to make use of the library. More information can be + * found in the PDF library reference.
      + *
      + * The P3D renderer doesn't support strokeCap() or + * strokeJoin(), which can lead to ugly results when using + * strokeWeight(). (Issue + * 123)
      + *
      + * The maximum width and height is limited by your operating system, and is + * usually the width and height of your actual screen. On some machines it + * may simply be the number of pixels on your current screen, meaning that + * a screen of 800x600 could support size(1600, 300), since it's the + * same number of pixels. This varies widely so you'll have to try + * different rendering modes and sizes until you get what you're looking + * for. If you need something larger, use createGraphics to create a + * non-visible drawing surface.
      + *
      + * Again, the size() function must be the first line of the code (or + * first item inside setup). Any code that appears before the size() + * command may run more than once, which can lead to confusing results. + * + * ( end auto-generated ) + * + *

      Advanced

      + * If using Java 1.3 or later, this will default to using + * PGraphics2, the Java2D-based renderer. If using Java 1.1, + * or if PGraphics2 is not available, then PGraphics will be used. + * To set your own renderer, use the other version of the size() + * method that takes a renderer as its last parameter. + *

      + * If called once a renderer has already been set, this will + * use the previous renderer and simply resize it. + * + * @webref environment + * @param width width of the display window in units of pixels + * @param height height of the display window in units of pixels + * @see PApplet#width + * @see PApplet#height + */ + public void size(int width, int height) { + // Check to make sure the width/height have actually changed. It's ok to + // have size() duplicated (and may be better to not remove it from where + // it sits in the code anyway when adding it to settings()). Only take + // action if things have changed. + if (width != this.width || + height != this.height) { + if (insideSettings("size", width, height)) { + this.width = width; + this.height = height; + } + } + } + + + public void size(int width, int height, String renderer) { + if (width != this.width || + height != this.height || + !renderer.equals(this.renderer)) { + //println(width, height, renderer, this.width, this.height, this.renderer); + if (insideSettings("size", width, height, "\"" + renderer + "\"")) { + this.width = width; + this.height = height; + this.renderer = renderer; + } + } + } + + + /** + * @nowebref + */ + public void size(int width, int height, String renderer, String path) { + // Don't bother checking path, it's probably been modified to absolute, + // so it would always trigger. But the alternative is comparing the + // canonical file, which seems overboard. + if (width != this.width || + height != this.height || + !renderer.equals(this.renderer)) { + if (insideSettings("size", width, height, "\"" + renderer + "\"", + "\"" + path + "\"")) { + this.width = width; + this.height = height; + this.renderer = renderer; + this.outputPath = path; + } + } + + /* + if (!renderer.equals(sketchRenderer())) { + if (external) { + // The PDE should have parsed it, but something still went wrong + final String msg = + String.format("Something bad happened when calling " + + "size(%d, %d, %s, %s)", w, h, renderer, path); + throw new RuntimeException(msg); + + } else { + System.err.println("Because you're not running from the PDE, add this to your code:"); + System.err.println("public String sketchRenderer() {"); + System.err.println(" return \"" + renderer + "\";"); + System.err.println("}"); + throw new RuntimeException("The sketchRenderer() method is not implemented."); + } + } + */ + + // size() shouldn't actually do anything here [3.0a8] +// surface.setSize(w, h); + // this won't be absolute, which will piss off PDF [3.0a8] +// g.setPath(path); // finally, a path + +// // Run this from the EDT, just cuz it's AWT stuff (or maybe later Swing) +// EventQueue.invokeLater(new Runnable() { +// public void run() { +// // Set the preferred size so that the layout managers can handle it +// setPreferredSize(new Dimension(w, h)); +// setSize(w, h); +// } +// }); +// +// // ensure that this is an absolute path +// if (path != null) path = savePath(path); +// +// String currentRenderer = g.getClass().getName(); +// if (currentRenderer.equals(renderer)) { +//// // Avoid infinite loop of throwing exception to reset renderer +//// resizeRenderer(w, h); +// surface.setSize(w, h); +// +// } else { // renderer change attempted +// // no longer kosher with 3.0a5 +// throw new RuntimeException("Y'all need to implement sketchRenderer()"); +// /* +// // otherwise ok to fall through and create renderer below +// // the renderer is changing, so need to create a new object +// g = makeGraphics(w, h, renderer, path, true); +// this.width = w; +// this.height = h; +// +// // fire resize event to make sure the applet is the proper size +//// setSize(iwidth, iheight); +// // this is the function that will run if the user does their own +// // size() command inside setup, so set defaultSize to false. +// defaultSize = false; +// +// // throw an exception so that setup() is called again +// // but with a properly sized render +// // this is for opengl, which needs a valid, properly sized +// // display before calling anything inside setup(). +// throw new RendererChangeException(); +// */ +// } + } + + + public PGraphics createGraphics(int w, int h) { + return createGraphics(w, h, JAVA2D); + } + + + /** + * ( begin auto-generated from createGraphics.xml ) + * + * Creates and returns a new PGraphics object of the types P2D or + * P3D. Use this class if you need to draw into an off-screen graphics + * buffer. The PDF renderer requires the filename parameter. The DXF + * renderer should not be used with createGraphics(), it's only + * built for use with beginRaw() and endRaw().
      + *
      + * It's important to call any drawing functions between beginDraw() + * and endDraw() statements. This is also true for any functions + * that affect drawing, such as smooth() or colorMode().
      + *
      the main drawing surface which is completely opaque, surfaces + * created with createGraphics() can have transparency. This makes + * it possible to draw into a graphics and maintain the alpha channel. By + * using save() to write a PNG or TGA file, the transparency of the + * graphics object will be honored. Note that transparency levels are + * binary: pixels are either complete opaque or transparent. For the time + * being, this means that text characters will be opaque blocks. This will + * be fixed in a future release (Issue 80). + * + * ( end auto-generated ) + *

      Advanced

      + * Create an offscreen PGraphics object for drawing. This can be used + * for bitmap or vector images drawing or rendering. + *
        + *
      • Do not use "new PGraphicsXxxx()", use this method. This method + * ensures that internal variables are set up properly that tie the + * new graphics context back to its parent PApplet. + *
      • The basic way to create bitmap images is to use the saveFrame() + * function. + *
      • If you want to create a really large scene and write that, + * first make sure that you've allocated a lot of memory in the Preferences. + *
      • If you want to create images that are larger than the screen, + * you should create your own PGraphics object, draw to that, and use + * save(). + *
        +   *
        +   * PGraphics big;
        +   *
        +   * void setup() {
        +   *   big = createGraphics(3000, 3000);
        +   *
        +   *   big.beginDraw();
        +   *   big.background(128);
        +   *   big.line(20, 1800, 1800, 900);
        +   *   // etc..
        +   *   big.endDraw();
        +   *
        +   *   // make sure the file is written to the sketch folder
        +   *   big.save("big.tif");
        +   * }
        +   *
        +   * 
        + *
      • It's important to always wrap drawing to createGraphics() with + * beginDraw() and endDraw() (beginFrame() and endFrame() prior to + * revision 0115). The reason is that the renderer needs to know when + * drawing has stopped, so that it can update itself internally. + * This also handles calling the defaults() method, for people familiar + * with that. + *
      • With Processing 0115 and later, it's possible to write images in + * formats other than the default .tga and .tiff. The exact formats and + * background information can be found in the developer's reference for + * PImage.save(). + *
      + * + * @webref rendering + * @param w width in pixels + * @param h height in pixels + * @param renderer Either P2D, P3D, or PDF + * @see PGraphics#PGraphics + * + */ + public PGraphics createGraphics(int w, int h, String renderer) { + return createGraphics(w, h, renderer, null); + } + + + /** + * Create an offscreen graphics surface for drawing, in this case + * for a renderer that writes to a file (such as PDF or DXF). + * @param path the name of the file (can be an absolute or relative path) + */ + public PGraphics createGraphics(int w, int h, + String renderer, String path) { + return makeGraphics(w, h, renderer, path, false); + /* + if (path != null) { + path = savePath(path); + } + PGraphics pg = makeGraphics(w, h, renderer, path, false); + //pg.parent = this; // why wasn't setParent() used before 3.0a6? + //pg.setParent(this); // make save() work + // Nevermind, parent is set in makeGraphics() + return pg; + */ + } + + +// public PGraphics makePrimaryGraphics(int wide, int high) { +// return makeGraphics(wide, high, sketchRenderer(), null, true); +// } + + + /** + * Version of createGraphics() used internally. + * @param path A path (or null if none), can be absolute or relative ({@link PApplet#savePath} will be called) + */ + protected PGraphics makeGraphics(int w, int h, + String renderer, String path, + boolean primary) { +// String openglError = external ? +// // This first one should no longer be possible +// "Before using OpenGL, first select " + +// "Import Library > OpenGL from the Sketch menu." : +// // Welcome to Java programming! The training wheels are off. +// "The Java classpath and native library path is not " + +// "properly set for using the OpenGL library."; + + if (!primary && !g.isGL()) { + if (renderer.equals(P2D)) { + throw new RuntimeException("createGraphics() with P2D requires size() to use P2D or P3D"); + } else if (renderer.equals(P3D)) { + throw new RuntimeException("createGraphics() with P3D or OPENGL requires size() to use P2D or P3D"); + } + } + + try { + Class rendererClass = + Thread.currentThread().getContextClassLoader().loadClass(renderer); + + Constructor constructor = rendererClass.getConstructor(new Class[] { }); + PGraphics pg = (PGraphics) constructor.newInstance(); + + pg.setParent(this); + pg.setPrimary(primary); + if (path != null) { + pg.setPath(savePath(path)); + } +// pg.setQuality(sketchQuality()); +// if (!primary) { +// surface.initImage(pg, w, h); +// } + pg.setSize(w, h); + + // everything worked, return it + return pg; + + } catch (InvocationTargetException ite) { + String msg = ite.getTargetException().getMessage(); + if ((msg != null) && + (msg.indexOf("no jogl in java.library.path") != -1)) { + // Is this true anymore, since the JARs contain the native libs? + throw new RuntimeException("The jogl library folder needs to be " + + "specified with -Djava.library.path=/path/to/jogl"); + + } else { + printStackTrace(ite.getTargetException()); + Throwable target = ite.getTargetException(); + /* + // removing for 3.2, we'll see + if (platform == MACOSX) { + target.printStackTrace(System.out); // OS X bug (still true?) + } + */ + throw new RuntimeException(target.getMessage()); + } + + } catch (ClassNotFoundException cnfe) { +// if (cnfe.getMessage().indexOf("processing.opengl.PGraphicsOpenGL") != -1) { +// throw new RuntimeException(openglError + +// " (The library .jar file is missing.)"); +// } else { + if (external) { + throw new RuntimeException("You need to use \"Import Library\" " + + "to add " + renderer + " to your sketch."); + } else { + throw new RuntimeException("The " + renderer + + " renderer is not in the class path."); + } + + } catch (Exception e) { + if ((e instanceof IllegalArgumentException) || + (e instanceof NoSuchMethodException) || + (e instanceof IllegalAccessException)) { + if (e.getMessage().contains("cannot be <= 0")) { + // IllegalArgumentException will be thrown if w/h is <= 0 + // http://code.google.com/p/processing/issues/detail?id=983 + throw new RuntimeException(e); + + } else { + printStackTrace(e); + String msg = renderer + " needs to be updated " + + "for the current release of Processing."; + throw new RuntimeException(msg); + } + } else { + /* + if (platform == MACOSX) { + e.printStackTrace(System.out); // OS X bug (still true?) + } + */ + printStackTrace(e); + throw new RuntimeException(e.getMessage()); + } + } + } + + + /** Create default renderer, likely to be resized, but needed for surface init. */ + protected PGraphics createPrimaryGraphics() { + return makeGraphics(sketchWidth(), sketchHeight(), + sketchRenderer(), sketchOutputPath(), true); + } + + + /** + * ( begin auto-generated from createImage.xml ) + * + * Creates a new PImage (the datatype for storing images). This provides a + * fresh buffer of pixels to play with. Set the size of the buffer with the + * width and height parameters. The format parameter + * defines how the pixels are stored. See the PImage reference for more information. + *

      + * Be sure to include all three parameters, specifying only the width and + * height (but no format) will produce a strange error. + *

      + * Advanced users please note that createImage() should be used instead of + * the syntax new PImage(). + * + * ( end auto-generated ) + *

      Advanced

      + * Preferred method of creating new PImage objects, ensures that a + * reference to the parent PApplet is included, which makes save() work + * without needing an absolute path. + * + * @webref image + * @param w width in pixels + * @param h height in pixels + * @param format Either RGB, ARGB, ALPHA (grayscale alpha channel) + * @see PImage + * @see PGraphics + */ + public PImage createImage(int w, int h, int format) { + PImage image = new PImage(w, h, format); + image.parent = this; // make save() work + return image; + } + + + ////////////////////////////////////////////////////////////// + + + protected boolean insideDraw; + + /** Last time in nanoseconds that frameRate was checked */ + protected long frameRateLastNanos = 0; + + + public void handleDraw() { + //debug("handleDraw() " + g + " " + looping + " " + redraw + " valid:" + this.isValid() + " visible:" + this.isVisible()); + + // canDraw = g != null && (looping || redraw); + if (g == null) return; + if (!looping && !redraw) return; +// System.out.println("looping/redraw = " + looping + " " + redraw); + + // no longer in use by any of our renderers +// if (!g.canDraw()) { +// debug("g.canDraw() is false"); +// // Don't draw if the renderer is not yet ready. +// // (e.g. OpenGL has to wait for a peer to be on screen) +// return; +// } + + // Store the quality setting in case it's changed during draw and the + // drawing context needs to be re-built before the next frame. +// int pquality = g.smooth; + + if (insideDraw) { + System.err.println("handleDraw() called before finishing"); + System.exit(1); + } + + insideDraw = true; + g.beginDraw(); + if (recorder != null) { + recorder.beginDraw(); + } + + long now = System.nanoTime(); + + if (frameCount == 0) { + // 3.0a5 should be no longer needed; handled by PSurface + //surface.checkDisplaySize(); + +// try { + //println("Calling setup()"); + setup(); + //println("Done with setup()"); + +// } catch (RendererChangeException e) { +// // Give up, instead set the new renderer and re-attempt setup() +// return; +// } +// defaultSize = false; + + } else { // frameCount > 0, meaning an actual draw() + // update the current frameRate + double rate = 1000000.0 / ((now - frameRateLastNanos) / 1000000.0); + float instantaneousRate = (float) (rate / 1000.0); + frameRate = (frameRate * 0.9f) + (instantaneousRate * 0.1f); + + if (frameCount != 0) { + handleMethods("pre"); + } + + // use dmouseX/Y as previous mouse pos, since this is the + // last position the mouse was in during the previous draw. + pmouseX = dmouseX; + pmouseY = dmouseY; + + //println("Calling draw()"); + draw(); + //println("Done calling draw()"); + + // dmouseX/Y is updated only once per frame (unlike emouseX/Y) + dmouseX = mouseX; + dmouseY = mouseY; + + // these are called *after* loop so that valid + // drawing commands can be run inside them. it can't + // be before, since a call to background() would wipe + // out anything that had been drawn so far. + dequeueEvents(); + + handleMethods("draw"); + + redraw = false; // unset 'redraw' flag in case it was set + // (only do this once draw() has run, not just setup()) + } + g.endDraw(); + +// if (pquality != g.smooth) { +// surface.setSmooth(g.smooth); +// } + + if (recorder != null) { + recorder.endDraw(); + } + insideDraw = false; + + if (frameCount != 0) { + handleMethods("post"); + } + + frameRateLastNanos = now; + frameCount++; + } + + +// /** Not official API, not guaranteed to work in the future. */ +// public boolean canDraw() { +// return g != null && (looping || redraw); +// } + + + ////////////////////////////////////////////////////////////// + + +/** + * ( begin auto-generated from redraw.xml ) + * + * Executes the code within draw() one time. This functions allows + * the program to update the display window only when necessary, for + * example when an event registered by mousePressed() or + * keyPressed() occurs. + *

      structuring a program, it only makes sense to call redraw() + * within events such as mousePressed(). This is because + * redraw() does not run draw() immediately (it only sets a + * flag that indicates an update is needed). + *

      redraw() within draw() has no effect because + * draw() is continuously called anyway. + * + * ( end auto-generated ) + * @webref structure + * @usage web_application + * @see PApplet#draw() + * @see PApplet#loop() + * @see PApplet#noLoop() + * @see PApplet#frameRate(float) + */ + synchronized public void redraw() { + if (!looping) { + redraw = true; +// if (thread != null) { +// // wake from sleep (necessary otherwise it'll be +// // up to 10 seconds before update) +// if (CRUSTY_THREADS) { +// thread.interrupt(); +// } else { +// synchronized (blocker) { +// blocker.notifyAll(); +// } +// } +// } + } + } + +/** + * ( begin auto-generated from loop.xml ) + * + * Causes Processing to continuously execute the code within draw(). + * If noLoop() is called, the code in draw() stops executing. + * + * ( end auto-generated ) + * @webref structure + * @usage web_application + * @see PApplet#noLoop() + * @see PApplet#redraw() + * @see PApplet#draw() + */ + synchronized public void loop() { + if (!looping) { + looping = true; + } + } + +/** + * ( begin auto-generated from noLoop.xml ) + * + * Stops Processing from continuously executing the code within + * draw(). If loop() is called, the code in draw() + * begin to run continuously again. If using noLoop() in + * setup(), it should be the last line inside the block. + *

      + * When noLoop() is used, it's not possible to manipulate or access + * the screen inside event handling functions such as mousePressed() + * or keyPressed(). Instead, use those functions to call + * redraw() or loop(), which will run draw(), which + * can update the screen properly. This means that when noLoop() has been + * called, no drawing can happen, and functions like saveFrame() or + * loadPixels() may not be used. + *

      + * Note that if the sketch is resized, redraw() will be called to + * update the sketch, even after noLoop() has been specified. + * Otherwise, the sketch would enter an odd state until loop() was called. + * + * ( end auto-generated ) + * @webref structure + * @usage web_application + * @see PApplet#loop() + * @see PApplet#redraw() + * @see PApplet#draw() + */ + synchronized public void noLoop() { + if (looping) { + looping = false; + } + } + + + public boolean isLooping() { + return looping; + } + + + ////////////////////////////////////////////////////////////// + + + BlockingQueue eventQueue = new LinkedBlockingQueue<>(); + private final Object eventQueueDequeueLock = new Object[0]; + + + /** + * Add an event to the internal event queue, or process it immediately if + * the sketch is not currently looping. + */ + public void postEvent(processing.event.Event pe) { + eventQueue.add(pe); + + if (!looping) { + dequeueEvents(); + } + } + + + protected void dequeueEvents() { + synchronized (eventQueueDequeueLock) { + while (!eventQueue.isEmpty()) { + Event e = eventQueue.remove(); + switch (e.getFlavor()) { + case Event.MOUSE: + handleMouseEvent((MouseEvent) e); + break; + case Event.KEY: + handleKeyEvent((KeyEvent) e); + break; + } + } + } + } + + + ////////////////////////////////////////////////////////////// + + + /** + * Actually take action based on a mouse event. + * Internally updates mouseX, mouseY, mousePressed, and mouseEvent. + * Then it calls the event type with no params, + * i.e. mousePressed() or mouseReleased() that the user may have + * overloaded to do something more useful. + */ + protected void handleMouseEvent(MouseEvent event) { + // http://dev.processing.org/bugs/show_bug.cgi?id=170 + // also prevents mouseExited() on the mac from hosing the mouse + // position, because x/y are bizarre values on the exit event. + // see also the id check below.. both of these go together. + // Not necessary to set mouseX/Y on RELEASE events because the + // actual position will have been set by a PRESS or DRAG event. + // However, PRESS events might come without a preceeding move, + // if the sketch window gains focus on that PRESS. + final int action = event.getAction(); + if (action == MouseEvent.DRAG || + action == MouseEvent.MOVE || + action == MouseEvent.PRESS) { + pmouseX = emouseX; + pmouseY = emouseY; + mouseX = event.getX(); + mouseY = event.getY(); + } + + // Get the (already processed) button code + mouseButton = event.getButton(); + + /* + // Compatibility for older code (these have AWT object params, not P5) + if (mouseEventMethods != null) { + // Probably also good to check this, in case anyone tries to call + // postEvent() with an artificial event they've created. + if (event.getNative() != null) { + mouseEventMethods.handle(new Object[] { event.getNative() }); + } + } + */ + + // this used to only be called on mouseMoved and mouseDragged + // change it back if people run into trouble + if (firstMouse) { + pmouseX = mouseX; + pmouseY = mouseY; + dmouseX = mouseX; + dmouseY = mouseY; + firstMouse = false; + } + + mouseEvent = event; + + // Do this up here in case a registered method relies on the + // boolean for mousePressed. + + switch (action) { + case MouseEvent.PRESS: + mousePressed = true; + break; + case MouseEvent.RELEASE: + mousePressed = false; + break; + } + + handleMethods("mouseEvent", new Object[] { event }); + + switch (action) { + case MouseEvent.PRESS: +// mousePressed = true; + mousePressed(event); + break; + case MouseEvent.RELEASE: +// mousePressed = false; + mouseReleased(event); + break; + case MouseEvent.CLICK: + mouseClicked(event); + break; + case MouseEvent.DRAG: + mouseDragged(event); + break; + case MouseEvent.MOVE: + mouseMoved(event); + break; + case MouseEvent.ENTER: + mouseEntered(event); + break; + case MouseEvent.EXIT: + mouseExited(event); + break; + case MouseEvent.WHEEL: + mouseWheel(event); + break; + } + + if ((action == MouseEvent.DRAG) || + (action == MouseEvent.MOVE)) { + emouseX = mouseX; + emouseY = mouseY; + } + } + + + /** + * ( begin auto-generated from mousePressed.xml ) + * + * The mousePressed() function is called once after every time a + * mouse button is pressed. The mouseButton variable (see the + * related reference entry) can be used to determine which button has been pressed. + * + * ( end auto-generated ) + *

      Advanced

      + * + * If you must, use + * int button = mouseEvent.getButton(); + * to figure out which button was clicked. It will be one of: + * MouseEvent.BUTTON1, MouseEvent.BUTTON2, MouseEvent.BUTTON3 + * Note, however, that this is completely inconsistent across + * platforms. + * @webref input:mouse + * @see PApplet#mouseX + * @see PApplet#mouseY + * @see PApplet#pmouseX + * @see PApplet#pmouseY + * @see PApplet#mousePressed + * @see PApplet#mouseReleased() + * @see PApplet#mouseClicked() + * @see PApplet#mouseMoved() + * @see PApplet#mouseDragged() + * @see PApplet#mouseButton + * @see PApplet#mouseWheel(MouseEvent) + */ + public void mousePressed() { } + + + public void mousePressed(MouseEvent event) { + mousePressed(); + } + + + /** + * ( begin auto-generated from mouseReleased.xml ) + * + * The mouseReleased() function is called every time a mouse button + * is released. + * + * ( end auto-generated ) + * @webref input:mouse + * @see PApplet#mouseX + * @see PApplet#mouseY + * @see PApplet#pmouseX + * @see PApplet#pmouseY + * @see PApplet#mousePressed + * @see PApplet#mousePressed() + * @see PApplet#mouseClicked() + * @see PApplet#mouseMoved() + * @see PApplet#mouseDragged() + * @see PApplet#mouseButton + * @see PApplet#mouseWheel(MouseEvent) + */ + public void mouseReleased() { } + + + public void mouseReleased(MouseEvent event) { + mouseReleased(); + } + + + /** + * ( begin auto-generated from mouseClicked.xml ) + * + * The mouseClicked() function is called once after a mouse button + * has been pressed and then released. + * + * ( end auto-generated ) + *

      Advanced

      + * When the mouse is clicked, mousePressed() will be called, + * then mouseReleased(), then mouseClicked(). Note that + * mousePressed is already false inside of mouseClicked(). + * @webref input:mouse + * @see PApplet#mouseX + * @see PApplet#mouseY + * @see PApplet#pmouseX + * @see PApplet#pmouseY + * @see PApplet#mousePressed + * @see PApplet#mousePressed() + * @see PApplet#mouseReleased() + * @see PApplet#mouseMoved() + * @see PApplet#mouseDragged() + * @see PApplet#mouseButton + * @see PApplet#mouseWheel(MouseEvent) + */ + public void mouseClicked() { } + + + public void mouseClicked(MouseEvent event) { + mouseClicked(); + } + + + /** + * ( begin auto-generated from mouseDragged.xml ) + * + * The mouseDragged() function is called once every time the mouse + * moves and a mouse button is pressed. + * + * ( end auto-generated ) + * @webref input:mouse + * @see PApplet#mouseX + * @see PApplet#mouseY + * @see PApplet#pmouseX + * @see PApplet#pmouseY + * @see PApplet#mousePressed + * @see PApplet#mousePressed() + * @see PApplet#mouseReleased() + * @see PApplet#mouseClicked() + * @see PApplet#mouseMoved() + * @see PApplet#mouseButton + * @see PApplet#mouseWheel(MouseEvent) + */ + public void mouseDragged() { } + + + public void mouseDragged(MouseEvent event) { + mouseDragged(); + } + + + /** + * ( begin auto-generated from mouseMoved.xml ) + * + * The mouseMoved() function is called every time the mouse moves + * and a mouse button is not pressed. + * + * ( end auto-generated ) + * @webref input:mouse + * @see PApplet#mouseX + * @see PApplet#mouseY + * @see PApplet#pmouseX + * @see PApplet#pmouseY + * @see PApplet#mousePressed + * @see PApplet#mousePressed() + * @see PApplet#mouseReleased() + * @see PApplet#mouseClicked() + * @see PApplet#mouseDragged() + * @see PApplet#mouseButton + * @see PApplet#mouseWheel(MouseEvent) + */ + public void mouseMoved() { } + + + public void mouseMoved(MouseEvent event) { + mouseMoved(); + } + + + public void mouseEntered() { } + + + public void mouseEntered(MouseEvent event) { + mouseEntered(); + } + + + public void mouseExited() { } + + + public void mouseExited(MouseEvent event) { + mouseExited(); + } + + /** + * @nowebref + */ + public void mouseWheel() { } + + /** + * The event.getAmount() method returns negative values if the mouse wheel + * if rotated up or away from the user and positive in the other direction. + * On OS X with "natural" scrolling enabled, the values are opposite. + * + * @webref input:mouse + * @param event the MouseEvent + * @see PApplet#mouseX + * @see PApplet#mouseY + * @see PApplet#pmouseX + * @see PApplet#pmouseY + * @see PApplet#mousePressed + * @see PApplet#mousePressed() + * @see PApplet#mouseReleased() + * @see PApplet#mouseClicked() + * @see PApplet#mouseMoved() + * @see PApplet#mouseDragged() + * @see PApplet#mouseButton + */ + public void mouseWheel(MouseEvent event) { + mouseWheel(); + } + + + + ////////////////////////////////////////////////////////////// + + + protected void handleKeyEvent(KeyEvent event) { + + // Get rid of auto-repeating keys if desired and supported + if (!keyRepeatEnabled && event.isAutoRepeat()) return; + + keyEvent = event; + key = event.getKey(); + keyCode = event.getKeyCode(); + + switch (event.getAction()) { + case KeyEvent.PRESS: + Long hash = ((long) keyCode << Character.SIZE) | key; + if (!pressedKeys.contains(hash)) pressedKeys.add(hash); + keyPressed = true; + keyPressed(keyEvent); + break; + case KeyEvent.RELEASE: + pressedKeys.remove(((long) keyCode << Character.SIZE) | key); + keyPressed = !pressedKeys.isEmpty(); + keyReleased(keyEvent); + break; + case KeyEvent.TYPE: + keyTyped(keyEvent); + break; + } + + /* + if (keyEventMethods != null) { + keyEventMethods.handle(new Object[] { event.getNative() }); + } + */ + + handleMethods("keyEvent", new Object[] { event }); + + // if someone else wants to intercept the key, they should + // set key to zero (or something besides the ESC). + if (event.getAction() == KeyEvent.PRESS) { + //if (key == java.awt.event.KeyEvent.VK_ESCAPE) { + if (key == ESC) { + exit(); + } + // When running tethered to the Processing application, respond to + // Ctrl-W (or Cmd-W) events by closing the sketch. Not enabled when + // running independently, because this sketch may be one component + // embedded inside an application that has its own close behavior. + if (external && + event.getKeyCode() == 'W' && + ((event.isMetaDown() && platform == MACOSX) || + (event.isControlDown() && platform != MACOSX))) { + // Can't use this native stuff b/c the native event might be NEWT +// if (external && event.getNative() instanceof java.awt.event.KeyEvent && +// ((java.awt.event.KeyEvent) event.getNative()).getModifiers() == +// Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() && +// event.getKeyCode() == 'W') { + exit(); + } + } + } + + + /** + * + * ( begin auto-generated from keyPressed.xml ) + * + * The keyPressed() function is called once every time a key is + * pressed. The key that was pressed is stored in the key variable. + *

      + * For non-ASCII keys, use the keyCode variable. The keys included + * in the ASCII specification (BACKSPACE, TAB, ENTER, RETURN, ESC, and + * DELETE) do not require checking to see if they key is coded, and you + * should simply use the key variable instead of keyCode If + * you're making cross-platform projects, note that the ENTER key is + * commonly used on PCs and Unix and the RETURN key is used instead on + * Macintosh. Check for both ENTER and RETURN to make sure your program + * will work for all platforms. + *

      + * Because of how operating systems handle key repeats, holding down a key + * may cause multiple calls to keyPressed() (and keyReleased() as well). + * The rate of repeat is set by the operating system and how each computer + * is configured. + * + * ( end auto-generated ) + *

      Advanced

      + * + * Called each time a single key on the keyboard is pressed. + * Because of how operating systems handle key repeats, holding + * down a key will cause multiple calls to keyPressed(), because + * the OS repeat takes over. + *

      + * Examples for key handling: + * (Tested on Windows XP, please notify if different on other + * platforms, I have a feeling Mac OS and Linux may do otherwise) + *

      +   * 1. Pressing 'a' on the keyboard:
      +   *    keyPressed  with key == 'a' and keyCode == 'A'
      +   *    keyTyped    with key == 'a' and keyCode ==  0
      +   *    keyReleased with key == 'a' and keyCode == 'A'
      +   *
      +   * 2. Pressing 'A' on the keyboard:
      +   *    keyPressed  with key == 'A' and keyCode == 'A'
      +   *    keyTyped    with key == 'A' and keyCode ==  0
      +   *    keyReleased with key == 'A' and keyCode == 'A'
      +   *
      +   * 3. Pressing 'shift', then 'a' on the keyboard (caps lock is off):
      +   *    keyPressed  with key == CODED and keyCode == SHIFT
      +   *    keyPressed  with key == 'A'   and keyCode == 'A'
      +   *    keyTyped    with key == 'A'   and keyCode == 0
      +   *    keyReleased with key == 'A'   and keyCode == 'A'
      +   *    keyReleased with key == CODED and keyCode == SHIFT
      +   *
      +   * 4. Holding down the 'a' key.
      +   *    The following will happen several times,
      +   *    depending on your machine's "key repeat rate" settings:
      +   *    keyPressed  with key == 'a' and keyCode == 'A'
      +   *    keyTyped    with key == 'a' and keyCode ==  0
      +   *    When you finally let go, you'll get:
      +   *    keyReleased with key == 'a' and keyCode == 'A'
      +   *
      +   * 5. Pressing and releasing the 'shift' key
      +   *    keyPressed  with key == CODED and keyCode == SHIFT
      +   *    keyReleased with key == CODED and keyCode == SHIFT
      +   *    (note there is no keyTyped)
      +   *
      +   * 6. Pressing the tab key in an applet with Java 1.4 will
      +   *    normally do nothing, but PApplet dynamically shuts
      +   *    this behavior off if Java 1.4 is in use (tested 1.4.2_05 Windows).
      +   *    Java 1.1 (Microsoft VM) passes the TAB key through normally.
      +   *    Not tested on other platforms or for 1.3.
      +   * 
      + * @webref input:keyboard + * @see PApplet#key + * @see PApplet#keyCode + * @see PApplet#keyPressed + * @see PApplet#keyReleased() + */ + public void keyPressed() { } + + + public void keyPressed(KeyEvent event) { + keyPressed(); + } + + + /** + * ( begin auto-generated from keyReleased.xml ) + * + * The keyReleased() function is called once every time a key is + * released. The key that was released will be stored in the key + * variable. See key and keyReleased for more information. + * + * ( end auto-generated ) + * @webref input:keyboard + * @see PApplet#key + * @see PApplet#keyCode + * @see PApplet#keyPressed + * @see PApplet#keyPressed() + */ + public void keyReleased() { } + + + public void keyReleased(KeyEvent event) { + keyReleased(); + } + + + /** + * ( begin auto-generated from keyTyped.xml ) + * + * The keyTyped() function is called once every time a key is + * pressed, but action keys such as Ctrl, Shift, and Alt are ignored. + * Because of how operating systems handle key repeats, holding down a key + * will cause multiple calls to keyTyped(), the rate is set by the + * operating system and how each computer is configured. + * + * ( end auto-generated ) + * @webref input:keyboard + * @see PApplet#keyPressed + * @see PApplet#key + * @see PApplet#keyCode + * @see PApplet#keyReleased() + */ + public void keyTyped() { } + + + public void keyTyped(KeyEvent event) { + keyTyped(); + } + + + + ////////////////////////////////////////////////////////////// + + // i am focused man, and i'm not afraid of death. + // and i'm going all out. i circle the vultures in a van + // and i run the block. + + + public void focusGained() { } + + + public void focusLost() { + // TODO: if user overrides this without calling super it's not gonna work + pressedKeys.clear(); + } + + + + ////////////////////////////////////////////////////////////// + + // getting the time + + + /** + * ( begin auto-generated from millis.xml ) + * + * Returns the number of milliseconds (thousandths of a second) since + * starting an applet. This information is often used for timing animation + * sequences. + * + * ( end auto-generated ) + * + *

      Advanced

      + *

      + * This is a function, rather than a variable, because it may + * change multiple times per frame. + * + * @webref input:time_date + * @see PApplet#second() + * @see PApplet#minute() + * @see PApplet#hour() + * @see PApplet#day() + * @see PApplet#month() + * @see PApplet#year() + * + */ + public int millis() { + return (int) (System.currentTimeMillis() - millisOffset); + } + + /** + * ( begin auto-generated from second.xml ) + * + * Processing communicates with the clock on your computer. The + * second() function returns the current second as a value from 0 - 59. + * + * ( end auto-generated ) + * @webref input:time_date + * @see PApplet#millis() + * @see PApplet#minute() + * @see PApplet#hour() + * @see PApplet#day() + * @see PApplet#month() + * @see PApplet#year() + * */ + static public int second() { + return Calendar.getInstance().get(Calendar.SECOND); + } + + /** + * ( begin auto-generated from minute.xml ) + * + * Processing communicates with the clock on your computer. The + * minute() function returns the current minute as a value from 0 - 59. + * + * ( end auto-generated ) + * + * @webref input:time_date + * @see PApplet#millis() + * @see PApplet#second() + * @see PApplet#hour() + * @see PApplet#day() + * @see PApplet#month() + * @see PApplet#year() + * + * */ + static public int minute() { + return Calendar.getInstance().get(Calendar.MINUTE); + } + + /** + * ( begin auto-generated from hour.xml ) + * + * Processing communicates with the clock on your computer. The + * hour() function returns the current hour as a value from 0 - 23. + * + * ( end auto-generated ) + * @webref input:time_date + * @see PApplet#millis() + * @see PApplet#second() + * @see PApplet#minute() + * @see PApplet#day() + * @see PApplet#month() + * @see PApplet#year() + * + */ + static public int hour() { + return Calendar.getInstance().get(Calendar.HOUR_OF_DAY); + } + + /** + * ( begin auto-generated from day.xml ) + * + * Processing communicates with the clock on your computer. The + * day() function returns the current day as a value from 1 - 31. + * + * ( end auto-generated ) + *

      Advanced

      + * Get the current day of the month (1 through 31). + *

      + * If you're looking for the day of the week (M-F or whatever) + * or day of the year (1..365) then use java's Calendar.get() + * + * @webref input:time_date + * @see PApplet#millis() + * @see PApplet#second() + * @see PApplet#minute() + * @see PApplet#hour() + * @see PApplet#month() + * @see PApplet#year() + */ + static public int day() { + return Calendar.getInstance().get(Calendar.DAY_OF_MONTH); + } + + /** + * ( begin auto-generated from month.xml ) + * + * Processing communicates with the clock on your computer. The + * month() function returns the current month as a value from 1 - 12. + * + * ( end auto-generated ) + * + * @webref input:time_date + * @see PApplet#millis() + * @see PApplet#second() + * @see PApplet#minute() + * @see PApplet#hour() + * @see PApplet#day() + * @see PApplet#year() + */ + static public int month() { + // months are number 0..11 so change to colloquial 1..12 + return Calendar.getInstance().get(Calendar.MONTH) + 1; + } + + /** + * ( begin auto-generated from year.xml ) + * + * Processing communicates with the clock on your computer. The + * year() function returns the current year as an integer (2003, + * 2004, 2005, etc). + * + * ( end auto-generated ) + * The year() function returns the current year as an integer (2003, 2004, 2005, etc). + * + * @webref input:time_date + * @see PApplet#millis() + * @see PApplet#second() + * @see PApplet#minute() + * @see PApplet#hour() + * @see PApplet#day() + * @see PApplet#month() + */ + static public int year() { + return Calendar.getInstance().get(Calendar.YEAR); + } + + + ////////////////////////////////////////////////////////////// + + // controlling time (playing god) + + + /** + * ( begin auto-generated from delay.xml ) + * + * The delay() function causes the program to halt for a specified time. + * Delay times are specified in thousandths of a second. For example, + * running delay(3000) will stop the program for three seconds and + * delay(500) will stop the program for a half-second. + * + * The screen only updates when the end of draw() is reached, so delay() + * cannot be used to slow down drawing. For instance, you cannot use delay() + * to control the timing of an animation. + * + * The delay() function should only be used for pausing scripts (i.e. + * a script that needs to pause a few seconds before attempting a download, + * or a sketch that needs to wait a few milliseconds before reading from + * the serial port). + * + * ( end auto-generated ) + * @webref environment + * @param napTime milliseconds to pause before running draw() again + * @see PApplet#frameRate + * @see PApplet#draw() + */ + public void delay(int napTime) { + //if (frameCount != 0) { + //if (napTime > 0) { + try { + Thread.sleep(napTime); + } catch (InterruptedException e) { } + //} + //} + } + + + /** + * ( begin auto-generated from frameRate.xml ) + * + * Specifies the number of frames to be displayed every second. If the + * processor is not fast enough to maintain the specified rate, it will not + * be achieved. For example, the function call frameRate(30) will + * attempt to refresh 30 times a second. It is recommended to set the frame + * rate within setup(). The default rate is 60 frames per second. + * + * ( end auto-generated ) + * @webref environment + * @param fps number of desired frames per second + * @see PApplet#frameRate + * @see PApplet#frameCount + * @see PApplet#setup() + * @see PApplet#draw() + * @see PApplet#loop() + * @see PApplet#noLoop() + * @see PApplet#redraw() + */ + public void frameRate(float fps) { + surface.setFrameRate(fps); + } + + + ////////////////////////////////////////////////////////////// + + + /** + * Links to a webpage either in the same window or in a new window. The + * complete URL must be specified. + * + *

      Advanced

      + * Link to an external page without all the muss. + *

      + * When run with an applet, uses the browser to open the url, + * for applications, attempts to launch a browser with the url. + * + * @param url the complete URL, as a String in quotes + */ + public void link(String url) { + try { + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().browse(new URI(url)); + } else { + // Just pass it off to open() and hope for the best + launch(url); + } + } catch (IOException e) { + printStackTrace(e); + } catch (URISyntaxException e) { + printStackTrace(e); + } + } + + + static String openLauncher; + + + /** + * ( begin auto-generated from launch.xml ) + * + * Attempts to open an application or file using your platform's launcher. + * The file parameter is a String specifying the file name and + * location. The location parameter must be a full path name, or the name + * of an executable in the system's PATH. In most cases, using a full path + * is the best option, rather than relying on the system PATH. Be sure to + * make the file executable before attempting to open it (chmod +x). + *

      + * The args parameter is a String or String array which is passed to + * the command line. If you have multiple parameters, e.g. an application + * and a document, or a command with multiple switches, use the version + * that takes a String array, and place each individual item in a separate + * element. + *

      + * If args is a String (not an array), then it can only be a single file or + * application with no parameters. It's not the same as executing that + * String using a shell. For instance, launch("javac -help") will not work + * properly. + *

      + * This function behaves differently on each platform. On Windows, the + * parameters are sent to the Windows shell via "cmd /c". On Mac OS X, the + * "open" command is used (type "man open" in Terminal.app for + * documentation). On Linux, it first tries gnome-open, then kde-open, but + * if neither are available, it sends the command to the shell without any + * alterations. + *

      + * For users familiar with Java, this is not quite the same as + * Runtime.exec(), because the launcher command is prepended. Instead, the + * exec(String[]) function is a shortcut for + * Runtime.getRuntime.exec(String[]). + * + * ( end auto-generated ) + * @webref input:files + * @param args arguments to the launcher, eg. a filename. + * @usage Application + */ + static public Process launch(String... args) { + String[] params = null; + + if (platform == WINDOWS) { + // just launching the .html file via the shell works + // but make sure to chmod +x the .html files first + // also place quotes around it in case there's a space + // in the user.dir part of the url + params = new String[] { "cmd", "/c" }; + + } else if (platform == MACOSX) { + params = new String[] { "open" }; + + } else if (platform == LINUX) { + // xdg-open is in the Free Desktop Specification and really should just + // work on desktop Linux. Not risking it though. + final String[] launchers = { "xdg-open", "gnome-open", "kde-open" }; + for (String launcher : launchers) { + if (openLauncher != null) break; + try { + Process p = Runtime.getRuntime().exec(new String[] { launcher }); + /*int result =*/ p.waitFor(); + // Not installed will throw an IOException (JDK 1.4.2, Ubuntu 7.04) + openLauncher = launcher; + } catch (Exception e) { } + } + if (openLauncher == null) { + System.err.println("Could not find xdg-open, gnome-open, or kde-open: " + + "the open() command may not work."); + } + if (openLauncher != null) { + params = new String[] { openLauncher }; + } + //} else { // give up and just pass it to Runtime.exec() + //open(new String[] { filename }); + //params = new String[] { filename }; + } + if (params != null) { + // If the 'open', 'gnome-open' or 'cmd' are already included + if (params[0].equals(args[0])) { + // then don't prepend those params again + return exec(args); + } else { + params = concat(params, args); + return exec(params); + } + } else { + return exec(args); + } + } + + + /** + * Pass a set of arguments directly to the command line. Uses Java's + * Runtime.exec() + * method. This is different from the launch() + * method, which uses the operating system's launcher to open the files. + * It's always a good idea to use a full path to the executable here. + *

      +   * exec("/usr/bin/say", "welcome to the command line");
      +   * 
      + * Or if you want to wait until it's completed, something like this: + *
      +   * Process p = exec("/usr/bin/say", "waiting until done");
      +   * try {
      +   *   int result = p.waitFor();
      +   *   println("the process returned " + result);
      +   * } catch (InterruptedException e) { }
      +   * 
      + * You can also get the system output and error streams from the Process + * object, but that's more that we'd like to cover here. + * @return a Process object + */ + static public Process exec(String... args) { + try { + return Runtime.getRuntime().exec(args); + } catch (Exception e) { + throw new RuntimeException("Could not open " + join(args, ' '), e); + } + } + + + /** + * Alternative version of exec() that retrieves stdout and stderr into the + * StringList objects provided. This is a convenience function that handles + * simple exec() calls. If the results will be more than a couple lines, + * you shouldn't use this function, you should use a more elaborate method + * that makes use of proper threading (to drain the shell output) and error + * handling to address the many things that can go wrong within this method. + * + * @param stdout a non-null StringList object to be filled with any output + * @param stderr a non-null StringList object to be filled with error lines + * @param args each argument to be passed as a series of String objects + * @return the result returned from the application, or -1 if an Exception + * occurs before the application is able to return a result. + */ + static public int exec(StringList stdout, StringList stderr, String... args) { + Process p = exec(args); + int result = -1; + try { + BufferedReader out = createReader(p.getInputStream()); + BufferedReader err = createReader(p.getErrorStream()); + result = p.waitFor(); + String line; + while ((line = out.readLine()) != null) { + stdout.append(line); + } + while ((line = err.readLine()) != null) { + stderr.append(line); + } + } catch (IOException e) { + e.printStackTrace(); + + } catch (InterruptedException e) { + // can be safely ignored + } + return result; + } + + + /** + * Same as exec() above, but prefixes the call with a shell. + */ + static public int shell(StringList stdout, StringList stderr, String... args) { + String shell; + String runCmd; + StringList argList = new StringList(); + if (platform == WINDOWS) { + shell = System.getenv("COMSPEC"); + runCmd = "/C"; + } else { + shell = "/bin/sh"; + runCmd = "-c"; + // attempt emulate the behavior of an interactive shell + // can't use -i or -l since the version of bash shipped with macOS does not support this together with -c + // also we want to make sure no motd or similar gets returned as stdout + argList.append("if [ -f /etc/profile ]; then . /etc/profile >/dev/null 2>&1; fi;"); + argList.append("if [ -f ~/.bash_profile ]; then . ~/.bash_profile >/dev/null 2>&1; elif [ -f ~/.bash_profile ]; then . ~/.bash_profile >/dev/null 2>&1; elif [ -f ~/.profile ]; then ~/.profile >/dev/null 2>&1; fi;"); + } + for (String arg : args) { + argList.append(arg); + } + return exec(stdout, stderr, shell, runCmd, argList.join(" ")); + } + + + /* + static private final String shellQuoted(String arg) { + if (arg.indexOf(' ') != -1) { + // check to see if already quoted + if ((arg.charAt(0) != '\"' || arg.charAt(arg.length()-1) != '\"') && + (arg.charAt(0) != '\'' || arg.charAt(arg.length()-1) != '\'')) { + + // see which quotes we can use + if (arg.indexOf('\"') == -1) { + // if no double quotes, try those first + return "\"" + arg + "\""; + + } else if (arg.indexOf('\'') == -1) { + // if no single quotes, let's use those + return "'" + arg + "'"; + } + } + } + return arg; + } + */ + + + ////////////////////////////////////////////////////////////// + + + /** + * Better way of handling e.printStackTrace() calls so that they can be + * handled by subclasses as necessary. + */ + protected void printStackTrace(Throwable t) { + t.printStackTrace(); + } + + + /** + * Function for an applet/application to kill itself and + * display an error. Mostly this is here to be improved later. + */ + public void die(String what) { + dispose(); + throw new RuntimeException(what); + } + + + /** + * Same as above but with an exception. Also needs work. + */ + public void die(String what, Exception e) { + if (e != null) e.printStackTrace(); + die(what); + } + + + /** + * ( begin auto-generated from exit.xml ) + * + * Quits/stops/exits the program. Programs without a draw() function + * exit automatically after the last line has run, but programs with + * draw() run continuously until the program is manually stopped or + * exit() is run.
      + *
      + * Rather than terminating immediately, exit() will cause the sketch + * to exit after draw() has completed (or after setup() + * completes if called during the setup() function).
      + *
      + * For Java programmers, this is not the same as System.exit(). + * Further, System.exit() should not be used because closing out an + * application while draw() is running may cause a crash + * (particularly with P3D). + * + * ( end auto-generated ) + * @webref structure + */ + public void exit() { + if (surface.isStopped()) { + // exit immediately, dispose() has already been called, + // meaning that the main thread has long since exited + exitActual(); + + } else if (looping) { + // dispose() will be called as the thread exits + finished = true; + // tell the code to call exitActual() to do a System.exit() + // once the next draw() has completed + exitCalled = true; + + } else if (!looping) { + // if not looping, shut down things explicitly, + // because the main thread will be sleeping + dispose(); + + // now get out + exitActual(); + } + } + + + public boolean exitCalled() { + return exitCalled; + } + + + /** + * Some subclasses (I'm looking at you, processing.py) might wish to do something + * other than actually terminate the JVM. This gives them a chance to do whatever + * they have in mind when cleaning up. + */ + public void exitActual() { + try { + System.exit(0); + } catch (SecurityException e) { + // don't care about applet security exceptions + } + } + + + /** + * Called to dispose of resources and shut down the sketch. + * Destroys the thread, dispose the renderer,and notify listeners. + *

      + * Not to be called or overriden by users. If called multiple times, + * will only notify listeners once. Register a dispose listener instead. + */ + public void dispose() { + // moved here from stop() + finished = true; // let the sketch know it is shut down time + + // don't run the disposers twice + if (surface.stopThread()) { + + // shut down renderer + if (g != null) { + g.dispose(); + } + // run dispose() methods registered by libraries + handleMethods("dispose"); + } + + if (platform == MACOSX) { + try { + final String td = "processing.core.ThinkDifferent"; + final Class thinkDifferent = getClass().getClassLoader().loadClass(td); + thinkDifferent.getMethod("cleanup").invoke(null); + } catch (Exception e) { + e.printStackTrace(); + } + } + + } + + + + ////////////////////////////////////////////////////////////// + + + /** + * Call a method in the current class based on its name. + *

      + * Note that the function being called must be public. Inside the PDE, + * 'public' is automatically added, but when used without the preprocessor, + * (like from Eclipse) you'll have to do it yourself. + */ + public void method(String name) { + try { + Method method = getClass().getMethod(name, new Class[] {}); + method.invoke(this, new Object[] { }); + + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.getTargetException().printStackTrace(); + } catch (NoSuchMethodException nsme) { + System.err.println("There is no public " + name + "() method " + + "in the class " + getClass().getName()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * Launch a new thread and call the specified function from that new thread. + * This is a very simple way to do a thread without needing to get into + * classes, runnables, etc. + *

      + * Note that the function being called must be public. Inside the PDE, + * 'public' is automatically added, but when used without the preprocessor, + * (like from Eclipse) you'll have to do it yourself. + * + * @webref structure + * @usage Application + * @param name name of the function to be executed in a separate thread + * @see PApplet#setup() + * @see PApplet#draw() + * @see PApplet#loop() + * @see PApplet#noLoop() + */ + public void thread(final String name) { + Thread later = new Thread() { + @Override + public void run() { + method(name); + } + }; + later.start(); + } + + + + ////////////////////////////////////////////////////////////// + + // SCREEN GRABASS + + + /** + * ( begin auto-generated from save.xml ) + * + * Saves an image from the display window. Images are saved in TIFF, TARGA, + * JPEG, and PNG format depending on the extension within the + * filename parameter. For example, "image.tif" will have a TIFF + * image and "image.png" will save a PNG image. If no extension is included + * in the filename, the image will save in TIFF format and .tif will + * be added to the name. These files are saved to the sketch's folder, + * which may be opened by selecting "Show sketch folder" from the "Sketch" + * menu. It is not possible to use save() while running the program + * in a web browser. + *
      images saved from the main drawing window will be opaque. To save + * images without a background, use createGraphics(). + * + * ( end auto-generated ) + * @webref output:image + * @param filename any sequence of letters and numbers + * @see PApplet#saveFrame() + * @see PApplet#createGraphics(int, int, String) + */ + public void save(String filename) { + g.save(savePath(filename)); + } + + + /** + */ + public void saveFrame() { + try { + g.save(savePath("screen-" + nf(frameCount, 4) + ".tif")); + } catch (SecurityException se) { + System.err.println("Can't use saveFrame() when running in a browser, " + + "unless using a signed applet."); + } + } + + + /** + * ( begin auto-generated from saveFrame.xml ) + * + * Saves a numbered sequence of images, one image each time the function is + * run. To save an image that is identical to the display window, run the + * function at the end of draw() or within mouse and key events such + * as mousePressed() and keyPressed(). If saveFrame() + * is called without parameters, it will save the files as screen-0000.tif, + * screen-0001.tif, etc. It is possible to specify the name of the sequence + * with the filename parameter and make the choice of saving TIFF, + * TARGA, PNG, or JPEG files with the ext parameter. These image + * sequences can be loaded into programs such as Apple's QuickTime software + * and made into movies. These files are saved to the sketch's folder, + * which may be opened by selecting "Show sketch folder" from the "Sketch" + * menu.
      + *
      + * It is not possible to use saveXxxxx() functions inside a web browser + * unless the sketch is signed applet. To + * save a file back to a server, see the save to + * web code snippet on the Processing Wiki.
      + *
      + * All images saved from the main drawing window will be opaque. To save + * images without a background, use createGraphics(). + * + * ( end auto-generated ) + * @webref output:image + * @see PApplet#save(String) + * @see PApplet#createGraphics(int, int, String, String) + * @see PApplet#frameCount + * @param filename any sequence of letters or numbers that ends with either ".tif", ".tga", ".jpg", or ".png" + */ + public void saveFrame(String filename) { + try { + g.save(savePath(insertFrame(filename))); + } catch (SecurityException se) { + System.err.println("Can't use saveFrame() when running in a browser, " + + "unless using a signed applet."); + } + } + + + /** + * Check a string for #### signs to see if the frame number should be + * inserted. Used for functions like saveFrame() and beginRecord() to + * replace the # marks with the frame number. If only one # is used, + * it will be ignored, under the assumption that it's probably not + * intended to be the frame number. + */ + public String insertFrame(String what) { + int first = what.indexOf('#'); + int last = what.lastIndexOf('#'); + + if ((first != -1) && (last - first > 0)) { + String prefix = what.substring(0, first); + int count = last - first + 1; + String suffix = what.substring(last + 1); + return prefix + nf(frameCount, count) + suffix; + } + return what; // no change + } + + + + ////////////////////////////////////////////////////////////// + + // CURSOR + + // + + + /** + * Set the cursor type + * @param kind either ARROW, CROSS, HAND, MOVE, TEXT, or WAIT + */ + public void cursor(int kind) { + surface.setCursor(kind); + } + + + /** + * Replace the cursor with the specified PImage. The x- and y- + * coordinate of the center will be the center of the image. + */ + public void cursor(PImage img) { + cursor(img, img.width/2, img.height/2); + } + + + /** + * ( begin auto-generated from cursor.xml ) + * + * Sets the cursor to a predefined symbol, an image, or makes it visible if + * already hidden. If you are trying to set an image as the cursor, it is + * recommended to make the size 16x16 or 32x32 pixels. It is not possible + * to load an image as the cursor if you are exporting your program for the + * Web and not all MODES work with all Web browsers. The values for + * parameters x and y must be less than the dimensions of the image. + *

      + * Setting or hiding the cursor generally does not work with "Present" mode + * (when running full-screen). + * + * ( end auto-generated ) + *

      Advanced

      + * Set a custom cursor to an image with a specific hotspot. + * Only works with JDK 1.2 and later. + * Currently seems to be broken on Java 1.4 for Mac OS X + *

      + * Based on code contributed by Amit Pitaru, plus additional + * code to handle Java versions via reflection by Jonathan Feinberg. + * Reflection removed for release 0128 and later. + * @webref environment + * @see PApplet#noCursor() + * @param img any variable of type PImage + * @param x the horizontal active spot of the cursor + * @param y the vertical active spot of the cursor + */ + public void cursor(PImage img, int x, int y) { + surface.setCursor(img, x, y); + } + + + /** + * Show the cursor after noCursor() was called. + * Notice that the program remembers the last set cursor type + */ + public void cursor() { + surface.showCursor(); + } + + + /** + * ( begin auto-generated from noCursor.xml ) + * + * Hides the cursor from view. Will not work when running the program in a + * web browser or when running in full screen (Present) mode. + * + * ( end auto-generated ) + *

      Advanced

      + * Hide the cursor by creating a transparent image + * and using it as a custom cursor. + * @webref environment + * @see PApplet#cursor() + * @usage Application + */ + public void noCursor() { + surface.hideCursor(); + } + + + ////////////////////////////////////////////////////////////// + +/** + * ( begin auto-generated from print.xml ) + * + * Writes to the console area of the Processing environment. This is often + * helpful for looking at the data a program is producing. The companion + * function println() works like print(), but creates a new + * line of text for each call to the function. Individual elements can be + * separated with quotes ("") and joined with the addition operator (+).
      + *
      + * Beginning with release 0125, to print the contents of an array, use + * println(). There's no sensible way to do a print() of an array, + * because there are too many possibilities for how to separate the data + * (spaces, commas, etc). If you want to print an array as a single line, + * use join(). With join(), you can choose any delimiter you + * like and print() the result.
      + *
      + * Using print() on an object will output null, a memory + * location that may look like "@10be08," or the result of the + * toString() method from the object that's being printed. Advanced + * users who want more useful output when calling print() on their + * own classes can add a toString() method to the class that returns + * a String. + * + * ( end auto-generated ) + * @webref output:text_area + * @usage IDE + * @param what data to print to console + * @see PApplet#println() + * @see PApplet#printArray(Object) + * @see PApplet#join(String[], char) + */ + static public void print(byte what) { + System.out.print(what); + System.out.flush(); + } + + static public void print(boolean what) { + System.out.print(what); + System.out.flush(); + } + + static public void print(char what) { + System.out.print(what); + System.out.flush(); + } + + static public void print(int what) { + System.out.print(what); + System.out.flush(); + } + + static public void print(long what) { + System.out.print(what); + System.out.flush(); + } + + static public void print(float what) { + System.out.print(what); + System.out.flush(); + } + + static public void print(double what) { + System.out.print(what); + System.out.flush(); + } + + static public void print(String what) { + System.out.print(what); + System.out.flush(); + } + + /** + * @param variables list of data, separated by commas + */ + static public void print(Object... variables) { + StringBuilder sb = new StringBuilder(); + for (Object o : variables) { + if (sb.length() != 0) { + sb.append(" "); + } + if (o == null) { + sb.append("null"); + } else { + sb.append(o.toString()); + } + } + System.out.print(sb.toString()); + } + + + /* + static public void print(Object what) { + if (what == null) { + // special case since this does fuggly things on > 1.1 + System.out.print("null"); + } else { + System.out.println(what.toString()); + } + } + */ + + + /** + * ( begin auto-generated from println.xml ) + * + * Writes to the text area of the Processing environment's console. This is + * often helpful for looking at the data a program is producing. Each call + * to this function creates a new line of output. Individual elements can + * be separated with quotes ("") and joined with the string concatenation + * operator (+). See print() for more about what to expect in the output. + *

      println() on an array (by itself) will write the + * contents of the array to the console. This is often helpful for looking + * at the data a program is producing. A new line is put between each + * element of the array. This function can only print one dimensional + * arrays. For arrays with higher dimensions, the result will be closer to + * that of print(). + * + * ( end auto-generated ) + * @webref output:text_area + * @usage IDE + * @see PApplet#print(byte) + * @see PApplet#printArray(Object) + */ + static public void println() { + System.out.println(); + } + + +/** + * @param what data to print to console + */ + static public void println(byte what) { + System.out.println(what); + System.out.flush(); + } + + static public void println(boolean what) { + System.out.println(what); + System.out.flush(); + } + + static public void println(char what) { + System.out.println(what); + System.out.flush(); + } + + static public void println(int what) { + System.out.println(what); + System.out.flush(); + } + + static public void println(long what) { + System.out.println(what); + System.out.flush(); + } + + static public void println(float what) { + System.out.println(what); + System.out.flush(); + } + + static public void println(double what) { + System.out.println(what); + System.out.flush(); + } + + static public void println(String what) { + System.out.println(what); + System.out.flush(); + } + + /** + * @param variables list of data, separated by commas + */ + static public void println(Object... variables) { +// System.out.println("got " + variables.length + " variables"); + print(variables); + println(); + } + + + /* + // Breaking this out since the compiler doesn't know the difference between + // Object... and just Object (with an array passed in). This should take care + // of the confusion for at least the most common case (a String array). + // On second thought, we're going the printArray() route, since the other + // object types are also used frequently. + static public void println(String[] array) { + for (int i = 0; i < array.length; i++) { + System.out.println("[" + i + "] \"" + array[i] + "\""); + } + System.out.flush(); + } + */ + + + /** + * For arrays, use printArray() instead. This function causes a warning + * because the new print(Object...) and println(Object...) functions can't + * be reliably bound by the compiler. + */ + static public void println(Object what) { + if (what == null) { + System.out.println("null"); + } else if (what.getClass().isArray()) { + printArray(what); + } else { + System.out.println(what.toString()); + System.out.flush(); + } + } + + /** + * ( begin auto-generated from printArray.xml ) + * + * To come... + * + * ( end auto-generated ) + * @webref output:text_area + * @param what one-dimensional array + * @usage IDE + * @see PApplet#print(byte) + * @see PApplet#println() + */ + static public void printArray(Object what) { + if (what == null) { + // special case since this does fuggly things on > 1.1 + System.out.println("null"); + + } else { + String name = what.getClass().getName(); + if (name.charAt(0) == '[') { + switch (name.charAt(1)) { + case '[': + // don't even mess with multi-dimensional arrays (case '[') + // or anything else that's not int, float, boolean, char + System.out.println(what); + break; + + case 'L': + // print a 1D array of objects as individual elements + Object poo[] = (Object[]) what; + for (int i = 0; i < poo.length; i++) { + if (poo[i] instanceof String) { + System.out.println("[" + i + "] \"" + poo[i] + "\""); + } else { + System.out.println("[" + i + "] " + poo[i]); + } + } + break; + + case 'Z': // boolean + boolean zz[] = (boolean[]) what; + for (int i = 0; i < zz.length; i++) { + System.out.println("[" + i + "] " + zz[i]); + } + break; + + case 'B': // byte + byte bb[] = (byte[]) what; + for (int i = 0; i < bb.length; i++) { + System.out.println("[" + i + "] " + bb[i]); + } + break; + + case 'C': // char + char cc[] = (char[]) what; + for (int i = 0; i < cc.length; i++) { + System.out.println("[" + i + "] '" + cc[i] + "'"); + } + break; + + case 'I': // int + int ii[] = (int[]) what; + for (int i = 0; i < ii.length; i++) { + System.out.println("[" + i + "] " + ii[i]); + } + break; + + case 'J': // int + long jj[] = (long[]) what; + for (int i = 0; i < jj.length; i++) { + System.out.println("[" + i + "] " + jj[i]); + } + break; + + case 'F': // float + float ff[] = (float[]) what; + for (int i = 0; i < ff.length; i++) { + System.out.println("[" + i + "] " + ff[i]); + } + break; + + case 'D': // double + double dd[] = (double[]) what; + for (int i = 0; i < dd.length; i++) { + System.out.println("[" + i + "] " + dd[i]); + } + break; + + default: + System.out.println(what); + } + } else { // not an array + System.out.println(what); + } + } + System.out.flush(); + } + + + static public void debug(String msg) { + if (DEBUG) println(msg); + } + // + + /* + // not very useful, because it only works for public (and protected?) + // fields of a class, not local variables to methods + public void printvar(String name) { + try { + Field field = getClass().getDeclaredField(name); + println(name + " = " + field.get(this)); + } catch (Exception e) { + e.printStackTrace(); + } + } + */ + + + ////////////////////////////////////////////////////////////// + + // MATH + + // lots of convenience methods for math with floats. + // doubles are overkill for processing applets, and casting + // things all the time is annoying, thus the functions below. + +/** + * ( begin auto-generated from abs.xml ) + * + * Calculates the absolute value (magnitude) of a number. The absolute + * value of a number is always positive. + * + * ( end auto-generated ) + * @webref math:calculation + * @param n number to compute + */ + static public final float abs(float n) { + return (n < 0) ? -n : n; + } + + static public final int abs(int n) { + return (n < 0) ? -n : n; + } + +/** + * ( begin auto-generated from sq.xml ) + * + * Squares a number (multiplies a number by itself). The result is always a + * positive number, as multiplying two negative numbers always yields a + * positive result. For example, -1 * -1 = 1. + * + * ( end auto-generated ) + * @webref math:calculation + * @param n number to square + * @see PApplet#sqrt(float) + */ + static public final float sq(float n) { + return n*n; + } + +/** + * ( begin auto-generated from sqrt.xml ) + * + * Calculates the square root of a number. The square root of a number is + * always positive, even though there may be a valid negative root. The + * square root s of number a is such that s*s = a. It + * is the opposite of squaring. + * + * ( end auto-generated ) + * @webref math:calculation + * @param n non-negative number + * @see PApplet#pow(float, float) + * @see PApplet#sq(float) + */ + static public final float sqrt(float n) { + return (float)Math.sqrt(n); + } + +/** + * ( begin auto-generated from log.xml ) + * + * Calculates the natural logarithm (the base-e logarithm) of a + * number. This function expects the values greater than 0.0. + * + * ( end auto-generated ) + * @webref math:calculation + * @param n number greater than 0.0 + */ + static public final float log(float n) { + return (float)Math.log(n); + } + +/** + * ( begin auto-generated from exp.xml ) + * + * Returns Euler's number e (2.71828...) raised to the power of the + * value parameter. + * + * ( end auto-generated ) + * @webref math:calculation + * @param n exponent to raise + */ + static public final float exp(float n) { + return (float)Math.exp(n); + } + +/** + * ( begin auto-generated from pow.xml ) + * + * Facilitates exponential expressions. The pow() function is an + * efficient way of multiplying numbers by themselves (or their reciprocal) + * in large quantities. For example, pow(3, 5) is equivalent to the + * expression 3*3*3*3*3 and pow(3, -5) is equivalent to 1 / 3*3*3*3*3. + * + * ( end auto-generated ) + * @webref math:calculation + * @param n base of the exponential expression + * @param e power by which to raise the base + * @see PApplet#sqrt(float) + */ + static public final float pow(float n, float e) { + return (float)Math.pow(n, e); + } + +/** + * ( begin auto-generated from max.xml ) + * + * Determines the largest value in a sequence of numbers. + * + * ( end auto-generated ) + * @webref math:calculation + * @param a first number to compare + * @param b second number to compare + * @see PApplet#min(float, float, float) + */ + static public final int max(int a, int b) { + return (a > b) ? a : b; + } + + static public final float max(float a, float b) { + return (a > b) ? a : b; + } + + /* + static public final double max(double a, double b) { + return (a > b) ? a : b; + } + */ + +/** + * @param c third number to compare + */ + static public final int max(int a, int b, int c) { + return (a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c); + } + + + static public final float max(float a, float b, float c) { + return (a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c); + } + + + /** + * @param list array of numbers to compare + */ + static public final int max(int[] list) { + if (list.length == 0) { + throw new ArrayIndexOutOfBoundsException(ERROR_MIN_MAX); + } + int max = list[0]; + for (int i = 1; i < list.length; i++) { + if (list[i] > max) max = list[i]; + } + return max; + } + + static public final float max(float[] list) { + if (list.length == 0) { + throw new ArrayIndexOutOfBoundsException(ERROR_MIN_MAX); + } + float max = list[0]; + for (int i = 1; i < list.length; i++) { + if (list[i] > max) max = list[i]; + } + return max; + } + + +// /** +// * Find the maximum value in an array. +// * Throws an ArrayIndexOutOfBoundsException if the array is length 0. +// * @param list the source array +// * @return The maximum value +// */ + /* + static public final double max(double[] list) { + if (list.length == 0) { + throw new ArrayIndexOutOfBoundsException(ERROR_MIN_MAX); + } + double max = list[0]; + for (int i = 1; i < list.length; i++) { + if (list[i] > max) max = list[i]; + } + return max; + } + */ + + + static public final int min(int a, int b) { + return (a < b) ? a : b; + } + + static public final float min(float a, float b) { + return (a < b) ? a : b; + } + + /* + static public final double min(double a, double b) { + return (a < b) ? a : b; + } + */ + + + static public final int min(int a, int b, int c) { + return (a < b) ? ((a < c) ? a : c) : ((b < c) ? b : c); + } + +/** + * ( begin auto-generated from min.xml ) + * + * Determines the smallest value in a sequence of numbers. + * + * ( end auto-generated ) + * @webref math:calculation + * @param a first number + * @param b second number + * @param c third number + * @see PApplet#max(float, float, float) + */ + static public final float min(float a, float b, float c) { + return (a < b) ? ((a < c) ? a : c) : ((b < c) ? b : c); + } + + /* + static public final double min(double a, double b, double c) { + return (a < b) ? ((a < c) ? a : c) : ((b < c) ? b : c); + } + */ + + + /** + * @param list array of numbers to compare + */ + static public final int min(int[] list) { + if (list.length == 0) { + throw new ArrayIndexOutOfBoundsException(ERROR_MIN_MAX); + } + int min = list[0]; + for (int i = 1; i < list.length; i++) { + if (list[i] < min) min = list[i]; + } + return min; + } + + static public final float min(float[] list) { + if (list.length == 0) { + throw new ArrayIndexOutOfBoundsException(ERROR_MIN_MAX); + } + float min = list[0]; + for (int i = 1; i < list.length; i++) { + if (list[i] < min) min = list[i]; + } + return min; + } + + + /* + * Find the minimum value in an array. + * Throws an ArrayIndexOutOfBoundsException if the array is length 0. + * @param list the source array + * @return The minimum value + */ + /* + static public final double min(double[] list) { + if (list.length == 0) { + throw new ArrayIndexOutOfBoundsException(ERROR_MIN_MAX); + } + double min = list[0]; + for (int i = 1; i < list.length; i++) { + if (list[i] < min) min = list[i]; + } + return min; + } + */ + + + static public final int constrain(int amt, int low, int high) { + return (amt < low) ? low : ((amt > high) ? high : amt); + } + +/** + * ( begin auto-generated from constrain.xml ) + * + * Constrains a value to not exceed a maximum and minimum value. + * + * ( end auto-generated ) + * @webref math:calculation + * @param amt the value to constrain + * @param low minimum limit + * @param high maximum limit + * @see PApplet#max(float, float, float) + * @see PApplet#min(float, float, float) + */ + + static public final float constrain(float amt, float low, float high) { + return (amt < low) ? low : ((amt > high) ? high : amt); + } + +/** + * ( begin auto-generated from sin.xml ) + * + * Calculates the sine of an angle. This function expects the values of the + * angle parameter to be provided in radians (values from 0 to + * 6.28). Values are returned in the range -1 to 1. + * + * ( end auto-generated ) + * @webref math:trigonometry + * @param angle an angle in radians + * @see PApplet#cos(float) + * @see PApplet#tan(float) + * @see PApplet#radians(float) + */ + static public final float sin(float angle) { + return (float)Math.sin(angle); + } + +/** + * ( begin auto-generated from cos.xml ) + * + * Calculates the cosine of an angle. This function expects the values of + * the angle parameter to be provided in radians (values from 0 to + * PI*2). Values are returned in the range -1 to 1. + * + * ( end auto-generated ) + * @webref math:trigonometry + * @param angle an angle in radians + * @see PApplet#sin(float) + * @see PApplet#tan(float) + * @see PApplet#radians(float) + */ + static public final float cos(float angle) { + return (float)Math.cos(angle); + } + +/** + * ( begin auto-generated from tan.xml ) + * + * Calculates the ratio of the sine and cosine of an angle. This function + * expects the values of the angle parameter to be provided in + * radians (values from 0 to PI*2). Values are returned in the range + * infinity to -infinity. + * + * ( end auto-generated ) + * @webref math:trigonometry + * @param angle an angle in radians + * @see PApplet#cos(float) + * @see PApplet#sin(float) + * @see PApplet#radians(float) + */ + static public final float tan(float angle) { + return (float)Math.tan(angle); + } + +/** + * ( begin auto-generated from asin.xml ) + * + * The inverse of sin(), returns the arc sine of a value. This + * function expects the values in the range of -1 to 1 and values are + * returned in the range -PI/2 to PI/2. + * + * ( end auto-generated ) + * @webref math:trigonometry + * @param value the value whose arc sine is to be returned + * @see PApplet#sin(float) + * @see PApplet#acos(float) + * @see PApplet#atan(float) + */ + static public final float asin(float value) { + return (float)Math.asin(value); + } + +/** + * ( begin auto-generated from acos.xml ) + * + * The inverse of cos(), returns the arc cosine of a value. This + * function expects the values in the range of -1 to 1 and values are + * returned in the range 0 to PI (3.1415927). + * + * ( end auto-generated ) + * @webref math:trigonometry + * @param value the value whose arc cosine is to be returned + * @see PApplet#cos(float) + * @see PApplet#asin(float) + * @see PApplet#atan(float) + */ + static public final float acos(float value) { + return (float)Math.acos(value); + } + +/** + * ( begin auto-generated from atan.xml ) + * + * The inverse of tan(), returns the arc tangent of a value. This + * function expects the values in the range of -Infinity to Infinity + * (exclusive) and values are returned in the range -PI/2 to PI/2 . + * + * ( end auto-generated ) + * @webref math:trigonometry + * @param value -Infinity to Infinity (exclusive) + * @see PApplet#tan(float) + * @see PApplet#asin(float) + * @see PApplet#acos(float) + */ + static public final float atan(float value) { + return (float)Math.atan(value); + } + +/** + * ( begin auto-generated from atan2.xml ) + * + * Calculates the angle (in radians) from a specified point to the + * coordinate origin as measured from the positive x-axis. Values are + * returned as a float in the range from PI to -PI. + * The atan2() function is most often used for orienting geometry to + * the position of the cursor. Note: The y-coordinate of the point is the + * first parameter and the x-coordinate is the second due the the structure + * of calculating the tangent. + * + * ( end auto-generated ) + * @webref math:trigonometry + * @param y y-coordinate of the point + * @param x x-coordinate of the point + * @see PApplet#tan(float) + */ + static public final float atan2(float y, float x) { + return (float)Math.atan2(y, x); + } + +/** + * ( begin auto-generated from degrees.xml ) + * + * Converts a radian measurement to its corresponding value in degrees. + * Radians and degrees are two ways of measuring the same thing. There are + * 360 degrees in a circle and 2*PI radians in a circle. For example, + * 90° = PI/2 = 1.5707964. All trigonometric functions in Processing + * require their parameters to be specified in radians. + * + * ( end auto-generated ) + * @webref math:trigonometry + * @param radians radian value to convert to degrees + * @see PApplet#radians(float) + */ + static public final float degrees(float radians) { + return radians * RAD_TO_DEG; + } + +/** + * ( begin auto-generated from radians.xml ) + * + * Converts a degree measurement to its corresponding value in radians. + * Radians and degrees are two ways of measuring the same thing. There are + * 360 degrees in a circle and 2*PI radians in a circle. For example, + * 90° = PI/2 = 1.5707964. All trigonometric functions in Processing + * require their parameters to be specified in radians. + * + * ( end auto-generated ) + * @webref math:trigonometry + * @param degrees degree value to convert to radians + * @see PApplet#degrees(float) + */ + static public final float radians(float degrees) { + return degrees * DEG_TO_RAD; + } + +/** + * ( begin auto-generated from ceil.xml ) + * + * Calculates the closest int value that is greater than or equal to the + * value of the parameter. For example, ceil(9.03) returns the value 10. + * + * ( end auto-generated ) + * @webref math:calculation + * @param n number to round up + * @see PApplet#floor(float) + * @see PApplet#round(float) + */ + static public final int ceil(float n) { + return (int) Math.ceil(n); + } + +/** + * ( begin auto-generated from floor.xml ) + * + * Calculates the closest int value that is less than or equal to the value + * of the parameter. + * + * ( end auto-generated ) + * @webref math:calculation + * @param n number to round down + * @see PApplet#ceil(float) + * @see PApplet#round(float) + */ + static public final int floor(float n) { + return (int) Math.floor(n); + } + +/** + * ( begin auto-generated from round.xml ) + * + * Calculates the integer closest to the value parameter. For + * example, round(9.2) returns the value 9. + * + * ( end auto-generated ) + * @webref math:calculation + * @param n number to round + * @see PApplet#floor(float) + * @see PApplet#ceil(float) + */ + static public final int round(float n) { + return Math.round(n); + } + + + static public final float mag(float a, float b) { + return (float)Math.sqrt(a*a + b*b); + } + +/** + * ( begin auto-generated from mag.xml ) + * + * Calculates the magnitude (or length) of a vector. A vector is a + * direction in space commonly used in computer graphics and linear + * algebra. Because it has no "start" position, the magnitude of a vector + * can be thought of as the distance from coordinate (0,0) to its (x,y) + * value. Therefore, mag() is a shortcut for writing "dist(0, 0, x, y)". + * + * ( end auto-generated ) + * @webref math:calculation + * @param a first value + * @param b second value + * @param c third value + * @see PApplet#dist(float, float, float, float) + */ + static public final float mag(float a, float b, float c) { + return (float)Math.sqrt(a*a + b*b + c*c); + } + + + static public final float dist(float x1, float y1, float x2, float y2) { + return sqrt(sq(x2-x1) + sq(y2-y1)); + } + +/** + * ( begin auto-generated from dist.xml ) + * + * Calculates the distance between two points. + * + * ( end auto-generated ) + * @webref math:calculation + * @param x1 x-coordinate of the first point + * @param y1 y-coordinate of the first point + * @param z1 z-coordinate of the first point + * @param x2 x-coordinate of the second point + * @param y2 y-coordinate of the second point + * @param z2 z-coordinate of the second point + */ + static public final float dist(float x1, float y1, float z1, + float x2, float y2, float z2) { + return sqrt(sq(x2-x1) + sq(y2-y1) + sq(z2-z1)); + } + +/** + * ( begin auto-generated from lerp.xml ) + * + * Calculates a number between two numbers at a specific increment. The + * amt parameter is the amount to interpolate between the two values + * where 0.0 equal to the first point, 0.1 is very near the first point, + * 0.5 is half-way in between, etc. The lerp function is convenient for + * creating motion along a straight path and for drawing dotted lines. + * + * ( end auto-generated ) + * @webref math:calculation + * @param start first value + * @param stop second value + * @param amt float between 0.0 and 1.0 + * @see PGraphics#curvePoint(float, float, float, float, float) + * @see PGraphics#bezierPoint(float, float, float, float, float) + * @see PVector#lerp(PVector, float) + * @see PGraphics#lerpColor(int, int, float) + */ + static public final float lerp(float start, float stop, float amt) { + return start + (stop-start) * amt; + } + + /** + * ( begin auto-generated from norm.xml ) + * + * Normalizes a number from another range into a value between 0 and 1. + *

      + * Identical to map(value, low, high, 0, 1); + *

      + * Numbers outside the range are not clamped to 0 and 1, because + * out-of-range values are often intentional and useful. + * + * ( end auto-generated ) + * @webref math:calculation + * @param value the incoming value to be converted + * @param start lower bound of the value's current range + * @param stop upper bound of the value's current range + * @see PApplet#map(float, float, float, float, float) + * @see PApplet#lerp(float, float, float) + */ + static public final float norm(float value, float start, float stop) { + return (value - start) / (stop - start); + } + + /** + * ( begin auto-generated from map.xml ) + * + * Re-maps a number from one range to another. In the example above, + * the number '25' is converted from a value in the range 0..100 into + * a value that ranges from the left edge (0) to the right edge (width) + * of the screen. + *

      + * Numbers outside the range are not clamped to 0 and 1, because + * out-of-range values are often intentional and useful. + * + * ( end auto-generated ) + * @webref math:calculation + * @param value the incoming value to be converted + * @param start1 lower bound of the value's current range + * @param stop1 upper bound of the value's current range + * @param start2 lower bound of the value's target range + * @param stop2 upper bound of the value's target range + * @see PApplet#norm(float, float, float) + * @see PApplet#lerp(float, float, float) + */ + static public final float map(float value, + float start1, float stop1, + float start2, float stop2) { + float outgoing = + start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1)); + String badness = null; + if (outgoing != outgoing) { + badness = "NaN (not a number)"; + + } else if (outgoing == Float.NEGATIVE_INFINITY || + outgoing == Float.POSITIVE_INFINITY) { + badness = "infinity"; + } + if (badness != null) { + final String msg = + String.format("map(%s, %s, %s, %s, %s) called, which returns %s", + nf(value), nf(start1), nf(stop1), + nf(start2), nf(stop2), badness); + PGraphics.showWarning(msg); + } + return outgoing; + } + + + /* + static public final double map(double value, + double istart, double istop, + double ostart, double ostop) { + return ostart + (ostop - ostart) * ((value - istart) / (istop - istart)); + } + */ + + + + ////////////////////////////////////////////////////////////// + + // RANDOM NUMBERS + + + Random internalRandom; + + /** + * + */ + public final float random(float high) { + // avoid an infinite loop when 0 or NaN are passed in + if (high == 0 || high != high) { + return 0; + } + + if (internalRandom == null) { + internalRandom = new Random(); + } + + // for some reason (rounding error?) Math.random() * 3 + // can sometimes return '3' (once in ~30 million tries) + // so a check was added to avoid the inclusion of 'howbig' + float value = 0; + do { + value = internalRandom.nextFloat() * high; + } while (value == high); + return value; + } + + /** + * ( begin auto-generated from randomGaussian.xml ) + * + * Returns a float from a random series of numbers having a mean of 0 + * and standard deviation of 1. Each time the randomGaussian() + * function is called, it returns a number fitting a Gaussian, or + * normal, distribution. There is theoretically no minimum or maximum + * value that randomGaussian() might return. Rather, there is + * just a very low probability that values far from the mean will be + * returned; and a higher probability that numbers near the mean will + * be returned. + * + * ( end auto-generated ) + * @webref math:random + * @see PApplet#random(float,float) + * @see PApplet#noise(float, float, float) + */ + public final float randomGaussian() { + if (internalRandom == null) { + internalRandom = new Random(); + } + return (float) internalRandom.nextGaussian(); + } + + + /** + * ( begin auto-generated from random.xml ) + * + * Generates random numbers. Each time the random() function is + * called, it returns an unexpected value within the specified range. If + * one parameter is passed to the function it will return a float + * between zero and the value of the high parameter. The function + * call random(5) returns values between 0 and 5 (starting at zero, + * up to but not including 5). If two parameters are passed, it will return + * a float with a value between the the parameters. The function + * call random(-5, 10.2) returns values starting at -5 up to (but + * not including) 10.2. To convert a floating-point random number to an + * integer, use the int() function. + * + * ( end auto-generated ) + * @webref math:random + * @param low lower limit + * @param high upper limit + * @see PApplet#randomSeed(long) + * @see PApplet#noise(float, float, float) + */ + public final float random(float low, float high) { + if (low >= high) return low; + float diff = high - low; + float value = 0; + // because of rounding error, can't just add low, otherwise it may hit high + // https://github.com/processing/processing/issues/4551 + do { + value = random(diff) + low; + } while (value == high); + return value; + } + + + /** + * ( begin auto-generated from randomSeed.xml ) + * + * Sets the seed value for random(). By default, random() + * produces different results each time the program is run. Set the + * value parameter to a constant to return the same pseudo-random + * numbers each time the software is run. + * + * ( end auto-generated ) + * @webref math:random + * @param seed seed value + * @see PApplet#random(float,float) + * @see PApplet#noise(float, float, float) + * @see PApplet#noiseSeed(long) + */ + public final void randomSeed(long seed) { + if (internalRandom == null) { + internalRandom = new Random(); + } + internalRandom.setSeed(seed); + } + + + + ////////////////////////////////////////////////////////////// + + // PERLIN NOISE + + // [toxi 040903] + // octaves and amplitude amount per octave are now user controlled + // via the noiseDetail() function. + + // [toxi 030902] + // cleaned up code and now using bagel's cosine table to speed up + + // [toxi 030901] + // implementation by the german demo group farbrausch + // as used in their demo "art": http://www.farb-rausch.de/fr010src.zip + + static final int PERLIN_YWRAPB = 4; + static final int PERLIN_YWRAP = 1<random()
      function. + * It was invented by Ken Perlin in the 1980s and been used since in + * graphical applications to produce procedural textures, natural motion, + * shapes, terrains etc.

      The main difference to the + * random() function is that Perlin noise is defined in an infinite + * n-dimensional space where each pair of coordinates corresponds to a + * fixed semi-random value (fixed only for the lifespan of the program). + * The resulting value will always be between 0.0 and 1.0. Processing can + * compute 1D, 2D and 3D noise, depending on the number of coordinates + * given. The noise value can be animated by moving through the noise space + * as demonstrated in the example above. The 2nd and 3rd dimension can also + * be interpreted as time.

      The actual noise is structured + * similar to an audio signal, in respect to the function's use of + * frequencies. Similar to the concept of harmonics in physics, perlin + * noise is computed over several octaves which are added together for the + * final result.

      Another way to adjust the character of the + * resulting sequence is the scale of the input coordinates. As the + * function works within an infinite space the value of the coordinates + * doesn't matter as such, only the distance between successive coordinates + * does (eg. when using noise() within a loop). As a general rule + * the smaller the difference between coordinates, the smoother the + * resulting noise sequence will be. Steps of 0.005-0.03 work best for most + * applications, but this will differ depending on use. + * + * ( end auto-generated ) + * + * @webref math:random + * @param x x-coordinate in noise space + * @param y y-coordinate in noise space + * @param z z-coordinate in noise space + * @see PApplet#noiseSeed(long) + * @see PApplet#noiseDetail(int, float) + * @see PApplet#random(float,float) + */ + public float noise(float x, float y, float z) { + if (perlin == null) { + if (perlinRandom == null) { + perlinRandom = new Random(); + } + perlin = new float[PERLIN_SIZE + 1]; + for (int i = 0; i < PERLIN_SIZE + 1; i++) { + perlin[i] = perlinRandom.nextFloat(); //(float)Math.random(); + } + // [toxi 031112] + // noise broke due to recent change of cos table in PGraphics + // this will take care of it + perlin_cosTable = PGraphics.cosLUT; + perlin_TWOPI = perlin_PI = PGraphics.SINCOS_LENGTH; + perlin_PI >>= 1; + } + + if (x<0) x=-x; + if (y<0) y=-y; + if (z<0) z=-z; + + int xi=(int)x, yi=(int)y, zi=(int)z; + float xf = x - xi; + float yf = y - yi; + float zf = z - zi; + float rxf, ryf; + + float r=0; + float ampl=0.5f; + + float n1,n2,n3; + + for (int i=0; i=1.0f) { xi++; xf--; } + if (yf>=1.0f) { yi++; yf--; } + if (zf>=1.0f) { zi++; zf--; } + } + return r; + } + + // [toxi 031112] + // now adjusts to the size of the cosLUT used via + // the new variables, defined above + private float noise_fsc(float i) { + // using bagel's cosine table instead + return 0.5f*(1.0f-perlin_cosTable[(int)(i*perlin_PI)%perlin_TWOPI]); + } + + // [toxi 040903] + // make perlin noise quality user controlled to allow + // for different levels of detail. lower values will produce + // smoother results as higher octaves are surpressed + + /** + * ( begin auto-generated from noiseDetail.xml ) + * + * Adjusts the character and level of detail produced by the Perlin noise + * function. Similar to harmonics in physics, noise is computed over + * several octaves. Lower octaves contribute more to the output signal and + * as such define the overal intensity of the noise, whereas higher octaves + * create finer grained details in the noise sequence. By default, noise is + * computed over 4 octaves with each octave contributing exactly half than + * its predecessor, starting at 50% strength for the 1st octave. This + * falloff amount can be changed by adding an additional function + * parameter. Eg. a falloff factor of 0.75 means each octave will now have + * 75% impact (25% less) of the previous lower octave. Any value between + * 0.0 and 1.0 is valid, however note that values greater than 0.5 might + * result in greater than 1.0 values returned by noise().

      By changing these parameters, the signal created by the noise() + * function can be adapted to fit very specific needs and characteristics. + * + * ( end auto-generated ) + * @webref math:random + * @param lod number of octaves to be used by the noise + * @see PApplet#noise(float, float, float) + */ + public void noiseDetail(int lod) { + if (lod>0) perlin_octaves=lod; + } + + /** + * @see #noiseDetail(int) + * @param falloff falloff factor for each octave + */ + public void noiseDetail(int lod, float falloff) { + if (lod>0) perlin_octaves=lod; + if (falloff>0) perlin_amp_falloff=falloff; + } + + /** + * ( begin auto-generated from noiseSeed.xml ) + * + * Sets the seed value for noise(). By default, noise() + * produces different results each time the program is run. Set the + * value parameter to a constant to return the same pseudo-random + * numbers each time the software is run. + * + * ( end auto-generated ) + * @webref math:random + * @param seed seed value + * @see PApplet#noise(float, float, float) + * @see PApplet#noiseDetail(int, float) + * @see PApplet#random(float,float) + * @see PApplet#randomSeed(long) + */ + public void noiseSeed(long seed) { + if (perlinRandom == null) perlinRandom = new Random(); + perlinRandom.setSeed(seed); + // force table reset after changing the random number seed [0122] + perlin = null; + } + + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + protected String[] loadImageFormats; + + /** + * ( begin auto-generated from loadImage.xml ) + * + * Loads an image into a variable of type PImage. Four types of + * images ( .gif, .jpg, .tga, .png) images may + * be loaded. To load correctly, images must be located in the data + * directory of the current sketch. In most cases, load all images in + * setup() to preload them at the start of the program. Loading + * images inside draw() will reduce the speed of a program.
      + *
      filename parameter can also be a URL to a file found + * online. For security reasons, a Processing sketch found online can only + * download files from the same server from which it came. Getting around + * this restriction requires a signed + * applet.
      + *
      extension parameter is used to determine the image type in + * cases where the image filename does not end with a proper extension. + * Specify the extension as the second parameter to loadImage(), as + * shown in the third example on this page.
      + *
      an image is not loaded successfully, the null value is + * returned and an error message will be printed to the console. The error + * message does not halt the program, however the null value may cause a + * NullPointerException if your code does not check whether the value + * returned from loadImage() is null.
      + *
      on the type of error, a PImage object may still be + * returned, but the width and height of the image will be set to -1. This + * happens if bad image data is returned or cannot be decoded properly. + * Sometimes this happens with image URLs that produce a 403 error or that + * redirect to a password prompt, because loadImage() will attempt + * to interpret the HTML as image data. + * + * ( end auto-generated ) + * + * @webref image:loading_displaying + * @param filename name of file to load, can be .gif, .jpg, .tga, or a handful of other image types depending on your platform + * @see PImage + * @see PGraphics#image(PImage, float, float, float, float) + * @see PGraphics#imageMode(int) + * @see PGraphics#background(float, float, float, float) + */ + public PImage loadImage(String filename) { +// return loadImage(filename, null, null); + return loadImage(filename, null); + } + +// /** +// * @param extension the type of image to load, for example "png", "gif", "jpg" +// */ +// public PImage loadImage(String filename, String extension) { +// return loadImage(filename, extension, null); +// } + +// /** +// * @nowebref +// */ +// public PImage loadImage(String filename, Object params) { +// return loadImage(filename, null, params); +// } + + /** + * @param extension type of image to load, for example "png", "gif", "jpg" + */ + public PImage loadImage(String filename, String extension) { //, Object params) { + + // await... has to run on the main thread, because P2D and P3D call GL functions + // If this runs on background, requestImage() already called await... on the main thread + if (g != null && !Thread.currentThread().getName().startsWith(ASYNC_IMAGE_LOADER_THREAD_PREFIX)) { + g.awaitAsyncSaveCompletion(filename); + } + + if (extension == null) { + String lower = filename.toLowerCase(); + int dot = filename.lastIndexOf('.'); + if (dot == -1) { + extension = "unknown"; // no extension found + + } else { + extension = lower.substring(dot + 1); + + // check for, and strip any parameters on the url, i.e. + // filename.jpg?blah=blah&something=that + int question = extension.indexOf('?'); + if (question != -1) { + extension = extension.substring(0, question); + } + } + } + + // just in case. them users will try anything! + extension = extension.toLowerCase(); + + if (extension.equals("tga")) { + try { + PImage image = loadImageTGA(filename); +// if (params != null) { +// image.setParams(g, params); +// } + return image; + } catch (IOException e) { + printStackTrace(e); + return null; + } + } + + if (extension.equals("tif") || extension.equals("tiff")) { + byte bytes[] = loadBytes(filename); + PImage image = (bytes == null) ? null : PImage.loadTIFF(bytes); +// if (params != null) { +// image.setParams(g, params); +// } + return image; + } + + // For jpeg, gif, and png, load them using createImage(), + // because the javax.imageio code was found to be much slower. + // http://dev.processing.org/bugs/show_bug.cgi?id=392 + try { + if (extension.equals("jpg") || extension.equals("jpeg") || + extension.equals("gif") || extension.equals("png") || + extension.equals("unknown")) { + byte bytes[] = loadBytes(filename); + if (bytes == null) { + return null; + } else { + //Image awtImage = Toolkit.getDefaultToolkit().createImage(bytes); + Image awtImage = new ImageIcon(bytes).getImage(); + + if (awtImage instanceof BufferedImage) { + BufferedImage buffImage = (BufferedImage) awtImage; + int space = buffImage.getColorModel().getColorSpace().getType(); + if (space == ColorSpace.TYPE_CMYK) { + System.err.println(filename + " is a CMYK image, " + + "only RGB images are supported."); + return null; + /* + // wishful thinking, appears to not be supported + // https://community.oracle.com/thread/1272045?start=0&tstart=0 + BufferedImage destImage = + new BufferedImage(buffImage.getWidth(), + buffImage.getHeight(), + BufferedImage.TYPE_3BYTE_BGR); + ColorConvertOp op = new ColorConvertOp(null); + op.filter(buffImage, destImage); + image = new PImage(destImage); + */ + } + } + + PImage image = new PImage(awtImage); + if (image.width == -1) { + System.err.println("The file " + filename + + " contains bad image data, or may not be an image."); + } + + // if it's a .gif image, test to see if it has transparency + if (extension.equals("gif") || extension.equals("png") || + extension.equals("unknown")) { + image.checkAlpha(); + } + +// if (params != null) { +// image.setParams(g, params); +// } + image.parent = this; + return image; + } + } + } catch (Exception e) { + // show error, but move on to the stuff below, see if it'll work + printStackTrace(e); + } + + if (loadImageFormats == null) { + loadImageFormats = ImageIO.getReaderFormatNames(); + } + if (loadImageFormats != null) { + for (int i = 0; i < loadImageFormats.length; i++) { + if (extension.equals(loadImageFormats[i])) { + return loadImageIO(filename); +// PImage image = loadImageIO(filename); +// if (params != null) { +// image.setParams(g, params); +// } +// return image; + } + } + } + + // failed, could not load image after all those attempts + System.err.println("Could not find a method to load " + filename); + return null; + } + + + public PImage requestImage(String filename) { +// return requestImage(filename, null, null); + return requestImage(filename, null); + } + + + /** + * ( begin auto-generated from requestImage.xml ) + * + * This function load images on a separate thread so that your sketch does + * not freeze while images load during setup(). While the image is + * loading, its width and height will be 0. If an error occurs while + * loading the image, its width and height will be set to -1. You'll know + * when the image has loaded properly because its width and height will be + * greater than 0. Asynchronous image loading (particularly when + * downloading from a server) can dramatically improve performance.
      + *
      extension parameter is used to determine the image type in + * cases where the image filename does not end with a proper extension. + * Specify the extension as the second parameter to requestImage(). + * + * ( end auto-generated ) + * + * @webref image:loading_displaying + * @param filename name of the file to load, can be .gif, .jpg, .tga, or a handful of other image types depending on your platform + * @param extension the type of image to load, for example "png", "gif", "jpg" + * @see PImage + * @see PApplet#loadImage(String, String) + */ + public PImage requestImage(String filename, String extension) { + // Make sure saving to this file completes before trying to load it + // Has to be called on main thread, because P2D and P3D need GL functions + if (g != null) { + g.awaitAsyncSaveCompletion(filename); + } + PImage vessel = createImage(0, 0, ARGB); + AsyncImageLoader ail = + new AsyncImageLoader(filename, extension, vessel); + ail.start(); + return vessel; + } + + +// /** +// * @nowebref +// */ +// public PImage requestImage(String filename, String extension, Object params) { +// PImage vessel = createImage(0, 0, ARGB, params); +// AsyncImageLoader ail = +// new AsyncImageLoader(filename, extension, vessel); +// ail.start(); +// return vessel; +// } + + + /** + * By trial and error, four image loading threads seem to work best when + * loading images from online. This is consistent with the number of open + * connections that web browsers will maintain. The variable is made public + * (however no accessor has been added since it's esoteric) if you really + * want to have control over the value used. For instance, when loading local + * files, it might be better to only have a single thread (or two) loading + * images so that you're disk isn't simply jumping around. + */ + public int requestImageMax = 4; + volatile int requestImageCount; + + private static final String ASYNC_IMAGE_LOADER_THREAD_PREFIX = "ASYNC_IMAGE_LOADER"; + + class AsyncImageLoader extends Thread { + String filename; + String extension; + PImage vessel; + + public AsyncImageLoader(String filename, String extension, PImage vessel) { + // Give these threads distinct name so we can check whether we are loading + // on the main/background thread; for now they are all named the same + super(ASYNC_IMAGE_LOADER_THREAD_PREFIX); + this.filename = filename; + this.extension = extension; + this.vessel = vessel; + } + + @Override + public void run() { + while (requestImageCount == requestImageMax) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { } + } + requestImageCount++; + + PImage actual = loadImage(filename, extension); + + // An error message should have already printed + if (actual == null) { + vessel.width = -1; + vessel.height = -1; + + } else { + vessel.width = actual.width; + vessel.height = actual.height; + vessel.format = actual.format; + vessel.pixels = actual.pixels; + + vessel.pixelWidth = actual.width; + vessel.pixelHeight = actual.height; + vessel.pixelDensity = 1; + } + requestImageCount--; + } + } + + + // done internally by ImageIcon +// /** +// * Load an AWT image synchronously by setting up a MediaTracker for +// * a single image, and blocking until it has loaded. +// */ +// protected PImage loadImageMT(Image awtImage) { +// MediaTracker tracker = new MediaTracker(this); +// tracker.addImage(awtImage, 0); +// try { +// tracker.waitForAll(); +// } catch (InterruptedException e) { +// //e.printStackTrace(); // non-fatal, right? +// } +// +// PImage image = new PImage(awtImage); +// image.parent = this; +// return image; +// } + + + /** + * Use Java 1.4 ImageIO methods to load an image. + */ + protected PImage loadImageIO(String filename) { + InputStream stream = createInput(filename); + if (stream == null) { + System.err.println("The image " + filename + " could not be found."); + return null; + } + + try { + BufferedImage bi = ImageIO.read(stream); + PImage outgoing = new PImage(bi.getWidth(), bi.getHeight()); + outgoing.parent = this; + + bi.getRGB(0, 0, outgoing.width, outgoing.height, + outgoing.pixels, 0, outgoing.width); + + // check the alpha for this image + // was gonna call getType() on the image to see if RGB or ARGB, + // but it's not actually useful, since gif images will come through + // as TYPE_BYTE_INDEXED, which means it'll still have to check for + // the transparency. also, would have to iterate through all the other + // types and guess whether alpha was in there, so.. just gonna stick + // with the old method. + outgoing.checkAlpha(); + + stream.close(); + // return the image + return outgoing; + + } catch (Exception e) { + printStackTrace(e); + return null; + } + } + + + /** + * Targa image loader for RLE-compressed TGA files. + *

      + * Rewritten for 0115 to read/write RLE-encoded targa images. + * For 0125, non-RLE encoded images are now supported, along with + * images whose y-order is reversed (which is standard for TGA files). + *

      + * A version of this function is in MovieMaker.java. Any fixes here + * should be applied over in MovieMaker as well. + *

      + * Known issue with RLE encoding and odd behavior in some apps: + * https://github.com/processing/processing/issues/2096 + * Please help! + */ + protected PImage loadImageTGA(String filename) throws IOException { + InputStream is = createInput(filename); + if (is == null) return null; + + byte header[] = new byte[18]; + int offset = 0; + do { + int count = is.read(header, offset, header.length - offset); + if (count == -1) return null; + offset += count; + } while (offset < 18); + + /* + header[2] image type code + 2 (0x02) - Uncompressed, RGB images. + 3 (0x03) - Uncompressed, black and white images. + 10 (0x0A) - Run-length encoded RGB images. + 11 (0x0B) - Compressed, black and white images. (grayscale?) + + header[16] is the bit depth (8, 24, 32) + + header[17] image descriptor (packed bits) + 0x20 is 32 = origin upper-left + 0x28 is 32 + 8 = origin upper-left + 32 bits + + 7 6 5 4 3 2 1 0 + 128 64 32 16 8 4 2 1 + */ + + int format = 0; + + if (((header[2] == 3) || (header[2] == 11)) && // B&W, plus RLE or not + (header[16] == 8) && // 8 bits + ((header[17] == 0x8) || (header[17] == 0x28))) { // origin, 32 bit + format = ALPHA; + + } else if (((header[2] == 2) || (header[2] == 10)) && // RGB, RLE or not + (header[16] == 24) && // 24 bits + ((header[17] == 0x20) || (header[17] == 0))) { // origin + format = RGB; + + } else if (((header[2] == 2) || (header[2] == 10)) && + (header[16] == 32) && + ((header[17] == 0x8) || (header[17] == 0x28))) { // origin, 32 + format = ARGB; + } + + if (format == 0) { + System.err.println("Unknown .tga file format for " + filename); + //" (" + header[2] + " " + + //(header[16] & 0xff) + " " + + //hex(header[17], 2) + ")"); + return null; + } + + int w = ((header[13] & 0xff) << 8) + (header[12] & 0xff); + int h = ((header[15] & 0xff) << 8) + (header[14] & 0xff); + PImage outgoing = createImage(w, h, format); + + // where "reversed" means upper-left corner (normal for most of + // the modernized world, but "reversed" for the tga spec) + //boolean reversed = (header[17] & 0x20) != 0; + // https://github.com/processing/processing/issues/1682 + boolean reversed = (header[17] & 0x20) == 0; + + if ((header[2] == 2) || (header[2] == 3)) { // not RLE encoded + if (reversed) { + int index = (h-1) * w; + switch (format) { + case ALPHA: + for (int y = h-1; y >= 0; y--) { + for (int x = 0; x < w; x++) { + outgoing.pixels[index + x] = is.read(); + } + index -= w; + } + break; + case RGB: + for (int y = h-1; y >= 0; y--) { + for (int x = 0; x < w; x++) { + outgoing.pixels[index + x] = + is.read() | (is.read() << 8) | (is.read() << 16) | + 0xff000000; + } + index -= w; + } + break; + case ARGB: + for (int y = h-1; y >= 0; y--) { + for (int x = 0; x < w; x++) { + outgoing.pixels[index + x] = + is.read() | (is.read() << 8) | (is.read() << 16) | + (is.read() << 24); + } + index -= w; + } + } + } else { // not reversed + int count = w * h; + switch (format) { + case ALPHA: + for (int i = 0; i < count; i++) { + outgoing.pixels[i] = is.read(); + } + break; + case RGB: + for (int i = 0; i < count; i++) { + outgoing.pixels[i] = + is.read() | (is.read() << 8) | (is.read() << 16) | + 0xff000000; + } + break; + case ARGB: + for (int i = 0; i < count; i++) { + outgoing.pixels[i] = + is.read() | (is.read() << 8) | (is.read() << 16) | + (is.read() << 24); + } + break; + } + } + + } else { // header[2] is 10 or 11 + int index = 0; + int px[] = outgoing.pixels; + + while (index < px.length) { + int num = is.read(); + boolean isRLE = (num & 0x80) != 0; + if (isRLE) { + num -= 127; // (num & 0x7F) + 1 + int pixel = 0; + switch (format) { + case ALPHA: + pixel = is.read(); + break; + case RGB: + pixel = 0xFF000000 | + is.read() | (is.read() << 8) | (is.read() << 16); + //(is.read() << 16) | (is.read() << 8) | is.read(); + break; + case ARGB: + pixel = is.read() | + (is.read() << 8) | (is.read() << 16) | (is.read() << 24); + break; + } + for (int i = 0; i < num; i++) { + px[index++] = pixel; + if (index == px.length) break; + } + } else { // write up to 127 bytes as uncompressed + num += 1; + switch (format) { + case ALPHA: + for (int i = 0; i < num; i++) { + px[index++] = is.read(); + } + break; + case RGB: + for (int i = 0; i < num; i++) { + px[index++] = 0xFF000000 | + is.read() | (is.read() << 8) | (is.read() << 16); + //(is.read() << 16) | (is.read() << 8) | is.read(); + } + break; + case ARGB: + for (int i = 0; i < num; i++) { + px[index++] = is.read() | //(is.read() << 24) | + (is.read() << 8) | (is.read() << 16) | (is.read() << 24); + //(is.read() << 16) | (is.read() << 8) | is.read(); + } + break; + } + } + } + + if (!reversed) { + int[] temp = new int[w]; + for (int y = 0; y < h/2; y++) { + int z = (h-1) - y; + System.arraycopy(px, y*w, temp, 0, w); + System.arraycopy(px, z*w, px, y*w, w); + System.arraycopy(temp, 0, px, z*w, w); + } + } + } + is.close(); + return outgoing; + } + + + + ////////////////////////////////////////////////////////////// + + // DATA I/O + + +// /** +// * @webref input:files +// * @brief Creates a new XML object +// * @param name the name to be given to the root element of the new XML object +// * @return an XML object, or null +// * @see XML +// * @see PApplet#loadXML(String) +// * @see PApplet#parseXML(String) +// * @see PApplet#saveXML(XML, String) +// */ +// public XML createXML(String name) { +// try { +// return new XML(name); +// } catch (Exception e) { +// e.printStackTrace(); +// return null; +// } +// } + + + /** + * @webref input:files + * @param filename name of a file in the data folder or a URL. + * @see XML + * @see PApplet#parseXML(String) + * @see PApplet#saveXML(XML, String) + * @see PApplet#loadBytes(String) + * @see PApplet#loadStrings(String) + * @see PApplet#loadTable(String) + */ + public XML loadXML(String filename) { + return loadXML(filename, null); + } + + + // version that uses 'options' though there are currently no supported options + /** + * @nowebref + */ + public XML loadXML(String filename, String options) { + try { + return new XML(createReader(filename), options); + + // can't use catch-all exception, since it might catch the + // RuntimeException about the incorrect case sensitivity + } catch (IOException e) { + throw new RuntimeException(e); + + } catch (ParserConfigurationException e) { + throw new RuntimeException(e); + + } catch (SAXException e) { + throw new RuntimeException(e); + } + } + + + /** + * @webref input:files + * @brief Converts String content to an XML object + * @param xmlString the content to be parsed as XML + * @return an XML object, or null + * @see XML + * @see PApplet#loadXML(String) + * @see PApplet#saveXML(XML, String) + */ + public XML parseXML(String xmlString) { + return parseXML(xmlString, null); + } + + + public XML parseXML(String xmlString, String options) { + try { + return XML.parse(xmlString, options); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + /** + * @webref output:files + * @param xml the XML object to save to disk + * @param filename name of the file to write to + * @see XML + * @see PApplet#loadXML(String) + * @see PApplet#parseXML(String) + */ + public boolean saveXML(XML xml, String filename) { + return saveXML(xml, filename, null); + } + + /** + * @nowebref + */ + public boolean saveXML(XML xml, String filename, String options) { + return xml.save(saveFile(filename), options); + } + + /** + * @webref input:files + * @param input String to parse as a JSONObject + * @see PApplet#loadJSONObject(String) + * @see PApplet#saveJSONObject(JSONObject, String) + */ + public JSONObject parseJSONObject(String input) { + return new JSONObject(new StringReader(input)); + } + + + /** + * @webref input:files + * @param filename name of a file in the data folder or a URL + * @see JSONObject + * @see JSONArray + * @see PApplet#loadJSONArray(String) + * @see PApplet#saveJSONObject(JSONObject, String) + * @see PApplet#saveJSONArray(JSONArray, String) + */ + public JSONObject loadJSONObject(String filename) { + return new JSONObject(createReader(filename)); + } + + /** + * @nowebref + */ + static public JSONObject loadJSONObject(File file) { + return new JSONObject(createReader(file)); + } + + + /** + * @webref output:files + * @param json the JSONObject to save + * @param filename the name of the file to save to + * @see JSONObject + * @see JSONArray + * @see PApplet#loadJSONObject(String) + * @see PApplet#loadJSONArray(String) + * @see PApplet#saveJSONArray(JSONArray, String) + */ + public boolean saveJSONObject(JSONObject json, String filename) { + return saveJSONObject(json, filename, null); + } + + /** + * @param options "compact" and "indent=N", replace N with the number of spaces + */ + public boolean saveJSONObject(JSONObject json, String filename, String options) { + return json.save(saveFile(filename), options); + } + +/** + * @webref input:files + * @param input String to parse as a JSONArray + * @see JSONObject + * @see PApplet#loadJSONObject(String) + * @see PApplet#saveJSONObject(JSONObject, String) + */ + public JSONArray parseJSONArray(String input) { + return new JSONArray(new StringReader(input)); + } + + + /** + * @webref input:files + * @param filename name of a file in the data folder or a URL + * @see JSONArray + * @see PApplet#loadJSONObject(String) + * @see PApplet#saveJSONObject(JSONObject, String) + * @see PApplet#saveJSONArray(JSONArray, String) + */ + public JSONArray loadJSONArray(String filename) { + return new JSONArray(createReader(filename)); + } + + + static public JSONArray loadJSONArray(File file) { + return new JSONArray(createReader(file)); + } + + + /** + * @webref output:files + * @param json the JSONArray to save + * @param filename the name of the file to save to + * @see JSONObject + * @see JSONArray + * @see PApplet#loadJSONObject(String) + * @see PApplet#loadJSONArray(String) + * @see PApplet#saveJSONObject(JSONObject, String) + */ + public boolean saveJSONArray(JSONArray json, String filename) { + return saveJSONArray(json, filename, null); + } + + /** + * @param options "compact" and "indent=N", replace N with the number of spaces + */ + public boolean saveJSONArray(JSONArray json, String filename, String options) { + return json.save(saveFile(filename), options); + } + + + +// /** +// * @webref input:files +// * @see Table +// * @see PApplet#loadTable(String) +// * @see PApplet#saveTable(Table, String) +// */ +// public Table createTable() { +// return new Table(); +// } + + + /** + * @webref input:files + * @param filename name of a file in the data folder or a URL. + * @see Table + * @see PApplet#saveTable(Table, String) + * @see PApplet#loadBytes(String) + * @see PApplet#loadStrings(String) + * @see PApplet#loadXML(String) + */ + public Table loadTable(String filename) { + return loadTable(filename, null); + } + + + /** + * Options may contain "header", "tsv", "csv", or "bin" separated by commas. + * + * Another option is "dictionary=filename.tsv", which allows users to + * specify a "dictionary" file that contains a mapping of the column titles + * and the data types used in the table file. This can be far more efficient + * (in terms of speed and memory usage) for loading and parsing tables. The + * dictionary file can only be tab separated values (.tsv) and its extension + * will be ignored. This option was added in Processing 2.0.2. + * + * @param options may contain "header", "tsv", "csv", or "bin" separated by commas + */ + public Table loadTable(String filename, String options) { + try { + String optionStr = Table.extensionOptions(true, filename, options); + String[] optionList = trim(split(optionStr, ',')); + + Table dictionary = null; + for (String opt : optionList) { + if (opt.startsWith("dictionary=")) { + dictionary = loadTable(opt.substring(opt.indexOf('=') + 1), "tsv"); + return dictionary.typedParse(createInput(filename), optionStr); + } + } + InputStream input = createInput(filename); + if (input == null) { + System.err.println(filename + " does not exist or could not be read"); + return null; + } + return new Table(input, optionStr); + + } catch (IOException e) { + printStackTrace(e); + return null; + } + } + + + /** + * @webref output:files + * @param table the Table object to save to a file + * @param filename the filename to which the Table should be saved + * @see Table + * @see PApplet#loadTable(String) + */ + public boolean saveTable(Table table, String filename) { + return saveTable(table, filename, null); + } + + + /** + * @param options can be one of "tsv", "csv", "bin", or "html" + */ + public boolean saveTable(Table table, String filename, String options) { +// String ext = checkExtension(filename); +// if (ext != null) { +// if (ext.equals("csv") || ext.equals("tsv") || ext.equals("bin") || ext.equals("html")) { +// if (options == null) { +// options = ext; +// } else { +// options = ext + "," + options; +// } +// } +// } + + try { + // Figure out location and make sure the target path exists + File outputFile = saveFile(filename); + // Open a stream and take care of .gz if necessary + return table.save(outputFile, options); + + } catch (IOException e) { + printStackTrace(e); + return false; + } + } + + + + ////////////////////////////////////////////////////////////// + + // FONT I/O + + /** + * ( begin auto-generated from loadFont.xml ) + * + * Loads a font into a variable of type PFont. To load correctly, + * fonts must be located in the data directory of the current sketch. To + * create a font to use with Processing, select "Create Font..." from the + * Tools menu. This will create a font in the format Processing requires + * and also adds it to the current sketch's data directory.
      + *
      + * Like loadImage() and other functions that load data, the + * loadFont() function should not be used inside draw(), + * because it will slow down the sketch considerably, as the font will be + * re-loaded from the disk (or network) on each frame.
      + *
      + * For most renderers, Processing displays fonts using the .vlw font + * format, which uses images for each letter, rather than defining them + * through vector data. When hint(ENABLE_NATIVE_FONTS) is used with + * the JAVA2D renderer, the native version of a font will be used if it is + * installed on the user's machine.
      + *
      + * Using createFont() (instead of loadFont) enables vector data to + * be used with the JAVA2D (default) renderer setting. This can be helpful + * when many font sizes are needed, or when using any renderer based on + * JAVA2D, such as the PDF library. + * + * ( end auto-generated ) + * @webref typography:loading_displaying + * @param filename name of the font to load + * @see PFont + * @see PGraphics#textFont(PFont, float) + * @see PApplet#createFont(String, float, boolean, char[]) + */ + public PFont loadFont(String filename) { + if (!filename.toLowerCase().endsWith(".vlw")) { + throw new IllegalArgumentException("loadFont() is for .vlw files, try createFont()"); + } + try { + InputStream input = createInput(filename); + return new PFont(input); + + } catch (Exception e) { + die("Could not load font " + filename + ". " + + "Make sure that the font has been copied " + + "to the data folder of your sketch.", e); + } + return null; + } + + + /** + * Used by PGraphics to remove the requirement for loading a font! + */ + protected PFont createDefaultFont(float size) { +// Font f = new Font("SansSerif", Font.PLAIN, 12); +// println("n: " + f.getName()); +// println("fn: " + f.getFontName()); +// println("ps: " + f.getPSName()); + return createFont("Lucida Sans", size, true, null); + } + + + public PFont createFont(String name, float size) { + return createFont(name, size, true, null); + } + + + public PFont createFont(String name, float size, boolean smooth) { + return createFont(name, size, smooth, null); + } + + + /** + * ( begin auto-generated from createFont.xml ) + * + * Dynamically converts a font to the format used by Processing from either + * a font name that's installed on the computer, or from a .ttf or .otf + * file inside the sketches "data" folder. This function is an advanced + * feature for precise control. On most occasions you should create fonts + * through selecting "Create Font..." from the Tools menu. + *

      + * Use the PFont.list() method to first determine the names for the + * fonts recognized by the computer and are compatible with this function. + * Because of limitations in Java, not all fonts can be used and some might + * work with one operating system and not others. When sharing a sketch + * with other people or posting it on the web, you may need to include a + * .ttf or .otf version of your font in the data directory of the sketch + * because other people might not have the font installed on their + * computer. Only fonts that can legally be distributed should be included + * with a sketch. + *

      + * The size parameter states the font size you want to generate. The + * smooth parameter specifies if the font should be antialiased or + * not, and the charset parameter is an array of chars that + * specifies the characters to generate. + *

      + * This function creates a bitmapped version of a font in the same manner + * as the Create Font tool. It loads a font by name, and converts it to a + * series of images based on the size of the font. When possible, the + * text() function will use a native font rather than the bitmapped + * version created behind the scenes with createFont(). For + * instance, when using P2D, the actual native version of the font will be + * employed by the sketch, improving drawing quality and performance. With + * the P3D renderer, the bitmapped version will be used. While this can + * drastically improve speed and appearance, results are poor when + * exporting if the sketch does not include the .otf or .ttf file, and the + * requested font is not available on the machine running the sketch. + * + * ( end auto-generated ) + * @webref typography:loading_displaying + * @param name name of the font to load + * @param size point size of the font + * @param smooth true for an antialiased font, false for aliased + * @param charset array containing characters to be generated + * @see PFont + * @see PGraphics#textFont(PFont, float) + * @see PGraphics#text(String, float, float, float, float) + * @see PApplet#loadFont(String) + */ + public PFont createFont(String name, float size, + boolean smooth, char[] charset) { + if (g == null) { + throw new RuntimeException("createFont() can only be used inside setup() or after setup() has been called."); + } + return g.createFont(name, size, smooth, charset); + } + + + + ////////////////////////////////////////////////////////////// + + // FILE/FOLDER SELECTION + + + /* + private Frame selectFrame; + + private Frame selectFrame() { + if (frame != null) { + selectFrame = frame; + + } else if (selectFrame == null) { + Component comp = getParent(); + while (comp != null) { + if (comp instanceof Frame) { + selectFrame = (Frame) comp; + break; + } + comp = comp.getParent(); + } + // Who you callin' a hack? + if (selectFrame == null) { + selectFrame = new Frame(); + } + } + return selectFrame; + } + */ + + + static private boolean lookAndFeelCheck; + + /** + * Initialize the Look & Feel if it hasn't been already. + * Call this before using any Swing-related code in PApplet methods. + */ + static private void checkLookAndFeel() { + if (!lookAndFeelCheck) { + if (platform == WINDOWS) { + // Windows is defaulting to Metal or something else awful. + // Which also is not scaled properly with HiDPI interfaces. + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { } + } + lookAndFeelCheck = true; + } + } + + /** + * Open a platform-specific file chooser dialog to select a file for input. + * After the selection is made, the selected File will be passed to the + * 'callback' function. If the dialog is closed or canceled, null will be + * sent to the function, so that the program is not waiting for additional + * input. The callback is necessary because of how threading works. + * + *

      +   * void setup() {
      +   *   selectInput("Select a file to process:", "fileSelected");
      +   * }
      +   *
      +   * void fileSelected(File selection) {
      +   *   if (selection == null) {
      +   *     println("Window was closed or the user hit cancel.");
      +   *   } else {
      +   *     println("User selected " + fileSeleted.getAbsolutePath());
      +   *   }
      +   * }
      +   * 
      + * + * For advanced users, the method must be 'public', which is true for all + * methods inside a sketch when run from the PDE, but must explicitly be + * set when using Eclipse or other development environments. + * + * @webref input:files + * @param prompt message to the user + * @param callback name of the method to be called when the selection is made + */ + public void selectInput(String prompt, String callback) { + selectInput(prompt, callback, null); + } + + + public void selectInput(String prompt, String callback, File file) { + selectInput(prompt, callback, file, this); + } + + + public void selectInput(String prompt, String callback, + File file, Object callbackObject) { + selectInput(prompt, callback, file, callbackObject, null, this); //selectFrame()); + } + + + static public void selectInput(String prompt, String callbackMethod, + File file, Object callbackObject, Frame parent, + PApplet sketch) { + selectImpl(prompt, callbackMethod, file, callbackObject, parent, FileDialog.LOAD, sketch); + } + + + static public void selectInput(String prompt, String callbackMethod, + File file, Object callbackObject, Frame parent) { + selectImpl(prompt, callbackMethod, file, callbackObject, parent, FileDialog.LOAD, null); + } + + + /** + * See selectInput() for details. + * + * @webref output:files + * @param prompt message to the user + * @param callback name of the method to be called when the selection is made + */ + public void selectOutput(String prompt, String callback) { + selectOutput(prompt, callback, null); + } + + + public void selectOutput(String prompt, String callback, File file) { + selectOutput(prompt, callback, file, this); + } + + + public void selectOutput(String prompt, String callback, + File file, Object callbackObject) { + selectOutput(prompt, callback, file, callbackObject, null, this); //selectFrame()); + } + + + static public void selectOutput(String prompt, String callbackMethod, + File file, Object callbackObject, Frame parent) { + selectImpl(prompt, callbackMethod, file, callbackObject, parent, FileDialog.SAVE, null); + } + + + static public void selectOutput(String prompt, String callbackMethod, + File file, Object callbackObject, Frame parent, + PApplet sketch) { + selectImpl(prompt, callbackMethod, file, callbackObject, parent, FileDialog.SAVE, sketch); + } + + + // Will remove the 'sketch' parameter once we get an upstream JOGL fix + // https://github.com/processing/processing/issues/3831 + static protected void selectImpl(final String prompt, + final String callbackMethod, + final File defaultSelection, + final Object callbackObject, + final Frame parentFrame, + final int mode, + final PApplet sketch) { + EventQueue.invokeLater(new Runnable() { + public void run() { + File selectedFile = null; + + boolean hide = (sketch != null) && + (sketch.g instanceof PGraphicsOpenGL) && (platform == WINDOWS); + if (hide) sketch.surface.setVisible(false); + + if (useNativeSelect) { + FileDialog dialog = new FileDialog(parentFrame, prompt, mode); + if (defaultSelection != null) { + dialog.setDirectory(defaultSelection.getParent()); + dialog.setFile(defaultSelection.getName()); + } + + dialog.setVisible(true); + String directory = dialog.getDirectory(); + String filename = dialog.getFile(); + if (filename != null) { + selectedFile = new File(directory, filename); + } + + } else { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle(prompt); + if (defaultSelection != null) { + chooser.setSelectedFile(defaultSelection); + } + + int result = -1; + if (mode == FileDialog.SAVE) { + result = chooser.showSaveDialog(parentFrame); + } else if (mode == FileDialog.LOAD) { + result = chooser.showOpenDialog(parentFrame); + } + if (result == JFileChooser.APPROVE_OPTION) { + selectedFile = chooser.getSelectedFile(); + } + } + + if (hide) sketch.surface.setVisible(true); + selectCallback(selectedFile, callbackMethod, callbackObject); + } + }); + } + + + /** + * See selectInput() for details. + * + * @webref input:files + * @param prompt message to the user + * @param callback name of the method to be called when the selection is made + */ + public void selectFolder(String prompt, String callback) { + selectFolder(prompt, callback, null); + } + + + public void selectFolder(String prompt, String callback, File file) { + selectFolder(prompt, callback, file, this); + } + + + public void selectFolder(String prompt, String callback, + File file, Object callbackObject) { + selectFolder(prompt, callback, file, callbackObject, null, this); //selectFrame()); + } + + + static public void selectFolder(final String prompt, + final String callbackMethod, + final File defaultSelection, + final Object callbackObject, + final Frame parentFrame) { + selectFolder(prompt, callbackMethod, defaultSelection, callbackObject, parentFrame, null); + } + + + // Will remove the 'sketch' parameter once we get an upstream JOGL fix + // https://github.com/processing/processing/issues/3831 + static public void selectFolder(final String prompt, + final String callbackMethod, + final File defaultSelection, + final Object callbackObject, + final Frame parentFrame, + final PApplet sketch) { + EventQueue.invokeLater(new Runnable() { + public void run() { + File selectedFile = null; + + boolean hide = (sketch != null) && + (sketch.g instanceof PGraphicsOpenGL) && (platform == WINDOWS); + if (hide) sketch.surface.setVisible(false); + + if (platform == MACOSX && useNativeSelect != false) { + FileDialog fileDialog = + new FileDialog(parentFrame, prompt, FileDialog.LOAD); + if (defaultSelection != null) { + fileDialog.setDirectory(defaultSelection.getAbsolutePath()); + } + System.setProperty("apple.awt.fileDialogForDirectories", "true"); + fileDialog.setVisible(true); + System.setProperty("apple.awt.fileDialogForDirectories", "false"); + String filename = fileDialog.getFile(); + if (filename != null) { + selectedFile = new File(fileDialog.getDirectory(), fileDialog.getFile()); + } + } else { + checkLookAndFeel(); + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setDialogTitle(prompt); + fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + if (defaultSelection != null) { + fileChooser.setCurrentDirectory(defaultSelection); + } + + int result = fileChooser.showOpenDialog(parentFrame); + if (result == JFileChooser.APPROVE_OPTION) { + selectedFile = fileChooser.getSelectedFile(); + } + } + + if (hide) sketch.surface.setVisible(true); + selectCallback(selectedFile, callbackMethod, callbackObject); + } + }); + } + + + static private void selectCallback(File selectedFile, + String callbackMethod, + Object callbackObject) { + try { + Class callbackClass = callbackObject.getClass(); + Method selectMethod = + callbackClass.getMethod(callbackMethod, new Class[] { File.class }); + selectMethod.invoke(callbackObject, new Object[] { selectedFile }); + + } catch (IllegalAccessException iae) { + System.err.println(callbackMethod + "() must be public"); + + } catch (InvocationTargetException ite) { + ite.printStackTrace(); + + } catch (NoSuchMethodException nsme) { + System.err.println(callbackMethod + "() could not be found"); + } + } + + + + ////////////////////////////////////////////////////////////// + + // LISTING DIRECTORIES + + + public String[] listPaths(String path, String... options) { + File[] list = listFiles(path, options); + + int offset = 0; + for (String opt : options) { + if (opt.equals("relative")) { + if (!path.endsWith(File.pathSeparator)) { + path += File.pathSeparator; + } + offset = path.length(); + break; + } + } + String[] outgoing = new String[list.length]; + for (int i = 0; i < list.length; i++) { + // as of Java 1.8, substring(0) returns the original object + outgoing[i] = list[i].getAbsolutePath().substring(offset); + } + return outgoing; + } + + + public File[] listFiles(String path, String... options) { + File file = new File(path); + // if not an absolute path, make it relative to the sketch folder + if (!file.isAbsolute()) { + file = sketchFile(path); + } + return listFiles(file, options); + } + + + // "relative" -> no effect with the Files version, but important for listPaths + // "recursive" + // "extension=js" or "extensions=js|csv|txt" (no dot) + // "directories" -> only directories + // "files" -> only files + // "hidden" -> include hidden files (prefixed with .) disabled by default + static public File[] listFiles(File base, String... options) { + boolean recursive = false; + String[] extensions = null; + boolean directories = true; + boolean files = true; + boolean hidden = false; + + for (String opt : options) { + if (opt.equals("recursive")) { + recursive = true; + } else if (opt.startsWith("extension=")) { + extensions = new String[] { opt.substring(10) }; + } else if (opt.startsWith("extensions=")) { + extensions = split(opt.substring(10), ','); + } else if (opt.equals("files")) { + directories = false; + } else if (opt.equals("directories")) { + files = false; + } else if (opt.equals("hidden")) { + hidden = true; + } else if (opt.equals("relative")) { + // ignored + } else { + throw new RuntimeException(opt + " is not a listFiles() option"); + } + } + + if (extensions != null) { + for (int i = 0; i < extensions.length; i++) { + extensions[i] = "." + extensions[i]; + } + } + + if (!files && !directories) { + // just make "only files" and "only directories" mean... both + files = true; + directories = true; + } + + if (!base.canRead()) { + return null; + } + + List outgoing = new ArrayList<>(); + listFilesImpl(base, recursive, extensions, hidden, directories, files, outgoing); + return outgoing.toArray(new File[0]); + } + + + static void listFilesImpl(File folder, boolean recursive, + String[] extensions, boolean hidden, + boolean directories, boolean files, + List list) { + File[] items = folder.listFiles(); + if (items != null) { + for (File item : items) { + String name = item.getName(); + if (!hidden && name.charAt(0) == '.') { + continue; + } + if (item.isDirectory()) { + if (recursive) { + listFilesImpl(item, recursive, extensions, hidden, directories, files, list); + } + if (directories) { + list.add(item); + } + } else if (files) { + if (extensions == null) { + list.add(item); + } else { + for (String ext : extensions) { + if (item.getName().toLowerCase().endsWith(ext)) { + list.add(item); + } + } + } + } + } + } + } + + + + ////////////////////////////////////////////////////////////// + + // EXTENSIONS + + + /** + * Get the compression-free extension for this filename. + * @param filename The filename to check + * @return an extension, skipping past .gz if it's present + */ + static public String checkExtension(String filename) { + // Don't consider the .gz as part of the name, createInput() + // and createOuput() will take care of fixing that up. + if (filename.toLowerCase().endsWith(".gz")) { + filename = filename.substring(0, filename.length() - 3); + } + int dotIndex = filename.lastIndexOf('.'); + if (dotIndex != -1) { + return filename.substring(dotIndex + 1).toLowerCase(); + } + return null; + } + + + + ////////////////////////////////////////////////////////////// + + // READERS AND WRITERS + + + /** + * ( begin auto-generated from createReader.xml ) + * + * Creates a BufferedReader object that can be used to read files + * line-by-line as individual String objects. This is the complement + * to the createWriter() function. + *

      + * Starting with Processing release 0134, all files loaded and saved by the + * Processing API use UTF-8 encoding. In previous releases, the default + * encoding for your platform was used, which causes problems when files + * are moved to other platforms. + * + * ( end auto-generated ) + * @webref input:files + * @param filename name of the file to be opened + * @see BufferedReader + * @see PApplet#createWriter(String) + * @see PrintWriter + */ + public BufferedReader createReader(String filename) { + InputStream is = createInput(filename); + if (is == null) { + System.err.println("The file \"" + filename + "\" " + + "is missing or inaccessible, make sure " + + "the URL is valid or that the file has been " + + "added to your sketch and is readable."); + return null; + } + return createReader(is); + } + + + /** + * @nowebref + */ + static public BufferedReader createReader(File file) { + try { + InputStream is = new FileInputStream(file); + if (file.getName().toLowerCase().endsWith(".gz")) { + is = new GZIPInputStream(is); + } + return createReader(is); + + } catch (IOException e) { + // Re-wrap rather than forcing novices to learn about exceptions + throw new RuntimeException(e); + } + } + + + /** + * @nowebref + * I want to read lines from a stream. If I have to type the + * following lines any more I'm gonna send Sun my medical bills. + */ + static public BufferedReader createReader(InputStream input) { + InputStreamReader isr = + new InputStreamReader(input, StandardCharsets.UTF_8); + + BufferedReader reader = new BufferedReader(isr); + // consume the Unicode BOM (byte order marker) if present + try { + reader.mark(1); + int c = reader.read(); + // if not the BOM, back up to the beginning again + if (c != '\uFEFF') { + reader.reset(); + } + } catch (IOException e) { + e.printStackTrace(); + } + return reader; + } + + + /** + * ( begin auto-generated from createWriter.xml ) + * + * Creates a new file in the sketch folder, and a PrintWriter object + * to write to it. For the file to be made correctly, it should be flushed + * and must be closed with its flush() and close() methods + * (see above example). + *

      + * Starting with Processing release 0134, all files loaded and saved by the + * Processing API use UTF-8 encoding. In previous releases, the default + * encoding for your platform was used, which causes problems when files + * are moved to other platforms. + * + * ( end auto-generated ) + * + * @webref output:files + * @param filename name of the file to be created + * @see PrintWriter + * @see PApplet#createReader + * @see BufferedReader + */ + public PrintWriter createWriter(String filename) { + return createWriter(saveFile(filename)); + } + + + /** + * @nowebref + * I want to print lines to a file. I have RSI from typing these + * eight lines of code so many times. + */ + static public PrintWriter createWriter(File file) { + if (file == null) { + throw new RuntimeException("File passed to createWriter() was null"); + } + try { + createPath(file); // make sure in-between folders exist + OutputStream output = new FileOutputStream(file); + if (file.getName().toLowerCase().endsWith(".gz")) { + output = new GZIPOutputStream(output); + } + return createWriter(output); + + } catch (Exception e) { + throw new RuntimeException("Couldn't create a writer for " + + file.getAbsolutePath(), e); + } + } + + /** + * @nowebref + * I want to print lines to a file. Why am I always explaining myself? + * It's the JavaSoft API engineers who need to explain themselves. + */ + static public PrintWriter createWriter(OutputStream output) { + BufferedOutputStream bos = new BufferedOutputStream(output, 8192); + OutputStreamWriter osw = + new OutputStreamWriter(bos, StandardCharsets.UTF_8); + return new PrintWriter(osw); + } + + + + ////////////////////////////////////////////////////////////// + + // FILE INPUT + + + /** + * ( begin auto-generated from createInput.xml ) + * + * This is a function for advanced programmers to open a Java InputStream. + * It's useful if you want to use the facilities provided by PApplet to + * easily open files from the data folder or from a URL, but want an + * InputStream object so that you can use other parts of Java to take more + * control of how the stream is read.
      + *
      + * The filename passed in can be:
      + * - A URL, for instance openStream("http://processing.org/")
      + * - A file in the sketch's data folder
      + * - The full path to a file to be opened locally (when running as an + * application)
      + *
      + * If the requested item doesn't exist, null is returned. If not online, + * this will also check to see if the user is asking for a file whose name + * isn't properly capitalized. If capitalization is different, an error + * will be printed to the console. This helps prevent issues that appear + * when a sketch is exported to the web, where case sensitivity matters, as + * opposed to running from inside the Processing Development Environment on + * Windows or Mac OS, where case sensitivity is preserved but ignored.
      + *
      + * If the file ends with .gz, the stream will automatically be gzip + * decompressed. If you don't want the automatic decompression, use the + * related function createInputRaw(). + *
      + * In earlier releases, this function was called openStream().
      + *
      + * + * ( end auto-generated ) + * + *

      Advanced

      + * Simplified method to open a Java InputStream. + *

      + * This method is useful if you want to use the facilities provided + * by PApplet to easily open things from the data folder or from a URL, + * but want an InputStream object so that you can use other Java + * methods to take more control of how the stream is read. + *

      + * If the requested item doesn't exist, null is returned. + * (Prior to 0096, die() would be called, killing the applet) + *

      + * For 0096+, the "data" folder is exported intact with subfolders, + * and openStream() properly handles subdirectories from the data folder + *

      + * If not online, this will also check to see if the user is asking + * for a file whose name isn't properly capitalized. This helps prevent + * issues when a sketch is exported to the web, where case sensitivity + * matters, as opposed to Windows and the Mac OS default where + * case sensitivity is preserved but ignored. + *

      + * It is strongly recommended that libraries use this method to open + * data files, so that the loading sequence is handled in the same way + * as functions like loadBytes(), loadImage(), etc. + *

      + * The filename passed in can be: + *

        + *
      • A URL, for instance openStream("http://processing.org/"); + *
      • A file in the sketch's data folder + *
      • Another file to be opened locally (when running as an application) + *
      + * + * @webref input:files + * @param filename the name of the file to use as input + * @see PApplet#createOutput(String) + * @see PApplet#selectOutput(String,String) + * @see PApplet#selectInput(String,String) + * + */ + public InputStream createInput(String filename) { + InputStream input = createInputRaw(filename); + if (input != null) { + // if it's gzip-encoded, automatically decode + final String lower = filename.toLowerCase(); + if (lower.endsWith(".gz") || lower.endsWith(".svgz")) { + try { + // buffered has to go *around* the GZ, otherwise 25x slower + return new BufferedInputStream(new GZIPInputStream(input)); + + } catch (IOException e) { + printStackTrace(e); + } + } else { + return new BufferedInputStream(input); + } + } + return null; + } + + + /** + * Call openStream() without automatic gzip decompression. + */ + public InputStream createInputRaw(String filename) { + if (filename == null) return null; + + if (sketchPath == null) { + System.err.println("The sketch path is not set."); + throw new RuntimeException("Files must be loaded inside setup() or after it has been called."); + } + + if (filename.length() == 0) { + // an error will be called by the parent function + //System.err.println("The filename passed to openStream() was empty."); + return null; + } + + // First check whether this looks like a URL + if (filename.contains(":")) { // at least smells like URL + try { + URL url = new URL(filename); + URLConnection conn = url.openConnection(); + + if (conn instanceof HttpURLConnection) { + HttpURLConnection httpConn = (HttpURLConnection) conn; + // Will not handle a protocol change (see below) + httpConn.setInstanceFollowRedirects(true); + int response = httpConn.getResponseCode(); + // Default won't follow HTTP -> HTTPS redirects for security reasons + // http://stackoverflow.com/a/1884427 + if (response >= 300 && response < 400) { + String newLocation = httpConn.getHeaderField("Location"); + return createInputRaw(newLocation); + } + return conn.getInputStream(); + } else if (conn instanceof JarURLConnection) { + return url.openStream(); + } + } catch (MalformedURLException mfue) { + // not a url, that's fine + + } catch (FileNotFoundException fnfe) { + // Added in 0119 b/c Java 1.5 throws FNFE when URL not available. + // http://dev.processing.org/bugs/show_bug.cgi?id=403 + + } catch (IOException e) { + // changed for 0117, shouldn't be throwing exception + printStackTrace(e); + //System.err.println("Error downloading from URL " + filename); + return null; + //throw new RuntimeException("Error downloading from URL " + filename); + } + } + + InputStream stream = null; + + // Moved this earlier than the getResourceAsStream() checks, because + // calling getResourceAsStream() on a directory lists its contents. + // http://dev.processing.org/bugs/show_bug.cgi?id=716 + try { + // First see if it's in a data folder. This may fail by throwing + // a SecurityException. If so, this whole block will be skipped. + File file = new File(dataPath(filename)); + if (!file.exists()) { + // next see if it's just in the sketch folder + file = sketchFile(filename); + } + + if (file.isDirectory()) { + return null; + } + if (file.exists()) { + try { + // handle case sensitivity check + String filePath = file.getCanonicalPath(); + String filenameActual = new File(filePath).getName(); + // make sure there isn't a subfolder prepended to the name + String filenameShort = new File(filename).getName(); + // if the actual filename is the same, but capitalized + // differently, warn the user. + //if (filenameActual.equalsIgnoreCase(filenameShort) && + //!filenameActual.equals(filenameShort)) { + if (!filenameActual.equals(filenameShort)) { + throw new RuntimeException("This file is named " + + filenameActual + " not " + + filename + ". Rename the file " + + "or change your code."); + } + } catch (IOException e) { } + } + + // if this file is ok, may as well just load it + stream = new FileInputStream(file); + if (stream != null) return stream; + + // have to break these out because a general Exception might + // catch the RuntimeException being thrown above + } catch (IOException ioe) { + } catch (SecurityException se) { } + + // Using getClassLoader() prevents java from converting dots + // to slashes or requiring a slash at the beginning. + // (a slash as a prefix means that it'll load from the root of + // the jar, rather than trying to dig into the package location) + ClassLoader cl = getClass().getClassLoader(); + + // by default, data files are exported to the root path of the jar. + // (not the data folder) so check there first. + stream = cl.getResourceAsStream("data/" + filename); + if (stream != null) { + String cn = stream.getClass().getName(); + // this is an irritation of sun's java plug-in, which will return + // a non-null stream for an object that doesn't exist. like all good + // things, this is probably introduced in java 1.5. awesome! + // http://dev.processing.org/bugs/show_bug.cgi?id=359 + if (!cn.equals("sun.plugin.cache.EmptyInputStream")) { + return stream; + } + } + + // When used with an online script, also need to check without the + // data folder, in case it's not in a subfolder called 'data'. + // http://dev.processing.org/bugs/show_bug.cgi?id=389 + stream = cl.getResourceAsStream(filename); + if (stream != null) { + String cn = stream.getClass().getName(); + if (!cn.equals("sun.plugin.cache.EmptyInputStream")) { + return stream; + } + } + + try { + // attempt to load from a local file, used when running as + // an application, or as a signed applet + try { // first try to catch any security exceptions + try { + stream = new FileInputStream(dataPath(filename)); + if (stream != null) return stream; + } catch (IOException e2) { } + + try { + stream = new FileInputStream(sketchPath(filename)); + if (stream != null) return stream; + } catch (Exception e) { } // ignored + + try { + stream = new FileInputStream(filename); + if (stream != null) return stream; + } catch (IOException e1) { } + + } catch (SecurityException se) { } // online, whups + + } catch (Exception e) { + printStackTrace(e); + } + + return null; + } + + + /** + * @nowebref + */ + static public InputStream createInput(File file) { + if (file == null) { + throw new IllegalArgumentException("File passed to createInput() was null"); + } + if (!file.exists()) { + System.err.println(file + " does not exist, createInput() will return null"); + return null; + } + try { + InputStream input = new FileInputStream(file); + final String lower = file.getName().toLowerCase(); + if (lower.endsWith(".gz") || lower.endsWith(".svgz")) { + return new BufferedInputStream(new GZIPInputStream(input)); + } + return new BufferedInputStream(input); + + } catch (IOException e) { + System.err.println("Could not createInput() for " + file); + e.printStackTrace(); + return null; + } + } + + + /** + * ( begin auto-generated from loadBytes.xml ) + * + * Reads the contents of a file or url and places it in a byte array. If a + * file is specified, it must be located in the sketch's "data" + * directory/folder.
      + *
      + * The filename parameter can also be a URL to a file found online. For + * security reasons, a Processing sketch found online can only download + * files from the same server from which it came. Getting around this + * restriction requires a signed applet. + * + * ( end auto-generated ) + * @webref input:files + * @param filename name of a file in the data folder or a URL. + * @see PApplet#loadStrings(String) + * @see PApplet#saveStrings(String, String[]) + * @see PApplet#saveBytes(String, byte[]) + * + */ + public byte[] loadBytes(String filename) { + String lower = filename.toLowerCase(); + // If it's not a .gz file, then we might be able to uncompress it into + // a fixed-size buffer, which should help speed because we won't have to + // reallocate and resize the target array each time it gets full. + if (!lower.endsWith(".gz")) { + // If this looks like a URL, try to load it that way. Use the fact that + // URL connections may have a content length header to size the array. + if (filename.contains(":")) { // at least smells like URL + try { + URL url = new URL(filename); + URLConnection conn = url.openConnection(); + InputStream input = null; + int length = -1; + + if (conn instanceof HttpURLConnection) { + HttpURLConnection httpConn = (HttpURLConnection) conn; + // Will not handle a protocol change (see below) + httpConn.setInstanceFollowRedirects(true); + int response = httpConn.getResponseCode(); + // Default won't follow HTTP -> HTTPS redirects for security reasons + // http://stackoverflow.com/a/1884427 + if (response >= 300 && response < 400) { + String newLocation = httpConn.getHeaderField("Location"); + return loadBytes(newLocation); + } + length = conn.getContentLength(); + input = conn.getInputStream(); + } else if (conn instanceof JarURLConnection) { + length = conn.getContentLength(); + input = url.openStream(); + } + + if (input != null) { + byte[] buffer = null; + if (length != -1) { + buffer = new byte[length]; + int count; + int offset = 0; + while ((count = input.read(buffer, offset, length - offset)) > 0) { + offset += count; + } + } else { + buffer = loadBytes(input); + } + input.close(); + return buffer; + } + } catch (MalformedURLException mfue) { + // not a url, that's fine + + } catch (FileNotFoundException fnfe) { + // Java 1.5+ throws FNFE when URL not available + // http://dev.processing.org/bugs/show_bug.cgi?id=403 + + } catch (IOException e) { + printStackTrace(e); + return null; + } + } + } + + InputStream is = createInput(filename); + if (is != null) { + byte[] outgoing = loadBytes(is); + try { + is.close(); + } catch (IOException e) { + printStackTrace(e); // shouldn't happen + } + return outgoing; + } + + System.err.println("The file \"" + filename + "\" " + + "is missing or inaccessible, make sure " + + "the URL is valid or that the file has been " + + "added to your sketch and is readable."); + return null; + } + + + /** + * @nowebref + */ + static public byte[] loadBytes(InputStream input) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + + int bytesRead = input.read(buffer); + while (bytesRead != -1) { + out.write(buffer, 0, bytesRead); + bytesRead = input.read(buffer); + } + out.flush(); + return out.toByteArray(); + + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + + /** + * @nowebref + */ + static public byte[] loadBytes(File file) { + if (!file.exists()) { + System.err.println(file + " does not exist, loadBytes() will return null"); + return null; + } + + try { + InputStream input; + int length; + + if (file.getName().toLowerCase().endsWith(".gz")) { + RandomAccessFile raf = new RandomAccessFile(file, "r"); + raf.seek(raf.length() - 4); + int b4 = raf.read(); + int b3 = raf.read(); + int b2 = raf.read(); + int b1 = raf.read(); + length = (b1 << 24) | (b2 << 16) + (b3 << 8) + b4; + raf.close(); + + // buffered has to go *around* the GZ, otherwise 25x slower + input = new BufferedInputStream(new GZIPInputStream(new FileInputStream(file))); + + } else { + long len = file.length(); + // http://stackoverflow.com/a/3039805 + int maxArraySize = Integer.MAX_VALUE - 5; + if (len > maxArraySize) { + System.err.println("Cannot use loadBytes() on a file larger than " + maxArraySize); + return null; + } + length = (int) len; + input = new BufferedInputStream(new FileInputStream(file)); + } + byte[] buffer = new byte[length]; + int count; + int offset = 0; + // count will come back 0 when complete (or -1 if somehow going long?) + while ((count = input.read(buffer, offset, length - offset)) > 0) { + offset += count; + } + input.close(); + return buffer; + + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + + /** + * @nowebref + */ + static public String[] loadStrings(File file) { + if (!file.exists()) { + System.err.println(file + " does not exist, loadStrings() will return null"); + return null; + } + + InputStream is = createInput(file); + if (is != null) { + String[] outgoing = loadStrings(is); + try { + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return outgoing; + } + return null; + } + + + /** + * ( begin auto-generated from loadStrings.xml ) + * + * Reads the contents of a file or url and creates a String array of its + * individual lines. If a file is specified, it must be located in the + * sketch's "data" directory/folder.
      + *
      + * The filename parameter can also be a URL to a file found online. For + * security reasons, a Processing sketch found online can only download + * files from the same server from which it came. Getting around this + * restriction requires a signed applet. + *
      + * If the file is not available or an error occurs, null will be + * returned and an error message will be printed to the console. The error + * message does not halt the program, however the null value may cause a + * NullPointerException if your code does not check whether the value + * returned is null. + *

      + * Starting with Processing release 0134, all files loaded and saved by the + * Processing API use UTF-8 encoding. In previous releases, the default + * encoding for your platform was used, which causes problems when files + * are moved to other platforms. + * + * ( end auto-generated ) + * + *

      Advanced

      + * Load data from a file and shove it into a String array. + *

      + * Exceptions are handled internally, when an error, occurs, an + * exception is printed to the console and 'null' is returned, + * but the program continues running. This is a tradeoff between + * 1) showing the user that there was a problem but 2) not requiring + * that all i/o code is contained in try/catch blocks, for the sake + * of new users (or people who are just trying to get things done + * in a "scripting" fashion. If you want to handle exceptions, + * use Java methods for I/O. + * + * @webref input:files + * @param filename name of the file or url to load + * @see PApplet#loadBytes(String) + * @see PApplet#saveStrings(String, String[]) + * @see PApplet#saveBytes(String, byte[]) + */ + public String[] loadStrings(String filename) { + InputStream is = createInput(filename); + if (is != null) { + String[] strArr = loadStrings(is); + try { + is.close(); + } catch (IOException e) { + printStackTrace(e); + } + return strArr; + } + + System.err.println("The file \"" + filename + "\" " + + "is missing or inaccessible, make sure " + + "the URL is valid or that the file has been " + + "added to your sketch and is readable."); + return null; + } + + /** + * @nowebref + */ + static public String[] loadStrings(InputStream input) { + try { + BufferedReader reader = + new BufferedReader(new InputStreamReader(input, "UTF-8")); + return loadStrings(reader); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + + static public String[] loadStrings(BufferedReader reader) { + try { + String lines[] = new String[100]; + int lineCount = 0; + String line = null; + while ((line = reader.readLine()) != null) { + if (lineCount == lines.length) { + String temp[] = new String[lineCount << 1]; + System.arraycopy(lines, 0, temp, 0, lineCount); + lines = temp; + } + lines[lineCount++] = line; + } + reader.close(); + + if (lineCount == lines.length) { + return lines; + } + + // resize array to appropriate amount for these lines + String output[] = new String[lineCount]; + System.arraycopy(lines, 0, output, 0, lineCount); + return output; + + } catch (IOException e) { + e.printStackTrace(); + //throw new RuntimeException("Error inside loadStrings()"); + } + return null; + } + + + + ////////////////////////////////////////////////////////////// + + // FILE OUTPUT + + + /** + * ( begin auto-generated from createOutput.xml ) + * + * Similar to createInput(), this creates a Java OutputStream + * for a given filename or path. The file will be created in the sketch + * folder, or in the same folder as an exported application. + *

      + * If the path does not exist, intermediate folders will be created. If an + * exception occurs, it will be printed to the console, and null + * will be returned. + *

      + * This function is a convenience over the Java approach that requires you + * to 1) create a FileOutputStream object, 2) determine the exact file + * location, and 3) handle exceptions. Exceptions are handled internally by + * the function, which is more appropriate for "sketch" projects. + *

      + * If the output filename ends with .gz, the output will be + * automatically GZIP compressed as it is written. + * + * ( end auto-generated ) + * @webref output:files + * @param filename name of the file to open + * @see PApplet#createInput(String) + * @see PApplet#selectOutput(String,String) + */ + public OutputStream createOutput(String filename) { + return createOutput(saveFile(filename)); + } + + /** + * @nowebref + */ + static public OutputStream createOutput(File file) { + try { + createPath(file); // make sure the path exists + OutputStream output = new FileOutputStream(file); + if (file.getName().toLowerCase().endsWith(".gz")) { + return new BufferedOutputStream(new GZIPOutputStream(output)); + } + return new BufferedOutputStream(output); + + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + + /** + * ( begin auto-generated from saveStream.xml ) + * + * Save the contents of a stream to a file in the sketch folder. This is + * basically saveBytes(blah, loadBytes()), but done more efficiently + * (and with less confusing syntax).
      + *
      + * When using the targetFile parameter, it writes to a File + * object for greater control over the file location. (Note that unlike + * some other functions, this will not automatically compress or uncompress + * gzip files.) + * + * ( end auto-generated ) + * + * @webref output:files + * @param target name of the file to write to + * @param source location to read from (a filename, path, or URL) + * @see PApplet#createOutput(String) + */ + public boolean saveStream(String target, String source) { + return saveStream(saveFile(target), source); + } + + /** + * Identical to the other saveStream(), but writes to a File + * object, for greater control over the file location. + *

      + * Note that unlike other api methods, this will not automatically + * compress or uncompress gzip files. + */ + public boolean saveStream(File target, String source) { + return saveStream(target, createInputRaw(source)); + } + + /** + * @nowebref + */ + public boolean saveStream(String target, InputStream source) { + return saveStream(saveFile(target), source); + } + + /** + * @nowebref + */ + static public boolean saveStream(File target, InputStream source) { + File tempFile = null; + try { + // make sure that this path actually exists before writing + createPath(target); + tempFile = createTempFile(target); + FileOutputStream targetStream = new FileOutputStream(tempFile); + + saveStream(targetStream, source); + targetStream.close(); + targetStream = null; + + if (target.exists()) { + if (!target.delete()) { + System.err.println("Could not replace " + + target.getAbsolutePath() + "."); + } + } + if (!tempFile.renameTo(target)) { + System.err.println("Could not rename temporary file " + + tempFile.getAbsolutePath()); + return false; + } + return true; + + } catch (IOException e) { + if (tempFile != null) { + tempFile.delete(); + } + e.printStackTrace(); + return false; + } + } + + /** + * @nowebref + */ + static public void saveStream(OutputStream target, + InputStream source) throws IOException { + BufferedInputStream bis = new BufferedInputStream(source, 16384); + BufferedOutputStream bos = new BufferedOutputStream(target); + + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = bis.read(buffer)) != -1) { + bos.write(buffer, 0, bytesRead); + } + + bos.flush(); + } + + + /** + * ( begin auto-generated from saveBytes.xml ) + * + * Opposite of loadBytes(), will write an entire array of bytes to a + * file. The data is saved in binary format. This file is saved to the + * sketch's folder, which is opened by selecting "Show sketch folder" from + * the "Sketch" menu.
      + *
      + * It is not possible to use saveXxxxx() functions inside a web browser + * unless the sketch is signed applet. To + * save a file back to a server, see the save to + * web code snippet on the Processing Wiki. + * + * ( end auto-generated ) + * + * @webref output:files + * @param filename name of the file to write to + * @param data array of bytes to be written + * @see PApplet#loadStrings(String) + * @see PApplet#loadBytes(String) + * @see PApplet#saveStrings(String, String[]) + */ + public void saveBytes(String filename, byte[] data) { + saveBytes(saveFile(filename), data); + } + + + /** + * Creates a temporary file based on the name/extension of another file + * and in the same parent directory. Ensures that the same extension is used + * (i.e. so that .gz files are gzip compressed on output) and that it's done + * from the same directory so that renaming the file later won't cross file + * system boundaries. + */ + static private File createTempFile(File file) throws IOException { + File parentDir = file.getParentFile(); + String name = file.getName(); + String prefix; + String suffix = null; + int dot = name.lastIndexOf('.'); + if (dot == -1) { + prefix = name; + } else { + // preserve the extension so that .gz works properly + prefix = name.substring(0, dot); + suffix = name.substring(dot); + } + // Prefix must be three characters + if (prefix.length() < 3) { + prefix += "processing"; + } + return File.createTempFile(prefix, suffix, parentDir); + } + + + /** + * @nowebref + * Saves bytes to a specific File location specified by the user. + */ + static public void saveBytes(File file, byte[] data) { + File tempFile = null; + try { + tempFile = createTempFile(file); + + OutputStream output = createOutput(tempFile); + saveBytes(output, data); + output.close(); + output = null; + + if (file.exists()) { + if (!file.delete()) { + System.err.println("Could not replace " + file.getAbsolutePath()); + } + } + + if (!tempFile.renameTo(file)) { + System.err.println("Could not rename temporary file " + + tempFile.getAbsolutePath()); + } + + } catch (IOException e) { + System.err.println("error saving bytes to " + file); + if (tempFile != null) { + tempFile.delete(); + } + e.printStackTrace(); + } + } + + + /** + * @nowebref + * Spews a buffer of bytes to an OutputStream. + */ + static public void saveBytes(OutputStream output, byte[] data) { + try { + output.write(data); + output.flush(); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + + // + + /** + * ( begin auto-generated from saveStrings.xml ) + * + * Writes an array of strings to a file, one line per string. This file is + * saved to the sketch's folder, which is opened by selecting "Show sketch + * folder" from the "Sketch" menu.
      + *
      + * It is not possible to use saveXxxxx() functions inside a web browser + * unless the sketch is signed applet. To + * save a file back to a server, see the save to + * web code snippet on the Processing Wiki.
      + *
      + * Starting with Processing 1.0, all files loaded and saved by the + * Processing API use UTF-8 encoding. In previous releases, the default + * encoding for your platform was used, which causes problems when files + * are moved to other platforms. + * + * ( end auto-generated ) + * @webref output:files + * @param filename filename for output + * @param data string array to be written + * @see PApplet#loadStrings(String) + * @see PApplet#loadBytes(String) + * @see PApplet#saveBytes(String, byte[]) + */ + public void saveStrings(String filename, String data[]) { + saveStrings(saveFile(filename), data); + } + + + /** + * @nowebref + */ + static public void saveStrings(File file, String data[]) { + saveStrings(createOutput(file), data); + } + + + /** + * @nowebref + */ + static public void saveStrings(OutputStream output, String[] data) { + PrintWriter writer = createWriter(output); + for (int i = 0; i < data.length; i++) { + writer.println(data[i]); + } + writer.flush(); + writer.close(); + } + + + ////////////////////////////////////////////////////////////// + + + static protected String calcSketchPath() { + // try to get the user folder. if running under java web start, + // this may cause a security exception if the code is not signed. + // http://processing.org/discourse/yabb_beta/YaBB.cgi?board=Integrate;action=display;num=1159386274 + String folder = null; + try { + folder = System.getProperty("user.dir"); + + URL jarURL = + PApplet.class.getProtectionDomain().getCodeSource().getLocation(); + // Decode URL + String jarPath = jarURL.toURI().getSchemeSpecificPart(); + + // Workaround for bug in Java for OS X from Oracle (7u51) + // https://github.com/processing/processing/issues/2181 + if (platform == MACOSX) { + if (jarPath.contains("Contents/Java/")) { + String appPath = jarPath.substring(0, jarPath.indexOf(".app") + 4); + File containingFolder = new File(appPath).getParentFile(); + folder = containingFolder.getAbsolutePath(); + } + } else { + // Working directory may not be set properly, try some options + // https://github.com/processing/processing/issues/2195 + if (jarPath.contains("/lib/")) { + // Windows or Linux, back up a directory to get the executable + folder = new File(jarPath, "../..").getCanonicalPath(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return folder; + } + + + public String sketchPath() { + if (sketchPath == null) { + sketchPath = calcSketchPath(); + } + return sketchPath; + } + + + /** + * Prepend the sketch folder path to the filename (or path) that is + * passed in. External libraries should use this function to save to + * the sketch folder. + *

      + * Note that when running as an applet inside a web browser, + * the sketchPath will be set to null, because security restrictions + * prevent applets from accessing that information. + *

      + * This will also cause an error if the sketch is not inited properly, + * meaning that init() was never called on the PApplet when hosted + * my some other main() or by other code. For proper use of init(), + * see the examples in the main description text for PApplet. + */ + public String sketchPath(String where) { + if (sketchPath() == null) { + return where; + } + // isAbsolute() could throw an access exception, but so will writing + // to the local disk using the sketch path, so this is safe here. + // for 0120, added a try/catch anyways. + try { + if (new File(where).isAbsolute()) return where; + } catch (Exception e) { } + + return sketchPath() + File.separator + where; + } + + + public File sketchFile(String where) { + return new File(sketchPath(where)); + } + + + /** + * Returns a path inside the applet folder to save to. Like sketchPath(), + * but creates any in-between folders so that things save properly. + *

      + * All saveXxxx() functions use the path to the sketch folder, rather than + * its data folder. Once exported, the data folder will be found inside the + * jar file of the exported application or applet. In this case, it's not + * possible to save data into the jar file, because it will often be running + * from a server, or marked in-use if running from a local file system. + * With this in mind, saving to the data path doesn't make sense anyway. + * If you know you're running locally, and want to save to the data folder, + * use saveXxxx("data/blah.dat"). + */ + public String savePath(String where) { + if (where == null) return null; + String filename = sketchPath(where); + createPath(filename); + return filename; + } + + + /** + * Identical to savePath(), but returns a File object. + */ + public File saveFile(String where) { + return new File(savePath(where)); + } + + + static File desktopFolder; + + /** Not a supported function. For testing use only. */ + static public File desktopFile(String what) { + if (desktopFolder == null) { + // Should work on Linux and OS X (on OS X, even with the localized version). + desktopFolder = new File(System.getProperty("user.home"), "Desktop"); + if (!desktopFolder.exists()) { + if (platform == WINDOWS) { + FileSystemView filesys = FileSystemView.getFileSystemView(); + desktopFolder = filesys.getHomeDirectory(); + } else { + throw new UnsupportedOperationException("Could not find a suitable desktop foldder"); + } + } + } + return new File(desktopFolder, what); + } + + + /** Not a supported function. For testing use only. */ + static public String desktopPath(String what) { + return desktopFile(what).getAbsolutePath(); + } + + + /** + * This function almost certainly does not do the thing you want it to. + * The data path is handled differently on each platform, and should not be + * considered a location to write files. It should also not be assumed that + * this location can be read from or listed. This function is used internally + * as a possible location for reading files. It's still "public" as a + * holdover from earlier code. + *

      + * Libraries should use createInput() to get an InputStream or createOutput() + * to get an OutputStream. sketchPath() can be used to get a location + * relative to the sketch. Again, do not use this to get relative + * locations of files. You'll be disappointed when your app runs on different + * platforms. + */ + public String dataPath(String where) { + return dataFile(where).getAbsolutePath(); + } + + + /** + * Return a full path to an item in the data folder as a File object. + * See the dataPath() method for more information. + */ + public File dataFile(String where) { + // isAbsolute() could throw an access exception, but so will writing + // to the local disk using the sketch path, so this is safe here. + File why = new File(where); + if (why.isAbsolute()) return why; + + URL jarURL = getClass().getProtectionDomain().getCodeSource().getLocation(); + // Decode URL + String jarPath; + try { + jarPath = jarURL.toURI().getPath(); + } catch (URISyntaxException e) { + e.printStackTrace(); + return null; + } + if (jarPath.contains("Contents/Java/")) { + File containingFolder = new File(jarPath).getParentFile(); + File dataFolder = new File(containingFolder, "data"); + return new File(dataFolder, where); + } + // Windows, Linux, or when not using a Mac OS X .app file + File workingDirItem = + new File(sketchPath + File.separator + "data" + File.separator + where); +// if (workingDirItem.exists()) { + return workingDirItem; +// } +// // In some cases, the current working directory won't be set properly. + } + + + /** + * On Windows and Linux, this is simply the data folder. On Mac OS X, this is + * the path to the data folder buried inside Contents/Java + */ +// public File inputFile(String where) { +// } + + +// public String inputPath(String where) { +// } + + + /** + * Takes a path and creates any in-between folders if they don't + * already exist. Useful when trying to save to a subfolder that + * may not actually exist. + */ + static public void createPath(String path) { + createPath(new File(path)); + } + + + static public void createPath(File file) { + try { + String parent = file.getParent(); + if (parent != null) { + File unit = new File(parent); + if (!unit.exists()) unit.mkdirs(); + } + } catch (SecurityException se) { + System.err.println("You don't have permissions to create " + + file.getAbsolutePath()); + } + } + + + static public String getExtension(String filename) { + String extension; + + String lower = filename.toLowerCase(); + int dot = filename.lastIndexOf('.'); + if (dot == -1) { + return ""; // no extension found + } + extension = lower.substring(dot + 1); + + // check for, and strip any parameters on the url, i.e. + // filename.jpg?blah=blah&something=that + int question = extension.indexOf('?'); + if (question != -1) { + extension = extension.substring(0, question); + } + + return extension; + } + + + ////////////////////////////////////////////////////////////// + + // URL ENCODING + + + static public String urlEncode(String str) { + try { + return URLEncoder.encode(str, "UTF-8"); + } catch (UnsupportedEncodingException e) { // oh c'mon + return null; + } + } + + + // DO NOT use for file paths, URLDecoder can't handle RFC2396 + // "The recommended way to manage the encoding and decoding of + // URLs is to use URI, and to convert between these two classes + // using toURI() and URI.toURL()." + // https://docs.oracle.com/javase/8/docs/api/java/net/URL.html + static public String urlDecode(String str) { + try { + return URLDecoder.decode(str, "UTF-8"); + } catch (UnsupportedEncodingException e) { // safe per the JDK source + return null; + } + } + + + + ////////////////////////////////////////////////////////////// + + // SORT + + + /** + * ( begin auto-generated from sort.xml ) + * + * Sorts an array of numbers from smallest to largest and puts an array of + * words in alphabetical order. The original array is not modified, a + * re-ordered array is returned. The count parameter states the + * number of elements to sort. For example if there are 12 elements in an + * array and if count is the value 5, only the first five elements on the + * array will be sorted. + * + * ( end auto-generated ) + * @webref data:array_functions + * @param list array to sort + * @see PApplet#reverse(boolean[]) + */ + static public byte[] sort(byte list[]) { + return sort(list, list.length); + } + + /** + * @param count number of elements to sort, starting from 0 + */ + static public byte[] sort(byte[] list, int count) { + byte[] outgoing = new byte[list.length]; + System.arraycopy(list, 0, outgoing, 0, list.length); + Arrays.sort(outgoing, 0, count); + return outgoing; + } + + static public char[] sort(char list[]) { + return sort(list, list.length); + } + + static public char[] sort(char[] list, int count) { + char[] outgoing = new char[list.length]; + System.arraycopy(list, 0, outgoing, 0, list.length); + Arrays.sort(outgoing, 0, count); + return outgoing; + } + + static public int[] sort(int list[]) { + return sort(list, list.length); + } + + static public int[] sort(int[] list, int count) { + int[] outgoing = new int[list.length]; + System.arraycopy(list, 0, outgoing, 0, list.length); + Arrays.sort(outgoing, 0, count); + return outgoing; + } + + static public float[] sort(float list[]) { + return sort(list, list.length); + } + + static public float[] sort(float[] list, int count) { + float[] outgoing = new float[list.length]; + System.arraycopy(list, 0, outgoing, 0, list.length); + Arrays.sort(outgoing, 0, count); + return outgoing; + } + + static public String[] sort(String list[]) { + return sort(list, list.length); + } + + static public String[] sort(String[] list, int count) { + String[] outgoing = new String[list.length]; + System.arraycopy(list, 0, outgoing, 0, list.length); + Arrays.sort(outgoing, 0, count); + return outgoing; + } + + + + ////////////////////////////////////////////////////////////// + + // ARRAY UTILITIES + + + /** + * ( begin auto-generated from arrayCopy.xml ) + * + * Copies an array (or part of an array) to another array. The src + * array is copied to the dst array, beginning at the position + * specified by srcPos and into the position specified by + * dstPos. The number of elements to copy is determined by + * length. The simplified version with two arguments copies an + * entire array to another of the same size. It is equivalent to + * "arrayCopy(src, 0, dst, 0, src.length)". This function is far more + * efficient for copying array data than iterating through a for and + * copying each element. + * + * ( end auto-generated ) + * @webref data:array_functions + * @param src the source array + * @param srcPosition starting position in the source array + * @param dst the destination array of the same data type as the source array + * @param dstPosition starting position in the destination array + * @param length number of array elements to be copied + * @see PApplet#concat(boolean[], boolean[]) + */ + static public void arrayCopy(Object src, int srcPosition, + Object dst, int dstPosition, + int length) { + System.arraycopy(src, srcPosition, dst, dstPosition, length); + } + + /** + * Convenience method for arraycopy(). + * Identical to arraycopy(src, 0, dst, 0, length); + */ + static public void arrayCopy(Object src, Object dst, int length) { + System.arraycopy(src, 0, dst, 0, length); + } + + /** + * Shortcut to copy the entire contents of + * the source into the destination array. + * Identical to arraycopy(src, 0, dst, 0, src.length); + */ + static public void arrayCopy(Object src, Object dst) { + System.arraycopy(src, 0, dst, 0, Array.getLength(src)); + } + + // + /** + * @deprecated Use arrayCopy() instead. + */ + static public void arraycopy(Object src, int srcPosition, + Object dst, int dstPosition, + int length) { + System.arraycopy(src, srcPosition, dst, dstPosition, length); + } + + /** + * @deprecated Use arrayCopy() instead. + */ + static public void arraycopy(Object src, Object dst, int length) { + System.arraycopy(src, 0, dst, 0, length); + } + + /** + * @deprecated Use arrayCopy() instead. + */ + static public void arraycopy(Object src, Object dst) { + System.arraycopy(src, 0, dst, 0, Array.getLength(src)); + } + + /** + * ( begin auto-generated from expand.xml ) + * + * Increases the size of an array. By default, this function doubles the + * size of the array, but the optional newSize parameter provides + * precise control over the increase in size. + *

      + * When using an array of objects, the data returned from the function must + * be cast to the object array's data type. For example: SomeClass[] + * items = (SomeClass[]) expand(originalArray). + * + * ( end auto-generated ) + * + * @webref data:array_functions + * @param list the array to expand + * @see PApplet#shorten(boolean[]) + */ + static public boolean[] expand(boolean list[]) { + return expand(list, list.length > 0 ? list.length << 1 : 1); + } + + /** + * @param newSize new size for the array + */ + static public boolean[] expand(boolean list[], int newSize) { + boolean temp[] = new boolean[newSize]; + System.arraycopy(list, 0, temp, 0, Math.min(newSize, list.length)); + return temp; + } + + static public byte[] expand(byte list[]) { + return expand(list, list.length > 0 ? list.length << 1 : 1); + } + + static public byte[] expand(byte list[], int newSize) { + byte temp[] = new byte[newSize]; + System.arraycopy(list, 0, temp, 0, Math.min(newSize, list.length)); + return temp; + } + + static public char[] expand(char list[]) { + return expand(list, list.length > 0 ? list.length << 1 : 1); + } + + static public char[] expand(char list[], int newSize) { + char temp[] = new char[newSize]; + System.arraycopy(list, 0, temp, 0, Math.min(newSize, list.length)); + return temp; + } + + static public int[] expand(int list[]) { + return expand(list, list.length > 0 ? list.length << 1 : 1); + } + + static public int[] expand(int list[], int newSize) { + int temp[] = new int[newSize]; + System.arraycopy(list, 0, temp, 0, Math.min(newSize, list.length)); + return temp; + } + + static public long[] expand(long list[]) { + return expand(list, list.length > 0 ? list.length << 1 : 1); + } + + static public long[] expand(long list[], int newSize) { + long temp[] = new long[newSize]; + System.arraycopy(list, 0, temp, 0, Math.min(newSize, list.length)); + return temp; + } + + static public float[] expand(float list[]) { + return expand(list, list.length > 0 ? list.length << 1 : 1); + } + + static public float[] expand(float list[], int newSize) { + float temp[] = new float[newSize]; + System.arraycopy(list, 0, temp, 0, Math.min(newSize, list.length)); + return temp; + } + + static public double[] expand(double list[]) { + return expand(list, list.length > 0 ? list.length << 1 : 1); + } + + static public double[] expand(double list[], int newSize) { + double temp[] = new double[newSize]; + System.arraycopy(list, 0, temp, 0, Math.min(newSize, list.length)); + return temp; + } + + static public String[] expand(String list[]) { + return expand(list, list.length > 0 ? list.length << 1 : 1); + } + + static public String[] expand(String list[], int newSize) { + String temp[] = new String[newSize]; + // in case the new size is smaller than list.length + System.arraycopy(list, 0, temp, 0, Math.min(newSize, list.length)); + return temp; + } + + /** + * @nowebref + */ + static public Object expand(Object array) { + int len = Array.getLength(array); + return expand(array, len > 0 ? len << 1 : 1); + } + + static public Object expand(Object list, int newSize) { + Class type = list.getClass().getComponentType(); + Object temp = Array.newInstance(type, newSize); + System.arraycopy(list, 0, temp, 0, + Math.min(Array.getLength(list), newSize)); + return temp; + } + + // contract() has been removed in revision 0124, use subset() instead. + // (expand() is also functionally equivalent) + + /** + * ( begin auto-generated from append.xml ) + * + * Expands an array by one element and adds data to the new position. The + * datatype of the element parameter must be the same as the + * datatype of the array. + *

      + * When using an array of objects, the data returned from the function must + * be cast to the object array's data type. For example: SomeClass[] + * items = (SomeClass[]) append(originalArray, element). + * + * ( end auto-generated ) + * + * @webref data:array_functions + * @param array array to append + * @param value new data for the array + * @see PApplet#shorten(boolean[]) + * @see PApplet#expand(boolean[]) + */ + static public byte[] append(byte array[], byte value) { + array = expand(array, array.length + 1); + array[array.length-1] = value; + return array; + } + + static public char[] append(char array[], char value) { + array = expand(array, array.length + 1); + array[array.length-1] = value; + return array; + } + + static public int[] append(int array[], int value) { + array = expand(array, array.length + 1); + array[array.length-1] = value; + return array; + } + + static public float[] append(float array[], float value) { + array = expand(array, array.length + 1); + array[array.length-1] = value; + return array; + } + + static public String[] append(String array[], String value) { + array = expand(array, array.length + 1); + array[array.length-1] = value; + return array; + } + + static public Object append(Object array, Object value) { + int length = Array.getLength(array); + array = expand(array, length + 1); + Array.set(array, length, value); + return array; + } + + + /** + * ( begin auto-generated from shorten.xml ) + * + * Decreases an array by one element and returns the shortened array. + *

      + * When using an array of objects, the data returned from the function must + * be cast to the object array's data type. For example: SomeClass[] + * items = (SomeClass[]) shorten(originalArray). + * + * ( end auto-generated ) + * + * @webref data:array_functions + * @param list array to shorten + * @see PApplet#append(byte[], byte) + * @see PApplet#expand(boolean[]) + */ + static public boolean[] shorten(boolean list[]) { + return subset(list, 0, list.length-1); + } + + static public byte[] shorten(byte list[]) { + return subset(list, 0, list.length-1); + } + + static public char[] shorten(char list[]) { + return subset(list, 0, list.length-1); + } + + static public int[] shorten(int list[]) { + return subset(list, 0, list.length-1); + } + + static public float[] shorten(float list[]) { + return subset(list, 0, list.length-1); + } + + static public String[] shorten(String list[]) { + return subset(list, 0, list.length-1); + } + + static public Object shorten(Object list) { + int length = Array.getLength(list); + return subset(list, 0, length - 1); + } + + + /** + * ( begin auto-generated from splice.xml ) + * + * Inserts a value or array of values into an existing array. The first two + * parameters must be of the same datatype. The array parameter + * defines the array which will be modified and the second parameter + * defines the data which will be inserted. + *

      + * When using an array of objects, the data returned from the function must + * be cast to the object array's data type. For example: SomeClass[] + * items = (SomeClass[]) splice(array1, array2, index). + * + * ( end auto-generated ) + * @webref data:array_functions + * @param list array to splice into + * @param value value to be spliced in + * @param index position in the array from which to insert data + * @see PApplet#concat(boolean[], boolean[]) + * @see PApplet#subset(boolean[], int, int) + */ + static final public boolean[] splice(boolean list[], + boolean value, int index) { + boolean outgoing[] = new boolean[list.length + 1]; + System.arraycopy(list, 0, outgoing, 0, index); + outgoing[index] = value; + System.arraycopy(list, index, outgoing, index + 1, + list.length - index); + return outgoing; + } + + static final public boolean[] splice(boolean list[], + boolean value[], int index) { + boolean outgoing[] = new boolean[list.length + value.length]; + System.arraycopy(list, 0, outgoing, 0, index); + System.arraycopy(value, 0, outgoing, index, value.length); + System.arraycopy(list, index, outgoing, index + value.length, + list.length - index); + return outgoing; + } + + static final public byte[] splice(byte list[], + byte value, int index) { + byte outgoing[] = new byte[list.length + 1]; + System.arraycopy(list, 0, outgoing, 0, index); + outgoing[index] = value; + System.arraycopy(list, index, outgoing, index + 1, + list.length - index); + return outgoing; + } + + static final public byte[] splice(byte list[], + byte value[], int index) { + byte outgoing[] = new byte[list.length + value.length]; + System.arraycopy(list, 0, outgoing, 0, index); + System.arraycopy(value, 0, outgoing, index, value.length); + System.arraycopy(list, index, outgoing, index + value.length, + list.length - index); + return outgoing; + } + + + static final public char[] splice(char list[], + char value, int index) { + char outgoing[] = new char[list.length + 1]; + System.arraycopy(list, 0, outgoing, 0, index); + outgoing[index] = value; + System.arraycopy(list, index, outgoing, index + 1, + list.length - index); + return outgoing; + } + + static final public char[] splice(char list[], + char value[], int index) { + char outgoing[] = new char[list.length + value.length]; + System.arraycopy(list, 0, outgoing, 0, index); + System.arraycopy(value, 0, outgoing, index, value.length); + System.arraycopy(list, index, outgoing, index + value.length, + list.length - index); + return outgoing; + } + + static final public int[] splice(int list[], + int value, int index) { + int outgoing[] = new int[list.length + 1]; + System.arraycopy(list, 0, outgoing, 0, index); + outgoing[index] = value; + System.arraycopy(list, index, outgoing, index + 1, + list.length - index); + return outgoing; + } + + static final public int[] splice(int list[], + int value[], int index) { + int outgoing[] = new int[list.length + value.length]; + System.arraycopy(list, 0, outgoing, 0, index); + System.arraycopy(value, 0, outgoing, index, value.length); + System.arraycopy(list, index, outgoing, index + value.length, + list.length - index); + return outgoing; + } + + static final public float[] splice(float list[], + float value, int index) { + float outgoing[] = new float[list.length + 1]; + System.arraycopy(list, 0, outgoing, 0, index); + outgoing[index] = value; + System.arraycopy(list, index, outgoing, index + 1, + list.length - index); + return outgoing; + } + + static final public float[] splice(float list[], + float value[], int index) { + float outgoing[] = new float[list.length + value.length]; + System.arraycopy(list, 0, outgoing, 0, index); + System.arraycopy(value, 0, outgoing, index, value.length); + System.arraycopy(list, index, outgoing, index + value.length, + list.length - index); + return outgoing; + } + + static final public String[] splice(String list[], + String value, int index) { + String outgoing[] = new String[list.length + 1]; + System.arraycopy(list, 0, outgoing, 0, index); + outgoing[index] = value; + System.arraycopy(list, index, outgoing, index + 1, + list.length - index); + return outgoing; + } + + static final public String[] splice(String list[], + String value[], int index) { + String outgoing[] = new String[list.length + value.length]; + System.arraycopy(list, 0, outgoing, 0, index); + System.arraycopy(value, 0, outgoing, index, value.length); + System.arraycopy(list, index, outgoing, index + value.length, + list.length - index); + return outgoing; + } + + static final public Object splice(Object list, Object value, int index) { + Class type = list.getClass().getComponentType(); + Object outgoing = null; + int length = Array.getLength(list); + + // check whether item being spliced in is an array + if (value.getClass().getName().charAt(0) == '[') { + int vlength = Array.getLength(value); + outgoing = Array.newInstance(type, length + vlength); + System.arraycopy(list, 0, outgoing, 0, index); + System.arraycopy(value, 0, outgoing, index, vlength); + System.arraycopy(list, index, outgoing, index + vlength, length - index); + + } else { + outgoing = Array.newInstance(type, length + 1); + System.arraycopy(list, 0, outgoing, 0, index); + Array.set(outgoing, index, value); + System.arraycopy(list, index, outgoing, index + 1, length - index); + } + return outgoing; + } + + static public boolean[] subset(boolean list[], int start) { + return subset(list, start, list.length - start); + } + + /** + * ( begin auto-generated from subset.xml ) + * + * Extracts an array of elements from an existing array. The array + * parameter defines the array from which the elements will be copied and + * the offset and length parameters determine which elements + * to extract. If no length is given, elements will be extracted + * from the offset to the end of the array. When specifying the + * offset remember the first array element is 0. This function does + * not change the source array. + *

      + * When using an array of objects, the data returned from the function must + * be cast to the object array's data type. For example: SomeClass[] + * items = (SomeClass[]) subset(originalArray, 0, 4). + * + * ( end auto-generated ) + * @webref data:array_functions + * @param list array to extract from + * @param start position to begin + * @param count number of values to extract + * @see PApplet#splice(boolean[], boolean, int) + */ + static public boolean[] subset(boolean list[], int start, int count) { + boolean output[] = new boolean[count]; + System.arraycopy(list, start, output, 0, count); + return output; + } + + static public byte[] subset(byte list[], int start) { + return subset(list, start, list.length - start); + } + + static public byte[] subset(byte list[], int start, int count) { + byte output[] = new byte[count]; + System.arraycopy(list, start, output, 0, count); + return output; + } + + + static public char[] subset(char list[], int start) { + return subset(list, start, list.length - start); + } + + static public char[] subset(char list[], int start, int count) { + char output[] = new char[count]; + System.arraycopy(list, start, output, 0, count); + return output; + } + + static public int[] subset(int list[], int start) { + return subset(list, start, list.length - start); + } + + static public int[] subset(int list[], int start, int count) { + int output[] = new int[count]; + System.arraycopy(list, start, output, 0, count); + return output; + } + + static public float[] subset(float list[], int start) { + return subset(list, start, list.length - start); + } + + static public float[] subset(float list[], int start, int count) { + float output[] = new float[count]; + System.arraycopy(list, start, output, 0, count); + return output; + } + + + static public String[] subset(String list[], int start) { + return subset(list, start, list.length - start); + } + + static public String[] subset(String list[], int start, int count) { + String output[] = new String[count]; + System.arraycopy(list, start, output, 0, count); + return output; + } + + + static public Object subset(Object list, int start) { + int length = Array.getLength(list); + return subset(list, start, length - start); + } + + static public Object subset(Object list, int start, int count) { + Class type = list.getClass().getComponentType(); + Object outgoing = Array.newInstance(type, count); + System.arraycopy(list, start, outgoing, 0, count); + return outgoing; + } + + + /** + * ( begin auto-generated from concat.xml ) + * + * Concatenates two arrays. For example, concatenating the array { 1, 2, 3 + * } and the array { 4, 5, 6 } yields { 1, 2, 3, 4, 5, 6 }. Both parameters + * must be arrays of the same datatype. + *

      + * When using an array of objects, the data returned from the function must + * be cast to the object array's data type. For example: SomeClass[] + * items = (SomeClass[]) concat(array1, array2). + * + * ( end auto-generated ) + * @webref data:array_functions + * @param a first array to concatenate + * @param b second array to concatenate + * @see PApplet#splice(boolean[], boolean, int) + * @see PApplet#arrayCopy(Object, int, Object, int, int) + */ + static public boolean[] concat(boolean a[], boolean b[]) { + boolean c[] = new boolean[a.length + b.length]; + System.arraycopy(a, 0, c, 0, a.length); + System.arraycopy(b, 0, c, a.length, b.length); + return c; + } + + static public byte[] concat(byte a[], byte b[]) { + byte c[] = new byte[a.length + b.length]; + System.arraycopy(a, 0, c, 0, a.length); + System.arraycopy(b, 0, c, a.length, b.length); + return c; + } + + static public char[] concat(char a[], char b[]) { + char c[] = new char[a.length + b.length]; + System.arraycopy(a, 0, c, 0, a.length); + System.arraycopy(b, 0, c, a.length, b.length); + return c; + } + + static public int[] concat(int a[], int b[]) { + int c[] = new int[a.length + b.length]; + System.arraycopy(a, 0, c, 0, a.length); + System.arraycopy(b, 0, c, a.length, b.length); + return c; + } + + static public float[] concat(float a[], float b[]) { + float c[] = new float[a.length + b.length]; + System.arraycopy(a, 0, c, 0, a.length); + System.arraycopy(b, 0, c, a.length, b.length); + return c; + } + + static public String[] concat(String a[], String b[]) { + String c[] = new String[a.length + b.length]; + System.arraycopy(a, 0, c, 0, a.length); + System.arraycopy(b, 0, c, a.length, b.length); + return c; + } + + static public Object concat(Object a, Object b) { + Class type = a.getClass().getComponentType(); + int alength = Array.getLength(a); + int blength = Array.getLength(b); + Object outgoing = Array.newInstance(type, alength + blength); + System.arraycopy(a, 0, outgoing, 0, alength); + System.arraycopy(b, 0, outgoing, alength, blength); + return outgoing; + } + + // + + + /** + * ( begin auto-generated from reverse.xml ) + * + * Reverses the order of an array. + * + * ( end auto-generated ) + * @webref data:array_functions + * @param list booleans[], bytes[], chars[], ints[], floats[], or Strings[] + * @see PApplet#sort(String[], int) + */ + static public boolean[] reverse(boolean list[]) { + boolean outgoing[] = new boolean[list.length]; + int length1 = list.length - 1; + for (int i = 0; i < list.length; i++) { + outgoing[i] = list[length1 - i]; + } + return outgoing; + } + + static public byte[] reverse(byte list[]) { + byte outgoing[] = new byte[list.length]; + int length1 = list.length - 1; + for (int i = 0; i < list.length; i++) { + outgoing[i] = list[length1 - i]; + } + return outgoing; + } + + static public char[] reverse(char list[]) { + char outgoing[] = new char[list.length]; + int length1 = list.length - 1; + for (int i = 0; i < list.length; i++) { + outgoing[i] = list[length1 - i]; + } + return outgoing; + } + + static public int[] reverse(int list[]) { + int outgoing[] = new int[list.length]; + int length1 = list.length - 1; + for (int i = 0; i < list.length; i++) { + outgoing[i] = list[length1 - i]; + } + return outgoing; + } + + static public float[] reverse(float list[]) { + float outgoing[] = new float[list.length]; + int length1 = list.length - 1; + for (int i = 0; i < list.length; i++) { + outgoing[i] = list[length1 - i]; + } + return outgoing; + } + + static public String[] reverse(String list[]) { + String outgoing[] = new String[list.length]; + int length1 = list.length - 1; + for (int i = 0; i < list.length; i++) { + outgoing[i] = list[length1 - i]; + } + return outgoing; + } + + static public Object reverse(Object list) { + Class type = list.getClass().getComponentType(); + int length = Array.getLength(list); + Object outgoing = Array.newInstance(type, length); + for (int i = 0; i < length; i++) { + Array.set(outgoing, i, Array.get(list, (length - 1) - i)); + } + return outgoing; + } + + + + ////////////////////////////////////////////////////////////// + + // STRINGS + + + /** + * ( begin auto-generated from trim.xml ) + * + * Removes whitespace characters from the beginning and end of a String. In + * addition to standard whitespace characters such as space, carriage + * return, and tab, this function also removes the Unicode "nbsp" character. + * + * ( end auto-generated ) + * @webref data:string_functions + * @param str any string + * @see PApplet#split(String, String) + * @see PApplet#join(String[], char) + */ + static public String trim(String str) { + if (str == null) { + return null; + } + return str.replace('\u00A0', ' ').trim(); + } + + + /** + * @param array a String array + */ + static public String[] trim(String[] array) { + if (array == null) { + return null; + } + String[] outgoing = new String[array.length]; + for (int i = 0; i < array.length; i++) { + if (array[i] != null) { + outgoing[i] = trim(array[i]); + } + } + return outgoing; + } + + + /** + * ( begin auto-generated from join.xml ) + * + * Combines an array of Strings into one String, each separated by the + * character(s) used for the separator parameter. To join arrays of + * ints or floats, it's necessary to first convert them to strings using + * nf() or nfs(). + * + * ( end auto-generated ) + * @webref data:string_functions + * @param list array of Strings + * @param separator char or String to be placed between each item + * @see PApplet#split(String, String) + * @see PApplet#trim(String) + * @see PApplet#nf(float, int, int) + * @see PApplet#nfs(float, int, int) + */ + static public String join(String[] list, char separator) { + return join(list, String.valueOf(separator)); + } + + + static public String join(String[] list, String separator) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < list.length; i++) { + if (i != 0) sb.append(separator); + sb.append(list[i]); + } + return sb.toString(); + } + + + static public String[] splitTokens(String value) { + return splitTokens(value, WHITESPACE); + } + + + /** + * ( begin auto-generated from splitTokens.xml ) + * + * The splitTokens() function splits a String at one or many character + * "tokens." The tokens parameter specifies the character or + * characters to be used as a boundary. + *

      + * If no tokens character is specified, any whitespace character is + * used to split. Whitespace characters include tab (\\t), line feed (\\n), + * carriage return (\\r), form feed (\\f), and space. To convert a String + * to an array of integers or floats, use the datatype conversion functions + * int() and float() to convert the array of Strings. + * + * ( end auto-generated ) + * @webref data:string_functions + * @param value the String to be split + * @param delim list of individual characters that will be used as separators + * @see PApplet#split(String, String) + * @see PApplet#join(String[], String) + * @see PApplet#trim(String) + */ + static public String[] splitTokens(String value, String delim) { + StringTokenizer toker = new StringTokenizer(value, delim); + String pieces[] = new String[toker.countTokens()]; + + int index = 0; + while (toker.hasMoreTokens()) { + pieces[index++] = toker.nextToken(); + } + return pieces; + } + + + /** + * ( begin auto-generated from split.xml ) + * + * The split() function breaks a string into pieces using a character or + * string as the divider. The delim parameter specifies the + * character or characters that mark the boundaries between each piece. A + * String[] array is returned that contains each of the pieces. + *

      + * If the result is a set of numbers, you can convert the String[] array to + * to a float[] or int[] array using the datatype conversion functions + * int() and float() (see example above). + *

      + * The splitTokens() function works in a similar fashion, except + * that it splits using a range of characters instead of a specific + * character or sequence. + * + * + * ( end auto-generated ) + * @webref data:string_functions + * @usage web_application + * @param value the String to be split + * @param delim the character or String used to separate the data + */ + static public String[] split(String value, char delim) { + // do this so that the exception occurs inside the user's + // program, rather than appearing to be a bug inside split() + if (value == null) return null; + //return split(what, String.valueOf(delim)); // huh + + char chars[] = value.toCharArray(); + int splitCount = 0; //1; + for (int i = 0; i < chars.length; i++) { + if (chars[i] == delim) splitCount++; + } + // make sure that there is something in the input string + //if (chars.length > 0) { + // if the last char is a delimeter, get rid of it.. + //if (chars[chars.length-1] == delim) splitCount--; + // on second thought, i don't agree with this, will disable + //} + if (splitCount == 0) { + String splits[] = new String[1]; + splits[0] = value; + return splits; + } + //int pieceCount = splitCount + 1; + String splits[] = new String[splitCount + 1]; + int splitIndex = 0; + int startIndex = 0; + for (int i = 0; i < chars.length; i++) { + if (chars[i] == delim) { + splits[splitIndex++] = + new String(chars, startIndex, i-startIndex); + startIndex = i + 1; + } + } + //if (startIndex != chars.length) { + splits[splitIndex] = + new String(chars, startIndex, chars.length-startIndex); + //} + return splits; + } + + + static public String[] split(String value, String delim) { + List items = new ArrayList<>(); + int index; + int offset = 0; + while ((index = value.indexOf(delim, offset)) != -1) { + items.add(value.substring(offset, index)); + offset = index + delim.length(); + } + items.add(value.substring(offset)); + String[] outgoing = new String[items.size()]; + items.toArray(outgoing); + return outgoing; + } + + + static protected LinkedHashMap matchPatterns; + + static Pattern matchPattern(String regexp) { + Pattern p = null; + if (matchPatterns == null) { + matchPatterns = new LinkedHashMap(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + // Limit the number of match patterns at 10 most recently used + return size() == 10; + } + }; + } else { + p = matchPatterns.get(regexp); + } + if (p == null) { + p = Pattern.compile(regexp, Pattern.MULTILINE | Pattern.DOTALL); + matchPatterns.put(regexp, p); + } + return p; + } + + + /** + * ( begin auto-generated from match.xml ) + * + * The match() function is used to apply a regular expression to a piece of + * text, and return matching groups (elements found inside parentheses) as + * a String array. No match will return null. If no groups are specified in + * the regexp, but the sequence matches, an array of length one (with the + * matched text as the first element of the array) will be returned.
      + *
      + * To use the function, first check to see if the result is null. If the + * result is null, then the sequence did not match. If the sequence did + * match, an array is returned. + * If there are groups (specified by sets of parentheses) in the regexp, + * then the contents of each will be returned in the array. + * Element [0] of a regexp match returns the entire matching string, and + * the match groups start at element [1] (the first group is [1], the + * second [2], and so on).
      + *
      + * The syntax can be found in the reference for Java's Pattern class. + * For regular expression syntax, read the Java + * Tutorial on the topic. + * + * ( end auto-generated ) + * @webref data:string_functions + * @param str the String to be searched + * @param regexp the regexp to be used for matching + * @see PApplet#matchAll(String, String) + * @see PApplet#split(String, String) + * @see PApplet#splitTokens(String, String) + * @see PApplet#join(String[], String) + * @see PApplet#trim(String) + */ + static public String[] match(String str, String regexp) { + Pattern p = matchPattern(regexp); + Matcher m = p.matcher(str); + if (m.find()) { + int count = m.groupCount() + 1; + String[] groups = new String[count]; + for (int i = 0; i < count; i++) { + groups[i] = m.group(i); + } + return groups; + } + return null; + } + + + /** + * ( begin auto-generated from matchAll.xml ) + * + * This function is used to apply a regular expression to a piece of text, + * and return a list of matching groups (elements found inside parentheses) + * as a two-dimensional String array. No matches will return null. If no + * groups are specified in the regexp, but the sequence matches, a two + * dimensional array is still returned, but the second dimension is only of + * length one.
      + *
      + * To use the function, first check to see if the result is null. If the + * result is null, then the sequence did not match at all. If the sequence + * did match, a 2D array is returned. If there are groups (specified by + * sets of parentheses) in the regexp, then the contents of each will be + * returned in the array. + * Assuming, a loop with counter variable i, element [i][0] of a regexp + * match returns the entire matching string, and the match groups start at + * element [i][1] (the first group is [i][1], the second [i][2], and so + * on).
      + *
      + * The syntax can be found in the reference for Java's Pattern class. + * For regular expression syntax, read the Java + * Tutorial on the topic. + * + * ( end auto-generated ) + * @webref data:string_functions + * @param str the String to be searched + * @param regexp the regexp to be used for matching + * @see PApplet#match(String, String) + * @see PApplet#split(String, String) + * @see PApplet#splitTokens(String, String) + * @see PApplet#join(String[], String) + * @see PApplet#trim(String) + */ + static public String[][] matchAll(String str, String regexp) { + Pattern p = matchPattern(regexp); + Matcher m = p.matcher(str); + List results = new ArrayList<>(); + int count = m.groupCount() + 1; + while (m.find()) { + String[] groups = new String[count]; + for (int i = 0; i < count; i++) { + groups[i] = m.group(i); + } + results.add(groups); + } + if (results.isEmpty()) { + return null; + } + String[][] matches = new String[results.size()][count]; + for (int i = 0; i < matches.length; i++) { + matches[i] = results.get(i); + } + return matches; + } + + + + ////////////////////////////////////////////////////////////// + + // CASTING FUNCTIONS, INSERTED BY PREPROC + + + /** + * Convert a char to a boolean. 'T', 't', and '1' will become the + * boolean value true, while 'F', 'f', or '0' will become false. + */ + /* + static final public boolean parseBoolean(char what) { + return ((what == 't') || (what == 'T') || (what == '1')); + } + */ + + /** + *

      Convert an integer to a boolean. Because of how Java handles upgrading + * numbers, this will also cover byte and char (as they will upgrade to + * an int without any sort of explicit cast).

      + *

      The preprocessor will convert boolean(what) to parseBoolean(what).

      + * @return false if 0, true if any other number + */ + static final public boolean parseBoolean(int what) { + return (what != 0); + } + + /* + // removed because this makes no useful sense + static final public boolean parseBoolean(float what) { + return (what != 0); + } + */ + + /** + * Convert the string "true" or "false" to a boolean. + * @return true if 'what' is "true" or "TRUE", false otherwise + */ + static final public boolean parseBoolean(String what) { + return Boolean.parseBoolean(what); + } + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + /* + // removed, no need to introduce strange syntax from other languages + static final public boolean[] parseBoolean(char what[]) { + boolean outgoing[] = new boolean[what.length]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = + ((what[i] == 't') || (what[i] == 'T') || (what[i] == '1')); + } + return outgoing; + } + */ + + /** + * Convert a byte array to a boolean array. Each element will be + * evaluated identical to the integer case, where a byte equal + * to zero will return false, and any other value will return true. + * @return array of boolean elements + */ + /* + static final public boolean[] parseBoolean(byte what[]) { + boolean outgoing[] = new boolean[what.length]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = (what[i] != 0); + } + return outgoing; + } + */ + + /** + * Convert an int array to a boolean array. An int equal + * to zero will return false, and any other value will return true. + * @return array of boolean elements + */ + static final public boolean[] parseBoolean(int what[]) { + boolean outgoing[] = new boolean[what.length]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = (what[i] != 0); + } + return outgoing; + } + + /* + // removed, not necessary... if necessary, convert to int array first + static final public boolean[] parseBoolean(float what[]) { + boolean outgoing[] = new boolean[what.length]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = (what[i] != 0); + } + return outgoing; + } + */ + + static final public boolean[] parseBoolean(String what[]) { + boolean outgoing[] = new boolean[what.length]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = Boolean.parseBoolean(what[i]); + } + return outgoing; + } + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + static final public byte parseByte(boolean what) { + return what ? (byte)1 : 0; + } + + static final public byte parseByte(char what) { + return (byte) what; + } + + static final public byte parseByte(int what) { + return (byte) what; + } + + static final public byte parseByte(float what) { + return (byte) what; + } + + /* + // nixed, no precedent + static final public byte[] parseByte(String what) { // note: array[] + return what.getBytes(); + } + */ + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + static final public byte[] parseByte(boolean what[]) { + byte outgoing[] = new byte[what.length]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = what[i] ? (byte)1 : 0; + } + return outgoing; + } + + static final public byte[] parseByte(char what[]) { + byte outgoing[] = new byte[what.length]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = (byte) what[i]; + } + return outgoing; + } + + static final public byte[] parseByte(int what[]) { + byte outgoing[] = new byte[what.length]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = (byte) what[i]; + } + return outgoing; + } + + static final public byte[] parseByte(float what[]) { + byte outgoing[] = new byte[what.length]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = (byte) what[i]; + } + return outgoing; + } + + /* + static final public byte[][] parseByte(String what[]) { // note: array[][] + byte outgoing[][] = new byte[what.length][]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = what[i].getBytes(); + } + return outgoing; + } + */ + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + /* + static final public char parseChar(boolean what) { // 0/1 or T/F ? + return what ? 't' : 'f'; + } + */ + + static final public char parseChar(byte what) { + return (char) (what & 0xff); + } + + static final public char parseChar(int what) { + return (char) what; + } + + /* + static final public char parseChar(float what) { // nonsensical + return (char) what; + } + + static final public char[] parseChar(String what) { // note: array[] + return what.toCharArray(); + } + */ + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + /* + static final public char[] parseChar(boolean what[]) { // 0/1 or T/F ? + char outgoing[] = new char[what.length]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = what[i] ? 't' : 'f'; + } + return outgoing; + } + */ + + static final public char[] parseChar(byte what[]) { + char outgoing[] = new char[what.length]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = (char) (what[i] & 0xff); + } + return outgoing; + } + + static final public char[] parseChar(int what[]) { + char outgoing[] = new char[what.length]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = (char) what[i]; + } + return outgoing; + } + + /* + static final public char[] parseChar(float what[]) { // nonsensical + char outgoing[] = new char[what.length]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = (char) what[i]; + } + return outgoing; + } + + static final public char[][] parseChar(String what[]) { // note: array[][] + char outgoing[][] = new char[what.length][]; + for (int i = 0; i < what.length; i++) { + outgoing[i] = what[i].toCharArray(); + } + return outgoing; + } + */ + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + static final public int parseInt(boolean what) { + return what ? 1 : 0; + } + + /** + * Note that parseInt() will un-sign a signed byte value. + */ + static final public int parseInt(byte what) { + return what & 0xff; + } + + /** + * Note that parseInt('5') is unlike String in the sense that it + * won't return 5, but the ascii value. This is because ((int) someChar) + * returns the ascii value, and parseInt() is just longhand for the cast. + */ + static final public int parseInt(char what) { + return what; + } + + /** + * Same as floor(), or an (int) cast. + */ + static final public int parseInt(float what) { + return (int) what; + } + + /** + * Parse a String into an int value. Returns 0 if the value is bad. + */ + static final public int parseInt(String what) { + return parseInt(what, 0); + } + + /** + * Parse a String to an int, and provide an alternate value that + * should be used when the number is invalid. + */ + static final public int parseInt(String what, int otherwise) { + try { + int offset = what.indexOf('.'); + if (offset == -1) { + return Integer.parseInt(what); + } else { + return Integer.parseInt(what.substring(0, offset)); + } + } catch (NumberFormatException e) { } + return otherwise; + } + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + static final public int[] parseInt(boolean what[]) { + int list[] = new int[what.length]; + for (int i = 0; i < what.length; i++) { + list[i] = what[i] ? 1 : 0; + } + return list; + } + + static final public int[] parseInt(byte what[]) { // note this unsigns + int list[] = new int[what.length]; + for (int i = 0; i < what.length; i++) { + list[i] = (what[i] & 0xff); + } + return list; + } + + static final public int[] parseInt(char what[]) { + int list[] = new int[what.length]; + for (int i = 0; i < what.length; i++) { + list[i] = what[i]; + } + return list; + } + + static public int[] parseInt(float what[]) { + int inties[] = new int[what.length]; + for (int i = 0; i < what.length; i++) { + inties[i] = (int)what[i]; + } + return inties; + } + + /** + * Make an array of int elements from an array of String objects. + * If the String can't be parsed as a number, it will be set to zero. + * + * String s[] = { "1", "300", "44" }; + * int numbers[] = parseInt(s); + * + * numbers will contain { 1, 300, 44 } + */ + static public int[] parseInt(String what[]) { + return parseInt(what, 0); + } + + /** + * Make an array of int elements from an array of String objects. + * If the String can't be parsed as a number, its entry in the + * array will be set to the value of the "missing" parameter. + * + * String s[] = { "1", "300", "apple", "44" }; + * int numbers[] = parseInt(s, 9999); + * + * numbers will contain { 1, 300, 9999, 44 } + */ + static public int[] parseInt(String what[], int missing) { + int output[] = new int[what.length]; + for (int i = 0; i < what.length; i++) { + try { + output[i] = Integer.parseInt(what[i]); + } catch (NumberFormatException e) { + output[i] = missing; + } + } + return output; + } + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + /* + static final public float parseFloat(boolean what) { + return what ? 1 : 0; + } + */ + + /** + * Convert an int to a float value. Also handles bytes because of + * Java's rules for upgrading values. + */ + static final public float parseFloat(int what) { // also handles byte + return what; + } + + static final public float parseFloat(String what) { + return parseFloat(what, Float.NaN); + } + + static final public float parseFloat(String what, float otherwise) { + try { + return Float.parseFloat(what); + } catch (NumberFormatException e) { } + + return otherwise; + } + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + /* + static final public float[] parseFloat(boolean what[]) { + float floaties[] = new float[what.length]; + for (int i = 0; i < what.length; i++) { + floaties[i] = what[i] ? 1 : 0; + } + return floaties; + } + + static final public float[] parseFloat(char what[]) { + float floaties[] = new float[what.length]; + for (int i = 0; i < what.length; i++) { + floaties[i] = (char) what[i]; + } + return floaties; + } + */ + + static final public float[] parseFloat(byte what[]) { + float floaties[] = new float[what.length]; + for (int i = 0; i < what.length; i++) { + floaties[i] = what[i]; + } + return floaties; + } + + static final public float[] parseFloat(int what[]) { + float floaties[] = new float[what.length]; + for (int i = 0; i < what.length; i++) { + floaties[i] = what[i]; + } + return floaties; + } + + static final public float[] parseFloat(String what[]) { + return parseFloat(what, Float.NaN); + } + + static final public float[] parseFloat(String what[], float missing) { + float output[] = new float[what.length]; + for (int i = 0; i < what.length; i++) { + try { + output[i] = Float.parseFloat(what[i]); + } catch (NumberFormatException e) { + output[i] = missing; + } + } + return output; + } + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + static final public String str(boolean x) { + return String.valueOf(x); + } + + static final public String str(byte x) { + return String.valueOf(x); + } + + static final public String str(char x) { + return String.valueOf(x); + } + + static final public String str(int x) { + return String.valueOf(x); + } + + static final public String str(float x) { + return String.valueOf(x); + } + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + static final public String[] str(boolean x[]) { + String s[] = new String[x.length]; + for (int i = 0; i < x.length; i++) s[i] = String.valueOf(x[i]); + return s; + } + + static final public String[] str(byte x[]) { + String s[] = new String[x.length]; + for (int i = 0; i < x.length; i++) s[i] = String.valueOf(x[i]); + return s; + } + + static final public String[] str(char x[]) { + String s[] = new String[x.length]; + for (int i = 0; i < x.length; i++) s[i] = String.valueOf(x[i]); + return s; + } + + static final public String[] str(int x[]) { + String s[] = new String[x.length]; + for (int i = 0; i < x.length; i++) s[i] = String.valueOf(x[i]); + return s; + } + + static final public String[] str(float x[]) { + String s[] = new String[x.length]; + for (int i = 0; i < x.length; i++) s[i] = String.valueOf(x[i]); + return s; + } + + + ////////////////////////////////////////////////////////////// + + // INT NUMBER FORMATTING + + static public String nf(float num) { + int inum = (int) num; + if (num == inum) { + return str(inum); + } + return str(num); + } + + static public String[] nf(float[] nums) { + String[] outgoing = new String[nums.length]; + for (int i = 0; i < nums.length; i++) { + outgoing[i] = nf(nums[i]); + } + return outgoing; + } + + /** + * Integer number formatter. + */ + + static private NumberFormat int_nf; + static private int int_nf_digits; + static private boolean int_nf_commas; + + /** + * ( begin auto-generated from nf.xml ) + * + * Utility function for formatting numbers into strings. There are two + * versions, one for formatting floats and one for formatting ints. The + * values for the digits, left, and right parameters + * should always be positive integers.

      As shown in the above + * example, nf() is used to add zeros to the left and/or right of a + * number. This is typically for aligning a list of numbers. To + * remove digits from a floating-point number, use the + * int(), ceil(), floor(), or round() + * functions. + * + * ( end auto-generated ) + * @webref data:string_functions + * @param nums the numbers to format + * @param digits number of digits to pad with zero + * @see PApplet#nfs(float, int, int) + * @see PApplet#nfp(float, int, int) + * @see PApplet#nfc(float, int) + * @see int(float) + */ + + static public String[] nf(int nums[], int digits) { + String formatted[] = new String[nums.length]; + for (int i = 0; i < formatted.length; i++) { + formatted[i] = nf(nums[i], digits); + } + return formatted; + } + + /** + * @param num the number to format + */ + static public String nf(int num, int digits) { + if ((int_nf != null) && + (int_nf_digits == digits) && + !int_nf_commas) { + return int_nf.format(num); + } + + int_nf = NumberFormat.getInstance(); + int_nf.setGroupingUsed(false); // no commas + int_nf_commas = false; + int_nf.setMinimumIntegerDigits(digits); + int_nf_digits = digits; + return int_nf.format(num); + } + + /** + * ( begin auto-generated from nfc.xml ) + * + * Utility function for formatting numbers into strings and placing + * appropriate commas to mark units of 1000. There are two versions, one + * for formatting ints and one for formatting an array of ints. The value + * for the digits parameter should always be a positive integer. + *

      + * For a non-US locale, this will insert periods instead of commas, or + * whatever is apprioriate for that region. + * + * ( end auto-generated ) + * @webref data:string_functions + * @param nums the numbers to format + * @see PApplet#nf(float, int, int) + * @see PApplet#nfp(float, int, int) + * @see PApplet#nfs(float, int, int) + */ + static public String[] nfc(int nums[]) { + String formatted[] = new String[nums.length]; + for (int i = 0; i < formatted.length; i++) { + formatted[i] = nfc(nums[i]); + } + return formatted; + } + + + /** + * @param num the number to format + */ + static public String nfc(int num) { + if ((int_nf != null) && + (int_nf_digits == 0) && + int_nf_commas) { + return int_nf.format(num); + } + + int_nf = NumberFormat.getInstance(); + int_nf.setGroupingUsed(true); + int_nf_commas = true; + int_nf.setMinimumIntegerDigits(0); + int_nf_digits = 0; + return int_nf.format(num); + } + + + /** + * number format signed (or space) + * Formats a number but leaves a blank space in the front + * when it's positive so that it can be properly aligned with + * numbers that have a negative sign in front of them. + */ + + /** + * ( begin auto-generated from nfs.xml ) + * + * Utility function for formatting numbers into strings. Similar to + * nf() but leaves a blank space in front of positive numbers so + * they align with negative numbers in spite of the minus symbol. There are + * two versions, one for formatting floats and one for formatting ints. The + * values for the digits, left, and right parameters + * should always be positive integers. + * + * ( end auto-generated ) + * @webref data:string_functions + * @param num the number to format + * @param digits number of digits to pad with zeroes + * @see PApplet#nf(float, int, int) + * @see PApplet#nfp(float, int, int) + * @see PApplet#nfc(float, int) + */ + static public String nfs(int num, int digits) { + return (num < 0) ? nf(num, digits) : (' ' + nf(num, digits)); + } + + /** + * @param nums the numbers to format + */ + static public String[] nfs(int nums[], int digits) { + String formatted[] = new String[nums.length]; + for (int i = 0; i < formatted.length; i++) { + formatted[i] = nfs(nums[i], digits); + } + return formatted; + } + + // + + /** + * number format positive (or plus) + * Formats a number, always placing a - or + sign + * in the front when it's negative or positive. + */ + /** + * ( begin auto-generated from nfp.xml ) + * + * Utility function for formatting numbers into strings. Similar to + * nf() but puts a "+" in front of positive numbers and a "-" in + * front of negative numbers. There are two versions, one for formatting + * floats and one for formatting ints. The values for the digits, + * left, and right parameters should always be positive integers. + * + * ( end auto-generated ) + * @webref data:string_functions + * @param num the number to format + * @param digits number of digits to pad with zeroes + * @see PApplet#nf(float, int, int) + * @see PApplet#nfs(float, int, int) + * @see PApplet#nfc(float, int) + */ + static public String nfp(int num, int digits) { + return (num < 0) ? nf(num, digits) : ('+' + nf(num, digits)); + } + /** + * @param nums the numbers to format + */ + static public String[] nfp(int nums[], int digits) { + String formatted[] = new String[nums.length]; + for (int i = 0; i < formatted.length; i++) { + formatted[i] = nfp(nums[i], digits); + } + return formatted; + } + + + + ////////////////////////////////////////////////////////////// + + // FLOAT NUMBER FORMATTING + + static private NumberFormat float_nf; + static private int float_nf_left, float_nf_right; + static private boolean float_nf_commas; + + /** + * @param left number of digits to the left of the decimal point + * @param right number of digits to the right of the decimal point + */ + static public String[] nf(float nums[], int left, int right) { + String formatted[] = new String[nums.length]; + for (int i = 0; i < formatted.length; i++) { + formatted[i] = nf(nums[i], left, right); + } + return formatted; + } + + static public String nf(float num, int left, int right) { + if ((float_nf != null) && + (float_nf_left == left) && + (float_nf_right == right) && + !float_nf_commas) { + return float_nf.format(num); + } + + float_nf = NumberFormat.getInstance(); + float_nf.setGroupingUsed(false); + float_nf_commas = false; + + if (left != 0) float_nf.setMinimumIntegerDigits(left); + if (right != 0) { + float_nf.setMinimumFractionDigits(right); + float_nf.setMaximumFractionDigits(right); + } + float_nf_left = left; + float_nf_right = right; + return float_nf.format(num); + } + + /** + * @param right number of digits to the right of the decimal point + */ + static public String[] nfc(float nums[], int right) { + String formatted[] = new String[nums.length]; + for (int i = 0; i < formatted.length; i++) { + formatted[i] = nfc(nums[i], right); + } + return formatted; + } + + static public String nfc(float num, int right) { + if ((float_nf != null) && + (float_nf_left == 0) && + (float_nf_right == right) && + float_nf_commas) { + return float_nf.format(num); + } + + float_nf = NumberFormat.getInstance(); + float_nf.setGroupingUsed(true); + float_nf_commas = true; + + if (right != 0) { + float_nf.setMinimumFractionDigits(right); + float_nf.setMaximumFractionDigits(right); + } + float_nf_left = 0; + float_nf_right = right; + return float_nf.format(num); + } + + + /** + * @param left the number of digits to the left of the decimal point + * @param right the number of digits to the right of the decimal point + */ + static public String[] nfs(float nums[], int left, int right) { + String formatted[] = new String[nums.length]; + for (int i = 0; i < formatted.length; i++) { + formatted[i] = nfs(nums[i], left, right); + } + return formatted; + } + + static public String nfs(float num, int left, int right) { + return (num < 0) ? nf(num, left, right) : (' ' + nf(num, left, right)); + } + + /** + * @param left the number of digits to the left of the decimal point + * @param right the number of digits to the right of the decimal point + */ + static public String[] nfp(float nums[], int left, int right) { + String formatted[] = new String[nums.length]; + for (int i = 0; i < formatted.length; i++) { + formatted[i] = nfp(nums[i], left, right); + } + return formatted; + } + + static public String nfp(float num, int left, int right) { + return (num < 0) ? nf(num, left, right) : ('+' + nf(num, left, right)); + } + + + + ////////////////////////////////////////////////////////////// + + // HEX/BINARY CONVERSION + + + /** + * ( begin auto-generated from hex.xml ) + * + * Converts a byte, char, int, or color to a String containing the + * equivalent hexadecimal notation. For example color(0, 102, 153) will + * convert to the String "FF006699". This function can help make your geeky + * debugging sessions much happier. + *

      + * Note that the maximum number of digits is 8, because an int value can + * only represent up to 32 bits. Specifying more than eight digits will + * simply shorten the string to eight anyway. + * + * ( end auto-generated ) + * @webref data:conversion + * @param value the value to convert + * @see PApplet#unhex(String) + * @see PApplet#binary(byte) + * @see PApplet#unbinary(String) + */ + static final public String hex(byte value) { + return hex(value, 2); + } + + static final public String hex(char value) { + return hex(value, 4); + } + + static final public String hex(int value) { + return hex(value, 8); + } +/** + * @param digits the number of digits (maximum 8) + */ + static final public String hex(int value, int digits) { + String stuff = Integer.toHexString(value).toUpperCase(); + if (digits > 8) { + digits = 8; + } + + int length = stuff.length(); + if (length > digits) { + return stuff.substring(length - digits); + + } else if (length < digits) { + return "00000000".substring(8 - (digits-length)) + stuff; + } + return stuff; + } + + /** + * ( begin auto-generated from unhex.xml ) + * + * Converts a String representation of a hexadecimal number to its + * equivalent integer value. + * + * ( end auto-generated ) + * + * @webref data:conversion + * @param value String to convert to an integer + * @see PApplet#hex(int, int) + * @see PApplet#binary(byte) + * @see PApplet#unbinary(String) + */ + static final public int unhex(String value) { + // has to parse as a Long so that it'll work for numbers bigger than 2^31 + return (int) (Long.parseLong(value, 16)); + } + + // + + /** + * Returns a String that contains the binary value of a byte. + * The returned value will always have 8 digits. + */ + static final public String binary(byte value) { + return binary(value, 8); + } + + /** + * Returns a String that contains the binary value of a char. + * The returned value will always have 16 digits because chars + * are two bytes long. + */ + static final public String binary(char value) { + return binary(value, 16); + } + + /** + * Returns a String that contains the binary value of an int. The length + * depends on the size of the number itself. If you want a specific number + * of digits use binary(int what, int digits) to specify how many. + */ + static final public String binary(int value) { + return binary(value, 32); + } + + /* + * Returns a String that contains the binary value of an int. + * The digits parameter determines how many digits will be used. + */ + + /** + * ( begin auto-generated from binary.xml ) + * + * Converts a byte, char, int, or color to a String containing the + * equivalent binary notation. For example color(0, 102, 153, 255) will + * convert to the String "11111111000000000110011010011001". This function + * can help make your geeky debugging sessions much happier. + *

      + * Note that the maximum number of digits is 32, because an int value can + * only represent up to 32 bits. Specifying more than 32 digits will simply + * shorten the string to 32 anyway. + * + * ( end auto-generated ) + * @webref data:conversion + * @param value value to convert + * @param digits number of digits to return + * @see PApplet#unbinary(String) + * @see PApplet#hex(int,int) + * @see PApplet#unhex(String) + */ + static final public String binary(int value, int digits) { + String stuff = Integer.toBinaryString(value); + if (digits > 32) { + digits = 32; + } + + int length = stuff.length(); + if (length > digits) { + return stuff.substring(length - digits); + + } else if (length < digits) { + int offset = 32 - (digits-length); + return "00000000000000000000000000000000".substring(offset) + stuff; + } + return stuff; + } + + + /** + * ( begin auto-generated from unbinary.xml ) + * + * Converts a String representation of a binary number to its equivalent + * integer value. For example, unbinary("00001000") will return 8. + * + * ( end auto-generated ) + * @webref data:conversion + * @param value String to convert to an integer + * @see PApplet#binary(byte) + * @see PApplet#hex(int,int) + * @see PApplet#unhex(String) + */ + static final public int unbinary(String value) { + return Integer.parseInt(value, 2); + } + + + + ////////////////////////////////////////////////////////////// + + // COLOR FUNCTIONS + + // moved here so that they can work without + // the graphics actually being instantiated (outside setup) + + + /** + * ( begin auto-generated from color.xml ) + * + * Creates colors for storing in variables of the color datatype. + * The parameters are interpreted as RGB or HSB values depending on the + * current colorMode(). The default mode is RGB values from 0 to 255 + * and therefore, the function call color(255, 204, 0) will return a + * bright yellow color. More about how colors are stored can be found in + * the reference for the color datatype. + * + * ( end auto-generated ) + * @webref color:creating_reading + * @param gray number specifying value between white and black + * @see PApplet#colorMode(int) + */ + public final int color(int gray) { + if (g == null) { + if (gray > 255) gray = 255; else if (gray < 0) gray = 0; + return 0xff000000 | (gray << 16) | (gray << 8) | gray; + } + return g.color(gray); + } + + + /** + * @nowebref + * @param fgray number specifying value between white and black + */ + public final int color(float fgray) { + if (g == null) { + int gray = (int) fgray; + if (gray > 255) gray = 255; else if (gray < 0) gray = 0; + return 0xff000000 | (gray << 16) | (gray << 8) | gray; + } + return g.color(fgray); + } + + + /** + * As of 0116 this also takes color(#FF8800, alpha) + * @param alpha relative to current color range + */ + public final int color(int gray, int alpha) { + if (g == null) { + if (alpha > 255) alpha = 255; else if (alpha < 0) alpha = 0; + if (gray > 255) { + // then assume this is actually a #FF8800 + return (alpha << 24) | (gray & 0xFFFFFF); + } else { + //if (gray > 255) gray = 255; else if (gray < 0) gray = 0; + return (alpha << 24) | (gray << 16) | (gray << 8) | gray; + } + } + return g.color(gray, alpha); + } + + + /** + * @nowebref + */ + public final int color(float fgray, float falpha) { + if (g == null) { + int gray = (int) fgray; + int alpha = (int) falpha; + if (gray > 255) gray = 255; else if (gray < 0) gray = 0; + if (alpha > 255) alpha = 255; else if (alpha < 0) alpha = 0; + return (alpha << 24) | (gray << 16) | (gray << 8) | gray; + } + return g.color(fgray, falpha); + } + + + /** + * @param v1 red or hue values relative to the current color range + * @param v2 green or saturation values relative to the current color range + * @param v3 blue or brightness values relative to the current color range + */ + public final int color(int v1, int v2, int v3) { + if (g == null) { + if (v1 > 255) v1 = 255; else if (v1 < 0) v1 = 0; + if (v2 > 255) v2 = 255; else if (v2 < 0) v2 = 0; + if (v3 > 255) v3 = 255; else if (v3 < 0) v3 = 0; + + return 0xff000000 | (v1 << 16) | (v2 << 8) | v3; + } + return g.color(v1, v2, v3); + } + + + public final int color(int v1, int v2, int v3, int alpha) { + if (g == null) { + if (alpha > 255) alpha = 255; else if (alpha < 0) alpha = 0; + if (v1 > 255) v1 = 255; else if (v1 < 0) v1 = 0; + if (v2 > 255) v2 = 255; else if (v2 < 0) v2 = 0; + if (v3 > 255) v3 = 255; else if (v3 < 0) v3 = 0; + + return (alpha << 24) | (v1 << 16) | (v2 << 8) | v3; + } + return g.color(v1, v2, v3, alpha); + } + + + public final int color(float v1, float v2, float v3) { + if (g == null) { + if (v1 > 255) v1 = 255; else if (v1 < 0) v1 = 0; + if (v2 > 255) v2 = 255; else if (v2 < 0) v2 = 0; + if (v3 > 255) v3 = 255; else if (v3 < 0) v3 = 0; + + return 0xff000000 | ((int)v1 << 16) | ((int)v2 << 8) | (int)v3; + } + return g.color(v1, v2, v3); + } + + + public final int color(float v1, float v2, float v3, float alpha) { + if (g == null) { + if (alpha > 255) alpha = 255; else if (alpha < 0) alpha = 0; + if (v1 > 255) v1 = 255; else if (v1 < 0) v1 = 0; + if (v2 > 255) v2 = 255; else if (v2 < 0) v2 = 0; + if (v3 > 255) v3 = 255; else if (v3 < 0) v3 = 0; + + return ((int)alpha << 24) | ((int)v1 << 16) | ((int)v2 << 8) | (int)v3; + } + return g.color(v1, v2, v3, alpha); + } + + + /** + * ( begin auto-generated from lerpColor.xml ) + * + * Calculates a color or colors between two color at a specific increment. + * The amt parameter is the amount to interpolate between the two + * values where 0.0 equal to the first point, 0.1 is very near the first + * point, 0.5 is half-way in between, etc. + * + * ( end auto-generated ) + * + * @webref color:creating_reading + * @usage web_application + * @param c1 interpolate from this color + * @param c2 interpolate to this color + * @param amt between 0.0 and 1.0 + * @see PImage#blendColor(int, int, int) + * @see PGraphics#color(float, float, float, float) + * @see PApplet#lerp(float, float, float) + */ + public int lerpColor(int c1, int c2, float amt) { + if (g != null) { + return g.lerpColor(c1, c2, amt); + } + // use the default mode (RGB) if lerpColor is called before setup() + return PGraphics.lerpColor(c1, c2, amt, RGB); + } + + + static public int blendColor(int c1, int c2, int mode) { + return PImage.blendColor(c1, c2, mode); + } + + + + ////////////////////////////////////////////////////////////// + + + public void frameMoved(int x, int y) { + if (!fullScreen) { + System.err.println(EXTERNAL_MOVE + " " + x + " " + y); + System.err.flush(); // doesn't seem to help or hurt + } + } + + + public void frameResized(int w, int h) { + } + + + ////////////////////////////////////////////////////////////// + + // MAIN + + + /** + * main() method for running this class from the command line. + *

      + * Usage: PApplet [options] <class name> [sketch args] + *

        + *
      • The [options] are one or several of the parameters seen below. + *
      • The class name is required. If you're running outside the PDE and + * your class is in a package, this should include the full name. That means + * that if the class is called Sketchy and the package is com.sketchycompany + * then com.sketchycompany.Sketchy should be used as the class name. + *
      • The [sketch args] are any command line parameters you want to send to + * the sketch itself. These will be passed into the args[] array in PApplet. + *

        + * The simplest way to turn and sketch into an application is to + * add the following code to your program: + *

        static public void main(String args[]) {
        +   *   PApplet.main("YourSketchName");
        +   * }
        + * That will properly launch your code from a double-clickable .jar + * or from the command line. + *
        +   * Parameters useful for launching or also used by the PDE:
        +   *
        +   * --location=x,y         Upper-lefthand corner of where the applet
        +   *                        should appear on screen. If not used,
        +   *                        the default is to center on the main screen.
        +   *
        +   * --present              Presentation mode: blanks the entire screen and
        +   *                        shows the sketch by itself. If the sketch is
        +   *                        smaller than the screen, the background around it
        +   *                        will use the --window-color setting.
        +   *
        +   * --hide-stop            Use to hide the stop button in situations where
        +   *                        you don't want to allow users to exit. also
        +   *                        see the FAQ on information for capturing the ESC
        +   *                        key when running in presentation mode.
        +   *
        +   * --stop-color=#xxxxxx   Color of the 'stop' text used to quit an
        +   *                        sketch when it's in present mode.
        +   *
        +   * --window-color=#xxxxxx Background color of the window. The color used
        +   *                        around the sketch when it's smaller than the
        +   *                        minimum window size for the OS, and the matte
        +   *                        color when using 'present' mode.
        +   *
        +   * --sketch-path          Location of where to save files from functions
        +   *                        like saveStrings() or saveFrame(). defaults to
        +   *                        the folder that the java application was
        +   *                        launched from, which means if this isn't set by
        +   *                        the pde, everything goes into the same folder
        +   *                        as processing.exe.
        +   *
        +   * --display=n            Set what display should be used by this sketch.
        +   *                        Displays are numbered starting from 1. This will
        +   *                        be overridden by fullScreen() calls that specify
        +   *                        a display. Omitting this option will cause the
        +   *                        default display to be used.
        +   *
        +   * Parameters used by Processing when running via the PDE
        +   *
        +   * --external             set when the applet is being used by the PDE
        +   *
        +   * --editor-location=x,y  position of the upper-lefthand corner of the
        +   *                        editor window, for placement of applet window
        +   *
        +   * All parameters *after* the sketch class name are passed to the sketch
        +   * itself and available from its 'args' array while the sketch is running.
        +   *
        +   * @see PApplet#args
        +   * 
        + */ + static public void main(final String[] args) { + runSketch(args, null); + } + + + /** + * Convenience method so that PApplet.main(YourSketch.class) + * launches a sketch, rather than having to call getName() on it. + */ + static public void main(final Class mainClass, String... args) { + main(mainClass.getName(), args); + } + + + /** + * Convenience method so that PApplet.main("YourSketch") launches a sketch, + * rather than having to wrap it into a single element String array. + * @param mainClass name of the class to load (with package if any) + */ + static public void main(final String mainClass) { + main(mainClass, null); + } + + + /** + * Convenience method so that PApplet.main("YourSketch", args) launches a + * sketch, rather than having to wrap it into a String array, and appending + * the 'args' array when not null. + * @param mainClass name of the class to load (with package if any) + * @param sketchArgs command line arguments to pass to the sketch's 'args' + * array. Note that this is not the same as the args passed + * to (and understood by) PApplet such as --display. + */ + static public void main(final String mainClass, final String[] sketchArgs) { + String[] args = new String[] { mainClass }; + if (sketchArgs != null) { + args = concat(args, sketchArgs); + } + runSketch(args, null); + } + + + // Moving this back off the EDT for alpha 10. Not sure if we're helping or + // hurting, but unless we do, errors inside settings() are never passed + // through to the PDE. There are other ways around that, no doubt, but I'm + // also suspecting that these "not showing up" bugs might be EDT issues. + static public void runSketch(final String[] args, + final PApplet constructedSketch) { +// EventQueue.invokeLater(new Runnable() { +// public void run() { +// runSketchEDT(args, constructedSketch); +// } +// }); +// } +// +// +// /** +// * Moving this to the EDT for 3.0a6 because that's the proper thing to do +// * when messing with Swing components. But mostly we're AWT, so who knows. +// */ +// static protected void runSketchEDT(final String[] args, +// final PApplet constructedSketch) { + // Supposed to help with flicker, but no effect on OS X. + // TODO IIRC this helped on Windows, but need to double check. + System.setProperty("sun.awt.noerasebackground", "true"); + + // Remove 60fps limit on the JavaFX "pulse" timer + System.setProperty("javafx.animation.fullspeed", "true"); + + // Doesn't seem to do anything helpful here (that can't be done via Runner) + //System.setProperty("com.apple.mrj.application.apple.menu.about.name", "potato"); + + Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + public void uncaughtException(Thread t, Throwable e) { + e.printStackTrace(); + uncaughtThrowable = e; + } + }); + + // This doesn't work, need to mess with Info.plist instead + /* + // In an exported application, add the Contents/Java folder to the + // java.library.path, so that native libraries work properly. + // Without this, the library path is only set to Contents/MacOS + // where the launcher binary lives. + if (platform == MACOSX) { + URL coreJarURL = + PApplet.class.getProtectionDomain().getCodeSource().getLocation(); + // The jarPath from above will/may be URL encoded (%20 for spaces) + String coreJarPath = urlDecode(coreJarURL.getPath()); + if (coreJarPath.endsWith("/Contents/Java/core.jar")) { + // remove the /core.jar part from the end + String javaPath = coreJarPath.substring(0, coreJarPath.length() - 9); + String libraryPath = System.getProperty("java.library.path"); + libraryPath += File.pathSeparator + javaPath; + System.setProperty("java.library.path", libraryPath); + } + } + */ + + // Catch any HeadlessException to provide more useful feedback + try { + // Call validate() while resize events are in progress + Toolkit.getDefaultToolkit().setDynamicLayout(true); + } catch (HeadlessException e) { + System.err.println("Cannot run sketch without a display. Read this for possible solutions:"); + System.err.println("https://github.com/processing/processing/wiki/Running-without-a-Display"); + System.exit(1); + } + + // So that the system proxy setting are used by default + System.setProperty("java.net.useSystemProxies", "true"); + + if (args.length < 1) { + System.err.println("Usage: PApplet [options] [sketch args]"); + System.err.println("See the Javadoc for PApplet for an explanation."); + System.exit(1); + } + + boolean external = false; + int[] location = null; + int[] editorLocation = null; + + String name = null; + int windowColor = 0; + int stopColor = 0xff808080; + boolean hideStop = false; + + int displayNum = -1; // use default +// boolean fullScreen = false; + boolean present = false; +// boolean spanDisplays = false; + int density = -1; + + String param = null, value = null; + String folder = calcSketchPath(); + + int argIndex = 0; + while (argIndex < args.length) { + int equals = args[argIndex].indexOf('='); + if (equals != -1) { + param = args[argIndex].substring(0, equals); + value = args[argIndex].substring(equals + 1); + + if (param.equals(ARGS_EDITOR_LOCATION)) { + external = true; + editorLocation = parseInt(split(value, ',')); + + } else if (param.equals(ARGS_DISPLAY)) { + displayNum = parseInt(value, -1); + if (displayNum == -1) { + System.err.println("Could not parse " + value + " for " + ARGS_DISPLAY); + } + + } else if (param.equals(ARGS_WINDOW_COLOR)) { + if (value.charAt(0) == '#' && value.length() == 7) { + value = value.substring(1); + windowColor = 0xff000000 | Integer.parseInt(value, 16); + } else { + System.err.println(ARGS_WINDOW_COLOR + " should be a # followed by six digits"); + } + + } else if (param.equals(ARGS_STOP_COLOR)) { + if (value.charAt(0) == '#' && value.length() == 7) { + value = value.substring(1); + stopColor = 0xff000000 | Integer.parseInt(value, 16); + } else { + System.err.println(ARGS_STOP_COLOR + " should be a # followed by six digits"); + } + + } else if (param.equals(ARGS_SKETCH_FOLDER)) { + folder = value; + + } else if (param.equals(ARGS_LOCATION)) { + location = parseInt(split(value, ',')); + + } else if (param.equals(ARGS_DENSITY)) { + density = parseInt(value, -1); + if (density == -1) { + System.err.println("Could not parse " + value + " for " + ARGS_DENSITY); + } else if (density != 1 && density != 2) { + density = -1; + System.err.println(ARGS_DENSITY + " should be 1 or 2"); + } + } + + } else { + if (args[argIndex].equals(ARGS_PRESENT)) { + present = true; + +// } else if (args[argIndex].equals(ARGS_SPAN_DISPLAYS)) { +// spanDisplays = true; + + } else if (args[argIndex].equals(ARGS_HIDE_STOP)) { + hideStop = true; + + } else if (args[argIndex].equals(ARGS_EXTERNAL)) { + external = true; + + } else { + name = args[argIndex]; + break; // because of break, argIndex won't increment again + } + } + argIndex++; + } + +// // Now that sketch path is passed in args after the sketch name +// // it's not set in the above loop(the above loop breaks after +// // finding sketch name). So setting sketch path here. +// // https://github.com/processing/processing/commit/0a14835e6f5f4766b022e73a8fe562318636727c +// // TODO this is a hack added for PDE X and needs to be removed [fry 141104] +// for (int i = 0; i < args.length; i++) { +// if (args[i].startsWith(ARGS_SKETCH_FOLDER)){ +// folder = args[i].substring(args[i].indexOf('=') + 1); +// } +// } + + final PApplet sketch; + if (constructedSketch != null) { + sketch = constructedSketch; + } else { + try { + Class c = + Thread.currentThread().getContextClassLoader().loadClass(name); + sketch = (PApplet) c.getDeclaredConstructor().newInstance(); + } catch (RuntimeException re) { + // Don't re-package runtime exceptions + throw re; + } catch (Exception e) { + // Package non-runtime exceptions so we can throw them freely + throw new RuntimeException(e); + } + } + + if (platform == MACOSX) { + try { + final String td = "processing.core.ThinkDifferent"; + Class thinkDifferent = + Thread.currentThread().getContextClassLoader().loadClass(td); + Method method = + thinkDifferent.getMethod("init", new Class[] { PApplet.class }); + method.invoke(null, new Object[] { sketch }); + } catch (Exception e) { + e.printStackTrace(); // That's unfortunate + } + } + + // Set the suggested display that's coming from the command line + // (and most likely, from the PDE's preference setting). + sketch.display = displayNum; + + // Set the suggested density that is coming from command line + // (most likely set from the PDE based on a system DPI scaling) + sketch.suggestedDensity = density; + + sketch.present = present; + + // For 3.0.1, moved this above handleSettings() so that loadImage() can be + // used inside settings(). Sets a terrible precedent, but the alternative + // of not being able to size a sketch to an image is driving people loopy. + // A handful of things that need to be set before init/start. +// if (folder == null) { +// folder = calcSketchPath(); +// } + sketch.sketchPath = folder; + + // Don't set 'args' to a zero-length array if it should be null [3.0a8] + if (args.length != argIndex + 1) { + // pass everything after the class name in as args to the sketch itself + // (fixed for 2.0a5, this was just subsetting by 1, which didn't skip opts) + sketch.args = PApplet.subset(args, argIndex + 1); + } + + // Call the settings() method which will give us our size() call +// try { + sketch.handleSettings(); +// } catch (Throwable t) { +// System.err.println("I think I'm gonna hurl"); +// } + +//// sketch.spanDisplays = spanDisplays; +// // If spanning screens, that means we're also full screen. +//// fullScreen |= spanDisplays; +// if (spanDisplays) { +// displayIndex = SPAN; +//// fullScreen = true; +// } + +// // If the applet doesn't call for full screen, but the command line does, +// // enable it. Conversely, if the command line does not, don't disable it. +// // Query the applet to see if it wants to be full screen all the time. +// //fullScreen |= sketch.sketchFullScreen(); +// sketch.fullScreen |= fullScreen; + + sketch.external = external; + + if (windowColor != 0) { + sketch.windowColor = windowColor; + } + + final PSurface surface = sketch.initSurface(); +// sketch.initSurface(windowColor, displayIndex, fullScreen, spanDisplays); + + /* + // Wait until the applet has figured out its width. In a static mode app, + // everything happens inside setup(), so this will be after setup() has + // completed, and the empty draw() has set "finished" to true. + while (sketch.defaultSize && !sketch.finished) { + //System.out.println("default size"); + try { + Thread.sleep(5); + + } catch (InterruptedException e) { + //System.out.println("interrupt"); + } + } + */ + + if (present) { + if (hideStop) { + stopColor = 0; // they'll get the hint + } + surface.placePresent(stopColor); + } else { + surface.placeWindow(location, editorLocation); + } + + // not always running externally when in present mode + // moved above setVisible() in 3.0 alpha 11 + if (sketch.external) { + surface.setupExternalMessages(); + } + + sketch.showSurface(); + sketch.startSurface(); + /* + if (sketch.getGraphics().displayable()) { + surface.setVisible(true); + } + + //sketch.init(); + surface.startThread(); + */ + } + + + /** Danger: available for advanced subclassing, but here be dragons. */ + protected void showSurface() { + if (getGraphics().displayable()) { + surface.setVisible(true); + } + } + + + /** See warning in showSurface() */ + protected void startSurface() { + surface.startThread(); + } + + + protected PSurface initSurface() { + g = createPrimaryGraphics(); + surface = g.createSurface(); + + // Create fake Frame object to warn user about the changes + if (g.displayable()) { + frame = new Frame() { + @Override + public void setResizable(boolean resizable) { + deprecationWarning("setResizable"); + surface.setResizable(resizable); + } + + @Override + public void setVisible(boolean visible) { + deprecationWarning("setVisible"); + surface.setVisible(visible); + } + + @Override + public void setTitle(String title) { + deprecationWarning("setTitle"); + surface.setTitle(title); + } + + @Override + public void setUndecorated(boolean ignored) { + throw new RuntimeException("'frame' has been removed from Processing 3, " + + "use fullScreen() to get an undecorated full screen frame"); + } + + // Can't override this one because it's called by Window's constructor + /* + @Override + public void setLocation(int x, int y) { + deprecationWarning("setLocation"); + surface.setLocation(x, y); + } + */ + + @Override + public void setSize(int w, int h) { + deprecationWarning("setSize"); + surface.setSize(w, h); + } + + private void deprecationWarning(String method) { + PGraphics.showWarning("Use surface." + method + "() instead of " + + "frame." + method + " in Processing 3"); + //new Exception(method).printStackTrace(System.out); + } + }; + + surface.initFrame(this); //, backgroundColor, displayNum, fullScreen, spanDisplays); + surface.setTitle(getClass().getSimpleName()); + + } else { + surface.initOffscreen(this); // for PDF/PSurfaceNone and friends + } + +// init(); + return surface; + } + + +// protected void createSurface() { +// surface = g.createSurface(); +// if (surface == null) { +// System.err.println("This renderer needs to be updated for Processing 3"); +// System.err.println("The createSurface() method returned null."); +// System.exit(1); +// } +// } + + +// /** +// * Return a Canvas object that can be embedded into other Java GUIs. +// * This is necessary because PApplet no longer subclasses Component. +// * +// *
        +//   * PApplet sketch = new EmbedSketch();
        +//   * Canvas canvas = sketch.getCanvas();
        +//   * // add the canvas object to your project and validate() it
        +//   * sketch.init()  // start the animation thread
        +//   */
        +//  public Component getComponent() {
        +//    g = createPrimaryGraphics();
        +//    surface = g.createSurface();
        +//    return surface.initComponent(this);
        +//  }
        +
        +
        +  /** Convenience method, should only be called by PSurface subclasses. */
        +  static public void hideMenuBar() {
        +    if (PApplet.platform == PConstants.MACOSX) {
        +      // Call some native code to remove the menu bar on OS X. Not necessary
        +      // on Linux and Windows, who are happy to make full screen windows.
        +      japplemenubar.JAppleMenuBar.hide();
        +    }
        +  }
        +
        +
        +  /**
        +   * Convenience method for Python Mode to run an already-constructed sketch.
        +   * This makes it makes it easy to launch a sketch in Jython:
        +   *
        +   * 
        class MySketch(PApplet):
        +   *     pass
        +   *
        +   *MySketch().runSketch();
        + */ + protected void runSketch(final String[] args) { + final String[] argsWithSketchName = new String[args.length + 1]; + System.arraycopy(args, 0, argsWithSketchName, 0, args.length); + final String className = this.getClass().getSimpleName(); + final String cleanedClass = + className.replaceAll("__[^_]+__\\$", "").replaceAll("\\$\\d+", ""); + argsWithSketchName[args.length] = cleanedClass; + runSketch(argsWithSketchName, this); + } + + + /** Convenience method for Python Mode */ + protected void runSketch() { + runSketch(new String[0]); + } + + /** Convenience method for propane with jdk9 */ + public void runPropane() { + runSketch(new String[0]); + } + + + ////////////////////////////////////////////////////////////// + + + /** + * ( begin auto-generated from beginRecord.xml ) + * + * Opens a new file and all subsequent drawing functions are echoed to this + * file as well as the display window. The beginRecord() function + * requires two parameters, the first is the renderer and the second is the + * file name. This function is always used with endRecord() to stop + * the recording process and close the file. + *

        + * Note that beginRecord() will only pick up any settings that happen after + * it has been called. For instance, if you call textFont() before + * beginRecord(), then that font will not be set for the file that you're + * recording to. + * + * ( end auto-generated ) + * + * @webref output:files + * @param renderer PDF or SVG + * @param filename filename for output + * @see PApplet#endRecord() + */ + public PGraphics beginRecord(String renderer, String filename) { + filename = insertFrame(filename); + PGraphics rec = createGraphics(width, height, renderer, filename); + beginRecord(rec); + return rec; + } + + + /** + * @nowebref + * Begin recording (echoing) commands to the specified PGraphics object. + */ + public void beginRecord(PGraphics recorder) { + this.recorder = recorder; + recorder.beginDraw(); + } + + + /** + * ( begin auto-generated from endRecord.xml ) + * + * Stops the recording process started by beginRecord() and closes + * the file. + * + * ( end auto-generated ) + * @webref output:files + * @see PApplet#beginRecord(String, String) + */ + public void endRecord() { + if (recorder != null) { + recorder.endDraw(); + recorder.dispose(); + recorder = null; + } + } + + + /** + * ( begin auto-generated from beginRaw.xml ) + * + * To create vectors from 3D data, use the beginRaw() and + * endRaw() commands. These commands will grab the shape data just + * before it is rendered to the screen. At this stage, your entire scene is + * nothing but a long list of individual lines and triangles. This means + * that a shape created with sphere() function will be made up of + * hundreds of triangles, rather than a single object. Or that a + * multi-segment line shape (such as a curve) will be rendered as + * individual segments. + *

        + * When using beginRaw() and endRaw(), it's possible to write + * to either a 2D or 3D renderer. For instance, beginRaw() with the + * PDF library will write the geometry as flattened triangles and lines, + * even if recording from the P3D renderer. + *

        + * If you want a background to show up in your files, use rect(0, 0, + * width, height) after setting the fill() to the background + * color. Otherwise the background will not be rendered to the file because + * the background is not shape. + *

        + * Using hint(ENABLE_DEPTH_SORT) can improve the appearance of 3D + * geometry drawn to 2D file formats. See the hint() reference for + * more details. + *

        + * See examples in the reference for the PDF and DXF + * libraries for more information. + * + * ( end auto-generated ) + * + * @webref output:files + * @param renderer for example, PDF or DXF + * @param filename filename for output + * @see PApplet#endRaw() + * @see PApplet#hint(int) + */ + public PGraphics beginRaw(String renderer, String filename) { + filename = insertFrame(filename); + PGraphics rec = createGraphics(width, height, renderer, filename); + g.beginRaw(rec); + return rec; + } + + + + /** + * @nowebref + * Begin recording raw shape data to the specified renderer. + * + * This simply echoes to g.beginRaw(), but since is placed here (rather than + * generated by preproc.pl) for clarity and so that it doesn't echo the + * command should beginRecord() be in use. + * + * @param rawGraphics ??? + */ + public void beginRaw(PGraphics rawGraphics) { + g.beginRaw(rawGraphics); + } + + + /** + * ( begin auto-generated from endRaw.xml ) + * + * Complement to beginRaw(); they must always be used together. See + * the beginRaw() reference for details. + * + * ( end auto-generated ) + * + * @webref output:files + * @see PApplet#beginRaw(String, String) + */ + public void endRaw() { + g.endRaw(); + } + + + /** + * Starts shape recording and returns the PShape object that will + * contain the geometry. + */ + /* + public PShape beginRecord() { + return g.beginRecord(); + } + */ + + ////////////////////////////////////////////////////////////// + + + /** + * ( begin auto-generated from loadPixels.xml ) + * + * Loads the pixel data for the display window into the pixels[] + * array. This function must always be called before reading from or + * writing to pixels[]. + *

        renderers may or may not seem to require loadPixels() + * or updatePixels(). However, the rule is that any time you want to + * manipulate the pixels[] array, you must first call + * loadPixels(), and after changes have been made, call + * updatePixels(). Even if the renderer may not seem to use this + * function in the current Processing release, this will always be subject + * to change. + * + * ( end auto-generated ) + *

        Advanced

        + * Override the g.pixels[] function to set the pixels[] array + * that's part of the PApplet object. Allows the use of + * pixels[] in the code, rather than g.pixels[]. + * + * @webref image:pixels + * @see PApplet#pixels + * @see PApplet#updatePixels() + */ + public void loadPixels() { + g.loadPixels(); + pixels = g.pixels; + } + + /** + * ( begin auto-generated from updatePixels.xml ) + * + * Updates the display window with the data in the pixels[] array. + * Use in conjunction with loadPixels(). If you're only reading + * pixels from the array, there's no need to call updatePixels() + * unless there are changes. + *

        renderers may or may not seem to require loadPixels() + * or updatePixels(). However, the rule is that any time you want to + * manipulate the pixels[] array, you must first call + * loadPixels(), and after changes have been made, call + * updatePixels(). Even if the renderer may not seem to use this + * function in the current Processing release, this will always be subject + * to change. + *

        + * Currently, none of the renderers use the additional parameters to + * updatePixels(), however this may be implemented in the future. + * + * ( end auto-generated ) + * @webref image:pixels + * @see PApplet#loadPixels() + * @see PApplet#pixels + */ + public void updatePixels() { + g.updatePixels(); + } + + /** + * @nowebref + * @param x1 x-coordinate of the upper-left corner + * @param y1 y-coordinate of the upper-left corner + * @param x2 width of the region + * @param y2 height of the region + */ + public void updatePixels(int x1, int y1, int x2, int y2) { + g.updatePixels(x1, y1, x2, y2); + } + + + ////////////////////////////////////////////////////////////// + + // EVERYTHING BELOW THIS LINE IS AUTOMATICALLY GENERATED. DO NOT TOUCH! + // This includes the Javadoc comments, which are automatically copied from + // the PImage and PGraphics source code files. + + // public functions for processing.core + + + public PGL beginPGL() { + return g.beginPGL(); + } + + + public void endPGL() { + if (recorder != null) recorder.endPGL(); + g.endPGL(); + } + + + public void flush() { + if (recorder != null) recorder.flush(); + g.flush(); + } + + + public void hint(int which) { + if (recorder != null) recorder.hint(which); + g.hint(which); + } + + + /** + * Start a new shape of type POLYGON + */ + public void beginShape() { + if (recorder != null) recorder.beginShape(); + g.beginShape(); + } + + + /** + * ( begin auto-generated from beginShape.xml ) + * + * Using the beginShape() and endShape() functions allow + * creating more complex forms. beginShape() begins recording + * vertices for a shape and endShape() stops recording. The value of + * the MODE parameter tells it which types of shapes to create from + * the provided vertices. With no mode specified, the shape can be any + * irregular polygon. The parameters available for beginShape() are POINTS, + * LINES, TRIANGLES, TRIANGLE_FAN, TRIANGLE_STRIP, QUADS, and QUAD_STRIP. + * After calling the beginShape() function, a series of + * vertex() commands must follow. To stop drawing the shape, call + * endShape(). The vertex() function with two parameters + * specifies a position in 2D and the vertex() function with three + * parameters specifies a position in 3D. Each shape will be outlined with + * the current stroke color and filled with the fill color. + *

        + * Transformations such as translate(), rotate(), and + * scale() do not work within beginShape(). It is also not + * possible to use other shapes, such as ellipse() or rect() + * within beginShape(). + *

        + * The P3D renderer settings allow stroke() and fill() + * settings to be altered per-vertex, however the default P2D renderer does + * not. Settings such as strokeWeight(), strokeCap(), and + * strokeJoin() cannot be changed while inside a + * beginShape()/endShape() block with any renderer. + * + * ( end auto-generated ) + * @webref shape:vertex + * @param kind Either POINTS, LINES, TRIANGLES, TRIANGLE_FAN, TRIANGLE_STRIP, QUADS, or QUAD_STRIP + * @see PShape + * @see PGraphics#endShape() + * @see PGraphics#vertex(float, float, float, float, float) + * @see PGraphics#curveVertex(float, float, float) + * @see PGraphics#bezierVertex(float, float, float, float, float, float, float, float, float) + */ + public void beginShape(int kind) { + if (recorder != null) recorder.beginShape(kind); + g.beginShape(kind); + } + + + /** + * Sets whether the upcoming vertex is part of an edge. + * Equivalent to glEdgeFlag(), for people familiar with OpenGL. + */ + public void edge(boolean edge) { + if (recorder != null) recorder.edge(edge); + g.edge(edge); + } + + + /** + * ( begin auto-generated from normal.xml ) + * + * Sets the current normal vector. This is for drawing three dimensional + * shapes and surfaces and specifies a vector perpendicular to the surface + * of the shape which determines how lighting affects it. Processing + * attempts to automatically assign normals to shapes, but since that's + * imperfect, this is a better option when you want more control. This + * function is identical to glNormal3f() in OpenGL. + * + * ( end auto-generated ) + * @webref lights_camera:lights + * @param nx x direction + * @param ny y direction + * @param nz z direction + * @see PGraphics#beginShape(int) + * @see PGraphics#endShape(int) + * @see PGraphics#lights() + */ + public void normal(float nx, float ny, float nz) { + if (recorder != null) recorder.normal(nx, ny, nz); + g.normal(nx, ny, nz); + } + + + public void attribPosition(String name, float x, float y, float z) { + if (recorder != null) recorder.attribPosition(name, x, y, z); + g.attribPosition(name, x, y, z); + } + + + public void attribNormal(String name, float nx, float ny, float nz) { + if (recorder != null) recorder.attribNormal(name, nx, ny, nz); + g.attribNormal(name, nx, ny, nz); + } + + + public void attribColor(String name, int color) { + if (recorder != null) recorder.attribColor(name, color); + g.attribColor(name, color); + } + + + public void attrib(String name, float... values) { + if (recorder != null) recorder.attrib(name, values); + g.attrib(name, values); + } + + + public void attrib(String name, int... values) { + if (recorder != null) recorder.attrib(name, values); + g.attrib(name, values); + } + + + public void attrib(String name, boolean... values) { + if (recorder != null) recorder.attrib(name, values); + g.attrib(name, values); + } + + + /** + * ( begin auto-generated from textureMode.xml ) + * + * Sets the coordinate space for texture mapping. There are two options, + * IMAGE, which refers to the actual coordinates of the image, and + * NORMAL, which refers to a normalized space of values ranging from 0 + * to 1. The default mode is IMAGE. In IMAGE, if an image is 100 x 200 + * pixels, mapping the image onto the entire size of a quad would require + * the points (0,0) (0,100) (100,200) (0,200). The same mapping in + * NORMAL_SPACE is (0,0) (0,1) (1,1) (0,1). + * + * ( end auto-generated ) + * @webref image:textures + * @param mode either IMAGE or NORMAL + * @see PGraphics#texture(PImage) + * @see PGraphics#textureWrap(int) + */ + public void textureMode(int mode) { + if (recorder != null) recorder.textureMode(mode); + g.textureMode(mode); + } + + + /** + * ( begin auto-generated from textureWrap.xml ) + * + * Description to come... + * + * ( end auto-generated from textureWrap.xml ) + * + * @webref image:textures + * @param wrap Either CLAMP (default) or REPEAT + * @see PGraphics#texture(PImage) + * @see PGraphics#textureMode(int) + */ + public void textureWrap(int wrap) { + if (recorder != null) recorder.textureWrap(wrap); + g.textureWrap(wrap); + } + + + /** + * ( begin auto-generated from texture.xml ) + * + * Sets a texture to be applied to vertex points. The texture() + * function must be called between beginShape() and + * endShape() and before any calls to vertex(). + *

        + * When textures are in use, the fill color is ignored. Instead, use tint() + * to specify the color of the texture as it is applied to the shape. + * + * ( end auto-generated ) + * @webref image:textures + * @param image reference to a PImage object + * @see PGraphics#textureMode(int) + * @see PGraphics#textureWrap(int) + * @see PGraphics#beginShape(int) + * @see PGraphics#endShape(int) + * @see PGraphics#vertex(float, float, float, float, float) + */ + public void texture(PImage image) { + if (recorder != null) recorder.texture(image); + g.texture(image); + } + + + /** + * Removes texture image for current shape. + * Needs to be called between beginShape and endShape + * + */ + public void noTexture() { + if (recorder != null) recorder.noTexture(); + g.noTexture(); + } + + + public void vertex(float x, float y) { + if (recorder != null) recorder.vertex(x, y); + g.vertex(x, y); + } + + + public void vertex(float x, float y, float z) { + if (recorder != null) recorder.vertex(x, y, z); + g.vertex(x, y, z); + } + + + /** + * Used by renderer subclasses or PShape to efficiently pass in already + * formatted vertex information. + * @param v vertex parameters, as a float array of length VERTEX_FIELD_COUNT + */ + public void vertex(float[] v) { + if (recorder != null) recorder.vertex(v); + g.vertex(v); + } + + + public void vertex(float x, float y, float u, float v) { + if (recorder != null) recorder.vertex(x, y, u, v); + g.vertex(x, y, u, v); + } + + +/** + * ( begin auto-generated from vertex.xml ) + * + * All shapes are constructed by connecting a series of vertices. + * vertex() is used to specify the vertex coordinates for points, + * lines, triangles, quads, and polygons and is used exclusively within the + * beginShape() and endShape() function.
        + *
        + * Drawing a vertex in 3D using the z parameter requires the P3D + * parameter in combination with size as shown in the above example.
        + *
        + * This function is also used to map a texture onto the geometry. The + * texture() function declares the texture to apply to the geometry + * and the u and v coordinates set define the mapping of this + * texture to the form. By default, the coordinates used for u and + * v are specified in relation to the image's size in pixels, but + * this relation can be changed with textureMode(). + * + * ( end auto-generated ) + * @webref shape:vertex + * @param x x-coordinate of the vertex + * @param y y-coordinate of the vertex + * @param z z-coordinate of the vertex + * @param u horizontal coordinate for the texture mapping + * @param v vertical coordinate for the texture mapping + * @see PGraphics#beginShape(int) + * @see PGraphics#endShape(int) + * @see PGraphics#bezierVertex(float, float, float, float, float, float, float, float, float) + * @see PGraphics#quadraticVertex(float, float, float, float, float, float) + * @see PGraphics#curveVertex(float, float, float) + * @see PGraphics#texture(PImage) + */ + public void vertex(float x, float y, float z, float u, float v) { + if (recorder != null) recorder.vertex(x, y, z, u, v); + g.vertex(x, y, z, u, v); + } + + + /** + * @webref shape:vertex + */ + public void beginContour() { + if (recorder != null) recorder.beginContour(); + g.beginContour(); + } + + + /** + * @webref shape:vertex + */ + public void endContour() { + if (recorder != null) recorder.endContour(); + g.endContour(); + } + + + public void endShape() { + if (recorder != null) recorder.endShape(); + g.endShape(); + } + + + /** + * ( begin auto-generated from endShape.xml ) + * + * The endShape() function is the companion to beginShape() + * and may only be called after beginShape(). When endshape() + * is called, all of image data defined since the previous call to + * beginShape() is written into the image buffer. The constant CLOSE + * as the value for the MODE parameter to close the shape (to connect the + * beginning and the end). + * + * ( end auto-generated ) + * @webref shape:vertex + * @param mode use CLOSE to close the shape + * @see PShape + * @see PGraphics#beginShape(int) + */ + public void endShape(int mode) { + if (recorder != null) recorder.endShape(mode); + g.endShape(mode); + } + + + /** + * @webref shape + * @param filename name of file to load, can be .svg or .obj + * @see PShape + * @see PApplet#createShape() + */ + public PShape loadShape(String filename) { + return g.loadShape(filename); + } + + + /** + * @nowebref + */ + public PShape loadShape(String filename, String options) { + return g.loadShape(filename, options); + } + + + /** + * @webref shape + * @see PShape + * @see PShape#endShape() + * @see PApplet#loadShape(String) + */ + public PShape createShape() { + return g.createShape(); + } + + + public PShape createShape(int type) { + return g.createShape(type); + } + + + /** + * @param kind either POINT, LINE, TRIANGLE, QUAD, RECT, ELLIPSE, ARC, BOX, SPHERE + * @param p parameters that match the kind of shape + */ + public PShape createShape(int kind, float... p) { + return g.createShape(kind, p); + } + + + /** + * ( begin auto-generated from loadShader.xml ) + * + * This is a new reference entry for Processing 2.0. It will be updated shortly. + * + * ( end auto-generated ) + * + * @webref rendering:shaders + * @param fragFilename name of fragment shader file + */ + public PShader loadShader(String fragFilename) { + return g.loadShader(fragFilename); + } + + + /** + * @param vertFilename name of vertex shader file + */ + public PShader loadShader(String fragFilename, String vertFilename) { + return g.loadShader(fragFilename, vertFilename); + } + + + /** + * ( begin auto-generated from shader.xml ) + * + * This is a new reference entry for Processing 2.0. It will be updated shortly. + * + * ( end auto-generated ) + * + * @webref rendering:shaders + * @param shader name of shader file + */ + public void shader(PShader shader) { + if (recorder != null) recorder.shader(shader); + g.shader(shader); + } + + + /** + * @param kind type of shader, either POINTS, LINES, or TRIANGLES + */ + public void shader(PShader shader, int kind) { + if (recorder != null) recorder.shader(shader, kind); + g.shader(shader, kind); + } + + + /** + * ( begin auto-generated from resetShader.xml ) + * + * This is a new reference entry for Processing 2.0. It will be updated shortly. + * + * ( end auto-generated ) + * + * @webref rendering:shaders + */ + public void resetShader() { + if (recorder != null) recorder.resetShader(); + g.resetShader(); + } + + + /** + * @param kind type of shader, either POINTS, LINES, or TRIANGLES + */ + public void resetShader(int kind) { + if (recorder != null) recorder.resetShader(kind); + g.resetShader(kind); + } + + + /** + * @param shader the fragment shader to apply + */ + public void filter(PShader shader) { + if (recorder != null) recorder.filter(shader); + g.filter(shader); + } + + + /** + * ( begin auto-generated from clip.xml ) + * + * Limits the rendering to the boundaries of a rectangle defined + * by the parameters. The boundaries are drawn based on the state + * of the imageMode() fuction, either CORNER, CORNERS, or CENTER. + * + * ( end auto-generated ) + * + * @webref rendering + * @param a x-coordinate of the rectangle, by default + * @param b y-coordinate of the rectangle, by default + * @param c width of the rectangle, by default + * @param d height of the rectangle, by default + */ + public void clip(float a, float b, float c, float d) { + if (recorder != null) recorder.clip(a, b, c, d); + g.clip(a, b, c, d); + } + + + /** + * ( begin auto-generated from noClip.xml ) + * + * Disables the clipping previously started by the clip() function. + * + * ( end auto-generated ) + * + * @webref rendering + */ + public void noClip() { + if (recorder != null) recorder.noClip(); + g.noClip(); + } + + + /** + * ( begin auto-generated from blendMode.xml ) + * + * This is a new reference entry for Processing 2.0. It will be updated shortly. + * + * ( end auto-generated ) + * + * @webref rendering + * @param mode the blending mode to use + */ + public void blendMode(int mode) { + if (recorder != null) recorder.blendMode(mode); + g.blendMode(mode); + } + + + public void bezierVertex(float x2, float y2, + float x3, float y3, + float x4, float y4) { + if (recorder != null) recorder.bezierVertex(x2, y2, x3, y3, x4, y4); + g.bezierVertex(x2, y2, x3, y3, x4, y4); + } + + +/** + * ( begin auto-generated from bezierVertex.xml ) + * + * Specifies vertex coordinates for Bezier curves. Each call to + * bezierVertex() defines the position of two control points and one + * anchor point of a Bezier curve, adding a new segment to a line or shape. + * The first time bezierVertex() is used within a + * beginShape() call, it must be prefaced with a call to + * vertex() to set the first anchor point. This function must be + * used between beginShape() and endShape() and only when + * there is no MODE parameter specified to beginShape(). Using the + * 3D version requires rendering with P3D (see the Environment reference + * for more information). + * + * ( end auto-generated ) + * @webref shape:vertex + * @param x2 the x-coordinate of the 1st control point + * @param y2 the y-coordinate of the 1st control point + * @param z2 the z-coordinate of the 1st control point + * @param x3 the x-coordinate of the 2nd control point + * @param y3 the y-coordinate of the 2nd control point + * @param z3 the z-coordinate of the 2nd control point + * @param x4 the x-coordinate of the anchor point + * @param y4 the y-coordinate of the anchor point + * @param z4 the z-coordinate of the anchor point + * @see PGraphics#curveVertex(float, float, float) + * @see PGraphics#vertex(float, float, float, float, float) + * @see PGraphics#quadraticVertex(float, float, float, float, float, float) + * @see PGraphics#bezier(float, float, float, float, float, float, float, float, float, float, float, float) + */ + public void bezierVertex(float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + if (recorder != null) recorder.bezierVertex(x2, y2, z2, x3, y3, z3, x4, y4, z4); + g.bezierVertex(x2, y2, z2, x3, y3, z3, x4, y4, z4); + } + + + /** + * @webref shape:vertex + * @param cx the x-coordinate of the control point + * @param cy the y-coordinate of the control point + * @param x3 the x-coordinate of the anchor point + * @param y3 the y-coordinate of the anchor point + * @see PGraphics#curveVertex(float, float, float) + * @see PGraphics#vertex(float, float, float, float, float) + * @see PGraphics#bezierVertex(float, float, float, float, float, float) + * @see PGraphics#bezier(float, float, float, float, float, float, float, float, float, float, float, float) + */ + public void quadraticVertex(float cx, float cy, + float x3, float y3) { + if (recorder != null) recorder.quadraticVertex(cx, cy, x3, y3); + g.quadraticVertex(cx, cy, x3, y3); + } + + + /** + * @param cz the z-coordinate of the control point + * @param z3 the z-coordinate of the anchor point + */ + public void quadraticVertex(float cx, float cy, float cz, + float x3, float y3, float z3) { + if (recorder != null) recorder.quadraticVertex(cx, cy, cz, x3, y3, z3); + g.quadraticVertex(cx, cy, cz, x3, y3, z3); + } + + + /** + * ( begin auto-generated from curveVertex.xml ) + * + * Specifies vertex coordinates for curves. This function may only be used + * between beginShape() and endShape() and only when there is + * no MODE parameter specified to beginShape(). The first and last + * points in a series of curveVertex() lines will be used to guide + * the beginning and end of a the curve. A minimum of four points is + * required to draw a tiny curve between the second and third points. + * Adding a fifth point with curveVertex() will draw the curve + * between the second, third, and fourth points. The curveVertex() + * function is an implementation of Catmull-Rom splines. Using the 3D + * version requires rendering with P3D (see the Environment reference for + * more information). + * + * ( end auto-generated ) + * + * @webref shape:vertex + * @param x the x-coordinate of the vertex + * @param y the y-coordinate of the vertex + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#beginShape(int) + * @see PGraphics#endShape(int) + * @see PGraphics#vertex(float, float, float, float, float) + * @see PGraphics#bezier(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#quadraticVertex(float, float, float, float, float, float) + */ + public void curveVertex(float x, float y) { + if (recorder != null) recorder.curveVertex(x, y); + g.curveVertex(x, y); + } + + + /** + * @param z the z-coordinate of the vertex + */ + public void curveVertex(float x, float y, float z) { + if (recorder != null) recorder.curveVertex(x, y, z); + g.curveVertex(x, y, z); + } + + + /** + * ( begin auto-generated from point.xml ) + * + * Draws a point, a coordinate in space at the dimension of one pixel. The + * first parameter is the horizontal value for the point, the second value + * is the vertical value for the point, and the optional third value is the + * depth value. Drawing this shape in 3D with the z parameter + * requires the P3D parameter in combination with size() as shown in + * the above example. + * + * ( end auto-generated ) + * + * @webref shape:2d_primitives + * @param x x-coordinate of the point + * @param y y-coordinate of the point + * @see PGraphics#stroke(int) + */ + public void point(float x, float y) { + if (recorder != null) recorder.point(x, y); + g.point(x, y); + } + + + /** + * @param z z-coordinate of the point + */ + public void point(float x, float y, float z) { + if (recorder != null) recorder.point(x, y, z); + g.point(x, y, z); + } + + + /** + * ( begin auto-generated from line.xml ) + * + * Draws a line (a direct path between two points) to the screen. The + * version of line() with four parameters draws the line in 2D. To + * color a line, use the stroke() function. A line cannot be filled, + * therefore the fill() function will not affect the color of a + * line. 2D lines are drawn with a width of one pixel by default, but this + * can be changed with the strokeWeight() function. The version with + * six parameters allows the line to be placed anywhere within XYZ space. + * Drawing this shape in 3D with the z parameter requires the P3D + * parameter in combination with size() as shown in the above example. + * + * ( end auto-generated ) + * @webref shape:2d_primitives + * @param x1 x-coordinate of the first point + * @param y1 y-coordinate of the first point + * @param x2 x-coordinate of the second point + * @param y2 y-coordinate of the second point + * @see PGraphics#strokeWeight(float) + * @see PGraphics#strokeJoin(int) + * @see PGraphics#strokeCap(int) + * @see PGraphics#beginShape() + */ + public void line(float x1, float y1, float x2, float y2) { + if (recorder != null) recorder.line(x1, y1, x2, y2); + g.line(x1, y1, x2, y2); + } + + + /** + * @param z1 z-coordinate of the first point + * @param z2 z-coordinate of the second point + */ + public void line(float x1, float y1, float z1, + float x2, float y2, float z2) { + if (recorder != null) recorder.line(x1, y1, z1, x2, y2, z2); + g.line(x1, y1, z1, x2, y2, z2); + } + + + /** + * ( begin auto-generated from triangle.xml ) + * + * A triangle is a plane created by connecting three points. The first two + * arguments specify the first point, the middle two arguments specify the + * second point, and the last two arguments specify the third point. + * + * ( end auto-generated ) + * @webref shape:2d_primitives + * @param x1 x-coordinate of the first point + * @param y1 y-coordinate of the first point + * @param x2 x-coordinate of the second point + * @param y2 y-coordinate of the second point + * @param x3 x-coordinate of the third point + * @param y3 y-coordinate of the third point + * @see PApplet#beginShape() + */ + public void triangle(float x1, float y1, float x2, float y2, + float x3, float y3) { + if (recorder != null) recorder.triangle(x1, y1, x2, y2, x3, y3); + g.triangle(x1, y1, x2, y2, x3, y3); + } + + + /** + * ( begin auto-generated from quad.xml ) + * + * A quad is a quadrilateral, a four sided polygon. It is similar to a + * rectangle, but the angles between its edges are not constrained to + * ninety degrees. The first pair of parameters (x1,y1) sets the first + * vertex and the subsequent pairs should proceed clockwise or + * counter-clockwise around the defined shape. + * + * ( end auto-generated ) + * @webref shape:2d_primitives + * @param x1 x-coordinate of the first corner + * @param y1 y-coordinate of the first corner + * @param x2 x-coordinate of the second corner + * @param y2 y-coordinate of the second corner + * @param x3 x-coordinate of the third corner + * @param y3 y-coordinate of the third corner + * @param x4 x-coordinate of the fourth corner + * @param y4 y-coordinate of the fourth corner + */ + public void quad(float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4) { + if (recorder != null) recorder.quad(x1, y1, x2, y2, x3, y3, x4, y4); + g.quad(x1, y1, x2, y2, x3, y3, x4, y4); + } + + + /** + * ( begin auto-generated from rectMode.xml ) + * + * Modifies the location from which rectangles draw. The default mode is + * rectMode(CORNER), which specifies the location to be the upper + * left corner of the shape and uses the third and fourth parameters of + * rect() to specify the width and height. The syntax + * rectMode(CORNERS) uses the first and second parameters of + * rect() to set the location of one corner and uses the third and + * fourth parameters to set the opposite corner. The syntax + * rectMode(CENTER) draws the image from its center point and uses + * the third and forth parameters of rect() to specify the image's + * width and height. The syntax rectMode(RADIUS) draws the image + * from its center point and uses the third and forth parameters of + * rect() to specify half of the image's width and height. The + * parameter must be written in ALL CAPS because Processing is a case + * sensitive language. Note: In version 125, the mode named CENTER_RADIUS + * was shortened to RADIUS. + * + * ( end auto-generated ) + * @webref shape:attributes + * @param mode either CORNER, CORNERS, CENTER, or RADIUS + * @see PGraphics#rect(float, float, float, float) + */ + public void rectMode(int mode) { + if (recorder != null) recorder.rectMode(mode); + g.rectMode(mode); + } + + + /** + * ( begin auto-generated from rect.xml ) + * + * Draws a rectangle to the screen. A rectangle is a four-sided shape with + * every angle at ninety degrees. By default, the first two parameters set + * the location of the upper-left corner, the third sets the width, and the + * fourth sets the height. These parameters may be changed with the + * rectMode() function. + * + * ( end auto-generated ) + * + * @webref shape:2d_primitives + * @param a x-coordinate of the rectangle by default + * @param b y-coordinate of the rectangle by default + * @param c width of the rectangle by default + * @param d height of the rectangle by default + * @see PGraphics#rectMode(int) + * @see PGraphics#quad(float, float, float, float, float, float, float, float) + */ + public void rect(float a, float b, float c, float d) { + if (recorder != null) recorder.rect(a, b, c, d); + g.rect(a, b, c, d); + } + + + /** + * @param r radii for all four corners + */ + public void rect(float a, float b, float c, float d, float r) { + if (recorder != null) recorder.rect(a, b, c, d, r); + g.rect(a, b, c, d, r); + } + + + /** + * @param tl radius for top-left corner + * @param tr radius for top-right corner + * @param br radius for bottom-right corner + * @param bl radius for bottom-left corner + */ + public void rect(float a, float b, float c, float d, + float tl, float tr, float br, float bl) { + if (recorder != null) recorder.rect(a, b, c, d, tl, tr, br, bl); + g.rect(a, b, c, d, tl, tr, br, bl); + } + + + /** + * ( begin auto-generated from ellipseMode.xml ) + * + * The origin of the ellipse is modified by the ellipseMode() + * function. The default configuration is ellipseMode(CENTER), which + * specifies the location of the ellipse as the center of the shape. The + * RADIUS mode is the same, but the width and height parameters to + * ellipse() specify the radius of the ellipse, rather than the + * diameter. The CORNER mode draws the shape from the upper-left + * corner of its bounding box. The CORNERS mode uses the four + * parameters to ellipse() to set two opposing corners of the + * ellipse's bounding box. The parameter must be written in ALL CAPS + * because Processing is a case-sensitive language. + * + * ( end auto-generated ) + * @webref shape:attributes + * @param mode either CENTER, RADIUS, CORNER, or CORNERS + * @see PApplet#ellipse(float, float, float, float) + * @see PApplet#arc(float, float, float, float, float, float) + */ + public void ellipseMode(int mode) { + if (recorder != null) recorder.ellipseMode(mode); + g.ellipseMode(mode); + } + + + /** + * ( begin auto-generated from ellipse.xml ) + * + * Draws an ellipse (oval) in the display window. An ellipse with an equal + * width and height is a circle. The first two parameters set + * the location, the third sets the width, and the fourth sets the height. + * The origin may be changed with the ellipseMode() function. + * + * ( end auto-generated ) + * @webref shape:2d_primitives + * @param a x-coordinate of the ellipse + * @param b y-coordinate of the ellipse + * @param c width of the ellipse by default + * @param d height of the ellipse by default + * @see PApplet#ellipseMode(int) + * @see PApplet#arc(float, float, float, float, float, float) + */ + public void ellipse(float a, float b, float c, float d) { + if (recorder != null) recorder.ellipse(a, b, c, d); + g.ellipse(a, b, c, d); + } + + + /** + * ( begin auto-generated from arc.xml ) + * + * Draws an arc in the display window. Arcs are drawn along the outer edge + * of an ellipse defined by the x, y, width and + * height parameters. The origin or the arc's ellipse may be changed + * with the ellipseMode() function. The start and stop + * parameters specify the angles at which to draw the arc. + * + * ( end auto-generated ) + * @webref shape:2d_primitives + * @param a x-coordinate of the arc's ellipse + * @param b y-coordinate of the arc's ellipse + * @param c width of the arc's ellipse by default + * @param d height of the arc's ellipse by default + * @param start angle to start the arc, specified in radians + * @param stop angle to stop the arc, specified in radians + * @see PApplet#ellipse(float, float, float, float) + * @see PApplet#ellipseMode(int) + * @see PApplet#radians(float) + * @see PApplet#degrees(float) + */ + public void arc(float a, float b, float c, float d, + float start, float stop) { + if (recorder != null) recorder.arc(a, b, c, d, start, stop); + g.arc(a, b, c, d, start, stop); + } + + + /* + * @param mode either OPEN, CHORD, or PIE + */ + public void arc(float a, float b, float c, float d, + float start, float stop, int mode) { + if (recorder != null) recorder.arc(a, b, c, d, start, stop, mode); + g.arc(a, b, c, d, start, stop, mode); + } + + + /** + * ( begin auto-generated from box.xml ) + * + * A box is an extruded rectangle. A box with equal dimension on all sides + * is a cube. + * + * ( end auto-generated ) + * + * @webref shape:3d_primitives + * @param size dimension of the box in all dimensions (creates a cube) + * @see PGraphics#sphere(float) + */ + public void box(float size) { + if (recorder != null) recorder.box(size); + g.box(size); + } + + + /** + * @param w dimension of the box in the x-dimension + * @param h dimension of the box in the y-dimension + * @param d dimension of the box in the z-dimension + */ + public void box(float w, float h, float d) { + if (recorder != null) recorder.box(w, h, d); + g.box(w, h, d); + } + + + /** + * ( begin auto-generated from sphereDetail.xml ) + * + * Controls the detail used to render a sphere by adjusting the number of + * vertices of the sphere mesh. The default resolution is 30, which creates + * a fairly detailed sphere definition with vertices every 360/30 = 12 + * degrees. If you're going to render a great number of spheres per frame, + * it is advised to reduce the level of detail using this function. The + * setting stays active until sphereDetail() is called again with a + * new parameter and so should not be called prior to every + * sphere() statement, unless you wish to render spheres with + * different settings, e.g. using less detail for smaller spheres or ones + * further away from the camera. To control the detail of the horizontal + * and vertical resolution independently, use the version of the functions + * with two parameters. + * + * ( end auto-generated ) + * + *

        Advanced

        + * Code for sphereDetail() submitted by toxi [031031]. + * Code for enhanced u/v version from davbol [080801]. + * + * @param res number of segments (minimum 3) used per full circle revolution + * @webref shape:3d_primitives + * @see PGraphics#sphere(float) + */ + public void sphereDetail(int res) { + if (recorder != null) recorder.sphereDetail(res); + g.sphereDetail(res); + } + + + /** + * @param ures number of segments used longitudinally per full circle revolutoin + * @param vres number of segments used latitudinally from top to bottom + */ + public void sphereDetail(int ures, int vres) { + if (recorder != null) recorder.sphereDetail(ures, vres); + g.sphereDetail(ures, vres); + } + + + /** + * ( begin auto-generated from sphere.xml ) + * + * A sphere is a hollow ball made from tessellated triangles. + * + * ( end auto-generated ) + * + *

        Advanced

        + *

        + * Implementation notes: + *

        + * cache all the points of the sphere in a static array + * top and bottom are just a bunch of triangles that land + * in the center point + *

        + * sphere is a series of concentric circles who radii vary + * along the shape, based on, er.. cos or something + *

        +   * [toxi 031031] new sphere code. removed all multiplies with
        +   * radius, as scale() will take care of that anyway
        +   *
        +   * [toxi 031223] updated sphere code (removed modulos)
        +   * and introduced sphereAt(x,y,z,r)
        +   * to avoid additional translate()'s on the user/sketch side
        +   *
        +   * [davbol 080801] now using separate sphereDetailU/V
        +   * 
        + * + * @webref shape:3d_primitives + * @param r the radius of the sphere + * @see PGraphics#sphereDetail(int) + */ + public void sphere(float r) { + if (recorder != null) recorder.sphere(r); + g.sphere(r); + } + + + /** + * ( begin auto-generated from bezierPoint.xml ) + * + * Evaluates the Bezier at point t for points a, b, c, d. The parameter t + * varies between 0 and 1, a and d are points on the curve, and b and c are + * the control points. This can be done once with the x coordinates and a + * second time with the y coordinates to get the location of a bezier curve + * at t. + * + * ( end auto-generated ) + * + *

        Advanced

        + * For instance, to convert the following example:
        +   * stroke(255, 102, 0);
        +   * line(85, 20, 10, 10);
        +   * line(90, 90, 15, 80);
        +   * stroke(0, 0, 0);
        +   * bezier(85, 20, 10, 10, 90, 90, 15, 80);
        +   *
        +   * // draw it in gray, using 10 steps instead of the default 20
        +   * // this is a slower way to do it, but useful if you need
        +   * // to do things with the coordinates at each step
        +   * stroke(128);
        +   * beginShape(LINE_STRIP);
        +   * for (int i = 0; i <= 10; i++) {
        +   *   float t = i / 10.0f;
        +   *   float x = bezierPoint(85, 10, 90, 15, t);
        +   *   float y = bezierPoint(20, 10, 90, 80, t);
        +   *   vertex(x, y);
        +   * }
        +   * endShape();
        + * + * @webref shape:curves + * @param a coordinate of first point on the curve + * @param b coordinate of first control point + * @param c coordinate of second control point + * @param d coordinate of second point on the curve + * @param t value between 0 and 1 + * @see PGraphics#bezier(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#bezierVertex(float, float, float, float, float, float) + * @see PGraphics#curvePoint(float, float, float, float, float) + */ + public float bezierPoint(float a, float b, float c, float d, float t) { + return g.bezierPoint(a, b, c, d, t); + } + + + /** + * ( begin auto-generated from bezierTangent.xml ) + * + * Calculates the tangent of a point on a Bezier curve. There is a good + * definition of tangent on Wikipedia. + * + * ( end auto-generated ) + * + *

        Advanced

        + * Code submitted by Dave Bollinger (davol) for release 0136. + * + * @webref shape:curves + * @param a coordinate of first point on the curve + * @param b coordinate of first control point + * @param c coordinate of second control point + * @param d coordinate of second point on the curve + * @param t value between 0 and 1 + * @see PGraphics#bezier(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#bezierVertex(float, float, float, float, float, float) + * @see PGraphics#curvePoint(float, float, float, float, float) + */ + public float bezierTangent(float a, float b, float c, float d, float t) { + return g.bezierTangent(a, b, c, d, t); + } + + + /** + * ( begin auto-generated from bezierDetail.xml ) + * + * Sets the resolution at which Beziers display. The default value is 20. + * This function is only useful when using the P3D renderer as the default + * P2D renderer does not use this information. + * + * ( end auto-generated ) + * + * @webref shape:curves + * @param detail resolution of the curves + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#curveVertex(float, float, float) + * @see PGraphics#curveTightness(float) + */ + public void bezierDetail(int detail) { + if (recorder != null) recorder.bezierDetail(detail); + g.bezierDetail(detail); + } + + + public void bezier(float x1, float y1, + float x2, float y2, + float x3, float y3, + float x4, float y4) { + if (recorder != null) recorder.bezier(x1, y1, x2, y2, x3, y3, x4, y4); + g.bezier(x1, y1, x2, y2, x3, y3, x4, y4); + } + + + /** + * ( begin auto-generated from bezier.xml ) + * + * Draws a Bezier curve on the screen. These curves are defined by a series + * of anchor and control points. The first two parameters specify the first + * anchor point and the last two parameters specify the other anchor point. + * The middle parameters specify the control points which define the shape + * of the curve. Bezier curves were developed by French engineer Pierre + * Bezier. Using the 3D version requires rendering with P3D (see the + * Environment reference for more information). + * + * ( end auto-generated ) + * + *

        Advanced

        + * Draw a cubic bezier curve. The first and last points are + * the on-curve points. The middle two are the 'control' points, + * or 'handles' in an application like Illustrator. + *

        + * Identical to typing: + *

        beginShape();
        +   * vertex(x1, y1);
        +   * bezierVertex(x2, y2, x3, y3, x4, y4);
        +   * endShape();
        +   * 
        + * In Postscript-speak, this would be: + *
        moveto(x1, y1);
        +   * curveto(x2, y2, x3, y3, x4, y4);
        + * If you were to try and continue that curve like so: + *
        curveto(x5, y5, x6, y6, x7, y7);
        + * This would be done in processing by adding these statements: + *
        bezierVertex(x5, y5, x6, y6, x7, y7)
        +   * 
        + * To draw a quadratic (instead of cubic) curve, + * use the control point twice by doubling it: + *
        bezier(x1, y1, cx, cy, cx, cy, x2, y2);
        + * + * @webref shape:curves + * @param x1 coordinates for the first anchor point + * @param y1 coordinates for the first anchor point + * @param z1 coordinates for the first anchor point + * @param x2 coordinates for the first control point + * @param y2 coordinates for the first control point + * @param z2 coordinates for the first control point + * @param x3 coordinates for the second control point + * @param y3 coordinates for the second control point + * @param z3 coordinates for the second control point + * @param x4 coordinates for the second anchor point + * @param y4 coordinates for the second anchor point + * @param z4 coordinates for the second anchor point + * + * @see PGraphics#bezierVertex(float, float, float, float, float, float) + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + */ + public void bezier(float x1, float y1, float z1, + float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + if (recorder != null) recorder.bezier(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4); + g.bezier(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4); + } + + + /** + * ( begin auto-generated from curvePoint.xml ) + * + * Evalutes the curve at point t for points a, b, c, d. The parameter t + * varies between 0 and 1, a and d are points on the curve, and b and c are + * the control points. This can be done once with the x coordinates and a + * second time with the y coordinates to get the location of a curve at t. + * + * ( end auto-generated ) + * + * @webref shape:curves + * @param a coordinate of first point on the curve + * @param b coordinate of second point on the curve + * @param c coordinate of third point on the curve + * @param d coordinate of fourth point on the curve + * @param t value between 0 and 1 + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#curveVertex(float, float) + * @see PGraphics#bezierPoint(float, float, float, float, float) + */ + public float curvePoint(float a, float b, float c, float d, float t) { + return g.curvePoint(a, b, c, d, t); + } + + + /** + * ( begin auto-generated from curveTangent.xml ) + * + * Calculates the tangent of a point on a curve. There's a good definition + * of tangent on Wikipedia. + * + * ( end auto-generated ) + * + *

        Advanced

        + * Code thanks to Dave Bollinger (Bug #715) + * + * @webref shape:curves + * @param a coordinate of first point on the curve + * @param b coordinate of first control point + * @param c coordinate of second control point + * @param d coordinate of second point on the curve + * @param t value between 0 and 1 + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#curveVertex(float, float) + * @see PGraphics#curvePoint(float, float, float, float, float) + * @see PGraphics#bezierTangent(float, float, float, float, float) + */ + public float curveTangent(float a, float b, float c, float d, float t) { + return g.curveTangent(a, b, c, d, t); + } + + + /** + * ( begin auto-generated from curveDetail.xml ) + * + * Sets the resolution at which curves display. The default value is 20. + * This function is only useful when using the P3D renderer as the default + * P2D renderer does not use this information. + * + * ( end auto-generated ) + * + * @webref shape:curves + * @param detail resolution of the curves + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#curveVertex(float, float) + * @see PGraphics#curveTightness(float) + */ + public void curveDetail(int detail) { + if (recorder != null) recorder.curveDetail(detail); + g.curveDetail(detail); + } + + + /** + * ( begin auto-generated from curveTightness.xml ) + * + * Modifies the quality of forms created with curve() and + * curveVertex(). The parameter squishy determines how the + * curve fits to the vertex points. The value 0.0 is the default value for + * squishy (this value defines the curves to be Catmull-Rom splines) + * and the value 1.0 connects all the points with straight lines. Values + * within the range -5.0 and 5.0 will deform the curves but will leave them + * recognizable and as values increase in magnitude, they will continue to deform. + * + * ( end auto-generated ) + * + * @webref shape:curves + * @param tightness amount of deformation from the original vertices + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#curveVertex(float, float) + */ + public void curveTightness(float tightness) { + if (recorder != null) recorder.curveTightness(tightness); + g.curveTightness(tightness); + } + + + /** + * ( begin auto-generated from curve.xml ) + * + * Draws a curved line on the screen. The first and second parameters + * specify the beginning control point and the last two parameters specify + * the ending control point. The middle parameters specify the start and + * stop of the curve. Longer curves can be created by putting a series of + * curve() functions together or using curveVertex(). An + * additional function called curveTightness() provides control for + * the visual quality of the curve. The curve() function is an + * implementation of Catmull-Rom splines. Using the 3D version requires + * rendering with P3D (see the Environment reference for more information). + * + * ( end auto-generated ) + * + *

        Advanced

        + * As of revision 0070, this function no longer doubles the first + * and last points. The curves are a bit more boring, but it's more + * mathematically correct, and properly mirrored in curvePoint(). + *

        + * Identical to typing out:

        +   * beginShape();
        +   * curveVertex(x1, y1);
        +   * curveVertex(x2, y2);
        +   * curveVertex(x3, y3);
        +   * curveVertex(x4, y4);
        +   * endShape();
        +   * 
        + * + * @webref shape:curves + * @param x1 coordinates for the beginning control point + * @param y1 coordinates for the beginning control point + * @param x2 coordinates for the first point + * @param y2 coordinates for the first point + * @param x3 coordinates for the second point + * @param y3 coordinates for the second point + * @param x4 coordinates for the ending control point + * @param y4 coordinates for the ending control point + * @see PGraphics#curveVertex(float, float) + * @see PGraphics#curveTightness(float) + * @see PGraphics#bezier(float, float, float, float, float, float, float, float, float, float, float, float) + */ + public void curve(float x1, float y1, + float x2, float y2, + float x3, float y3, + float x4, float y4) { + if (recorder != null) recorder.curve(x1, y1, x2, y2, x3, y3, x4, y4); + g.curve(x1, y1, x2, y2, x3, y3, x4, y4); + } + + + /** + * @param z1 coordinates for the beginning control point + * @param z2 coordinates for the first point + * @param z3 coordinates for the second point + * @param z4 coordinates for the ending control point + */ + public void curve(float x1, float y1, float z1, + float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + if (recorder != null) recorder.curve(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4); + g.curve(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4); + } + + + /** + * ( begin auto-generated from imageMode.xml ) + * + * Modifies the location from which images draw. The default mode is + * imageMode(CORNER), which specifies the location to be the upper + * left corner and uses the fourth and fifth parameters of image() + * to set the image's width and height. The syntax + * imageMode(CORNERS) uses the second and third parameters of + * image() to set the location of one corner of the image and uses + * the fourth and fifth parameters to set the opposite corner. Use + * imageMode(CENTER) to draw images centered at the given x and y + * position.
        + *
        + * The parameter to imageMode() must be written in ALL CAPS because + * Processing is a case-sensitive language. + * + * ( end auto-generated ) + * + * @webref image:loading_displaying + * @param mode either CORNER, CORNERS, or CENTER + * @see PApplet#loadImage(String, String) + * @see PImage + * @see PGraphics#image(PImage, float, float, float, float) + * @see PGraphics#background(float, float, float, float) + */ + public void imageMode(int mode) { + if (recorder != null) recorder.imageMode(mode); + g.imageMode(mode); + } + + + /** + * ( begin auto-generated from image.xml ) + * + * Displays images to the screen. The images must be in the sketch's "data" + * directory to load correctly. Select "Add file..." from the "Sketch" menu + * to add the image. Processing currently works with GIF, JPEG, and Targa + * images. The img parameter specifies the image to display and the + * x and y parameters define the location of the image from + * its upper-left corner. The image is displayed at its original size + * unless the width and height parameters specify a different + * size.
        + *
        + * The imageMode() function changes the way the parameters work. For + * example, a call to imageMode(CORNERS) will change the + * width and height parameters to define the x and y values + * of the opposite corner of the image.
        + *
        + * The color of an image may be modified with the tint() function. + * This function will maintain transparency for GIF and PNG images. + * + * ( end auto-generated ) + * + *

        Advanced

        + * Starting with release 0124, when using the default (JAVA2D) renderer, + * smooth() will also improve image quality of resized images. + * + * @webref image:loading_displaying + * @param img the image to display + * @param a x-coordinate of the image by default + * @param b y-coordinate of the image by default + * @see PApplet#loadImage(String, String) + * @see PImage + * @see PGraphics#imageMode(int) + * @see PGraphics#tint(float) + * @see PGraphics#background(float, float, float, float) + * @see PGraphics#alpha(int) + */ + public void image(PImage img, float a, float b) { + if (recorder != null) recorder.image(img, a, b); + g.image(img, a, b); + } + + + /** + * @param c width to display the image by default + * @param d height to display the image by default + */ + public void image(PImage img, float a, float b, float c, float d) { + if (recorder != null) recorder.image(img, a, b, c, d); + g.image(img, a, b, c, d); + } + + + /** + * Draw an image(), also specifying u/v coordinates. + * In this method, the u, v coordinates are always based on image space + * location, regardless of the current textureMode(). + * + * @nowebref + */ + public void image(PImage img, + float a, float b, float c, float d, + int u1, int v1, int u2, int v2) { + if (recorder != null) recorder.image(img, a, b, c, d, u1, v1, u2, v2); + g.image(img, a, b, c, d, u1, v1, u2, v2); + } + + + /** + * ( begin auto-generated from shapeMode.xml ) + * + * Modifies the location from which shapes draw. The default mode is + * shapeMode(CORNER), which specifies the location to be the upper + * left corner of the shape and uses the third and fourth parameters of + * shape() to specify the width and height. The syntax + * shapeMode(CORNERS) uses the first and second parameters of + * shape() to set the location of one corner and uses the third and + * fourth parameters to set the opposite corner. The syntax + * shapeMode(CENTER) draws the shape from its center point and uses + * the third and forth parameters of shape() to specify the width + * and height. The parameter must be written in "ALL CAPS" because + * Processing is a case sensitive language. + * + * ( end auto-generated ) + * + * @webref shape:loading_displaying + * @param mode either CORNER, CORNERS, CENTER + * @see PShape + * @see PGraphics#shape(PShape) + * @see PGraphics#rectMode(int) + */ + public void shapeMode(int mode) { + if (recorder != null) recorder.shapeMode(mode); + g.shapeMode(mode); + } + + + public void shape(PShape shape) { + if (recorder != null) recorder.shape(shape); + g.shape(shape); + } + + + /** + * ( begin auto-generated from shape.xml ) + * + * Displays shapes to the screen. The shapes must be in the sketch's "data" + * directory to load correctly. Select "Add file..." from the "Sketch" menu + * to add the shape. Processing currently works with SVG shapes only. The + * sh parameter specifies the shape to display and the x and + * y parameters define the location of the shape from its upper-left + * corner. The shape is displayed at its original size unless the + * width and height parameters specify a different size. The + * shapeMode() function changes the way the parameters work. A call + * to shapeMode(CORNERS), for example, will change the width and + * height parameters to define the x and y values of the opposite corner of + * the shape. + *

        + * Note complex shapes may draw awkwardly with P3D. This renderer does not + * yet support shapes that have holes or complicated breaks. + * + * ( end auto-generated ) + * + * @webref shape:loading_displaying + * @param shape the shape to display + * @param x x-coordinate of the shape + * @param y y-coordinate of the shape + * @see PShape + * @see PApplet#loadShape(String) + * @see PGraphics#shapeMode(int) + * + * Convenience method to draw at a particular location. + */ + public void shape(PShape shape, float x, float y) { + if (recorder != null) recorder.shape(shape, x, y); + g.shape(shape, x, y); + } + + + /** + * @param a x-coordinate of the shape + * @param b y-coordinate of the shape + * @param c width to display the shape + * @param d height to display the shape + */ + public void shape(PShape shape, float a, float b, float c, float d) { + if (recorder != null) recorder.shape(shape, a, b, c, d); + g.shape(shape, a, b, c, d); + } + + + public void textAlign(int alignX) { + if (recorder != null) recorder.textAlign(alignX); + g.textAlign(alignX); + } + + + /** + * ( begin auto-generated from textAlign.xml ) + * + * Sets the current alignment for drawing text. The parameters LEFT, + * CENTER, and RIGHT set the display characteristics of the letters in + * relation to the values for the x and y parameters of the + * text() function. + *

        + * In Processing 0125 and later, an optional second parameter can be used + * to vertically align the text. BASELINE is the default, and the vertical + * alignment will be reset to BASELINE if the second parameter is not used. + * The TOP and CENTER parameters are straightforward. The BOTTOM parameter + * offsets the line based on the current textDescent(). For multiple + * lines, the final line will be aligned to the bottom, with the previous + * lines appearing above it. + *

        + * When using text() with width and height parameters, BASELINE is + * ignored, and treated as TOP. (Otherwise, text would by default draw + * outside the box, since BASELINE is the default setting. BASELINE is not + * a useful drawing mode for text drawn in a rectangle.) + *

        + * The vertical alignment is based on the value of textAscent(), + * which many fonts do not specify correctly. It may be necessary to use a + * hack and offset by a few pixels by hand so that the offset looks + * correct. To do this as less of a hack, use some percentage of + * textAscent() or textDescent() so that the hack works even + * if you change the size of the font. + * + * ( end auto-generated ) + * + * @webref typography:attributes + * @param alignX horizontal alignment, either LEFT, CENTER, or RIGHT + * @param alignY vertical alignment, either TOP, BOTTOM, CENTER, or BASELINE + * @see PApplet#loadFont(String) + * @see PFont + * @see PGraphics#text(String, float, float) + * @see PGraphics#textSize(float) + * @see PGraphics#textAscent() + * @see PGraphics#textDescent() + */ + public void textAlign(int alignX, int alignY) { + if (recorder != null) recorder.textAlign(alignX, alignY); + g.textAlign(alignX, alignY); + } + + + /** + * ( begin auto-generated from textAscent.xml ) + * + * Returns ascent of the current font at its current size. This information + * is useful for determining the height of the font above the baseline. For + * example, adding the textAscent() and textDescent() values + * will give you the total height of the line. + * + * ( end auto-generated ) + * + * @webref typography:metrics + * @see PGraphics#textDescent() + */ + public float textAscent() { + return g.textAscent(); + } + + + /** + * ( begin auto-generated from textDescent.xml ) + * + * Returns descent of the current font at its current size. This + * information is useful for determining the height of the font below the + * baseline. For example, adding the textAscent() and + * textDescent() values will give you the total height of the line. + * + * ( end auto-generated ) + * + * @webref typography:metrics + * @see PGraphics#textAscent() + */ + public float textDescent() { + return g.textDescent(); + } + + + /** + * ( begin auto-generated from textFont.xml ) + * + * Sets the current font that will be drawn with the text() + * function. Fonts must be loaded with loadFont() before it can be + * used. This font will be used in all subsequent calls to the + * text() function. If no size parameter is input, the font + * will appear at its original size (the size it was created at with the + * "Create Font..." tool) until it is changed with textSize().

        Because fonts are usually bitmaped, you should create fonts at + * the sizes that will be used most commonly. Using textFont() + * without the size parameter will result in the cleanest-looking text.

        With the default (JAVA2D) and PDF renderers, it's also possible + * to enable the use of native fonts via the command + * hint(ENABLE_NATIVE_FONTS). This will produce vector text in + * JAVA2D sketches and PDF output in cases where the vector data is + * available: when the font is still installed, or the font is created via + * the createFont() function (rather than the Create Font tool). + * + * ( end auto-generated ) + * + * @webref typography:loading_displaying + * @param which any variable of the type PFont + * @see PApplet#createFont(String, float, boolean) + * @see PApplet#loadFont(String) + * @see PFont + * @see PGraphics#text(String, float, float) + * @see PGraphics#textSize(float) + */ + public void textFont(PFont which) { + if (recorder != null) recorder.textFont(which); + g.textFont(which); + } + + + /** + * @param size the size of the letters in units of pixels + */ + public void textFont(PFont which, float size) { + if (recorder != null) recorder.textFont(which, size); + g.textFont(which, size); + } + + + /** + * ( begin auto-generated from textLeading.xml ) + * + * Sets the spacing between lines of text in units of pixels. This setting + * will be used in all subsequent calls to the text() function. + * + * ( end auto-generated ) + * + * @webref typography:attributes + * @param leading the size in pixels for spacing between lines + * @see PApplet#loadFont(String) + * @see PFont#PFont + * @see PGraphics#text(String, float, float) + * @see PGraphics#textFont(PFont) + * @see PGraphics#textSize(float) + */ + public void textLeading(float leading) { + if (recorder != null) recorder.textLeading(leading); + g.textLeading(leading); + } + + + /** + * ( begin auto-generated from textMode.xml ) + * + * Sets the way text draws to the screen. In the default configuration, the + * MODEL mode, it's possible to rotate, scale, and place letters in + * two and three dimensional space.
        + *
        + * The SHAPE mode draws text using the the glyph outlines of + * individual characters rather than as textures. This mode is only + * supported with the PDF and P3D renderer settings. With the + * PDF renderer, you must call textMode(SHAPE) before any + * other drawing occurs. If the outlines are not available, then + * textMode(SHAPE) will be ignored and textMode(MODEL) will + * be used instead.
        + *
        + * The textMode(SHAPE) option in P3D can be combined with + * beginRaw() to write vector-accurate text to 2D and 3D output + * files, for instance DXF or PDF. The SHAPE mode is + * not currently optimized for P3D, so if recording shape data, use + * textMode(MODEL) until you're ready to capture the geometry with beginRaw(). + * + * ( end auto-generated ) + * + * @webref typography:attributes + * @param mode either MODEL or SHAPE + * @see PApplet#loadFont(String) + * @see PFont#PFont + * @see PGraphics#text(String, float, float) + * @see PGraphics#textFont(PFont) + * @see PGraphics#beginRaw(PGraphics) + * @see PApplet#createFont(String, float, boolean) + */ + public void textMode(int mode) { + if (recorder != null) recorder.textMode(mode); + g.textMode(mode); + } + + + /** + * ( begin auto-generated from textSize.xml ) + * + * Sets the current font size. This size will be used in all subsequent + * calls to the text() function. Font size is measured in units of pixels. + * + * ( end auto-generated ) + * + * @webref typography:attributes + * @param size the size of the letters in units of pixels + * @see PApplet#loadFont(String) + * @see PFont#PFont + * @see PGraphics#text(String, float, float) + * @see PGraphics#textFont(PFont) + */ + public void textSize(float size) { + if (recorder != null) recorder.textSize(size); + g.textSize(size); + } + + + /** + * @param c the character to measure + */ + public float textWidth(char c) { + return g.textWidth(c); + } + + + /** + * ( begin auto-generated from textWidth.xml ) + * + * Calculates and returns the width of any character or text string. + * + * ( end auto-generated ) + * + * @webref typography:attributes + * @param str the String of characters to measure + * @see PApplet#loadFont(String) + * @see PFont#PFont + * @see PGraphics#text(String, float, float) + * @see PGraphics#textFont(PFont) + * @see PGraphics#textSize(float) + */ + public float textWidth(String str) { + return g.textWidth(str); + } + + + /** + * @nowebref + */ + public float textWidth(char[] chars, int start, int length) { + return g.textWidth(chars, start, length); + } + + + /** + * ( begin auto-generated from text.xml ) + * + * Draws text to the screen. Displays the information specified in the + * data or stringdata parameters on the screen in the + * position specified by the x and y parameters and the + * optional z parameter. A default font will be used unless a font + * is set with the textFont() function. Change the color of the text + * with the fill() function. The text displays in relation to the + * textAlign() function, which gives the option to draw to the left, + * right, and center of the coordinates. + *

        + * The x2 and y2 parameters define a rectangular area to + * display within and may only be used with string data. For text drawn + * inside a rectangle, the coordinates are interpreted based on the current + * rectMode() setting. + * + * ( end auto-generated ) + * + * @webref typography:loading_displaying + * @param c the alphanumeric character to be displayed + * @param x x-coordinate of text + * @param y y-coordinate of text + * @see PGraphics#textAlign(int, int) + * @see PGraphics#textFont(PFont) + * @see PGraphics#textMode(int) + * @see PGraphics#textSize(float) + * @see PGraphics#textLeading(float) + * @see PGraphics#textWidth(String) + * @see PGraphics#textAscent() + * @see PGraphics#textDescent() + * @see PGraphics#rectMode(int) + * @see PGraphics#fill(int, float) + * @see_external String + */ + public void text(char c, float x, float y) { + if (recorder != null) recorder.text(c, x, y); + g.text(c, x, y); + } + + + /** + * @param z z-coordinate of text + */ + public void text(char c, float x, float y, float z) { + if (recorder != null) recorder.text(c, x, y, z); + g.text(c, x, y, z); + } + + + /** + *

        Advanced

        + * Draw a chunk of text. + * Newlines that are \n (Unix newline or linefeed char, ascii 10) + * are honored, but \r (carriage return, Windows and Mac OS) are + * ignored. + */ + public void text(String str, float x, float y) { + if (recorder != null) recorder.text(str, x, y); + g.text(str, x, y); + } + + + /** + *

        Advanced

        + * Method to draw text from an array of chars. This method will usually be + * more efficient than drawing from a String object, because the String will + * not be converted to a char array before drawing. + * @param chars the alphanumberic symbols to be displayed + * @param start array index at which to start writing characters + * @param stop array index at which to stop writing characters + */ + public void text(char[] chars, int start, int stop, float x, float y) { + if (recorder != null) recorder.text(chars, start, stop, x, y); + g.text(chars, start, stop, x, y); + } + + + /** + * Same as above but with a z coordinate. + */ + public void text(String str, float x, float y, float z) { + if (recorder != null) recorder.text(str, x, y, z); + g.text(str, x, y, z); + } + + + public void text(char[] chars, int start, int stop, + float x, float y, float z) { + if (recorder != null) recorder.text(chars, start, stop, x, y, z); + g.text(chars, start, stop, x, y, z); + } + + + /** + *

        Advanced

        + * Draw text in a box that is constrained to a particular size. + * The current rectMode() determines what the coordinates mean + * (whether x1/y1/x2/y2 or x/y/w/h). + *

        + * Note that the x,y coords of the start of the box + * will align with the *ascent* of the text, not the baseline, + * as is the case for the other text() functions. + *

        + * Newlines that are \n (Unix newline or linefeed char, ascii 10) + * are honored, and \r (carriage return, Windows and Mac OS) are + * ignored. + * + * @param x1 by default, the x-coordinate of text, see rectMode() for more info + * @param y1 by default, the y-coordinate of text, see rectMode() for more info + * @param x2 by default, the width of the text box, see rectMode() for more info + * @param y2 by default, the height of the text box, see rectMode() for more info + */ + public void text(String str, float x1, float y1, float x2, float y2) { + if (recorder != null) recorder.text(str, x1, y1, x2, y2); + g.text(str, x1, y1, x2, y2); + } + + + public void text(int num, float x, float y) { + if (recorder != null) recorder.text(num, x, y); + g.text(num, x, y); + } + + + public void text(int num, float x, float y, float z) { + if (recorder != null) recorder.text(num, x, y, z); + g.text(num, x, y, z); + } + + + /** + * This does a basic number formatting, to avoid the + * generally ugly appearance of printing floats. + * Users who want more control should use their own nf() cmmand, + * or if they want the long, ugly version of float, + * use String.valueOf() to convert the float to a String first. + * + * @param num the numeric value to be displayed + */ + public void text(float num, float x, float y) { + if (recorder != null) recorder.text(num, x, y); + g.text(num, x, y); + } + + + public void text(float num, float x, float y, float z) { + if (recorder != null) recorder.text(num, x, y, z); + g.text(num, x, y, z); + } + + + /** + * ( begin auto-generated from pushMatrix.xml ) + * + * Pushes the current transformation matrix onto the matrix stack. + * Understanding pushMatrix() and popMatrix() requires + * understanding the concept of a matrix stack. The pushMatrix() + * function saves the current coordinate system to the stack and + * popMatrix() restores the prior coordinate system. + * pushMatrix() and popMatrix() are used in conjuction with + * the other transformation functions and may be embedded to control the + * scope of the transformations. + * + * ( end auto-generated ) + * + * @webref transform + * @see PGraphics#popMatrix() + * @see PGraphics#translate(float, float, float) + * @see PGraphics#scale(float) + * @see PGraphics#rotate(float) + * @see PGraphics#rotateX(float) + * @see PGraphics#rotateY(float) + * @see PGraphics#rotateZ(float) + */ + public void pushMatrix() { + if (recorder != null) recorder.pushMatrix(); + g.pushMatrix(); + } + + + /** + * ( begin auto-generated from popMatrix.xml ) + * + * Pops the current transformation matrix off the matrix stack. + * Understanding pushing and popping requires understanding the concept of + * a matrix stack. The pushMatrix() function saves the current + * coordinate system to the stack and popMatrix() restores the prior + * coordinate system. pushMatrix() and popMatrix() are used + * in conjuction with the other transformation functions and may be + * embedded to control the scope of the transformations. + * + * ( end auto-generated ) + * + * @webref transform + * @see PGraphics#pushMatrix() + */ + public void popMatrix() { + if (recorder != null) recorder.popMatrix(); + g.popMatrix(); + } + + + /** + * ( begin auto-generated from translate.xml ) + * + * Specifies an amount to displace objects within the display window. The + * x parameter specifies left/right translation, the y + * parameter specifies up/down translation, and the z parameter + * specifies translations toward/away from the screen. Using this function + * with the z parameter requires using P3D as a parameter in + * combination with size as shown in the above example. Transformations + * apply to everything that happens after and subsequent calls to the + * function accumulates the effect. For example, calling translate(50, + * 0) and then translate(20, 0) is the same as translate(70, + * 0). If translate() is called within draw(), the + * transformation is reset when the loop begins again. This function can be + * further controlled by the pushMatrix() and popMatrix(). + * + * ( end auto-generated ) + * + * @webref transform + * @param x left/right translation + * @param y up/down translation + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#rotate(float) + * @see PGraphics#rotateX(float) + * @see PGraphics#rotateY(float) + * @see PGraphics#rotateZ(float) + * @see PGraphics#scale(float, float, float) + */ + public void translate(float x, float y) { + if (recorder != null) recorder.translate(x, y); + g.translate(x, y); + } + + + /** + * @param z forward/backward translation + */ + public void translate(float x, float y, float z) { + if (recorder != null) recorder.translate(x, y, z); + g.translate(x, y, z); + } + + + /** + * ( begin auto-generated from rotate.xml ) + * + * Rotates a shape the amount specified by the angle parameter. + * Angles should be specified in radians (values from 0 to TWO_PI) or + * converted to radians with the radians() function. + *

        + * Objects are always rotated around their relative position to the origin + * and positive numbers rotate objects in a clockwise direction. + * Transformations apply to everything that happens after and subsequent + * calls to the function accumulates the effect. For example, calling + * rotate(HALF_PI) and then rotate(HALF_PI) is the same as + * rotate(PI). All tranformations are reset when draw() + * begins again. + *

        + * Technically, rotate() multiplies the current transformation + * matrix by a rotation matrix. This function can be further controlled by + * the pushMatrix() and popMatrix(). + * + * ( end auto-generated ) + * + * @webref transform + * @param angle angle of rotation specified in radians + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#rotateX(float) + * @see PGraphics#rotateY(float) + * @see PGraphics#rotateZ(float) + * @see PGraphics#scale(float, float, float) + * @see PApplet#radians(float) + */ + public void rotate(float angle) { + if (recorder != null) recorder.rotate(angle); + g.rotate(angle); + } + + + /** + * ( begin auto-generated from rotateX.xml ) + * + * Rotates a shape around the x-axis the amount specified by the + * angle parameter. Angles should be specified in radians (values + * from 0 to PI*2) or converted to radians with the radians() + * function. Objects are always rotated around their relative position to + * the origin and positive numbers rotate objects in a counterclockwise + * direction. Transformations apply to everything that happens after and + * subsequent calls to the function accumulates the effect. For example, + * calling rotateX(PI/2) and then rotateX(PI/2) is the same + * as rotateX(PI). If rotateX() is called within the + * draw(), the transformation is reset when the loop begins again. + * This function requires using P3D as a third parameter to size() + * as shown in the example above. + * + * ( end auto-generated ) + * + * @webref transform + * @param angle angle of rotation specified in radians + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#rotate(float) + * @see PGraphics#rotateY(float) + * @see PGraphics#rotateZ(float) + * @see PGraphics#scale(float, float, float) + * @see PGraphics#translate(float, float, float) + */ + public void rotateX(float angle) { + if (recorder != null) recorder.rotateX(angle); + g.rotateX(angle); + } + + + /** + * ( begin auto-generated from rotateY.xml ) + * + * Rotates a shape around the y-axis the amount specified by the + * angle parameter. Angles should be specified in radians (values + * from 0 to PI*2) or converted to radians with the radians() + * function. Objects are always rotated around their relative position to + * the origin and positive numbers rotate objects in a counterclockwise + * direction. Transformations apply to everything that happens after and + * subsequent calls to the function accumulates the effect. For example, + * calling rotateY(PI/2) and then rotateY(PI/2) is the same + * as rotateY(PI). If rotateY() is called within the + * draw(), the transformation is reset when the loop begins again. + * This function requires using P3D as a third parameter to size() + * as shown in the examples above. + * + * ( end auto-generated ) + * + * @webref transform + * @param angle angle of rotation specified in radians + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#rotate(float) + * @see PGraphics#rotateX(float) + * @see PGraphics#rotateZ(float) + * @see PGraphics#scale(float, float, float) + * @see PGraphics#translate(float, float, float) + */ + public void rotateY(float angle) { + if (recorder != null) recorder.rotateY(angle); + g.rotateY(angle); + } + + + /** + * ( begin auto-generated from rotateZ.xml ) + * + * Rotates a shape around the z-axis the amount specified by the + * angle parameter. Angles should be specified in radians (values + * from 0 to PI*2) or converted to radians with the radians() + * function. Objects are always rotated around their relative position to + * the origin and positive numbers rotate objects in a counterclockwise + * direction. Transformations apply to everything that happens after and + * subsequent calls to the function accumulates the effect. For example, + * calling rotateZ(PI/2) and then rotateZ(PI/2) is the same + * as rotateZ(PI). If rotateZ() is called within the + * draw(), the transformation is reset when the loop begins again. + * This function requires using P3D as a third parameter to size() + * as shown in the examples above. + * + * ( end auto-generated ) + * + * @webref transform + * @param angle angle of rotation specified in radians + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#rotate(float) + * @see PGraphics#rotateX(float) + * @see PGraphics#rotateY(float) + * @see PGraphics#scale(float, float, float) + * @see PGraphics#translate(float, float, float) + */ + public void rotateZ(float angle) { + if (recorder != null) recorder.rotateZ(angle); + g.rotateZ(angle); + } + + + /** + *

        Advanced

        + * Rotate about a vector in space. Same as the glRotatef() function. + * @nowebref + * @param x + * @param y + * @param z + */ + public void rotate(float angle, float x, float y, float z) { + if (recorder != null) recorder.rotate(angle, x, y, z); + g.rotate(angle, x, y, z); + } + + + /** + * ( begin auto-generated from scale.xml ) + * + * Increases or decreases the size of a shape by expanding and contracting + * vertices. Objects always scale from their relative origin to the + * coordinate system. Scale values are specified as decimal percentages. + * For example, the function call scale(2.0) increases the dimension + * of a shape by 200%. Transformations apply to everything that happens + * after and subsequent calls to the function multiply the effect. For + * example, calling scale(2.0) and then scale(1.5) is the + * same as scale(3.0). If scale() is called within + * draw(), the transformation is reset when the loop begins again. + * Using this fuction with the z parameter requires using P3D as a + * parameter for size() as shown in the example above. This function + * can be further controlled by pushMatrix() and popMatrix(). + * + * ( end auto-generated ) + * + * @webref transform + * @param s percentage to scale the object + * @see PGraphics#pushMatrix() + * @see PGraphics#popMatrix() + * @see PGraphics#translate(float, float, float) + * @see PGraphics#rotate(float) + * @see PGraphics#rotateX(float) + * @see PGraphics#rotateY(float) + * @see PGraphics#rotateZ(float) + */ + public void scale(float s) { + if (recorder != null) recorder.scale(s); + g.scale(s); + } + + + /** + *

        Advanced

        + * Scale in X and Y. Equivalent to scale(sx, sy, 1). + * + * Not recommended for use in 3D, because the z-dimension is just + * scaled by 1, since there's no way to know what else to scale it by. + * + * @param x percentage to scale the object in the x-axis + * @param y percentage to scale the object in the y-axis + */ + public void scale(float x, float y) { + if (recorder != null) recorder.scale(x, y); + g.scale(x, y); + } + + + /** + * @param z percentage to scale the object in the z-axis + */ + public void scale(float x, float y, float z) { + if (recorder != null) recorder.scale(x, y, z); + g.scale(x, y, z); + } + + + /** + * ( begin auto-generated from shearX.xml ) + * + * Shears a shape around the x-axis the amount specified by the + * angle parameter. Angles should be specified in radians (values + * from 0 to PI*2) or converted to radians with the radians() + * function. Objects are always sheared around their relative position to + * the origin and positive numbers shear objects in a clockwise direction. + * Transformations apply to everything that happens after and subsequent + * calls to the function accumulates the effect. For example, calling + * shearX(PI/2) and then shearX(PI/2) is the same as + * shearX(PI). If shearX() is called within the + * draw(), the transformation is reset when the loop begins again. + *

        + * Technically, shearX() multiplies the current transformation + * matrix by a rotation matrix. This function can be further controlled by + * the pushMatrix() and popMatrix() functions. + * + * ( end auto-generated ) + * + * @webref transform + * @param angle angle of shear specified in radians + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#shearY(float) + * @see PGraphics#scale(float, float, float) + * @see PGraphics#translate(float, float, float) + * @see PApplet#radians(float) + */ + public void shearX(float angle) { + if (recorder != null) recorder.shearX(angle); + g.shearX(angle); + } + + + /** + * ( begin auto-generated from shearY.xml ) + * + * Shears a shape around the y-axis the amount specified by the + * angle parameter. Angles should be specified in radians (values + * from 0 to PI*2) or converted to radians with the radians() + * function. Objects are always sheared around their relative position to + * the origin and positive numbers shear objects in a clockwise direction. + * Transformations apply to everything that happens after and subsequent + * calls to the function accumulates the effect. For example, calling + * shearY(PI/2) and then shearY(PI/2) is the same as + * shearY(PI). If shearY() is called within the + * draw(), the transformation is reset when the loop begins again. + *

        + * Technically, shearY() multiplies the current transformation + * matrix by a rotation matrix. This function can be further controlled by + * the pushMatrix() and popMatrix() functions. + * + * ( end auto-generated ) + * + * @webref transform + * @param angle angle of shear specified in radians + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#shearX(float) + * @see PGraphics#scale(float, float, float) + * @see PGraphics#translate(float, float, float) + * @see PApplet#radians(float) + */ + public void shearY(float angle) { + if (recorder != null) recorder.shearY(angle); + g.shearY(angle); + } + + + /** + * ( begin auto-generated from resetMatrix.xml ) + * + * Replaces the current matrix with the identity matrix. The equivalent + * function in OpenGL is glLoadIdentity(). + * + * ( end auto-generated ) + * + * @webref transform + * @see PGraphics#pushMatrix() + * @see PGraphics#popMatrix() + * @see PGraphics#applyMatrix(PMatrix) + * @see PGraphics#printMatrix() + */ + public void resetMatrix() { + if (recorder != null) recorder.resetMatrix(); + g.resetMatrix(); + } + + + /** + * ( begin auto-generated from applyMatrix.xml ) + * + * Multiplies the current matrix by the one specified through the + * parameters. This is very slow because it will try to calculate the + * inverse of the transform, so avoid it whenever possible. The equivalent + * function in OpenGL is glMultMatrix(). + * + * ( end auto-generated ) + * + * @webref transform + * @source + * @see PGraphics#pushMatrix() + * @see PGraphics#popMatrix() + * @see PGraphics#resetMatrix() + * @see PGraphics#printMatrix() + */ + public void applyMatrix(PMatrix source) { + if (recorder != null) recorder.applyMatrix(source); + g.applyMatrix(source); + } + + + public void applyMatrix(PMatrix2D source) { + if (recorder != null) recorder.applyMatrix(source); + g.applyMatrix(source); + } + + + /** + * @param n00 numbers which define the 4x4 matrix to be multiplied + * @param n01 numbers which define the 4x4 matrix to be multiplied + * @param n02 numbers which define the 4x4 matrix to be multiplied + * @param n10 numbers which define the 4x4 matrix to be multiplied + * @param n11 numbers which define the 4x4 matrix to be multiplied + * @param n12 numbers which define the 4x4 matrix to be multiplied + */ + public void applyMatrix(float n00, float n01, float n02, + float n10, float n11, float n12) { + if (recorder != null) recorder.applyMatrix(n00, n01, n02, n10, n11, n12); + g.applyMatrix(n00, n01, n02, n10, n11, n12); + } + + + public void applyMatrix(PMatrix3D source) { + if (recorder != null) recorder.applyMatrix(source); + g.applyMatrix(source); + } + + + /** + * @param n03 numbers which define the 4x4 matrix to be multiplied + * @param n13 numbers which define the 4x4 matrix to be multiplied + * @param n20 numbers which define the 4x4 matrix to be multiplied + * @param n21 numbers which define the 4x4 matrix to be multiplied + * @param n22 numbers which define the 4x4 matrix to be multiplied + * @param n23 numbers which define the 4x4 matrix to be multiplied + * @param n30 numbers which define the 4x4 matrix to be multiplied + * @param n31 numbers which define the 4x4 matrix to be multiplied + * @param n32 numbers which define the 4x4 matrix to be multiplied + * @param n33 numbers which define the 4x4 matrix to be multiplied + */ + public void applyMatrix(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + if (recorder != null) recorder.applyMatrix(n00, n01, n02, n03, n10, n11, n12, n13, n20, n21, n22, n23, n30, n31, n32, n33); + g.applyMatrix(n00, n01, n02, n03, n10, n11, n12, n13, n20, n21, n22, n23, n30, n31, n32, n33); + } + + + public PMatrix getMatrix() { + return g.getMatrix(); + } + + + /** + * Copy the current transformation matrix into the specified target. + * Pass in null to create a new matrix. + */ + public PMatrix2D getMatrix(PMatrix2D target) { + return g.getMatrix(target); + } + + + /** + * Copy the current transformation matrix into the specified target. + * Pass in null to create a new matrix. + */ + public PMatrix3D getMatrix(PMatrix3D target) { + return g.getMatrix(target); + } + + + /** + * Set the current transformation matrix to the contents of another. + */ + public void setMatrix(PMatrix source) { + if (recorder != null) recorder.setMatrix(source); + g.setMatrix(source); + } + + + /** + * Set the current transformation to the contents of the specified source. + */ + public void setMatrix(PMatrix2D source) { + if (recorder != null) recorder.setMatrix(source); + g.setMatrix(source); + } + + + /** + * Set the current transformation to the contents of the specified source. + */ + public void setMatrix(PMatrix3D source) { + if (recorder != null) recorder.setMatrix(source); + g.setMatrix(source); + } + + + /** + * ( begin auto-generated from printMatrix.xml ) + * + * Prints the current matrix to the Console (the text window at the bottom + * of Processing). + * + * ( end auto-generated ) + * + * @webref transform + * @see PGraphics#pushMatrix() + * @see PGraphics#popMatrix() + * @see PGraphics#resetMatrix() + * @see PGraphics#applyMatrix(PMatrix) + */ + public void printMatrix() { + if (recorder != null) recorder.printMatrix(); + g.printMatrix(); + } + + + /** + * ( begin auto-generated from beginCamera.xml ) + * + * The beginCamera() and endCamera() functions enable + * advanced customization of the camera space. The functions are useful if + * you want to more control over camera movement, however for most users, + * the camera() function will be sufficient.

        The camera + * functions will replace any transformations (such as rotate() or + * translate()) that occur before them in draw(), but they + * will not automatically replace the camera transform itself. For this + * reason, camera functions should be placed at the beginning of + * draw() (so that transformations happen afterwards), and the + * camera() function can be used after beginCamera() if you + * want to reset the camera before applying transformations.

        This function sets the matrix mode to the camera matrix so calls such + * as translate(), rotate(), applyMatrix() and resetMatrix() + * affect the camera. beginCamera() should always be used with a + * following endCamera() and pairs of beginCamera() and + * endCamera() cannot be nested. + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + * @see PGraphics#camera() + * @see PGraphics#endCamera() + * @see PGraphics#applyMatrix(PMatrix) + * @see PGraphics#resetMatrix() + * @see PGraphics#translate(float, float, float) + * @see PGraphics#scale(float, float, float) + */ + public void beginCamera() { + if (recorder != null) recorder.beginCamera(); + g.beginCamera(); + } + + + /** + * ( begin auto-generated from endCamera.xml ) + * + * The beginCamera() and endCamera() functions enable + * advanced customization of the camera space. Please see the reference for + * beginCamera() for a description of how the functions are used. + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + * @see PGraphics#beginCamera() + * @see PGraphics#camera(float, float, float, float, float, float, float, float, float) + */ + public void endCamera() { + if (recorder != null) recorder.endCamera(); + g.endCamera(); + } + + + /** + * ( begin auto-generated from camera.xml ) + * + * Sets the position of the camera through setting the eye position, the + * center of the scene, and which axis is facing upward. Moving the eye + * position and the direction it is pointing (the center of the scene) + * allows the images to be seen from different angles. The version without + * any parameters sets the camera to the default position, pointing to the + * center of the display window with the Y axis as up. The default values + * are camera(width/2.0, height/2.0, (height/2.0) / tan(PI*30.0 / + * 180.0), width/2.0, height/2.0, 0, 0, 1, 0). This function is similar + * to gluLookAt() in OpenGL, but it first clears the current camera settings. + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + * @see PGraphics#beginCamera() + * @see PGraphics#endCamera() + * @see PGraphics#frustum(float, float, float, float, float, float) + */ + public void camera() { + if (recorder != null) recorder.camera(); + g.camera(); + } + + +/** + * @param eyeX x-coordinate for the eye + * @param eyeY y-coordinate for the eye + * @param eyeZ z-coordinate for the eye + * @param centerX x-coordinate for the center of the scene + * @param centerY y-coordinate for the center of the scene + * @param centerZ z-coordinate for the center of the scene + * @param upX usually 0.0, 1.0, or -1.0 + * @param upY usually 0.0, 1.0, or -1.0 + * @param upZ usually 0.0, 1.0, or -1.0 + */ + public void camera(float eyeX, float eyeY, float eyeZ, + float centerX, float centerY, float centerZ, + float upX, float upY, float upZ) { + if (recorder != null) recorder.camera(eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ); + g.camera(eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ); + } + + +/** + * ( begin auto-generated from printCamera.xml ) + * + * Prints the current camera matrix to the Console (the text window at the + * bottom of Processing). + * + * ( end auto-generated ) + * @webref lights_camera:camera + * @see PGraphics#camera(float, float, float, float, float, float, float, float, float) + */ + public void printCamera() { + if (recorder != null) recorder.printCamera(); + g.printCamera(); + } + + + /** + * ( begin auto-generated from ortho.xml ) + * + * Sets an orthographic projection and defines a parallel clipping volume. + * All objects with the same dimension appear the same size, regardless of + * whether they are near or far from the camera. The parameters to this + * function specify the clipping volume where left and right are the + * minimum and maximum x values, top and bottom are the minimum and maximum + * y values, and near and far are the minimum and maximum z values. If no + * parameters are given, the default is used: ortho(0, width, 0, height, + * -10, 10). + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + */ + public void ortho() { + if (recorder != null) recorder.ortho(); + g.ortho(); + } + + + /** + * @param left left plane of the clipping volume + * @param right right plane of the clipping volume + * @param bottom bottom plane of the clipping volume + * @param top top plane of the clipping volume + */ + public void ortho(float left, float right, + float bottom, float top) { + if (recorder != null) recorder.ortho(left, right, bottom, top); + g.ortho(left, right, bottom, top); + } + + + /** + * @param near maximum distance from the origin to the viewer + * @param far maximum distance from the origin away from the viewer + */ + public void ortho(float left, float right, + float bottom, float top, + float near, float far) { + if (recorder != null) recorder.ortho(left, right, bottom, top, near, far); + g.ortho(left, right, bottom, top, near, far); + } + + + /** + * ( begin auto-generated from perspective.xml ) + * + * Sets a perspective projection applying foreshortening, making distant + * objects appear smaller than closer ones. The parameters define a viewing + * volume with the shape of truncated pyramid. Objects near to the front of + * the volume appear their actual size, while farther objects appear + * smaller. This projection simulates the perspective of the world more + * accurately than orthographic projection. The version of perspective + * without parameters sets the default perspective and the version with + * four parameters allows the programmer to set the area precisely. The + * default values are: perspective(PI/3.0, width/height, cameraZ/10.0, + * cameraZ*10.0) where cameraZ is ((height/2.0) / tan(PI*60.0/360.0)); + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + */ + public void perspective() { + if (recorder != null) recorder.perspective(); + g.perspective(); + } + + + /** + * @param fovy field-of-view angle (in radians) for vertical direction + * @param aspect ratio of width to height + * @param zNear z-position of nearest clipping plane + * @param zFar z-position of farthest clipping plane + */ + public void perspective(float fovy, float aspect, float zNear, float zFar) { + if (recorder != null) recorder.perspective(fovy, aspect, zNear, zFar); + g.perspective(fovy, aspect, zNear, zFar); + } + + + /** + * ( begin auto-generated from frustum.xml ) + * + * Sets a perspective matrix defined through the parameters. Works like + * glFrustum, except it wipes out the current perspective matrix rather + * than muliplying itself with it. + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + * @param left left coordinate of the clipping plane + * @param right right coordinate of the clipping plane + * @param bottom bottom coordinate of the clipping plane + * @param top top coordinate of the clipping plane + * @param near near component of the clipping plane; must be greater than zero + * @param far far component of the clipping plane; must be greater than the near value + * @see PGraphics#camera(float, float, float, float, float, float, float, float, float) + * @see PGraphics#beginCamera() + * @see PGraphics#endCamera() + * @see PGraphics#perspective(float, float, float, float) + */ + public void frustum(float left, float right, + float bottom, float top, + float near, float far) { + if (recorder != null) recorder.frustum(left, right, bottom, top, near, far); + g.frustum(left, right, bottom, top, near, far); + } + + + /** + * ( begin auto-generated from printProjection.xml ) + * + * Prints the current projection matrix to the Console (the text window at + * the bottom of Processing). + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + * @see PGraphics#camera(float, float, float, float, float, float, float, float, float) + */ + public void printProjection() { + if (recorder != null) recorder.printProjection(); + g.printProjection(); + } + + + /** + * ( begin auto-generated from screenX.xml ) + * + * Takes a three-dimensional X, Y, Z position and returns the X value for + * where it will appear on a (two-dimensional) screen. + * + * ( end auto-generated ) + * + * @webref lights_camera:coordinates + * @param x 3D x-coordinate to be mapped + * @param y 3D y-coordinate to be mapped + * @see PGraphics#screenY(float, float, float) + * @see PGraphics#screenZ(float, float, float) + */ + public float screenX(float x, float y) { + return g.screenX(x, y); + } + + + /** + * ( begin auto-generated from screenY.xml ) + * + * Takes a three-dimensional X, Y, Z position and returns the Y value for + * where it will appear on a (two-dimensional) screen. + * + * ( end auto-generated ) + * + * @webref lights_camera:coordinates + * @param x 3D x-coordinate to be mapped + * @param y 3D y-coordinate to be mapped + * @see PGraphics#screenX(float, float, float) + * @see PGraphics#screenZ(float, float, float) + */ + public float screenY(float x, float y) { + return g.screenY(x, y); + } + + + /** + * @param z 3D z-coordinate to be mapped + */ + public float screenX(float x, float y, float z) { + return g.screenX(x, y, z); + } + + + /** + * @param z 3D z-coordinate to be mapped + */ + public float screenY(float x, float y, float z) { + return g.screenY(x, y, z); + } + + + /** + * ( begin auto-generated from screenZ.xml ) + * + * Takes a three-dimensional X, Y, Z position and returns the Z value for + * where it will appear on a (two-dimensional) screen. + * + * ( end auto-generated ) + * + * @webref lights_camera:coordinates + * @param x 3D x-coordinate to be mapped + * @param y 3D y-coordinate to be mapped + * @param z 3D z-coordinate to be mapped + * @see PGraphics#screenX(float, float, float) + * @see PGraphics#screenY(float, float, float) + */ + public float screenZ(float x, float y, float z) { + return g.screenZ(x, y, z); + } + + + /** + * ( begin auto-generated from modelX.xml ) + * + * Returns the three-dimensional X, Y, Z position in model space. This + * returns the X value for a given coordinate based on the current set of + * transformations (scale, rotate, translate, etc.) The X value can be used + * to place an object in space relative to the location of the original + * point once the transformations are no longer in use. + *

        + * In the example, the modelX(), modelY(), and + * modelZ() functions record the location of a box in space after + * being placed using a series of translate and rotate commands. After + * popMatrix() is called, those transformations no longer apply, but the + * (x, y, z) coordinate returned by the model functions is used to place + * another box in the same location. + * + * ( end auto-generated ) + * + * @webref lights_camera:coordinates + * @param x 3D x-coordinate to be mapped + * @param y 3D y-coordinate to be mapped + * @param z 3D z-coordinate to be mapped + * @see PGraphics#modelY(float, float, float) + * @see PGraphics#modelZ(float, float, float) + */ + public float modelX(float x, float y, float z) { + return g.modelX(x, y, z); + } + + + /** + * ( begin auto-generated from modelY.xml ) + * + * Returns the three-dimensional X, Y, Z position in model space. This + * returns the Y value for a given coordinate based on the current set of + * transformations (scale, rotate, translate, etc.) The Y value can be used + * to place an object in space relative to the location of the original + * point once the transformations are no longer in use.
        + *
        + * In the example, the modelX(), modelY(), and + * modelZ() functions record the location of a box in space after + * being placed using a series of translate and rotate commands. After + * popMatrix() is called, those transformations no longer apply, but the + * (x, y, z) coordinate returned by the model functions is used to place + * another box in the same location. + * + * ( end auto-generated ) + * + * @webref lights_camera:coordinates + * @param x 3D x-coordinate to be mapped + * @param y 3D y-coordinate to be mapped + * @param z 3D z-coordinate to be mapped + * @see PGraphics#modelX(float, float, float) + * @see PGraphics#modelZ(float, float, float) + */ + public float modelY(float x, float y, float z) { + return g.modelY(x, y, z); + } + + + /** + * ( begin auto-generated from modelZ.xml ) + * + * Returns the three-dimensional X, Y, Z position in model space. This + * returns the Z value for a given coordinate based on the current set of + * transformations (scale, rotate, translate, etc.) The Z value can be used + * to place an object in space relative to the location of the original + * point once the transformations are no longer in use.
        + *
        + * In the example, the modelX(), modelY(), and + * modelZ() functions record the location of a box in space after + * being placed using a series of translate and rotate commands. After + * popMatrix() is called, those transformations no longer apply, but the + * (x, y, z) coordinate returned by the model functions is used to place + * another box in the same location. + * + * ( end auto-generated ) + * + * @webref lights_camera:coordinates + * @param x 3D x-coordinate to be mapped + * @param y 3D y-coordinate to be mapped + * @param z 3D z-coordinate to be mapped + * @see PGraphics#modelX(float, float, float) + * @see PGraphics#modelY(float, float, float) + */ + public float modelZ(float x, float y, float z) { + return g.modelZ(x, y, z); + } + + + /** + * ( begin auto-generated from pushStyle.xml ) + * + * The pushStyle() function saves the current style settings and + * popStyle() restores the prior settings. Note that these functions + * are always used together. They allow you to change the style settings + * and later return to what you had. When a new style is started with + * pushStyle(), it builds on the current style information. The + * pushStyle() and popStyle() functions can be embedded to + * provide more control (see the second example above for a demonstration.) + *

        + * The style information controlled by the following functions are included + * in the style: + * fill(), stroke(), tint(), strokeWeight(), strokeCap(), strokeJoin(), + * imageMode(), rectMode(), ellipseMode(), shapeMode(), colorMode(), + * textAlign(), textFont(), textMode(), textSize(), textLeading(), + * emissive(), specular(), shininess(), ambient() + * + * ( end auto-generated ) + * + * @webref structure + * @see PGraphics#popStyle() + */ + public void pushStyle() { + if (recorder != null) recorder.pushStyle(); + g.pushStyle(); + } + + + /** + * ( begin auto-generated from popStyle.xml ) + * + * The pushStyle() function saves the current style settings and + * popStyle() restores the prior settings; these functions are + * always used together. They allow you to change the style settings and + * later return to what you had. When a new style is started with + * pushStyle(), it builds on the current style information. The + * pushStyle() and popStyle() functions can be embedded to + * provide more control (see the second example above for a demonstration.) + * + * ( end auto-generated ) + * + * @webref structure + * @see PGraphics#pushStyle() + */ + public void popStyle() { + if (recorder != null) recorder.popStyle(); + g.popStyle(); + } + + + public void style(PStyle s) { + if (recorder != null) recorder.style(s); + g.style(s); + } + + + /** + * ( begin auto-generated from strokeWeight.xml ) + * + * Sets the width of the stroke used for lines, points, and the border + * around shapes. All widths are set in units of pixels. + *

        + * When drawing with P3D, series of connected lines (such as the stroke + * around a polygon, triangle, or ellipse) produce unattractive results + * when a thick stroke weight is set (see + * Issue 123). With P3D, the minimum and maximum values for + * strokeWeight() are controlled by the graphics card and the + * operating system's OpenGL implementation. For instance, the thickness + * may not go higher than 10 pixels. + * + * ( end auto-generated ) + * + * @webref shape:attributes + * @param weight the weight (in pixels) of the stroke + * @see PGraphics#stroke(int, float) + * @see PGraphics#strokeJoin(int) + * @see PGraphics#strokeCap(int) + */ + public void strokeWeight(float weight) { + if (recorder != null) recorder.strokeWeight(weight); + g.strokeWeight(weight); + } + + + /** + * ( begin auto-generated from strokeJoin.xml ) + * + * Sets the style of the joints which connect line segments. These joints + * are either mitered, beveled, or rounded and specified with the + * corresponding parameters MITER, BEVEL, and ROUND. The default joint is + * MITER. + *

        + * This function is not available with the P3D renderer, (see + * Issue 123). More information about the renderers can be found in the + * size() reference. + * + * ( end auto-generated ) + * + * @webref shape:attributes + * @param join either MITER, BEVEL, ROUND + * @see PGraphics#stroke(int, float) + * @see PGraphics#strokeWeight(float) + * @see PGraphics#strokeCap(int) + */ + public void strokeJoin(int join) { + if (recorder != null) recorder.strokeJoin(join); + g.strokeJoin(join); + } + + + /** + * ( begin auto-generated from strokeCap.xml ) + * + * Sets the style for rendering line endings. These ends are either + * squared, extended, or rounded and specified with the corresponding + * parameters SQUARE, PROJECT, and ROUND. The default cap is ROUND. + *

        + * This function is not available with the P3D renderer (see + * Issue 123). More information about the renderers can be found in the + * size() reference. + * + * ( end auto-generated ) + * + * @webref shape:attributes + * @param cap either SQUARE, PROJECT, or ROUND + * @see PGraphics#stroke(int, float) + * @see PGraphics#strokeWeight(float) + * @see PGraphics#strokeJoin(int) + * @see PApplet#size(int, int, String, String) + */ + public void strokeCap(int cap) { + if (recorder != null) recorder.strokeCap(cap); + g.strokeCap(cap); + } + + + /** + * ( begin auto-generated from noStroke.xml ) + * + * Disables drawing the stroke (outline). If both noStroke() and + * noFill() are called, nothing will be drawn to the screen. + * + * ( end auto-generated ) + * + * @webref color:setting + * @see PGraphics#stroke(int, float) + * @see PGraphics#fill(float, float, float, float) + * @see PGraphics#noFill() + */ + public void noStroke() { + if (recorder != null) recorder.noStroke(); + g.noStroke(); + } + + + /** + * ( begin auto-generated from stroke.xml ) + * + * Sets the color used to draw lines and borders around shapes. This color + * is either specified in terms of the RGB or HSB color depending on the + * current colorMode() (the default color space is RGB, with each + * value in the range from 0 to 255). + *

        + * When using hexadecimal notation to specify a color, use "#" or "0x" + * before the values (e.g. #CCFFAA, 0xFFCCFFAA). The # syntax uses six + * digits to specify a color (the way colors are specified in HTML and + * CSS). When using the hexadecimal notation starting with "0x", the + * hexadecimal value must be specified with eight characters; the first two + * characters define the alpha component and the remainder the red, green, + * and blue components. + *

        + * The value for the parameter "gray" must be less than or equal to the + * current maximum value as specified by colorMode(). The default + * maximum value is 255. + * + * ( end auto-generated ) + * + * @param rgb color value in hexadecimal notation + * @see PGraphics#noStroke() + * @see PGraphics#strokeWeight(float) + * @see PGraphics#strokeJoin(int) + * @see PGraphics#strokeCap(int) + * @see PGraphics#fill(int, float) + * @see PGraphics#noFill() + * @see PGraphics#tint(int, float) + * @see PGraphics#background(float, float, float, float) + * @see PGraphics#colorMode(int, float, float, float, float) + */ + public void stroke(int rgb) { + if (recorder != null) recorder.stroke(rgb); + g.stroke(rgb); + } + + + /** + * @param alpha opacity of the stroke + */ + public void stroke(int rgb, float alpha) { + if (recorder != null) recorder.stroke(rgb, alpha); + g.stroke(rgb, alpha); + } + + + /** + * @param gray specifies a value between white and black + */ + public void stroke(float gray) { + if (recorder != null) recorder.stroke(gray); + g.stroke(gray); + } + + + public void stroke(float gray, float alpha) { + if (recorder != null) recorder.stroke(gray, alpha); + g.stroke(gray, alpha); + } + + + /** + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + * @webref color:setting + */ + public void stroke(float v1, float v2, float v3) { + if (recorder != null) recorder.stroke(v1, v2, v3); + g.stroke(v1, v2, v3); + } + + + public void stroke(float v1, float v2, float v3, float alpha) { + if (recorder != null) recorder.stroke(v1, v2, v3, alpha); + g.stroke(v1, v2, v3, alpha); + } + + + /** + * ( begin auto-generated from noTint.xml ) + * + * Removes the current fill value for displaying images and reverts to + * displaying images with their original hues. + * + * ( end auto-generated ) + * + * @webref image:loading_displaying + * @usage web_application + * @see PGraphics#tint(float, float, float, float) + * @see PGraphics#image(PImage, float, float, float, float) + */ + public void noTint() { + if (recorder != null) recorder.noTint(); + g.noTint(); + } + + + /** + * ( begin auto-generated from tint.xml ) + * + * Sets the fill value for displaying images. Images can be tinted to + * specified colors or made transparent by setting the alpha.
        + *
        + * To make an image transparent, but not change it's color, use white as + * the tint color and specify an alpha value. For instance, tint(255, 128) + * will make an image 50% transparent (unless colorMode() has been + * used).
        + *
        + * When using hexadecimal notation to specify a color, use "#" or "0x" + * before the values (e.g. #CCFFAA, 0xFFCCFFAA). The # syntax uses six + * digits to specify a color (the way colors are specified in HTML and + * CSS). When using the hexadecimal notation starting with "0x", the + * hexadecimal value must be specified with eight characters; the first two + * characters define the alpha component and the remainder the red, green, + * and blue components.
        + *
        + * The value for the parameter "gray" must be less than or equal to the + * current maximum value as specified by colorMode(). The default + * maximum value is 255.
        + *
        + * The tint() function is also used to control the coloring of + * textures in 3D. + * + * ( end auto-generated ) + * + * @webref image:loading_displaying + * @usage web_application + * @param rgb color value in hexadecimal notation + * @see PGraphics#noTint() + * @see PGraphics#image(PImage, float, float, float, float) + */ + public void tint(int rgb) { + if (recorder != null) recorder.tint(rgb); + g.tint(rgb); + } + + + /** + * @param alpha opacity of the image + */ + public void tint(int rgb, float alpha) { + if (recorder != null) recorder.tint(rgb, alpha); + g.tint(rgb, alpha); + } + + + /** + * @param gray specifies a value between white and black + */ + public void tint(float gray) { + if (recorder != null) recorder.tint(gray); + g.tint(gray); + } + + + public void tint(float gray, float alpha) { + if (recorder != null) recorder.tint(gray, alpha); + g.tint(gray, alpha); + } + + +/** + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + */ + public void tint(float v1, float v2, float v3) { + if (recorder != null) recorder.tint(v1, v2, v3); + g.tint(v1, v2, v3); + } + + + public void tint(float v1, float v2, float v3, float alpha) { + if (recorder != null) recorder.tint(v1, v2, v3, alpha); + g.tint(v1, v2, v3, alpha); + } + + + /** + * ( begin auto-generated from noFill.xml ) + * + * Disables filling geometry. If both noStroke() and noFill() + * are called, nothing will be drawn to the screen. + * + * ( end auto-generated ) + * + * @webref color:setting + * @usage web_application + * @see PGraphics#fill(float, float, float, float) + * @see PGraphics#stroke(int, float) + * @see PGraphics#noStroke() + */ + public void noFill() { + if (recorder != null) recorder.noFill(); + g.noFill(); + } + + + /** + * ( begin auto-generated from fill.xml ) + * + * Sets the color used to fill shapes. For example, if you run fill(204, + * 102, 0), all subsequent shapes will be filled with orange. This + * color is either specified in terms of the RGB or HSB color depending on + * the current colorMode() (the default color space is RGB, with + * each value in the range from 0 to 255). + *

        + * When using hexadecimal notation to specify a color, use "#" or "0x" + * before the values (e.g. #CCFFAA, 0xFFCCFFAA). The # syntax uses six + * digits to specify a color (the way colors are specified in HTML and + * CSS). When using the hexadecimal notation starting with "0x", the + * hexadecimal value must be specified with eight characters; the first two + * characters define the alpha component and the remainder the red, green, + * and blue components. + *

        + * The value for the parameter "gray" must be less than or equal to the + * current maximum value as specified by colorMode(). The default + * maximum value is 255. + *

        + * To change the color of an image (or a texture), use tint(). + * + * ( end auto-generated ) + * + * @webref color:setting + * @usage web_application + * @param rgb color variable or hex value + * @see PGraphics#noFill() + * @see PGraphics#stroke(int, float) + * @see PGraphics#noStroke() + * @see PGraphics#tint(int, float) + * @see PGraphics#background(float, float, float, float) + * @see PGraphics#colorMode(int, float, float, float, float) + */ + public void fill(int rgb) { + if (recorder != null) recorder.fill(rgb); + g.fill(rgb); + } + + + /** + * @param alpha opacity of the fill + */ + public void fill(int rgb, float alpha) { + if (recorder != null) recorder.fill(rgb, alpha); + g.fill(rgb, alpha); + } + + + /** + * @param gray number specifying value between white and black + */ + public void fill(float gray) { + if (recorder != null) recorder.fill(gray); + g.fill(gray); + } + + + public void fill(float gray, float alpha) { + if (recorder != null) recorder.fill(gray, alpha); + g.fill(gray, alpha); + } + + + /** + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + */ + public void fill(float v1, float v2, float v3) { + if (recorder != null) recorder.fill(v1, v2, v3); + g.fill(v1, v2, v3); + } + + + public void fill(float v1, float v2, float v3, float alpha) { + if (recorder != null) recorder.fill(v1, v2, v3, alpha); + g.fill(v1, v2, v3, alpha); + } + + + /** + * ( begin auto-generated from ambient.xml ) + * + * Sets the ambient reflectance for shapes drawn to the screen. This is + * combined with the ambient light component of environment. The color + * components set through the parameters define the reflectance. For + * example in the default color mode, setting v1=255, v2=126, v3=0, would + * cause all the red light to reflect and half of the green light to + * reflect. Used in combination with emissive(), specular(), + * and shininess() in setting the material properties of shapes. + * + * ( end auto-generated ) + * + * @webref lights_camera:material_properties + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#emissive(float, float, float) + * @see PGraphics#specular(float, float, float) + * @see PGraphics#shininess(float) + */ + public void ambient(int rgb) { + if (recorder != null) recorder.ambient(rgb); + g.ambient(rgb); + } + + +/** + * @param gray number specifying value between white and black + */ + public void ambient(float gray) { + if (recorder != null) recorder.ambient(gray); + g.ambient(gray); + } + + +/** + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + */ + public void ambient(float v1, float v2, float v3) { + if (recorder != null) recorder.ambient(v1, v2, v3); + g.ambient(v1, v2, v3); + } + + + /** + * ( begin auto-generated from specular.xml ) + * + * Sets the specular color of the materials used for shapes drawn to the + * screen, which sets the color of hightlights. Specular refers to light + * which bounces off a surface in a perferred direction (rather than + * bouncing in all directions like a diffuse light). Used in combination + * with emissive(), ambient(), and shininess() in + * setting the material properties of shapes. + * + * ( end auto-generated ) + * + * @webref lights_camera:material_properties + * @usage web_application + * @param rgb color to set + * @see PGraphics#lightSpecular(float, float, float) + * @see PGraphics#ambient(float, float, float) + * @see PGraphics#emissive(float, float, float) + * @see PGraphics#shininess(float) + */ + public void specular(int rgb) { + if (recorder != null) recorder.specular(rgb); + g.specular(rgb); + } + + +/** + * gray number specifying value between white and black + */ + public void specular(float gray) { + if (recorder != null) recorder.specular(gray); + g.specular(gray); + } + + +/** + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + */ + public void specular(float v1, float v2, float v3) { + if (recorder != null) recorder.specular(v1, v2, v3); + g.specular(v1, v2, v3); + } + + + /** + * ( begin auto-generated from shininess.xml ) + * + * Sets the amount of gloss in the surface of shapes. Used in combination + * with ambient(), specular(), and emissive() in + * setting the material properties of shapes. + * + * ( end auto-generated ) + * + * @webref lights_camera:material_properties + * @usage web_application + * @param shine degree of shininess + * @see PGraphics#emissive(float, float, float) + * @see PGraphics#ambient(float, float, float) + * @see PGraphics#specular(float, float, float) + */ + public void shininess(float shine) { + if (recorder != null) recorder.shininess(shine); + g.shininess(shine); + } + + + /** + * ( begin auto-generated from emissive.xml ) + * + * Sets the emissive color of the material used for drawing shapes drawn to + * the screen. Used in combination with ambient(), + * specular(), and shininess() in setting the material + * properties of shapes. + * + * ( end auto-generated ) + * + * @webref lights_camera:material_properties + * @usage web_application + * @param rgb color to set + * @see PGraphics#ambient(float, float, float) + * @see PGraphics#specular(float, float, float) + * @see PGraphics#shininess(float) + */ + public void emissive(int rgb) { + if (recorder != null) recorder.emissive(rgb); + g.emissive(rgb); + } + + + /** + * gray number specifying value between white and black + */ + public void emissive(float gray) { + if (recorder != null) recorder.emissive(gray); + g.emissive(gray); + } + + + /** + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + */ + public void emissive(float v1, float v2, float v3) { + if (recorder != null) recorder.emissive(v1, v2, v3); + g.emissive(v1, v2, v3); + } + + + /** + * ( begin auto-generated from lights.xml ) + * + * Sets the default ambient light, directional light, falloff, and specular + * values. The defaults are ambientLight(128, 128, 128) and + * directionalLight(128, 128, 128, 0, 0, -1), lightFalloff(1, 0, 0), and + * lightSpecular(0, 0, 0). Lights need to be included in the draw() to + * remain persistent in a looping program. Placing them in the setup() of a + * looping program will cause them to only have an effect the first time + * through the loop. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @see PGraphics#ambientLight(float, float, float, float, float, float) + * @see PGraphics#directionalLight(float, float, float, float, float, float) + * @see PGraphics#pointLight(float, float, float, float, float, float) + * @see PGraphics#spotLight(float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#noLights() + */ + public void lights() { + if (recorder != null) recorder.lights(); + g.lights(); + } + + + /** + * ( begin auto-generated from noLights.xml ) + * + * Disable all lighting. Lighting is turned off by default and enabled with + * the lights() function. This function can be used to disable + * lighting so that 2D geometry (which does not require lighting) can be + * drawn after a set of lighted 3D geometry. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @see PGraphics#lights() + */ + public void noLights() { + if (recorder != null) recorder.noLights(); + g.noLights(); + } + + + /** + * ( begin auto-generated from ambientLight.xml ) + * + * Adds an ambient light. Ambient light doesn't come from a specific + * direction, the rays have light have bounced around so much that objects + * are evenly lit from all sides. Ambient lights are almost always used in + * combination with other types of lights. Lights need to be included in + * the draw() to remain persistent in a looping program. Placing + * them in the setup() of a looping program will cause them to only + * have an effect the first time through the loop. The effect of the + * parameters is determined by the current color mode. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + * @see PGraphics#lights() + * @see PGraphics#directionalLight(float, float, float, float, float, float) + * @see PGraphics#pointLight(float, float, float, float, float, float) + * @see PGraphics#spotLight(float, float, float, float, float, float, float, float, float, float, float) + */ + public void ambientLight(float v1, float v2, float v3) { + if (recorder != null) recorder.ambientLight(v1, v2, v3); + g.ambientLight(v1, v2, v3); + } + + + /** + * @param x x-coordinate of the light + * @param y y-coordinate of the light + * @param z z-coordinate of the light + */ + public void ambientLight(float v1, float v2, float v3, + float x, float y, float z) { + if (recorder != null) recorder.ambientLight(v1, v2, v3, x, y, z); + g.ambientLight(v1, v2, v3, x, y, z); + } + + + /** + * ( begin auto-generated from directionalLight.xml ) + * + * Adds a directional light. Directional light comes from one direction and + * is stronger when hitting a surface squarely and weaker if it hits at a a + * gentle angle. After hitting a surface, a directional lights scatters in + * all directions. Lights need to be included in the draw() to + * remain persistent in a looping program. Placing them in the + * setup() of a looping program will cause them to only have an + * effect the first time through the loop. The affect of the v1, + * v2, and v3 parameters is determined by the current color + * mode. The nx, ny, and nz parameters specify the + * direction the light is facing. For example, setting ny to -1 will + * cause the geometry to be lit from below (the light is facing directly upward). + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + * @param nx direction along the x-axis + * @param ny direction along the y-axis + * @param nz direction along the z-axis + * @see PGraphics#lights() + * @see PGraphics#ambientLight(float, float, float, float, float, float) + * @see PGraphics#pointLight(float, float, float, float, float, float) + * @see PGraphics#spotLight(float, float, float, float, float, float, float, float, float, float, float) + */ + public void directionalLight(float v1, float v2, float v3, + float nx, float ny, float nz) { + if (recorder != null) recorder.directionalLight(v1, v2, v3, nx, ny, nz); + g.directionalLight(v1, v2, v3, nx, ny, nz); + } + + + /** + * ( begin auto-generated from pointLight.xml ) + * + * Adds a point light. Lights need to be included in the draw() to + * remain persistent in a looping program. Placing them in the + * setup() of a looping program will cause them to only have an + * effect the first time through the loop. The affect of the v1, + * v2, and v3 parameters is determined by the current color + * mode. The x, y, and z parameters set the position + * of the light. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + * @param x x-coordinate of the light + * @param y y-coordinate of the light + * @param z z-coordinate of the light + * @see PGraphics#lights() + * @see PGraphics#directionalLight(float, float, float, float, float, float) + * @see PGraphics#ambientLight(float, float, float, float, float, float) + * @see PGraphics#spotLight(float, float, float, float, float, float, float, float, float, float, float) + */ + public void pointLight(float v1, float v2, float v3, + float x, float y, float z) { + if (recorder != null) recorder.pointLight(v1, v2, v3, x, y, z); + g.pointLight(v1, v2, v3, x, y, z); + } + + + /** + * ( begin auto-generated from spotLight.xml ) + * + * Adds a spot light. Lights need to be included in the draw() to + * remain persistent in a looping program. Placing them in the + * setup() of a looping program will cause them to only have an + * effect the first time through the loop. The affect of the v1, + * v2, and v3 parameters is determined by the current color + * mode. The x, y, and z parameters specify the + * position of the light and nx, ny, nz specify the + * direction or light. The angle parameter affects angle of the + * spotlight cone. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + * @param x x-coordinate of the light + * @param y y-coordinate of the light + * @param z z-coordinate of the light + * @param nx direction along the x axis + * @param ny direction along the y axis + * @param nz direction along the z axis + * @param angle angle of the spotlight cone + * @param concentration exponent determining the center bias of the cone + * @see PGraphics#lights() + * @see PGraphics#directionalLight(float, float, float, float, float, float) + * @see PGraphics#pointLight(float, float, float, float, float, float) + * @see PGraphics#ambientLight(float, float, float, float, float, float) + */ + public void spotLight(float v1, float v2, float v3, + float x, float y, float z, + float nx, float ny, float nz, + float angle, float concentration) { + if (recorder != null) recorder.spotLight(v1, v2, v3, x, y, z, nx, ny, nz, angle, concentration); + g.spotLight(v1, v2, v3, x, y, z, nx, ny, nz, angle, concentration); + } + + + /** + * ( begin auto-generated from lightFalloff.xml ) + * + * Sets the falloff rates for point lights, spot lights, and ambient + * lights. The parameters are used to determine the falloff with the + * following equation:

        d = distance from light position to + * vertex position
        falloff = 1 / (CONSTANT + d * LINEAR + (d*d) * + * QUADRATIC)

        Like fill(), it affects only the elements + * which are created after it in the code. The default value if + * LightFalloff(1.0, 0.0, 0.0). Thinking about an ambient light with + * a falloff can be tricky. It is used, for example, if you wanted a region + * of your scene to be lit ambiently one color and another region to be lit + * ambiently by another color, you would use an ambient light with location + * and falloff. You can think of it as a point light that doesn't care + * which direction a surface is facing. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @param constant constant value or determining falloff + * @param linear linear value for determining falloff + * @param quadratic quadratic value for determining falloff + * @see PGraphics#lights() + * @see PGraphics#ambientLight(float, float, float, float, float, float) + * @see PGraphics#pointLight(float, float, float, float, float, float) + * @see PGraphics#spotLight(float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#lightSpecular(float, float, float) + */ + public void lightFalloff(float constant, float linear, float quadratic) { + if (recorder != null) recorder.lightFalloff(constant, linear, quadratic); + g.lightFalloff(constant, linear, quadratic); + } + + + /** + * ( begin auto-generated from lightSpecular.xml ) + * + * Sets the specular color for lights. Like fill(), it affects only + * the elements which are created after it in the code. Specular refers to + * light which bounces off a surface in a perferred direction (rather than + * bouncing in all directions like a diffuse light) and is used for + * creating highlights. The specular quality of a light interacts with the + * specular material qualities set through the specular() and + * shininess() functions. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + * @see PGraphics#specular(float, float, float) + * @see PGraphics#lights() + * @see PGraphics#ambientLight(float, float, float, float, float, float) + * @see PGraphics#pointLight(float, float, float, float, float, float) + * @see PGraphics#spotLight(float, float, float, float, float, float, float, float, float, float, float) + */ + public void lightSpecular(float v1, float v2, float v3) { + if (recorder != null) recorder.lightSpecular(v1, v2, v3); + g.lightSpecular(v1, v2, v3); + } + + + /** + * ( begin auto-generated from background.xml ) + * + * The background() function sets the color used for the background + * of the Processing window. The default background is light gray. In the + * draw() function, the background color is used to clear the + * display window at the beginning of each frame. + *

        + * An image can also be used as the background for a sketch, however its + * width and height must be the same size as the sketch window. To resize + * an image 'b' to the size of the sketch window, use b.resize(width, height). + *

        + * Images used as background will ignore the current tint() setting. + *

        + * It is not possible to use transparency (alpha) in background colors with + * the main drawing surface, however they will work properly with createGraphics(). + * + * ( end auto-generated ) + * + *

        Advanced

        + *

        Clear the background with a color that includes an alpha value. This can + * only be used with objects created by createGraphics(), because the main + * drawing surface cannot be set transparent.

        + *

        It might be tempting to use this function to partially clear the screen + * on each frame, however that's not how this function works. When calling + * background(), the pixels will be replaced with pixels that have that level + * of transparency. To do a semi-transparent overlay, use fill() with alpha + * and draw a rectangle.

        + * + * @webref color:setting + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#stroke(float) + * @see PGraphics#fill(float) + * @see PGraphics#tint(float) + * @see PGraphics#colorMode(int) + */ + public void background(int rgb) { + if (recorder != null) recorder.background(rgb); + g.background(rgb); + } + + + /** + * @param alpha opacity of the background + */ + public void background(int rgb, float alpha) { + if (recorder != null) recorder.background(rgb, alpha); + g.background(rgb, alpha); + } + + + /** + * @param gray specifies a value between white and black + */ + public void background(float gray) { + if (recorder != null) recorder.background(gray); + g.background(gray); + } + + + public void background(float gray, float alpha) { + if (recorder != null) recorder.background(gray, alpha); + g.background(gray, alpha); + } + + + /** + * @param v1 red or hue value (depending on the current color mode) + * @param v2 green or saturation value (depending on the current color mode) + * @param v3 blue or brightness value (depending on the current color mode) + */ + public void background(float v1, float v2, float v3) { + if (recorder != null) recorder.background(v1, v2, v3); + g.background(v1, v2, v3); + } + + + public void background(float v1, float v2, float v3, float alpha) { + if (recorder != null) recorder.background(v1, v2, v3, alpha); + g.background(v1, v2, v3, alpha); + } + + + /** + * @webref color:setting + */ + public void clear() { + if (recorder != null) recorder.clear(); + g.clear(); + } + + + /** + * Takes an RGB or ARGB image and sets it as the background. + * The width and height of the image must be the same size as the sketch. + * Use image.resize(width, height) to make short work of such a task.
        + *
        + * Note that even if the image is set as RGB, the high 8 bits of each pixel + * should be set opaque (0xFF000000) because the image data will be copied + * directly to the screen, and non-opaque background images may have strange + * behavior. Use image.filter(OPAQUE) to handle this easily.
        + *
        + * When using 3D, this will also clear the zbuffer (if it exists). + * + * @param image PImage to set as background (must be same size as the sketch window) + */ + public void background(PImage image) { + if (recorder != null) recorder.background(image); + g.background(image); + } + + + /** + * ( begin auto-generated from colorMode.xml ) + * + * Changes the way Processing interprets color data. By default, the + * parameters for fill(), stroke(), background(), and + * color() are defined by values between 0 and 255 using the RGB + * color model. The colorMode() function is used to change the + * numerical range used for specifying colors and to switch color systems. + * For example, calling colorMode(RGB, 1.0) will specify that values + * are specified between 0 and 1. The limits for defining colors are + * altered by setting the parameters range1, range2, range3, and range 4. + * + * ( end auto-generated ) + * + * @webref color:setting + * @usage web_application + * @param mode Either RGB or HSB, corresponding to Red/Green/Blue and Hue/Saturation/Brightness + * @see PGraphics#background(float) + * @see PGraphics#fill(float) + * @see PGraphics#stroke(float) + */ + public void colorMode(int mode) { + if (recorder != null) recorder.colorMode(mode); + g.colorMode(mode); + } + + + /** + * @param max range for all color elements + */ + public void colorMode(int mode, float max) { + if (recorder != null) recorder.colorMode(mode, max); + g.colorMode(mode, max); + } + + + /** + * @param max1 range for the red or hue depending on the current color mode + * @param max2 range for the green or saturation depending on the current color mode + * @param max3 range for the blue or brightness depending on the current color mode + */ + public void colorMode(int mode, float max1, float max2, float max3) { + if (recorder != null) recorder.colorMode(mode, max1, max2, max3); + g.colorMode(mode, max1, max2, max3); + } + + + /** + * @param maxA range for the alpha + */ + public void colorMode(int mode, + float max1, float max2, float max3, float maxA) { + if (recorder != null) recorder.colorMode(mode, max1, max2, max3, maxA); + g.colorMode(mode, max1, max2, max3, maxA); + } + + + /** + * ( begin auto-generated from alpha.xml ) + * + * Extracts the alpha value from a color. + * + * ( end auto-generated ) + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#red(int) + * @see PGraphics#green(int) + * @see PGraphics#blue(int) + * @see PGraphics#hue(int) + * @see PGraphics#saturation(int) + * @see PGraphics#brightness(int) + */ + public final float alpha(int rgb) { + return g.alpha(rgb); + } + + + /** + * ( begin auto-generated from red.xml ) + * + * Extracts the red value from a color, scaled to match current + * colorMode(). This value is always returned as a float so be + * careful not to assign it to an int value.

        The red() function + * is easy to use and undestand, but is slower than another technique. To + * achieve the same results when working in colorMode(RGB, 255), but + * with greater speed, use the >> (right shift) operator with a bit + * mask. For example, the following two lines of code are equivalent:
        float r1 = red(myColor);
        float r2 = myColor >> 16 + * & 0xFF;
        + * + * ( end auto-generated ) + * + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#green(int) + * @see PGraphics#blue(int) + * @see PGraphics#alpha(int) + * @see PGraphics#hue(int) + * @see PGraphics#saturation(int) + * @see PGraphics#brightness(int) + * @see_external rightshift + */ + public final float red(int rgb) { + return g.red(rgb); + } + + + /** + * ( begin auto-generated from green.xml ) + * + * Extracts the green value from a color, scaled to match current + * colorMode(). This value is always returned as a float so be + * careful not to assign it to an int value.

        The green() + * function is easy to use and undestand, but is slower than another + * technique. To achieve the same results when working in colorMode(RGB, + * 255), but with greater speed, use the >> (right shift) + * operator with a bit mask. For example, the following two lines of code + * are equivalent:
        float r1 = green(myColor);
        float r2 = + * myColor >> 8 & 0xFF;
        + * + * ( end auto-generated ) + * + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#red(int) + * @see PGraphics#blue(int) + * @see PGraphics#alpha(int) + * @see PGraphics#hue(int) + * @see PGraphics#saturation(int) + * @see PGraphics#brightness(int) + * @see_external rightshift + */ + public final float green(int rgb) { + return g.green(rgb); + } + + + /** + * ( begin auto-generated from blue.xml ) + * + * Extracts the blue value from a color, scaled to match current + * colorMode(). This value is always returned as a float so be + * careful not to assign it to an int value.

        The blue() + * function is easy to use and undestand, but is slower than another + * technique. To achieve the same results when working in colorMode(RGB, + * 255), but with greater speed, use a bit mask to remove the other + * color components. For example, the following two lines of code are + * equivalent:
        float r1 = blue(myColor);
        float r2 = myColor + * & 0xFF;
        + * + * ( end auto-generated ) + * + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#red(int) + * @see PGraphics#green(int) + * @see PGraphics#alpha(int) + * @see PGraphics#hue(int) + * @see PGraphics#saturation(int) + * @see PGraphics#brightness(int) + * @see_external rightshift + */ + public final float blue(int rgb) { + return g.blue(rgb); + } + + + /** + * ( begin auto-generated from hue.xml ) + * + * Extracts the hue value from a color. + * + * ( end auto-generated ) + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#red(int) + * @see PGraphics#green(int) + * @see PGraphics#blue(int) + * @see PGraphics#alpha(int) + * @see PGraphics#saturation(int) + * @see PGraphics#brightness(int) + */ + public final float hue(int rgb) { + return g.hue(rgb); + } + + + /** + * ( begin auto-generated from saturation.xml ) + * + * Extracts the saturation value from a color. + * + * ( end auto-generated ) + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#red(int) + * @see PGraphics#green(int) + * @see PGraphics#blue(int) + * @see PGraphics#alpha(int) + * @see PGraphics#hue(int) + * @see PGraphics#brightness(int) + */ + public final float saturation(int rgb) { + return g.saturation(rgb); + } + + + /** + * ( begin auto-generated from brightness.xml ) + * + * Extracts the brightness value from a color. + * + * ( end auto-generated ) + * + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#red(int) + * @see PGraphics#green(int) + * @see PGraphics#blue(int) + * @see PGraphics#alpha(int) + * @see PGraphics#hue(int) + * @see PGraphics#saturation(int) + */ + public final float brightness(int rgb) { + return g.brightness(rgb); + } + + + /** + * @nowebref + * Interpolate between two colors. Like lerp(), but for the + * individual color components of a color supplied as an int value. + */ + static public int lerpColor(int c1, int c2, float amt, int mode) { + return PGraphics.lerpColor(c1, c2, amt, mode); + } + + + /** + * Display a warning that the specified method is only available with 3D. + * @param method The method name (no parentheses) + */ + static public void showDepthWarning(String method) { + PGraphics.showDepthWarning(method); + } + + + /** + * Display a warning that the specified method that takes x, y, z parameters + * can only be used with x and y parameters in this renderer. + * @param method The method name (no parentheses) + */ + static public void showDepthWarningXYZ(String method) { + PGraphics.showDepthWarningXYZ(method); + } + + + /** + * Display a warning that the specified method is simply unavailable. + */ + static public void showMethodWarning(String method) { + PGraphics.showMethodWarning(method); + } + + + /** + * Error that a particular variation of a method is unavailable (even though + * other variations are). For instance, if vertex(x, y, u, v) is not + * available, but vertex(x, y) is just fine. + */ + static public void showVariationWarning(String str) { + PGraphics.showVariationWarning(str); + } + + + /** + * Display a warning that the specified method is not implemented, meaning + * that it could be either a completely missing function, although other + * variations of it may still work properly. + */ + static public void showMissingWarning(String method) { + PGraphics.showMissingWarning(method); + } + + + /** + * ( begin auto-generated from PImage_get.xml ) + * + * Reads the color of any pixel or grabs a section of an image. If no + * parameters are specified, the entire image is returned. Use the x + * and y parameters to get the value of one pixel. Get a section of + * the display window by specifying an additional width and + * height parameter. When getting an image, the x and + * y parameters define the coordinates for the upper-left corner of + * the image, regardless of the current imageMode().
        + *
        + * If the pixel requested is outside of the image window, black is + * returned. The numbers returned are scaled according to the current color + * ranges, but only RGB values are returned by this function. For example, + * even though you may have drawn a shape with colorMode(HSB), the + * numbers returned will be in RGB format.
        + *
        + * Getting the color of a single pixel with get(x, y) is easy, but + * not as fast as grabbing the data directly from pixels[]. The + * equivalent statement to get(x, y) using pixels[] is + * pixels[y*width+x]. See the reference for pixels[] for more information. + * + * ( end auto-generated ) + * + *

        Advanced

        + * Returns an ARGB "color" type (a packed 32 bit int with the color. + * If the coordinate is outside the image, zero is returned + * (black, but completely transparent). + *

        + * If the image is in RGB format (i.e. on a PVideo object), + * the value will get its high bits set, just to avoid cases where + * they haven't been set already. + *

        + * If the image is in ALPHA format, this returns a white with its + * alpha value set. + *

        + * This function is included primarily for beginners. It is quite + * slow because it has to check to see if the x, y that was provided + * is inside the bounds, and then has to check to see what image + * type it is. If you want things to be more efficient, access the + * pixels[] array directly. + * + * @webref image:pixels + * @brief Reads the color of any pixel or grabs a rectangle of pixels + * @usage web_application + * @param x x-coordinate of the pixel + * @param y y-coordinate of the pixel + * @see PApplet#set(int, int, int) + * @see PApplet#pixels + * @see PApplet#copy(PImage, int, int, int, int, int, int, int, int) + */ + public int get(int x, int y) { + return g.get(x, y); + } + + + /** + * @param w width of pixel rectangle to get + * @param h height of pixel rectangle to get + */ + public PImage get(int x, int y, int w, int h) { + return g.get(x, y, w, h); + } + + + /** + * Returns a copy of this PImage. Equivalent to get(0, 0, width, height). + * Deprecated, just use copy() instead. + */ + public PImage get() { + return g.get(); + } + + + public PImage copy() { + return g.copy(); + } + + + /** + * ( begin auto-generated from PImage_set.xml ) + * + * Changes the color of any pixel or writes an image directly into the + * display window.
        + *
        + * The x and y parameters specify the pixel to change and the + * color parameter specifies the color value. The color parameter is + * affected by the current color mode (the default is RGB values from 0 to + * 255). When setting an image, the x and y parameters define + * the coordinates for the upper-left corner of the image, regardless of + * the current imageMode(). + *

        + * Setting the color of a single pixel with set(x, y) is easy, but + * not as fast as putting the data directly into pixels[]. The + * equivalent statement to set(x, y, #000000) using pixels[] + * is pixels[y*width+x] = #000000. See the reference for + * pixels[] for more information. + * + * ( end auto-generated ) + * + * @webref image:pixels + * @brief writes a color to any pixel or writes an image into another + * @usage web_application + * @param x x-coordinate of the pixel + * @param y y-coordinate of the pixel + * @param c any value of the color datatype + * @see PImage#get(int, int, int, int) + * @see PImage#pixels + * @see PImage#copy(PImage, int, int, int, int, int, int, int, int) + */ + public void set(int x, int y, int c) { + if (recorder != null) recorder.set(x, y, c); + g.set(x, y, c); + } + + + /** + *

        Advanced

        + * Efficient method of drawing an image's pixels directly to this surface. + * No variations are employed, meaning that any scale, tint, or imageMode + * settings will be ignored. + * + * @param img image to copy into the original image + */ + public void set(int x, int y, PImage img) { + if (recorder != null) recorder.set(x, y, img); + g.set(x, y, img); + } + + + /** + * ( begin auto-generated from PImage_mask.xml ) + * + * Masks part of an image from displaying by loading another image and + * using it as an alpha channel. This mask image should only contain + * grayscale data, but only the blue color channel is used. The mask image + * needs to be the same size as the image to which it is applied.
        + *
        + * In addition to using a mask image, an integer array containing the alpha + * channel data can be specified directly. This method is useful for + * creating dynamically generated alpha masks. This array must be of the + * same length as the target image's pixels array and should contain only + * grayscale data of values between 0-255. + * + * ( end auto-generated ) + * + *

        Advanced

        + * + * Set alpha channel for an image. Black colors in the source + * image will make the destination image completely transparent, + * and white will make things fully opaque. Gray values will + * be in-between steps. + *

        + * Strictly speaking the "blue" value from the source image is + * used as the alpha color. For a fully grayscale image, this + * is correct, but for a color image it's not 100% accurate. + * For a more accurate conversion, first use filter(GRAY) + * which will make the image into a "correct" grayscale by + * performing a proper luminance-based conversion. + * + * @webref pimage:method + * @usage web_application + * @param img image to use as the mask + * @brief Masks part of an image with another image as an alpha channel + */ + public void mask(PImage img) { + if (recorder != null) recorder.mask(img); + g.mask(img); + } + + + public void filter(int kind) { + if (recorder != null) recorder.filter(kind); + g.filter(kind); + } + + + /** + * ( begin auto-generated from PImage_filter.xml ) + * + * Filters an image as defined by one of the following modes:

        THRESHOLD - converts the image to black and white pixels depending if + * they are above or below the threshold defined by the level parameter. + * The level must be between 0.0 (black) and 1.0(white). If no level is + * specified, 0.5 is used.
        + *
        + * GRAY - converts any colors in the image to grayscale equivalents
        + *
        + * INVERT - sets each pixel to its inverse value
        + *
        + * POSTERIZE - limits each channel of the image to the number of colors + * specified as the level parameter
        + *
        + * BLUR - executes a Guassian blur with the level parameter specifying the + * extent of the blurring. If no level parameter is used, the blur is + * equivalent to Guassian blur of radius 1
        + *
        + * OPAQUE - sets the alpha channel to entirely opaque
        + *
        + * ERODE - reduces the light areas with the amount defined by the level + * parameter
        + *
        + * DILATE - increases the light areas with the amount defined by the level parameter + * + * ( end auto-generated ) + * + *

        Advanced

        + * Method to apply a variety of basic filters to this image. + *

        + *

          + *
        • filter(BLUR) provides a basic blur. + *
        • filter(GRAY) converts the image to grayscale based on luminance. + *
        • filter(INVERT) will invert the color components in the image. + *
        • filter(OPAQUE) set all the high bits in the image to opaque + *
        • filter(THRESHOLD) converts the image to black and white. + *
        • filter(DILATE) grow white/light areas + *
        • filter(ERODE) shrink white/light areas + *
        + * Luminance conversion code contributed by + * toxi + *

        + * Gaussian blur code contributed by + * Mario Klingemann + * + * @webref image:pixels + * @brief Converts the image to grayscale or black and white + * @usage web_application + * @param kind Either THRESHOLD, GRAY, OPAQUE, INVERT, POSTERIZE, BLUR, ERODE, or DILATE + * @param param unique for each, see above + */ + public void filter(int kind, float param) { + if (recorder != null) recorder.filter(kind, param); + g.filter(kind, param); + } + + + /** + * ( begin auto-generated from PImage_copy.xml ) + * + * Copies a region of pixels from one image into another. If the source and + * destination regions aren't the same size, it will automatically resize + * source pixels to fit the specified target region. No alpha information + * is used in the process, however if the source image has an alpha channel + * set, it will be copied as well. + *

        + * As of release 0149, this function ignores imageMode(). + * + * ( end auto-generated ) + * + * @webref image:pixels + * @brief Copies the entire image + * @usage web_application + * @param sx X coordinate of the source's upper left corner + * @param sy Y coordinate of the source's upper left corner + * @param sw source image width + * @param sh source image height + * @param dx X coordinate of the destination's upper left corner + * @param dy Y coordinate of the destination's upper left corner + * @param dw destination image width + * @param dh destination image height + * @see PGraphics#alpha(int) + * @see PImage#blend(PImage, int, int, int, int, int, int, int, int, int) + */ + public void copy(int sx, int sy, int sw, int sh, + int dx, int dy, int dw, int dh) { + if (recorder != null) recorder.copy(sx, sy, sw, sh, dx, dy, dw, dh); + g.copy(sx, sy, sw, sh, dx, dy, dw, dh); + } + + +/** + * @param src an image variable referring to the source image. + */ + public void copy(PImage src, + int sx, int sy, int sw, int sh, + int dx, int dy, int dw, int dh) { + if (recorder != null) recorder.copy(src, sx, sy, sw, sh, dx, dy, dw, dh); + g.copy(src, sx, sy, sw, sh, dx, dy, dw, dh); + } + + + public void blend(int sx, int sy, int sw, int sh, + int dx, int dy, int dw, int dh, int mode) { + if (recorder != null) recorder.blend(sx, sy, sw, sh, dx, dy, dw, dh, mode); + g.blend(sx, sy, sw, sh, dx, dy, dw, dh, mode); + } + + + /** + * ( begin auto-generated from PImage_blend.xml ) + * + * Blends a region of pixels into the image specified by the img + * parameter. These copies utilize full alpha channel support and a choice + * of the following modes to blend the colors of source pixels (A) with the + * ones of pixels in the destination image (B):
        + *
        + * BLEND - linear interpolation of colours: C = A*factor + B
        + *
        + * ADD - additive blending with white clip: C = min(A*factor + B, 255)
        + *
        + * SUBTRACT - subtractive blending with black clip: C = max(B - A*factor, + * 0)
        + *
        + * DARKEST - only the darkest colour succeeds: C = min(A*factor, B)
        + *
        + * LIGHTEST - only the lightest colour succeeds: C = max(A*factor, B)
        + *
        + * DIFFERENCE - subtract colors from underlying image.
        + *
        + * EXCLUSION - similar to DIFFERENCE, but less extreme.
        + *
        + * MULTIPLY - Multiply the colors, result will always be darker.
        + *
        + * SCREEN - Opposite multiply, uses inverse values of the colors.
        + *
        + * OVERLAY - A mix of MULTIPLY and SCREEN. Multiplies dark values, + * and screens light values.
        + *
        + * HARD_LIGHT - SCREEN when greater than 50% gray, MULTIPLY when lower.
        + *
        + * SOFT_LIGHT - Mix of DARKEST and LIGHTEST. + * Works like OVERLAY, but not as harsh.
        + *
        + * DODGE - Lightens light tones and increases contrast, ignores darks. + * Called "Color Dodge" in Illustrator and Photoshop.
        + *
        + * BURN - Darker areas are applied, increasing contrast, ignores lights. + * Called "Color Burn" in Illustrator and Photoshop.
        + *
        + * All modes use the alpha information (highest byte) of source image + * pixels as the blending factor. If the source and destination regions are + * different sizes, the image will be automatically resized to match the + * destination size. If the srcImg parameter is not used, the + * display window is used as the source image.
        + *
        + * As of release 0149, this function ignores imageMode(). + * + * ( end auto-generated ) + * + * @webref image:pixels + * @brief Copies a pixel or rectangle of pixels using different blending modes + * @param src an image variable referring to the source image + * @param sx X coordinate of the source's upper left corner + * @param sy Y coordinate of the source's upper left corner + * @param sw source image width + * @param sh source image height + * @param dx X coordinate of the destinations's upper left corner + * @param dy Y coordinate of the destinations's upper left corner + * @param dw destination image width + * @param dh destination image height + * @param mode Either BLEND, ADD, SUBTRACT, LIGHTEST, DARKEST, DIFFERENCE, EXCLUSION, MULTIPLY, SCREEN, OVERLAY, HARD_LIGHT, SOFT_LIGHT, DODGE, BURN + * + * @see PApplet#alpha(int) + * @see PImage#copy(PImage, int, int, int, int, int, int, int, int) + * @see PImage#blendColor(int,int,int) + */ + public void blend(PImage src, + int sx, int sy, int sw, int sh, + int dx, int dy, int dw, int dh, int mode) { + if (recorder != null) recorder.blend(src, sx, sy, sw, sh, dx, dy, dw, dh, mode); + g.blend(src, sx, sy, sw, sh, dx, dy, dw, dh, mode); + } +} diff --git a/src/main/java/processing/core/PConstants.java b/src/main/java/processing/core/PConstants.java new file mode 100644 index 0000000..6792ec0 --- /dev/null +++ b/src/main/java/processing/core/PConstants.java @@ -0,0 +1,527 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2004-11 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + +import java.awt.Cursor; +import java.awt.event.KeyEvent; + + +/** + * Numbers shared throughout processing.core. + *

        + * An attempt is made to keep the constants as short/non-verbose + * as possible. For instance, the constant is TIFF instead of + * FILE_TYPE_TIFF. We'll do this as long as we can get away with it. + * + * @usage Web & Application + */ +public interface PConstants { + + static public final int X = 0; + static public final int Y = 1; + static public final int Z = 2; + + + // renderers known to processing.core + + /* + // List of renderers used inside PdePreprocessor + static final StringList rendererList = new StringList(new String[] { + "JAVA2D", "JAVA2D_2X", + "P2D", "P2D_2X", "P3D", "P3D_2X", "OPENGL", + "E2D", "FX2D", "FX2D_2X", // experimental + "LWJGL.P2D", "LWJGL.P3D", // hmm + "PDF" // no DXF because that's only for beginRaw() + }); + */ + + static final String JAVA2D = "processing.awt.PGraphicsJava2D"; + + static final String P2D = "processing.opengl.PGraphics2D"; + static final String P3D = "processing.opengl.PGraphics3D"; + + // When will it be time to remove this? + @Deprecated + static final String OPENGL = P3D; + + // Experimental, higher-performance Java 2D renderer (but no pixel ops) +// static final String E2D = PGraphicsDanger2D.class.getName(); + + // Experimental JavaFX renderer; even better 2D performance + static final String FX2D = "processing.javafx.PGraphicsFX2D"; + + static final String PDF = "processing.pdf.PGraphicsPDF"; + static final String SVG = "processing.svg.PGraphicsSVG"; + static final String DXF = "processing.dxf.RawDXF"; + + // platform IDs for PApplet.platform + + static final int OTHER = 0; + static final int WINDOWS = 1; + static final int MACOSX = 2; + static final int LINUX = 3; + + static final String[] platformNames = { + "other", "windows", "macosx", "linux" + }; + + + static final float EPSILON = 0.0001f; + + + // max/min values for numbers + + /** + * Same as Float.MAX_VALUE, but included for parity with MIN_VALUE, + * and to avoid teaching static methods on the first day. + */ + static final float MAX_FLOAT = Float.MAX_VALUE; + /** + * Note that Float.MIN_VALUE is the smallest positive value + * for a floating point number, not actually the minimum (negative) value + * for a float. This constant equals 0xFF7FFFFF, the smallest (farthest + * negative) value a float can have before it hits NaN. + */ + static final float MIN_FLOAT = -Float.MAX_VALUE; + /** Largest possible (positive) integer value */ + static final int MAX_INT = Integer.MAX_VALUE; + /** Smallest possible (negative) integer value */ + static final int MIN_INT = Integer.MIN_VALUE; + + // shapes + + static public final int VERTEX = 0; + static public final int BEZIER_VERTEX = 1; + static public final int QUADRATIC_VERTEX = 2; + static public final int CURVE_VERTEX = 3; + static public final int BREAK = 4; + + @Deprecated + static public final int QUAD_BEZIER_VERTEX = 2; // should not have been exposed + + // useful goodness + + /** + * ( begin auto-generated from PI.xml ) + * + * PI is a mathematical constant with the value 3.14159265358979323846. It + * is the ratio of the circumference of a circle to its diameter. It is + * useful in combination with the trigonometric functions sin() and + * cos(). + * + * ( end auto-generated ) + * @webref constants + * @see PConstants#TWO_PI + * @see PConstants#TAU + * @see PConstants#HALF_PI + * @see PConstants#QUARTER_PI + * + */ + static final float PI = (float) Math.PI; + /** + * ( begin auto-generated from HALF_PI.xml ) + * + * HALF_PI is a mathematical constant with the value + * 1.57079632679489661923. It is half the ratio of the circumference of a + * circle to its diameter. It is useful in combination with the + * trigonometric functions sin() and cos(). + * + * ( end auto-generated ) + * @webref constants + * @see PConstants#PI + * @see PConstants#TWO_PI + * @see PConstants#TAU + * @see PConstants#QUARTER_PI + */ + static final float HALF_PI = (float) (Math.PI / 2.0); + static final float THIRD_PI = (float) (Math.PI / 3.0); + /** + * ( begin auto-generated from QUARTER_PI.xml ) + * + * QUARTER_PI is a mathematical constant with the value 0.7853982. It is + * one quarter the ratio of the circumference of a circle to its diameter. + * It is useful in combination with the trigonometric functions + * sin() and cos(). + * + * ( end auto-generated ) + * @webref constants + * @see PConstants#PI + * @see PConstants#TWO_PI + * @see PConstants#TAU + * @see PConstants#HALF_PI + */ + static final float QUARTER_PI = (float) (Math.PI / 4.0); + /** + * ( begin auto-generated from TWO_PI.xml ) + * + * TWO_PI is a mathematical constant with the value 6.28318530717958647693. + * It is twice the ratio of the circumference of a circle to its diameter. + * It is useful in combination with the trigonometric functions + * sin() and cos(). + * + * ( end auto-generated ) + * @webref constants + * @see PConstants#PI + * @see PConstants#TAU + * @see PConstants#HALF_PI + * @see PConstants#QUARTER_PI + */ + static final float TWO_PI = (float) (2.0 * Math.PI); + /** + * ( begin auto-generated from TAU.xml ) + * + * TAU is an alias for TWO_PI, a mathematical constant with the value + * 6.28318530717958647693. It is twice the ratio of the circumference + * of a circle to its diameter. It is useful in combination with the + * trigonometric functions sin() and cos(). + * + * ( end auto-generated ) + * @webref constants + * @see PConstants#PI + * @see PConstants#TWO_PI + * @see PConstants#HALF_PI + * @see PConstants#QUARTER_PI + */ + static final float TAU = (float) (2.0 * Math.PI); + + static final float DEG_TO_RAD = PI/180.0f; + static final float RAD_TO_DEG = 180.0f/PI; + + + // angle modes + + //static final int RADIANS = 0; + //static final int DEGREES = 1; + + + // used by split, all the standard whitespace chars + // (also includes unicode nbsp, that little bostage) + + static final String WHITESPACE = " \t\n\r\f\u00A0"; + + + // for colors and/or images + + static final int RGB = 1; // image & color + static final int ARGB = 2; // image + static final int HSB = 3; // color + static final int ALPHA = 4; // image +// static final int CMYK = 5; // image & color (someday) + + + // image file types + + static final int TIFF = 0; + static final int TARGA = 1; + static final int JPEG = 2; + static final int GIF = 3; + + + // filter/convert types + + static final int BLUR = 11; + static final int GRAY = 12; + static final int INVERT = 13; + static final int OPAQUE = 14; + static final int POSTERIZE = 15; + static final int THRESHOLD = 16; + static final int ERODE = 17; + static final int DILATE = 18; + + + // blend mode keyword definitions + // @see processing.core.PImage#blendColor(int,int,int) + + public final static int REPLACE = 0; + public final static int BLEND = 1 << 0; + public final static int ADD = 1 << 1; + public final static int SUBTRACT = 1 << 2; + public final static int LIGHTEST = 1 << 3; + public final static int DARKEST = 1 << 4; + public final static int DIFFERENCE = 1 << 5; + public final static int EXCLUSION = 1 << 6; + public final static int MULTIPLY = 1 << 7; + public final static int SCREEN = 1 << 8; + public final static int OVERLAY = 1 << 9; + public final static int HARD_LIGHT = 1 << 10; + public final static int SOFT_LIGHT = 1 << 11; + public final static int DODGE = 1 << 12; + public final static int BURN = 1 << 13; + + // for messages + + static final int CHATTER = 0; + static final int COMPLAINT = 1; + static final int PROBLEM = 2; + + + // types of transformation matrices + + static final int PROJECTION = 0; + static final int MODELVIEW = 1; + + // types of projection matrices + + static final int CUSTOM = 0; // user-specified fanciness + static final int ORTHOGRAPHIC = 2; // 2D isometric projection + static final int PERSPECTIVE = 3; // perspective matrix + + + // shapes + + // the low four bits set the variety, + // higher bits set the specific shape type + + static final int GROUP = 0; // createShape() + + static final int POINT = 2; // primitive + static final int POINTS = 3; // vertices + + static final int LINE = 4; // primitive + static final int LINES = 5; // beginShape(), createShape() + static final int LINE_STRIP = 50; // beginShape() + static final int LINE_LOOP = 51; + + static final int TRIANGLE = 8; // primitive + static final int TRIANGLES = 9; // vertices + static final int TRIANGLE_STRIP = 10; // vertices + static final int TRIANGLE_FAN = 11; // vertices + + static final int QUAD = 16; // primitive + static final int QUADS = 17; // vertices + static final int QUAD_STRIP = 18; // vertices + + static final int POLYGON = 20; // in the end, probably cannot + static final int PATH = 21; // separate these two + + static final int RECT = 30; // primitive + static final int ELLIPSE = 31; // primitive + static final int ARC = 32; // primitive + + static final int SPHERE = 40; // primitive + static final int BOX = 41; // primitive + +// static public final int POINT_SPRITES = 52; +// static public final int NON_STROKED_SHAPE = 60; +// static public final int STROKED_SHAPE = 61; + + + // shape closing modes + + static final int OPEN = 1; + static final int CLOSE = 2; + + + // shape drawing modes + + /** Draw mode convention to use (x, y) to (width, height) */ + static final int CORNER = 0; + /** Draw mode convention to use (x1, y1) to (x2, y2) coordinates */ + static final int CORNERS = 1; + /** Draw mode from the center, and using the radius */ + static final int RADIUS = 2; + /** + * Draw from the center, using second pair of values as the diameter. + * Formerly called CENTER_DIAMETER in alpha releases. + */ + static final int CENTER = 3; + /** + * Synonym for the CENTER constant. Draw from the center, + * using second pair of values as the diameter. + */ + static final int DIAMETER = 3; + + + // arc drawing modes + + //static final int OPEN = 1; // shared + static final int CHORD = 2; + static final int PIE = 3; + + + // vertically alignment modes for text + + /** Default vertical alignment for text placement */ + static final int BASELINE = 0; + /** Align text to the top */ + static final int TOP = 101; + /** Align text from the bottom, using the baseline. */ + static final int BOTTOM = 102; + + + // uv texture orientation modes + + /** texture coordinates in 0..1 range */ + static final int NORMAL = 1; + /** texture coordinates based on image width/height */ + static final int IMAGE = 2; + + + // texture wrapping modes + + /** textures are clamped to their edges */ + public static final int CLAMP = 0; + /** textures wrap around when uv values go outside 0..1 range */ + public static final int REPEAT = 1; + + + // text placement modes + + /** + * textMode(MODEL) is the default, meaning that characters + * will be affected by transformations like any other shapes. + *

        + * Changed value in 0093 to not interfere with LEFT, CENTER, and RIGHT. + */ + static final int MODEL = 4; + + /** + * textMode(SHAPE) draws text using the the glyph outlines of + * individual characters rather than as textures. If the outlines are + * not available, then textMode(SHAPE) will be ignored and textMode(MODEL) + * will be used instead. For this reason, be sure to call textMode() + * after calling textFont(). + *

        + * Currently, textMode(SHAPE) is only supported by OPENGL mode. + * It also requires Java 1.2 or higher (OPENGL requires 1.4 anyway) + */ + static final int SHAPE = 5; + + + // text alignment modes + // are inherited from LEFT, CENTER, RIGHT + + // stroke modes + + static final int SQUARE = 1 << 0; // called 'butt' in the svg spec + static final int ROUND = 1 << 1; + static final int PROJECT = 1 << 2; // called 'square' in the svg spec + static final int MITER = 1 << 3; + static final int BEVEL = 1 << 5; + + + // lighting + + static final int AMBIENT = 0; + static final int DIRECTIONAL = 1; + //static final int POINT = 2; // shared with shape feature + static final int SPOT = 3; + + + // key constants + + // only including the most-used of these guys + // if people need more esoteric keys, they can learn about + // the esoteric java KeyEvent api and of virtual keys + + // both key and keyCode will equal these values + // for 0125, these were changed to 'char' values, because they + // can be upgraded to ints automatically by Java, but having them + // as ints prevented split(blah, TAB) from working + static final char BACKSPACE = 8; + static final char TAB = 9; + static final char ENTER = 10; + static final char RETURN = 13; + static final char ESC = 27; + static final char DELETE = 127; + + // i.e. if ((key == CODED) && (keyCode == UP)) + static final int CODED = 0xffff; + + // key will be CODED and keyCode will be this value + static final int UP = KeyEvent.VK_UP; + static final int DOWN = KeyEvent.VK_DOWN; + static final int LEFT = KeyEvent.VK_LEFT; + static final int RIGHT = KeyEvent.VK_RIGHT; + + // key will be CODED and keyCode will be this value + static final int ALT = KeyEvent.VK_ALT; + static final int CONTROL = KeyEvent.VK_CONTROL; + static final int SHIFT = KeyEvent.VK_SHIFT; + + + // orientations (only used on Android, ignored on desktop) + + /** Screen orientation constant for portrait (the hamburger way). */ + static final int PORTRAIT = 1; + /** Screen orientation constant for landscape (the hot dog way). */ + static final int LANDSCAPE = 2; + + /** Use with fullScreen() to indicate all available displays. */ + static final int SPAN = 0; + + // cursor types + + static final int ARROW = Cursor.DEFAULT_CURSOR; + static final int CROSS = Cursor.CROSSHAIR_CURSOR; + static final int HAND = Cursor.HAND_CURSOR; + static final int MOVE = Cursor.MOVE_CURSOR; + static final int TEXT = Cursor.TEXT_CURSOR; + static final int WAIT = Cursor.WAIT_CURSOR; + + + // hints - hint values are positive for the alternate version, + // negative of the same value returns to the normal/default state + + @Deprecated + static final int ENABLE_NATIVE_FONTS = 1; + @Deprecated + static final int DISABLE_NATIVE_FONTS = -1; + + static final int DISABLE_DEPTH_TEST = 2; + static final int ENABLE_DEPTH_TEST = -2; + + static final int ENABLE_DEPTH_SORT = 3; + static final int DISABLE_DEPTH_SORT = -3; + + static final int DISABLE_OPENGL_ERRORS = 4; + static final int ENABLE_OPENGL_ERRORS = -4; + + static final int DISABLE_DEPTH_MASK = 5; + static final int ENABLE_DEPTH_MASK = -5; + + static final int DISABLE_OPTIMIZED_STROKE = 6; + static final int ENABLE_OPTIMIZED_STROKE = -6; + + static final int ENABLE_STROKE_PERSPECTIVE = 7; + static final int DISABLE_STROKE_PERSPECTIVE = -7; + + static final int DISABLE_TEXTURE_MIPMAPS = 8; + static final int ENABLE_TEXTURE_MIPMAPS = -8; + + static final int ENABLE_STROKE_PURE = 9; + static final int DISABLE_STROKE_PURE = -9; + + static final int ENABLE_BUFFER_READING = 10; + static final int DISABLE_BUFFER_READING = -10; + + static final int DISABLE_KEY_REPEAT = 11; + static final int ENABLE_KEY_REPEAT = -11; + + static final int DISABLE_ASYNC_SAVEFRAME = 12; + static final int ENABLE_ASYNC_SAVEFRAME = -12; + + static final int HINT_COUNT = 13; +} diff --git a/src/main/java/processing/core/PFont.java b/src/main/java/processing/core/PFont.java new file mode 100644 index 0000000..632cd7e --- /dev/null +++ b/src/main/java/processing/core/PFont.java @@ -0,0 +1,1098 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry & Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of version 2.01 of the GNU Lesser General + Public License as published by the Free Software Foundation. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + +import java.awt.*; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.geom.PathIterator; +import java.awt.image.*; +import java.io.*; +import java.util.Arrays; +import java.util.HashMap; + + +/** + * Grayscale bitmap font class used by Processing. + *

        + * Awful (and by that, I mean awesome) ASCII (non-)art for how this works: + *

        + *   |
        + *   |                   height is the full used height of the image
        + *   |
        + *   |   ..XX..       }
        + *   |   ..XX..       }
        + *   |   ......       }
        + *   |   XXXX..       }  topExtent (top y is baseline - topExtent)
        + *   |   ..XX..       }
        + *   |   ..XX..       }  dotted areas are where the image data
        + *   |   ..XX..       }  is actually located for the character
        + *   +---XXXXXX----   }  (it extends to the right and down
        + *   |                   for power of two texture sizes)
        + *   ^^^^ leftExtent (amount to move over before drawing the image
        + *
        + *   ^^^^^^^^^^^^^^ setWidth (width displaced by char)
        + * 
        + * @webref typography + * @see PApplet#loadFont(String) + * @see PApplet#createFont(String, float, boolean, char[]) + * @see PGraphics#textFont(PFont) + */ +public class PFont implements PConstants { + + /** Number of character glyphs in this font. */ + protected int glyphCount; + + /** + * Actual glyph data. The length of this array won't necessarily be the + * same size as glyphCount, in cases where lazy font loading is in use. + */ + protected Glyph[] glyphs; + + /** + * Name of the font as seen by Java when it was created. + * If the font is available, the native version will be used. + */ + protected String name; + + /** + * Postscript name of the font that this bitmap was created from. + */ + protected String psname; + + /** + * The original size of the font when it was first created + */ + protected int size; + + /** Default density set to 1 for backwards compatibility with loadFont(). */ + protected int density = 1; + + /** true if smoothing was enabled for this font, used for native impl */ + protected boolean smooth; + + /** + * The ascent of the font. If the 'd' character is present in this PFont, + * this value is replaced with its pixel height, because the values returned + * by FontMetrics.getAscent() seem to be terrible. + */ + protected int ascent; + + /** + * The descent of the font. If the 'p' character is present in this PFont, + * this value is replaced with its lowest pixel height, because the values + * returned by FontMetrics.getDescent() are gross. + */ + protected int descent; + + /** + * A more efficient array lookup for straight ASCII characters. For Unicode + * characters, a QuickSort-style search is used. + */ + protected int[] ascii; + + /** + * True if this font is set to load dynamically. This is the default when + * createFont() method is called without a character set. Bitmap versions of + * characters are only created when prompted by an index() call. + */ + protected boolean lazy; + + /** + * Native Java version of the font. If possible, this allows the + * PGraphics subclass to just use Java's font rendering stuff + * in situations where that's faster. + */ + protected Font font; + + /** + * True if this font was loaded from an InputStream, rather than by name + * from the OS. It's best to use the native version of a font loaded from + * a TTF file, since that will ensure that the font is available when the + * sketch is exported. + */ + protected boolean stream; + + /** + * True if this font should return 'null' for getFont(), so that the native + * font will be used to create a subset, but the native version of the font + * will not be used. + */ + protected boolean subsetting; + + /** True if already tried to find the native AWT version of this font. */ + protected boolean fontSearched; + + /** + * Array of the native system fonts. Used to lookup native fonts by their + * PostScript name. This is a workaround for a several year old Apple Java + * bug that they can't be bothered to fix. + */ + static protected Font[] fonts; + static protected HashMap fontDifferent; + +// /** +// * If not null, this font is set to load dynamically. This is the default +// * when createFont() method is called without a character set. Bitmap +// * versions of characters are only created when prompted by an index() call. +// */ +// protected Font lazyFont; + protected BufferedImage lazyImage; + protected Graphics2D lazyGraphics; + protected FontMetrics lazyMetrics; + protected int[] lazySamples; + + + /** for subclasses that need to store metadata about the font */ +// protected HashMap cacheMap; + + /** + * @nowebref + */ + public PFont() { } // for subclasses + + + /** + * ( begin auto-generated from PFont.xml ) + * + * PFont is the font class for Processing. To create a font to use with + * Processing, select "Create Font..." from the Tools menu. This will + * create a font in the format Processing requires and also adds it to the + * current sketch's data directory. Processing displays fonts using the + * .vlw font format, which uses images for each letter, rather than + * defining them through vector data. The loadFont() function + * constructs a new font and textFont() makes a font active. The + * list() method creates a list of the fonts installed on the + * computer, which is useful information to use with the + * createFont() function for dynamically converting fonts into a + * format to use with Processing. + * + * ( end auto-generated ) + * + * @nowebref + * @param font font the font object to create from + * @param smooth smooth true to enable smoothing/anti-aliasing + */ + public PFont(Font font, boolean smooth) { + this(font, smooth, null); + } + + + /** + * Create a new image-based font on the fly. If charset is set to null, + * the characters will only be created as bitmaps when they're drawn. + * + * @nowebref + * @param charset array of all unicode chars that should be included + */ + public PFont(Font font, boolean smooth, char charset[]) { + // save this so that we can use the native version + this.font = font; + this.smooth = smooth; + + name = font.getName(); + psname = font.getPSName(); + size = font.getSize(); + + // no, i'm not interested in getting off the couch + //lazy = true; + // not sure what else to do here + //mbox2 = 0; + + int initialCount = 10; + glyphs = new Glyph[initialCount]; + + ascii = new int[128]; + Arrays.fill(ascii, -1); + + int mbox3 = size * 3; + + lazyImage = new BufferedImage(mbox3, mbox3, BufferedImage.TYPE_INT_RGB); + lazyGraphics = (Graphics2D) lazyImage.getGraphics(); + lazyGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + smooth ? + RenderingHints.VALUE_ANTIALIAS_ON : + RenderingHints.VALUE_ANTIALIAS_OFF); + // adding this for post-1.0.9 + lazyGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + smooth ? + RenderingHints.VALUE_TEXT_ANTIALIAS_ON : + RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); + + lazyGraphics.setFont(font); + lazyMetrics = lazyGraphics.getFontMetrics(); + lazySamples = new int[mbox3 * mbox3]; + + // These values are terrible/unusable. Verified again for Processing 1.1. + // They vary widely per-platform and per-font, so instead we'll use the + // calculate-by-hand method of measuring pixels in characters. + //ascent = lazyMetrics.getAscent(); + //descent = lazyMetrics.getDescent(); + + if (charset == null) { + lazy = true; +// lazyFont = font; + + } else { + // charset needs to be sorted to make index lookup run more quickly + // http://dev.processing.org/bugs/show_bug.cgi?id=494 + Arrays.sort(charset); + + glyphs = new Glyph[charset.length]; + + glyphCount = 0; + for (char c : charset) { + if (font.canDisplay(c)) { + Glyph glyf = new Glyph(c); + if (glyf.value < 128) { + ascii[glyf.value] = glyphCount; + } + glyf.index = glyphCount; + glyphs[glyphCount++] = glyf; + } + } + + // shorten the array if necessary + if (glyphCount != charset.length) { + glyphs = (Glyph[]) PApplet.subset(glyphs, 0, glyphCount); + } + + // foreign font, so just make ascent the max topExtent + // for > 1.0.9, not doing this anymore. + // instead using getAscent() and getDescent() values for these cases. +// if ((ascent == 0) && (descent == 0)) { +// //for (int i = 0; i < charCount; i++) { +// for (Glyph glyph : glyphs) { +// char cc = (char) glyph.value; +// //char cc = (char) glyphs[i].value; +// if (Character.isWhitespace(cc) || +// (cc == '\u00A0') || (cc == '\u2007') || (cc == '\u202F')) { +// continue; +// } +// if (glyph.topExtent > ascent) { +// ascent = glyph.topExtent; +// } +// int d = -glyph.topExtent + glyph.height; +// if (d > descent) { +// descent = d; +// } +// } +// } + } + + // If not already created, just create these two characters to calculate + // the ascent and descent values for the font. This was tested to only + // require 5-10 ms on a 2.4 GHz MacBook Pro. + // In versions 1.0.9 and earlier, fonts that could not display d or p + // used the max up/down values as calculated by looking through the font. + // That's no longer valid with the auto-generating fonts, so we'll just + // use getAscent() and getDescent() in such (minor) cases. + if (ascent == 0) { + if (font.canDisplay('d')) { + new Glyph('d'); + } else { + ascent = lazyMetrics.getAscent(); + } + } + if (descent == 0) { + if (font.canDisplay('p')) { + new Glyph('p'); + } else { + descent = lazyMetrics.getDescent(); + } + } + } + + + /** + * Adds an additional parameter that indicates the font came from a file, + * not a built-in OS font. + * + * @nowebref + */ + public PFont(Font font, boolean smooth, char charset[], + boolean stream, int density) { + this(font, smooth, charset); + this.stream = stream; + this.density = density; + } + + /** + * @nowebref + * @param input InputStream + */ + public PFont(InputStream input) throws IOException { + DataInputStream is = new DataInputStream(input); + + // number of character images stored in this font + glyphCount = is.readInt(); + + // used to be the bitCount, but now used for version number. + // version 8 is any font before 69, so 9 is anything from 83+ + // 9 was buggy so gonna increment to 10. + int version = is.readInt(); + + // this was formerly ignored, now it's the actual font size + //mbox = is.readInt(); + size = is.readInt(); + + // this was formerly mboxY, the one that was used + // this will make new fonts downward compatible + is.readInt(); // ignore the other mbox attribute + + ascent = is.readInt(); // formerly baseHt (zero/ignored) + descent = is.readInt(); // formerly ignored struct padding + + // allocate enough space for the character info + glyphs = new Glyph[glyphCount]; + + ascii = new int[128]; + Arrays.fill(ascii, -1); + + // read the information about the individual characters + for (int i = 0; i < glyphCount; i++) { + Glyph glyph = new Glyph(is); + // cache locations of the ascii charset + if (glyph.value < 128) { + ascii[glyph.value] = i; + } + glyph.index = i; + glyphs[i] = glyph; + } + + // not a roman font, so throw an error and ask to re-build. + // that way can avoid a bunch of error checking hacks in here. + if ((ascent == 0) && (descent == 0)) { + throw new RuntimeException("Please use \"Create Font\" to " + + "re-create this font."); + } + + for (Glyph glyph : glyphs) { + glyph.readBitmap(is); + } + + if (version >= 10) { // includes the font name at the end of the file + name = is.readUTF(); + psname = is.readUTF(); + } + if (version == 11) { + smooth = is.readBoolean(); + } + // See if there's a native version of this font that can be used, + // in case that's of interest later. +// findNative(); + } + + + /** + * Write this PFont to an OutputStream. + *

        + * This is used by the Create Font tool, or whatever anyone else dreams + * up for messing with fonts themselves. + *

        + * It is assumed that the calling class will handle closing + * the stream when finished. + */ + public void save(OutputStream output) throws IOException { + DataOutputStream os = new DataOutputStream(output); + + os.writeInt(glyphCount); + + if ((name == null) || (psname == null)) { + name = ""; + psname = ""; + } + + os.writeInt(11); // formerly numBits, now used for version number + os.writeInt(size); // formerly mboxX (was 64, now 48) + os.writeInt(0); // formerly mboxY, now ignored + os.writeInt(ascent); // formerly baseHt (was ignored) + os.writeInt(descent); // formerly struct padding for c version + + for (int i = 0; i < glyphCount; i++) { + glyphs[i].writeHeader(os); + } + + for (int i = 0; i < glyphCount; i++) { + glyphs[i].writeBitmap(os); + } + + // version 11 + os.writeUTF(name); + os.writeUTF(psname); + os.writeBoolean(smooth); + + os.flush(); + } + + + /** + * Create a new glyph, and add the character to the current font. + * @param c character to create an image for. + */ + protected void addGlyph(char c) { + Glyph glyph = new Glyph(c); + + if (glyphCount == glyphs.length) { + glyphs = (Glyph[]) PApplet.expand(glyphs); + } + if (glyphCount == 0) { + glyph.index = 0; + glyphs[glyphCount] = glyph; + if (glyph.value < 128) { + ascii[glyph.value] = 0; + } + + } else if (glyphs[glyphCount-1].value < glyph.value) { + glyphs[glyphCount] = glyph; + if (glyph.value < 128) { + ascii[glyph.value] = glyphCount; + } + + } else { + for (int i = 0; i < glyphCount; i++) { + if (glyphs[i].value > c) { + for (int j = glyphCount; j > i; --j) { + glyphs[j] = glyphs[j-1]; + if (glyphs[j].value < 128) { + ascii[glyphs[j].value] = j; + } + } + glyph.index = i; + glyphs[i] = glyph; + // cache locations of the ascii charset + if (c < 128) ascii[c] = i; + break; + } + } + } + glyphCount++; + } + + + public String getName() { + return name; + } + + + public String getPostScriptName() { + return psname; + } + + + /** + * Set the native complement of this font. Might be set internally via the + * findFont() function, or externally by a deriveFont() call if the font + * is resized by PGraphicsJava2D. + */ + public void setNative(Object font) { + this.font = (Font) font; + } + + + /** + * Use the getNative() method instead, which allows library interfaces to be + * written in a cross-platform fashion for desktop, Android, and others. + */ + @Deprecated + public Font getFont() { + return font; + } + + + /** + * Return the native java.awt.Font associated with this PFont (if any). + */ + public Object getNative() { + if (subsetting) { + return null; // don't return the font for use + } + return font; + } + + + /** + * Return size of this font. + */ + public int getSize() { + return size; + } + + +// public void setDefaultSize(int size) { +// defaultSize = size; +// } + + + /** + * Returns the size that will be used when textFont(font) is called. + * When drawing with 2x pixel density, bitmap fonts in OpenGL need to be + * created (behind the scenes) at double the requested size. This ensures + * that they're shown at half on displays (so folks don't have to change + * their sketch code). + */ + public int getDefaultSize() { + //return defaultSize; + return size / density; + } + + + public boolean isSmooth() { + return smooth; + } + + + public boolean isStream() { + return stream; + } + + + public void setSubsetting() { + subsetting = true; + } + + + /** + * Attempt to find the native version of this font. + * (Public so that it can be used by OpenGL or other renderers.) + */ + public Object findNative() { + if (font == null) { + if (!fontSearched) { + // this font may or may not be installed + font = new Font(name, Font.PLAIN, size); + // if the ps name matches, then we're in fine shape + if (!font.getPSName().equals(psname)) { + // on osx java 1.4 (not 1.3.. ugh), you can specify the ps name + // of the font, so try that in case this .vlw font was created on pc + // and the name is different, but the ps name is found on the + // java 1.4 mac that's currently running this sketch. + font = new Font(psname, Font.PLAIN, size); + } + // check again, and if still bad, screw em + if (!font.getPSName().equals(psname)) { + font = null; + } + fontSearched = true; + } + } + return font; + } + + + public Glyph getGlyph(char c) { + int index = index(c); + return (index == -1) ? null : glyphs[index]; + } + + + /** + * Get index for the character. + * @return index into arrays or -1 if not found + */ + protected int index(char c) { + if (lazy) { + int index = indexActual(c); + if (index != -1) { + return index; + } + if (font != null && font.canDisplay(c)) { + // create the glyph + addGlyph(c); + // now where did i put that? + return indexActual(c); + + } else { + return -1; + } + + } else { + return indexActual(c); + } + } + + + protected int indexActual(char c) { + // degenerate case, but the find function will have trouble + // if there are somehow zero chars in the lookup + //if (value.length == 0) return -1; + if (glyphCount == 0) return -1; + + // quicker lookup for the ascii fellers + if (c < 128) return ascii[c]; + + // some other unicode char, hunt it out + //return index_hunt(c, 0, value.length-1); + return indexHunt(c, 0, glyphCount-1); + } + + + protected int indexHunt(int c, int start, int stop) { + int pivot = (start + stop) / 2; + + // if this is the char, then return it + if (c == glyphs[pivot].value) return pivot; + + // char doesn't exist, otherwise would have been the pivot + //if (start == stop) return -1; + if (start >= stop) return -1; + + // if it's in the lower half, continue searching that + if (c < glyphs[pivot].value) return indexHunt(c, start, pivot-1); + + // if it's in the upper half, continue there + return indexHunt(c, pivot+1, stop); + } + + + /** + * Currently un-implemented for .vlw fonts, + * but honored for layout in case subclasses use it. + */ + public float kern(char a, char b) { + return 0; + } + + + /** + * Returns the ascent of this font from the baseline. + * The value is based on a font of size 1. + */ + public float ascent() { + return ((float) ascent / (float) size); + } + + + /** + * Returns how far this font descends from the baseline. + * The value is based on a font size of 1. + */ + public float descent() { + return ((float) descent / (float) size); + } + + + /** + * Width of this character for a font of size 1. + */ + public float width(char c) { + if (c == 32) return width('i'); + + int cc = index(c); + if (cc == -1) return 0; + + return ((float) glyphs[cc].setWidth / (float) size); + } + + + ////////////////////////////////////////////////////////////// + + + public int getGlyphCount() { + return glyphCount; + } + + + public Glyph getGlyph(int i) { + return glyphs[i]; + } + + + public PShape getShape(char ch) { + return getShape(ch, 0); + } + + + public PShape getShape(char ch, float detail) { + Font font = (Font) getNative(); + if (font == null) { + throw new IllegalArgumentException("getShape() only works on fonts loaded with createFont()"); + } + + PShape s = new PShape(PShape.PATH); + + // six element array received from the Java2D path iterator + float[] iterPoints = new float[6]; + // array passed to createGylphVector + char[] textArray = new char[] { ch }; + + //Graphics2D graphics = (Graphics2D) this.getGraphics(); + //FontRenderContext frc = graphics.getFontRenderContext(); + @SuppressWarnings("deprecation") + FontRenderContext frc = + Toolkit.getDefaultToolkit().getFontMetrics(font).getFontRenderContext(); + GlyphVector gv = font.createGlyphVector(frc, textArray); + Shape shp = gv.getOutline(); + // make everything into moveto and lineto + PathIterator iter = (detail == 0) ? + shp.getPathIterator(null) : // maintain curves + shp.getPathIterator(null, detail); // convert to line segments + + int contours = 0; + //boolean outer = true; +// boolean contour = false; + while (!iter.isDone()) { + int type = iter.currentSegment(iterPoints); + switch (type) { + case PathIterator.SEG_MOVETO: // 1 point (2 vars) in textPoints +// System.out.println("moveto"); +// if (!contour) { + if (contours == 0) { + s.beginShape(); + } else { + s.beginContour(); +// contour = true; + } + contours++; + s.vertex(iterPoints[0], iterPoints[1]); + break; + + case PathIterator.SEG_LINETO: // 1 point +// System.out.println("lineto"); +// PApplet.println(PApplet.subset(iterPoints, 0, 2)); + s.vertex(iterPoints[0], iterPoints[1]); + break; + + case PathIterator.SEG_QUADTO: // 2 points +// System.out.println("quadto"); +// PApplet.println(PApplet.subset(iterPoints, 0, 4)); + s.quadraticVertex(iterPoints[0], iterPoints[1], + iterPoints[2], iterPoints[3]); + break; + + case PathIterator.SEG_CUBICTO: // 3 points +// System.out.println("cubicto"); +// PApplet.println(iterPoints); + s.quadraticVertex(iterPoints[0], iterPoints[1], + iterPoints[2], iterPoints[3], + iterPoints[4], iterPoints[5]); + break; + + case PathIterator.SEG_CLOSE: +// System.out.println("close"); + if (contours > 1) { +// contours--; +// if (contours == 0) { +//// s.endShape(); +// } else { + s.endContour(); + } + break; + } +// PApplet.println(iterPoints); + iter.next(); + } + s.endShape(CLOSE); + return s; + } + + + ////////////////////////////////////////////////////////////// + + + static final char[] EXTRA_CHARS = { + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, + 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, + 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, + 0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, + 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x00BA, + 0x00BB, 0x00BF, 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, + 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, + 0x00CE, 0x00CF, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, + 0x00D7, 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, + 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, + 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x00FF, 0x0102, 0x0103, + 0x0104, 0x0105, 0x0106, 0x0107, 0x010C, 0x010D, 0x010E, 0x010F, + 0x0110, 0x0111, 0x0118, 0x0119, 0x011A, 0x011B, 0x0131, 0x0139, + 0x013A, 0x013D, 0x013E, 0x0141, 0x0142, 0x0143, 0x0144, 0x0147, + 0x0148, 0x0150, 0x0151, 0x0152, 0x0153, 0x0154, 0x0155, 0x0158, + 0x0159, 0x015A, 0x015B, 0x015E, 0x015F, 0x0160, 0x0161, 0x0162, + 0x0163, 0x0164, 0x0165, 0x016E, 0x016F, 0x0170, 0x0171, 0x0178, + 0x0179, 0x017A, 0x017B, 0x017C, 0x017D, 0x017E, 0x0192, 0x02C6, + 0x02C7, 0x02D8, 0x02D9, 0x02DA, 0x02DB, 0x02DC, 0x02DD, 0x03A9, + 0x03C0, 0x2013, 0x2014, 0x2018, 0x2019, 0x201A, 0x201C, 0x201D, + 0x201E, 0x2020, 0x2021, 0x2022, 0x2026, 0x2030, 0x2039, 0x203A, + 0x2044, 0x20AC, 0x2122, 0x2202, 0x2206, 0x220F, 0x2211, 0x221A, + 0x221E, 0x222B, 0x2248, 0x2260, 0x2264, 0x2265, 0x25CA, 0xF8FF, + 0xFB01, 0xFB02 + }; + + + /** + * The default Processing character set. + *

        + * This is the union of the Mac Roman and Windows ANSI (CP1250) + * character sets. ISO 8859-1 Latin 1 is Unicode characters 0x80 -> 0xFF, + * and would seem a good standard, but in practice, most P5 users would + * rather have characters that they expect from their platform's fonts. + *

        + * This is more of an interim solution until a much better + * font solution can be determined. (i.e. create fonts on + * the fly from some sort of vector format). + *

        + * Not that I expect that to happen. + */ + static public char[] CHARSET; + static { + CHARSET = new char[126-33+1 + EXTRA_CHARS.length]; + int index = 0; + for (int i = 33; i <= 126; i++) { + CHARSET[index++] = (char)i; + } + for (int i = 0; i < EXTRA_CHARS.length; i++) { + CHARSET[index++] = EXTRA_CHARS[i]; + } + }; + + + /** + * ( begin auto-generated from PFont_list.xml ) + * + * Gets a list of the fonts installed on the system. The data is returned + * as a String array. This list provides the names of each font for input + * into createFont(), which allows Processing to dynamically format + * fonts. This function is meant as a tool for programming local + * applications and is not recommended for use in applets. + * + * ( end auto-generated ) + * + * @webref pfont + * @usage application + * @brief Gets a list of the fonts installed on the system + */ + static public String[] list() { + loadFonts(); + String list[] = new String[fonts.length]; + for (int i = 0; i < list.length; i++) { + list[i] = fonts[i].getName(); + } + return list; + } + + + static public void loadFonts() { + if (fonts == null) { + GraphicsEnvironment ge = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + fonts = ge.getAllFonts(); + if (PApplet.platform == PConstants.MACOSX) { + fontDifferent = new HashMap(); + for (Font font : fonts) { + // getName() returns the PostScript name on OS X 10.6 w/ Java 6. + fontDifferent.put(font.getName(), font); + //fontDifferent.put(font.getPSName(), font); + } + } + } + } + + + /** + * Starting with Java 1.5, Apple broke the ability to specify most fonts. + * This bug was filed years ago as #4769141 at bugreporter.apple.com. More: + * Bug 407. + */ + static public Font findFont(String name) { + loadFonts(); + if (PApplet.platform == PConstants.MACOSX) { + Font maybe = fontDifferent.get(name); + if (maybe != null) { + return maybe; + } +// for (int i = 0; i < fonts.length; i++) { +// if (name.equals(fonts[i].getName())) { +// return fonts[i]; +// } +// } + } + return new Font(name, Font.PLAIN, 1); + } + + + ////////////////////////////////////////////////////////////// + + + /** + * A single character, and its visage. + */ + public class Glyph { + public PImage image; + public int value; + public int height; + public int width; + public int index; + public int setWidth; + public int topExtent; + public int leftExtent; + + + public Glyph() { + index = -1; + // used when reading from a stream or for subclasses + } + + + public Glyph(DataInputStream is) throws IOException { + index = -1; + readHeader(is); + } + + + protected void readHeader(DataInputStream is) throws IOException { + value = is.readInt(); + height = is.readInt(); + width = is.readInt(); + setWidth = is.readInt(); + topExtent = is.readInt(); + leftExtent = is.readInt(); + + // pointer from a struct in the c version, ignored + is.readInt(); + + // the values for getAscent() and getDescent() from FontMetrics + // seem to be way too large.. perhaps they're the max? + // as such, use a more traditional marker for ascent/descent + if (value == 'd') { + if (ascent == 0) ascent = topExtent; + } + if (value == 'p') { + if (descent == 0) descent = -topExtent + height; + } + } + + + protected void writeHeader(DataOutputStream os) throws IOException { + os.writeInt(value); + os.writeInt(height); + os.writeInt(width); + os.writeInt(setWidth); + os.writeInt(topExtent); + os.writeInt(leftExtent); + os.writeInt(0); // padding + } + + + protected void readBitmap(DataInputStream is) throws IOException { + image = new PImage(width, height, ALPHA); + int bitmapSize = width * height; + + byte[] temp = new byte[bitmapSize]; + is.readFully(temp); + + // convert the bitmap to an alpha channel + int w = width; + int h = height; + int[] pixels = image.pixels; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + pixels[y * width + x] = temp[y*w + x] & 0xff; +// System.out.print((image.pixels[y*64+x] > 128) ? "*" : "."); + } +// System.out.println(); + } +// System.out.println(); + } + + + protected void writeBitmap(DataOutputStream os) throws IOException { + int[] pixels = image.pixels; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + os.write(pixels[y * width + x] & 0xff); + } + } + } + + + protected Glyph(char c) { + int mbox3 = size * 3; + lazyGraphics.setColor(Color.white); + lazyGraphics.fillRect(0, 0, mbox3, mbox3); + lazyGraphics.setColor(Color.black); + lazyGraphics.drawString(String.valueOf(c), size, size * 2); + + WritableRaster raster = lazyImage.getRaster(); + raster.getDataElements(0, 0, mbox3, mbox3, lazySamples); + + int minX = 1000, maxX = 0; + int minY = 1000, maxY = 0; + boolean pixelFound = false; + + for (int y = 0; y < mbox3; y++) { + for (int x = 0; x < mbox3; x++) { + int sample = lazySamples[y * mbox3 + x] & 0xff; + if (sample != 255) { + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + pixelFound = true; + } + } + } + + if (!pixelFound) { + minX = minY = 0; + maxX = maxY = 0; + // this will create a 1 pixel white (clear) character.. + // maybe better to set one to -1 so nothing is added? + } + + value = c; + height = (maxY - minY) + 1; + width = (maxX - minX) + 1; + setWidth = lazyMetrics.charWidth(c); + + // offset from vertical location of baseline + // of where the char was drawn (size*2) + topExtent = size*2 - minY; + + // offset from left of where coord was drawn + leftExtent = minX - size; + + image = new PImage(width, height, ALPHA); + int[] pixels = image.pixels; + for (int y = minY; y <= maxY; y++) { + for (int x = minX; x <= maxX; x++) { + int val = 255 - (lazySamples[y * mbox3 + x] & 0xff); + int pindex = (y - minY) * width + (x - minX); + pixels[pindex] = val; + } + } + + // replace the ascent/descent values with something.. err, decent. + if (value == 'd') { + if (ascent == 0) ascent = topExtent; + } + if (value == 'p') { + if (descent == 0) descent = -topExtent + height; + } + } + } +} diff --git a/src/main/java/processing/core/PGraphics.java b/src/main/java/processing/core/PGraphics.java new file mode 100644 index 0000000..3bc19fe --- /dev/null +++ b/src/main/java/processing/core/PGraphics.java @@ -0,0 +1,8467 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2013-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + +// Used for color conversion functions +import java.awt.Color; + +// Used for the 'image' object that's been here forever +import java.awt.Font; +import java.awt.Image; + +import java.io.File; +import java.io.InputStream; +import java.util.Map; +import java.util.HashMap; +import java.util.WeakHashMap; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; + +import processing.opengl.PGL; +import processing.opengl.PShader; + + /** + * ( begin auto-generated from PGraphics.xml ) + * + * Main graphics and rendering context, as well as the base API + * implementation for processing "core". Use this class if you need to draw + * into an off-screen graphics buffer. A PGraphics object can be + * constructed with the createGraphics() function. The + * beginDraw() and endDraw() methods (see above example) are + * necessary to set up the buffer and to finalize it. The fields and + * methods for this class are extensive. For a complete list, visit the developer's reference. + * + * ( end auto-generated ) + * + *

        Advanced

        + * Main graphics and rendering context, as well as the base API implementation. + * + *

        Subclassing and initializing PGraphics objects

        + * Starting in release 0149, subclasses of PGraphics are handled differently. + * The constructor for subclasses takes no parameters, instead a series of + * functions are called by the hosting PApplet to specify its attributes. + *
          + *
        • setParent(PApplet) - is called to specify the parent PApplet. + *
        • setPrimary(boolean) - called with true if this PGraphics will be the + * primary drawing surface used by the sketch, or false if not. + *
        • setPath(String) - called when the renderer needs a filename or output + * path, such as with the PDF or DXF renderers. + *
        • setSize(int, int) - this is called last, at which point it's safe for + * the renderer to complete its initialization routine. + *
        + * The functions were broken out because of the growing number of parameters + * such as these that might be used by a renderer, yet with the exception of + * setSize(), it's not clear which will be necessary. So while the size could + * be passed in to the constructor instead of a setSize() function, a function + * would still be needed that would notify the renderer that it was time to + * finish its initialization. Thus, setSize() simply does both. + * + *

        Know your rights: public vs. private methods

        + * Methods that are protected are often subclassed by other renderers, however + * they are not set 'public' because they shouldn't be part of the user-facing + * public API accessible from PApplet. That is, we don't want sketches calling + * textModeCheck() or vertexTexture() directly. + * + *

        Handling warnings and exceptions

        + * Methods that are unavailable generally show a warning, unless their lack of + * availability will soon cause another exception. For instance, if a method + * like getMatrix() returns null because it is unavailable, an exception will + * be thrown stating that the method is unavailable, rather than waiting for + * the NullPointerException that will occur when the sketch tries to use that + * method. As of release 0149, warnings will only be shown once, and exceptions + * have been changed to warnings where possible. + * + *

        Using xxxxImpl() for subclassing smoothness

        + * The xxxImpl() methods are generally renderer-specific handling for some + * subset if tasks for a particular function (vague enough for you?) For + * instance, imageImpl() handles drawing an image whose x/y/w/h and u/v coords + * have been specified, and screen placement (independent of imageMode) has + * been determined. There's no point in all renderers implementing the + * if (imageMode == BLAH) placement/sizing logic, so that's handled + * by PGraphics, which then calls imageImpl() once all that is figured out. + * + *

        His brother PImage

        + * PGraphics subclasses PImage so that it can be drawn and manipulated in a + * similar fashion. As such, many methods are inherited from PGraphics, + * though many are unavailable: for instance, resize() is not likely to be + * implemented; the same goes for mask(), depending on the situation. + * + *

        What's in PGraphics, what ain't

        + * For the benefit of subclasses, as much as possible has been placed inside + * PGraphics. For instance, bezier interpolation code and implementations of + * the strokeCap() method (that simply sets the strokeCap variable) are + * handled here. Features that will vary widely between renderers are located + * inside the subclasses themselves. For instance, all matrix handling code + * is per-renderer: Java 2D uses its own AffineTransform, P2D uses a PMatrix2D, + * and PGraphics3D needs to keep continually update forward and reverse + * transformations. A proper (future) OpenGL implementation will have all its + * matrix madness handled by the card. Lighting also falls under this + * category, however the base material property settings (emissive, specular, + * et al.) are handled in PGraphics because they use the standard colorMode() + * logic. Subclasses should override methods like emissiveFromCalc(), which + * is a point where a valid color has been defined internally, and can be + * applied in some manner based on the calcXxxx values. + * + *

        What's in the PGraphics documentation, what ain't

        + * Some things are noted here, some things are not. For public API, always + * refer to the reference + * on Processing.org for proper explanations. No attempt has been made to + * keep the javadoc up to date or complete. It's an enormous task for + * which we simply do not have the time. That is, it's not something that + * to be done once—it's a matter of keeping the multiple references + * synchronized (to say nothing of the translation issues), while targeting + * them for their separate audiences. Ouch. + * + * We're working right now on synchronizing the two references, so the website reference + * is generated from the javadoc comments. Yay. + * + * @webref rendering + * @instanceName graphics any object of the type PGraphics + * @usage Web & Application + * @see PApplet#createGraphics(int, int, String) + */ +public class PGraphics extends PImage implements PConstants { + +// /// Canvas object that covers rendering this graphics on screen. +// public Canvas canvas; + + // ........................................................ + + // width and height are already inherited from PImage + + +// /// width minus one (useful for many calculations) +// protected int width1; +// +// /// height minus one (useful for many calculations) +// protected int height1; + + /// width * height (useful for many calculations) + public int pixelCount; + +// /// true if smoothing is enabled (read-only) +// public boolean smooth; + + /// the anti-aliasing level for renderers that support it + public int smooth; + + + // ........................................................ + + /// true if defaults() has been called a first time + protected boolean settingsInited; + + /// true if settings should be re-applied on next beginDraw() + protected boolean reapplySettings; + + /// set to a PGraphics object being used inside a beginRaw/endRaw() block + protected PGraphics raw; + + // ........................................................ + + /** path to the file being saved for this renderer (if any) */ + protected String path; + + /** + * True if this is the main graphics context for a sketch. + * False for offscreen buffers retrieved via createGraphics(). + */ + protected boolean primaryGraphics; + +// // TODO nervous about leaving this here since it seems likely to create +// // back-references where we don't want them +// protected PSurface surface; + + // ........................................................ + + /** + * Array of hint[] items. These are hacks to get around various + * temporary workarounds inside the environment. + *

        + * Note that this array cannot be static, as a hint() may result in a + * runtime change specific to a renderer. For instance, calling + * hint(DISABLE_DEPTH_TEST) has to call glDisable() right away on an + * instance of PGraphicsOpenGL. + *

        + * The hints[] array is allocated early on because it might + * be used inside beginDraw(), allocate(), etc. + */ + protected boolean[] hints = new boolean[HINT_COUNT]; + + // ........................................................ + + /** + * Storage for renderer-specific image data. In 1.x, renderers wrote cache + * data into the image object. In 2.x, the renderer has a weak-referenced + * map that points at any of the images it has worked on already. When the + * images go out of scope, they will be properly garbage collected. + */ + protected WeakHashMap cacheMap = + new WeakHashMap(); + + + //////////////////////////////////////////////////////////// + + // Vertex fields, moved from PConstants (after 2.0a8) because they're too + // general to show up in all sketches as defined variables. + + // X, Y and Z are still stored in PConstants because of their general + // usefulness, and that X we'll always want to be 0, etc. + + static public final int R = 3; // actual rgb, after lighting + static public final int G = 4; // fill stored here, transform in place + static public final int B = 5; // TODO don't do that anymore (?) + static public final int A = 6; + + static public final int U = 7; // texture + static public final int V = 8; + + static public final int NX = 9; // normal + static public final int NY = 10; + static public final int NZ = 11; + + static public final int EDGE = 12; + + // stroke + + /** stroke argb values */ + static public final int SR = 13; + static public final int SG = 14; + static public final int SB = 15; + static public final int SA = 16; + + /** stroke weight */ + static public final int SW = 17; + + // transformations (2D and 3D) + + static public final int TX = 18; // transformed xyzw + static public final int TY = 19; + static public final int TZ = 20; + + static public final int VX = 21; // view space coords + static public final int VY = 22; + static public final int VZ = 23; + static public final int VW = 24; + + + // material properties + + // Ambient color (usually to be kept the same as diffuse) + // fill(_) sets both ambient and diffuse. + static public final int AR = 25; + static public final int AG = 26; + static public final int AB = 27; + + // Diffuse is shared with fill. + static public final int DR = 3; // TODO needs to not be shared, this is a material property + static public final int DG = 4; + static public final int DB = 5; + static public final int DA = 6; + + // specular (by default kept white) + static public final int SPR = 28; + static public final int SPG = 29; + static public final int SPB = 30; + + static public final int SHINE = 31; + + // emissive (by default kept black) + static public final int ER = 32; + static public final int EG = 33; + static public final int EB = 34; + + // has this vertex been lit yet + static public final int BEEN_LIT = 35; + + // has this vertex been assigned a normal yet + static public final int HAS_NORMAL = 36; + + static public final int VERTEX_FIELD_COUNT = 37; + + + //////////////////////////////////////////////////////////// + + // STYLE PROPERTIES + + // Also inherits imageMode() and smooth() (among others) from PImage. + + /** The current colorMode */ + public int colorMode; // = RGB; + + /** Max value for red (or hue) set by colorMode */ + public float colorModeX; // = 255; + + /** Max value for green (or saturation) set by colorMode */ + public float colorModeY; // = 255; + + /** Max value for blue (or value) set by colorMode */ + public float colorModeZ; // = 255; + + /** Max value for alpha set by colorMode */ + public float colorModeA; // = 255; + + /** True if colors are not in the range 0..1 */ + boolean colorModeScale; // = true; + + /** + * True if colorMode(RGB, 255). Defaults to true so that color() + * used as part of a field declaration will properly assign values. + */ + boolean colorModeDefault = true; + + // ........................................................ + + // Tint color for images + + /** + * True if tint() is enabled (read-only). + * + * Using tint/tintColor seems a better option for naming than + * tintEnabled/tint because the latter seems ugly, even though + * g.tint as the actual color seems a little more intuitive, + * it's just that g.tintEnabled is even more unintuitive. + * Same goes for fill and stroke, et al. + */ + public boolean tint; + + /** tint that was last set (read-only) */ + public int tintColor; + + protected boolean tintAlpha; + protected float tintR, tintG, tintB, tintA; + protected int tintRi, tintGi, tintBi, tintAi; + + // ........................................................ + + // Fill color + + /** true if fill() is enabled, (read-only) */ + public boolean fill; + + /** fill that was last set (read-only) */ + public int fillColor = 0xffFFFFFF; + + protected boolean fillAlpha; + protected float fillR, fillG, fillB, fillA; + protected int fillRi, fillGi, fillBi, fillAi; + + // ........................................................ + + // Stroke color + + /** true if stroke() is enabled, (read-only) */ + public boolean stroke; + + /** stroke that was last set (read-only) */ + public int strokeColor = 0xff000000; + + protected boolean strokeAlpha; + protected float strokeR, strokeG, strokeB, strokeA; + protected int strokeRi, strokeGi, strokeBi, strokeAi; + + // ........................................................ + + // Additional stroke properties + + static protected final float DEFAULT_STROKE_WEIGHT = 1; + static protected final int DEFAULT_STROKE_JOIN = MITER; + static protected final int DEFAULT_STROKE_CAP = ROUND; + + /** + * Last value set by strokeWeight() (read-only). This has a default + * setting, rather than fighting with renderers about whether that + * renderer supports thick lines. + */ + public float strokeWeight = DEFAULT_STROKE_WEIGHT; + + /** + * Set by strokeJoin() (read-only). This has a default setting + * so that strokeJoin() need not be called by defaults, + * because subclasses may not implement it (i.e. PGraphicsGL) + */ + public int strokeJoin = DEFAULT_STROKE_JOIN; + + /** + * Set by strokeCap() (read-only). This has a default setting + * so that strokeCap() need not be called by defaults, + * because subclasses may not implement it (i.e. PGraphicsGL) + */ + public int strokeCap = DEFAULT_STROKE_CAP; + + // ........................................................ + + // Shape placement properties + + // imageMode() is inherited from PImage + + /** The current rect mode (read-only) */ + public int rectMode; + + /** The current ellipse mode (read-only) */ + public int ellipseMode; + + /** The current shape alignment mode (read-only) */ + public int shapeMode; + + /** The current image alignment (read-only) */ + public int imageMode = CORNER; + + // ........................................................ + + // Text and font properties + + /** The current text font (read-only) */ + public PFont textFont; + + /** The current text align (read-only) */ + public int textAlign = LEFT; + + /** The current vertical text alignment (read-only) */ + public int textAlignY = BASELINE; + + /** The current text mode (read-only) */ + public int textMode = MODEL; + + /** The current text size (read-only) */ + public float textSize; + + /** The current text leading (read-only) */ + public float textLeading; + + static final protected String ERROR_TEXTFONT_NULL_PFONT = + "A null PFont was passed to textFont()"; + + // ........................................................ + + // Material properties + +// PMaterial material; +// PMaterial[] materialStack; +// int materialStackPointer; + + public int ambientColor; + public float ambientR, ambientG, ambientB; + public boolean setAmbient; + + public int specularColor; + public float specularR, specularG, specularB; + + public int emissiveColor; + public float emissiveR, emissiveG, emissiveB; + + public float shininess; + + + // Style stack + + static final int STYLE_STACK_DEPTH = 64; + PStyle[] styleStack = new PStyle[STYLE_STACK_DEPTH]; + int styleStackDepth; + + + //////////////////////////////////////////////////////////// + + + /** Last background color that was set, zero if an image */ + public int backgroundColor = 0xffCCCCCC; + + protected boolean backgroundAlpha; + protected float backgroundR, backgroundG, backgroundB, backgroundA; + protected int backgroundRi, backgroundGi, backgroundBi, backgroundAi; + + static final protected String ERROR_BACKGROUND_IMAGE_SIZE = + "background image must be the same size as your application"; + static final protected String ERROR_BACKGROUND_IMAGE_FORMAT = + "background images should be RGB or ARGB"; + + + /** The current blending mode. */ + protected int blendMode; + + + // ........................................................ + + /** + * Current model-view matrix transformation of the form m[row][column], + * which is a "column vector" (as opposed to "row vector") matrix. + */ +// PMatrix matrix; +// public float m00, m01, m02, m03; +// public float m10, m11, m12, m13; +// public float m20, m21, m22, m23; +// public float m30, m31, m32, m33; + +// static final int MATRIX_STACK_DEPTH = 32; +// float[][] matrixStack = new float[MATRIX_STACK_DEPTH][16]; +// float[][] matrixInvStack = new float[MATRIX_STACK_DEPTH][16]; +// int matrixStackDepth; + + static final protected int MATRIX_STACK_DEPTH = 32; + + static final protected String ERROR_PUSHMATRIX_OVERFLOW = + "Too many calls to pushMatrix()."; + static final protected String ERROR_PUSHMATRIX_UNDERFLOW = + "Too many calls to popMatrix(), and not enough to pushMatrix()."; + + + // ........................................................ + + /** + * Java AWT Image object associated with this renderer. For the 1.0 version + * of P2D and P3D, this was associated with their MemoryImageSource. + * For PGraphicsJava2D, it will be the offscreen drawing buffer. + */ + public Image image; + + /** Surface object that we're talking to */ + protected PSurface surface; + + // ........................................................ + + // internal color for setting/calculating + protected float calcR, calcG, calcB, calcA; + protected int calcRi, calcGi, calcBi, calcAi; + protected int calcColor; + protected boolean calcAlpha; + + /** The last RGB value converted to HSB */ + int cacheHsbKey; + /** Result of the last conversion to HSB */ + float[] cacheHsbValue = new float[3]; + + // ........................................................ + + /** + * Type of shape passed to beginShape(), + * zero if no shape is currently being drawn. + */ + protected int shape; + + // vertices + public static final int DEFAULT_VERTICES = 512; + protected float vertices[][] = + new float[DEFAULT_VERTICES][VERTEX_FIELD_COUNT]; + protected int vertexCount; // total number of vertices + + // ........................................................ + + protected boolean bezierInited = false; + public int bezierDetail = 20; + + // used by both curve and bezier, so just init here + protected PMatrix3D bezierBasisMatrix = + new PMatrix3D(-1, 3, -3, 1, + 3, -6, 3, 0, + -3, 3, 0, 0, + 1, 0, 0, 0); + + //protected PMatrix3D bezierForwardMatrix; + protected PMatrix3D bezierDrawMatrix; + + // ........................................................ + + protected boolean curveInited = false; + public int curveDetail = 20; + public float curveTightness = 0; + // catmull-rom basis matrix, perhaps with optional s parameter + protected PMatrix3D curveBasisMatrix; + protected PMatrix3D curveDrawMatrix; + + protected PMatrix3D bezierBasisInverse; + protected PMatrix3D curveToBezierMatrix; + + // ........................................................ + + // spline vertices + + protected float curveVertices[][]; + protected int curveVertexCount; + + // ........................................................ + + // precalculate sin/cos lookup tables [toxi] + // circle resolution is determined from the actual used radii + // passed to ellipse() method. this will automatically take any + // scale transformations into account too + + // [toxi 031031] + // changed table's precision to 0.5 degree steps + // introduced new vars for more flexible code + static final protected float sinLUT[]; + static final protected float cosLUT[]; + static final protected float SINCOS_PRECISION = 0.5f; + static final protected int SINCOS_LENGTH = (int) (360f / SINCOS_PRECISION); + static { + sinLUT = new float[SINCOS_LENGTH]; + cosLUT = new float[SINCOS_LENGTH]; + for (int i = 0; i < SINCOS_LENGTH; i++) { + sinLUT[i] = (float) Math.sin(i * DEG_TO_RAD * SINCOS_PRECISION); + cosLUT[i] = (float) Math.cos(i * DEG_TO_RAD * SINCOS_PRECISION); + } + } + + // ........................................................ + + /** The current font if a Java version of it is installed */ + //protected Font textFontNative; + + /** Metrics for the current native Java font */ + //protected FontMetrics textFontNativeMetrics; + +// /** Last text position, because text often mixed on lines together */ +// protected float textX, textY, textZ; + + /** + * Internal buffer used by the text() functions + * because the String object is slow + */ + protected char[] textBuffer = new char[8 * 1024]; + protected char[] textWidthBuffer = new char[8 * 1024]; + + protected int textBreakCount; + protected int[] textBreakStart; + protected int[] textBreakStop; + + // ........................................................ + + public boolean edge = true; + + // ........................................................ + + /// normal calculated per triangle + static protected final int NORMAL_MODE_AUTO = 0; + /// one normal manually specified per shape + static protected final int NORMAL_MODE_SHAPE = 1; + /// normals specified for each shape vertex + static protected final int NORMAL_MODE_VERTEX = 2; + + /// Current mode for normals, one of AUTO, SHAPE, or VERTEX + protected int normalMode; + + /// Keep track of how many calls to normal, to determine the mode. + //protected int normalCount; + + protected boolean autoNormal; + + /** Current normal vector. */ + public float normalX, normalY, normalZ; + + // ........................................................ + + /** + * Sets whether texture coordinates passed to + * vertex() calls will be based on coordinates that are + * based on the IMAGE or NORMALIZED. + */ + public int textureMode = IMAGE; + + /** + * Current horizontal coordinate for texture, will always + * be between 0 and 1, even if using textureMode(IMAGE). + */ + public float textureU; + + /** Current vertical coordinate for texture, see above. */ + public float textureV; + + /** Current image being used as a texture */ + public PImage textureImage; + + // ........................................................ + + // [toxi031031] new & faster sphere code w/ support flexible resolutions + // will be set by sphereDetail() or 1st call to sphere() + protected float sphereX[], sphereY[], sphereZ[]; + + /// Number of U steps (aka "theta") around longitudinally spanning 2*pi + public int sphereDetailU = 0; + /// Number of V steps (aka "phi") along latitudinally top-to-bottom spanning pi + public int sphereDetailV = 0; + + + ////////////////////////////////////////////////////////////// + + // INTERNAL + + // Most renderers will only override the default implementation of one or + // two of the setXxxx() methods, so they're broken out here since the + // default implementations for each are simple, obvious, and common. + // They're also separate to avoid a monolithic and fragile constructor. + + + public PGraphics() { + // In 3.1.2, giving up on the async image saving as the default + hints[DISABLE_ASYNC_SAVEFRAME] = true; + } + + + public void setParent(PApplet parent) { // ignore + this.parent = parent; + + // Some renderers (OpenGL) need to know what smoothing level will be used + // before the rendering surface is even created. + smooth = parent.sketchSmooth(); + pixelDensity = parent.sketchPixelDensity(); + } + + + /** + * Set (or unset) this as the main drawing surface. Meaning that it can + * safely be set to opaque (and given a default gray background), or anything + * else that goes along with that. + */ + public void setPrimary(boolean primary) { // ignore + this.primaryGraphics = primary; + + // base images must be opaque (for performance and general + // headache reasons.. argh, a semi-transparent opengl surface?) + // use createGraphics() if you want a transparent surface. + if (primaryGraphics) { + format = RGB; + } + } + + + public void setPath(String path) { // ignore + this.path = path; + } + + +// public void setQuality(int samples) { // ignore +// this.quality = samples; +// } + + + /** + * The final step in setting up a renderer, set its size of this renderer. + * This was formerly handled by the constructor, but instead it's been broken + * out so that setParent/setPrimary/setPath can be handled differently. + * + * Important: this is ignored by the Methods task because otherwise it will + * override setSize() in PApplet/Applet/Component, which will 1) not call + * super.setSize(), and 2) will cause the renderer to be resized from the + * event thread (EDT), causing a nasty crash as it collides with the + * animation thread. + */ + public void setSize(int w, int h) { // ignore + width = w; + height = h; + + /** {@link PImage.pixelFactor} set in {@link PImage#PImage()} */ + pixelWidth = width * pixelDensity; + pixelHeight = height * pixelDensity; + +// if (surface != null) { +// allocate(); +// } +// reapplySettings(); + reapplySettings = true; + } + + +// public void setSmooth(int level) { +// this.smooth = level; +// } + + +// /** +// * Allocate memory or an image buffer for this renderer. +// */ +// protected void allocate() { } + + + /** + * Handle any takedown for this graphics context. + *

        + * This is called when a sketch is shut down and this renderer was + * specified using the size() command, or inside endRecord() and + * endRaw(), in order to shut things off. + */ + public void dispose() { // ignore + if (primaryGraphics && asyncImageSaver != null) { + asyncImageSaver.dispose(); + asyncImageSaver = null; + } + } + + + public PSurface createSurface() { // ignore + return surface = new PSurfaceNone(this); + } + + + + ////////////////////////////////////////////////////////////// + + // IMAGE METADATA FOR THIS RENDERER + + /** + * Store data of some kind for the renderer that requires extra metadata of + * some kind. Usually this is a renderer-specific representation of the + * image data, for instance a BufferedImage with tint() settings applied for + * PGraphicsJava2D, or resized image data and OpenGL texture indices for + * PGraphicsOpenGL. + * @param image The image to be stored + * @param storage The metadata required by the renderer + */ + public void setCache(PImage image, Object storage) { // ignore + cacheMap.put(image, storage); + } + + + /** + * Get cache storage data for the specified renderer. Because each renderer + * will cache data in different formats, it's necessary to store cache data + * keyed by the renderer object. Otherwise, attempting to draw the same + * image to both a PGraphicsJava2D and a PGraphicsOpenGL will cause errors. + * @return metadata stored for the specified renderer + */ + public Object getCache(PImage image) { // ignore + return cacheMap.get(image); + } + + + /** + * Remove information associated with this renderer from the cache, if any. + * @param image The image whose cache data should be removed + */ + public void removeCache(PImage image) { // ignore + cacheMap.remove(image); + } + + + + ////////////////////////////////////////////////////////////// + + // FRAME + + +// /** +// * Some renderers have requirements re: when they are ready to draw. +// */ +// public boolean canDraw() { // ignore +// return true; +// } + + + // removing because renderers will have their own animation threads and + // can handle this however they wish +// /** +// * Try to draw, or put a draw request on the queue. +// */ +// public void requestDraw() { // ignore +// parent.handleDraw(); +// } + + + /** + * ( begin auto-generated from PGraphics_beginDraw.xml ) + * + * Sets the default properties for a PGraphics object. It should be called + * before anything is drawn into the object. + * + * ( end auto-generated ) + *

        Advanced

        + * When creating your own PGraphics, you should call this before + * drawing anything. + * + * @webref pgraphics:method + * @brief Sets the default properties for a PGraphics object + */ + public void beginDraw() { // ignore + } + + + /** + * ( begin auto-generated from PGraphics_endDraw.xml ) + * + * Finalizes the rendering of a PGraphics object so that it can be shown on screen. + * + * ( end auto-generated ) + *

        Advanced

        + *

        + * When creating your own PGraphics, you should call this when + * you're finished drawing. + * + * @webref pgraphics:method + * @brief Finalizes the rendering of a PGraphics object + */ + public void endDraw() { // ignore + } + + + public PGL beginPGL() { + showMethodWarning("beginGL"); + return null; + } + + + public void endPGL() { + showMethodWarning("endGL"); + } + + + public void flush() { + // no-op, mostly for P3D to write sorted stuff + } + + + protected void checkSettings() { + if (!settingsInited) defaultSettings(); + if (reapplySettings) reapplySettings(); + } + + + /** + * Set engine's default values. This has to be called by PApplet, + * somewhere inside setup() or draw() because it talks to the + * graphics buffer, meaning that for subclasses like OpenGL, there + * needs to be a valid graphics context to mess with otherwise + * you'll get some good crashing action. + * + * This is currently called by checkSettings(), during beginDraw(). + */ + protected void defaultSettings() { // ignore +// System.out.println("PGraphics.defaultSettings() " + width + " " + height); + +// //smooth(); // 2.0a5 +// if (quality > 0) { // 2.0a5 +// smooth(); +// } else { +// noSmooth(); +// } + + colorMode(RGB, 255); + fill(255); + stroke(0); + + // as of 0178, no longer relying on local versions of the variables + // being set, because subclasses may need to take extra action. + strokeWeight(DEFAULT_STROKE_WEIGHT); + strokeJoin(DEFAULT_STROKE_JOIN); + strokeCap(DEFAULT_STROKE_CAP); + + // init shape stuff + shape = 0; + + rectMode(CORNER); + ellipseMode(DIAMETER); + + autoNormal = true; + + // no current font + textFont = null; + textSize = 12; + textLeading = 14; + textAlign = LEFT; + textMode = MODEL; + + // if this fella is associated with an applet, then clear its background. + // if it's been created by someone else through createGraphics, + // they have to call background() themselves, otherwise everything gets + // a gray background (when just a transparent surface or an empty pdf + // is what's desired). + // this background() call is for the Java 2D and OpenGL renderers. + if (primaryGraphics) { + //System.out.println("main drawing surface bg " + getClass().getName()); + background(backgroundColor); + } + + blendMode(BLEND); + + settingsInited = true; + // defaultSettings() overlaps reapplySettings(), don't do both + reapplySettings = false; + } + + + /** + * Re-apply current settings. Some methods, such as textFont(), require that + * their methods be called (rather than simply setting the textFont variable) + * because they affect the graphics context, or they require parameters from + * the context (e.g. getting native fonts for text). + * + * This will only be called from an allocate(), which is only called from + * size(), which is safely called from inside beginDraw(). And it cannot be + * called before defaultSettings(), so we should be safe. + */ + protected void reapplySettings() { + // This might be called by allocate... So if beginDraw() has never run, + // we don't want to reapply here, we actually just need to let + // defaultSettings() get called a little from inside beginDraw(). + if (!settingsInited) return; // if this is the initial setup, no need to reapply + + colorMode(colorMode, colorModeX, colorModeY, colorModeZ); + if (fill) { +// PApplet.println(" fill " + PApplet.hex(fillColor)); + fill(fillColor); + } else { + noFill(); + } + if (stroke) { + stroke(strokeColor); + + // The if() statements should be handled inside the functions, + // otherwise an actual reset/revert won't work properly. + //if (strokeWeight != DEFAULT_STROKE_WEIGHT) { + strokeWeight(strokeWeight); + //} +// if (strokeCap != DEFAULT_STROKE_CAP) { + strokeCap(strokeCap); +// } +// if (strokeJoin != DEFAULT_STROKE_JOIN) { + strokeJoin(strokeJoin); +// } + } else { + noStroke(); + } + if (tint) { + tint(tintColor); + } else { + noTint(); + } +// if (smooth) { +// smooth(); +// } else { +// // Don't bother setting this, cuz it'll anger P3D. +// noSmooth(); +// } + if (textFont != null) { +// System.out.println(" textFont in reapply is " + textFont); + // textFont() resets the leading, so save it in case it's changed + float saveLeading = textLeading; + textFont(textFont, textSize); + textLeading(saveLeading); + } + textMode(textMode); + textAlign(textAlign, textAlignY); + background(backgroundColor); + + blendMode(blendMode); + + reapplySettings = false; + } + + // inherit from PImage + //public void resize(int wide, int high){ } + + ////////////////////////////////////////////////////////////// + + // HINTS + + /** + * ( begin auto-generated from hint.xml ) + * + * Set various hints and hacks for the renderer. This is used to handle + * obscure rendering features that cannot be implemented in a consistent + * manner across renderers. Many options will often graduate to standard + * features instead of hints over time. + *

        + * hint(ENABLE_OPENGL_4X_SMOOTH) - Enable 4x anti-aliasing for P3D. This + * can help force anti-aliasing if it has not been enabled by the user. On + * some graphics cards, this can also be set by the graphics driver's + * control panel, however not all cards make this available. This hint must + * be called immediately after the size() command because it resets the + * renderer, obliterating any settings and anything drawn (and like size(), + * re-running the code that came before it again). + *

        + * hint(DISABLE_OPENGL_2X_SMOOTH) - In Processing 1.0, Processing always + * enables 2x smoothing when the P3D renderer is used. This hint disables + * the default 2x smoothing and returns the smoothing behavior found in + * earlier releases, where smooth() and noSmooth() could be used to enable + * and disable smoothing, though the quality was inferior. + *

        + * hint(ENABLE_NATIVE_FONTS) - Use the native version fonts when they are + * installed, rather than the bitmapped version from a .vlw file. This is + * useful with the default (or JAVA2D) renderer setting, as it will improve + * font rendering speed. This is not enabled by default, because it can be + * misleading while testing because the type will look great on your + * machine (because you have the font installed) but lousy on others' + * machines if the identical font is unavailable. This option can only be + * set per-sketch, and must be called before any use of textFont(). + *

        + * hint(DISABLE_DEPTH_TEST) - Disable the zbuffer, allowing you to draw on + * top of everything at will. When depth testing is disabled, items will be + * drawn to the screen sequentially, like a painting. This hint is most + * often used to draw in 3D, then draw in 2D on top of it (for instance, to + * draw GUI controls in 2D on top of a 3D interface). Starting in release + * 0149, this will also clear the depth buffer. Restore the default with + * hint(ENABLE_DEPTH_TEST), but note that with the depth buffer cleared, + * any 3D drawing that happens later in draw() will ignore existing shapes + * on the screen. + *

        + * hint(ENABLE_DEPTH_SORT) - Enable primitive z-sorting of triangles and + * lines in P3D and OPENGL. This can slow performance considerably, and the + * algorithm is not yet perfect. Restore the default with hint(DISABLE_DEPTH_SORT). + *

        + * hint(DISABLE_OPENGL_ERROR_REPORT) - Speeds up the P3D renderer setting + * by not checking for errors while running. Undo with hint(ENABLE_OPENGL_ERROR_REPORT). + *

        + * hint(ENABLE_BUFFER_READING) - Depth and stencil buffers in P2D/P3D will be + * downsampled to make PGL#readPixels work with multisampling. Enabling this + * introduces some overhead, so if you experience bad performance, disable + * multisampling with noSmooth() instead. This hint is not intended to be + * enabled and disabled repeatedely, so call this once in setup() or after + * creating your PGraphics2D/3D. You can restore the default with + * hint(DISABLE_BUFFER_READING) if you don't plan to read depth from + * this PGraphics anymore. + *

        + * hint(ENABLE_KEY_REPEAT) - Auto-repeating key events are discarded + * by default (works only in P2D/P3D); use this hint to get all the key events + * (including auto-repeated). Call hint(DISABLE_KEY_REPEAT) to get events + * only when the key goes physically up or down. + *

        + * hint(DISABLE_ASYNC_SAVEFRAME) - P2D/P3D only - save() and saveFrame() + * will not use separate threads for saving and will block until the image + * is written to the drive. This was the default behavior in 3.0b7 and before. + * To enable, call hint(ENABLE_ASYNC_SAVEFRAME). + *

        + * As of release 0149, unhint() has been removed in favor of adding + * additional ENABLE/DISABLE constants to reset the default behavior. This + * prevents the double negatives, and also reinforces which hints can be + * enabled or disabled. + * + * ( end auto-generated ) + * + * @webref rendering + * @param which name of the hint to be enabled or disabled + * @see PGraphics + * @see PApplet#createGraphics(int, int, String, String) + * @see PApplet#size(int, int) + */ + @SuppressWarnings("deprecation") + public void hint(int which) { + if (which == ENABLE_NATIVE_FONTS || + which == DISABLE_NATIVE_FONTS) { + showWarning("hint(ENABLE_NATIVE_FONTS) no longer supported. " + + "Use createFont() instead."); + } + if (which == ENABLE_KEY_REPEAT) { + parent.keyRepeatEnabled = true; + } else if (which == DISABLE_KEY_REPEAT) { + parent.keyRepeatEnabled = false; + } + if (which > 0) { + hints[which] = true; + } else { + hints[-which] = false; + } + } + + + ////////////////////////////////////////////////////////////// + + // VERTEX SHAPES + + /** + * Start a new shape of type POLYGON + */ + public void beginShape() { + beginShape(POLYGON); + } + + + /** + * ( begin auto-generated from beginShape.xml ) + * + * Using the beginShape() and endShape() functions allow + * creating more complex forms. beginShape() begins recording + * vertices for a shape and endShape() stops recording. The value of + * the MODE parameter tells it which types of shapes to create from + * the provided vertices. With no mode specified, the shape can be any + * irregular polygon. The parameters available for beginShape() are POINTS, + * LINES, TRIANGLES, TRIANGLE_FAN, TRIANGLE_STRIP, QUADS, and QUAD_STRIP. + * After calling the beginShape() function, a series of + * vertex() commands must follow. To stop drawing the shape, call + * endShape(). The vertex() function with two parameters + * specifies a position in 2D and the vertex() function with three + * parameters specifies a position in 3D. Each shape will be outlined with + * the current stroke color and filled with the fill color. + *

        + * Transformations such as translate(), rotate(), and + * scale() do not work within beginShape(). It is also not + * possible to use other shapes, such as ellipse() or rect() + * within beginShape(). + *

        + * The P3D renderer settings allow stroke() and fill() + * settings to be altered per-vertex, however the default P2D renderer does + * not. Settings such as strokeWeight(), strokeCap(), and + * strokeJoin() cannot be changed while inside a + * beginShape()/endShape() block with any renderer. + * + * ( end auto-generated ) + * @webref shape:vertex + * @param kind Either POINTS, LINES, TRIANGLES, TRIANGLE_FAN, TRIANGLE_STRIP, QUADS, or QUAD_STRIP + * @see PShape + * @see PGraphics#endShape() + * @see PGraphics#vertex(float, float, float, float, float) + * @see PGraphics#curveVertex(float, float, float) + * @see PGraphics#bezierVertex(float, float, float, float, float, float, float, float, float) + */ + public void beginShape(int kind) { + shape = kind; + } + + + /** + * Sets whether the upcoming vertex is part of an edge. + * Equivalent to glEdgeFlag(), for people familiar with OpenGL. + */ + public void edge(boolean edge) { + this.edge = edge; + } + + + /** + * ( begin auto-generated from normal.xml ) + * + * Sets the current normal vector. This is for drawing three dimensional + * shapes and surfaces and specifies a vector perpendicular to the surface + * of the shape which determines how lighting affects it. Processing + * attempts to automatically assign normals to shapes, but since that's + * imperfect, this is a better option when you want more control. This + * function is identical to glNormal3f() in OpenGL. + * + * ( end auto-generated ) + * @webref lights_camera:lights + * @param nx x direction + * @param ny y direction + * @param nz z direction + * @see PGraphics#beginShape(int) + * @see PGraphics#endShape(int) + * @see PGraphics#lights() + */ + public void normal(float nx, float ny, float nz) { + normalX = nx; + normalY = ny; + normalZ = nz; + + // if drawing a shape and the normal hasn't been set yet, + // then we need to set the normals for each vertex so far + if (shape != 0) { + if (normalMode == NORMAL_MODE_AUTO) { + // One normal per begin/end shape + normalMode = NORMAL_MODE_SHAPE; + } else if (normalMode == NORMAL_MODE_SHAPE) { + // a separate normal for each vertex + normalMode = NORMAL_MODE_VERTEX; + } + } + } + + + public void attribPosition(String name, float x, float y, float z) { + showMissingWarning("attrib"); + } + + + public void attribNormal(String name, float nx, float ny, float nz) { + showMissingWarning("attrib"); + } + + + public void attribColor(String name, int color) { + showMissingWarning("attrib"); + } + + + public void attrib(String name, float... values) { + showMissingWarning("attrib"); + } + + + public void attrib(String name, int... values) { + showMissingWarning("attrib"); + } + + + public void attrib(String name, boolean... values) { + showMissingWarning("attrib"); + } + + + /** + * ( begin auto-generated from textureMode.xml ) + * + * Sets the coordinate space for texture mapping. There are two options, + * IMAGE, which refers to the actual coordinates of the image, and + * NORMAL, which refers to a normalized space of values ranging from 0 + * to 1. The default mode is IMAGE. In IMAGE, if an image is 100 x 200 + * pixels, mapping the image onto the entire size of a quad would require + * the points (0,0) (0,100) (100,200) (0,200). The same mapping in + * NORMAL_SPACE is (0,0) (0,1) (1,1) (0,1). + * + * ( end auto-generated ) + * @webref image:textures + * @param mode either IMAGE or NORMAL + * @see PGraphics#texture(PImage) + * @see PGraphics#textureWrap(int) + */ + public void textureMode(int mode) { + if (mode != IMAGE && mode != NORMAL) { + throw new RuntimeException("textureMode() only supports IMAGE and NORMAL"); + } + this.textureMode = mode; + } + + /** + * ( begin auto-generated from textureWrap.xml ) + * + * Description to come... + * + * ( end auto-generated from textureWrap.xml ) + * + * @webref image:textures + * @param wrap Either CLAMP (default) or REPEAT + * @see PGraphics#texture(PImage) + * @see PGraphics#textureMode(int) + */ + public void textureWrap(int wrap) { + showMissingWarning("textureWrap"); + } + + + /** + * ( begin auto-generated from texture.xml ) + * + * Sets a texture to be applied to vertex points. The texture() + * function must be called between beginShape() and + * endShape() and before any calls to vertex(). + *

        + * When textures are in use, the fill color is ignored. Instead, use tint() + * to specify the color of the texture as it is applied to the shape. + * + * ( end auto-generated ) + * @webref image:textures + * @param image reference to a PImage object + * @see PGraphics#textureMode(int) + * @see PGraphics#textureWrap(int) + * @see PGraphics#beginShape(int) + * @see PGraphics#endShape(int) + * @see PGraphics#vertex(float, float, float, float, float) + */ + public void texture(PImage image) { + textureImage = image; + } + + + /** + * Removes texture image for current shape. + * Needs to be called between beginShape and endShape + * + */ + public void noTexture() { + textureImage = null; + } + + + protected void vertexCheck() { + if (vertexCount == vertices.length) { + float temp[][] = new float[vertexCount << 1][VERTEX_FIELD_COUNT]; + System.arraycopy(vertices, 0, temp, 0, vertexCount); + vertices = temp; + } + } + + + public void vertex(float x, float y) { + vertexCheck(); + float[] vertex = vertices[vertexCount]; + + curveVertexCount = 0; + + vertex[X] = x; + vertex[Y] = y; + vertex[Z] = 0; + + vertex[EDGE] = edge ? 1 : 0; + +// if (fill) { +// vertex[R] = fillR; +// vertex[G] = fillG; +// vertex[B] = fillB; +// vertex[A] = fillA; +// } + boolean textured = textureImage != null; + if (fill || textured) { + if (!textured) { + vertex[R] = fillR; + vertex[G] = fillG; + vertex[B] = fillB; + vertex[A] = fillA; + } else { + if (tint) { + vertex[R] = tintR; + vertex[G] = tintG; + vertex[B] = tintB; + vertex[A] = tintA; + } else { + vertex[R] = 1; + vertex[G] = 1; + vertex[B] = 1; + vertex[A] = 1; + } + } + } + + if (stroke) { + vertex[SR] = strokeR; + vertex[SG] = strokeG; + vertex[SB] = strokeB; + vertex[SA] = strokeA; + vertex[SW] = strokeWeight; + } + + if (textured) { + vertex[U] = textureU; + vertex[V] = textureV; + } + + if (autoNormal) { + float norm2 = normalX * normalX + normalY * normalY + normalZ * normalZ; + if (norm2 < EPSILON) { + vertex[HAS_NORMAL] = 0; + } else { + if (Math.abs(norm2 - 1) > EPSILON) { + // The normal vector is not normalized. + float norm = PApplet.sqrt(norm2); + normalX /= norm; + normalY /= norm; + normalZ /= norm; + } + vertex[HAS_NORMAL] = 1; + } + } else { + vertex[HAS_NORMAL] = 1; + } + + vertexCount++; + } + + + public void vertex(float x, float y, float z) { + vertexCheck(); + float[] vertex = vertices[vertexCount]; + + // only do this if we're using an irregular (POLYGON) shape that + // will go through the triangulator. otherwise it'll do thinks like + // disappear in mathematically odd ways + // http://dev.processing.org/bugs/show_bug.cgi?id=444 + if (shape == POLYGON) { + if (vertexCount > 0) { + float pvertex[] = vertices[vertexCount-1]; + if ((Math.abs(pvertex[X] - x) < EPSILON) && + (Math.abs(pvertex[Y] - y) < EPSILON) && + (Math.abs(pvertex[Z] - z) < EPSILON)) { + // this vertex is identical, don't add it, + // because it will anger the triangulator + return; + } + } + } + + // User called vertex(), so that invalidates anything queued up for curve + // vertices. If this is internally called by curveVertexSegment, + // then curveVertexCount will be saved and restored. + curveVertexCount = 0; + + vertex[X] = x; + vertex[Y] = y; + vertex[Z] = z; + + vertex[EDGE] = edge ? 1 : 0; + + boolean textured = textureImage != null; + if (fill || textured) { + if (!textured) { + vertex[R] = fillR; + vertex[G] = fillG; + vertex[B] = fillB; + vertex[A] = fillA; + } else { + if (tint) { + vertex[R] = tintR; + vertex[G] = tintG; + vertex[B] = tintB; + vertex[A] = tintA; + } else { + vertex[R] = 1; + vertex[G] = 1; + vertex[B] = 1; + vertex[A] = 1; + } + } + + vertex[AR] = ambientR; + vertex[AG] = ambientG; + vertex[AB] = ambientB; + + vertex[SPR] = specularR; + vertex[SPG] = specularG; + vertex[SPB] = specularB; + //vertex[SPA] = specularA; + + vertex[SHINE] = shininess; + + vertex[ER] = emissiveR; + vertex[EG] = emissiveG; + vertex[EB] = emissiveB; + } + + if (stroke) { + vertex[SR] = strokeR; + vertex[SG] = strokeG; + vertex[SB] = strokeB; + vertex[SA] = strokeA; + vertex[SW] = strokeWeight; + } + + if (textured) { + vertex[U] = textureU; + vertex[V] = textureV; + } + + if (autoNormal) { + float norm2 = normalX * normalX + normalY * normalY + normalZ * normalZ; + if (norm2 < EPSILON) { + vertex[HAS_NORMAL] = 0; + } else { + if (Math.abs(norm2 - 1) > EPSILON) { + // The normal vector is not normalized. + float norm = PApplet.sqrt(norm2); + normalX /= norm; + normalY /= norm; + normalZ /= norm; + } + vertex[HAS_NORMAL] = 1; + } + } else { + vertex[HAS_NORMAL] = 1; + } + + vertex[NX] = normalX; + vertex[NY] = normalY; + vertex[NZ] = normalZ; + + vertex[BEEN_LIT] = 0; + + vertexCount++; + } + + + /** + * Used by renderer subclasses or PShape to efficiently pass in already + * formatted vertex information. + * @param v vertex parameters, as a float array of length VERTEX_FIELD_COUNT + */ + public void vertex(float[] v) { + vertexCheck(); + curveVertexCount = 0; + float[] vertex = vertices[vertexCount]; + System.arraycopy(v, 0, vertex, 0, VERTEX_FIELD_COUNT); + vertexCount++; + } + + + public void vertex(float x, float y, float u, float v) { + vertexTexture(u, v); + vertex(x, y); + } + +/** + * ( begin auto-generated from vertex.xml ) + * + * All shapes are constructed by connecting a series of vertices. + * vertex() is used to specify the vertex coordinates for points, + * lines, triangles, quads, and polygons and is used exclusively within the + * beginShape() and endShape() function.
        + *
        + * Drawing a vertex in 3D using the z parameter requires the P3D + * parameter in combination with size as shown in the above example.
        + *
        + * This function is also used to map a texture onto the geometry. The + * texture() function declares the texture to apply to the geometry + * and the u and v coordinates set define the mapping of this + * texture to the form. By default, the coordinates used for u and + * v are specified in relation to the image's size in pixels, but + * this relation can be changed with textureMode(). + * + * ( end auto-generated ) + * @webref shape:vertex + * @param x x-coordinate of the vertex + * @param y y-coordinate of the vertex + * @param z z-coordinate of the vertex + * @param u horizontal coordinate for the texture mapping + * @param v vertical coordinate for the texture mapping + * @see PGraphics#beginShape(int) + * @see PGraphics#endShape(int) + * @see PGraphics#bezierVertex(float, float, float, float, float, float, float, float, float) + * @see PGraphics#quadraticVertex(float, float, float, float, float, float) + * @see PGraphics#curveVertex(float, float, float) + * @see PGraphics#texture(PImage) + */ + public void vertex(float x, float y, float z, float u, float v) { + vertexTexture(u, v); + vertex(x, y, z); + } + + + /** + * Internal method to copy all style information for the given vertex. + * Can be overridden by subclasses to handle only properties pertinent to + * that renderer. (e.g. no need to copy the emissive color in P2D) + */ +// protected void vertexStyle() { +// } + + + /** + * Set (U, V) coords for the next vertex in the current shape. + * This is ugly as its own function, and will (almost?) always be + * coincident with a call to vertex. As of beta, this was moved to + * the protected method you see here, and called from an optional + * param of and overloaded vertex(). + *

        + * The parameters depend on the current textureMode. When using + * textureMode(IMAGE), the coordinates will be relative to the size + * of the image texture, when used with textureMode(NORMAL), + * they'll be in the range 0..1. + *

        + * Used by both PGraphics2D (for images) and PGraphics3D. + */ + protected void vertexTexture(float u, float v) { + if (textureImage == null) { + throw new RuntimeException("You must first call texture() before " + + "using u and v coordinates with vertex()"); + } + if (textureMode == IMAGE) { + u /= textureImage.width; + v /= textureImage.height; + } + + textureU = u; + textureV = v; + + if (textureU < 0) textureU = 0; + else if (textureU > 1) textureU = 1; + + if (textureV < 0) textureV = 0; + else if (textureV > 1) textureV = 1; + } + + +// /** This feature is in testing, do not use or rely upon its implementation */ +// public void breakShape() { +// showWarning("This renderer cannot currently handle concave shapes, " + +// "or shapes with holes."); +// } + + /** + * @webref shape:vertex + */ + public void beginContour() { + showMissingWarning("beginContour"); + } + + + /** + * @webref shape:vertex + */ + public void endContour() { + showMissingWarning("endContour"); + } + + + public void endShape() { + endShape(OPEN); + } + + + /** + * ( begin auto-generated from endShape.xml ) + * + * The endShape() function is the companion to beginShape() + * and may only be called after beginShape(). When endshape() + * is called, all of image data defined since the previous call to + * beginShape() is written into the image buffer. The constant CLOSE + * as the value for the MODE parameter to close the shape (to connect the + * beginning and the end). + * + * ( end auto-generated ) + * @webref shape:vertex + * @param mode use CLOSE to close the shape + * @see PShape + * @see PGraphics#beginShape(int) + */ + public void endShape(int mode) { + } + + + + ////////////////////////////////////////////////////////////// + + // SHAPE I/O + + + /** + * @webref shape + * @param filename name of file to load, can be .svg or .obj + * @see PShape + * @see PApplet#createShape() + */ + public PShape loadShape(String filename) { + return loadShape(filename, null); + } + + + /** + * @nowebref + */ + public PShape loadShape(String filename, String options) { + showMissingWarning("loadShape"); + return null; + } + + + + ////////////////////////////////////////////////////////////// + + // SHAPE CREATION + + + /** + * @webref shape + * @see PShape + * @see PShape#endShape() + * @see PApplet#loadShape(String) + */ + public PShape createShape() { + // Defaults to GEOMETRY (rather than GROUP like the default constructor) + // because that's how people will use it within a sketch. + return createShape(PShape.GEOMETRY); + } + + + // POINTS, LINES, TRIANGLES, TRIANGLE_FAN, TRIANGLE_STRIP, QUADS, QUAD_STRIP + public PShape createShape(int type) { + // If it's a PRIMITIVE, it needs the 'params' field anyway + if (type == PConstants.GROUP || + type == PShape.PATH || + type == PShape.GEOMETRY) { + return createShapeFamily(type); + } + final String msg = + "Only GROUP, PShape.PATH, and PShape.GEOMETRY work with createShape()"; + throw new IllegalArgumentException(msg); + } + + + /** Override this method to return an appropriate shape for your renderer */ + protected PShape createShapeFamily(int type) { + return new PShape(this, type); +// showMethodWarning("createShape()"); +// return null; + } + + + /** + * @param kind either POINT, LINE, TRIANGLE, QUAD, RECT, ELLIPSE, ARC, BOX, SPHERE + * @param p parameters that match the kind of shape + */ + public PShape createShape(int kind, float... p) { + int len = p.length; + + if (kind == POINT) { + if (is3D() && len != 2 && len != 3) { + throw new IllegalArgumentException("Use createShape(POINT, x, y) or createShape(POINT, x, y, z)"); + } else if (is2D() && len != 2) { + throw new IllegalArgumentException("Use createShape(POINT, x, y)"); + } + return createShapePrimitive(kind, p); + + } else if (kind == LINE) { + if (is3D() && len != 4 && len != 6) { + throw new IllegalArgumentException("Use createShape(LINE, x1, y1, x2, y2) or createShape(LINE, x1, y1, z1, x2, y2, z1)"); + } else if (is2D() && len != 4) { + throw new IllegalArgumentException("Use createShape(LINE, x1, y1, x2, y2)"); + } + return createShapePrimitive(kind, p); + + } else if (kind == TRIANGLE) { + if (len != 6) { + throw new IllegalArgumentException("Use createShape(TRIANGLE, x1, y1, x2, y2, x3, y3)"); + } + return createShapePrimitive(kind, p); + + } else if (kind == QUAD) { + if (len != 8) { + throw new IllegalArgumentException("Use createShape(QUAD, x1, y1, x2, y2, x3, y3, x4, y4)"); + } + return createShapePrimitive(kind, p); + + } else if (kind == RECT) { + if (len != 4 && len != 5 && len != 8) { + throw new IllegalArgumentException("Wrong number of parameters for createShape(RECT), see the reference"); + } + return createShapePrimitive(kind, p); + + } else if (kind == ELLIPSE) { + if (len != 4) { + throw new IllegalArgumentException("Use createShape(ELLIPSE, x, y, w, h)"); + } + return createShapePrimitive(kind, p); + + } else if (kind == ARC) { + if (len != 6 && len != 7) { + throw new IllegalArgumentException("Use createShape(ARC, x, y, w, h, start, stop) or createShape(ARC, x, y, w, h, start, stop, arcMode)"); + } + return createShapePrimitive(kind, p); + + } else if (kind == BOX) { + if (!is3D()) { + throw new IllegalArgumentException("createShape(BOX) is not supported in 2D"); + } else if (len != 1 && len != 3) { + throw new IllegalArgumentException("Use createShape(BOX, size) or createShape(BOX, width, height, depth)"); + } + return createShapePrimitive(kind, p); + + } else if (kind == SPHERE) { + if (!is3D()) { + throw new IllegalArgumentException("createShape(SPHERE) is not supported in 2D"); + } else if (len != 1) { + throw new IllegalArgumentException("Use createShape(SPHERE, radius)"); + } + return createShapePrimitive(kind, p); + } + throw new IllegalArgumentException("Unknown shape type passed to createShape()"); + } + + + /** Override this to have a custom shape object used by your renderer. */ + protected PShape createShapePrimitive(int kind, float... p) { +// showMethodWarning("createShape()"); +// return null; + return new PShape(this, kind, p); + } + + + + ////////////////////////////////////////////////////////////// + + // SHADERS + + /** + * ( begin auto-generated from loadShader.xml ) + * + * This is a new reference entry for Processing 2.0. It will be updated shortly. + * + * ( end auto-generated ) + * + * @webref rendering:shaders + * @param fragFilename name of fragment shader file + */ + public PShader loadShader(String fragFilename) { + showMissingWarning("loadShader"); + return null; + } + + + /** + * @param vertFilename name of vertex shader file + */ + public PShader loadShader(String fragFilename, String vertFilename) { + showMissingWarning("loadShader"); + return null; + } + + + /** + * ( begin auto-generated from shader.xml ) + * + * This is a new reference entry for Processing 2.0. It will be updated shortly. + * + * ( end auto-generated ) + * + * @webref rendering:shaders + * @param shader name of shader file + */ + public void shader(PShader shader) { + showMissingWarning("shader"); + } + + + /** + * @param kind type of shader, either POINTS, LINES, or TRIANGLES + */ + public void shader(PShader shader, int kind) { + showMissingWarning("shader"); + } + + + /** + * ( begin auto-generated from resetShader.xml ) + * + * This is a new reference entry for Processing 2.0. It will be updated shortly. + * + * ( end auto-generated ) + * + * @webref rendering:shaders + */ + public void resetShader() { + showMissingWarning("resetShader"); + } + + + /** + * @param kind type of shader, either POINTS, LINES, or TRIANGLES + */ + public void resetShader(int kind) { + showMissingWarning("resetShader"); + } + + + /** + * @param shader the fragment shader to apply + */ + public void filter(PShader shader) { + showMissingWarning("filter"); + } + + + + ////////////////////////////////////////////////////////////// + + // CLIPPING + + /** + * ( begin auto-generated from clip.xml ) + * + * Limits the rendering to the boundaries of a rectangle defined + * by the parameters. The boundaries are drawn based on the state + * of the imageMode() fuction, either CORNER, CORNERS, or CENTER. + * + * ( end auto-generated ) + * + * @webref rendering + * @param a x-coordinate of the rectangle, by default + * @param b y-coordinate of the rectangle, by default + * @param c width of the rectangle, by default + * @param d height of the rectangle, by default + */ + public void clip(float a, float b, float c, float d) { + if (imageMode == CORNER) { + if (c < 0) { // reset a negative width + a += c; c = -c; + } + if (d < 0) { // reset a negative height + b += d; d = -d; + } + + clipImpl(a, b, a + c, b + d); + + } else if (imageMode == CORNERS) { + if (c < a) { // reverse because x2 < x1 + float temp = a; a = c; c = temp; + } + if (d < b) { // reverse because y2 < y1 + float temp = b; b = d; d = temp; + } + + clipImpl(a, b, c, d); + + } else if (imageMode == CENTER) { + // c and d are width/height + if (c < 0) c = -c; + if (d < 0) d = -d; + float x1 = a - c/2; + float y1 = b - d/2; + + clipImpl(x1, y1, x1 + c, y1 + d); + } + } + + + protected void clipImpl(float x1, float y1, float x2, float y2) { + showMissingWarning("clip"); + } + + /** + * ( begin auto-generated from noClip.xml ) + * + * Disables the clipping previously started by the clip() function. + * + * ( end auto-generated ) + * + * @webref rendering + */ + public void noClip() { + showMissingWarning("noClip"); + } + + + + ////////////////////////////////////////////////////////////// + + // BLEND + + /** + * ( begin auto-generated from blendMode.xml ) + * + * This is a new reference entry for Processing 2.0. It will be updated shortly. + * + * ( end auto-generated ) + * + * @webref rendering + * @param mode the blending mode to use + */ + public void blendMode(int mode) { + this.blendMode = mode; + blendModeImpl(); + } + + + protected void blendModeImpl() { + if (blendMode != BLEND) { + showMissingWarning("blendMode"); + } + } + + + + ////////////////////////////////////////////////////////////// + + // CURVE/BEZIER VERTEX HANDLING + + + protected void bezierVertexCheck() { + bezierVertexCheck(shape, vertexCount); + } + + + protected void bezierVertexCheck(int shape, int vertexCount) { + if (shape == 0 || shape != POLYGON) { + throw new RuntimeException("beginShape() or beginShape(POLYGON) " + + "must be used before bezierVertex() or quadraticVertex()"); + } + if (vertexCount == 0) { + throw new RuntimeException("vertex() must be used at least once " + + "before bezierVertex() or quadraticVertex()"); + } + } + + + public void bezierVertex(float x2, float y2, + float x3, float y3, + float x4, float y4) { + bezierInitCheck(); + bezierVertexCheck(); + PMatrix3D draw = bezierDrawMatrix; + + float[] prev = vertices[vertexCount-1]; + float x1 = prev[X]; + float y1 = prev[Y]; + + float xplot1 = draw.m10*x1 + draw.m11*x2 + draw.m12*x3 + draw.m13*x4; + float xplot2 = draw.m20*x1 + draw.m21*x2 + draw.m22*x3 + draw.m23*x4; + float xplot3 = draw.m30*x1 + draw.m31*x2 + draw.m32*x3 + draw.m33*x4; + + float yplot1 = draw.m10*y1 + draw.m11*y2 + draw.m12*y3 + draw.m13*y4; + float yplot2 = draw.m20*y1 + draw.m21*y2 + draw.m22*y3 + draw.m23*y4; + float yplot3 = draw.m30*y1 + draw.m31*y2 + draw.m32*y3 + draw.m33*y4; + + for (int j = 0; j < bezierDetail; j++) { + x1 += xplot1; xplot1 += xplot2; xplot2 += xplot3; + y1 += yplot1; yplot1 += yplot2; yplot2 += yplot3; + vertex(x1, y1); + } + } + + +/** + * ( begin auto-generated from bezierVertex.xml ) + * + * Specifies vertex coordinates for Bezier curves. Each call to + * bezierVertex() defines the position of two control points and one + * anchor point of a Bezier curve, adding a new segment to a line or shape. + * The first time bezierVertex() is used within a + * beginShape() call, it must be prefaced with a call to + * vertex() to set the first anchor point. This function must be + * used between beginShape() and endShape() and only when + * there is no MODE parameter specified to beginShape(). Using the + * 3D version requires rendering with P3D (see the Environment reference + * for more information). + * + * ( end auto-generated ) + * @webref shape:vertex + * @param x2 the x-coordinate of the 1st control point + * @param y2 the y-coordinate of the 1st control point + * @param z2 the z-coordinate of the 1st control point + * @param x3 the x-coordinate of the 2nd control point + * @param y3 the y-coordinate of the 2nd control point + * @param z3 the z-coordinate of the 2nd control point + * @param x4 the x-coordinate of the anchor point + * @param y4 the y-coordinate of the anchor point + * @param z4 the z-coordinate of the anchor point + * @see PGraphics#curveVertex(float, float, float) + * @see PGraphics#vertex(float, float, float, float, float) + * @see PGraphics#quadraticVertex(float, float, float, float, float, float) + * @see PGraphics#bezier(float, float, float, float, float, float, float, float, float, float, float, float) + */ + public void bezierVertex(float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + bezierInitCheck(); + bezierVertexCheck(); + PMatrix3D draw = bezierDrawMatrix; + + float[] prev = vertices[vertexCount-1]; + float x1 = prev[X]; + float y1 = prev[Y]; + float z1 = prev[Z]; + + float xplot1 = draw.m10*x1 + draw.m11*x2 + draw.m12*x3 + draw.m13*x4; + float xplot2 = draw.m20*x1 + draw.m21*x2 + draw.m22*x3 + draw.m23*x4; + float xplot3 = draw.m30*x1 + draw.m31*x2 + draw.m32*x3 + draw.m33*x4; + + float yplot1 = draw.m10*y1 + draw.m11*y2 + draw.m12*y3 + draw.m13*y4; + float yplot2 = draw.m20*y1 + draw.m21*y2 + draw.m22*y3 + draw.m23*y4; + float yplot3 = draw.m30*y1 + draw.m31*y2 + draw.m32*y3 + draw.m33*y4; + + float zplot1 = draw.m10*z1 + draw.m11*z2 + draw.m12*z3 + draw.m13*z4; + float zplot2 = draw.m20*z1 + draw.m21*z2 + draw.m22*z3 + draw.m23*z4; + float zplot3 = draw.m30*z1 + draw.m31*z2 + draw.m32*z3 + draw.m33*z4; + + for (int j = 0; j < bezierDetail; j++) { + x1 += xplot1; xplot1 += xplot2; xplot2 += xplot3; + y1 += yplot1; yplot1 += yplot2; yplot2 += yplot3; + z1 += zplot1; zplot1 += zplot2; zplot2 += zplot3; + vertex(x1, y1, z1); + } + } + + + /** + * @webref shape:vertex + * @param cx the x-coordinate of the control point + * @param cy the y-coordinate of the control point + * @param x3 the x-coordinate of the anchor point + * @param y3 the y-coordinate of the anchor point + * @see PGraphics#curveVertex(float, float, float) + * @see PGraphics#vertex(float, float, float, float, float) + * @see PGraphics#bezierVertex(float, float, float, float, float, float) + * @see PGraphics#bezier(float, float, float, float, float, float, float, float, float, float, float, float) + */ + public void quadraticVertex(float cx, float cy, + float x3, float y3) { + float[] prev = vertices[vertexCount-1]; + float x1 = prev[X]; + float y1 = prev[Y]; + + bezierVertex(x1 + ((cx-x1)*2/3.0f), y1 + ((cy-y1)*2/3.0f), + x3 + ((cx-x3)*2/3.0f), y3 + ((cy-y3)*2/3.0f), + x3, y3); + } + + + /** + * @param cz the z-coordinate of the control point + * @param z3 the z-coordinate of the anchor point + */ + public void quadraticVertex(float cx, float cy, float cz, + float x3, float y3, float z3) { + float[] prev = vertices[vertexCount-1]; + float x1 = prev[X]; + float y1 = prev[Y]; + float z1 = prev[Z]; + + bezierVertex(x1 + ((cx-x1)*2/3.0f), y1 + ((cy-y1)*2/3.0f), z1 + ((cz-z1)*2/3.0f), + x3 + ((cx-x3)*2/3.0f), y3 + ((cy-y3)*2/3.0f), z3 + ((cz-z3)*2/3.0f), + x3, y3, z3); + } + + + protected void curveVertexCheck() { + curveVertexCheck(shape); + } + + + /** + * Perform initialization specific to curveVertex(), and handle standard + * error modes. Can be overridden by subclasses that need the flexibility. + */ + protected void curveVertexCheck(int shape) { + if (shape != POLYGON) { + throw new RuntimeException("You must use beginShape() or " + + "beginShape(POLYGON) before curveVertex()"); + } + // to improve code init time, allocate on first use. + if (curveVertices == null) { + curveVertices = new float[128][3]; + } + + if (curveVertexCount == curveVertices.length) { + // Can't use PApplet.expand() cuz it doesn't do the copy properly + float[][] temp = new float[curveVertexCount << 1][3]; + System.arraycopy(curveVertices, 0, temp, 0, curveVertexCount); + curveVertices = temp; + } + curveInitCheck(); + } + + + /** + * ( begin auto-generated from curveVertex.xml ) + * + * Specifies vertex coordinates for curves. This function may only be used + * between beginShape() and endShape() and only when there is + * no MODE parameter specified to beginShape(). The first and last + * points in a series of curveVertex() lines will be used to guide + * the beginning and end of a the curve. A minimum of four points is + * required to draw a tiny curve between the second and third points. + * Adding a fifth point with curveVertex() will draw the curve + * between the second, third, and fourth points. The curveVertex() + * function is an implementation of Catmull-Rom splines. Using the 3D + * version requires rendering with P3D (see the Environment reference for + * more information). + * + * ( end auto-generated ) + * + * @webref shape:vertex + * @param x the x-coordinate of the vertex + * @param y the y-coordinate of the vertex + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#beginShape(int) + * @see PGraphics#endShape(int) + * @see PGraphics#vertex(float, float, float, float, float) + * @see PGraphics#bezier(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#quadraticVertex(float, float, float, float, float, float) + */ + public void curveVertex(float x, float y) { + curveVertexCheck(); + float[] vertex = curveVertices[curveVertexCount]; + vertex[X] = x; + vertex[Y] = y; + curveVertexCount++; + + // draw a segment if there are enough points + if (curveVertexCount > 3) { + curveVertexSegment(curveVertices[curveVertexCount-4][X], + curveVertices[curveVertexCount-4][Y], + curveVertices[curveVertexCount-3][X], + curveVertices[curveVertexCount-3][Y], + curveVertices[curveVertexCount-2][X], + curveVertices[curveVertexCount-2][Y], + curveVertices[curveVertexCount-1][X], + curveVertices[curveVertexCount-1][Y]); + } + } + + /** + * @param z the z-coordinate of the vertex + */ + public void curveVertex(float x, float y, float z) { + curveVertexCheck(); + float[] vertex = curveVertices[curveVertexCount]; + vertex[X] = x; + vertex[Y] = y; + vertex[Z] = z; + curveVertexCount++; + + // draw a segment if there are enough points + if (curveVertexCount > 3) { + curveVertexSegment(curveVertices[curveVertexCount-4][X], + curveVertices[curveVertexCount-4][Y], + curveVertices[curveVertexCount-4][Z], + curveVertices[curveVertexCount-3][X], + curveVertices[curveVertexCount-3][Y], + curveVertices[curveVertexCount-3][Z], + curveVertices[curveVertexCount-2][X], + curveVertices[curveVertexCount-2][Y], + curveVertices[curveVertexCount-2][Z], + curveVertices[curveVertexCount-1][X], + curveVertices[curveVertexCount-1][Y], + curveVertices[curveVertexCount-1][Z]); + } + } + + + /** + * Handle emitting a specific segment of Catmull-Rom curve. This can be + * overridden by subclasses that need more efficient rendering options. + */ + protected void curveVertexSegment(float x1, float y1, + float x2, float y2, + float x3, float y3, + float x4, float y4) { + float x0 = x2; + float y0 = y2; + + PMatrix3D draw = curveDrawMatrix; + + float xplot1 = draw.m10*x1 + draw.m11*x2 + draw.m12*x3 + draw.m13*x4; + float xplot2 = draw.m20*x1 + draw.m21*x2 + draw.m22*x3 + draw.m23*x4; + float xplot3 = draw.m30*x1 + draw.m31*x2 + draw.m32*x3 + draw.m33*x4; + + float yplot1 = draw.m10*y1 + draw.m11*y2 + draw.m12*y3 + draw.m13*y4; + float yplot2 = draw.m20*y1 + draw.m21*y2 + draw.m22*y3 + draw.m23*y4; + float yplot3 = draw.m30*y1 + draw.m31*y2 + draw.m32*y3 + draw.m33*y4; + + // vertex() will reset splineVertexCount, so save it + int savedCount = curveVertexCount; + + vertex(x0, y0); + for (int j = 0; j < curveDetail; j++) { + x0 += xplot1; xplot1 += xplot2; xplot2 += xplot3; + y0 += yplot1; yplot1 += yplot2; yplot2 += yplot3; + vertex(x0, y0); + } + curveVertexCount = savedCount; + } + + + /** + * Handle emitting a specific segment of Catmull-Rom curve. This can be + * overridden by subclasses that need more efficient rendering options. + */ + protected void curveVertexSegment(float x1, float y1, float z1, + float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + float x0 = x2; + float y0 = y2; + float z0 = z2; + + PMatrix3D draw = curveDrawMatrix; + + float xplot1 = draw.m10*x1 + draw.m11*x2 + draw.m12*x3 + draw.m13*x4; + float xplot2 = draw.m20*x1 + draw.m21*x2 + draw.m22*x3 + draw.m23*x4; + float xplot3 = draw.m30*x1 + draw.m31*x2 + draw.m32*x3 + draw.m33*x4; + + float yplot1 = draw.m10*y1 + draw.m11*y2 + draw.m12*y3 + draw.m13*y4; + float yplot2 = draw.m20*y1 + draw.m21*y2 + draw.m22*y3 + draw.m23*y4; + float yplot3 = draw.m30*y1 + draw.m31*y2 + draw.m32*y3 + draw.m33*y4; + + // vertex() will reset splineVertexCount, so save it + int savedCount = curveVertexCount; + + float zplot1 = draw.m10*z1 + draw.m11*z2 + draw.m12*z3 + draw.m13*z4; + float zplot2 = draw.m20*z1 + draw.m21*z2 + draw.m22*z3 + draw.m23*z4; + float zplot3 = draw.m30*z1 + draw.m31*z2 + draw.m32*z3 + draw.m33*z4; + + vertex(x0, y0, z0); + for (int j = 0; j < curveDetail; j++) { + x0 += xplot1; xplot1 += xplot2; xplot2 += xplot3; + y0 += yplot1; yplot1 += yplot2; yplot2 += yplot3; + z0 += zplot1; zplot1 += zplot2; zplot2 += zplot3; + vertex(x0, y0, z0); + } + curveVertexCount = savedCount; + } + + + + ////////////////////////////////////////////////////////////// + + // SIMPLE SHAPES WITH ANALOGUES IN beginShape() + + + /** + * ( begin auto-generated from point.xml ) + * + * Draws a point, a coordinate in space at the dimension of one pixel. The + * first parameter is the horizontal value for the point, the second value + * is the vertical value for the point, and the optional third value is the + * depth value. Drawing this shape in 3D with the z parameter + * requires the P3D parameter in combination with size() as shown in + * the above example. + * + * ( end auto-generated ) + * + * @webref shape:2d_primitives + * @param x x-coordinate of the point + * @param y y-coordinate of the point + * @see PGraphics#stroke(int) + */ + public void point(float x, float y) { + beginShape(POINTS); + vertex(x, y); + endShape(); + } + + /** + * @param z z-coordinate of the point + */ + public void point(float x, float y, float z) { + beginShape(POINTS); + vertex(x, y, z); + endShape(); + } + + /** + * ( begin auto-generated from line.xml ) + * + * Draws a line (a direct path between two points) to the screen. The + * version of line() with four parameters draws the line in 2D. To + * color a line, use the stroke() function. A line cannot be filled, + * therefore the fill() function will not affect the color of a + * line. 2D lines are drawn with a width of one pixel by default, but this + * can be changed with the strokeWeight() function. The version with + * six parameters allows the line to be placed anywhere within XYZ space. + * Drawing this shape in 3D with the z parameter requires the P3D + * parameter in combination with size() as shown in the above example. + * + * ( end auto-generated ) + * @webref shape:2d_primitives + * @param x1 x-coordinate of the first point + * @param y1 y-coordinate of the first point + * @param x2 x-coordinate of the second point + * @param y2 y-coordinate of the second point + * @see PGraphics#strokeWeight(float) + * @see PGraphics#strokeJoin(int) + * @see PGraphics#strokeCap(int) + * @see PGraphics#beginShape() + */ + public void line(float x1, float y1, float x2, float y2) { + beginShape(LINES); + vertex(x1, y1); + vertex(x2, y2); + endShape(); + } + + /** + * @param z1 z-coordinate of the first point + * @param z2 z-coordinate of the second point + */ + public void line(float x1, float y1, float z1, + float x2, float y2, float z2) { + beginShape(LINES); + vertex(x1, y1, z1); + vertex(x2, y2, z2); + endShape(); + } + + /** + * ( begin auto-generated from triangle.xml ) + * + * A triangle is a plane created by connecting three points. The first two + * arguments specify the first point, the middle two arguments specify the + * second point, and the last two arguments specify the third point. + * + * ( end auto-generated ) + * @webref shape:2d_primitives + * @param x1 x-coordinate of the first point + * @param y1 y-coordinate of the first point + * @param x2 x-coordinate of the second point + * @param y2 y-coordinate of the second point + * @param x3 x-coordinate of the third point + * @param y3 y-coordinate of the third point + * @see PApplet#beginShape() + */ + public void triangle(float x1, float y1, float x2, float y2, + float x3, float y3) { + beginShape(TRIANGLES); + vertex(x1, y1); + vertex(x2, y2); + vertex(x3, y3); + endShape(); + } + + + /** + * ( begin auto-generated from quad.xml ) + * + * A quad is a quadrilateral, a four sided polygon. It is similar to a + * rectangle, but the angles between its edges are not constrained to + * ninety degrees. The first pair of parameters (x1,y1) sets the first + * vertex and the subsequent pairs should proceed clockwise or + * counter-clockwise around the defined shape. + * + * ( end auto-generated ) + * @webref shape:2d_primitives + * @param x1 x-coordinate of the first corner + * @param y1 y-coordinate of the first corner + * @param x2 x-coordinate of the second corner + * @param y2 y-coordinate of the second corner + * @param x3 x-coordinate of the third corner + * @param y3 y-coordinate of the third corner + * @param x4 x-coordinate of the fourth corner + * @param y4 y-coordinate of the fourth corner + */ + public void quad(float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4) { + beginShape(QUADS); + vertex(x1, y1); + vertex(x2, y2); + vertex(x3, y3); + vertex(x4, y4); + endShape(); + } + + + + ////////////////////////////////////////////////////////////// + + // RECT + + /** + * ( begin auto-generated from rectMode.xml ) + * + * Modifies the location from which rectangles draw. The default mode is + * rectMode(CORNER), which specifies the location to be the upper + * left corner of the shape and uses the third and fourth parameters of + * rect() to specify the width and height. The syntax + * rectMode(CORNERS) uses the first and second parameters of + * rect() to set the location of one corner and uses the third and + * fourth parameters to set the opposite corner. The syntax + * rectMode(CENTER) draws the image from its center point and uses + * the third and forth parameters of rect() to specify the image's + * width and height. The syntax rectMode(RADIUS) draws the image + * from its center point and uses the third and forth parameters of + * rect() to specify half of the image's width and height. The + * parameter must be written in ALL CAPS because Processing is a case + * sensitive language. Note: In version 125, the mode named CENTER_RADIUS + * was shortened to RADIUS. + * + * ( end auto-generated ) + * @webref shape:attributes + * @param mode either CORNER, CORNERS, CENTER, or RADIUS + * @see PGraphics#rect(float, float, float, float) + */ + public void rectMode(int mode) { + rectMode = mode; + } + + + /** + * ( begin auto-generated from rect.xml ) + * + * Draws a rectangle to the screen. A rectangle is a four-sided shape with + * every angle at ninety degrees. By default, the first two parameters set + * the location of the upper-left corner, the third sets the width, and the + * fourth sets the height. These parameters may be changed with the + * rectMode() function. + * + * ( end auto-generated ) + * + * @webref shape:2d_primitives + * @param a x-coordinate of the rectangle by default + * @param b y-coordinate of the rectangle by default + * @param c width of the rectangle by default + * @param d height of the rectangle by default + * @see PGraphics#rectMode(int) + * @see PGraphics#quad(float, float, float, float, float, float, float, float) + */ + public void rect(float a, float b, float c, float d) { + float hradius, vradius; + switch (rectMode) { + case CORNERS: + break; + case CORNER: + c += a; d += b; + break; + case RADIUS: + hradius = c; + vradius = d; + c = a + hradius; + d = b + vradius; + a -= hradius; + b -= vradius; + break; + case CENTER: + hradius = c / 2.0f; + vradius = d / 2.0f; + c = a + hradius; + d = b + vradius; + a -= hradius; + b -= vradius; + } + + if (a > c) { + float temp = a; a = c; c = temp; + } + + if (b > d) { + float temp = b; b = d; d = temp; + } + + rectImpl(a, b, c, d); + } + + + protected void rectImpl(float x1, float y1, float x2, float y2) { + quad(x1, y1, x2, y1, x2, y2, x1, y2); + } + + + // Still need to do a lot of work here to make it behave across renderers + // (e.g. not all renderers use the vertices array) + // Also seems to be some issues on quality here (too dense) + // http://code.google.com/p/processing/issues/detail?id=265 +// private void quadraticVertex(float cpx, float cpy, float x, float y) { +// float[] prev = vertices[vertexCount - 1]; +// float prevX = prev[X]; +// float prevY = prev[Y]; +// float cp1x = prevX + 2.0f/3.0f*(cpx - prevX); +// float cp1y = prevY + 2.0f/3.0f*(cpy - prevY); +// float cp2x = cp1x + (x - prevX)/3.0f; +// float cp2y = cp1y + (y - prevY)/3.0f; +// bezierVertex(cp1x, cp1y, cp2x, cp2y, x, y); +// } + + /** + * @param r radii for all four corners + */ + public void rect(float a, float b, float c, float d, float r) { + rect(a, b, c, d, r, r, r, r); + } + + /** + * @param tl radius for top-left corner + * @param tr radius for top-right corner + * @param br radius for bottom-right corner + * @param bl radius for bottom-left corner + */ + public void rect(float a, float b, float c, float d, + float tl, float tr, float br, float bl) { + float hradius, vradius; + switch (rectMode) { + case CORNERS: + break; + case CORNER: + c += a; d += b; + break; + case RADIUS: + hradius = c; + vradius = d; + c = a + hradius; + d = b + vradius; + a -= hradius; + b -= vradius; + break; + case CENTER: + hradius = c / 2.0f; + vradius = d / 2.0f; + c = a + hradius; + d = b + vradius; + a -= hradius; + b -= vradius; + } + + if (a > c) { + float temp = a; a = c; c = temp; + } + + if (b > d) { + float temp = b; b = d; d = temp; + } + + float maxRounding = PApplet.min((c - a) / 2, (d - b) / 2); + if (tl > maxRounding) tl = maxRounding; + if (tr > maxRounding) tr = maxRounding; + if (br > maxRounding) br = maxRounding; + if (bl > maxRounding) bl = maxRounding; + + rectImpl(a, b, c, d, tl, tr, br, bl); + } + + + protected void rectImpl(float x1, float y1, float x2, float y2, + float tl, float tr, float br, float bl) { + beginShape(); +// vertex(x1+tl, y1); + if (tr != 0) { + vertex(x2-tr, y1); + quadraticVertex(x2, y1, x2, y1+tr); + } else { + vertex(x2, y1); + } + if (br != 0) { + vertex(x2, y2-br); + quadraticVertex(x2, y2, x2-br, y2); + } else { + vertex(x2, y2); + } + if (bl != 0) { + vertex(x1+bl, y2); + quadraticVertex(x1, y2, x1, y2-bl); + } else { + vertex(x1, y2); + } + if (tl != 0) { + vertex(x1, y1+tl); + quadraticVertex(x1, y1, x1+tl, y1); + } else { + vertex(x1, y1); + } +// endShape(); + endShape(CLOSE); + } + + + + ////////////////////////////////////////////////////////////// + + // ELLIPSE AND ARC + + + /** + * ( begin auto-generated from ellipseMode.xml ) + * + * The origin of the ellipse is modified by the ellipseMode() + * function. The default configuration is ellipseMode(CENTER), which + * specifies the location of the ellipse as the center of the shape. The + * RADIUS mode is the same, but the width and height parameters to + * ellipse() specify the radius of the ellipse, rather than the + * diameter. The CORNER mode draws the shape from the upper-left + * corner of its bounding box. The CORNERS mode uses the four + * parameters to ellipse() to set two opposing corners of the + * ellipse's bounding box. The parameter must be written in ALL CAPS + * because Processing is a case-sensitive language. + * + * ( end auto-generated ) + * @webref shape:attributes + * @param mode either CENTER, RADIUS, CORNER, or CORNERS + * @see PApplet#ellipse(float, float, float, float) + * @see PApplet#arc(float, float, float, float, float, float) + */ + public void ellipseMode(int mode) { + ellipseMode = mode; + } + + + /** + * ( begin auto-generated from ellipse.xml ) + * + * Draws an ellipse (oval) in the display window. An ellipse with an equal + * width and height is a circle. The first two parameters set + * the location, the third sets the width, and the fourth sets the height. + * The origin may be changed with the ellipseMode() function. + * + * ( end auto-generated ) + * @webref shape:2d_primitives + * @param a x-coordinate of the ellipse + * @param b y-coordinate of the ellipse + * @param c width of the ellipse by default + * @param d height of the ellipse by default + * @see PApplet#ellipseMode(int) + * @see PApplet#arc(float, float, float, float, float, float) + */ + public void ellipse(float a, float b, float c, float d) { + float x = a; + float y = b; + float w = c; + float h = d; + + if (ellipseMode == CORNERS) { + w = c - a; + h = d - b; + + } else if (ellipseMode == RADIUS) { + x = a - c; + y = b - d; + w = c * 2; + h = d * 2; + + } else if (ellipseMode == DIAMETER) { + x = a - c/2f; + y = b - d/2f; + } + + if (w < 0) { // undo negative width + x += w; + w = -w; + } + + if (h < 0) { // undo negative height + y += h; + h = -h; + } + + ellipseImpl(x, y, w, h); + } + + + protected void ellipseImpl(float x, float y, float w, float h) { + } + + + /** + * ( begin auto-generated from arc.xml ) + * + * Draws an arc in the display window. Arcs are drawn along the outer edge + * of an ellipse defined by the x, y, width and + * height parameters. The origin or the arc's ellipse may be changed + * with the ellipseMode() function. The start and stop + * parameters specify the angles at which to draw the arc. + * + * ( end auto-generated ) + * @webref shape:2d_primitives + * @param a x-coordinate of the arc's ellipse + * @param b y-coordinate of the arc's ellipse + * @param c width of the arc's ellipse by default + * @param d height of the arc's ellipse by default + * @param start angle to start the arc, specified in radians + * @param stop angle to stop the arc, specified in radians + * @see PApplet#ellipse(float, float, float, float) + * @see PApplet#ellipseMode(int) + * @see PApplet#radians(float) + * @see PApplet#degrees(float) + */ + public void arc(float a, float b, float c, float d, + float start, float stop) { + arc(a, b, c, d, start, stop, 0); + } + + /* + * @param mode either OPEN, CHORD, or PIE + */ + public void arc(float a, float b, float c, float d, + float start, float stop, int mode) { + float x = a; + float y = b; + float w = c; + float h = d; + + if (ellipseMode == CORNERS) { + w = c - a; + h = d - b; + + } else if (ellipseMode == RADIUS) { + x = a - c; + y = b - d; + w = c * 2; + h = d * 2; + + } else if (ellipseMode == CENTER) { + x = a - c/2f; + y = b - d/2f; + } + + // make sure the loop will exit before starting while + if (!Float.isInfinite(start) && !Float.isInfinite(stop)) { + // ignore equal and degenerate cases + if (stop > start) { + // make sure that we're starting at a useful point + while (start < 0) { + start += TWO_PI; + stop += TWO_PI; + } + + if (stop - start > TWO_PI) { + // don't change start, it is visible in PIE mode + stop = start + TWO_PI; + } + arcImpl(x, y, w, h, start, stop, mode); + } + } + } + + +// protected void arcImpl(float x, float y, float w, float h, +// float start, float stop) { +// } + + + /** + * Start and stop are in radians, converted by the parent function. + * Note that the radians can be greater (or less) than TWO_PI. + * This is so that an arc can be drawn that crosses zero mark, + * and the user will still collect $200. + */ + protected void arcImpl(float x, float y, float w, float h, + float start, float stop, int mode) { + showMissingWarning("arc"); + } + + + ////////////////////////////////////////////////////////////// + + // BOX + + /** + * ( begin auto-generated from box.xml ) + * + * A box is an extruded rectangle. A box with equal dimension on all sides + * is a cube. + * + * ( end auto-generated ) + * + * @webref shape:3d_primitives + * @param size dimension of the box in all dimensions (creates a cube) + * @see PGraphics#sphere(float) + */ + public void box(float size) { + box(size, size, size); + } + + + /** + * @param w dimension of the box in the x-dimension + * @param h dimension of the box in the y-dimension + * @param d dimension of the box in the z-dimension + */ + public void box(float w, float h, float d) { + float x1 = -w/2f; float x2 = w/2f; + float y1 = -h/2f; float y2 = h/2f; + float z1 = -d/2f; float z2 = d/2f; + + // TODO not the least bit efficient, it even redraws lines + // along the vertices. ugly ugly ugly! + + beginShape(QUADS); + + // front + normal(0, 0, 1); + vertex(x1, y1, z1); + vertex(x2, y1, z1); + vertex(x2, y2, z1); + vertex(x1, y2, z1); + + // right + normal(1, 0, 0); + vertex(x2, y1, z1); + vertex(x2, y1, z2); + vertex(x2, y2, z2); + vertex(x2, y2, z1); + + // back + normal(0, 0, -1); + vertex(x2, y1, z2); + vertex(x1, y1, z2); + vertex(x1, y2, z2); + vertex(x2, y2, z2); + + // left + normal(-1, 0, 0); + vertex(x1, y1, z2); + vertex(x1, y1, z1); + vertex(x1, y2, z1); + vertex(x1, y2, z2); + + // top + normal(0, 1, 0); + vertex(x1, y1, z2); + vertex(x2, y1, z2); + vertex(x2, y1, z1); + vertex(x1, y1, z1); + + // bottom + normal(0, -1, 0); + vertex(x1, y2, z1); + vertex(x2, y2, z1); + vertex(x2, y2, z2); + vertex(x1, y2, z2); + + endShape(); + } + + + + ////////////////////////////////////////////////////////////// + + // SPHERE + + /** + * ( begin auto-generated from sphereDetail.xml ) + * + * Controls the detail used to render a sphere by adjusting the number of + * vertices of the sphere mesh. The default resolution is 30, which creates + * a fairly detailed sphere definition with vertices every 360/30 = 12 + * degrees. If you're going to render a great number of spheres per frame, + * it is advised to reduce the level of detail using this function. The + * setting stays active until sphereDetail() is called again with a + * new parameter and so should not be called prior to every + * sphere() statement, unless you wish to render spheres with + * different settings, e.g. using less detail for smaller spheres or ones + * further away from the camera. To control the detail of the horizontal + * and vertical resolution independently, use the version of the functions + * with two parameters. + * + * ( end auto-generated ) + * + *

        Advanced

        + * Code for sphereDetail() submitted by toxi [031031]. + * Code for enhanced u/v version from davbol [080801]. + * + * @param res number of segments (minimum 3) used per full circle revolution + * @webref shape:3d_primitives + * @see PGraphics#sphere(float) + */ + public void sphereDetail(int res) { + sphereDetail(res, res); + } + + + /** + * @param ures number of segments used longitudinally per full circle revolutoin + * @param vres number of segments used latitudinally from top to bottom + */ + public void sphereDetail(int ures, int vres) { + if (ures < 3) ures = 3; // force a minimum res + if (vres < 2) vres = 2; // force a minimum res + if ((ures == sphereDetailU) && (vres == sphereDetailV)) return; + + float delta = (float)SINCOS_LENGTH/ures; + float[] cx = new float[ures]; + float[] cz = new float[ures]; + // calc unit circle in XZ plane + for (int i = 0; i < ures; i++) { + cx[i] = cosLUT[(int) (i*delta) % SINCOS_LENGTH]; + cz[i] = sinLUT[(int) (i*delta) % SINCOS_LENGTH]; + } + // computing vertexlist + // vertexlist starts at south pole + int vertCount = ures * (vres-1) + 2; + int currVert = 0; + + // re-init arrays to store vertices + sphereX = new float[vertCount]; + sphereY = new float[vertCount]; + sphereZ = new float[vertCount]; + + float angle_step = (SINCOS_LENGTH*0.5f)/vres; + float angle = angle_step; + + // step along Y axis + for (int i = 1; i < vres; i++) { + float curradius = sinLUT[(int) angle % SINCOS_LENGTH]; + float currY = cosLUT[(int) angle % SINCOS_LENGTH]; + for (int j = 0; j < ures; j++) { + sphereX[currVert] = cx[j] * curradius; + sphereY[currVert] = currY; + sphereZ[currVert++] = cz[j] * curradius; + } + angle += angle_step; + } + sphereDetailU = ures; + sphereDetailV = vres; + } + + + /** + * ( begin auto-generated from sphere.xml ) + * + * A sphere is a hollow ball made from tessellated triangles. + * + * ( end auto-generated ) + * + *

        Advanced

        + *

        + * Implementation notes: + *

        + * cache all the points of the sphere in a static array + * top and bottom are just a bunch of triangles that land + * in the center point + *

        + * sphere is a series of concentric circles who radii vary + * along the shape, based on, er.. cos or something + *

        +   * [toxi 031031] new sphere code. removed all multiplies with
        +   * radius, as scale() will take care of that anyway
        +   *
        +   * [toxi 031223] updated sphere code (removed modulos)
        +   * and introduced sphereAt(x,y,z,r)
        +   * to avoid additional translate()'s on the user/sketch side
        +   *
        +   * [davbol 080801] now using separate sphereDetailU/V
        +   * 
        + * + * @webref shape:3d_primitives + * @param r the radius of the sphere + * @see PGraphics#sphereDetail(int) + */ + public void sphere(float r) { + if ((sphereDetailU < 3) || (sphereDetailV < 2)) { + sphereDetail(30); + } + + edge(false); + + + // 1st ring from south pole + beginShape(TRIANGLE_STRIP); + for (int i = 0; i < sphereDetailU; i++) { + normal(0, -1, 0); + vertex(0, -r, 0); + normal(sphereX[i], sphereY[i], sphereZ[i]); + vertex(r * sphereX[i], r * sphereY[i], r * sphereZ[i]); + } + normal(0, -r, 0); + vertex(0, -r, 0); + normal(sphereX[0], sphereY[0], sphereZ[0]); + vertex(r * sphereX[0], r * sphereY[0], r * sphereZ[0]); + endShape(); + + int v1,v11,v2; + + // middle rings + int voff = 0; + for (int i = 2; i < sphereDetailV; i++) { + v1 = v11 = voff; + voff += sphereDetailU; + v2 = voff; + beginShape(TRIANGLE_STRIP); + for (int j = 0; j < sphereDetailU; j++) { + normal(sphereX[v1], sphereY[v1], sphereZ[v1]); + vertex(r * sphereX[v1], r * sphereY[v1], r * sphereZ[v1++]); + normal(sphereX[v2], sphereY[v2], sphereZ[v2]); + vertex(r * sphereX[v2], r * sphereY[v2], r * sphereZ[v2++]); + } + // close each ring + v1 = v11; + v2 = voff; + normal(sphereX[v1], sphereY[v1], sphereZ[v1]); + vertex(r * sphereX[v1], r * sphereY[v1], r * sphereZ[v1]); + normal(sphereX[v2], sphereY[v2], sphereZ[v2]); + vertex(r * sphereX[v2], r * sphereY[v2], r * sphereZ[v2]); + endShape(); + } + + // add the northern cap + beginShape(TRIANGLE_STRIP); + for (int i = 0; i < sphereDetailU; i++) { + v2 = voff + i; + normal(sphereX[v2], sphereY[v2], sphereZ[v2]); + vertex(r * sphereX[v2], r * sphereY[v2], r * sphereZ[v2]); + normal(0, 1, 0); + vertex(0, r, 0); + } + normal(sphereX[voff], sphereY[voff], sphereZ[voff]); + vertex(r * sphereX[voff], r * sphereY[voff], r * sphereZ[voff]); + normal(0, 1, 0); + vertex(0, r, 0); + endShape(); + + edge(true); + } + + + + ////////////////////////////////////////////////////////////// + + // BEZIER + + /** + * ( begin auto-generated from bezierPoint.xml ) + * + * Evaluates the Bezier at point t for points a, b, c, d. The parameter t + * varies between 0 and 1, a and d are points on the curve, and b and c are + * the control points. This can be done once with the x coordinates and a + * second time with the y coordinates to get the location of a bezier curve + * at t. + * + * ( end auto-generated ) + * + *

        Advanced

        + * For instance, to convert the following example:
        +   * stroke(255, 102, 0);
        +   * line(85, 20, 10, 10);
        +   * line(90, 90, 15, 80);
        +   * stroke(0, 0, 0);
        +   * bezier(85, 20, 10, 10, 90, 90, 15, 80);
        +   *
        +   * // draw it in gray, using 10 steps instead of the default 20
        +   * // this is a slower way to do it, but useful if you need
        +   * // to do things with the coordinates at each step
        +   * stroke(128);
        +   * beginShape(LINE_STRIP);
        +   * for (int i = 0; i <= 10; i++) {
        +   *   float t = i / 10.0f;
        +   *   float x = bezierPoint(85, 10, 90, 15, t);
        +   *   float y = bezierPoint(20, 10, 90, 80, t);
        +   *   vertex(x, y);
        +   * }
        +   * endShape();
        + * + * @webref shape:curves + * @param a coordinate of first point on the curve + * @param b coordinate of first control point + * @param c coordinate of second control point + * @param d coordinate of second point on the curve + * @param t value between 0 and 1 + * @see PGraphics#bezier(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#bezierVertex(float, float, float, float, float, float) + * @see PGraphics#curvePoint(float, float, float, float, float) + */ + public float bezierPoint(float a, float b, float c, float d, float t) { + float t1 = 1.0f - t; + return (a*t1 + 3*b*t)*t1*t1 + (3*c*t1 + d*t)*t*t; + } + /** + * ( begin auto-generated from bezierTangent.xml ) + * + * Calculates the tangent of a point on a Bezier curve. There is a good + * definition of tangent on Wikipedia. + * + * ( end auto-generated ) + * + *

        Advanced

        + * Code submitted by Dave Bollinger (davol) for release 0136. + * + * @webref shape:curves + * @param a coordinate of first point on the curve + * @param b coordinate of first control point + * @param c coordinate of second control point + * @param d coordinate of second point on the curve + * @param t value between 0 and 1 + * @see PGraphics#bezier(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#bezierVertex(float, float, float, float, float, float) + * @see PGraphics#curvePoint(float, float, float, float, float) + */ + public float bezierTangent(float a, float b, float c, float d, float t) { + return (3*t*t * (-a+3*b-3*c+d) + + 6*t * (a-2*b+c) + + 3 * (-a+b)); + } + + + protected void bezierInitCheck() { + if (!bezierInited) { + bezierInit(); + } + } + + + protected void bezierInit() { + // overkill to be broken out, but better parity with the curve stuff below + bezierDetail(bezierDetail); + bezierInited = true; + } + + + /** + * ( begin auto-generated from bezierDetail.xml ) + * + * Sets the resolution at which Beziers display. The default value is 20. + * This function is only useful when using the P3D renderer as the default + * P2D renderer does not use this information. + * + * ( end auto-generated ) + * + * @webref shape:curves + * @param detail resolution of the curves + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#curveVertex(float, float, float) + * @see PGraphics#curveTightness(float) + */ + public void bezierDetail(int detail) { + bezierDetail = detail; + + if (bezierDrawMatrix == null) { + bezierDrawMatrix = new PMatrix3D(); + } + + // setup matrix for forward differencing to speed up drawing + splineForward(detail, bezierDrawMatrix); + + // multiply the basis and forward diff matrices together + // saves much time since this needn't be done for each curve + //mult_spline_matrix(bezierForwardMatrix, bezier_basis, bezierDrawMatrix, 4); + //bezierDrawMatrix.set(bezierForwardMatrix); + bezierDrawMatrix.apply(bezierBasisMatrix); + } + + + + public void bezier(float x1, float y1, + float x2, float y2, + float x3, float y3, + float x4, float y4) { + beginShape(); + vertex(x1, y1); + bezierVertex(x2, y2, x3, y3, x4, y4); + endShape(); + } + + /** + * ( begin auto-generated from bezier.xml ) + * + * Draws a Bezier curve on the screen. These curves are defined by a series + * of anchor and control points. The first two parameters specify the first + * anchor point and the last two parameters specify the other anchor point. + * The middle parameters specify the control points which define the shape + * of the curve. Bezier curves were developed by French engineer Pierre + * Bezier. Using the 3D version requires rendering with P3D (see the + * Environment reference for more information). + * + * ( end auto-generated ) + * + *

        Advanced

        + * Draw a cubic bezier curve. The first and last points are + * the on-curve points. The middle two are the 'control' points, + * or 'handles' in an application like Illustrator. + *

        + * Identical to typing: + *

        beginShape();
        +   * vertex(x1, y1);
        +   * bezierVertex(x2, y2, x3, y3, x4, y4);
        +   * endShape();
        +   * 
        + * In Postscript-speak, this would be: + *
        moveto(x1, y1);
        +   * curveto(x2, y2, x3, y3, x4, y4);
        + * If you were to try and continue that curve like so: + *
        curveto(x5, y5, x6, y6, x7, y7);
        + * This would be done in processing by adding these statements: + *
        bezierVertex(x5, y5, x6, y6, x7, y7)
        +   * 
        + * To draw a quadratic (instead of cubic) curve, + * use the control point twice by doubling it: + *
        bezier(x1, y1, cx, cy, cx, cy, x2, y2);
        + * + * @webref shape:curves + * @param x1 coordinates for the first anchor point + * @param y1 coordinates for the first anchor point + * @param z1 coordinates for the first anchor point + * @param x2 coordinates for the first control point + * @param y2 coordinates for the first control point + * @param z2 coordinates for the first control point + * @param x3 coordinates for the second control point + * @param y3 coordinates for the second control point + * @param z3 coordinates for the second control point + * @param x4 coordinates for the second anchor point + * @param y4 coordinates for the second anchor point + * @param z4 coordinates for the second anchor point + * + * @see PGraphics#bezierVertex(float, float, float, float, float, float) + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + */ + public void bezier(float x1, float y1, float z1, + float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + beginShape(); + vertex(x1, y1, z1); + bezierVertex(x2, y2, z2, + x3, y3, z3, + x4, y4, z4); + endShape(); + } + + + + ////////////////////////////////////////////////////////////// + + // CATMULL-ROM CURVE + + /** + * ( begin auto-generated from curvePoint.xml ) + * + * Evalutes the curve at point t for points a, b, c, d. The parameter t + * varies between 0 and 1, a and d are points on the curve, and b and c are + * the control points. This can be done once with the x coordinates and a + * second time with the y coordinates to get the location of a curve at t. + * + * ( end auto-generated ) + * + * @webref shape:curves + * @param a coordinate of first point on the curve + * @param b coordinate of second point on the curve + * @param c coordinate of third point on the curve + * @param d coordinate of fourth point on the curve + * @param t value between 0 and 1 + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#curveVertex(float, float) + * @see PGraphics#bezierPoint(float, float, float, float, float) + */ + public float curvePoint(float a, float b, float c, float d, float t) { + curveInitCheck(); + + float tt = t * t; + float ttt = t * tt; + PMatrix3D cb = curveBasisMatrix; + + // not optimized (and probably need not be) + return (a * (ttt*cb.m00 + tt*cb.m10 + t*cb.m20 + cb.m30) + + b * (ttt*cb.m01 + tt*cb.m11 + t*cb.m21 + cb.m31) + + c * (ttt*cb.m02 + tt*cb.m12 + t*cb.m22 + cb.m32) + + d * (ttt*cb.m03 + tt*cb.m13 + t*cb.m23 + cb.m33)); + } + + + /** + * ( begin auto-generated from curveTangent.xml ) + * + * Calculates the tangent of a point on a curve. There's a good definition + * of tangent on Wikipedia. + * + * ( end auto-generated ) + * + *

        Advanced

        + * Code thanks to Dave Bollinger (Bug #715) + * + * @webref shape:curves + * @param a coordinate of first point on the curve + * @param b coordinate of first control point + * @param c coordinate of second control point + * @param d coordinate of second point on the curve + * @param t value between 0 and 1 + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#curveVertex(float, float) + * @see PGraphics#curvePoint(float, float, float, float, float) + * @see PGraphics#bezierTangent(float, float, float, float, float) + */ + public float curveTangent(float a, float b, float c, float d, float t) { + curveInitCheck(); + + float tt3 = t * t * 3; + float t2 = t * 2; + PMatrix3D cb = curveBasisMatrix; + + // not optimized (and probably need not be) + return (a * (tt3*cb.m00 + t2*cb.m10 + cb.m20) + + b * (tt3*cb.m01 + t2*cb.m11 + cb.m21) + + c * (tt3*cb.m02 + t2*cb.m12 + cb.m22) + + d * (tt3*cb.m03 + t2*cb.m13 + cb.m23) ); + } + + + /** + * ( begin auto-generated from curveDetail.xml ) + * + * Sets the resolution at which curves display. The default value is 20. + * This function is only useful when using the P3D renderer as the default + * P2D renderer does not use this information. + * + * ( end auto-generated ) + * + * @webref shape:curves + * @param detail resolution of the curves + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#curveVertex(float, float) + * @see PGraphics#curveTightness(float) + */ + public void curveDetail(int detail) { + curveDetail = detail; + curveInit(); + } + + + /** + * ( begin auto-generated from curveTightness.xml ) + * + * Modifies the quality of forms created with curve() and + * curveVertex(). The parameter squishy determines how the + * curve fits to the vertex points. The value 0.0 is the default value for + * squishy (this value defines the curves to be Catmull-Rom splines) + * and the value 1.0 connects all the points with straight lines. Values + * within the range -5.0 and 5.0 will deform the curves but will leave them + * recognizable and as values increase in magnitude, they will continue to deform. + * + * ( end auto-generated ) + * + * @webref shape:curves + * @param tightness amount of deformation from the original vertices + * @see PGraphics#curve(float, float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#curveVertex(float, float) + */ + public void curveTightness(float tightness) { + curveTightness = tightness; + curveInit(); + } + + + protected void curveInitCheck() { + if (!curveInited) { + curveInit(); + } + } + + + /** + * Set the number of segments to use when drawing a Catmull-Rom + * curve, and setting the s parameter, which defines how tightly + * the curve fits to each vertex. Catmull-Rom curves are actually + * a subset of this curve type where the s is set to zero. + *

        + * (This function is not optimized, since it's not expected to + * be called all that often. there are many juicy and obvious + * opimizations in here, but it's probably better to keep the + * code more readable) + */ + protected void curveInit() { + // allocate only if/when used to save startup time + if (curveDrawMatrix == null) { + curveBasisMatrix = new PMatrix3D(); + curveDrawMatrix = new PMatrix3D(); + curveInited = true; + } + + float s = curveTightness; + curveBasisMatrix.set((s-1)/2f, (s+3)/2f, (-3-s)/2f, (1-s)/2f, + (1-s), (-5-s)/2f, (s+2), (s-1)/2f, + (s-1)/2f, 0, (1-s)/2f, 0, + 0, 1, 0, 0); + + //setup_spline_forward(segments, curveForwardMatrix); + splineForward(curveDetail, curveDrawMatrix); + + if (bezierBasisInverse == null) { + bezierBasisInverse = bezierBasisMatrix.get(); + bezierBasisInverse.invert(); + curveToBezierMatrix = new PMatrix3D(); + } + + // TODO only needed for PGraphicsJava2D? if so, move it there + // actually, it's generally useful for other renderers, so keep it + // or hide the implementation elsewhere. + curveToBezierMatrix.set(curveBasisMatrix); + curveToBezierMatrix.preApply(bezierBasisInverse); + + // multiply the basis and forward diff matrices together + // saves much time since this needn't be done for each curve + curveDrawMatrix.apply(curveBasisMatrix); + } + + + /** + * ( begin auto-generated from curve.xml ) + * + * Draws a curved line on the screen. The first and second parameters + * specify the beginning control point and the last two parameters specify + * the ending control point. The middle parameters specify the start and + * stop of the curve. Longer curves can be created by putting a series of + * curve() functions together or using curveVertex(). An + * additional function called curveTightness() provides control for + * the visual quality of the curve. The curve() function is an + * implementation of Catmull-Rom splines. Using the 3D version requires + * rendering with P3D (see the Environment reference for more information). + * + * ( end auto-generated ) + * + *

        Advanced

        + * As of revision 0070, this function no longer doubles the first + * and last points. The curves are a bit more boring, but it's more + * mathematically correct, and properly mirrored in curvePoint(). + *

        + * Identical to typing out:

        +   * beginShape();
        +   * curveVertex(x1, y1);
        +   * curveVertex(x2, y2);
        +   * curveVertex(x3, y3);
        +   * curveVertex(x4, y4);
        +   * endShape();
        +   * 
        + * + * @webref shape:curves + * @param x1 coordinates for the beginning control point + * @param y1 coordinates for the beginning control point + * @param x2 coordinates for the first point + * @param y2 coordinates for the first point + * @param x3 coordinates for the second point + * @param y3 coordinates for the second point + * @param x4 coordinates for the ending control point + * @param y4 coordinates for the ending control point + * @see PGraphics#curveVertex(float, float) + * @see PGraphics#curveTightness(float) + * @see PGraphics#bezier(float, float, float, float, float, float, float, float, float, float, float, float) + */ + public void curve(float x1, float y1, + float x2, float y2, + float x3, float y3, + float x4, float y4) { + beginShape(); + curveVertex(x1, y1); + curveVertex(x2, y2); + curveVertex(x3, y3); + curveVertex(x4, y4); + endShape(); + } + + /** + * @param z1 coordinates for the beginning control point + * @param z2 coordinates for the first point + * @param z3 coordinates for the second point + * @param z4 coordinates for the ending control point + */ + public void curve(float x1, float y1, float z1, + float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + beginShape(); + curveVertex(x1, y1, z1); + curveVertex(x2, y2, z2); + curveVertex(x3, y3, z3); + curveVertex(x4, y4, z4); + endShape(); + } + + + + ////////////////////////////////////////////////////////////// + + // SPLINE UTILITY FUNCTIONS (used by both Bezier and Catmull-Rom) + + + /** + * Setup forward-differencing matrix to be used for speedy + * curve rendering. It's based on using a specific number + * of curve segments and just doing incremental adds for each + * vertex of the segment, rather than running the mathematically + * expensive cubic equation. + * @param segments number of curve segments to use when drawing + * @param matrix target object for the new matrix + */ + protected void splineForward(int segments, PMatrix3D matrix) { + float f = 1.0f / segments; + float ff = f * f; + float fff = ff * f; + + matrix.set(0, 0, 0, 1, + fff, ff, f, 0, + 6*fff, 2*ff, 0, 0, + 6*fff, 0, 0, 0); + } + + + + ////////////////////////////////////////////////////////////// + + // SMOOTHING + + + public void smooth() { // ignore + smooth(1); + } + + + public void smooth(int quality) { // ignore + if (primaryGraphics) { + parent.smooth(quality); + } else { + // for createGraphics(), make sure beginDraw() not called yet + if (settingsInited) { + // ignore if it's just a repeat of the current state + if (this.smooth != quality) { + smoothWarning("smooth"); + } + } else { + this.smooth = quality; + } + } + } + + + public void noSmooth() { // ignore + smooth(0); + } + + + private void smoothWarning(String method) { + PGraphics.showWarning("%s() can only be used before beginDraw()", method); + } + + + + ////////////////////////////////////////////////////////////// + + // IMAGE + + + /** + * ( begin auto-generated from imageMode.xml ) + * + * Modifies the location from which images draw. The default mode is + * imageMode(CORNER), which specifies the location to be the upper + * left corner and uses the fourth and fifth parameters of image() + * to set the image's width and height. The syntax + * imageMode(CORNERS) uses the second and third parameters of + * image() to set the location of one corner of the image and uses + * the fourth and fifth parameters to set the opposite corner. Use + * imageMode(CENTER) to draw images centered at the given x and y + * position.
        + *
        + * The parameter to imageMode() must be written in ALL CAPS because + * Processing is a case-sensitive language. + * + * ( end auto-generated ) + * + * @webref image:loading_displaying + * @param mode either CORNER, CORNERS, or CENTER + * @see PApplet#loadImage(String, String) + * @see PImage + * @see PGraphics#image(PImage, float, float, float, float) + * @see PGraphics#background(float, float, float, float) + */ + public void imageMode(int mode) { + if ((mode == CORNER) || (mode == CORNERS) || (mode == CENTER)) { + imageMode = mode; + } else { + String msg = + "imageMode() only works with CORNER, CORNERS, or CENTER"; + throw new RuntimeException(msg); + } + } + + + /** + * ( begin auto-generated from image.xml ) + * + * Displays images to the screen. The images must be in the sketch's "data" + * directory to load correctly. Select "Add file..." from the "Sketch" menu + * to add the image. Processing currently works with GIF, JPEG, and Targa + * images. The img parameter specifies the image to display and the + * x and y parameters define the location of the image from + * its upper-left corner. The image is displayed at its original size + * unless the width and height parameters specify a different + * size.
        + *
        + * The imageMode() function changes the way the parameters work. For + * example, a call to imageMode(CORNERS) will change the + * width and height parameters to define the x and y values + * of the opposite corner of the image.
        + *
        + * The color of an image may be modified with the tint() function. + * This function will maintain transparency for GIF and PNG images. + * + * ( end auto-generated ) + * + *

        Advanced

        + * Starting with release 0124, when using the default (JAVA2D) renderer, + * smooth() will also improve image quality of resized images. + * + * @webref image:loading_displaying + * @param img the image to display + * @param a x-coordinate of the image by default + * @param b y-coordinate of the image by default + * @see PApplet#loadImage(String, String) + * @see PImage + * @see PGraphics#imageMode(int) + * @see PGraphics#tint(float) + * @see PGraphics#background(float, float, float, float) + * @see PGraphics#alpha(int) + */ + public void image(PImage img, float a, float b) { + // Starting in release 0144, image errors are simply ignored. + // loadImageAsync() sets width and height to -1 when loading fails. + if (img.width == -1 || img.height == -1) return; + + if (imageMode == CORNER || imageMode == CORNERS) { + imageImpl(img, + a, b, a+img.width, b+img.height, + 0, 0, img.width, img.height); + + } else if (imageMode == CENTER) { + float x1 = a - img.width/2; + float y1 = b - img.height/2; + imageImpl(img, + x1, y1, x1+img.width, y1+img.height, + 0, 0, img.width, img.height); + } + } + + /** + * @param c width to display the image by default + * @param d height to display the image by default + */ + public void image(PImage img, float a, float b, float c, float d) { + image(img, a, b, c, d, 0, 0, img.width, img.height); + } + + + /** + * Draw an image(), also specifying u/v coordinates. + * In this method, the u, v coordinates are always based on image space + * location, regardless of the current textureMode(). + * + * @nowebref + */ + public void image(PImage img, + float a, float b, float c, float d, + int u1, int v1, int u2, int v2) { + // Starting in release 0144, image errors are simply ignored. + // loadImageAsync() sets width and height to -1 when loading fails. + if (img.width == -1 || img.height == -1) return; + + if (imageMode == CORNER) { + if (c < 0) { // reset a negative width + a += c; c = -c; + } + if (d < 0) { // reset a negative height + b += d; d = -d; + } + + imageImpl(img, + a, b, a + c, b + d, + u1, v1, u2, v2); + + } else if (imageMode == CORNERS) { + if (c < a) { // reverse because x2 < x1 + float temp = a; a = c; c = temp; + } + if (d < b) { // reverse because y2 < y1 + float temp = b; b = d; d = temp; + } + + imageImpl(img, + a, b, c, d, + u1, v1, u2, v2); + + } else if (imageMode == CENTER) { + // c and d are width/height + if (c < 0) c = -c; + if (d < 0) d = -d; + float x1 = a - c/2; + float y1 = b - d/2; + + imageImpl(img, + x1, y1, x1 + c, y1 + d, + u1, v1, u2, v2); + } + } + + + /** + * Expects x1, y1, x2, y2 coordinates where (x2 >= x1) and (y2 >= y1). + * If tint() has been called, the image will be colored. + *

        + * The default implementation draws an image as a textured quad. + * The (u, v) coordinates are in image space (they're ints, after all..) + */ + protected void imageImpl(PImage img, + float x1, float y1, float x2, float y2, + int u1, int v1, int u2, int v2) { + boolean savedStroke = stroke; +// boolean savedFill = fill; + int savedTextureMode = textureMode; + + stroke = false; +// fill = true; + textureMode = IMAGE; + +// float savedFillR = fillR; +// float savedFillG = fillG; +// float savedFillB = fillB; +// float savedFillA = fillA; +// +// if (tint) { +// fillR = tintR; +// fillG = tintG; +// fillB = tintB; +// fillA = tintA; +// +// } else { +// fillR = 1; +// fillG = 1; +// fillB = 1; +// fillA = 1; +// } + + u1 *= img.pixelDensity; + u2 *= img.pixelDensity; + v1 *= img.pixelDensity; + v2 *= img.pixelDensity; + + beginShape(QUADS); + texture(img); + vertex(x1, y1, u1, v1); + vertex(x1, y2, u1, v2); + vertex(x2, y2, u2, v2); + vertex(x2, y1, u2, v1); + endShape(); + + stroke = savedStroke; +// fill = savedFill; + textureMode = savedTextureMode; + +// fillR = savedFillR; +// fillG = savedFillG; +// fillB = savedFillB; +// fillA = savedFillA; + } + + + ////////////////////////////////////////////////////////////// + + // SHAPE + + + /** + * ( begin auto-generated from shapeMode.xml ) + * + * Modifies the location from which shapes draw. The default mode is + * shapeMode(CORNER), which specifies the location to be the upper + * left corner of the shape and uses the third and fourth parameters of + * shape() to specify the width and height. The syntax + * shapeMode(CORNERS) uses the first and second parameters of + * shape() to set the location of one corner and uses the third and + * fourth parameters to set the opposite corner. The syntax + * shapeMode(CENTER) draws the shape from its center point and uses + * the third and forth parameters of shape() to specify the width + * and height. The parameter must be written in "ALL CAPS" because + * Processing is a case sensitive language. + * + * ( end auto-generated ) + * + * @webref shape:loading_displaying + * @param mode either CORNER, CORNERS, CENTER + * @see PShape + * @see PGraphics#shape(PShape) + * @see PGraphics#rectMode(int) + */ + public void shapeMode(int mode) { + this.shapeMode = mode; + } + + + public void shape(PShape shape) { + if (shape.isVisible()) { // don't do expensive matrix ops if invisible + // Flushing any remaining geometry generated in the immediate mode + // to avoid depth-sorting issues. + flush(); + + if (shapeMode == CENTER) { + pushMatrix(); + translate(-shape.getWidth()/2, -shape.getHeight()/2); + } + + shape.draw(this); // needs to handle recorder too + + if (shapeMode == CENTER) { + popMatrix(); + } + } + } + + + + /** + * ( begin auto-generated from shape.xml ) + * + * Displays shapes to the screen. The shapes must be in the sketch's "data" + * directory to load correctly. Select "Add file..." from the "Sketch" menu + * to add the shape. Processing currently works with SVG shapes only. The + * sh parameter specifies the shape to display and the x and + * y parameters define the location of the shape from its upper-left + * corner. The shape is displayed at its original size unless the + * width and height parameters specify a different size. The + * shapeMode() function changes the way the parameters work. A call + * to shapeMode(CORNERS), for example, will change the width and + * height parameters to define the x and y values of the opposite corner of + * the shape. + *

        + * Note complex shapes may draw awkwardly with P3D. This renderer does not + * yet support shapes that have holes or complicated breaks. + * + * ( end auto-generated ) + * + * @webref shape:loading_displaying + * @param shape the shape to display + * @param x x-coordinate of the shape + * @param y y-coordinate of the shape + * @see PShape + * @see PApplet#loadShape(String) + * @see PGraphics#shapeMode(int) + * + * Convenience method to draw at a particular location. + */ + public void shape(PShape shape, float x, float y) { + if (shape.isVisible()) { // don't do expensive matrix ops if invisible + flush(); + + pushMatrix(); + + if (shapeMode == CENTER) { + translate(x - shape.getWidth()/2, y - shape.getHeight()/2); + + } else if ((shapeMode == CORNER) || (shapeMode == CORNERS)) { + translate(x, y); + } + shape.draw(this); + + popMatrix(); + } + } + + + // TODO unapproved + protected void shape(PShape shape, float x, float y, float z) { + showMissingWarning("shape"); + } + + + /** + * @param a x-coordinate of the shape + * @param b y-coordinate of the shape + * @param c width to display the shape + * @param d height to display the shape + */ + public void shape(PShape shape, float a, float b, float c, float d) { + if (shape.isVisible()) { // don't do expensive matrix ops if invisible + flush(); + + pushMatrix(); + + if (shapeMode == CENTER) { + // x and y are center, c and d refer to a diameter + translate(a - c/2f, b - d/2f); + scale(c / shape.getWidth(), d / shape.getHeight()); + + } else if (shapeMode == CORNER) { + translate(a, b); + scale(c / shape.getWidth(), d / shape.getHeight()); + + } else if (shapeMode == CORNERS) { + // c and d are x2/y2, make them into width/height + c -= a; + d -= b; + // then same as above + translate(a, b); + scale(c / shape.getWidth(), d / shape.getHeight()); + } + shape.draw(this); + + popMatrix(); + } + } + + + // TODO unapproved + protected void shape(PShape shape, float x, float y, float z, float c, float d, float e) { + showMissingWarning("shape"); + } + + + ////////////////////////////////////////////////////////////// + + // TEXT/FONTS + + + protected PFont createFont(String name, float size, + boolean smooth, char[] charset) { + String lowerName = name.toLowerCase(); + Font baseFont = null; + + try { + InputStream stream = null; + if (lowerName.endsWith(".otf") || lowerName.endsWith(".ttf")) { + stream = parent.createInput(name); + if (stream == null) { + System.err.println("The font \"" + name + "\" " + + "is missing or inaccessible, make sure " + + "the URL is valid or that the file has been " + + "added to your sketch and is readable."); + return null; + } + baseFont = Font.createFont(Font.TRUETYPE_FONT, parent.createInput(name)); + + } else { + baseFont = PFont.findFont(name); + } + return new PFont(baseFont.deriveFont(size * parent.pixelDensity), + smooth, charset, stream != null, + parent.pixelDensity); + + } catch (Exception e) { + System.err.println("Problem with createFont(\"" + name + "\")"); + e.printStackTrace(); + return null; + } + } + + + public void textAlign(int alignX) { + textAlign(alignX, BASELINE); + } + + + /** + * ( begin auto-generated from textAlign.xml ) + * + * Sets the current alignment for drawing text. The parameters LEFT, + * CENTER, and RIGHT set the display characteristics of the letters in + * relation to the values for the x and y parameters of the + * text() function. + *

        + * In Processing 0125 and later, an optional second parameter can be used + * to vertically align the text. BASELINE is the default, and the vertical + * alignment will be reset to BASELINE if the second parameter is not used. + * The TOP and CENTER parameters are straightforward. The BOTTOM parameter + * offsets the line based on the current textDescent(). For multiple + * lines, the final line will be aligned to the bottom, with the previous + * lines appearing above it. + *

        + * When using text() with width and height parameters, BASELINE is + * ignored, and treated as TOP. (Otherwise, text would by default draw + * outside the box, since BASELINE is the default setting. BASELINE is not + * a useful drawing mode for text drawn in a rectangle.) + *

        + * The vertical alignment is based on the value of textAscent(), + * which many fonts do not specify correctly. It may be necessary to use a + * hack and offset by a few pixels by hand so that the offset looks + * correct. To do this as less of a hack, use some percentage of + * textAscent() or textDescent() so that the hack works even + * if you change the size of the font. + * + * ( end auto-generated ) + * + * @webref typography:attributes + * @param alignX horizontal alignment, either LEFT, CENTER, or RIGHT + * @param alignY vertical alignment, either TOP, BOTTOM, CENTER, or BASELINE + * @see PApplet#loadFont(String) + * @see PFont + * @see PGraphics#text(String, float, float) + * @see PGraphics#textSize(float) + * @see PGraphics#textAscent() + * @see PGraphics#textDescent() + */ + public void textAlign(int alignX, int alignY) { + textAlign = alignX; + textAlignY = alignY; + } + + + /** + * ( begin auto-generated from textAscent.xml ) + * + * Returns ascent of the current font at its current size. This information + * is useful for determining the height of the font above the baseline. For + * example, adding the textAscent() and textDescent() values + * will give you the total height of the line. + * + * ( end auto-generated ) + * + * @webref typography:metrics + * @see PGraphics#textDescent() + */ + public float textAscent() { + if (textFont == null) { + defaultFontOrDeath("textAscent"); + } + return textFont.ascent() * textSize; + } + + + /** + * ( begin auto-generated from textDescent.xml ) + * + * Returns descent of the current font at its current size. This + * information is useful for determining the height of the font below the + * baseline. For example, adding the textAscent() and + * textDescent() values will give you the total height of the line. + * + * ( end auto-generated ) + * + * @webref typography:metrics + * @see PGraphics#textAscent() + */ + public float textDescent() { + if (textFont == null) { + defaultFontOrDeath("textDescent"); + } + return textFont.descent() * textSize; + } + + + /** + * ( begin auto-generated from textFont.xml ) + * + * Sets the current font that will be drawn with the text() + * function. Fonts must be loaded with loadFont() before it can be + * used. This font will be used in all subsequent calls to the + * text() function. If no size parameter is input, the font + * will appear at its original size (the size it was created at with the + * "Create Font..." tool) until it is changed with textSize().

        Because fonts are usually bitmaped, you should create fonts at + * the sizes that will be used most commonly. Using textFont() + * without the size parameter will result in the cleanest-looking text.

        With the default (JAVA2D) and PDF renderers, it's also possible + * to enable the use of native fonts via the command + * hint(ENABLE_NATIVE_FONTS). This will produce vector text in + * JAVA2D sketches and PDF output in cases where the vector data is + * available: when the font is still installed, or the font is created via + * the createFont() function (rather than the Create Font tool). + * + * ( end auto-generated ) + * + * @webref typography:loading_displaying + * @param which any variable of the type PFont + * @see PApplet#createFont(String, float, boolean) + * @see PApplet#loadFont(String) + * @see PFont + * @see PGraphics#text(String, float, float) + * @see PGraphics#textSize(float) + */ + public void textFont(PFont which) { + if (which == null) { + throw new RuntimeException(ERROR_TEXTFONT_NULL_PFONT); + } + textFontImpl(which, which.getDefaultSize()); + } + + + /** + * @param size the size of the letters in units of pixels + */ + public void textFont(PFont which, float size) { + if (which == null) { + throw new RuntimeException(ERROR_TEXTFONT_NULL_PFONT); + } + // https://github.com/processing/processing/issues/3110 + if (size <= 0) { + // Using System.err instead of showWarning to avoid running out of + // memory with a bunch of textSize() variants (cause of this bug is + // usually something done with map() or in a loop). + System.err.println("textFont: ignoring size " + size + " px:" + + "the text size must be larger than zero"); + size = textSize; + } + textFontImpl(which, size); + } + + + /** + * Called from textFont. Check the validity of args and + * print possible errors to the user before calling this. + * Subclasses will want to override this one. + * + * @param which font to set, not null + * @param size size to set, greater than zero + */ + protected void textFontImpl(PFont which, float size) { + textFont = which; +// if (hints[ENABLE_NATIVE_FONTS]) { +// //if (which.font == null) { +// which.findNative(); +// //} +// } + /* + textFontNative = which.font; + + //textFontNativeMetrics = null; + // changed for rev 0104 for textMode(SHAPE) in opengl + if (textFontNative != null) { + // TODO need a better way to handle this. could use reflection to get + // rid of the warning, but that'd be a little silly. supporting this is + // an artifact of supporting java 1.1, otherwise we'd use getLineMetrics, + // as recommended by the @deprecated flag. + textFontNativeMetrics = + Toolkit.getDefaultToolkit().getFontMetrics(textFontNative); + // The following is what needs to be done, however we need to be able + // to get the actual graphics context where the drawing is happening. + // For instance, parent.getGraphics() doesn't work for OpenGL since + // an OpenGL drawing surface is an embedded component. +// if (parent != null) { +// textFontNativeMetrics = parent.getGraphics().getFontMetrics(textFontNative); +// } + + // float w = font.getStringBounds(text, g2.getFontRenderContext()).getWidth(); + } + */ + + handleTextSize(size); + } + + + /** + * ( begin auto-generated from textLeading.xml ) + * + * Sets the spacing between lines of text in units of pixels. This setting + * will be used in all subsequent calls to the text() function. + * + * ( end auto-generated ) + * + * @webref typography:attributes + * @param leading the size in pixels for spacing between lines + * @see PApplet#loadFont(String) + * @see PFont#PFont + * @see PGraphics#text(String, float, float) + * @see PGraphics#textFont(PFont) + * @see PGraphics#textSize(float) + */ + public void textLeading(float leading) { + textLeading = leading; + } + + + /** + * ( begin auto-generated from textMode.xml ) + * + * Sets the way text draws to the screen. In the default configuration, the + * MODEL mode, it's possible to rotate, scale, and place letters in + * two and three dimensional space.
        + *
        + * The SHAPE mode draws text using the the glyph outlines of + * individual characters rather than as textures. This mode is only + * supported with the PDF and P3D renderer settings. With the + * PDF renderer, you must call textMode(SHAPE) before any + * other drawing occurs. If the outlines are not available, then + * textMode(SHAPE) will be ignored and textMode(MODEL) will + * be used instead.
        + *
        + * The textMode(SHAPE) option in P3D can be combined with + * beginRaw() to write vector-accurate text to 2D and 3D output + * files, for instance DXF or PDF. The SHAPE mode is + * not currently optimized for P3D, so if recording shape data, use + * textMode(MODEL) until you're ready to capture the geometry with beginRaw(). + * + * ( end auto-generated ) + * + * @webref typography:attributes + * @param mode either MODEL or SHAPE + * @see PApplet#loadFont(String) + * @see PFont#PFont + * @see PGraphics#text(String, float, float) + * @see PGraphics#textFont(PFont) + * @see PGraphics#beginRaw(PGraphics) + * @see PApplet#createFont(String, float, boolean) + */ + public void textMode(int mode) { + // CENTER and MODEL overlap (they're both 3) + if ((mode == LEFT) || (mode == RIGHT)) { + showWarning("Since Processing 1.0 beta, textMode() is now textAlign()."); + return; + } + if (mode == SCREEN) { + showWarning("textMode(SCREEN) has been removed from Processing 2.0."); + return; + } + + if (textModeCheck(mode)) { + textMode = mode; + } else { + String modeStr = String.valueOf(mode); + switch (mode) { + case MODEL: modeStr = "MODEL"; break; + case SHAPE: modeStr = "SHAPE"; break; + } + showWarning("textMode(" + modeStr + ") is not supported by this renderer."); + } + } + + + protected boolean textModeCheck(int mode) { + return true; + } + + + /** + * ( begin auto-generated from textSize.xml ) + * + * Sets the current font size. This size will be used in all subsequent + * calls to the text() function. Font size is measured in units of pixels. + * + * ( end auto-generated ) + * + * @webref typography:attributes + * @param size the size of the letters in units of pixels + * @see PApplet#loadFont(String) + * @see PFont#PFont + * @see PGraphics#text(String, float, float) + * @see PGraphics#textFont(PFont) + */ + public void textSize(float size) { + // https://github.com/processing/processing/issues/3110 + if (size <= 0) { + // Using System.err instead of showWarning to avoid running out of + // memory with a bunch of textSize() variants (cause of this bug is + // usually something done with map() or in a loop). + System.err.println("textSize(" + size + ") ignored: " + + "the text size must be larger than zero"); + return; + } + if (textFont == null) { + defaultFontOrDeath("textSize", size); + } + textSizeImpl(size); + } + + + /** + * Called from textSize() after validating size. Subclasses + * will want to override this one. + * @param size size of the text, greater than zero + */ + protected void textSizeImpl(float size) { + handleTextSize(size); + } + + + /** + * Sets the actual size. Called from textSizeImpl and + * from textFontImpl after setting the font. + * @param size size of the text, greater than zero + */ + protected void handleTextSize(float size) { + textSize = size; + textLeading = (textAscent() + textDescent()) * 1.275f; + } + + + // ........................................................ + + + /** + * @param c the character to measure + */ + public float textWidth(char c) { + textWidthBuffer[0] = c; + return textWidthImpl(textWidthBuffer, 0, 1); + } + + + /** + * ( begin auto-generated from textWidth.xml ) + * + * Calculates and returns the width of any character or text string. + * + * ( end auto-generated ) + * + * @webref typography:attributes + * @param str the String of characters to measure + * @see PApplet#loadFont(String) + * @see PFont#PFont + * @see PGraphics#text(String, float, float) + * @see PGraphics#textFont(PFont) + * @see PGraphics#textSize(float) + */ + public float textWidth(String str) { + if (textFont == null) { + defaultFontOrDeath("textWidth"); + } + + int length = str.length(); + if (length > textWidthBuffer.length) { + textWidthBuffer = new char[length + 10]; + } + str.getChars(0, length, textWidthBuffer, 0); + + float wide = 0; + int index = 0; + int start = 0; + + while (index < length) { + if (textWidthBuffer[index] == '\n') { + wide = Math.max(wide, textWidthImpl(textWidthBuffer, start, index)); + start = index+1; + } + index++; + } + if (start < length) { + wide = Math.max(wide, textWidthImpl(textWidthBuffer, start, index)); + } + return wide; + } + + + /** + * @nowebref + */ + public float textWidth(char[] chars, int start, int length) { + return textWidthImpl(chars, start, start + length); + } + + + /** + * Implementation of returning the text width of + * the chars [start, stop) in the buffer. + * Unlike the previous version that was inside PFont, this will + * return the size not of a 1 pixel font, but the actual current size. + */ + protected float textWidthImpl(char buffer[], int start, int stop) { + float wide = 0; + for (int i = start; i < stop; i++) { + // could add kerning here, but it just ain't implemented + wide += textFont.width(buffer[i]) * textSize; + } + return wide; + } + + + // ........................................................ + + + /** + * ( begin auto-generated from text.xml ) + * + * Draws text to the screen. Displays the information specified in the + * data or stringdata parameters on the screen in the + * position specified by the x and y parameters and the + * optional z parameter. A default font will be used unless a font + * is set with the textFont() function. Change the color of the text + * with the fill() function. The text displays in relation to the + * textAlign() function, which gives the option to draw to the left, + * right, and center of the coordinates. + *

        + * The x2 and y2 parameters define a rectangular area to + * display within and may only be used with string data. For text drawn + * inside a rectangle, the coordinates are interpreted based on the current + * rectMode() setting. + * + * ( end auto-generated ) + * + * @webref typography:loading_displaying + * @param c the alphanumeric character to be displayed + * @param x x-coordinate of text + * @param y y-coordinate of text + * @see PGraphics#textAlign(int, int) + * @see PGraphics#textFont(PFont) + * @see PGraphics#textMode(int) + * @see PGraphics#textSize(float) + * @see PGraphics#textLeading(float) + * @see PGraphics#textWidth(String) + * @see PGraphics#textAscent() + * @see PGraphics#textDescent() + * @see PGraphics#rectMode(int) + * @see PGraphics#fill(int, float) + * @see_external String + */ + public void text(char c, float x, float y) { + if (textFont == null) { + defaultFontOrDeath("text"); + } + + if (textAlignY == CENTER) { + y += textAscent() / 2; + } else if (textAlignY == TOP) { + y += textAscent(); + } else if (textAlignY == BOTTOM) { + y -= textDescent(); + //} else if (textAlignY == BASELINE) { + // do nothing + } + + textBuffer[0] = c; + textLineAlignImpl(textBuffer, 0, 1, x, y); + } + + + /** + * @param z z-coordinate of text + */ + public void text(char c, float x, float y, float z) { +// if ((z != 0) && (textMode == SCREEN)) { +// String msg = "textMode(SCREEN) cannot have a z coordinate"; +// throw new RuntimeException(msg); +// } + + if (z != 0) translate(0, 0, z); // slowness, badness + + text(c, x, y); +// textZ = z; + + if (z != 0) translate(0, 0, -z); + } + + + /** + * @param str the alphanumeric symbols to be displayed + */ +// public void text(String str) { +// text(str, textX, textY, textZ); +// } + + + /** + *

        Advanced

        + * Draw a chunk of text. + * Newlines that are \n (Unix newline or linefeed char, ascii 10) + * are honored, but \r (carriage return, Windows and Mac OS) are + * ignored. + */ + public void text(String str, float x, float y) { + if (textFont == null) { + defaultFontOrDeath("text"); + } + + int length = str.length(); + if (length > textBuffer.length) { + textBuffer = new char[length + 10]; + } + str.getChars(0, length, textBuffer, 0); + text(textBuffer, 0, length, x, y); + } + + + /** + *

        Advanced

        + * Method to draw text from an array of chars. This method will usually be + * more efficient than drawing from a String object, because the String will + * not be converted to a char array before drawing. + * @param chars the alphanumberic symbols to be displayed + * @param start array index at which to start writing characters + * @param stop array index at which to stop writing characters + */ + public void text(char[] chars, int start, int stop, float x, float y) { + // If multiple lines, sum the height of the additional lines + float high = 0; //-textAscent(); + for (int i = start; i < stop; i++) { + if (chars[i] == '\n') { + high += textLeading; + } + } + if (textAlignY == CENTER) { + // for a single line, this adds half the textAscent to y + // for multiple lines, subtract half the additional height + //y += (textAscent() - textDescent() - high)/2; + y += (textAscent() - high)/2; + } else if (textAlignY == TOP) { + // for a single line, need to add textAscent to y + // for multiple lines, no different + y += textAscent(); + } else if (textAlignY == BOTTOM) { + // for a single line, this is just offset by the descent + // for multiple lines, subtract leading for each line + y -= textDescent() + high; + //} else if (textAlignY == BASELINE) { + // do nothing + } + +// int start = 0; + int index = 0; + while (index < stop) { //length) { + if (chars[index] == '\n') { + textLineAlignImpl(chars, start, index, x, y); + start = index + 1; + y += textLeading; + } + index++; + } + if (start < stop) { //length) { + textLineAlignImpl(chars, start, index, x, y); + } + } + + + /** + * Same as above but with a z coordinate. + */ + public void text(String str, float x, float y, float z) { + if (z != 0) translate(0, 0, z); // slow! + + text(str, x, y); +// textZ = z; + + if (z != 0) translate(0, 0, -z); // inaccurate! + } + + + public void text(char[] chars, int start, int stop, + float x, float y, float z) { + if (z != 0) translate(0, 0, z); // slow! + + text(chars, start, stop, x, y); +// textZ = z; + + if (z != 0) translate(0, 0, -z); // inaccurate! + } + + + /** + *

        Advanced

        + * Draw text in a box that is constrained to a particular size. + * The current rectMode() determines what the coordinates mean + * (whether x1/y1/x2/y2 or x/y/w/h). + *

        + * Note that the x,y coords of the start of the box + * will align with the *ascent* of the text, not the baseline, + * as is the case for the other text() functions. + *

        + * Newlines that are \n (Unix newline or linefeed char, ascii 10) + * are honored, and \r (carriage return, Windows and Mac OS) are + * ignored. + * + * @param x1 by default, the x-coordinate of text, see rectMode() for more info + * @param y1 by default, the y-coordinate of text, see rectMode() for more info + * @param x2 by default, the width of the text box, see rectMode() for more info + * @param y2 by default, the height of the text box, see rectMode() for more info + */ + public void text(String str, float x1, float y1, float x2, float y2) { + if (textFont == null) { + defaultFontOrDeath("text"); + } + + float hradius, vradius; + switch (rectMode) { + case CORNER: + x2 += x1; y2 += y1; + break; + case RADIUS: + hradius = x2; + vradius = y2; + x2 = x1 + hradius; + y2 = y1 + vradius; + x1 -= hradius; + y1 -= vradius; + break; + case CENTER: + hradius = x2 / 2.0f; + vradius = y2 / 2.0f; + x2 = x1 + hradius; + y2 = y1 + vradius; + x1 -= hradius; + y1 -= vradius; + } + if (x2 < x1) { + float temp = x1; x1 = x2; x2 = temp; + } + if (y2 < y1) { + float temp = y1; y1 = y2; y2 = temp; + } + +// float currentY = y1; + float boxWidth = x2 - x1; + +// // ala illustrator, the text itself must fit inside the box +// currentY += textAscent(); //ascent() * textSize; +// // if the box is already too small, tell em to f off +// if (currentY > y2) return; + + float spaceWidth = textWidth(' '); + + if (textBreakStart == null) { + textBreakStart = new int[20]; + textBreakStop = new int[20]; + } + textBreakCount = 0; + + int length = str.length(); + if (length + 1 > textBuffer.length) { + textBuffer = new char[length + 1]; + } + str.getChars(0, length, textBuffer, 0); + // add a fake newline to simplify calculations + textBuffer[length++] = '\n'; + + int sentenceStart = 0; + for (int i = 0; i < length; i++) { + if (textBuffer[i] == '\n') { +// currentY = textSentence(textBuffer, sentenceStart, i, +// lineX, boxWidth, currentY, y2, spaceWidth); + boolean legit = + textSentence(textBuffer, sentenceStart, i, boxWidth, spaceWidth); + if (!legit) break; +// if (Float.isNaN(currentY)) break; // word too big (or error) +// if (currentY > y2) break; // past the box + sentenceStart = i + 1; + } + } + + // lineX is the position where the text starts, which is adjusted + // to left/center/right based on the current textAlign + float lineX = x1; //boxX1; + if (textAlign == CENTER) { + lineX = lineX + boxWidth/2f; + } else if (textAlign == RIGHT) { + lineX = x2; //boxX2; + } + + float boxHeight = y2 - y1; + //int lineFitCount = 1 + PApplet.floor((boxHeight - textAscent()) / textLeading); + // incorporate textAscent() for the top (baseline will be y1 + ascent) + // and textDescent() for the bottom, so that lower parts of letters aren't + // outside the box. [0151] + float topAndBottom = textAscent() + textDescent(); + int lineFitCount = 1 + PApplet.floor((boxHeight - topAndBottom) / textLeading); + int lineCount = Math.min(textBreakCount, lineFitCount); + + if (textAlignY == CENTER) { + float lineHigh = textAscent() + textLeading * (lineCount - 1); + float y = y1 + textAscent() + (boxHeight - lineHigh) / 2; + for (int i = 0; i < lineCount; i++) { + textLineAlignImpl(textBuffer, textBreakStart[i], textBreakStop[i], lineX, y); + y += textLeading; + } + + } else if (textAlignY == BOTTOM) { + float y = y2 - textDescent() - textLeading * (lineCount - 1); + for (int i = 0; i < lineCount; i++) { + textLineAlignImpl(textBuffer, textBreakStart[i], textBreakStop[i], lineX, y); + y += textLeading; + } + + } else { // TOP or BASELINE just go to the default + float y = y1 + textAscent(); + for (int i = 0; i < lineCount; i++) { + textLineAlignImpl(textBuffer, textBreakStart[i], textBreakStop[i], lineX, y); + y += textLeading; + } + } + } + + + /** + * Emit a sentence of text, defined as a chunk of text without any newlines. + * @param stop non-inclusive, the end of the text in question + * @return false if cannot fit + */ + protected boolean textSentence(char[] buffer, int start, int stop, + float boxWidth, float spaceWidth) { + float runningX = 0; + + // Keep track of this separately from index, since we'll need to back up + // from index when breaking words that are too long to fit. + int lineStart = start; + int wordStart = start; + int index = start; + while (index <= stop) { + // boundary of a word or end of this sentence + if ((buffer[index] == ' ') || (index == stop)) { +// System.out.println((index == stop) + " " + wordStart + " " + index); + float wordWidth = 0; + if (index > wordStart) { + // we have a non-empty word, measure it + wordWidth = textWidthImpl(buffer, wordStart, index); + } + + if (runningX + wordWidth >= boxWidth) { + if (runningX != 0) { + // Next word is too big, output the current line and advance + index = wordStart; + textSentenceBreak(lineStart, index); + // Eat whitespace before the first word on the next line. + while ((index < stop) && (buffer[index] == ' ')) { + index++; + } + } else { // (runningX == 0) + // If this is the first word on the line, and its width is greater + // than the width of the text box, then break the word where at the + // max width, and send the rest of the word to the next line. + if (index - wordStart < 25) { + do { + index--; + if (index == wordStart) { + // Not a single char will fit on this line. screw 'em. + return false; + } + wordWidth = textWidthImpl(buffer, wordStart, index); + } while (wordWidth > boxWidth); + } else { + // This word is more than 25 characters long, might be faster to + // start from the beginning of the text rather than shaving from + // the end of it, which is super slow if it's 1000s of letters. + // https://github.com/processing/processing/issues/211 + int lastIndex = index; + index = wordStart + 1; + // walk to the right while things fit + while ((wordWidth = textWidthImpl(buffer, wordStart, index)) < boxWidth) { + index++; + if (index > lastIndex) { // Unreachable? + break; + } + } + index--; + if (index == wordStart) { + return false; // nothing fits + } + } + + //textLineImpl(buffer, lineStart, index, x, y); + textSentenceBreak(lineStart, index); + } + lineStart = index; + wordStart = index; + runningX = 0; + + } else if (index == stop) { + // last line in the block, time to unload + //textLineImpl(buffer, lineStart, index, x, y); + textSentenceBreak(lineStart, index); +// y += textLeading; + index++; + + } else { // this word will fit, just add it to the line + runningX += wordWidth; + wordStart = index ; // move on to the next word including the space before the word + index++; + } + } else { // not a space or the last character + index++; // this is just another letter + } + } +// return y; + return true; + } + + + protected void textSentenceBreak(int start, int stop) { + if (textBreakCount == textBreakStart.length) { + textBreakStart = PApplet.expand(textBreakStart); + textBreakStop = PApplet.expand(textBreakStop); + } + textBreakStart[textBreakCount] = start; + textBreakStop[textBreakCount] = stop; + textBreakCount++; + } + + +// public void text(String s, float a, float b, float c, float d, float z) { +// if (z != 0) translate(0, 0, z); // slowness, badness +// +// text(s, a, b, c, d); +// textZ = z; +// +// if (z != 0) translate(0, 0, -z); // TEMPORARY HACK! SLOW! +// } + + + public void text(int num, float x, float y) { + text(String.valueOf(num), x, y); + } + + + public void text(int num, float x, float y, float z) { + text(String.valueOf(num), x, y, z); + } + + + /** + * This does a basic number formatting, to avoid the + * generally ugly appearance of printing floats. + * Users who want more control should use their own nf() cmmand, + * or if they want the long, ugly version of float, + * use String.valueOf() to convert the float to a String first. + * + * @param num the numeric value to be displayed + */ + public void text(float num, float x, float y) { + text(PApplet.nfs(num, 0, 3), x, y); + } + + + public void text(float num, float x, float y, float z) { + text(PApplet.nfs(num, 0, 3), x, y, z); + } + + + ////////////////////////////////////////////////////////////// + + // TEXT IMPL + + // These are most likely to be overridden by subclasses, since the other + // (public) functions handle generic features like setting alignment. + + + /** + * Handles placement of a text line, then calls textLineImpl + * to actually render at the specific point. + */ + protected void textLineAlignImpl(char buffer[], int start, int stop, + float x, float y) { + if (textAlign == CENTER) { + x -= textWidthImpl(buffer, start, stop) / 2f; + + } else if (textAlign == RIGHT) { + x -= textWidthImpl(buffer, start, stop); + } + + textLineImpl(buffer, start, stop, x, y); + } + + + /** + * Implementation of actual drawing for a line of text. + */ + protected void textLineImpl(char buffer[], int start, int stop, + float x, float y) { + for (int index = start; index < stop; index++) { + textCharImpl(buffer[index], x, y); + + // this doesn't account for kerning + x += textWidth(buffer[index]); + } +// textX = x; +// textY = y; +// textZ = 0; // this will get set by the caller if non-zero + } + + + protected void textCharImpl(char ch, float x, float y) { //, float z) { + PFont.Glyph glyph = textFont.getGlyph(ch); + if (glyph != null) { + if (textMode == MODEL) { + float high = glyph.height / (float) textFont.getSize(); + float bwidth = glyph.width / (float) textFont.getSize(); + float lextent = glyph.leftExtent / (float) textFont.getSize(); + float textent = glyph.topExtent / (float) textFont.getSize(); + + float x1 = x + lextent * textSize; + float y1 = y - textent * textSize; + float x2 = x1 + bwidth * textSize; + float y2 = y1 + high * textSize; + + textCharModelImpl(glyph.image, + x1, y1, x2, y2, + glyph.width, glyph.height); + } + } else if (ch != ' ' && ch != 127) { + showWarning("No glyph found for the " + ch + " (\\u" + PApplet.hex(ch, 4) + ") character"); + } + } + + + protected void textCharModelImpl(PImage glyph, + float x1, float y1, //float z1, + float x2, float y2, //float z2, + int u2, int v2) { + boolean savedTint = tint; + int savedTintColor = tintColor; + float savedTintR = tintR; + float savedTintG = tintG; + float savedTintB = tintB; + float savedTintA = tintA; + boolean savedTintAlpha = tintAlpha; + + tint = true; + tintColor = fillColor; + tintR = fillR; + tintG = fillG; + tintB = fillB; + tintA = fillA; + tintAlpha = fillAlpha; + + imageImpl(glyph, x1, y1, x2, y2, 0, 0, u2, v2); + + tint = savedTint; + tintColor = savedTintColor; + tintR = savedTintR; + tintG = savedTintG; + tintB = savedTintB; + tintA = savedTintA; + tintAlpha = savedTintAlpha; + } + + + /* + protected void textCharScreenImpl(PImage glyph, + int xx, int yy, + int w0, int h0) { + int x0 = 0; + int y0 = 0; + + if ((xx >= width) || (yy >= height) || + (xx + w0 < 0) || (yy + h0 < 0)) return; + + if (xx < 0) { + x0 -= xx; + w0 += xx; + xx = 0; + } + if (yy < 0) { + y0 -= yy; + h0 += yy; + yy = 0; + } + if (xx + w0 > width) { + w0 -= ((xx + w0) - width); + } + if (yy + h0 > height) { + h0 -= ((yy + h0) - height); + } + + int fr = fillRi; + int fg = fillGi; + int fb = fillBi; + int fa = fillAi; + + int pixels1[] = glyph.pixels; //images[glyph].pixels; + + // TODO this can be optimized a bit + for (int row = y0; row < y0 + h0; row++) { + for (int col = x0; col < x0 + w0; col++) { + //int a1 = (fa * pixels1[row * textFont.twidth + col]) >> 8; + int a1 = (fa * pixels1[row * glyph.width + col]) >> 8; + int a2 = a1 ^ 0xff; + //int p1 = pixels1[row * glyph.width + col]; + int p2 = pixels[(yy + row-y0)*width + (xx+col-x0)]; + + pixels[(yy + row-y0)*width + xx+col-x0] = + (0xff000000 | + (((a1 * fr + a2 * ((p2 >> 16) & 0xff)) & 0xff00) << 8) | + (( a1 * fg + a2 * ((p2 >> 8) & 0xff)) & 0xff00) | + (( a1 * fb + a2 * ( p2 & 0xff)) >> 8)); + } + } + } + */ + + +// /** +// * Convenience method to get a legit FontMetrics object. Where possible, +// * override this any renderer subclass so that you're not using what's +// * returned by getDefaultToolkit() to get your metrics. +// */ +// @SuppressWarnings("deprecation") +// public FontMetrics getFontMetrics(Font font) { // ignore +// Frame frame = parent.frame; +// if (frame != null) { +// return frame.getToolkit().getFontMetrics(font); +// } +// return Toolkit.getDefaultToolkit().getFontMetrics(font); +// } +// +// +// /** +// * Convenience method to jump through some Java2D hoops and get an FRC. +// */ +// public FontRenderContext getFontRenderContext(Font font) { // ignore +// return getFontMetrics(font).getFontRenderContext(); +// } + + + + ////////////////////////////////////////////////////////////// + + // MATRIX STACK + + + /** + * ( begin auto-generated from pushMatrix.xml ) + * + * Pushes the current transformation matrix onto the matrix stack. + * Understanding pushMatrix() and popMatrix() requires + * understanding the concept of a matrix stack. The pushMatrix() + * function saves the current coordinate system to the stack and + * popMatrix() restores the prior coordinate system. + * pushMatrix() and popMatrix() are used in conjuction with + * the other transformation functions and may be embedded to control the + * scope of the transformations. + * + * ( end auto-generated ) + * + * @webref transform + * @see PGraphics#popMatrix() + * @see PGraphics#translate(float, float, float) + * @see PGraphics#scale(float) + * @see PGraphics#rotate(float) + * @see PGraphics#rotateX(float) + * @see PGraphics#rotateY(float) + * @see PGraphics#rotateZ(float) + */ + public void pushMatrix() { + showMethodWarning("pushMatrix"); + } + + + /** + * ( begin auto-generated from popMatrix.xml ) + * + * Pops the current transformation matrix off the matrix stack. + * Understanding pushing and popping requires understanding the concept of + * a matrix stack. The pushMatrix() function saves the current + * coordinate system to the stack and popMatrix() restores the prior + * coordinate system. pushMatrix() and popMatrix() are used + * in conjuction with the other transformation functions and may be + * embedded to control the scope of the transformations. + * + * ( end auto-generated ) + * + * @webref transform + * @see PGraphics#pushMatrix() + */ + public void popMatrix() { + showMethodWarning("popMatrix"); + } + + + + ////////////////////////////////////////////////////////////// + + // MATRIX TRANSFORMATIONS + + + /** + * ( begin auto-generated from translate.xml ) + * + * Specifies an amount to displace objects within the display window. The + * x parameter specifies left/right translation, the y + * parameter specifies up/down translation, and the z parameter + * specifies translations toward/away from the screen. Using this function + * with the z parameter requires using P3D as a parameter in + * combination with size as shown in the above example. Transformations + * apply to everything that happens after and subsequent calls to the + * function accumulates the effect. For example, calling translate(50, + * 0) and then translate(20, 0) is the same as translate(70, + * 0). If translate() is called within draw(), the + * transformation is reset when the loop begins again. This function can be + * further controlled by the pushMatrix() and popMatrix(). + * + * ( end auto-generated ) + * + * @webref transform + * @param x left/right translation + * @param y up/down translation + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#rotate(float) + * @see PGraphics#rotateX(float) + * @see PGraphics#rotateY(float) + * @see PGraphics#rotateZ(float) + * @see PGraphics#scale(float, float, float) + */ + public void translate(float x, float y) { + showMissingWarning("translate"); + } + + + /** + * @param z forward/backward translation + */ + public void translate(float x, float y, float z) { + showMissingWarning("translate"); + } + + + /** + * ( begin auto-generated from rotate.xml ) + * + * Rotates a shape the amount specified by the angle parameter. + * Angles should be specified in radians (values from 0 to TWO_PI) or + * converted to radians with the radians() function. + *

        + * Objects are always rotated around their relative position to the origin + * and positive numbers rotate objects in a clockwise direction. + * Transformations apply to everything that happens after and subsequent + * calls to the function accumulates the effect. For example, calling + * rotate(HALF_PI) and then rotate(HALF_PI) is the same as + * rotate(PI). All tranformations are reset when draw() + * begins again. + *

        + * Technically, rotate() multiplies the current transformation + * matrix by a rotation matrix. This function can be further controlled by + * the pushMatrix() and popMatrix(). + * + * ( end auto-generated ) + * + * @webref transform + * @param angle angle of rotation specified in radians + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#rotateX(float) + * @see PGraphics#rotateY(float) + * @see PGraphics#rotateZ(float) + * @see PGraphics#scale(float, float, float) + * @see PApplet#radians(float) + */ + public void rotate(float angle) { + showMissingWarning("rotate"); + } + + + /** + * ( begin auto-generated from rotateX.xml ) + * + * Rotates a shape around the x-axis the amount specified by the + * angle parameter. Angles should be specified in radians (values + * from 0 to PI*2) or converted to radians with the radians() + * function. Objects are always rotated around their relative position to + * the origin and positive numbers rotate objects in a counterclockwise + * direction. Transformations apply to everything that happens after and + * subsequent calls to the function accumulates the effect. For example, + * calling rotateX(PI/2) and then rotateX(PI/2) is the same + * as rotateX(PI). If rotateX() is called within the + * draw(), the transformation is reset when the loop begins again. + * This function requires using P3D as a third parameter to size() + * as shown in the example above. + * + * ( end auto-generated ) + * + * @webref transform + * @param angle angle of rotation specified in radians + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#rotate(float) + * @see PGraphics#rotateY(float) + * @see PGraphics#rotateZ(float) + * @see PGraphics#scale(float, float, float) + * @see PGraphics#translate(float, float, float) + */ + public void rotateX(float angle) { + showMethodWarning("rotateX"); + } + + + /** + * ( begin auto-generated from rotateY.xml ) + * + * Rotates a shape around the y-axis the amount specified by the + * angle parameter. Angles should be specified in radians (values + * from 0 to PI*2) or converted to radians with the radians() + * function. Objects are always rotated around their relative position to + * the origin and positive numbers rotate objects in a counterclockwise + * direction. Transformations apply to everything that happens after and + * subsequent calls to the function accumulates the effect. For example, + * calling rotateY(PI/2) and then rotateY(PI/2) is the same + * as rotateY(PI). If rotateY() is called within the + * draw(), the transformation is reset when the loop begins again. + * This function requires using P3D as a third parameter to size() + * as shown in the examples above. + * + * ( end auto-generated ) + * + * @webref transform + * @param angle angle of rotation specified in radians + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#rotate(float) + * @see PGraphics#rotateX(float) + * @see PGraphics#rotateZ(float) + * @see PGraphics#scale(float, float, float) + * @see PGraphics#translate(float, float, float) + */ + public void rotateY(float angle) { + showMethodWarning("rotateY"); + } + + + /** + * ( begin auto-generated from rotateZ.xml ) + * + * Rotates a shape around the z-axis the amount specified by the + * angle parameter. Angles should be specified in radians (values + * from 0 to PI*2) or converted to radians with the radians() + * function. Objects are always rotated around their relative position to + * the origin and positive numbers rotate objects in a counterclockwise + * direction. Transformations apply to everything that happens after and + * subsequent calls to the function accumulates the effect. For example, + * calling rotateZ(PI/2) and then rotateZ(PI/2) is the same + * as rotateZ(PI). If rotateZ() is called within the + * draw(), the transformation is reset when the loop begins again. + * This function requires using P3D as a third parameter to size() + * as shown in the examples above. + * + * ( end auto-generated ) + * + * @webref transform + * @param angle angle of rotation specified in radians + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#rotate(float) + * @see PGraphics#rotateX(float) + * @see PGraphics#rotateY(float) + * @see PGraphics#scale(float, float, float) + * @see PGraphics#translate(float, float, float) + */ + public void rotateZ(float angle) { + showMethodWarning("rotateZ"); + } + + + /** + *

        Advanced

        + * Rotate about a vector in space. Same as the glRotatef() function. + * @nowebref + * @param x + * @param y + * @param z + */ + public void rotate(float angle, float x, float y, float z) { + showMissingWarning("rotate"); + } + + + /** + * ( begin auto-generated from scale.xml ) + * + * Increases or decreases the size of a shape by expanding and contracting + * vertices. Objects always scale from their relative origin to the + * coordinate system. Scale values are specified as decimal percentages. + * For example, the function call scale(2.0) increases the dimension + * of a shape by 200%. Transformations apply to everything that happens + * after and subsequent calls to the function multiply the effect. For + * example, calling scale(2.0) and then scale(1.5) is the + * same as scale(3.0). If scale() is called within + * draw(), the transformation is reset when the loop begins again. + * Using this fuction with the z parameter requires using P3D as a + * parameter for size() as shown in the example above. This function + * can be further controlled by pushMatrix() and popMatrix(). + * + * ( end auto-generated ) + * + * @webref transform + * @param s percentage to scale the object + * @see PGraphics#pushMatrix() + * @see PGraphics#popMatrix() + * @see PGraphics#translate(float, float, float) + * @see PGraphics#rotate(float) + * @see PGraphics#rotateX(float) + * @see PGraphics#rotateY(float) + * @see PGraphics#rotateZ(float) + */ + public void scale(float s) { + showMissingWarning("scale"); + } + + + /** + *

        Advanced

        + * Scale in X and Y. Equivalent to scale(sx, sy, 1). + * + * Not recommended for use in 3D, because the z-dimension is just + * scaled by 1, since there's no way to know what else to scale it by. + * + * @param x percentage to scale the object in the x-axis + * @param y percentage to scale the object in the y-axis + */ + public void scale(float x, float y) { + showMissingWarning("scale"); + } + + + /** + * @param z percentage to scale the object in the z-axis + */ + public void scale(float x, float y, float z) { + showMissingWarning("scale"); + } + + + /** + * ( begin auto-generated from shearX.xml ) + * + * Shears a shape around the x-axis the amount specified by the + * angle parameter. Angles should be specified in radians (values + * from 0 to PI*2) or converted to radians with the radians() + * function. Objects are always sheared around their relative position to + * the origin and positive numbers shear objects in a clockwise direction. + * Transformations apply to everything that happens after and subsequent + * calls to the function accumulates the effect. For example, calling + * shearX(PI/2) and then shearX(PI/2) is the same as + * shearX(PI). If shearX() is called within the + * draw(), the transformation is reset when the loop begins again. + *

        + * Technically, shearX() multiplies the current transformation + * matrix by a rotation matrix. This function can be further controlled by + * the pushMatrix() and popMatrix() functions. + * + * ( end auto-generated ) + * + * @webref transform + * @param angle angle of shear specified in radians + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#shearY(float) + * @see PGraphics#scale(float, float, float) + * @see PGraphics#translate(float, float, float) + * @see PApplet#radians(float) + */ + public void shearX(float angle) { + showMissingWarning("shearX"); + } + + + /** + * ( begin auto-generated from shearY.xml ) + * + * Shears a shape around the y-axis the amount specified by the + * angle parameter. Angles should be specified in radians (values + * from 0 to PI*2) or converted to radians with the radians() + * function. Objects are always sheared around their relative position to + * the origin and positive numbers shear objects in a clockwise direction. + * Transformations apply to everything that happens after and subsequent + * calls to the function accumulates the effect. For example, calling + * shearY(PI/2) and then shearY(PI/2) is the same as + * shearY(PI). If shearY() is called within the + * draw(), the transformation is reset when the loop begins again. + *

        + * Technically, shearY() multiplies the current transformation + * matrix by a rotation matrix. This function can be further controlled by + * the pushMatrix() and popMatrix() functions. + * + * ( end auto-generated ) + * + * @webref transform + * @param angle angle of shear specified in radians + * @see PGraphics#popMatrix() + * @see PGraphics#pushMatrix() + * @see PGraphics#shearX(float) + * @see PGraphics#scale(float, float, float) + * @see PGraphics#translate(float, float, float) + * @see PApplet#radians(float) + */ + public void shearY(float angle) { + showMissingWarning("shearY"); + } + + + ////////////////////////////////////////////////////////////// + + // MATRIX FULL MONTY + + + /** + * ( begin auto-generated from resetMatrix.xml ) + * + * Replaces the current matrix with the identity matrix. The equivalent + * function in OpenGL is glLoadIdentity(). + * + * ( end auto-generated ) + * + * @webref transform + * @see PGraphics#pushMatrix() + * @see PGraphics#popMatrix() + * @see PGraphics#applyMatrix(PMatrix) + * @see PGraphics#printMatrix() + */ + public void resetMatrix() { + showMethodWarning("resetMatrix"); + } + + /** + * ( begin auto-generated from applyMatrix.xml ) + * + * Multiplies the current matrix by the one specified through the + * parameters. This is very slow because it will try to calculate the + * inverse of the transform, so avoid it whenever possible. The equivalent + * function in OpenGL is glMultMatrix(). + * + * ( end auto-generated ) + * + * @webref transform + * @source + * @see PGraphics#pushMatrix() + * @see PGraphics#popMatrix() + * @see PGraphics#resetMatrix() + * @see PGraphics#printMatrix() + */ + public void applyMatrix(PMatrix source) { + if (source instanceof PMatrix2D) { + applyMatrix((PMatrix2D) source); + } else if (source instanceof PMatrix3D) { + applyMatrix((PMatrix3D) source); + } + } + + + public void applyMatrix(PMatrix2D source) { + applyMatrix(source.m00, source.m01, source.m02, + source.m10, source.m11, source.m12); + } + + + /** + * @param n00 numbers which define the 4x4 matrix to be multiplied + * @param n01 numbers which define the 4x4 matrix to be multiplied + * @param n02 numbers which define the 4x4 matrix to be multiplied + * @param n10 numbers which define the 4x4 matrix to be multiplied + * @param n11 numbers which define the 4x4 matrix to be multiplied + * @param n12 numbers which define the 4x4 matrix to be multiplied + */ + public void applyMatrix(float n00, float n01, float n02, + float n10, float n11, float n12) { + showMissingWarning("applyMatrix"); + } + + public void applyMatrix(PMatrix3D source) { + applyMatrix(source.m00, source.m01, source.m02, source.m03, + source.m10, source.m11, source.m12, source.m13, + source.m20, source.m21, source.m22, source.m23, + source.m30, source.m31, source.m32, source.m33); + } + + + /** + * @param n03 numbers which define the 4x4 matrix to be multiplied + * @param n13 numbers which define the 4x4 matrix to be multiplied + * @param n20 numbers which define the 4x4 matrix to be multiplied + * @param n21 numbers which define the 4x4 matrix to be multiplied + * @param n22 numbers which define the 4x4 matrix to be multiplied + * @param n23 numbers which define the 4x4 matrix to be multiplied + * @param n30 numbers which define the 4x4 matrix to be multiplied + * @param n31 numbers which define the 4x4 matrix to be multiplied + * @param n32 numbers which define the 4x4 matrix to be multiplied + * @param n33 numbers which define the 4x4 matrix to be multiplied + */ + public void applyMatrix(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + showMissingWarning("applyMatrix"); + } + + + + ////////////////////////////////////////////////////////////// + + // MATRIX GET/SET/PRINT + + + public PMatrix getMatrix() { + showMissingWarning("getMatrix"); + return null; + } + + + /** + * Copy the current transformation matrix into the specified target. + * Pass in null to create a new matrix. + */ + public PMatrix2D getMatrix(PMatrix2D target) { + showMissingWarning("getMatrix"); + return null; + } + + + /** + * Copy the current transformation matrix into the specified target. + * Pass in null to create a new matrix. + */ + public PMatrix3D getMatrix(PMatrix3D target) { + showMissingWarning("getMatrix"); + return null; + } + + + /** + * Set the current transformation matrix to the contents of another. + */ + public void setMatrix(PMatrix source) { + if (source instanceof PMatrix2D) { + setMatrix((PMatrix2D) source); + } else if (source instanceof PMatrix3D) { + setMatrix((PMatrix3D) source); + } + } + + + /** + * Set the current transformation to the contents of the specified source. + */ + public void setMatrix(PMatrix2D source) { + showMissingWarning("setMatrix"); + } + + + /** + * Set the current transformation to the contents of the specified source. + */ + public void setMatrix(PMatrix3D source) { + showMissingWarning("setMatrix"); + } + + + /** + * ( begin auto-generated from printMatrix.xml ) + * + * Prints the current matrix to the Console (the text window at the bottom + * of Processing). + * + * ( end auto-generated ) + * + * @webref transform + * @see PGraphics#pushMatrix() + * @see PGraphics#popMatrix() + * @see PGraphics#resetMatrix() + * @see PGraphics#applyMatrix(PMatrix) + */ + public void printMatrix() { + showMethodWarning("printMatrix"); + } + + + ////////////////////////////////////////////////////////////// + + // CAMERA + + /** + * ( begin auto-generated from beginCamera.xml ) + * + * The beginCamera() and endCamera() functions enable + * advanced customization of the camera space. The functions are useful if + * you want to more control over camera movement, however for most users, + * the camera() function will be sufficient.

        The camera + * functions will replace any transformations (such as rotate() or + * translate()) that occur before them in draw(), but they + * will not automatically replace the camera transform itself. For this + * reason, camera functions should be placed at the beginning of + * draw() (so that transformations happen afterwards), and the + * camera() function can be used after beginCamera() if you + * want to reset the camera before applying transformations.

        This function sets the matrix mode to the camera matrix so calls such + * as translate(), rotate(), applyMatrix() and resetMatrix() + * affect the camera. beginCamera() should always be used with a + * following endCamera() and pairs of beginCamera() and + * endCamera() cannot be nested. + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + * @see PGraphics#camera() + * @see PGraphics#endCamera() + * @see PGraphics#applyMatrix(PMatrix) + * @see PGraphics#resetMatrix() + * @see PGraphics#translate(float, float, float) + * @see PGraphics#scale(float, float, float) + */ + public void beginCamera() { + showMethodWarning("beginCamera"); + } + + /** + * ( begin auto-generated from endCamera.xml ) + * + * The beginCamera() and endCamera() functions enable + * advanced customization of the camera space. Please see the reference for + * beginCamera() for a description of how the functions are used. + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + * @see PGraphics#beginCamera() + * @see PGraphics#camera(float, float, float, float, float, float, float, float, float) + */ + public void endCamera() { + showMethodWarning("endCamera"); + } + + /** + * ( begin auto-generated from camera.xml ) + * + * Sets the position of the camera through setting the eye position, the + * center of the scene, and which axis is facing upward. Moving the eye + * position and the direction it is pointing (the center of the scene) + * allows the images to be seen from different angles. The version without + * any parameters sets the camera to the default position, pointing to the + * center of the display window with the Y axis as up. The default values + * are camera(width/2.0, height/2.0, (height/2.0) / tan(PI*30.0 / + * 180.0), width/2.0, height/2.0, 0, 0, 1, 0). This function is similar + * to gluLookAt() in OpenGL, but it first clears the current camera settings. + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + * @see PGraphics#beginCamera() + * @see PGraphics#endCamera() + * @see PGraphics#frustum(float, float, float, float, float, float) + */ + public void camera() { + showMissingWarning("camera"); + } + +/** + * @param eyeX x-coordinate for the eye + * @param eyeY y-coordinate for the eye + * @param eyeZ z-coordinate for the eye + * @param centerX x-coordinate for the center of the scene + * @param centerY y-coordinate for the center of the scene + * @param centerZ z-coordinate for the center of the scene + * @param upX usually 0.0, 1.0, or -1.0 + * @param upY usually 0.0, 1.0, or -1.0 + * @param upZ usually 0.0, 1.0, or -1.0 + */ + public void camera(float eyeX, float eyeY, float eyeZ, + float centerX, float centerY, float centerZ, + float upX, float upY, float upZ) { + showMissingWarning("camera"); + } + +/** + * ( begin auto-generated from printCamera.xml ) + * + * Prints the current camera matrix to the Console (the text window at the + * bottom of Processing). + * + * ( end auto-generated ) + * @webref lights_camera:camera + * @see PGraphics#camera(float, float, float, float, float, float, float, float, float) + */ + public void printCamera() { + showMethodWarning("printCamera"); + } + + + + ////////////////////////////////////////////////////////////// + + // PROJECTION + + /** + * ( begin auto-generated from ortho.xml ) + * + * Sets an orthographic projection and defines a parallel clipping volume. + * All objects with the same dimension appear the same size, regardless of + * whether they are near or far from the camera. The parameters to this + * function specify the clipping volume where left and right are the + * minimum and maximum x values, top and bottom are the minimum and maximum + * y values, and near and far are the minimum and maximum z values. If no + * parameters are given, the default is used: ortho(0, width, 0, height, + * -10, 10). + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + */ + public void ortho() { + showMissingWarning("ortho"); + } + + /** + * @param left left plane of the clipping volume + * @param right right plane of the clipping volume + * @param bottom bottom plane of the clipping volume + * @param top top plane of the clipping volume + */ + public void ortho(float left, float right, + float bottom, float top) { + showMissingWarning("ortho"); + } + + /** + * @param near maximum distance from the origin to the viewer + * @param far maximum distance from the origin away from the viewer + */ + public void ortho(float left, float right, + float bottom, float top, + float near, float far) { + showMissingWarning("ortho"); + } + + /** + * ( begin auto-generated from perspective.xml ) + * + * Sets a perspective projection applying foreshortening, making distant + * objects appear smaller than closer ones. The parameters define a viewing + * volume with the shape of truncated pyramid. Objects near to the front of + * the volume appear their actual size, while farther objects appear + * smaller. This projection simulates the perspective of the world more + * accurately than orthographic projection. The version of perspective + * without parameters sets the default perspective and the version with + * four parameters allows the programmer to set the area precisely. The + * default values are: perspective(PI/3.0, width/height, cameraZ/10.0, + * cameraZ*10.0) where cameraZ is ((height/2.0) / tan(PI*60.0/360.0)); + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + */ + public void perspective() { + showMissingWarning("perspective"); + } + + /** + * @param fovy field-of-view angle (in radians) for vertical direction + * @param aspect ratio of width to height + * @param zNear z-position of nearest clipping plane + * @param zFar z-position of farthest clipping plane + */ + public void perspective(float fovy, float aspect, float zNear, float zFar) { + showMissingWarning("perspective"); + } + + /** + * ( begin auto-generated from frustum.xml ) + * + * Sets a perspective matrix defined through the parameters. Works like + * glFrustum, except it wipes out the current perspective matrix rather + * than muliplying itself with it. + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + * @param left left coordinate of the clipping plane + * @param right right coordinate of the clipping plane + * @param bottom bottom coordinate of the clipping plane + * @param top top coordinate of the clipping plane + * @param near near component of the clipping plane; must be greater than zero + * @param far far component of the clipping plane; must be greater than the near value + * @see PGraphics#camera(float, float, float, float, float, float, float, float, float) + * @see PGraphics#beginCamera() + * @see PGraphics#endCamera() + * @see PGraphics#perspective(float, float, float, float) + */ + public void frustum(float left, float right, + float bottom, float top, + float near, float far) { + showMethodWarning("frustum"); + } + + /** + * ( begin auto-generated from printProjection.xml ) + * + * Prints the current projection matrix to the Console (the text window at + * the bottom of Processing). + * + * ( end auto-generated ) + * + * @webref lights_camera:camera + * @see PGraphics#camera(float, float, float, float, float, float, float, float, float) + */ + public void printProjection() { + showMethodWarning("printProjection"); + } + + + + ////////////////////////////////////////////////////////////// + + // SCREEN TRANSFORMS + + + /** + * ( begin auto-generated from screenX.xml ) + * + * Takes a three-dimensional X, Y, Z position and returns the X value for + * where it will appear on a (two-dimensional) screen. + * + * ( end auto-generated ) + * + * @webref lights_camera:coordinates + * @param x 3D x-coordinate to be mapped + * @param y 3D y-coordinate to be mapped + * @see PGraphics#screenY(float, float, float) + * @see PGraphics#screenZ(float, float, float) + */ + public float screenX(float x, float y) { + showMissingWarning("screenX"); + return 0; + } + + + /** + * ( begin auto-generated from screenY.xml ) + * + * Takes a three-dimensional X, Y, Z position and returns the Y value for + * where it will appear on a (two-dimensional) screen. + * + * ( end auto-generated ) + * + * @webref lights_camera:coordinates + * @param x 3D x-coordinate to be mapped + * @param y 3D y-coordinate to be mapped + * @see PGraphics#screenX(float, float, float) + * @see PGraphics#screenZ(float, float, float) + */ + public float screenY(float x, float y) { + showMissingWarning("screenY"); + return 0; + } + + + /** + * @param z 3D z-coordinate to be mapped + */ + public float screenX(float x, float y, float z) { + showMissingWarning("screenX"); + return 0; + } + + + /** + * @param z 3D z-coordinate to be mapped + */ + public float screenY(float x, float y, float z) { + showMissingWarning("screenY"); + return 0; + } + + + + /** + * ( begin auto-generated from screenZ.xml ) + * + * Takes a three-dimensional X, Y, Z position and returns the Z value for + * where it will appear on a (two-dimensional) screen. + * + * ( end auto-generated ) + * + * @webref lights_camera:coordinates + * @param x 3D x-coordinate to be mapped + * @param y 3D y-coordinate to be mapped + * @param z 3D z-coordinate to be mapped + * @see PGraphics#screenX(float, float, float) + * @see PGraphics#screenY(float, float, float) + */ + public float screenZ(float x, float y, float z) { + showMissingWarning("screenZ"); + return 0; + } + + + /** + * ( begin auto-generated from modelX.xml ) + * + * Returns the three-dimensional X, Y, Z position in model space. This + * returns the X value for a given coordinate based on the current set of + * transformations (scale, rotate, translate, etc.) The X value can be used + * to place an object in space relative to the location of the original + * point once the transformations are no longer in use. + *

        + * In the example, the modelX(), modelY(), and + * modelZ() functions record the location of a box in space after + * being placed using a series of translate and rotate commands. After + * popMatrix() is called, those transformations no longer apply, but the + * (x, y, z) coordinate returned by the model functions is used to place + * another box in the same location. + * + * ( end auto-generated ) + * + * @webref lights_camera:coordinates + * @param x 3D x-coordinate to be mapped + * @param y 3D y-coordinate to be mapped + * @param z 3D z-coordinate to be mapped + * @see PGraphics#modelY(float, float, float) + * @see PGraphics#modelZ(float, float, float) + */ + public float modelX(float x, float y, float z) { + showMissingWarning("modelX"); + return 0; + } + + + /** + * ( begin auto-generated from modelY.xml ) + * + * Returns the three-dimensional X, Y, Z position in model space. This + * returns the Y value for a given coordinate based on the current set of + * transformations (scale, rotate, translate, etc.) The Y value can be used + * to place an object in space relative to the location of the original + * point once the transformations are no longer in use.
        + *
        + * In the example, the modelX(), modelY(), and + * modelZ() functions record the location of a box in space after + * being placed using a series of translate and rotate commands. After + * popMatrix() is called, those transformations no longer apply, but the + * (x, y, z) coordinate returned by the model functions is used to place + * another box in the same location. + * + * ( end auto-generated ) + * + * @webref lights_camera:coordinates + * @param x 3D x-coordinate to be mapped + * @param y 3D y-coordinate to be mapped + * @param z 3D z-coordinate to be mapped + * @see PGraphics#modelX(float, float, float) + * @see PGraphics#modelZ(float, float, float) + */ + public float modelY(float x, float y, float z) { + showMissingWarning("modelY"); + return 0; + } + + + /** + * ( begin auto-generated from modelZ.xml ) + * + * Returns the three-dimensional X, Y, Z position in model space. This + * returns the Z value for a given coordinate based on the current set of + * transformations (scale, rotate, translate, etc.) The Z value can be used + * to place an object in space relative to the location of the original + * point once the transformations are no longer in use.
        + *
        + * In the example, the modelX(), modelY(), and + * modelZ() functions record the location of a box in space after + * being placed using a series of translate and rotate commands. After + * popMatrix() is called, those transformations no longer apply, but the + * (x, y, z) coordinate returned by the model functions is used to place + * another box in the same location. + * + * ( end auto-generated ) + * + * @webref lights_camera:coordinates + * @param x 3D x-coordinate to be mapped + * @param y 3D y-coordinate to be mapped + * @param z 3D z-coordinate to be mapped + * @see PGraphics#modelX(float, float, float) + * @see PGraphics#modelY(float, float, float) + */ + public float modelZ(float x, float y, float z) { + showMissingWarning("modelZ"); + return 0; + } + + + + ////////////////////////////////////////////////////////////// + + // STYLE + + /** + * ( begin auto-generated from pushStyle.xml ) + * + * The pushStyle() function saves the current style settings and + * popStyle() restores the prior settings. Note that these functions + * are always used together. They allow you to change the style settings + * and later return to what you had. When a new style is started with + * pushStyle(), it builds on the current style information. The + * pushStyle() and popStyle() functions can be embedded to + * provide more control (see the second example above for a demonstration.) + *

        + * The style information controlled by the following functions are included + * in the style: + * fill(), stroke(), tint(), strokeWeight(), strokeCap(), strokeJoin(), + * imageMode(), rectMode(), ellipseMode(), shapeMode(), colorMode(), + * textAlign(), textFont(), textMode(), textSize(), textLeading(), + * emissive(), specular(), shininess(), ambient() + * + * ( end auto-generated ) + * + * @webref structure + * @see PGraphics#popStyle() + */ + public void pushStyle() { + if (styleStackDepth == styleStack.length) { + styleStack = (PStyle[]) PApplet.expand(styleStack); + } + if (styleStack[styleStackDepth] == null) { + styleStack[styleStackDepth] = new PStyle(); + } + PStyle s = styleStack[styleStackDepth++]; + getStyle(s); + } + + /** + * ( begin auto-generated from popStyle.xml ) + * + * The pushStyle() function saves the current style settings and + * popStyle() restores the prior settings; these functions are + * always used together. They allow you to change the style settings and + * later return to what you had. When a new style is started with + * pushStyle(), it builds on the current style information. The + * pushStyle() and popStyle() functions can be embedded to + * provide more control (see the second example above for a demonstration.) + * + * ( end auto-generated ) + * + * @webref structure + * @see PGraphics#pushStyle() + */ + public void popStyle() { + if (styleStackDepth == 0) { + throw new RuntimeException("Too many popStyle() without enough pushStyle()"); + } + styleStackDepth--; + style(styleStack[styleStackDepth]); + } + + + public void style(PStyle s) { + // if (s.smooth) { + // smooth(); + // } else { + // noSmooth(); + // } + + imageMode(s.imageMode); + rectMode(s.rectMode); + ellipseMode(s.ellipseMode); + shapeMode(s.shapeMode); + + blendMode(s.blendMode); + + if (s.tint) { + tint(s.tintColor); + } else { + noTint(); + } + if (s.fill) { + fill(s.fillColor); + } else { + noFill(); + } + if (s.stroke) { + stroke(s.strokeColor); + } else { + noStroke(); + } + strokeWeight(s.strokeWeight); + strokeCap(s.strokeCap); + strokeJoin(s.strokeJoin); + + // Set the colorMode() for the material properties. + // TODO this is really inefficient, need to just have a material() method, + // but this has the least impact to the API. + colorMode(RGB, 1); + ambient(s.ambientR, s.ambientG, s.ambientB); + emissive(s.emissiveR, s.emissiveG, s.emissiveB); + specular(s.specularR, s.specularG, s.specularB); + shininess(s.shininess); + + /* + s.ambientR = ambientR; + s.ambientG = ambientG; + s.ambientB = ambientB; + s.specularR = specularR; + s.specularG = specularG; + s.specularB = specularB; + s.emissiveR = emissiveR; + s.emissiveG = emissiveG; + s.emissiveB = emissiveB; + s.shininess = shininess; + */ + // material(s.ambientR, s.ambientG, s.ambientB, + // s.emissiveR, s.emissiveG, s.emissiveB, + // s.specularR, s.specularG, s.specularB, + // s.shininess); + + // Set this after the material properties. + colorMode(s.colorMode, + s.colorModeX, s.colorModeY, s.colorModeZ, s.colorModeA); + + // This is a bit asymmetric, since there's no way to do "noFont()", + // and a null textFont will produce an error (since usually that means that + // the font couldn't load properly). So in some cases, the font won't be + // 'cleared' to null, even though that's technically correct. + if (s.textFont != null) { + textFont(s.textFont, s.textSize); + textLeading(s.textLeading); + } + // These don't require a font to be set. + textAlign(s.textAlign, s.textAlignY); + textMode(s.textMode); + } + + + public PStyle getStyle() { // ignore + return getStyle(null); + } + + + public PStyle getStyle(PStyle s) { // ignore + if (s == null) { + s = new PStyle(); + } + + s.imageMode = imageMode; + s.rectMode = rectMode; + s.ellipseMode = ellipseMode; + s.shapeMode = shapeMode; + + s.blendMode = blendMode; + + s.colorMode = colorMode; + s.colorModeX = colorModeX; + s.colorModeY = colorModeY; + s.colorModeZ = colorModeZ; + s.colorModeA = colorModeA; + + s.tint = tint; + s.tintColor = tintColor; + s.fill = fill; + s.fillColor = fillColor; + s.stroke = stroke; + s.strokeColor = strokeColor; + s.strokeWeight = strokeWeight; + s.strokeCap = strokeCap; + s.strokeJoin = strokeJoin; + + s.ambientR = ambientR; + s.ambientG = ambientG; + s.ambientB = ambientB; + s.specularR = specularR; + s.specularG = specularG; + s.specularB = specularB; + s.emissiveR = emissiveR; + s.emissiveG = emissiveG; + s.emissiveB = emissiveB; + s.shininess = shininess; + + s.textFont = textFont; + s.textAlign = textAlign; + s.textAlignY = textAlignY; + s.textMode = textMode; + s.textSize = textSize; + s.textLeading = textLeading; + + return s; + } + + + + ////////////////////////////////////////////////////////////// + + // STROKE CAP/JOIN/WEIGHT + + /** + * ( begin auto-generated from strokeWeight.xml ) + * + * Sets the width of the stroke used for lines, points, and the border + * around shapes. All widths are set in units of pixels. + *

        + * When drawing with P3D, series of connected lines (such as the stroke + * around a polygon, triangle, or ellipse) produce unattractive results + * when a thick stroke weight is set (see + * Issue 123). With P3D, the minimum and maximum values for + * strokeWeight() are controlled by the graphics card and the + * operating system's OpenGL implementation. For instance, the thickness + * may not go higher than 10 pixels. + * + * ( end auto-generated ) + * + * @webref shape:attributes + * @param weight the weight (in pixels) of the stroke + * @see PGraphics#stroke(int, float) + * @see PGraphics#strokeJoin(int) + * @see PGraphics#strokeCap(int) + */ + public void strokeWeight(float weight) { + strokeWeight = weight; + } + + /** + * ( begin auto-generated from strokeJoin.xml ) + * + * Sets the style of the joints which connect line segments. These joints + * are either mitered, beveled, or rounded and specified with the + * corresponding parameters MITER, BEVEL, and ROUND. The default joint is + * MITER. + *

        + * This function is not available with the P3D renderer, (see + * Issue 123). More information about the renderers can be found in the + * size() reference. + * + * ( end auto-generated ) + * + * @webref shape:attributes + * @param join either MITER, BEVEL, ROUND + * @see PGraphics#stroke(int, float) + * @see PGraphics#strokeWeight(float) + * @see PGraphics#strokeCap(int) + */ + public void strokeJoin(int join) { + strokeJoin = join; + } + + /** + * ( begin auto-generated from strokeCap.xml ) + * + * Sets the style for rendering line endings. These ends are either + * squared, extended, or rounded and specified with the corresponding + * parameters SQUARE, PROJECT, and ROUND. The default cap is ROUND. + *

        + * This function is not available with the P3D renderer (see + * Issue 123). More information about the renderers can be found in the + * size() reference. + * + * ( end auto-generated ) + * + * @webref shape:attributes + * @param cap either SQUARE, PROJECT, or ROUND + * @see PGraphics#stroke(int, float) + * @see PGraphics#strokeWeight(float) + * @see PGraphics#strokeJoin(int) + * @see PApplet#size(int, int, String, String) + */ + public void strokeCap(int cap) { + strokeCap = cap; + } + + + + ////////////////////////////////////////////////////////////// + + // STROKE COLOR + + + /** + * ( begin auto-generated from noStroke.xml ) + * + * Disables drawing the stroke (outline). If both noStroke() and + * noFill() are called, nothing will be drawn to the screen. + * + * ( end auto-generated ) + * + * @webref color:setting + * @see PGraphics#stroke(int, float) + * @see PGraphics#fill(float, float, float, float) + * @see PGraphics#noFill() + */ + public void noStroke() { + stroke = false; + } + + + /** + * ( begin auto-generated from stroke.xml ) + * + * Sets the color used to draw lines and borders around shapes. This color + * is either specified in terms of the RGB or HSB color depending on the + * current colorMode() (the default color space is RGB, with each + * value in the range from 0 to 255). + *

        + * When using hexadecimal notation to specify a color, use "#" or "0x" + * before the values (e.g. #CCFFAA, 0xFFCCFFAA). The # syntax uses six + * digits to specify a color (the way colors are specified in HTML and + * CSS). When using the hexadecimal notation starting with "0x", the + * hexadecimal value must be specified with eight characters; the first two + * characters define the alpha component and the remainder the red, green, + * and blue components. + *

        + * The value for the parameter "gray" must be less than or equal to the + * current maximum value as specified by colorMode(). The default + * maximum value is 255. + * + * ( end auto-generated ) + * + * @param rgb color value in hexadecimal notation + * @see PGraphics#noStroke() + * @see PGraphics#strokeWeight(float) + * @see PGraphics#strokeJoin(int) + * @see PGraphics#strokeCap(int) + * @see PGraphics#fill(int, float) + * @see PGraphics#noFill() + * @see PGraphics#tint(int, float) + * @see PGraphics#background(float, float, float, float) + * @see PGraphics#colorMode(int, float, float, float, float) + */ + public void stroke(int rgb) { + colorCalc(rgb); + strokeFromCalc(); + } + + + /** + * @param alpha opacity of the stroke + */ + public void stroke(int rgb, float alpha) { + colorCalc(rgb, alpha); + strokeFromCalc(); + } + + + /** + * @param gray specifies a value between white and black + */ + public void stroke(float gray) { + colorCalc(gray); + strokeFromCalc(); + } + + + public void stroke(float gray, float alpha) { + colorCalc(gray, alpha); + strokeFromCalc(); + } + + + /** + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + * @webref color:setting + */ + public void stroke(float v1, float v2, float v3) { + colorCalc(v1, v2, v3); + strokeFromCalc(); + } + + + public void stroke(float v1, float v2, float v3, float alpha) { + colorCalc(v1, v2, v3, alpha); + strokeFromCalc(); + } + + + protected void strokeFromCalc() { + stroke = true; + strokeR = calcR; + strokeG = calcG; + strokeB = calcB; + strokeA = calcA; + strokeRi = calcRi; + strokeGi = calcGi; + strokeBi = calcBi; + strokeAi = calcAi; + strokeColor = calcColor; + strokeAlpha = calcAlpha; + } + + + + ////////////////////////////////////////////////////////////// + + // TINT COLOR + + + /** + * ( begin auto-generated from noTint.xml ) + * + * Removes the current fill value for displaying images and reverts to + * displaying images with their original hues. + * + * ( end auto-generated ) + * + * @webref image:loading_displaying + * @usage web_application + * @see PGraphics#tint(float, float, float, float) + * @see PGraphics#image(PImage, float, float, float, float) + */ + public void noTint() { + tint = false; + } + + + /** + * ( begin auto-generated from tint.xml ) + * + * Sets the fill value for displaying images. Images can be tinted to + * specified colors or made transparent by setting the alpha.
        + *
        + * To make an image transparent, but not change it's color, use white as + * the tint color and specify an alpha value. For instance, tint(255, 128) + * will make an image 50% transparent (unless colorMode() has been + * used).
        + *
        + * When using hexadecimal notation to specify a color, use "#" or "0x" + * before the values (e.g. #CCFFAA, 0xFFCCFFAA). The # syntax uses six + * digits to specify a color (the way colors are specified in HTML and + * CSS). When using the hexadecimal notation starting with "0x", the + * hexadecimal value must be specified with eight characters; the first two + * characters define the alpha component and the remainder the red, green, + * and blue components.
        + *
        + * The value for the parameter "gray" must be less than or equal to the + * current maximum value as specified by colorMode(). The default + * maximum value is 255.
        + *
        + * The tint() function is also used to control the coloring of + * textures in 3D. + * + * ( end auto-generated ) + * + * @webref image:loading_displaying + * @usage web_application + * @param rgb color value in hexadecimal notation + * @see PGraphics#noTint() + * @see PGraphics#image(PImage, float, float, float, float) + */ + public void tint(int rgb) { + colorCalc(rgb); + tintFromCalc(); + } + + + /** + * @param alpha opacity of the image + */ + public void tint(int rgb, float alpha) { + colorCalc(rgb, alpha); + tintFromCalc(); + } + + + /** + * @param gray specifies a value between white and black + */ + public void tint(float gray) { + colorCalc(gray); + tintFromCalc(); + } + + + public void tint(float gray, float alpha) { + colorCalc(gray, alpha); + tintFromCalc(); + } + +/** + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + */ + public void tint(float v1, float v2, float v3) { + colorCalc(v1, v2, v3); + tintFromCalc(); + } + + + public void tint(float v1, float v2, float v3, float alpha) { + colorCalc(v1, v2, v3, alpha); + tintFromCalc(); + } + + + protected void tintFromCalc() { + tint = true; + tintR = calcR; + tintG = calcG; + tintB = calcB; + tintA = calcA; + tintRi = calcRi; + tintGi = calcGi; + tintBi = calcBi; + tintAi = calcAi; + tintColor = calcColor; + tintAlpha = calcAlpha; + } + + + + ////////////////////////////////////////////////////////////// + + // FILL COLOR + + + /** + * ( begin auto-generated from noFill.xml ) + * + * Disables filling geometry. If both noStroke() and noFill() + * are called, nothing will be drawn to the screen. + * + * ( end auto-generated ) + * + * @webref color:setting + * @usage web_application + * @see PGraphics#fill(float, float, float, float) + * @see PGraphics#stroke(int, float) + * @see PGraphics#noStroke() + */ + public void noFill() { + fill = false; + } + + + /** + * ( begin auto-generated from fill.xml ) + * + * Sets the color used to fill shapes. For example, if you run fill(204, + * 102, 0), all subsequent shapes will be filled with orange. This + * color is either specified in terms of the RGB or HSB color depending on + * the current colorMode() (the default color space is RGB, with + * each value in the range from 0 to 255). + *

        + * When using hexadecimal notation to specify a color, use "#" or "0x" + * before the values (e.g. #CCFFAA, 0xFFCCFFAA). The # syntax uses six + * digits to specify a color (the way colors are specified in HTML and + * CSS). When using the hexadecimal notation starting with "0x", the + * hexadecimal value must be specified with eight characters; the first two + * characters define the alpha component and the remainder the red, green, + * and blue components. + *

        + * The value for the parameter "gray" must be less than or equal to the + * current maximum value as specified by colorMode(). The default + * maximum value is 255. + *

        + * To change the color of an image (or a texture), use tint(). + * + * ( end auto-generated ) + * + * @webref color:setting + * @usage web_application + * @param rgb color variable or hex value + * @see PGraphics#noFill() + * @see PGraphics#stroke(int, float) + * @see PGraphics#noStroke() + * @see PGraphics#tint(int, float) + * @see PGraphics#background(float, float, float, float) + * @see PGraphics#colorMode(int, float, float, float, float) + */ + public void fill(int rgb) { + colorCalc(rgb); + fillFromCalc(); + } + + /** + * @param alpha opacity of the fill + */ + public void fill(int rgb, float alpha) { + colorCalc(rgb, alpha); + fillFromCalc(); + } + + + /** + * @param gray number specifying value between white and black + */ + public void fill(float gray) { + colorCalc(gray); + fillFromCalc(); + } + + + public void fill(float gray, float alpha) { + colorCalc(gray, alpha); + fillFromCalc(); + } + + + /** + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + */ + public void fill(float v1, float v2, float v3) { + colorCalc(v1, v2, v3); + fillFromCalc(); + } + + + public void fill(float v1, float v2, float v3, float alpha) { + colorCalc(v1, v2, v3, alpha); + fillFromCalc(); + } + + + protected void fillFromCalc() { + fill = true; + fillR = calcR; + fillG = calcG; + fillB = calcB; + fillA = calcA; + fillRi = calcRi; + fillGi = calcGi; + fillBi = calcBi; + fillAi = calcAi; + fillColor = calcColor; + fillAlpha = calcAlpha; + } + + + + ////////////////////////////////////////////////////////////// + + // MATERIAL PROPERTIES + + /** + * ( begin auto-generated from ambient.xml ) + * + * Sets the ambient reflectance for shapes drawn to the screen. This is + * combined with the ambient light component of environment. The color + * components set through the parameters define the reflectance. For + * example in the default color mode, setting v1=255, v2=126, v3=0, would + * cause all the red light to reflect and half of the green light to + * reflect. Used in combination with emissive(), specular(), + * and shininess() in setting the material properties of shapes. + * + * ( end auto-generated ) + * + * @webref lights_camera:material_properties + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#emissive(float, float, float) + * @see PGraphics#specular(float, float, float) + * @see PGraphics#shininess(float) + */ + public void ambient(int rgb) { +// if (((rgb & 0xff000000) == 0) && (rgb <= colorModeX)) { +// ambient((float) rgb); +// +// } else { +// colorCalcARGB(rgb, colorModeA); +// ambientFromCalc(); +// } + colorCalc(rgb); + ambientFromCalc(); + } + +/** + * @param gray number specifying value between white and black + */ + public void ambient(float gray) { + colorCalc(gray); + ambientFromCalc(); + } + +/** + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + */ + public void ambient(float v1, float v2, float v3) { + colorCalc(v1, v2, v3); + ambientFromCalc(); + } + + + protected void ambientFromCalc() { + ambientColor = calcColor; + ambientR = calcR; + ambientG = calcG; + ambientB = calcB; + setAmbient = true; + } + + /** + * ( begin auto-generated from specular.xml ) + * + * Sets the specular color of the materials used for shapes drawn to the + * screen, which sets the color of hightlights. Specular refers to light + * which bounces off a surface in a perferred direction (rather than + * bouncing in all directions like a diffuse light). Used in combination + * with emissive(), ambient(), and shininess() in + * setting the material properties of shapes. + * + * ( end auto-generated ) + * + * @webref lights_camera:material_properties + * @usage web_application + * @param rgb color to set + * @see PGraphics#lightSpecular(float, float, float) + * @see PGraphics#ambient(float, float, float) + * @see PGraphics#emissive(float, float, float) + * @see PGraphics#shininess(float) + */ + public void specular(int rgb) { +// if (((rgb & 0xff000000) == 0) && (rgb <= colorModeX)) { +// specular((float) rgb); +// +// } else { +// colorCalcARGB(rgb, colorModeA); +// specularFromCalc(); +// } + colorCalc(rgb); + specularFromCalc(); + } + + +/** + * gray number specifying value between white and black + */ + public void specular(float gray) { + colorCalc(gray); + specularFromCalc(); + } + + +/** + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + */ + public void specular(float v1, float v2, float v3) { + colorCalc(v1, v2, v3); + specularFromCalc(); + } + + + protected void specularFromCalc() { + specularColor = calcColor; + specularR = calcR; + specularG = calcG; + specularB = calcB; + } + + + /** + * ( begin auto-generated from shininess.xml ) + * + * Sets the amount of gloss in the surface of shapes. Used in combination + * with ambient(), specular(), and emissive() in + * setting the material properties of shapes. + * + * ( end auto-generated ) + * + * @webref lights_camera:material_properties + * @usage web_application + * @param shine degree of shininess + * @see PGraphics#emissive(float, float, float) + * @see PGraphics#ambient(float, float, float) + * @see PGraphics#specular(float, float, float) + */ + public void shininess(float shine) { + shininess = shine; + } + + /** + * ( begin auto-generated from emissive.xml ) + * + * Sets the emissive color of the material used for drawing shapes drawn to + * the screen. Used in combination with ambient(), + * specular(), and shininess() in setting the material + * properties of shapes. + * + * ( end auto-generated ) + * + * @webref lights_camera:material_properties + * @usage web_application + * @param rgb color to set + * @see PGraphics#ambient(float, float, float) + * @see PGraphics#specular(float, float, float) + * @see PGraphics#shininess(float) + */ + public void emissive(int rgb) { +// if (((rgb & 0xff000000) == 0) && (rgb <= colorModeX)) { +// emissive((float) rgb); +// +// } else { +// colorCalcARGB(rgb, colorModeA); +// emissiveFromCalc(); +// } + colorCalc(rgb); + emissiveFromCalc(); + } + + /** + * gray number specifying value between white and black + */ + public void emissive(float gray) { + colorCalc(gray); + emissiveFromCalc(); + } + + /** + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + */ + public void emissive(float v1, float v2, float v3) { + colorCalc(v1, v2, v3); + emissiveFromCalc(); + } + + + protected void emissiveFromCalc() { + emissiveColor = calcColor; + emissiveR = calcR; + emissiveG = calcG; + emissiveB = calcB; + } + + + + ////////////////////////////////////////////////////////////// + + // LIGHTS + + // The details of lighting are very implementation-specific, so this base + // class does not handle any details of settings lights. It does however + // display warning messages that the functions are not available. + + /** + * ( begin auto-generated from lights.xml ) + * + * Sets the default ambient light, directional light, falloff, and specular + * values. The defaults are ambientLight(128, 128, 128) and + * directionalLight(128, 128, 128, 0, 0, -1), lightFalloff(1, 0, 0), and + * lightSpecular(0, 0, 0). Lights need to be included in the draw() to + * remain persistent in a looping program. Placing them in the setup() of a + * looping program will cause them to only have an effect the first time + * through the loop. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @see PGraphics#ambientLight(float, float, float, float, float, float) + * @see PGraphics#directionalLight(float, float, float, float, float, float) + * @see PGraphics#pointLight(float, float, float, float, float, float) + * @see PGraphics#spotLight(float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#noLights() + */ + public void lights() { + showMethodWarning("lights"); + } + + /** + * ( begin auto-generated from noLights.xml ) + * + * Disable all lighting. Lighting is turned off by default and enabled with + * the lights() function. This function can be used to disable + * lighting so that 2D geometry (which does not require lighting) can be + * drawn after a set of lighted 3D geometry. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @see PGraphics#lights() + */ + public void noLights() { + showMethodWarning("noLights"); + } + + /** + * ( begin auto-generated from ambientLight.xml ) + * + * Adds an ambient light. Ambient light doesn't come from a specific + * direction, the rays have light have bounced around so much that objects + * are evenly lit from all sides. Ambient lights are almost always used in + * combination with other types of lights. Lights need to be included in + * the draw() to remain persistent in a looping program. Placing + * them in the setup() of a looping program will cause them to only + * have an effect the first time through the loop. The effect of the + * parameters is determined by the current color mode. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + * @see PGraphics#lights() + * @see PGraphics#directionalLight(float, float, float, float, float, float) + * @see PGraphics#pointLight(float, float, float, float, float, float) + * @see PGraphics#spotLight(float, float, float, float, float, float, float, float, float, float, float) + */ + public void ambientLight(float v1, float v2, float v3) { + showMethodWarning("ambientLight"); + } + + /** + * @param x x-coordinate of the light + * @param y y-coordinate of the light + * @param z z-coordinate of the light + */ + public void ambientLight(float v1, float v2, float v3, + float x, float y, float z) { + showMethodWarning("ambientLight"); + } + + /** + * ( begin auto-generated from directionalLight.xml ) + * + * Adds a directional light. Directional light comes from one direction and + * is stronger when hitting a surface squarely and weaker if it hits at a a + * gentle angle. After hitting a surface, a directional lights scatters in + * all directions. Lights need to be included in the draw() to + * remain persistent in a looping program. Placing them in the + * setup() of a looping program will cause them to only have an + * effect the first time through the loop. The affect of the v1, + * v2, and v3 parameters is determined by the current color + * mode. The nx, ny, and nz parameters specify the + * direction the light is facing. For example, setting ny to -1 will + * cause the geometry to be lit from below (the light is facing directly upward). + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + * @param nx direction along the x-axis + * @param ny direction along the y-axis + * @param nz direction along the z-axis + * @see PGraphics#lights() + * @see PGraphics#ambientLight(float, float, float, float, float, float) + * @see PGraphics#pointLight(float, float, float, float, float, float) + * @see PGraphics#spotLight(float, float, float, float, float, float, float, float, float, float, float) + */ + public void directionalLight(float v1, float v2, float v3, + float nx, float ny, float nz) { + showMethodWarning("directionalLight"); + } + + /** + * ( begin auto-generated from pointLight.xml ) + * + * Adds a point light. Lights need to be included in the draw() to + * remain persistent in a looping program. Placing them in the + * setup() of a looping program will cause them to only have an + * effect the first time through the loop. The affect of the v1, + * v2, and v3 parameters is determined by the current color + * mode. The x, y, and z parameters set the position + * of the light. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + * @param x x-coordinate of the light + * @param y y-coordinate of the light + * @param z z-coordinate of the light + * @see PGraphics#lights() + * @see PGraphics#directionalLight(float, float, float, float, float, float) + * @see PGraphics#ambientLight(float, float, float, float, float, float) + * @see PGraphics#spotLight(float, float, float, float, float, float, float, float, float, float, float) + */ + public void pointLight(float v1, float v2, float v3, + float x, float y, float z) { + showMethodWarning("pointLight"); + } + + /** + * ( begin auto-generated from spotLight.xml ) + * + * Adds a spot light. Lights need to be included in the draw() to + * remain persistent in a looping program. Placing them in the + * setup() of a looping program will cause them to only have an + * effect the first time through the loop. The affect of the v1, + * v2, and v3 parameters is determined by the current color + * mode. The x, y, and z parameters specify the + * position of the light and nx, ny, nz specify the + * direction or light. The angle parameter affects angle of the + * spotlight cone. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + * @param x x-coordinate of the light + * @param y y-coordinate of the light + * @param z z-coordinate of the light + * @param nx direction along the x axis + * @param ny direction along the y axis + * @param nz direction along the z axis + * @param angle angle of the spotlight cone + * @param concentration exponent determining the center bias of the cone + * @see PGraphics#lights() + * @see PGraphics#directionalLight(float, float, float, float, float, float) + * @see PGraphics#pointLight(float, float, float, float, float, float) + * @see PGraphics#ambientLight(float, float, float, float, float, float) + */ + public void spotLight(float v1, float v2, float v3, + float x, float y, float z, + float nx, float ny, float nz, + float angle, float concentration) { + showMethodWarning("spotLight"); + } + + /** + * ( begin auto-generated from lightFalloff.xml ) + * + * Sets the falloff rates for point lights, spot lights, and ambient + * lights. The parameters are used to determine the falloff with the + * following equation:

        d = distance from light position to + * vertex position
        falloff = 1 / (CONSTANT + d * LINEAR + (d*d) * + * QUADRATIC)

        Like fill(), it affects only the elements + * which are created after it in the code. The default value if + * LightFalloff(1.0, 0.0, 0.0). Thinking about an ambient light with + * a falloff can be tricky. It is used, for example, if you wanted a region + * of your scene to be lit ambiently one color and another region to be lit + * ambiently by another color, you would use an ambient light with location + * and falloff. You can think of it as a point light that doesn't care + * which direction a surface is facing. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @param constant constant value or determining falloff + * @param linear linear value for determining falloff + * @param quadratic quadratic value for determining falloff + * @see PGraphics#lights() + * @see PGraphics#ambientLight(float, float, float, float, float, float) + * @see PGraphics#pointLight(float, float, float, float, float, float) + * @see PGraphics#spotLight(float, float, float, float, float, float, float, float, float, float, float) + * @see PGraphics#lightSpecular(float, float, float) + */ + public void lightFalloff(float constant, float linear, float quadratic) { + showMethodWarning("lightFalloff"); + } + + /** + * ( begin auto-generated from lightSpecular.xml ) + * + * Sets the specular color for lights. Like fill(), it affects only + * the elements which are created after it in the code. Specular refers to + * light which bounces off a surface in a perferred direction (rather than + * bouncing in all directions like a diffuse light) and is used for + * creating highlights. The specular quality of a light interacts with the + * specular material qualities set through the specular() and + * shininess() functions. + * + * ( end auto-generated ) + * + * @webref lights_camera:lights + * @usage web_application + * @param v1 red or hue value (depending on current color mode) + * @param v2 green or saturation value (depending on current color mode) + * @param v3 blue or brightness value (depending on current color mode) + * @see PGraphics#specular(float, float, float) + * @see PGraphics#lights() + * @see PGraphics#ambientLight(float, float, float, float, float, float) + * @see PGraphics#pointLight(float, float, float, float, float, float) + * @see PGraphics#spotLight(float, float, float, float, float, float, float, float, float, float, float) + */ + public void lightSpecular(float v1, float v2, float v3) { + showMethodWarning("lightSpecular"); + } + + + + ////////////////////////////////////////////////////////////// + + // BACKGROUND + + + /** + * ( begin auto-generated from background.xml ) + * + * The background() function sets the color used for the background + * of the Processing window. The default background is light gray. In the + * draw() function, the background color is used to clear the + * display window at the beginning of each frame. + *

        + * An image can also be used as the background for a sketch, however its + * width and height must be the same size as the sketch window. To resize + * an image 'b' to the size of the sketch window, use b.resize(width, height). + *

        + * Images used as background will ignore the current tint() setting. + *

        + * It is not possible to use transparency (alpha) in background colors with + * the main drawing surface, however they will work properly with createGraphics(). + * + * ( end auto-generated ) + * + *

        Advanced

        + *

        Clear the background with a color that includes an alpha value. This can + * only be used with objects created by createGraphics(), because the main + * drawing surface cannot be set transparent.

        + *

        It might be tempting to use this function to partially clear the screen + * on each frame, however that's not how this function works. When calling + * background(), the pixels will be replaced with pixels that have that level + * of transparency. To do a semi-transparent overlay, use fill() with alpha + * and draw a rectangle.

        + * + * @webref color:setting + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#stroke(float) + * @see PGraphics#fill(float) + * @see PGraphics#tint(float) + * @see PGraphics#colorMode(int) + */ + public void background(int rgb) { +// if (((rgb & 0xff000000) == 0) && (rgb <= colorModeX)) { +// background((float) rgb); +// +// } else { +// if (format == RGB) { +// rgb |= 0xff000000; // ignore alpha for main drawing surface +// } +// colorCalcARGB(rgb, colorModeA); +// backgroundFromCalc(); +// backgroundImpl(); +// } + colorCalc(rgb); + backgroundFromCalc(); + } + + + /** + * @param alpha opacity of the background + */ + public void background(int rgb, float alpha) { +// if (format == RGB) { +// background(rgb); // ignore alpha for main drawing surface +// +// } else { +// if (((rgb & 0xff000000) == 0) && (rgb <= colorModeX)) { +// background((float) rgb, alpha); +// +// } else { +// colorCalcARGB(rgb, alpha); +// backgroundFromCalc(); +// backgroundImpl(); +// } +// } + colorCalc(rgb, alpha); + backgroundFromCalc(); + } + + + /** + * @param gray specifies a value between white and black + */ + public void background(float gray) { + colorCalc(gray); + backgroundFromCalc(); +// backgroundImpl(); + } + + + public void background(float gray, float alpha) { + if (format == RGB) { + background(gray); // ignore alpha for main drawing surface + + } else { + colorCalc(gray, alpha); + backgroundFromCalc(); +// backgroundImpl(); + } + } + + + /** + * @param v1 red or hue value (depending on the current color mode) + * @param v2 green or saturation value (depending on the current color mode) + * @param v3 blue or brightness value (depending on the current color mode) + */ + public void background(float v1, float v2, float v3) { + colorCalc(v1, v2, v3); + backgroundFromCalc(); +// backgroundImpl(); + } + + + public void background(float v1, float v2, float v3, float alpha) { + colorCalc(v1, v2, v3, alpha); + backgroundFromCalc(); + } + + /** + * @webref color:setting + */ + public void clear() { + background(0, 0, 0, 0); + } + + + protected void backgroundFromCalc() { + backgroundR = calcR; + backgroundG = calcG; + backgroundB = calcB; + //backgroundA = (format == RGB) ? colorModeA : calcA; + // If drawing surface is opaque, this maxes out at 1.0. [fry 150513] + backgroundA = (format == RGB) ? 1 : calcA; + backgroundRi = calcRi; + backgroundGi = calcGi; + backgroundBi = calcBi; + backgroundAi = (format == RGB) ? 255 : calcAi; + backgroundAlpha = (format == RGB) ? false : calcAlpha; + backgroundColor = calcColor; + + backgroundImpl(); + } + + + /** + * Takes an RGB or ARGB image and sets it as the background. + * The width and height of the image must be the same size as the sketch. + * Use image.resize(width, height) to make short work of such a task.
        + *
        + * Note that even if the image is set as RGB, the high 8 bits of each pixel + * should be set opaque (0xFF000000) because the image data will be copied + * directly to the screen, and non-opaque background images may have strange + * behavior. Use image.filter(OPAQUE) to handle this easily.
        + *
        + * When using 3D, this will also clear the zbuffer (if it exists). + * + * @param image PImage to set as background (must be same size as the sketch window) + */ + public void background(PImage image) { + if ((image.pixelWidth != pixelWidth) || (image.pixelHeight != pixelHeight)) { + throw new RuntimeException(ERROR_BACKGROUND_IMAGE_SIZE); + } + if ((image.format != RGB) && (image.format != ARGB)) { + throw new RuntimeException(ERROR_BACKGROUND_IMAGE_FORMAT); + } + backgroundColor = 0; // just zero it out for images + backgroundImpl(image); + } + + + /** + * Actually set the background image. This is separated from the error + * handling and other semantic goofiness that is shared across renderers. + */ + protected void backgroundImpl(PImage image) { + // blit image to the screen + set(0, 0, image); + } + + + /** + * Actual implementation of clearing the background, now that the + * internal variables for background color have been set. Called by the + * backgroundFromCalc() method, which is what all the other background() + * methods call once the work is done. + */ + protected void backgroundImpl() { + pushStyle(); + pushMatrix(); + resetMatrix(); + fill(backgroundColor); + rect(0, 0, width, height); + popMatrix(); + popStyle(); + } + + + /** + * Callback to handle clearing the background when begin/endRaw is in use. + * Handled as separate function for OpenGL (or other) subclasses that + * override backgroundImpl() but still needs this to work properly. + */ +// protected void backgroundRawImpl() { +// if (raw != null) { +// raw.colorMode(RGB, 1); +// raw.noStroke(); +// raw.fill(backgroundR, backgroundG, backgroundB); +// raw.beginShape(TRIANGLES); +// +// raw.vertex(0, 0); +// raw.vertex(width, 0); +// raw.vertex(0, height); +// +// raw.vertex(width, 0); +// raw.vertex(width, height); +// raw.vertex(0, height); +// +// raw.endShape(); +// } +// } + + + + ////////////////////////////////////////////////////////////// + + // COLOR MODE + + /** + * ( begin auto-generated from colorMode.xml ) + * + * Changes the way Processing interprets color data. By default, the + * parameters for fill(), stroke(), background(), and + * color() are defined by values between 0 and 255 using the RGB + * color model. The colorMode() function is used to change the + * numerical range used for specifying colors and to switch color systems. + * For example, calling colorMode(RGB, 1.0) will specify that values + * are specified between 0 and 1. The limits for defining colors are + * altered by setting the parameters range1, range2, range3, and range 4. + * + * ( end auto-generated ) + * + * @webref color:setting + * @usage web_application + * @param mode Either RGB or HSB, corresponding to Red/Green/Blue and Hue/Saturation/Brightness + * @see PGraphics#background(float) + * @see PGraphics#fill(float) + * @see PGraphics#stroke(float) + */ + public void colorMode(int mode) { + colorMode(mode, colorModeX, colorModeY, colorModeZ, colorModeA); + } + + + /** + * @param max range for all color elements + */ + public void colorMode(int mode, float max) { + colorMode(mode, max, max, max, max); + } + + + /** + * @param max1 range for the red or hue depending on the current color mode + * @param max2 range for the green or saturation depending on the current color mode + * @param max3 range for the blue or brightness depending on the current color mode + */ + public void colorMode(int mode, float max1, float max2, float max3) { + colorMode(mode, max1, max2, max3, colorModeA); + } + + + /** + * @param maxA range for the alpha + */ + public void colorMode(int mode, + float max1, float max2, float max3, float maxA) { + colorMode = mode; + + colorModeX = max1; // still needs to be set for hsb + colorModeY = max2; + colorModeZ = max3; + colorModeA = maxA; + + // if color max values are all 1, then no need to scale + colorModeScale = + ((maxA != 1) || (max1 != max2) || (max2 != max3) || (max3 != maxA)); + + // if color is rgb/0..255 this will make it easier for the + // red() green() etc functions + colorModeDefault = (colorMode == RGB) && + (colorModeA == 255) && (colorModeX == 255) && + (colorModeY == 255) && (colorModeZ == 255); + } + + + + ////////////////////////////////////////////////////////////// + + // COLOR CALCULATIONS + + // Given input values for coloring, these functions will fill the calcXxxx + // variables with values that have been properly filtered through the + // current colorMode settings. + + // Renderers that need to subclass any drawing properties such as fill or + // stroke will usally want to override methods like fillFromCalc (or the + // same for stroke, ambient, etc.) That way the color calcuations are + // covered by this based PGraphics class, leaving only a single function + // to override/implement in the subclass. + + + /** + * Set the fill to either a grayscale value or an ARGB int. + *

        + * The problem with this code is that it has to detect between these two + * situations automatically. This is done by checking to see if the high bits + * (the alpha for 0xAA000000) is set, and if not, whether the color value + * that follows is less than colorModeX (first param passed to colorMode). + *

        + * This auto-detect would break in the following situation: + *

        size(256, 256);
        +   * for (int i = 0; i < 256; i++) {
        +   *   color c = color(0, 0, 0, i);
        +   *   stroke(c);
        +   *   line(i, 0, i, 256);
        +   * }
        + * ...on the first time through the loop, where (i == 0), since the color + * itself is zero (black) then it would appear indistinguishable from code + * that reads "fill(0)". The solution is to use the four parameter versions + * of stroke or fill to more directly specify the desired result. + */ + protected void colorCalc(int rgb) { + if (((rgb & 0xff000000) == 0) && (rgb <= colorModeX)) { + colorCalc((float) rgb); + + } else { + colorCalcARGB(rgb, colorModeA); + } + } + + + protected void colorCalc(int rgb, float alpha) { + if (((rgb & 0xff000000) == 0) && (rgb <= colorModeX)) { // see above + colorCalc((float) rgb, alpha); + + } else { + colorCalcARGB(rgb, alpha); + } + } + + + protected void colorCalc(float gray) { + colorCalc(gray, colorModeA); + } + + + protected void colorCalc(float gray, float alpha) { + if (gray > colorModeX) gray = colorModeX; + if (alpha > colorModeA) alpha = colorModeA; + + if (gray < 0) gray = 0; + if (alpha < 0) alpha = 0; + + calcR = colorModeScale ? (gray / colorModeX) : gray; + calcG = calcR; + calcB = calcR; + calcA = colorModeScale ? (alpha / colorModeA) : alpha; + + calcRi = (int)(calcR*255); calcGi = (int)(calcG*255); + calcBi = (int)(calcB*255); calcAi = (int)(calcA*255); + calcColor = (calcAi << 24) | (calcRi << 16) | (calcGi << 8) | calcBi; + calcAlpha = (calcAi != 255); + } + + + protected void colorCalc(float x, float y, float z) { + colorCalc(x, y, z, colorModeA); + } + + + protected void colorCalc(float x, float y, float z, float a) { + if (x > colorModeX) x = colorModeX; + if (y > colorModeY) y = colorModeY; + if (z > colorModeZ) z = colorModeZ; + if (a > colorModeA) a = colorModeA; + + if (x < 0) x = 0; + if (y < 0) y = 0; + if (z < 0) z = 0; + if (a < 0) a = 0; + + switch (colorMode) { + case RGB: + if (colorModeScale) { + calcR = x / colorModeX; + calcG = y / colorModeY; + calcB = z / colorModeZ; + calcA = a / colorModeA; + } else { + calcR = x; calcG = y; calcB = z; calcA = a; + } + break; + + case HSB: + x /= colorModeX; // h + y /= colorModeY; // s + z /= colorModeZ; // b + + calcA = colorModeScale ? (a/colorModeA) : a; + + if (y == 0) { // saturation == 0 + calcR = calcG = calcB = z; + + } else { + float which = (x - (int)x) * 6.0f; + float f = which - (int)which; + float p = z * (1.0f - y); + float q = z * (1.0f - y * f); + float t = z * (1.0f - (y * (1.0f - f))); + + switch ((int)which) { + case 0: calcR = z; calcG = t; calcB = p; break; + case 1: calcR = q; calcG = z; calcB = p; break; + case 2: calcR = p; calcG = z; calcB = t; break; + case 3: calcR = p; calcG = q; calcB = z; break; + case 4: calcR = t; calcG = p; calcB = z; break; + case 5: calcR = z; calcG = p; calcB = q; break; + } + } + break; + } + calcRi = (int)(255*calcR); calcGi = (int)(255*calcG); + calcBi = (int)(255*calcB); calcAi = (int)(255*calcA); + calcColor = (calcAi << 24) | (calcRi << 16) | (calcGi << 8) | calcBi; + calcAlpha = (calcAi != 255); + } + + + /** + * Unpacks AARRGGBB color for direct use with colorCalc. + *

        + * Handled here with its own function since this is indepenent + * of the color mode. + *

        + * Strangely the old version of this code ignored the alpha + * value. not sure if that was a bug or what. + *

        + * Note, no need for a bounds check for 'argb' since it's a 32 bit number. + * Bounds now checked on alpha, however (rev 0225). + */ + protected void colorCalcARGB(int argb, float alpha) { + if (alpha == colorModeA) { + calcAi = (argb >> 24) & 0xff; + calcColor = argb; + } else { + calcAi = (int) (((argb >> 24) & 0xff) * PApplet.constrain((alpha / colorModeA), 0, 1)); + calcColor = (calcAi << 24) | (argb & 0xFFFFFF); + } + calcRi = (argb >> 16) & 0xff; + calcGi = (argb >> 8) & 0xff; + calcBi = argb & 0xff; + calcA = calcAi / 255.0f; + calcR = calcRi / 255.0f; + calcG = calcGi / 255.0f; + calcB = calcBi / 255.0f; + calcAlpha = (calcAi != 255); + } + + + + ////////////////////////////////////////////////////////////// + + // COLOR DATATYPE STUFFING + + // The 'color' primitive type in Processing syntax is in fact a 32-bit int. + // These functions handle stuffing color values into a 32-bit cage based + // on the current colorMode settings. + + // These functions are really slow (because they take the current colorMode + // into account), but they're easy to use. Advanced users can write their + // own bit shifting operations to setup 'color' data types. + + + public final int color(int c) { // ignore +// if (((c & 0xff000000) == 0) && (c <= colorModeX)) { +// if (colorModeDefault) { +// // bounds checking to make sure the numbers aren't to high or low +// if (c > 255) c = 255; else if (c < 0) c = 0; +// return 0xff000000 | (c << 16) | (c << 8) | c; +// } else { +// colorCalc(c); +// } +// } else { +// colorCalcARGB(c, colorModeA); +// } + colorCalc(c); + return calcColor; + } + + + public final int color(float gray) { // ignore + colorCalc(gray); + return calcColor; + } + + + /** + * @param c can be packed ARGB or a gray in this case + */ + public final int color(int c, int alpha) { // ignore +// if (colorModeDefault) { +// // bounds checking to make sure the numbers aren't to high or low +// if (c > 255) c = 255; else if (c < 0) c = 0; +// if (alpha > 255) alpha = 255; else if (alpha < 0) alpha = 0; +// +// return ((alpha & 0xff) << 24) | (c << 16) | (c << 8) | c; +// } + colorCalc(c, alpha); + return calcColor; + } + + + /** + * @param c can be packed ARGB or a gray in this case + */ + public final int color(int c, float alpha) { // ignore +// if (((c & 0xff000000) == 0) && (c <= colorModeX)) { + colorCalc(c, alpha); +// } else { +// colorCalcARGB(c, alpha); +// } + return calcColor; + } + + + public final int color(float gray, float alpha) { // ignore + colorCalc(gray, alpha); + return calcColor; + } + + + public final int color(int v1, int v2, int v3) { // ignore + colorCalc(v1, v2, v3); + return calcColor; + } + + + public final int color(float v1, float v2, float v3) { // ignore + colorCalc(v1, v2, v3); + return calcColor; + } + + + public final int color(int v1, int v2, int v3, int a) { // ignore + colorCalc(v1, v2, v3, a); + return calcColor; + } + + + public final int color(float v1, float v2, float v3, float a) { // ignore + colorCalc(v1, v2, v3, a); + return calcColor; + } + + + + ////////////////////////////////////////////////////////////// + + // COLOR DATATYPE EXTRACTION + + // Vee have veys of making the colors talk. + + /** + * ( begin auto-generated from alpha.xml ) + * + * Extracts the alpha value from a color. + * + * ( end auto-generated ) + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#red(int) + * @see PGraphics#green(int) + * @see PGraphics#blue(int) + * @see PGraphics#hue(int) + * @see PGraphics#saturation(int) + * @see PGraphics#brightness(int) + */ + public final float alpha(int rgb) { + float outgoing = (rgb >> 24) & 0xff; + if (colorModeA == 255) return outgoing; + return (outgoing / 255.0f) * colorModeA; + } + + + /** + * ( begin auto-generated from red.xml ) + * + * Extracts the red value from a color, scaled to match current + * colorMode(). This value is always returned as a float so be + * careful not to assign it to an int value.

        The red() function + * is easy to use and undestand, but is slower than another technique. To + * achieve the same results when working in colorMode(RGB, 255), but + * with greater speed, use the >> (right shift) operator with a bit + * mask. For example, the following two lines of code are equivalent:

        float r1 = red(myColor);
        float r2 = myColor >> 16 + * & 0xFF;
        + * + * ( end auto-generated ) + * + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#green(int) + * @see PGraphics#blue(int) + * @see PGraphics#alpha(int) + * @see PGraphics#hue(int) + * @see PGraphics#saturation(int) + * @see PGraphics#brightness(int) + * @see_external rightshift + */ + public final float red(int rgb) { + float c = (rgb >> 16) & 0xff; + if (colorModeDefault) return c; + return (c / 255.0f) * colorModeX; + } + + + /** + * ( begin auto-generated from green.xml ) + * + * Extracts the green value from a color, scaled to match current + * colorMode(). This value is always returned as a float so be + * careful not to assign it to an int value.

        The green() + * function is easy to use and undestand, but is slower than another + * technique. To achieve the same results when working in colorMode(RGB, + * 255), but with greater speed, use the >> (right shift) + * operator with a bit mask. For example, the following two lines of code + * are equivalent:
        float r1 = green(myColor);
        float r2 = + * myColor >> 8 & 0xFF;
        + * + * ( end auto-generated ) + * + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#red(int) + * @see PGraphics#blue(int) + * @see PGraphics#alpha(int) + * @see PGraphics#hue(int) + * @see PGraphics#saturation(int) + * @see PGraphics#brightness(int) + * @see_external rightshift + */ + public final float green(int rgb) { + float c = (rgb >> 8) & 0xff; + if (colorModeDefault) return c; + return (c / 255.0f) * colorModeY; + } + + + /** + * ( begin auto-generated from blue.xml ) + * + * Extracts the blue value from a color, scaled to match current + * colorMode(). This value is always returned as a float so be + * careful not to assign it to an int value.

        The blue() + * function is easy to use and undestand, but is slower than another + * technique. To achieve the same results when working in colorMode(RGB, + * 255), but with greater speed, use a bit mask to remove the other + * color components. For example, the following two lines of code are + * equivalent:
        float r1 = blue(myColor);
        float r2 = myColor + * & 0xFF;
        + * + * ( end auto-generated ) + * + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#red(int) + * @see PGraphics#green(int) + * @see PGraphics#alpha(int) + * @see PGraphics#hue(int) + * @see PGraphics#saturation(int) + * @see PGraphics#brightness(int) + * @see_external rightshift + */ + public final float blue(int rgb) { + float c = (rgb) & 0xff; + if (colorModeDefault) return c; + return (c / 255.0f) * colorModeZ; + } + + + /** + * ( begin auto-generated from hue.xml ) + * + * Extracts the hue value from a color. + * + * ( end auto-generated ) + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#red(int) + * @see PGraphics#green(int) + * @see PGraphics#blue(int) + * @see PGraphics#alpha(int) + * @see PGraphics#saturation(int) + * @see PGraphics#brightness(int) + */ + public final float hue(int rgb) { + if (rgb != cacheHsbKey) { + Color.RGBtoHSB((rgb >> 16) & 0xff, (rgb >> 8) & 0xff, + rgb & 0xff, cacheHsbValue); + cacheHsbKey = rgb; + } + return cacheHsbValue[0] * colorModeX; + } + + + /** + * ( begin auto-generated from saturation.xml ) + * + * Extracts the saturation value from a color. + * + * ( end auto-generated ) + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#red(int) + * @see PGraphics#green(int) + * @see PGraphics#blue(int) + * @see PGraphics#alpha(int) + * @see PGraphics#hue(int) + * @see PGraphics#brightness(int) + */ + public final float saturation(int rgb) { + if (rgb != cacheHsbKey) { + Color.RGBtoHSB((rgb >> 16) & 0xff, (rgb >> 8) & 0xff, + rgb & 0xff, cacheHsbValue); + cacheHsbKey = rgb; + } + return cacheHsbValue[1] * colorModeY; + } + + + /** + * ( begin auto-generated from brightness.xml ) + * + * Extracts the brightness value from a color. + * + * ( end auto-generated ) + * + * @webref color:creating_reading + * @usage web_application + * @param rgb any value of the color datatype + * @see PGraphics#red(int) + * @see PGraphics#green(int) + * @see PGraphics#blue(int) + * @see PGraphics#alpha(int) + * @see PGraphics#hue(int) + * @see PGraphics#saturation(int) + */ + public final float brightness(int rgb) { + if (rgb != cacheHsbKey) { + Color.RGBtoHSB((rgb >> 16) & 0xff, (rgb >> 8) & 0xff, + rgb & 0xff, cacheHsbValue); + cacheHsbKey = rgb; + } + return cacheHsbValue[2] * colorModeZ; + } + + + + ////////////////////////////////////////////////////////////// + + // COLOR DATATYPE INTERPOLATION + + // Against our better judgement. + + + /** + * ( begin auto-generated from lerpColor.xml ) + * + * Calculates a color or colors between two color at a specific increment. + * The amt parameter is the amount to interpolate between the two + * values where 0.0 equal to the first point, 0.1 is very near the first + * point, 0.5 is half-way in between, etc. + * + * ( end auto-generated ) + * + * @webref color:creating_reading + * @usage web_application + * @param c1 interpolate from this color + * @param c2 interpolate to this color + * @param amt between 0.0 and 1.0 + * @see PImage#blendColor(int, int, int) + * @see PGraphics#color(float, float, float, float) + * @see PApplet#lerp(float, float, float) + */ + public int lerpColor(int c1, int c2, float amt) { // ignore + return lerpColor(c1, c2, amt, colorMode); + } + + static float[] lerpColorHSB1; + static float[] lerpColorHSB2; + + /** + * @nowebref + * Interpolate between two colors. Like lerp(), but for the + * individual color components of a color supplied as an int value. + */ + static public int lerpColor(int c1, int c2, float amt, int mode) { + if (amt < 0) amt = 0; + if (amt > 1) amt = 1; + + if (mode == RGB) { + float a1 = ((c1 >> 24) & 0xff); + float r1 = (c1 >> 16) & 0xff; + float g1 = (c1 >> 8) & 0xff; + float b1 = c1 & 0xff; + float a2 = (c2 >> 24) & 0xff; + float r2 = (c2 >> 16) & 0xff; + float g2 = (c2 >> 8) & 0xff; + float b2 = c2 & 0xff; + + return ((PApplet.round(a1 + (a2-a1)*amt) << 24) | + (PApplet.round(r1 + (r2-r1)*amt) << 16) | + (PApplet.round(g1 + (g2-g1)*amt) << 8) | + (PApplet.round(b1 + (b2-b1)*amt))); + + } else if (mode == HSB) { + if (lerpColorHSB1 == null) { + lerpColorHSB1 = new float[3]; + lerpColorHSB2 = new float[3]; + } + + float a1 = (c1 >> 24) & 0xff; + float a2 = (c2 >> 24) & 0xff; + int alfa = (PApplet.round(a1 + (a2-a1)*amt)) << 24; + + Color.RGBtoHSB((c1 >> 16) & 0xff, (c1 >> 8) & 0xff, c1 & 0xff, + lerpColorHSB1); + Color.RGBtoHSB((c2 >> 16) & 0xff, (c2 >> 8) & 0xff, c2 & 0xff, + lerpColorHSB2); + + /* If mode is HSB, this will take the shortest path around the + * color wheel to find the new color. For instance, red to blue + * will go red violet blue (backwards in hue space) rather than + * cycling through ROYGBIV. + */ + // Disabling rollover (wasn't working anyway) for 0126. + // Otherwise it makes full spectrum scale impossible for + // those who might want it...in spite of how despicable + // a full spectrum scale might be. + // roll around when 0.9 to 0.1 + // more than 0.5 away means that it should roll in the other direction + /* + float h1 = lerpColorHSB1[0]; + float h2 = lerpColorHSB2[0]; + if (Math.abs(h1 - h2) > 0.5f) { + if (h1 > h2) { + // i.e. h1 is 0.7, h2 is 0.1 + h2 += 1; + } else { + // i.e. h1 is 0.1, h2 is 0.7 + h1 += 1; + } + } + float ho = (PApplet.lerp(lerpColorHSB1[0], lerpColorHSB2[0], amt)) % 1.0f; + */ + float ho = PApplet.lerp(lerpColorHSB1[0], lerpColorHSB2[0], amt); + float so = PApplet.lerp(lerpColorHSB1[1], lerpColorHSB2[1], amt); + float bo = PApplet.lerp(lerpColorHSB1[2], lerpColorHSB2[2], amt); + + return alfa | (Color.HSBtoRGB(ho, so, bo) & 0xFFFFFF); + } + return 0; + } + + + ////////////////////////////////////////////////////////////// + + // BEGINRAW/ENDRAW + + + /** + * Record individual lines and triangles by echoing them to another renderer. + */ + public void beginRaw(PGraphics rawGraphics) { // ignore + this.raw = rawGraphics; + rawGraphics.beginDraw(); + } + + + public void endRaw() { // ignore + if (raw != null) { + // for 3D, need to flush any geometry that's been stored for sorting + // (particularly if the ENABLE_DEPTH_SORT hint is set) + flush(); + + // just like beginDraw, this will have to be called because + // endDraw() will be happening outside of draw() + raw.endDraw(); + raw.dispose(); + raw = null; + } + } + + + public boolean haveRaw() { // ignore + return raw != null; + } + + + public PGraphics getRaw() { // ignore + return raw; + } + + + ////////////////////////////////////////////////////////////// + + // WARNINGS and EXCEPTIONS + + + static protected Map warnings; + + + /** + * Show a renderer error, and keep track of it so that it's only shown once. + * @param msg the error message (which will be stored for later comparison) + */ + static public void showWarning(String msg) { // ignore + if (warnings == null) { + warnings = new HashMap(); + } + if (!warnings.containsKey(msg)) { + System.err.println(msg); + warnings.put(msg, new Object()); + } + } + + + /** + * Version of showWarning() that takes a parsed String. + */ + static public void showWarning(String msg, Object... args) { // ignore + showWarning(String.format(msg, args)); + } + + + /** + * Display a warning that the specified method is only available with 3D. + * @param method The method name (no parentheses) + */ + static public void showDepthWarning(String method) { + showWarning(method + "() can only be used with a renderer that " + + "supports 3D, such as P3D."); + } + + + /** + * Display a warning that the specified method that takes x, y, z parameters + * can only be used with x and y parameters in this renderer. + * @param method The method name (no parentheses) + */ + static public void showDepthWarningXYZ(String method) { + showWarning(method + "() with x, y, and z coordinates " + + "can only be used with a renderer that " + + "supports 3D, such as P3D. " + + "Use a version without a z-coordinate instead."); + } + + + /** + * Display a warning that the specified method is simply unavailable. + */ + static public void showMethodWarning(String method) { + showWarning(method + "() is not available with this renderer."); + } + + + /** + * Error that a particular variation of a method is unavailable (even though + * other variations are). For instance, if vertex(x, y, u, v) is not + * available, but vertex(x, y) is just fine. + */ + static public void showVariationWarning(String str) { + showWarning(str + " is not available with this renderer."); + } + + + /** + * Display a warning that the specified method is not implemented, meaning + * that it could be either a completely missing function, although other + * variations of it may still work properly. + */ + static public void showMissingWarning(String method) { + showWarning(method + "(), or this particular variation of it, " + + "is not available with this renderer."); + } + + + /** + * Show an renderer-related exception that halts the program. Currently just + * wraps the message as a RuntimeException and throws it, but might do + * something more specific might be used in the future. + */ + static public void showException(String msg) { // ignore + throw new RuntimeException(msg); + } + + + /** + * Same as below, but defaults to a 12 point font, just as MacWrite intended. + */ + protected void defaultFontOrDeath(String method) { + defaultFontOrDeath(method, 12); + } + + + /** + * First try to create a default font, but if that's not possible, throw + * an exception that halts the program because textFont() has not been used + * prior to the specified method. + */ + protected void defaultFontOrDeath(String method, float size) { + if (parent != null) { + textFont = parent.createDefaultFont(size); + } else { + throw new RuntimeException("Use textFont() before " + method + "()"); + } + } + + + + ////////////////////////////////////////////////////////////// + + // RENDERER SUPPORT QUERIES + + + /** + * Return true if this renderer should be drawn to the screen. Defaults to + * returning true, since nearly all renderers are on-screen beasts. But can + * be overridden for subclasses like PDF so that a window doesn't open up. + *

        + * A better name? showFrame, displayable, isVisible, visible, shouldDisplay, + * what to call this? + */ + public boolean displayable() { // ignore + return true; + } + + + /** + * Return true if this renderer supports 2D drawing. Defaults to true. + */ + public boolean is2D() { // ignore + return true; + } + + + /** + * Return true if this renderer supports 3D drawing. Defaults to false. + */ + public boolean is3D() { // ignore + return false; + } + + + /** + * Return true if this renderer does rendering through OpenGL. Defaults to false. + */ + public boolean isGL() { // ignore + return false; + } + + + public boolean is2X() { + return pixelDensity == 2; + } + + + ////////////////////////////////////////////////////////////// + + // ASYNC IMAGE SAVING + + + @Override + public boolean save(String filename) { // ignore + + if (hints[DISABLE_ASYNC_SAVEFRAME]) { + return super.save(filename); + } + + if (asyncImageSaver == null) { + asyncImageSaver = new AsyncImageSaver(); + } + + if (!loaded) loadPixels(); + PImage target = asyncImageSaver.getAvailableTarget(pixelWidth, pixelHeight, + format); + if (target == null) return false; + int count = PApplet.min(pixels.length, target.pixels.length); + System.arraycopy(pixels, 0, target.pixels, 0, count); + asyncImageSaver.saveTargetAsync(this, target, parent.sketchFile(filename)); + + return true; + } + + protected void processImageBeforeAsyncSave(PImage image) { } + + + /** + * If there is running async save task for this file, blocks until it completes. + * Has to be called on main thread because OpenGL overrides this and calls GL. + * @param filename + */ + protected void awaitAsyncSaveCompletion(String filename) { + if (asyncImageSaver != null) { + asyncImageSaver.awaitAsyncSaveCompletion(parent.sketchFile(filename)); + } + } + + + protected static AsyncImageSaver asyncImageSaver; + + protected static class AsyncImageSaver { + + static final int TARGET_COUNT = + Math.max(1, Runtime.getRuntime().availableProcessors() - 1); + + BlockingQueue targetPool = new ArrayBlockingQueue<>(TARGET_COUNT); + ExecutorService saveExecutor = Executors.newFixedThreadPool(TARGET_COUNT); + + int targetsCreated = 0; + + Map> runningTasks = new HashMap<>(); + final Object runningTasksLock = new Object(); + + + static final int TIME_AVG_FACTOR = 32; + + volatile long avgNanos = 0; + long lastTime = 0; + int lastFrameCount = 0; + + + public AsyncImageSaver() { } // ignore + + + public void dispose() { // ignore + saveExecutor.shutdown(); + try { + saveExecutor.awaitTermination(5000, TimeUnit.SECONDS); + } catch (InterruptedException e) { } + } + + + public boolean hasAvailableTarget() { // ignore + return targetsCreated < TARGET_COUNT || targetPool.isEmpty(); + } + + + /** + * After taking a target, you must call saveTargetAsync() or + * returnUnusedTarget(), otherwise one thread won't be able to run + */ + public PImage getAvailableTarget(int requestedWidth, int requestedHeight, // ignore + int format) { + try { + PImage target; + if (targetsCreated < TARGET_COUNT && targetPool.isEmpty()) { + target = new PImage(requestedWidth, requestedHeight); + targetsCreated++; + } else { + target = targetPool.take(); + if (target.pixelWidth != requestedWidth || + target.pixelHeight != requestedHeight) { + // TODO: this kills performance when saving different sizes + target = new PImage(requestedWidth, requestedHeight); + } + } + target.format = format; + return target; + } catch (InterruptedException e) { + return null; + } + } + + + public void returnUnusedTarget(PImage target) { // ignore + targetPool.offer(target); + } + + + public void saveTargetAsync(final PGraphics renderer, final PImage target, // ignore + final File file) { + target.parent = renderer.parent; + + // if running every frame, smooth the framerate + if (target.parent.frameCount - 1 == lastFrameCount && TARGET_COUNT > 1) { + + // count with one less thread to reduce jitter + // 2 cores - 1 save thread - no wait + // 4 cores - 3 save threads - wait 1/2 of save time + // 8 cores - 7 save threads - wait 1/6 of save time + long avgTimePerFrame = avgNanos / (Math.max(1, TARGET_COUNT - 1)); + long now = System.nanoTime(); + long delay = PApplet.round((lastTime + avgTimePerFrame - now) / 1e6f); + try { + if (delay > 0) Thread.sleep(delay); + } catch (InterruptedException e) { } + } + + lastFrameCount = target.parent.frameCount; + lastTime = System.nanoTime(); + + awaitAsyncSaveCompletion(file); + + // Explicit lock, because submitting a task and putting it into map + // has to be atomic (and happen before task tries to remove itself) + synchronized (runningTasksLock) { + try { + Future task = saveExecutor.submit(() -> { + try { + long startTime = System.nanoTime(); + renderer.processImageBeforeAsyncSave(target); + target.save(file.getAbsolutePath()); + long saveNanos = System.nanoTime() - startTime; + synchronized (AsyncImageSaver.this) { + if (avgNanos == 0) { + avgNanos = saveNanos; + } else if (saveNanos < avgNanos) { + avgNanos = (avgNanos * (TIME_AVG_FACTOR - 1) + saveNanos) / + (TIME_AVG_FACTOR); + } else { + avgNanos = saveNanos; + } + } + } finally { + targetPool.offer(target); + synchronized (runningTasksLock) { + runningTasks.remove(file); + } + } + }); + runningTasks.put(file, task); + } catch (RejectedExecutionException e) { + // the executor service was probably shut down, no more saving for us + } + } + } + + + public void awaitAsyncSaveCompletion(final File file) { // ignore + Future taskWithSameFilename; + synchronized (runningTasksLock) { + taskWithSameFilename = runningTasks.get(file); + } + + if (taskWithSameFilename != null) { + try { + taskWithSameFilename.get(); + } catch (InterruptedException | ExecutionException e) { } + } + } + + } + +} diff --git a/src/main/java/processing/core/PImage.java b/src/main/java/processing/core/PImage.java new file mode 100644 index 0000000..049d198 --- /dev/null +++ b/src/main/java/processing/core/PImage.java @@ -0,0 +1,3438 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2004-14 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + +import java.awt.*; +import java.awt.image.*; +import java.io.*; +import java.util.Iterator; + +import javax.imageio.*; +import javax.imageio.metadata.*; + + +/** + * ( begin auto-generated from PImage.xml ) + * + * Datatype for storing images. Processing can display .gif, + * .jpg, .tga, and .png images. Images may be + * displayed in 2D and 3D space. Before an image is used, it must be loaded + * with the loadImage() function. The PImage class contains + * fields for the width and height of the image, as well as + * an array called pixels[] that contains the values for every pixel + * in the image. The methods described below allow easy access to the + * image's pixels and alpha channel and simplify the process of compositing.
        + *
        using the pixels[] array, be sure to use the + * loadPixels() method on the image to make sure that the pixel data + * is properly loaded.
        + *
        create a new image, use the createImage() function. Do not + * use the syntax new PImage(). + * + * ( end auto-generated ) + * + * @webref image + * @usage Web & Application + * @instanceName pimg any object of type PImage + * @see PApplet#loadImage(String) + * @see PApplet#imageMode(int) + * @see PApplet#createImage(int, int, int) + */ +public class PImage implements PConstants, Cloneable { + + /** + * Format for this image, one of RGB, ARGB or ALPHA. + * note that RGB images still require 0xff in the high byte + * because of how they'll be manipulated by other functions + */ + public int format; + + /** + * ( begin auto-generated from pixels.xml ) + * + * Array containing the values for all the pixels in the display window. + * These values are of the color datatype. This array is the size of the + * display window. For example, if the image is 100x100 pixels, there will + * be 10000 values and if the window is 200x300 pixels, there will be 60000 + * values. The index value defines the position of a value within + * the array. For example, the statement color b = pixels[230] will + * set the variable b to be equal to the value at that location in + * the array.
        + *
        + * Before accessing this array, the data must loaded with the + * loadPixels() function. After the array data has been modified, + * the updatePixels() function must be run to update the changes. + * Without loadPixels(), running the code may (or will in future + * releases) result in a NullPointerException. + * + * ( end auto-generated ) + * + * @webref image:pixels + * @usage web_application + * @brief Array containing the color of every pixel in the image + */ + public int[] pixels; + + /** 1 for most images, 2 for hi-dpi/retina */ + public int pixelDensity = 1; + + /** Actual dimensions of pixels array, taking into account the 2x setting. */ + public int pixelWidth; + public int pixelHeight; + + /** + * ( begin auto-generated from PImage_width.xml ) + * + * The width of the image in units of pixels. + * + * ( end auto-generated ) + * @webref pimage:field + * @usage web_application + * @brief Image width + */ + public int width; + + /** + * ( begin auto-generated from PImage_height.xml ) + * + * The height of the image in units of pixels. + * + * ( end auto-generated ) + * @webref pimage:field + * @usage web_application + * @brief Image height + */ + public int height; + + /** + * Path to parent object that will be used with save(). + * This prevents users from needing savePath() to use PImage.save(). + */ + public PApplet parent; + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** modified portion of the image */ + protected boolean modified; + protected int mx1, my1, mx2, my2; + + /** Loaded pixels flag */ + public boolean loaded = false; + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + // private fields + private int fracU, ifU, fracV, ifV, u1, u2, v1, v2, sX, sY, iw, iw1, ih1; + private int ul, ll, ur, lr, cUL, cLL, cUR, cLR; + private int srcXOffset, srcYOffset; + private int r, g, b, a; + private int[] srcBuffer; + + // fixed point precision is limited to 15 bits!! + static final int PRECISIONB = 15; + static final int PRECISIONF = 1 << PRECISIONB; + static final int PREC_MAXVAL = PRECISIONF-1; + static final int PREC_ALPHA_SHIFT = 24-PRECISIONB; + static final int PREC_RED_SHIFT = 16-PRECISIONB; + + // internal kernel stuff for the gaussian blur filter + private int blurRadius; + private int blurKernelSize; + private int[] blurKernel; + private int[][] blurMult; + + // colour component bitmasks (moved from PConstants in 2.0b7) + public static final int ALPHA_MASK = 0xff000000; + public static final int RED_MASK = 0x00ff0000; + public static final int GREEN_MASK = 0x0000ff00; + public static final int BLUE_MASK = 0x000000ff; + + + ////////////////////////////////////////////////////////////// + + + /** + * ( begin auto-generated from PImage.xml ) + * + * Datatype for storing images. Processing can display .gif, + * .jpg, .tga, and .png images. Images may be + * displayed in 2D and 3D space. Before an image is used, it must be loaded + * with the loadImage() function. The PImage object contains + * fields for the width and height of the image, as well as + * an array called pixels[] which contains the values for every + * pixel in the image. A group of methods, described below, allow easy + * access to the image's pixels and alpha channel and simplify the process + * of compositing. + *

        + * Before using the pixels[] array, be sure to use the + * loadPixels() method on the image to make sure that the pixel data + * is properly loaded. + *

        + * To create a new image, use the createImage() function (do not use + * new PImage()). + * ( end auto-generated ) + * @nowebref + * @usage web_application + * @see PApplet#loadImage(String, String) + * @see PApplet#imageMode(int) + * @see PApplet#createImage(int, int, int) + */ + public PImage() { + format = ARGB; // default to ARGB images for release 0116 + pixelDensity = 1; + } + + + /** + * @nowebref + * @param width image width + * @param height image height + */ + public PImage(int width, int height) { + init(width, height, RGB, 1); + + // toxi: is it maybe better to init the image with max alpha enabled? + //for(int i=0; ipixels[]
        array. This + * function must always be called before reading from or writing to pixels[]. + *

        renderers may or may not seem to require loadPixels() + * or updatePixels(). However, the rule is that any time you want to + * manipulate the pixels[] array, you must first call + * loadPixels(), and after changes have been made, call + * updatePixels(). Even if the renderer may not seem to use this + * function in the current Processing release, this will always be subject + * to change. + * + * ( end auto-generated ) + * + *

        Advanced

        + * Call this when you want to mess with the pixels[] array. + *

        + * For subclasses where the pixels[] buffer isn't set by default, + * this should copy all data into the pixels[] array + * + * @webref pimage:pixels + * @brief Loads the pixel data for the image into its pixels[] array + * @usage web_application + */ + public void loadPixels() { // ignore + if (pixels == null || pixels.length != pixelWidth*pixelHeight) { + pixels = new int[pixelWidth*pixelHeight]; + } + setLoaded(); + } + + + public void updatePixels() { // ignore + updatePixels(0, 0, pixelWidth, pixelHeight); + } + + + /** + * ( begin auto-generated from PImage_updatePixels.xml ) + * + * Updates the image with the data in its pixels[] array. Use in + * conjunction with loadPixels(). If you're only reading pixels from + * the array, there's no need to call updatePixels(). + *

        renderers may or may not seem to require loadPixels() + * or updatePixels(). However, the rule is that any time you want to + * manipulate the pixels[] array, you must first call + * loadPixels(), and after changes have been made, call + * updatePixels(). Even if the renderer may not seem to use this + * function in the current Processing release, this will always be subject + * to change. + *

        + * Currently, none of the renderers use the additional parameters to + * updatePixels(), however this may be implemented in the future. + * + * ( end auto-generated ) + *

        Advanced

        + * Mark the pixels in this region as needing an update. + * This is not currently used by any of the renderers, however the api + * is structured this way in the hope of being able to use this to + * speed things up in the future. + * @webref pimage:pixels + * @brief Updates the image with the data in its pixels[] array + * @usage web_application + * @param x x-coordinate of the upper-left corner + * @param y y-coordinate of the upper-left corner + * @param w width + * @param h height + */ + public void updatePixels(int x, int y, int w, int h) { // ignore + int x2 = x + w; + int y2 = y + h; + + if (!modified) { + mx1 = PApplet.max(0, x); + mx2 = PApplet.min(pixelWidth, x2); + my1 = PApplet.max(0, y); + my2 = PApplet.min(pixelHeight, y2); + modified = true; + + } else { + if (x < mx1) mx1 = PApplet.max(0, x); + if (x > mx2) mx2 = PApplet.min(pixelWidth, x); + if (y < my1) my1 = PApplet.max(0, y); + if (y > my2) my2 = PApplet.min(pixelHeight, y); + + if (x2 < mx1) mx1 = PApplet.max(0, x2); + if (x2 > mx2) mx2 = PApplet.min(pixelWidth, x2); + if (y2 < my1) my1 = PApplet.max(0, y2); + if (y2 > my2) my2 = PApplet.min(pixelHeight, y2); + } + } + + + ////////////////////////////////////////////////////////////// + + // COPYING IMAGE DATA + + + /** + * Duplicate an image, returns new PImage object. + * The pixels[] array for the new object will be unique + * and recopied from the source image. This is implemented as an + * override of Object.clone(). We recommend using get() instead, + * because it prevents you from needing to catch the + * CloneNotSupportedException, and from doing a cast from the result. + */ + @Override + public Object clone() throws CloneNotSupportedException { // ignore + return get(); + } + + + /** + * ( begin auto-generated from PImage_resize.xml ) + * + * Resize the image to a new width and height. To make the image scale + * proportionally, use 0 as the value for the wide or high + * parameter. For instance, to make the width of an image 150 pixels, and + * change the height using the same proportion, use resize(150, 0).
        + *
        + * Even though a PGraphics is technically a PImage, it is not possible to + * rescale the image data found in a PGraphics. (It's simply not possible + * to do this consistently across renderers: technically infeasible with + * P3D, or what would it even do with PDF?) If you want to resize PGraphics + * content, first get a copy of its image data using the get() + * method, and call resize() on the PImage that is returned. + * + * ( end auto-generated ) + * @webref pimage:method + * @brief Changes the size of an image to a new width and height + * @usage web_application + * @param w the resized image width + * @param h the resized image height + * @see PImage#get(int, int, int, int) + */ + public void resize(int w, int h) { // ignore + if (w <= 0 && h <= 0) { + throw new IllegalArgumentException("width or height must be > 0 for resize"); + } + + if (w == 0) { // Use height to determine relative size + float diff = (float) h / (float) height; + w = (int) (width * diff); + } else if (h == 0) { // Use the width to determine relative size + float diff = (float) w / (float) width; + h = (int) (height * diff); + } + + BufferedImage img = + shrinkImage((BufferedImage) getNative(), w*pixelDensity, h*pixelDensity); + + PImage temp = new PImage(img); + this.pixelWidth = temp.width; + this.pixelHeight = temp.height; + + // Get the resized pixel array + this.pixels = temp.pixels; + + this.width = pixelWidth / pixelDensity; + this.height = pixelHeight / pixelDensity; + + // Mark the pixels array as altered + updatePixels(); + } + + + // Adapted from getFasterScaledInstance() method from page 111 of + // "Filthy Rich Clients" by Chet Haase and Romain Guy + // Additional modifications and simplifications have been added, + // plus a fix to deal with an infinite loop if images are expanded. + // http://code.google.com/p/processing/issues/detail?id=1463 + static private BufferedImage shrinkImage(BufferedImage img, + int targetWidth, int targetHeight) { + int type = (img.getTransparency() == Transparency.OPAQUE) ? + BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; + BufferedImage outgoing = img; + BufferedImage scratchImage = null; + Graphics2D g2 = null; + int prevW = outgoing.getWidth(); + int prevH = outgoing.getHeight(); + boolean isTranslucent = img.getTransparency() != Transparency.OPAQUE; + + // Use multi-step technique: start with original size, then scale down in + // multiple passes with drawImage() until the target size is reached + int w = img.getWidth(); + int h = img.getHeight(); + + do { + if (w > targetWidth) { + w /= 2; + // if this is the last step, do the exact size + if (w < targetWidth) { + w = targetWidth; + } + } else if (targetWidth >= w) { + w = targetWidth; + } + if (h > targetHeight) { + h /= 2; + if (h < targetHeight) { + h = targetHeight; + } + } else if (targetHeight >= h) { + h = targetHeight; + } + if (scratchImage == null || isTranslucent) { + // Use a single scratch buffer for all iterations and then copy + // to the final, correctly-sized image before returning + scratchImage = new BufferedImage(w, h, type); + g2 = scratchImage.createGraphics(); + } + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(outgoing, 0, 0, w, h, 0, 0, prevW, prevH, null); + prevW = w; + prevH = h; + outgoing = scratchImage; + } while (w != targetWidth || h != targetHeight); + + if (g2 != null) { + g2.dispose(); + } + + // If we used a scratch buffer that is larger than our target size, + // create an image of the right size and copy the results into it + if (targetWidth != outgoing.getWidth() || + targetHeight != outgoing.getHeight()) { + scratchImage = new BufferedImage(targetWidth, targetHeight, type); + g2 = scratchImage.createGraphics(); + g2.drawImage(outgoing, 0, 0, null); + g2.dispose(); + outgoing = scratchImage; + } + return outgoing; + } + + + ////////////////////////////////////////////////////////////// + + // MARKING IMAGE AS LOADED / FOR USE IN RENDERERS + + + public boolean isLoaded() { // ignore + return loaded; + } + + + public void setLoaded() { // ignore + loaded = true; + } + + + public void setLoaded(boolean l) { // ignore + loaded = l; + } + + + ////////////////////////////////////////////////////////////// + + // GET/SET PIXELS + + + /** + * ( begin auto-generated from PImage_get.xml ) + * + * Reads the color of any pixel or grabs a section of an image. If no + * parameters are specified, the entire image is returned. Use the x + * and y parameters to get the value of one pixel. Get a section of + * the display window by specifying an additional width and + * height parameter. When getting an image, the x and + * y parameters define the coordinates for the upper-left corner of + * the image, regardless of the current imageMode().
        + *
        + * If the pixel requested is outside of the image window, black is + * returned. The numbers returned are scaled according to the current color + * ranges, but only RGB values are returned by this function. For example, + * even though you may have drawn a shape with colorMode(HSB), the + * numbers returned will be in RGB format.
        + *
        + * Getting the color of a single pixel with get(x, y) is easy, but + * not as fast as grabbing the data directly from pixels[]. The + * equivalent statement to get(x, y) using pixels[] is + * pixels[y*width+x]. See the reference for pixels[] for more information. + * + * ( end auto-generated ) + * + *

        Advanced

        + * Returns an ARGB "color" type (a packed 32 bit int with the color. + * If the coordinate is outside the image, zero is returned + * (black, but completely transparent). + *

        + * If the image is in RGB format (i.e. on a PVideo object), + * the value will get its high bits set, just to avoid cases where + * they haven't been set already. + *

        + * If the image is in ALPHA format, this returns a white with its + * alpha value set. + *

        + * This function is included primarily for beginners. It is quite + * slow because it has to check to see if the x, y that was provided + * is inside the bounds, and then has to check to see what image + * type it is. If you want things to be more efficient, access the + * pixels[] array directly. + * + * @webref image:pixels + * @brief Reads the color of any pixel or grabs a rectangle of pixels + * @usage web_application + * @param x x-coordinate of the pixel + * @param y y-coordinate of the pixel + * @see PApplet#set(int, int, int) + * @see PApplet#pixels + * @see PApplet#copy(PImage, int, int, int, int, int, int, int, int) + */ + public int get(int x, int y) { + if ((x < 0) || (y < 0) || (x >= pixelWidth) || (y >= pixelHeight)) return 0; + + switch (format) { + case RGB: + return pixels[y*pixelWidth + x] | 0xff000000; + + case ARGB: + return pixels[y*pixelWidth + x]; + + case ALPHA: + return (pixels[y*pixelWidth + x] << 24) | 0xffffff; + } + return 0; + } + + + /** + * @param w width of pixel rectangle to get + * @param h height of pixel rectangle to get + */ + public PImage get(int x, int y, int w, int h) { + int targetX = 0; + int targetY = 0; + int targetWidth = w; + int targetHeight = h; + boolean cropped = false; + + if (x < 0) { + w += x; // x is negative, removes the left edge from the width + targetX = -x; + cropped = true; + x = 0; + } + if (y < 0) { + h += y; // y is negative, clip the number of rows + targetY = -y; + cropped = true; + y = 0; + } + + if (x + w > pixelWidth) { + w = pixelWidth - x; + cropped = true; + } + if (y + h > pixelHeight) { + h = pixelHeight - y; + cropped = true; + } + + if (w < 0) { + w = 0; + } + if (h < 0) { + h = 0; + } + + int targetFormat = format; + if (cropped && format == RGB) { + targetFormat = ARGB; + } + + PImage target = new PImage(targetWidth / pixelDensity, + targetHeight / pixelDensity, + targetFormat, pixelDensity); + target.parent = parent; // parent may be null so can't use createImage() + if (w > 0 && h > 0) { + getImpl(x, y, w, h, target, targetX, targetY); + } + return target; + } + + + /** + * Returns a copy of this PImage. Equivalent to get(0, 0, width, height). + * Deprecated, just use copy() instead. + */ + public PImage get() { + // Formerly this used clone(), which caused memory problems. + // http://code.google.com/p/processing/issues/detail?id=42 + return get(0, 0, pixelWidth, pixelHeight); + } + + + public PImage copy() { + return get(0, 0, pixelWidth, pixelHeight); + } + + + /** + * Internal function to actually handle getting a block of pixels that + * has already been properly cropped to a valid region. That is, x/y/w/h + * are guaranteed to be inside the image space, so the implementation can + * use the fastest possible pixel copying method. + */ + protected void getImpl(int sourceX, int sourceY, + int sourceWidth, int sourceHeight, + PImage target, int targetX, int targetY) { + int sourceIndex = sourceY*pixelWidth + sourceX; + int targetIndex = targetY*target.pixelWidth + targetX; + for (int row = 0; row < sourceHeight; row++) { + System.arraycopy(pixels, sourceIndex, target.pixels, targetIndex, sourceWidth); + sourceIndex += pixelWidth; + targetIndex += target.pixelWidth; + } + } + + + /** + * ( begin auto-generated from PImage_set.xml ) + * + * Changes the color of any pixel or writes an image directly into the + * display window.
        + *
        + * The x and y parameters specify the pixel to change and the + * color parameter specifies the color value. The color parameter is + * affected by the current color mode (the default is RGB values from 0 to + * 255). When setting an image, the x and y parameters define + * the coordinates for the upper-left corner of the image, regardless of + * the current imageMode(). + *

        + * Setting the color of a single pixel with set(x, y) is easy, but + * not as fast as putting the data directly into pixels[]. The + * equivalent statement to set(x, y, #000000) using pixels[] + * is pixels[y*width+x] = #000000. See the reference for + * pixels[] for more information. + * + * ( end auto-generated ) + * + * @webref image:pixels + * @brief writes a color to any pixel or writes an image into another + * @usage web_application + * @param x x-coordinate of the pixel + * @param y y-coordinate of the pixel + * @param c any value of the color datatype + * @see PImage#get(int, int, int, int) + * @see PImage#pixels + * @see PImage#copy(PImage, int, int, int, int, int, int, int, int) + */ + public void set(int x, int y, int c) { + if ((x < 0) || (y < 0) || (x >= pixelWidth) || (y >= pixelHeight)) return; + pixels[y*pixelWidth + x] = c; + updatePixels(x, y, 1, 1); // slow... + } + + + /** + *

        Advanced

        + * Efficient method of drawing an image's pixels directly to this surface. + * No variations are employed, meaning that any scale, tint, or imageMode + * settings will be ignored. + * + * @param img image to copy into the original image + */ + public void set(int x, int y, PImage img) { + int sx = 0; + int sy = 0; + int sw = img.pixelWidth; + int sh = img.pixelHeight; + + if (x < 0) { // off left edge + sx -= x; + sw += x; + x = 0; + } + if (y < 0) { // off top edge + sy -= y; + sh += y; + y = 0; + } + if (x + sw > pixelWidth) { // off right edge + sw = pixelWidth - x; + } + if (y + sh > pixelHeight) { // off bottom edge + sh = pixelHeight - y; + } + + // this could be nonexistent + if ((sw <= 0) || (sh <= 0)) return; + + setImpl(img, sx, sy, sw, sh, x, y); + } + + + /** + * Internal function to actually handle setting a block of pixels that + * has already been properly cropped from the image to a valid region. + */ + protected void setImpl(PImage sourceImage, + int sourceX, int sourceY, + int sourceWidth, int sourceHeight, + int targetX, int targetY) { + int sourceOffset = sourceY * sourceImage.pixelWidth + sourceX; + int targetOffset = targetY * pixelWidth + targetX; + + for (int y = sourceY; y < sourceY + sourceHeight; y++) { + System.arraycopy(sourceImage.pixels, sourceOffset, pixels, targetOffset, sourceWidth); + sourceOffset += sourceImage.pixelWidth; + targetOffset += pixelWidth; + } + + //updatePixelsImpl(targetX, targetY, sourceWidth, sourceHeight); + updatePixels(targetX, targetY, sourceWidth, sourceHeight); + } + + + + ////////////////////////////////////////////////////////////// + + // ALPHA CHANNEL + + + /** + * @param maskArray array of integers used as the alpha channel, needs to be + * the same length as the image's pixel array. + */ + public void mask(int maskArray[]) { // ignore + loadPixels(); + // don't execute if mask image is different size + if (maskArray.length != pixels.length) { + throw new IllegalArgumentException("mask() can only be used with an image that's the same size."); + } + for (int i = 0; i < pixels.length; i++) { + pixels[i] = ((maskArray[i] & 0xff) << 24) | (pixels[i] & 0xffffff); + } + format = ARGB; + updatePixels(); + } + + + /** + * ( begin auto-generated from PImage_mask.xml ) + * + * Masks part of an image from displaying by loading another image and + * using it as an alpha channel. This mask image should only contain + * grayscale data, but only the blue color channel is used. The mask image + * needs to be the same size as the image to which it is applied.
        + *
        + * In addition to using a mask image, an integer array containing the alpha + * channel data can be specified directly. This method is useful for + * creating dynamically generated alpha masks. This array must be of the + * same length as the target image's pixels array and should contain only + * grayscale data of values between 0-255. + * + * ( end auto-generated ) + * + *

        Advanced

        + * + * Set alpha channel for an image. Black colors in the source + * image will make the destination image completely transparent, + * and white will make things fully opaque. Gray values will + * be in-between steps. + *

        + * Strictly speaking the "blue" value from the source image is + * used as the alpha color. For a fully grayscale image, this + * is correct, but for a color image it's not 100% accurate. + * For a more accurate conversion, first use filter(GRAY) + * which will make the image into a "correct" grayscale by + * performing a proper luminance-based conversion. + * + * @webref pimage:method + * @usage web_application + * @param img image to use as the mask + * @brief Masks part of an image with another image as an alpha channel + */ + public void mask(PImage img) { + img.loadPixels(); + mask(img.pixels); + } + + + + ////////////////////////////////////////////////////////////// + + // IMAGE FILTERS + + + public void filter(int kind) { + loadPixels(); + + switch (kind) { + case BLUR: + // TODO write basic low-pass filter blur here + // what does photoshop do on the edges with this guy? + // better yet.. why bother? just use gaussian with radius 1 + filter(BLUR, 1); + break; + + case GRAY: + if (format == ALPHA) { + // for an alpha image, convert it to an opaque grayscale + for (int i = 0; i < pixels.length; i++) { + int col = 255 - pixels[i]; + pixels[i] = 0xff000000 | (col << 16) | (col << 8) | col; + } + format = RGB; + + } else { + // Converts RGB image data into grayscale using + // weighted RGB components, and keeps alpha channel intact. + // [toxi 040115] + for (int i = 0; i < pixels.length; i++) { + int col = pixels[i]; + // luminance = 0.3*red + 0.59*green + 0.11*blue + // 0.30 * 256 = 77 + // 0.59 * 256 = 151 + // 0.11 * 256 = 28 + int lum = (77*(col>>16&0xff) + 151*(col>>8&0xff) + 28*(col&0xff))>>8; + pixels[i] = (col & ALPHA_MASK) | lum<<16 | lum<<8 | lum; + } + } + break; + + case INVERT: + for (int i = 0; i < pixels.length; i++) { + //pixels[i] = 0xff000000 | + pixels[i] ^= 0xffffff; + } + break; + + case POSTERIZE: + throw new RuntimeException("Use filter(POSTERIZE, int levels) " + + "instead of filter(POSTERIZE)"); + + case OPAQUE: + for (int i = 0; i < pixels.length; i++) { + pixels[i] |= 0xff000000; + } + format = RGB; + break; + + case THRESHOLD: + filter(THRESHOLD, 0.5f); + break; + + // [toxi 050728] added new filters + case ERODE: + erode(); // former dilate(true); + break; + + case DILATE: + dilate(); // former dilate(false); + break; + } + updatePixels(); // mark as modified + } + + + /** + * ( begin auto-generated from PImage_filter.xml ) + * + * Filters an image as defined by one of the following modes:

        THRESHOLD - converts the image to black and white pixels depending if + * they are above or below the threshold defined by the level parameter. + * The level must be between 0.0 (black) and 1.0(white). If no level is + * specified, 0.5 is used.
        + *
        + * GRAY - converts any colors in the image to grayscale equivalents
        + *
        + * INVERT - sets each pixel to its inverse value
        + *
        + * POSTERIZE - limits each channel of the image to the number of colors + * specified as the level parameter
        + *
        + * BLUR - executes a Guassian blur with the level parameter specifying the + * extent of the blurring. If no level parameter is used, the blur is + * equivalent to Guassian blur of radius 1
        + *
        + * OPAQUE - sets the alpha channel to entirely opaque
        + *
        + * ERODE - reduces the light areas with the amount defined by the level + * parameter
        + *
        + * DILATE - increases the light areas with the amount defined by the level parameter + * + * ( end auto-generated ) + * + *

        Advanced

        + * Method to apply a variety of basic filters to this image. + *

        + *

          + *
        • filter(BLUR) provides a basic blur. + *
        • filter(GRAY) converts the image to grayscale based on luminance. + *
        • filter(INVERT) will invert the color components in the image. + *
        • filter(OPAQUE) set all the high bits in the image to opaque + *
        • filter(THRESHOLD) converts the image to black and white. + *
        • filter(DILATE) grow white/light areas + *
        • filter(ERODE) shrink white/light areas + *
        + * Luminance conversion code contributed by + * toxi + *

        + * Gaussian blur code contributed by + * Mario Klingemann + * + * @webref image:pixels + * @brief Converts the image to grayscale or black and white + * @usage web_application + * @param kind Either THRESHOLD, GRAY, OPAQUE, INVERT, POSTERIZE, BLUR, ERODE, or DILATE + * @param param unique for each, see above + */ + public void filter(int kind, float param) { + loadPixels(); + + switch (kind) { + case BLUR: + if (format == ALPHA) + blurAlpha(param); + else if (format == ARGB) + blurARGB(param); + else + blurRGB(param); + break; + + case GRAY: + throw new RuntimeException("Use filter(GRAY) instead of " + + "filter(GRAY, param)"); + + case INVERT: + throw new RuntimeException("Use filter(INVERT) instead of " + + "filter(INVERT, param)"); + + case OPAQUE: + throw new RuntimeException("Use filter(OPAQUE) instead of " + + "filter(OPAQUE, param)"); + + case POSTERIZE: + int levels = (int)param; + if ((levels < 2) || (levels > 255)) { + throw new RuntimeException("Levels must be between 2 and 255 for " + + "filter(POSTERIZE, levels)"); + } + int levels1 = levels - 1; + for (int i = 0; i < pixels.length; i++) { + int rlevel = (pixels[i] >> 16) & 0xff; + int glevel = (pixels[i] >> 8) & 0xff; + int blevel = pixels[i] & 0xff; + rlevel = (((rlevel * levels) >> 8) * 255) / levels1; + glevel = (((glevel * levels) >> 8) * 255) / levels1; + blevel = (((blevel * levels) >> 8) * 255) / levels1; + pixels[i] = ((0xff000000 & pixels[i]) | + (rlevel << 16) | + (glevel << 8) | + blevel); + } + break; + + case THRESHOLD: // greater than or equal to the threshold + int thresh = (int) (param * 255); + for (int i = 0; i < pixels.length; i++) { + int max = Math.max((pixels[i] & RED_MASK) >> 16, + Math.max((pixels[i] & GREEN_MASK) >> 8, + (pixels[i] & BLUE_MASK))); + pixels[i] = (pixels[i] & ALPHA_MASK) | + ((max < thresh) ? 0x000000 : 0xffffff); + } + break; + + // [toxi20050728] added new filters + case ERODE: + throw new RuntimeException("Use filter(ERODE) instead of " + + "filter(ERODE, param)"); + case DILATE: + throw new RuntimeException("Use filter(DILATE) instead of " + + "filter(DILATE, param)"); + } + updatePixels(); // mark as modified + } + + + /** Set the high bits of all pixels to opaque. */ + protected void opaque() { + for (int i = 0; i < pixels.length; i++) { + pixels[i] = 0xFF000000 | pixels[i]; + } + } + + + /** + * Optimized code for building the blur kernel. + * further optimized blur code (approx. 15% for radius=20) + * bigger speed gains for larger radii (~30%) + * added support for various image types (ALPHA, RGB, ARGB) + * [toxi 050728] + */ + protected void buildBlurKernel(float r) { + int radius = (int) (r * 3.5f); + radius = (radius < 1) ? 1 : ((radius < 248) ? radius : 248); + if (blurRadius != radius) { + blurRadius = radius; + blurKernelSize = 1 + blurRadius<<1; + blurKernel = new int[blurKernelSize]; + blurMult = new int[blurKernelSize][256]; + + int bk,bki; + int[] bm,bmi; + + for (int i = 1, radiusi = radius - 1; i < radius; i++) { + blurKernel[radius+i] = blurKernel[radiusi] = bki = radiusi * radiusi; + bm=blurMult[radius+i]; + bmi=blurMult[radiusi--]; + for (int j = 0; j < 256; j++) + bm[j] = bmi[j] = bki*j; + } + bk = blurKernel[radius] = radius * radius; + bm = blurMult[radius]; + for (int j = 0; j < 256; j++) + bm[j] = bk*j; + } + } + + + protected void blurAlpha(float r) { + int sum, cb; + int read, ri, ym, ymi, bk0; + int b2[] = new int[pixels.length]; + int yi = 0; + + buildBlurKernel(r); + + for (int y = 0; y < pixelHeight; y++) { + for (int x = 0; x < pixelWidth; x++) { + //cb = cg = cr = sum = 0; + cb = sum = 0; + read = x - blurRadius; + if (read<0) { + bk0=-read; + read=0; + } else { + if (read >= pixelWidth) + break; + bk0=0; + } + for (int i = bk0; i < blurKernelSize; i++) { + if (read >= pixelWidth) + break; + int c = pixels[read + yi]; + int[] bm = blurMult[i]; + cb += bm[c & BLUE_MASK]; + sum += blurKernel[i]; + read++; + } + ri = yi + x; + b2[ri] = cb / sum; + } + yi += pixelWidth; + } + + yi = 0; + ym = -blurRadius; + ymi = ym * pixelWidth; + + for (int y = 0; y < pixelHeight; y++) { + for (int x = 0; x < pixelWidth; x++) { + cb = sum = 0; + if (ym < 0) { + bk0 = ri = -ym; + read = x; + } else { + if (ym >= pixelHeight) + break; + bk0 = 0; + ri = ym; + read = x + ymi; + } + for (int i = bk0; i < blurKernelSize; i++) { + if (ri >= pixelHeight) + break; + int[] bm = blurMult[i]; + cb += bm[b2[read]]; + sum += blurKernel[i]; + ri++; + read += pixelWidth; + } + pixels[x+yi] = (cb/sum); + } + yi += pixelWidth; + ymi += pixelWidth; + ym++; + } + } + + + protected void blurRGB(float r) { + int sum, cr, cg, cb; //, k; + int /*pixel,*/ read, ri, /*roff,*/ ym, ymi, /*riw,*/ bk0; + int r2[] = new int[pixels.length]; + int g2[] = new int[pixels.length]; + int b2[] = new int[pixels.length]; + int yi = 0; + + buildBlurKernel(r); + + for (int y = 0; y < pixelHeight; y++) { + for (int x = 0; x < pixelWidth; x++) { + cb = cg = cr = sum = 0; + read = x - blurRadius; + if (read < 0) { + bk0 = -read; + read = 0; + } else { + if (read >= pixelWidth) { + break; + } + bk0 = 0; + } + for (int i = bk0; i < blurKernelSize; i++) { + if (read >= pixelWidth) { + break; + } + int c = pixels[read + yi]; + int[] bm = blurMult[i]; + cr += bm[(c & RED_MASK) >> 16]; + cg += bm[(c & GREEN_MASK) >> 8]; + cb += bm[c & BLUE_MASK]; + sum += blurKernel[i]; + read++; + } + ri = yi + x; + r2[ri] = cr / sum; + g2[ri] = cg / sum; + b2[ri] = cb / sum; + } + yi += pixelWidth; + } + + yi = 0; + ym = -blurRadius; + ymi = ym * pixelWidth; + + for (int y = 0; y < pixelHeight; y++) { + for (int x = 0; x < pixelWidth; x++) { + cb = cg = cr = sum = 0; + if (ym < 0) { + bk0 = ri = -ym; + read = x; + } else { + if (ym >= pixelHeight) { + break; + } + bk0 = 0; + ri = ym; + read = x + ymi; + } + for (int i = bk0; i < blurKernelSize; i++) { + if (ri >= pixelHeight) { + break; + } + int[] bm = blurMult[i]; + cr += bm[r2[read]]; + cg += bm[g2[read]]; + cb += bm[b2[read]]; + sum += blurKernel[i]; + ri++; + read += pixelWidth; + } + pixels[x+yi] = 0xff000000 | (cr/sum)<<16 | (cg/sum)<<8 | (cb/sum); + } + yi += pixelWidth; + ymi += pixelWidth; + ym++; + } + } + + + protected void blurARGB(float r) { + int sum, cr, cg, cb, ca; + int /*pixel,*/ read, ri, /*roff,*/ ym, ymi, /*riw,*/ bk0; + int wh = pixels.length; + int r2[] = new int[wh]; + int g2[] = new int[wh]; + int b2[] = new int[wh]; + int a2[] = new int[wh]; + int yi = 0; + + buildBlurKernel(r); + + for (int y = 0; y < pixelHeight; y++) { + for (int x = 0; x < pixelWidth; x++) { + cb = cg = cr = ca = sum = 0; + read = x - blurRadius; + if (read < 0) { + bk0 = -read; + read = 0; + } else { + if (read >= pixelWidth) { + break; + } + bk0=0; + } + for (int i = bk0; i < blurKernelSize; i++) { + if (read >= pixelWidth) { + break; + } + int c = pixels[read + yi]; + int[] bm=blurMult[i]; + ca += bm[(c & ALPHA_MASK) >>> 24]; + cr += bm[(c & RED_MASK) >> 16]; + cg += bm[(c & GREEN_MASK) >> 8]; + cb += bm[c & BLUE_MASK]; + sum += blurKernel[i]; + read++; + } + ri = yi + x; + a2[ri] = ca / sum; + r2[ri] = cr / sum; + g2[ri] = cg / sum; + b2[ri] = cb / sum; + } + yi += pixelWidth; + } + + yi = 0; + ym = -blurRadius; + ymi = ym * pixelWidth; + + for (int y = 0; y < pixelHeight; y++) { + for (int x = 0; x < pixelWidth; x++) { + cb = cg = cr = ca = sum = 0; + if (ym < 0) { + bk0 = ri = -ym; + read = x; + } else { + if (ym >= pixelHeight) { + break; + } + bk0 = 0; + ri = ym; + read = x + ymi; + } + for (int i = bk0; i < blurKernelSize; i++) { + if (ri >= pixelHeight) { + break; + } + int[] bm=blurMult[i]; + ca += bm[a2[read]]; + cr += bm[r2[read]]; + cg += bm[g2[read]]; + cb += bm[b2[read]]; + sum += blurKernel[i]; + ri++; + read += pixelWidth; + } + pixels[x+yi] = (ca/sum)<<24 | (cr/sum)<<16 | (cg/sum)<<8 | (cb/sum); + } + yi += pixelWidth; + ymi += pixelWidth; + ym++; + } + } + + + /** + * Generic dilate/erode filter using luminance values + * as decision factor. [toxi 050728] + */ + protected void dilate() { // formerly dilate(false) + int index = 0; + int maxIndex = pixels.length; + int[] outgoing = new int[maxIndex]; + + // erosion (grow light areas) + while (index < maxIndex) { + int curRowIndex = index; + int maxRowIndex = index + pixelWidth; + while (index < maxRowIndex) { + int orig = pixels[index]; + int result = orig; + int idxLeft = index - 1; + int idxRight = index + 1; + int idxUp = index - pixelWidth; + int idxDown = index + pixelWidth; + if (idxLeft < curRowIndex) { + idxLeft = index; + } + if (idxRight >= maxRowIndex) { + idxRight = index; + } + if (idxUp < 0) { + idxUp = index; + } + if (idxDown >= maxIndex) { + idxDown = index; + } + + int colUp = pixels[idxUp]; + int colLeft = pixels[idxLeft]; + int colDown = pixels[idxDown]; + int colRight = pixels[idxRight]; + + // compute luminance + int currLum = + 77*(orig>>16&0xff) + 151*(orig>>8&0xff) + 28*(orig&0xff); + int lumLeft = + 77*(colLeft>>16&0xff) + 151*(colLeft>>8&0xff) + 28*(colLeft&0xff); + int lumRight = + 77*(colRight>>16&0xff) + 151*(colRight>>8&0xff) + 28*(colRight&0xff); + int lumUp = + 77*(colUp>>16&0xff) + 151*(colUp>>8&0xff) + 28*(colUp&0xff); + int lumDown = + 77*(colDown>>16&0xff) + 151*(colDown>>8&0xff) + 28*(colDown&0xff); + + if (lumLeft > currLum) { + result = colLeft; + currLum = lumLeft; + } + if (lumRight > currLum) { + result = colRight; + currLum = lumRight; + } + if (lumUp > currLum) { + result = colUp; + currLum = lumUp; + } + if (lumDown > currLum) { + result = colDown; + currLum = lumDown; + } + outgoing[index++] = result; + } + } + System.arraycopy(outgoing, 0, pixels, 0, maxIndex); + } + + + protected void erode() { // formerly dilate(true) + int index = 0; + int maxIndex = pixels.length; + int[] outgoing = new int[maxIndex]; + + // dilate (grow dark areas) + while (index < maxIndex) { + int curRowIndex = index; + int maxRowIndex = index + pixelWidth; + while (index < maxRowIndex) { + int orig = pixels[index]; + int result = orig; + int idxLeft = index - 1; + int idxRight = index + 1; + int idxUp = index - pixelWidth; + int idxDown = index + pixelWidth; + if (idxLeft < curRowIndex) { + idxLeft = index; + } + if (idxRight >= maxRowIndex) { + idxRight = index; + } + if (idxUp < 0) { + idxUp = index; + } + if (idxDown >= maxIndex) { + idxDown = index; + } + + int colUp = pixels[idxUp]; + int colLeft = pixels[idxLeft]; + int colDown = pixels[idxDown]; + int colRight = pixels[idxRight]; + + // compute luminance + int currLum = + 77*(orig>>16&0xff) + 151*(orig>>8&0xff) + 28*(orig&0xff); + int lumLeft = + 77*(colLeft>>16&0xff) + 151*(colLeft>>8&0xff) + 28*(colLeft&0xff); + int lumRight = + 77*(colRight>>16&0xff) + 151*(colRight>>8&0xff) + 28*(colRight&0xff); + int lumUp = + 77*(colUp>>16&0xff) + 151*(colUp>>8&0xff) + 28*(colUp&0xff); + int lumDown = + 77*(colDown>>16&0xff) + 151*(colDown>>8&0xff) + 28*(colDown&0xff); + + if (lumLeft < currLum) { + result = colLeft; + currLum = lumLeft; + } + if (lumRight < currLum) { + result = colRight; + currLum = lumRight; + } + if (lumUp < currLum) { + result = colUp; + currLum = lumUp; + } + if (lumDown < currLum) { + result = colDown; + currLum = lumDown; + } + outgoing[index++] = result; + } + } + System.arraycopy(outgoing, 0, pixels, 0, maxIndex); + } + + + + ////////////////////////////////////////////////////////////// + + // COPY + + + /** + * ( begin auto-generated from PImage_copy.xml ) + * + * Copies a region of pixels from one image into another. If the source and + * destination regions aren't the same size, it will automatically resize + * source pixels to fit the specified target region. No alpha information + * is used in the process, however if the source image has an alpha channel + * set, it will be copied as well. + *

        + * As of release 0149, this function ignores imageMode(). + * + * ( end auto-generated ) + * + * @webref image:pixels + * @brief Copies the entire image + * @usage web_application + * @param sx X coordinate of the source's upper left corner + * @param sy Y coordinate of the source's upper left corner + * @param sw source image width + * @param sh source image height + * @param dx X coordinate of the destination's upper left corner + * @param dy Y coordinate of the destination's upper left corner + * @param dw destination image width + * @param dh destination image height + * @see PGraphics#alpha(int) + * @see PImage#blend(PImage, int, int, int, int, int, int, int, int, int) + */ + public void copy(int sx, int sy, int sw, int sh, + int dx, int dy, int dw, int dh) { + blend(this, sx, sy, sw, sh, dx, dy, dw, dh, REPLACE); + } + + +/** + * @param src an image variable referring to the source image. + */ + public void copy(PImage src, + int sx, int sy, int sw, int sh, + int dx, int dy, int dw, int dh) { + blend(src, sx, sy, sw, sh, dx, dy, dw, dh, REPLACE); + } + + + + ////////////////////////////////////////////////////////////// + + // BLEND + + + /** + * ( begin auto-generated from blendColor.xml ) + * + * Blends two color values together based on the blending mode given as the + * MODE parameter. The possible modes are described in the reference + * for the blend() function. + * + * ( end auto-generated ) + *

        Advanced

        + *
          + *
        • REPLACE - destination colour equals colour of source pixel: C = A. + * Sometimes called "Normal" or "Copy" in other software. + * + *
        • BLEND - linear interpolation of colours: + * C = A*factor + B + * + *
        • ADD - additive blending with white clip: + * C = min(A*factor + B, 255). + * Clipped to 0..255, Photoshop calls this "Linear Burn", + * and Director calls it "Add Pin". + * + *
        • SUBTRACT - substractive blend with black clip: + * C = max(B - A*factor, 0). + * Clipped to 0..255, Photoshop calls this "Linear Dodge", + * and Director calls it "Subtract Pin". + * + *
        • DARKEST - only the darkest colour succeeds: + * C = min(A*factor, B). + * Illustrator calls this "Darken". + * + *
        • LIGHTEST - only the lightest colour succeeds: + * C = max(A*factor, B). + * Illustrator calls this "Lighten". + * + *
        • DIFFERENCE - subtract colors from underlying image. + * + *
        • EXCLUSION - similar to DIFFERENCE, but less extreme. + * + *
        • MULTIPLY - Multiply the colors, result will always be darker. + * + *
        • SCREEN - Opposite multiply, uses inverse values of the colors. + * + *
        • OVERLAY - A mix of MULTIPLY and SCREEN. Multiplies dark values, + * and screens light values. + * + *
        • HARD_LIGHT - SCREEN when greater than 50% gray, MULTIPLY when lower. + * + *
        • SOFT_LIGHT - Mix of DARKEST and LIGHTEST. + * Works like OVERLAY, but not as harsh. + * + *
        • DODGE - Lightens light tones and increases contrast, ignores darks. + * Called "Color Dodge" in Illustrator and Photoshop. + * + *
        • BURN - Darker areas are applied, increasing contrast, ignores lights. + * Called "Color Burn" in Illustrator and Photoshop. + *
        + *

        A useful reference for blending modes and their algorithms can be + * found in the SVG + * specification.

        + *

        It is important to note that Processing uses "fast" code, not + * necessarily "correct" code. No biggie, most software does. A nitpicker + * can find numerous "off by 1 division" problems in the blend code where + * >>8 or >>7 is used when strictly speaking + * /255.0 or /127.0 should have been used.

        + *

        For instance, exclusion (not intended for real-time use) reads + * r1 + r2 - ((2 * r1 * r2) / 255) because 255 == 1.0 + * not 256 == 1.0. In other words, (255*255)>>8 is not + * the same as (255*255)/255. But for real-time use the shifts + * are preferrable, and the difference is insignificant for applications + * built with Processing.

        + * + * @webref color:creating_reading + * @usage web_application + * @param c1 the first color to blend + * @param c2 the second color to blend + * @param mode either BLEND, ADD, SUBTRACT, DARKEST, LIGHTEST, DIFFERENCE, EXCLUSION, MULTIPLY, SCREEN, OVERLAY, HARD_LIGHT, SOFT_LIGHT, DODGE, or BURN + * @see PImage#blend(PImage, int, int, int, int, int, int, int, int, int) + * @see PApplet#color(float, float, float, float) + */ + static public int blendColor(int c1, int c2, int mode) { // ignore + switch (mode) { + case REPLACE: return c2; + case BLEND: return blend_blend(c1, c2); + + case ADD: return blend_add_pin(c1, c2); + case SUBTRACT: return blend_sub_pin(c1, c2); + + case LIGHTEST: return blend_lightest(c1, c2); + case DARKEST: return blend_darkest(c1, c2); + + case DIFFERENCE: return blend_difference(c1, c2); + case EXCLUSION: return blend_exclusion(c1, c2); + + case MULTIPLY: return blend_multiply(c1, c2); + case SCREEN: return blend_screen(c1, c2); + + case HARD_LIGHT: return blend_hard_light(c1, c2); + case SOFT_LIGHT: return blend_soft_light(c1, c2); + case OVERLAY: return blend_overlay(c1, c2); + + case DODGE: return blend_dodge(c1, c2); + case BURN: return blend_burn(c1, c2); + } + return 0; + } + + + public void blend(int sx, int sy, int sw, int sh, + int dx, int dy, int dw, int dh, int mode) { + blend(this, sx, sy, sw, sh, dx, dy, dw, dh, mode); + } + + + /** + * ( begin auto-generated from PImage_blend.xml ) + * + * Blends a region of pixels into the image specified by the img + * parameter. These copies utilize full alpha channel support and a choice + * of the following modes to blend the colors of source pixels (A) with the + * ones of pixels in the destination image (B):
        + *
        + * BLEND - linear interpolation of colours: C = A*factor + B
        + *
        + * ADD - additive blending with white clip: C = min(A*factor + B, 255)
        + *
        + * SUBTRACT - subtractive blending with black clip: C = max(B - A*factor, + * 0)
        + *
        + * DARKEST - only the darkest colour succeeds: C = min(A*factor, B)
        + *
        + * LIGHTEST - only the lightest colour succeeds: C = max(A*factor, B)
        + *
        + * DIFFERENCE - subtract colors from underlying image.
        + *
        + * EXCLUSION - similar to DIFFERENCE, but less extreme.
        + *
        + * MULTIPLY - Multiply the colors, result will always be darker.
        + *
        + * SCREEN - Opposite multiply, uses inverse values of the colors.
        + *
        + * OVERLAY - A mix of MULTIPLY and SCREEN. Multiplies dark values, + * and screens light values.
        + *
        + * HARD_LIGHT - SCREEN when greater than 50% gray, MULTIPLY when lower.
        + *
        + * SOFT_LIGHT - Mix of DARKEST and LIGHTEST. + * Works like OVERLAY, but not as harsh.
        + *
        + * DODGE - Lightens light tones and increases contrast, ignores darks. + * Called "Color Dodge" in Illustrator and Photoshop.
        + *
        + * BURN - Darker areas are applied, increasing contrast, ignores lights. + * Called "Color Burn" in Illustrator and Photoshop.
        + *
        + * All modes use the alpha information (highest byte) of source image + * pixels as the blending factor. If the source and destination regions are + * different sizes, the image will be automatically resized to match the + * destination size. If the srcImg parameter is not used, the + * display window is used as the source image.
        + *
        + * As of release 0149, this function ignores imageMode(). + * + * ( end auto-generated ) + * + * @webref image:pixels + * @brief Copies a pixel or rectangle of pixels using different blending modes + * @param src an image variable referring to the source image + * @param sx X coordinate of the source's upper left corner + * @param sy Y coordinate of the source's upper left corner + * @param sw source image width + * @param sh source image height + * @param dx X coordinate of the destinations's upper left corner + * @param dy Y coordinate of the destinations's upper left corner + * @param dw destination image width + * @param dh destination image height + * @param mode Either BLEND, ADD, SUBTRACT, LIGHTEST, DARKEST, DIFFERENCE, EXCLUSION, MULTIPLY, SCREEN, OVERLAY, HARD_LIGHT, SOFT_LIGHT, DODGE, BURN + * + * @see PApplet#alpha(int) + * @see PImage#copy(PImage, int, int, int, int, int, int, int, int) + * @see PImage#blendColor(int,int,int) + */ + public void blend(PImage src, + int sx, int sy, int sw, int sh, + int dx, int dy, int dw, int dh, int mode) { + int sx2 = sx + sw; + int sy2 = sy + sh; + int dx2 = dx + dw; + int dy2 = dy + dh; + + loadPixels(); + if (src == this) { + if (intersect(sx, sy, sx2, sy2, dx, dy, dx2, dy2)) { + blit_resize(get(sx, sy, sw, sh), + 0, 0, sw, sh, + pixels, pixelWidth, pixelHeight, dx, dy, dx2, dy2, mode); + } else { + // same as below, except skip the loadPixels() because it'd be redundant + blit_resize(src, sx, sy, sx2, sy2, + pixels, pixelWidth, pixelHeight, dx, dy, dx2, dy2, mode); + } + } else { + src.loadPixels(); + blit_resize(src, sx, sy, sx2, sy2, + pixels, pixelWidth, pixelHeight, dx, dy, dx2, dy2, mode); + //src.updatePixels(); + } + updatePixels(); + } + + + /** + * Check to see if two rectangles intersect one another + */ + private boolean intersect(int sx1, int sy1, int sx2, int sy2, + int dx1, int dy1, int dx2, int dy2) { + int sw = sx2 - sx1 + 1; + int sh = sy2 - sy1 + 1; + int dw = dx2 - dx1 + 1; + int dh = dy2 - dy1 + 1; + + if (dx1 < sx1) { + dw += dx1 - sx1; + if (dw > sw) { + dw = sw; + } + } else { + int w = sw + sx1 - dx1; + if (dw > w) { + dw = w; + } + } + if (dy1 < sy1) { + dh += dy1 - sy1; + if (dh > sh) { + dh = sh; + } + } else { + int h = sh + sy1 - dy1; + if (dh > h) { + dh = h; + } + } + return !(dw <= 0 || dh <= 0); + } + + + ////////////////////////////////////////////////////////////// + + + /** + * Internal blitter/resizer/copier from toxi. + * Uses bilinear filtering if smooth() has been enabled + * 'mode' determines the blending mode used in the process. + */ + private void blit_resize(PImage img, + int srcX1, int srcY1, int srcX2, int srcY2, + int[] destPixels, int screenW, int screenH, + int destX1, int destY1, int destX2, int destY2, + int mode) { + if (srcX1 < 0) srcX1 = 0; + if (srcY1 < 0) srcY1 = 0; + if (srcX2 > img.pixelWidth) srcX2 = img.pixelWidth; + if (srcY2 > img.pixelHeight) srcY2 = img.pixelHeight; + + int srcW = srcX2 - srcX1; + int srcH = srcY2 - srcY1; + int destW = destX2 - destX1; + int destH = destY2 - destY1; + + boolean smooth = true; // may as well go with the smoothing these days + + if (!smooth) { + srcW++; srcH++; + } + + if (destW <= 0 || destH <= 0 || + srcW <= 0 || srcH <= 0 || + destX1 >= screenW || destY1 >= screenH || + srcX1 >= img.pixelWidth || srcY1 >= img.pixelHeight) { + return; + } + + int dx = (int) (srcW / (float) destW * PRECISIONF); + int dy = (int) (srcH / (float) destH * PRECISIONF); + + srcXOffset = destX1 < 0 ? -destX1 * dx : srcX1 * PRECISIONF; + srcYOffset = destY1 < 0 ? -destY1 * dy : srcY1 * PRECISIONF; + + if (destX1 < 0) { + destW += destX1; + destX1 = 0; + } + if (destY1 < 0) { + destH += destY1; + destY1 = 0; + } + + destW = min(destW, screenW - destX1); + destH = min(destH, screenH - destY1); + + int destOffset = destY1 * screenW + destX1; + srcBuffer = img.pixels; + + if (smooth) { + // use bilinear filtering + iw = img.pixelWidth; + iw1 = img.pixelWidth - 1; + ih1 = img.pixelHeight - 1; + + switch (mode) { + + case BLEND: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + // davbol - renamed old blend_multiply to blend_blend + destPixels[destOffset + x] = + blend_blend(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case ADD: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_add_pin(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case SUBTRACT: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_sub_pin(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case LIGHTEST: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_lightest(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case DARKEST: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_darkest(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case REPLACE: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = filter_bilinear(); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case DIFFERENCE: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_difference(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case EXCLUSION: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_exclusion(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case MULTIPLY: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_multiply(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case SCREEN: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_screen(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case OVERLAY: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_overlay(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case HARD_LIGHT: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_hard_light(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case SOFT_LIGHT: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_soft_light(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + // davbol - proposed 2007-01-09 + case DODGE: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_dodge(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case BURN: + for (int y = 0; y < destH; y++) { + filter_new_scanline(); + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_burn(destPixels[destOffset + x], filter_bilinear()); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + } + + } else { + // nearest neighbour scaling (++fast!) + switch (mode) { + + case BLEND: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + // davbol - renamed old blend_multiply to blend_blend + destPixels[destOffset + x] = + blend_blend(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case ADD: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_add_pin(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case SUBTRACT: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_sub_pin(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case LIGHTEST: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_lightest(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case DARKEST: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_darkest(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case REPLACE: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = srcBuffer[sY + (sX >> PRECISIONB)]; + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case DIFFERENCE: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_difference(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case EXCLUSION: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_exclusion(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case MULTIPLY: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_multiply(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case SCREEN: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_screen(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case OVERLAY: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_overlay(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case HARD_LIGHT: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_hard_light(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case SOFT_LIGHT: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_soft_light(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + // davbol - proposed 2007-01-09 + case DODGE: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_dodge(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + case BURN: + for (int y = 0; y < destH; y++) { + sX = srcXOffset; + sY = (srcYOffset >> PRECISIONB) * img.pixelWidth; + for (int x = 0; x < destW; x++) { + destPixels[destOffset + x] = + blend_burn(destPixels[destOffset + x], + srcBuffer[sY + (sX >> PRECISIONB)]); + sX += dx; + } + destOffset += screenW; + srcYOffset += dy; + } + break; + + } + } + } + + + private void filter_new_scanline() { + sX = srcXOffset; + fracV = srcYOffset & PREC_MAXVAL; + ifV = PREC_MAXVAL - fracV + 1; + v1 = (srcYOffset >> PRECISIONB) * iw; + v2 = min((srcYOffset >> PRECISIONB) + 1, ih1) * iw; + } + + + private int filter_bilinear() { + fracU = sX & PREC_MAXVAL; + ifU = PREC_MAXVAL - fracU + 1; + ul = (ifU * ifV) >> PRECISIONB; + ll = ifU - ul; + ur = ifV - ul; + lr = PREC_MAXVAL + 1 - ul - ll - ur; + u1 = (sX >> PRECISIONB); + u2 = min(u1 + 1, iw1); + + // get color values of the 4 neighbouring texels + cUL = srcBuffer[v1 + u1]; + cUR = srcBuffer[v1 + u2]; + cLL = srcBuffer[v2 + u1]; + cLR = srcBuffer[v2 + u2]; + + r = ((ul*((cUL&RED_MASK)>>16) + ll*((cLL&RED_MASK)>>16) + + ur*((cUR&RED_MASK)>>16) + lr*((cLR&RED_MASK)>>16)) + << PREC_RED_SHIFT) & RED_MASK; + + g = ((ul*(cUL&GREEN_MASK) + ll*(cLL&GREEN_MASK) + + ur*(cUR&GREEN_MASK) + lr*(cLR&GREEN_MASK)) + >>> PRECISIONB) & GREEN_MASK; + + b = (ul*(cUL&BLUE_MASK) + ll*(cLL&BLUE_MASK) + + ur*(cUR&BLUE_MASK) + lr*(cLR&BLUE_MASK)) + >>> PRECISIONB; + + a = ((ul*((cUL&ALPHA_MASK)>>>24) + ll*((cLL&ALPHA_MASK)>>>24) + + ur*((cUR&ALPHA_MASK)>>>24) + lr*((cLR&ALPHA_MASK)>>>24)) + << PREC_ALPHA_SHIFT) & ALPHA_MASK; + + return a | r | g | b; + } + + + + ////////////////////////////////////////////////////////////// + + // internal blending methods + + + private static int min(int a, int b) { + return (a < b) ? a : b; + } + + + private static int max(int a, int b) { + return (a > b) ? a : b; + } + + + ///////////////////////////////////////////////////////////// + + // BLEND MODE IMPLEMENTATIONS + + /* + * Jakub Valtar + * + * All modes use SRC alpha to interpolate between DST and the result of + * the operation: + * + * R = (1 - SRC_ALPHA) * DST + SRC_ALPHA * + * + * Comments above each mode only specify the formula of its operation. + * + * These implementations treat alpha 127 (=255/2) as a perfect 50 % mix. + * + * One alpha value between 126 and 127 is intentionally left out, + * so the step 126 -> 127 is twice as big compared to other steps. + * This is because our colors are in 0..255 range, but we divide + * by right shifting 8 places (=256) which is much faster than + * (correct) float division by 255.0f. The missing value was placed + * between 126 and 127, because limits of the range (near 0 and 255) and + * the middle value (127) have to blend correctly. + * + * Below you will often see RED and BLUE channels (RB) manipulated together + * and GREEN channel (GN) manipulated separately. It is sometimes possible + * because the operation won't use more than 16 bits, so we process the RED + * channel in the upper 16 bits and BLUE channel in the lower 16 bits. This + * decreases the number of operations per pixel and thus makes things faster. + * + * Some of the modes are hand tweaked (various +1s etc.) to be more accurate + * and to produce correct values in extremes. Below is a sketch you can use + * to check any blending function for + * + * 1) Discrepancies between color channels: + * - highlighted by the offending color + * 2) Behavior at extremes (set colorCount to 256): + * - values of all corners are printed to the console + * 3) Rounding errors: + * - set colorCount to lower value to better see color bands + * + +// use powers of 2 in range 2..256 +// to better see color bands +final int colorCount = 256; + +final int blockSize = 3; + +void settings() { + size(blockSize * 256, blockSize * 256); +} + +void setup() { } + +void draw() { + noStroke(); + colorMode(RGB, colorCount-1); + int alpha = (mouseX / blockSize) << 24; + int r, g, b, r2, g2, b2 = 0; + for (int x = 0; x <= 0xFF; x++) { + for (int y = 0; y <= 0xFF; y++) { + int dst = (x << 16) | (x << 8) | x; + int src = (y << 16) | (y << 8) | y | alpha; + int result = testFunction(dst, src); + r = r2 = (result >> 16 & 0xFF); + g = g2 = (result >> 8 & 0xFF); + b = b2 = (result >> 0 & 0xFF); + if (r != g && r != b) r2 = (128 + r2) % 255; + if (g != r && g != b) g2 = (128 + g2) % 255; + if (b != r && b != g) b2 = (128 + b2) % 255; + fill(r2 % colorCount, g2 % colorCount, b2 % colorCount); + rect(x * blockSize, y * blockSize, blockSize, blockSize); + } + } + println( + "alpha:", mouseX/blockSize, + "TL:", hex(get(0, 0)), + "TR:", hex(get(width-1, 0)), + "BR:", hex(get(width-1, height-1)), + "BL:", hex(get(0, height-1))); +} + +int testFunction(int dst, int src) { + // your function here + return dst; +} + + * + * + */ + + private static final int RB_MASK = 0x00FF00FF; + private static final int GN_MASK = 0x0000FF00; + + /** + * Blend + * O = S + */ + private static int blend_blend(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + int d_a = 0x100 - s_a; + + return min((dst >>> 24) + a, 0xFF) << 24 | + ((dst & RB_MASK) * d_a + (src & RB_MASK) * s_a) >>> 8 & RB_MASK | + ((dst & GN_MASK) * d_a + (src & GN_MASK) * s_a) >>> 8 & GN_MASK; + } + + + /** + * Add + * O = MIN(D + S, 1) + */ + private static int blend_add_pin(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + + int rb = (dst & RB_MASK) + ((src & RB_MASK) * s_a >>> 8 & RB_MASK); + int gn = (dst & GN_MASK) + ((src & GN_MASK) * s_a >>> 8); + + return min((dst >>> 24) + a, 0xFF) << 24 | + min(rb & 0xFFFF0000, RED_MASK) | + min(gn & 0x00FFFF00, GREEN_MASK) | + min(rb & 0x0000FFFF, BLUE_MASK); + } + + + /** + * Subtract + * O = MAX(0, D - S) + */ + private static int blend_sub_pin(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + + int rb = ((src & RB_MASK) * s_a >>> 8); + int gn = ((src & GREEN_MASK) * s_a >>> 8); + + return min((dst >>> 24) + a, 0xFF) << 24 | + max((dst & RED_MASK) - (rb & RED_MASK), 0) | + max((dst & GREEN_MASK) - (gn & GREEN_MASK), 0) | + max((dst & BLUE_MASK) - (rb & BLUE_MASK), 0); + } + + + /** + * Lightest + * O = MAX(D, S) + */ + private static int blend_lightest(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + int d_a = 0x100 - s_a; + + int rb = max(src & RED_MASK, dst & RED_MASK) | + max(src & BLUE_MASK, dst & BLUE_MASK); + int gn = max(src & GREEN_MASK, dst & GREEN_MASK); + + return min((dst >>> 24) + a, 0xFF) << 24 | + ((dst & RB_MASK) * d_a + rb * s_a) >>> 8 & RB_MASK | + ((dst & GN_MASK) * d_a + gn * s_a) >>> 8 & GN_MASK; + } + + + /** + * Darkest + * O = MIN(D, S) + */ + private static int blend_darkest(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + int d_a = 0x100 - s_a; + + int rb = min(src & RED_MASK, dst & RED_MASK) | + min(src & BLUE_MASK, dst & BLUE_MASK); + int gn = min(src & GREEN_MASK, dst & GREEN_MASK); + + return min((dst >>> 24) + a, 0xFF) << 24 | + ((dst & RB_MASK) * d_a + rb * s_a) >>> 8 & RB_MASK | + ((dst & GN_MASK) * d_a + gn * s_a) >>> 8 & GN_MASK; + } + + + /** + * Difference + * O = ABS(D - S) + */ + private static int blend_difference(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + int d_a = 0x100 - s_a; + + int r = (dst & RED_MASK) - (src & RED_MASK); + int b = (dst & BLUE_MASK) - (src & BLUE_MASK); + int g = (dst & GREEN_MASK) - (src & GREEN_MASK); + + int rb = (r < 0 ? -r : r) | + (b < 0 ? -b : b); + int gn = (g < 0 ? -g : g); + + return min((dst >>> 24) + a, 0xFF) << 24 | + ((dst & RB_MASK) * d_a + rb * s_a) >>> 8 & RB_MASK | + ((dst & GN_MASK) * d_a + gn * s_a) >>> 8 & GN_MASK; + } + + + /** + * Exclusion + * O = (1 - S)D + S(1 - D) + * O = D + S - 2DS + */ + private static int blend_exclusion(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + int d_a = 0x100 - s_a; + + int d_rb = dst & RB_MASK; + int d_gn = dst & GN_MASK; + + int s_gn = src & GN_MASK; + + int f_r = (dst & RED_MASK) >> 16; + int f_b = (dst & BLUE_MASK); + + int rb_sub = + ((src & RED_MASK) * (f_r + (f_r >= 0x7F ? 1 : 0)) | + (src & BLUE_MASK) * (f_b + (f_b >= 0x7F ? 1 : 0))) + >>> 7 & 0x01FF01FF; + int gn_sub = s_gn * (d_gn + (d_gn >= 0x7F00 ? 0x100 : 0)) + >>> 15 & 0x0001FF00; + + return min((dst >>> 24) + a, 0xFF) << 24 | + (d_rb * d_a + (d_rb + (src & RB_MASK) - rb_sub) * s_a) >>> 8 & RB_MASK | + (d_gn * d_a + (d_gn + s_gn - gn_sub) * s_a) >>> 8 & GN_MASK; + } + + + /* + * Multiply + * O = DS + */ + private static int blend_multiply(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + int d_a = 0x100 - s_a; + + int d_gn = dst & GN_MASK; + + int f_r = (dst & RED_MASK) >> 16; + int f_b = (dst & BLUE_MASK); + + int rb = + ((src & RED_MASK) * (f_r + 1) | + (src & BLUE_MASK) * (f_b + 1)) + >>> 8 & RB_MASK; + int gn = + (src & GREEN_MASK) * (d_gn + 0x100) + >>> 16 & GN_MASK; + + return min((dst >>> 24) + a, 0xFF) << 24 | + ((dst & RB_MASK) * d_a + rb * s_a) >>> 8 & RB_MASK | + (d_gn * d_a + gn * s_a) >>> 8 & GN_MASK; + } + + + /** + * Screen + * O = 1 - (1 - D)(1 - S) + * O = D + S - DS + */ + private static int blend_screen(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + int d_a = 0x100 - s_a; + + int d_rb = dst & RB_MASK; + int d_gn = dst & GN_MASK; + + int s_gn = src & GN_MASK; + + int f_r = (dst & RED_MASK) >> 16; + int f_b = (dst & BLUE_MASK); + + int rb_sub = + ((src & RED_MASK) * (f_r + 1) | + (src & BLUE_MASK) * (f_b + 1)) + >>> 8 & RB_MASK; + int gn_sub = s_gn * (d_gn + 0x100) + >>> 16 & GN_MASK; + + return min((dst >>> 24) + a, 0xFF) << 24 | + (d_rb * d_a + (d_rb + (src & RB_MASK) - rb_sub) * s_a) >>> 8 & RB_MASK | + (d_gn * d_a + (d_gn + s_gn - gn_sub) * s_a) >>> 8 & GN_MASK; + } + + + /** + * Overlay + * O = 2 * MULTIPLY(D, S) = 2DS for D < 0.5 + * O = 2 * SCREEN(D, S) - 1 = 2(S + D - DS) - 1 otherwise + */ + private static int blend_overlay(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + int d_a = 0x100 - s_a; + + int d_r = dst & RED_MASK; + int d_g = dst & GREEN_MASK; + int d_b = dst & BLUE_MASK; + + int s_r = src & RED_MASK; + int s_g = src & GREEN_MASK; + int s_b = src & BLUE_MASK; + + int r = (d_r < 0x800000) ? + d_r * ((s_r >>> 16) + 1) >>> 7 : + 0xFF0000 - ((0x100 - (s_r >>> 16)) * (RED_MASK - d_r) >>> 7); + int g = (d_g < 0x8000) ? + d_g * (s_g + 0x100) >>> 15 : + (0xFF00 - ((0x10000 - s_g) * (GREEN_MASK - d_g) >>> 15)); + int b = (d_b < 0x80) ? + d_b * (s_b + 1) >>> 7 : + (0xFF00 - ((0x100 - s_b) * (BLUE_MASK - d_b) << 1)) >>> 8; + + return min((dst >>> 24) + a, 0xFF) << 24 | + ((dst & RB_MASK) * d_a + ((r | b) & RB_MASK) * s_a) >>> 8 & RB_MASK | + ((dst & GN_MASK) * d_a + (g & GN_MASK) * s_a) >>> 8 & GN_MASK; + } + + + /** + * Hard Light + * O = OVERLAY(S, D) + * + * O = 2 * MULTIPLY(D, S) = 2DS for S < 0.5 + * O = 2 * SCREEN(D, S) - 1 = 2(S + D - DS) - 1 otherwise + */ + private static int blend_hard_light(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + int d_a = 0x100 - s_a; + + int d_r = dst & RED_MASK; + int d_g = dst & GREEN_MASK; + int d_b = dst & BLUE_MASK; + + int s_r = src & RED_MASK; + int s_g = src & GREEN_MASK; + int s_b = src & BLUE_MASK; + + int r = (s_r < 0x800000) ? + s_r * ((d_r >>> 16) + 1) >>> 7 : + 0xFF0000 - ((0x100 - (d_r >>> 16)) * (RED_MASK - s_r) >>> 7); + int g = (s_g < 0x8000) ? + s_g * (d_g + 0x100) >>> 15 : + (0xFF00 - ((0x10000 - d_g) * (GREEN_MASK - s_g) >>> 15)); + int b = (s_b < 0x80) ? + s_b * (d_b + 1) >>> 7 : + (0xFF00 - ((0x100 - d_b) * (BLUE_MASK - s_b) << 1)) >>> 8; + + return min((dst >>> 24) + a, 0xFF) << 24 | + ((dst & RB_MASK) * d_a + ((r | b) & RB_MASK) * s_a) >>> 8 & RB_MASK | + ((dst & GN_MASK) * d_a + (g & GN_MASK) * s_a) >>> 8 & GN_MASK; + } + + + /** + * Soft Light (Pegtop) + * O = (1 - D) * MULTIPLY(D, S) + D * SCREEN(D, S) + * O = (1 - D) * DS + D * (1 - (1 - D)(1 - S)) + * O = 2DS + DD - 2DDS + */ + private static int blend_soft_light(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + int d_a = 0x100 - s_a; + + int d_r = dst & RED_MASK; + int d_g = dst & GREEN_MASK; + int d_b = dst & BLUE_MASK; + + int s_r1 = src & RED_MASK >> 16; + int s_g1 = src & GREEN_MASK >> 8; + int s_b1 = src & BLUE_MASK; + + int d_r1 = (d_r >> 16) + (s_r1 < 7F ? 1 : 0); + int d_g1 = (d_g >> 8) + (s_g1 < 7F ? 1 : 0); + int d_b1 = d_b + (s_b1 < 7F ? 1 : 0); + + int r = (s_r1 * d_r >> 7) + 0xFF * d_r1 * (d_r1 + 1) - + ((s_r1 * d_r1 * d_r1) << 1) & RED_MASK; + int g = (s_g1 * d_g << 1) + 0xFF * d_g1 * (d_g1 + 1) - + ((s_g1 * d_g1 * d_g1) << 1) >>> 8 & GREEN_MASK; + int b = (s_b1 * d_b << 9) + 0xFF * d_b1 * (d_b1 + 1) - + ((s_b1 * d_b1 * d_b1) << 1) >>> 16; + + return min((dst >>> 24) + a, 0xFF) << 24 | + ((dst & RB_MASK) * d_a + (r | b) * s_a) >>> 8 & RB_MASK | + ((dst & GN_MASK) * d_a + g * s_a) >>> 8 & GN_MASK; + } + + + /** + * Dodge + * O = D / (1 - S) + */ + private static int blend_dodge(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + int d_a = 0x100 - s_a; + + int r = (dst & RED_MASK) / (256 - ((src & RED_MASK) >> 16)); + int g = ((dst & GREEN_MASK) << 8) / (256 - ((src & GREEN_MASK) >> 8)); + int b = ((dst & BLUE_MASK) << 8) / (256 - (src & BLUE_MASK)); + + int rb = + (r > 0xFF00 ? 0xFF0000 : ((r << 8) & RED_MASK)) | + (b > 0x00FF ? 0x0000FF : b); + int gn = + (g > 0xFF00 ? 0x00FF00 : (g & GREEN_MASK)); + + return min((dst >>> 24) + a, 0xFF) << 24 | + ((dst & RB_MASK) * d_a + rb * s_a) >>> 8 & RB_MASK | + ((dst & GN_MASK) * d_a + gn * s_a) >>> 8 & GN_MASK; + } + + + /** + * Burn + * O = 1 - (1 - A) / B + */ + private static int blend_burn(int dst, int src) { + int a = src >>> 24; + + int s_a = a + (a >= 0x7F ? 1 : 0); + int d_a = 0x100 - s_a; + + int r = ((0xFF0000 - (dst & RED_MASK))) / (1 + (src & RED_MASK >> 16)); + int g = ((0x00FF00 - (dst & GREEN_MASK)) << 8) / (1 + (src & GREEN_MASK >> 8)); + int b = ((0x0000FF - (dst & BLUE_MASK)) << 8) / (1 + (src & BLUE_MASK)); + + int rb = RB_MASK - + (r > 0xFF00 ? 0xFF0000 : ((r << 8) & RED_MASK)) - + (b > 0x00FF ? 0x0000FF : b); + int gn = GN_MASK - + (g > 0xFF00 ? 0x00FF00 : (g & GREEN_MASK)); + + return min((dst >>> 24) + a, 0xFF) << 24 | + ((dst & RB_MASK) * d_a + rb * s_a) >>> 8 & RB_MASK | + ((dst & GN_MASK) * d_a + gn * s_a) >>> 8 & GN_MASK; + } + + + ////////////////////////////////////////////////////////////// + + // FILE I/O + + + static byte TIFF_HEADER[] = { + 77, 77, 0, 42, 0, 0, 0, 8, 0, 9, 0, -2, 0, 4, 0, 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 3, 0, 0, 0, 1, + 0, 0, 0, 0, 1, 2, 0, 3, 0, 0, 0, 3, 0, 0, 0, 122, 1, 6, 0, 3, 0, + 0, 0, 1, 0, 2, 0, 0, 1, 17, 0, 4, 0, 0, 0, 1, 0, 0, 3, 0, 1, 21, + 0, 3, 0, 0, 0, 1, 0, 3, 0, 0, 1, 22, 0, 3, 0, 0, 0, 1, 0, 0, 0, 0, + 1, 23, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 8, 0, 8 + }; + + + static final String TIFF_ERROR = + "Error: Processing can only read its own TIFF files."; + + static protected PImage loadTIFF(byte tiff[]) { + if ((tiff[42] != tiff[102]) || // width/height in both places + (tiff[43] != tiff[103])) { + System.err.println(TIFF_ERROR); + return null; + } + + int width = + ((tiff[30] & 0xff) << 8) | (tiff[31] & 0xff); + int height = + ((tiff[42] & 0xff) << 8) | (tiff[43] & 0xff); + + int count = + ((tiff[114] & 0xff) << 24) | + ((tiff[115] & 0xff) << 16) | + ((tiff[116] & 0xff) << 8) | + (tiff[117] & 0xff); + if (count != width * height * 3) { + System.err.println(TIFF_ERROR + " (" + width + ", " + height +")"); + return null; + } + + // check the rest of the header + for (int i = 0; i < TIFF_HEADER.length; i++) { + if ((i == 30) || (i == 31) || (i == 42) || (i == 43) || + (i == 102) || (i == 103) || + (i == 114) || (i == 115) || (i == 116) || (i == 117)) continue; + + if (tiff[i] != TIFF_HEADER[i]) { + System.err.println(TIFF_ERROR + " (" + i + ")"); + return null; + } + } + + PImage outgoing = new PImage(width, height, RGB); + int index = 768; + count /= 3; + for (int i = 0; i < count; i++) { + outgoing.pixels[i] = + 0xFF000000 | + (tiff[index++] & 0xff) << 16 | + (tiff[index++] & 0xff) << 8 | + (tiff[index++] & 0xff); + } + return outgoing; + } + + + protected boolean saveTIFF(OutputStream output) { + // shutting off the warning, people can figure this out themselves + /* + if (format != RGB) { + System.err.println("Warning: only RGB information is saved with " + + ".tif files. Use .tga or .png for ARGB images and others."); + } + */ + try { + byte tiff[] = new byte[768]; + System.arraycopy(TIFF_HEADER, 0, tiff, 0, TIFF_HEADER.length); + + tiff[30] = (byte) ((pixelWidth >> 8) & 0xff); + tiff[31] = (byte) ((pixelWidth) & 0xff); + tiff[42] = tiff[102] = (byte) ((pixelHeight >> 8) & 0xff); + tiff[43] = tiff[103] = (byte) ((pixelHeight) & 0xff); + + int count = pixelWidth*pixelHeight*3; + tiff[114] = (byte) ((count >> 24) & 0xff); + tiff[115] = (byte) ((count >> 16) & 0xff); + tiff[116] = (byte) ((count >> 8) & 0xff); + tiff[117] = (byte) ((count) & 0xff); + + // spew the header to the disk + output.write(tiff); + + for (int i = 0; i < pixels.length; i++) { + output.write((pixels[i] >> 16) & 0xff); + output.write((pixels[i] >> 8) & 0xff); + output.write(pixels[i] & 0xff); + } + output.flush(); + return true; + + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + + /** + * Creates a Targa32 formatted byte sequence of specified + * pixel buffer using RLE compression. + *

        + * Also figured out how to avoid parsing the image upside-down + * (there's a header flag to set the image origin to top-left) + *

        + * Starting with revision 0092, the format setting is taken into account: + *
          + *
        • ALPHA images written as 8bit grayscale (uses lowest byte) + *
        • RGB → 24 bits + *
        • ARGB → 32 bits + *
        + * All versions are RLE compressed. + *

        + * Contributed by toxi 8-10 May 2005, based on this RLE + * specification + */ + protected boolean saveTGA(OutputStream output) { + byte header[] = new byte[18]; + + if (format == ALPHA) { // save ALPHA images as 8bit grayscale + header[2] = 0x0B; + header[16] = 0x08; + header[17] = 0x28; + + } else if (format == RGB) { + header[2] = 0x0A; + header[16] = 24; + header[17] = 0x20; + + } else if (format == ARGB) { + header[2] = 0x0A; + header[16] = 32; + header[17] = 0x28; + + } else { + throw new RuntimeException("Image format not recognized inside save()"); + } + // set image dimensions lo-hi byte order + header[12] = (byte) (pixelWidth & 0xff); + header[13] = (byte) (pixelWidth >> 8); + header[14] = (byte) (pixelHeight & 0xff); + header[15] = (byte) (pixelHeight >> 8); + + try { + output.write(header); + + int maxLen = pixelHeight * pixelWidth; + int index = 0; + int col; //, prevCol; + int[] currChunk = new int[128]; + + // 8bit image exporter is in separate loop + // to avoid excessive conditionals... + if (format == ALPHA) { + while (index < maxLen) { + boolean isRLE = false; + int rle = 1; + currChunk[0] = col = pixels[index] & 0xff; + while (index + rle < maxLen) { + if (col != (pixels[index + rle]&0xff) || rle == 128) { + isRLE = (rle > 1); + break; + } + rle++; + } + if (isRLE) { + output.write(0x80 | (rle - 1)); + output.write(col); + + } else { + rle = 1; + while (index + rle < maxLen) { + int cscan = pixels[index + rle] & 0xff; + if ((col != cscan && rle < 128) || rle < 3) { + currChunk[rle] = col = cscan; + } else { + if (col == cscan) rle -= 2; + break; + } + rle++; + } + output.write(rle - 1); + for (int i = 0; i < rle; i++) output.write(currChunk[i]); + } + index += rle; + } + } else { // export 24/32 bit TARGA + while (index < maxLen) { + boolean isRLE = false; + currChunk[0] = col = pixels[index]; + int rle = 1; + // try to find repeating bytes (min. len = 2 pixels) + // maximum chunk size is 128 pixels + while (index + rle < maxLen) { + if (col != pixels[index + rle] || rle == 128) { + isRLE = (rle > 1); // set flag for RLE chunk + break; + } + rle++; + } + if (isRLE) { + output.write(128 | (rle - 1)); + output.write(col & 0xff); + output.write(col >> 8 & 0xff); + output.write(col >> 16 & 0xff); + if (format == ARGB) output.write(col >>> 24 & 0xff); + + } else { // not RLE + rle = 1; + while (index + rle < maxLen) { + if ((col != pixels[index + rle] && rle < 128) || rle < 3) { + currChunk[rle] = col = pixels[index + rle]; + } else { + // check if the exit condition was the start of + // a repeating colour + if (col == pixels[index + rle]) rle -= 2; + break; + } + rle++; + } + // write uncompressed chunk + output.write(rle - 1); + if (format == ARGB) { + for (int i = 0; i < rle; i++) { + col = currChunk[i]; + output.write(col & 0xff); + output.write(col >> 8 & 0xff); + output.write(col >> 16 & 0xff); + output.write(col >>> 24 & 0xff); + } + } else { + for (int i = 0; i < rle; i++) { + col = currChunk[i]; + output.write(col & 0xff); + output.write(col >> 8 & 0xff); + output.write(col >> 16 & 0xff); + } + } + } + index += rle; + } + } + output.flush(); + return true; + + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + + /** + * Use ImageIO functions from Java 1.4 and later to handle image save. + * Various formats are supported, typically jpeg, png, bmp, and wbmp. + * To get a list of the supported formats for writing, use:
        + * println(javax.imageio.ImageIO.getReaderFormatNames()) + */ + protected boolean saveImageIO(String path) throws IOException { + try { + int outputFormat = (format == ARGB) ? + BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB; + + String extension = + path.substring(path.lastIndexOf('.') + 1).toLowerCase(); + + // JPEG and BMP images that have an alpha channel set get pretty unhappy. + // BMP just doesn't write, and JPEG writes it as a CMYK image. + // http://code.google.com/p/processing/issues/detail?id=415 + if (extension.equals("bmp") || extension.equals("jpg") || extension.equals("jpeg")) { + outputFormat = BufferedImage.TYPE_INT_RGB; + } + + BufferedImage bimage = new BufferedImage(pixelWidth, pixelHeight, outputFormat); + bimage.setRGB(0, 0, pixelWidth, pixelHeight, pixels, 0, pixelWidth); + + File file = new File(path); + + ImageWriter writer = null; + ImageWriteParam param = null; + IIOMetadata metadata = null; + + if (extension.equals("jpg") || extension.equals("jpeg")) { + if ((writer = imageioWriter("jpeg")) != null) { + // Set JPEG quality to 90% with baseline optimization. Setting this + // to 1 was a huge jump (about triple the size), so this seems good. + // Oddly, a smaller file size than Photoshop at 90%, but I suppose + // it's a completely different algorithm. + param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(0.9f); + } + } + + if (extension.equals("png")) { + if ((writer = imageioWriter("png")) != null) { + param = writer.getDefaultWriteParam(); + if (false) { + metadata = imageioDPI(writer, param, 100); + } + } + } + + if (writer != null) { + BufferedOutputStream output = + new BufferedOutputStream(PApplet.createOutput(file)); + writer.setOutput(ImageIO.createImageOutputStream(output)); +// writer.write(null, new IIOImage(bimage, null, null), param); + writer.write(metadata, new IIOImage(bimage, null, metadata), param); + writer.dispose(); + + output.flush(); + output.close(); + return true; + } + // If iter.hasNext() somehow fails up top, it falls through to here + return javax.imageio.ImageIO.write(bimage, extension, file); + + } catch (Exception e) { + e.printStackTrace(); + throw new IOException("image save failed."); + } + } + + + private ImageWriter imageioWriter(String extension) { + Iterator iter = ImageIO.getImageWritersByFormatName(extension); + if (iter.hasNext()) { + return iter.next(); + } + return null; + } + + + private IIOMetadata imageioDPI(ImageWriter writer, ImageWriteParam param, double dpi) { + // http://stackoverflow.com/questions/321736/how-to-set-dpi-information-in-an-image + ImageTypeSpecifier typeSpecifier = + ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB); + IIOMetadata metadata = + writer.getDefaultImageMetadata(typeSpecifier, param); + + if (!metadata.isReadOnly() && metadata.isStandardMetadataFormatSupported()) { + // for PNG, it's dots per millimeter + double dotsPerMilli = dpi / 25.4; + + IIOMetadataNode horiz = new IIOMetadataNode("HorizontalPixelSize"); + horiz.setAttribute("value", Double.toString(dotsPerMilli)); + + IIOMetadataNode vert = new IIOMetadataNode("VerticalPixelSize"); + vert.setAttribute("value", Double.toString(dotsPerMilli)); + + IIOMetadataNode dim = new IIOMetadataNode("Dimension"); + dim.appendChild(horiz); + dim.appendChild(vert); + + IIOMetadataNode root = new IIOMetadataNode("javax_imageio_1.0"); + root.appendChild(dim); + + try { + metadata.mergeTree("javax_imageio_1.0", root); + return metadata; + + } catch (IIOInvalidTreeException e) { + System.err.println("Could not set the DPI of the output image"); + e.printStackTrace(); + } + } + return null; + } + + + protected String[] saveImageFormats; + + /** + * ( begin auto-generated from PImage_save.xml ) + * + * Saves the image into a file. Append a file extension to the name of + * the file, to indicate the file format to be used: either TIFF (.tif), + * TARGA (.tga), JPEG (.jpg), or PNG (.png). If no extension is included + * in the filename, the image will save in TIFF format and .tif will be + * added to the name. These files are saved to the sketch's folder, which + * may be opened by selecting "Show sketch folder" from the "Sketch" menu. + *

        To save an image created within the code, rather + * than through loading, it's necessary to make the image with the + * createImage() function so it is aware of the location of the + * program and can therefore save the file to the right place. See the + * createImage() reference for more information. + * + * ( end auto-generated ) + *

        Advanced

        + * Save this image to disk. + *

        + * As of revision 0100, this function requires an absolute path, + * in order to avoid confusion. To save inside the sketch folder, + * use the function savePath() from PApplet, or use saveFrame() instead. + * As of revision 0116, savePath() is not needed if this object has been + * created (as recommended) via createImage() or createGraphics() or + * one of its neighbors. + *

        + * As of revision 0115, when using Java 1.4 and later, you can write + * to several formats besides tga and tiff. If Java 1.4 is installed + * and the extension used is supported (usually png, jpg, jpeg, bmp, + * and tiff), then those methods will be used to write the image. + * To get a list of the supported formats for writing, use:
        + * println(javax.imageio.ImageIO.getReaderFormatNames()) + *

        + * To use the original built-in image writers, use .tga or .tif as the + * extension, or don't include an extension. When no extension is used, + * the extension .tif will be added to the file name. + *

        + * The ImageIO API claims to support wbmp files, however they probably + * require a black and white image. Basic testing produced a zero-length + * file with no error. + * + * @webref pimage:method + * @brief Saves the image to a TIFF, TARGA, PNG, or JPEG file + * @usage application + * @param filename a sequence of letters and numbers + */ + public boolean save(String filename) { // ignore + boolean success = false; + + if (parent != null) { + // use savePath(), so that the intermediate directories are created + filename = parent.savePath(filename); + + } else { + File file = new File(filename); + if (file.isAbsolute()) { + // make sure that the intermediate folders have been created + PApplet.createPath(file); + } else { + String msg = + "PImage.save() requires an absolute path. " + + "Use createImage(), or pass savePath() to save()."; + PGraphics.showException(msg); + } + } + + // Make sure the pixel data is ready to go + loadPixels(); + + try { + OutputStream os = null; + + if (saveImageFormats == null) { + saveImageFormats = javax.imageio.ImageIO.getWriterFormatNames(); + } + if (saveImageFormats != null) { + for (int i = 0; i < saveImageFormats.length; i++) { + if (filename.endsWith("." + saveImageFormats[i])) { + if (!saveImageIO(filename)) { + System.err.println("Error while saving image."); + return false; + } + return true; + } + } + } + + if (filename.toLowerCase().endsWith(".tga")) { + os = new BufferedOutputStream(new FileOutputStream(filename), 32768); + success = saveTGA(os); //, pixels, width, height, format); + + } else { + if (!filename.toLowerCase().endsWith(".tif") && + !filename.toLowerCase().endsWith(".tiff")) { + // if no .tif extension, add it.. + filename += ".tif"; + } + os = new BufferedOutputStream(new FileOutputStream(filename), 32768); + success = saveTIFF(os); //, pixels, width, height); + } + os.flush(); + os.close(); + + } catch (IOException e) { + System.err.println("Error while saving image."); + e.printStackTrace(); + success = false; + } + return success; + } +} diff --git a/src/main/java/processing/core/PMatrix.java b/src/main/java/processing/core/PMatrix.java new file mode 100644 index 0000000..edb1d26 --- /dev/null +++ b/src/main/java/processing/core/PMatrix.java @@ -0,0 +1,208 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2005-08 Ben Fry and Casey Reas + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + + +/** + * A matrix is used to define graphical transformations. PMatrix is the common + * interface for both the 2D and 3D matrix classes in Processing. A matrix is a + * grid of numbers, which can be multiplied by a vector to give another vector. + * Multiplying a point by a particular matrix might translate it, rotate it, + * or carry out a combination of transformations. + * + * Multiplying matrices by each other combines their effects; use the + * {@code apply} and {@code preApply} methods for this. + */ +public interface PMatrix { + + /** + * Make this an identity matrix. Multiplying by it will have no effect. + */ + public void reset(); + + /** + * Returns a copy of this PMatrix. + */ + public PMatrix get(); + + /** + * Copies the matrix contents into a float array. + * If target is null (or not the correct size), a new array will be created. + */ + public float[] get(float[] target); + + + /** + * Make this matrix become a copy of src. + */ + public void set(PMatrix src); + + /** + * Set the contents of this matrix to the contents of source. Fills the + * matrix left-to-right, starting in the top row. + */ + public void set(float[] source); + + /** + * Set the matrix content to this 2D matrix or its 3D equivalent. + */ + public void set(float m00, float m01, float m02, + float m10, float m11, float m12); + + /** + * Set the matrix content to the 3D matrix supplied, if this matrix is 3D. + */ + public void set(float m00, float m01, float m02, float m03, + float m10, float m11, float m12, float m13, + float m20, float m21, float m22, float m23, + float m30, float m31, float m32, float m33); + + + public void translate(float tx, float ty); + + public void translate(float tx, float ty, float tz); + + public void rotate(float angle); + + public void rotateX(float angle); + + public void rotateY(float angle); + + public void rotateZ(float angle); + + public void rotate(float angle, float v0, float v1, float v2); + + public void scale(float s); + + public void scale(float sx, float sy); + + public void scale(float x, float y, float z); + + public void shearX(float angle); + + public void shearY(float angle); + + /** + * Multiply this matrix by another. + */ + public void apply(PMatrix source); + + /** + * Multiply this matrix by another. + */ + public void apply(PMatrix2D source); + + /** + * Multiply this matrix by another. + */ + public void apply(PMatrix3D source); + + /** + * Multiply this matrix by another. + */ + public void apply(float n00, float n01, float n02, + float n10, float n11, float n12); + + /** + * Multiply this matrix by another. + */ + public void apply(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33); + + /** + * Apply another matrix to the left of this one. + */ + public void preApply(PMatrix left); + + /** + * Apply another matrix to the left of this one. + */ + public void preApply(PMatrix2D left); + + /** + * Apply another matrix to the left of this one. 3D only. + */ + public void preApply(PMatrix3D left); + + /** + * Apply another matrix to the left of this one. + */ + public void preApply(float n00, float n01, float n02, + float n10, float n11, float n12); + + /** + * Apply another matrix to the left of this one. 3D only. + */ + public void preApply(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33); + + + /** + * Multiply source by this matrix, and return the result. + * The result will be stored in target if target is non-null, and target + * will then be the matrix returned. This improves performance if you reuse + * target, so it's recommended if you call this many times in draw(). + */ + public PVector mult(PVector source, PVector target); + + + /** + * Multiply a multi-element vector against this matrix. + * Supplying and recycling a target array improves performance, so it's + * recommended if you call this many times in draw(). + */ + public float[] mult(float[] source, float[] target); + + +// public float multX(float x, float y); +// public float multY(float x, float y); + +// public float multX(float x, float y, float z); +// public float multY(float x, float y, float z); +// public float multZ(float x, float y, float z); + + + /** + * Transpose this matrix; rows become columns and columns rows. + */ + public void transpose(); + + + /** + * Invert this matrix. Will not necessarily succeed, because some matrices + * map more than one point to the same image point, and so are irreversible. + * @return true if successful + */ + public boolean invert(); + + + /** + * @return the determinant of the matrix + */ + public float determinant(); +} diff --git a/src/main/java/processing/core/PMatrix2D.java b/src/main/java/processing/core/PMatrix2D.java new file mode 100644 index 0000000..38f82bd --- /dev/null +++ b/src/main/java/processing/core/PMatrix2D.java @@ -0,0 +1,534 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2005-08 Ben Fry and Casey Reas + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + + +/** + * 3x2 affine matrix implementation. + * Matrices are used to describe a transformation; see {@link PMatrix} for a + * general description. This matrix looks like the following when multiplying + * a vector (x, y) in {@code mult()}. + *

        + * [m00 m01 m02][x]   [m00*x + m01*y + m02*1]   [x']
        + * [m10 m11 m12][y] = [m10*x + m11*y + m12*1] = [y']
        + * [ 0   0   1 ][1]   [ 0*x  +  0*y  +  1*1 ]   [ 1]
        + * (x', y') is returned. The values in the matrix determine the transformation. + * They are modified by the various transformation functions. + */ +public class PMatrix2D implements PMatrix { + + public float m00, m01, m02; + public float m10, m11, m12; + + + /** + * Create a new matrix, set to the identity matrix. + */ + public PMatrix2D() { + reset(); + } + + + public PMatrix2D(float m00, float m01, float m02, + float m10, float m11, float m12) { + set(m00, m01, m02, + m10, m11, m12); + } + + + public PMatrix2D(PMatrix matrix) { + set(matrix); + } + + + public void reset() { + set(1, 0, 0, + 0, 1, 0); + } + + + /** + * Returns a copy of this PMatrix. + */ + public PMatrix2D get() { + PMatrix2D outgoing = new PMatrix2D(); + outgoing.set(this); + return outgoing; + } + + + /** + * Copies the matrix contents into a 6 entry float array. + * If target is null (or not the correct size), a new array will be created. + * Returned in the order {@code {m00, m01, m02, m10, m11, m12}}. + */ + public float[] get(float[] target) { + if ((target == null) || (target.length != 6)) { + target = new float[6]; + } + target[0] = m00; + target[1] = m01; + target[2] = m02; + + target[3] = m10; + target[4] = m11; + target[5] = m12; + + return target; + } + + + /** + * If matrix is a PMatrix2D, sets this matrix to be a copy of it. + * @throws IllegalArgumentException If matrix is not 2D. + */ + public void set(PMatrix matrix) { + if (matrix instanceof PMatrix2D) { + PMatrix2D src = (PMatrix2D) matrix; + set(src.m00, src.m01, src.m02, + src.m10, src.m11, src.m12); + } else { + throw new IllegalArgumentException("PMatrix2D.set() only accepts PMatrix2D objects."); + } + } + + + /** + * Unavailable in 2D. Does nothing. + */ + public void set(PMatrix3D src) { + } + + + public void set(float[] source) { + m00 = source[0]; + m01 = source[1]; + m02 = source[2]; + + m10 = source[3]; + m11 = source[4]; + m12 = source[5]; + } + + + /** + * Sets the matrix content. + */ + public void set(float m00, float m01, float m02, + float m10, float m11, float m12) { + this.m00 = m00; this.m01 = m01; this.m02 = m02; + this.m10 = m10; this.m11 = m11; this.m12 = m12; + } + + + /** + * Unavailable in 2D. Does nothing. + */ + public void set(float m00, float m01, float m02, float m03, + float m10, float m11, float m12, float m13, + float m20, float m21, float m22, float m23, + float m30, float m31, float m32, float m33) { + + } + + + public void translate(float tx, float ty) { + m02 = tx*m00 + ty*m01 + m02; + m12 = tx*m10 + ty*m11 + m12; + } + + + /** + * Unavailable in 2D. + * @throws IllegalArgumentException + */ + public void translate(float x, float y, float z) { + throw new IllegalArgumentException("Cannot use translate(x, y, z) on a PMatrix2D."); + } + + + // Implementation roughly based on AffineTransform. + public void rotate(float angle) { + float s = sin(angle); + float c = cos(angle); + + float temp1 = m00; + float temp2 = m01; + m00 = c * temp1 + s * temp2; + m01 = -s * temp1 + c * temp2; + temp1 = m10; + temp2 = m11; + m10 = c * temp1 + s * temp2; + m11 = -s * temp1 + c * temp2; + } + + + /** + * Unavailable in 2D. + * @throws IllegalArgumentException + */ + public void rotateX(float angle) { + throw new IllegalArgumentException("Cannot use rotateX() on a PMatrix2D."); + } + + + /** + * Unavailable in 2D. + * @throws IllegalArgumentException + */ + public void rotateY(float angle) { + throw new IllegalArgumentException("Cannot use rotateY() on a PMatrix2D."); + } + + + public void rotateZ(float angle) { + rotate(angle); + } + + + /** + * Unavailable in 2D. + * @throws IllegalArgumentException + */ + public void rotate(float angle, float v0, float v1, float v2) { + throw new IllegalArgumentException("Cannot use this version of rotate() on a PMatrix2D."); + } + + + public void scale(float s) { + scale(s, s); + } + + + public void scale(float sx, float sy) { + m00 *= sx; m01 *= sy; + m10 *= sx; m11 *= sy; + } + + + /** + * Unavailable in 2D. + * @throws IllegalArgumentException + */ + public void scale(float x, float y, float z) { + throw new IllegalArgumentException("Cannot use this version of scale() on a PMatrix2D."); + } + + + public void shearX(float angle) { + apply(1, 0, 1, tan(angle), 0, 0); + } + + + public void shearY(float angle) { + apply(1, 0, 1, 0, tan(angle), 0); + } + + + public void apply(PMatrix source) { + if (source instanceof PMatrix2D) { + apply((PMatrix2D) source); + } else if (source instanceof PMatrix3D) { + apply((PMatrix3D) source); + } + } + + + public void apply(PMatrix2D source) { + apply(source.m00, source.m01, source.m02, + source.m10, source.m11, source.m12); + } + + + /** + * Unavailable in 2D. + * @throws IllegalArgumentException + */ + public void apply(PMatrix3D source) { + throw new IllegalArgumentException("Cannot use apply(PMatrix3D) on a PMatrix2D."); + } + + + public void apply(float n00, float n01, float n02, + float n10, float n11, float n12) { + float t0 = m00; + float t1 = m01; + m00 = n00 * t0 + n10 * t1; + m01 = n01 * t0 + n11 * t1; + m02 += n02 * t0 + n12 * t1; + + t0 = m10; + t1 = m11; + m10 = n00 * t0 + n10 * t1; + m11 = n01 * t0 + n11 * t1; + m12 += n02 * t0 + n12 * t1; + } + + + /** + * Unavailable in 2D. + * @throws IllegalArgumentException + */ + public void apply(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + throw new IllegalArgumentException("Cannot use this version of apply() on a PMatrix2D."); + } + + + /** + * Apply another matrix to the left of this one. + */ + public void preApply(PMatrix source) { + if (source instanceof PMatrix2D) { + preApply((PMatrix2D) source); + } else if (source instanceof PMatrix3D) { + preApply((PMatrix3D) source); + } + } + + + public void preApply(PMatrix2D left) { + preApply(left.m00, left.m01, left.m02, + left.m10, left.m11, left.m12); + } + + + /** + * Unavailable in 2D. + * @throws IllegalArgumentException + */ + public void preApply(PMatrix3D left) { + throw new IllegalArgumentException("Cannot use preApply(PMatrix3D) on a PMatrix2D."); + } + + + public void preApply(float n00, float n01, float n02, + float n10, float n11, float n12) { + float t0 = m02; + float t1 = m12; + n02 += t0 * n00 + t1 * n01; + n12 += t0 * n10 + t1 * n11; + + m02 = n02; + m12 = n12; + + t0 = m00; + t1 = m10; + m00 = t0 * n00 + t1 * n01; + m10 = t0 * n10 + t1 * n11; + + t0 = m01; + t1 = m11; + m01 = t0 * n00 + t1 * n01; + m11 = t0 * n10 + t1 * n11; + } + + + /** + * Unavailable in 2D. + * @throws IllegalArgumentException + */ + public void preApply(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + throw new IllegalArgumentException("Cannot use this version of preApply() on a PMatrix2D."); + } + + + ////////////////////////////////////////////////////////////// + + + /** + * {@inheritDoc} + * Ignores any z component. + */ + public PVector mult(PVector source, PVector target) { + if (target == null) { + target = new PVector(); + } + target.x = m00*source.x + m01*source.y + m02; + target.y = m10*source.x + m11*source.y + m12; + return target; + } + + + /** + * Multiply a two element vector against this matrix. + * If out is null or not length four, a new float array will be returned. + * The values for vec and out can be the same (though that's less efficient). + */ + public float[] mult(float vec[], float out[]) { + if (out == null || out.length != 2) { + out = new float[2]; + } + + if (vec == out) { + float tx = m00*vec[0] + m01*vec[1] + m02; + float ty = m10*vec[0] + m11*vec[1] + m12; + + out[0] = tx; + out[1] = ty; + + } else { + out[0] = m00*vec[0] + m01*vec[1] + m02; + out[1] = m10*vec[0] + m11*vec[1] + m12; + } + + return out; + } + + + /** + * Returns the x-coordinate of the result of multiplying the point (x, y) + * by this matrix. + */ + public float multX(float x, float y) { + return m00*x + m01*y + m02; + } + + + /** + * Returns the y-coordinate of the result of multiplying the point (x, y) + * by this matrix. + */ + public float multY(float x, float y) { + return m10*x + m11*y + m12; + } + + + + /** + * Unavailable in 2D. Does nothing. + */ + public void transpose() { + } + + + /* + * Implementation stolen from OpenJDK. + */ + public boolean invert() { + float determinant = determinant(); + if (Math.abs(determinant) <= Float.MIN_VALUE) { + return false; + } + + float t00 = m00; + float t01 = m01; + float t02 = m02; + float t10 = m10; + float t11 = m11; + float t12 = m12; + + m00 = t11 / determinant; + m10 = -t10 / determinant; + m01 = -t01 / determinant; + m11 = t00 / determinant; + m02 = (t01 * t12 - t11 * t02) / determinant; + m12 = (t10 * t02 - t00 * t12) / determinant; + + return true; + } + + + /** + * @return the determinant of the matrix + */ + public float determinant() { + return m00 * m11 - m01 * m10; + } + + + ////////////////////////////////////////////////////////////// + + + public void print() { + int big = (int) abs(max(PApplet.max(abs(m00), abs(m01), abs(m02)), + PApplet.max(abs(m10), abs(m11), abs(m12)))); + + int digits = 1; + if (Float.isNaN(big) || Float.isInfinite(big)) { // avoid infinite loop + digits = 5; + } else { + while ((big /= 10) != 0) digits++; // cheap log() + } + + System.out.println(PApplet.nfs(m00, digits, 4) + " " + + PApplet.nfs(m01, digits, 4) + " " + + PApplet.nfs(m02, digits, 4)); + + System.out.println(PApplet.nfs(m10, digits, 4) + " " + + PApplet.nfs(m11, digits, 4) + " " + + PApplet.nfs(m12, digits, 4)); + + System.out.println(); + } + + + ////////////////////////////////////////////////////////////// + + // TODO these need to be added as regular API, but the naming and + // implementation needs to be improved first. (e.g. actually keeping track + // of whether the matrix is in fact identity internally.) + + + protected boolean isIdentity() { + return ((m00 == 1) && (m01 == 0) && (m02 == 0) && + (m10 == 0) && (m11 == 1) && (m12 == 0)); + } + + + // TODO make this more efficient, or move into PMatrix2D + protected boolean isWarped() { + return ((m00 != 1) || (m01 != 0) && + (m10 != 0) || (m11 != 1)); + } + + + ////////////////////////////////////////////////////////////// + + + static private final float max(float a, float b) { + return (a > b) ? a : b; + } + + static private final float abs(float a) { + return (a < 0) ? -a : a; + } + + static private final float sin(float angle) { + return (float)Math.sin(angle); + } + + static private final float cos(float angle) { + return (float)Math.cos(angle); + } + + static private final float tan(float angle) { + return (float)Math.tan(angle); + } +} diff --git a/src/main/java/processing/core/PMatrix3D.java b/src/main/java/processing/core/PMatrix3D.java new file mode 100644 index 0000000..831d9ad --- /dev/null +++ b/src/main/java/processing/core/PMatrix3D.java @@ -0,0 +1,877 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2005-12 Ben Fry and Casey Reas + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License version 2.1 as published by the Free Software Foundation. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + + +/** + * 4x4 matrix implementation. + * Matrices are used to describe a transformation; see {@link PMatrix} for a + * general description. This matrix looks like the following when multiplying + * a vector (x, y, z, w) in {@code mult()}. + *
        + * [m00 m01 m02 m03][x]   [m00*x + m01*y + m02*z + m03*w]   [x']
        + * [m10 m11 m12 m13][y] = [m10*x + m11*y + m12*z + m13*w] = [y']
        + * [m20 m21 m22 m23][z]   [m20*x + m21*y + m22*z + m23*w]   [z']
        + * [m30 m31 m32 m33][w]   [m30*x + m31*y + m32*z + m33*w]   [w']
        + * (x', y', z', w') is returned. The values in the matrix determine the + * transformation. They are modified by the various transformation functions. + * + * To transform 3D coordinates, w is set to 1, amd w' is made to be 1 by + * setting the bottom row of the matrix to [0 0 0 1]. The + * resulting point is then (x', y', z'). + */ +public final class PMatrix3D implements PMatrix /*, PConstants*/ { + + public float m00, m01, m02, m03; + public float m10, m11, m12, m13; + public float m20, m21, m22, m23; + public float m30, m31, m32, m33; + + + // locally allocated version to avoid creating new memory + protected PMatrix3D inverseCopy; + + + public PMatrix3D() { + reset(); + } + + + public PMatrix3D(float m00, float m01, float m02, + float m10, float m11, float m12) { + set(m00, m01, m02, 0, + m10, m11, m12, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + public PMatrix3D(float m00, float m01, float m02, float m03, + float m10, float m11, float m12, float m13, + float m20, float m21, float m22, float m23, + float m30, float m31, float m32, float m33) { + set(m00, m01, m02, m03, + m10, m11, m12, m13, + m20, m21, m22, m23, + m30, m31, m32, m33); + } + + + public PMatrix3D(PMatrix matrix) { + set(matrix); + } + + + public void reset() { + set(1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + /** + * Returns a copy of this PMatrix. + */ + public PMatrix3D get() { + PMatrix3D outgoing = new PMatrix3D(); + outgoing.set(this); + return outgoing; + } + + + /** + * Copies the matrix contents into a 16 entry float array. + * If target is null (or not the correct size), a new array will be created. + */ + public float[] get(float[] target) { + if ((target == null) || (target.length != 16)) { + target = new float[16]; + } + target[0] = m00; + target[1] = m01; + target[2] = m02; + target[3] = m03; + + target[4] = m10; + target[5] = m11; + target[6] = m12; + target[7] = m13; + + target[8] = m20; + target[9] = m21; + target[10] = m22; + target[11] = m23; + + target[12] = m30; + target[13] = m31; + target[14] = m32; + target[15] = m33; + + return target; + } + + + public void set(PMatrix matrix) { + if (matrix instanceof PMatrix3D) { + PMatrix3D src = (PMatrix3D) matrix; + set(src.m00, src.m01, src.m02, src.m03, + src.m10, src.m11, src.m12, src.m13, + src.m20, src.m21, src.m22, src.m23, + src.m30, src.m31, src.m32, src.m33); + } else { + PMatrix2D src = (PMatrix2D) matrix; + set(src.m00, src.m01, 0, src.m02, + src.m10, src.m11, 0, src.m12, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + } + + + public void set(float[] source) { + if (source.length == 6) { + set(source[0], source[1], source[2], + source[3], source[4], source[5]); + + } else if (source.length == 16) { + m00 = source[0]; + m01 = source[1]; + m02 = source[2]; + m03 = source[3]; + + m10 = source[4]; + m11 = source[5]; + m12 = source[6]; + m13 = source[7]; + + m20 = source[8]; + m21 = source[9]; + m22 = source[10]; + m23 = source[11]; + + m30 = source[12]; + m31 = source[13]; + m32 = source[14]; + m33 = source[15]; + } + } + + + public void set(float m00, float m01, float m02, + float m10, float m11, float m12) { + set(m00, m01, 0, m02, + m10, m11, 0, m12, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + public void set(float m00, float m01, float m02, float m03, + float m10, float m11, float m12, float m13, + float m20, float m21, float m22, float m23, + float m30, float m31, float m32, float m33) { + this.m00 = m00; this.m01 = m01; this.m02 = m02; this.m03 = m03; + this.m10 = m10; this.m11 = m11; this.m12 = m12; this.m13 = m13; + this.m20 = m20; this.m21 = m21; this.m22 = m22; this.m23 = m23; + this.m30 = m30; this.m31 = m31; this.m32 = m32; this.m33 = m33; + } + + + public void translate(float tx, float ty) { + translate(tx, ty, 0); + } + +// public void invTranslate(float tx, float ty) { +// invTranslate(tx, ty, 0); +// } + + + public void translate(float tx, float ty, float tz) { + m03 += tx*m00 + ty*m01 + tz*m02; + m13 += tx*m10 + ty*m11 + tz*m12; + m23 += tx*m20 + ty*m21 + tz*m22; + m33 += tx*m30 + ty*m31 + tz*m32; + } + + + public void rotate(float angle) { + rotateZ(angle); + } + + + public void rotateX(float angle) { + float c = cos(angle); + float s = sin(angle); + apply(1, 0, 0, 0, 0, c, -s, 0, 0, s, c, 0, 0, 0, 0, 1); + } + + + public void rotateY(float angle) { + float c = cos(angle); + float s = sin(angle); + apply(c, 0, s, 0, 0, 1, 0, 0, -s, 0, c, 0, 0, 0, 0, 1); + } + + + public void rotateZ(float angle) { + float c = cos(angle); + float s = sin(angle); + apply(c, -s, 0, 0, s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + } + + + public void rotate(float angle, float v0, float v1, float v2) { + float norm2 = v0 * v0 + v1 * v1 + v2 * v2; + if (norm2 < PConstants.EPSILON) { + // The vector is zero, cannot apply rotation. + return; + } + + if (Math.abs(norm2 - 1) > PConstants.EPSILON) { + // The rotation vector is not normalized. + float norm = PApplet.sqrt(norm2); + v0 /= norm; + v1 /= norm; + v2 /= norm; + } + + float c = cos(angle); + float s = sin(angle); + float t = 1.0f - c; + + apply((t*v0*v0) + c, (t*v0*v1) - (s*v2), (t*v0*v2) + (s*v1), 0, + (t*v0*v1) + (s*v2), (t*v1*v1) + c, (t*v1*v2) - (s*v0), 0, + (t*v0*v2) - (s*v1), (t*v1*v2) + (s*v0), (t*v2*v2) + c, 0, + 0, 0, 0, 1); + } + + + public void scale(float s) { + //apply(s, 0, 0, 0, 0, s, 0, 0, 0, 0, s, 0, 0, 0, 0, 1); + scale(s, s, s); + } + + + public void scale(float sx, float sy) { + //apply(sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + scale(sx, sy, 1); + } + + + public void scale(float x, float y, float z) { + //apply(x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1); + m00 *= x; m01 *= y; m02 *= z; + m10 *= x; m11 *= y; m12 *= z; + m20 *= x; m21 *= y; m22 *= z; + m30 *= x; m31 *= y; m32 *= z; + } + + + public void shearX(float angle) { + float t = (float) Math.tan(angle); + apply(1, t, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + public void shearY(float angle) { + float t = (float) Math.tan(angle); + apply(1, 0, 0, 0, + t, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + public void apply(PMatrix source) { + if (source instanceof PMatrix2D) { + apply((PMatrix2D) source); + } else if (source instanceof PMatrix3D) { + apply((PMatrix3D) source); + } + } + + + public void apply(PMatrix2D source) { + apply(source.m00, source.m01, 0, source.m02, + source.m10, source.m11, 0, source.m12, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + public void apply(PMatrix3D source) { + apply(source.m00, source.m01, source.m02, source.m03, + source.m10, source.m11, source.m12, source.m13, + source.m20, source.m21, source.m22, source.m23, + source.m30, source.m31, source.m32, source.m33); + } + + + public void apply(float n00, float n01, float n02, + float n10, float n11, float n12) { + apply(n00, n01, 0, n02, + n10, n11, 0, n12, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + public void apply(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + + float r00 = m00*n00 + m01*n10 + m02*n20 + m03*n30; + float r01 = m00*n01 + m01*n11 + m02*n21 + m03*n31; + float r02 = m00*n02 + m01*n12 + m02*n22 + m03*n32; + float r03 = m00*n03 + m01*n13 + m02*n23 + m03*n33; + + float r10 = m10*n00 + m11*n10 + m12*n20 + m13*n30; + float r11 = m10*n01 + m11*n11 + m12*n21 + m13*n31; + float r12 = m10*n02 + m11*n12 + m12*n22 + m13*n32; + float r13 = m10*n03 + m11*n13 + m12*n23 + m13*n33; + + float r20 = m20*n00 + m21*n10 + m22*n20 + m23*n30; + float r21 = m20*n01 + m21*n11 + m22*n21 + m23*n31; + float r22 = m20*n02 + m21*n12 + m22*n22 + m23*n32; + float r23 = m20*n03 + m21*n13 + m22*n23 + m23*n33; + + float r30 = m30*n00 + m31*n10 + m32*n20 + m33*n30; + float r31 = m30*n01 + m31*n11 + m32*n21 + m33*n31; + float r32 = m30*n02 + m31*n12 + m32*n22 + m33*n32; + float r33 = m30*n03 + m31*n13 + m32*n23 + m33*n33; + + m00 = r00; m01 = r01; m02 = r02; m03 = r03; + m10 = r10; m11 = r11; m12 = r12; m13 = r13; + m20 = r20; m21 = r21; m22 = r22; m23 = r23; + m30 = r30; m31 = r31; m32 = r32; m33 = r33; + } + + + /** + * Apply the 3D equivalent of the 2D matrix supplied to the left of this one. + */ + public void preApply(PMatrix2D left) { + preApply(left.m00, left.m01, 0, left.m02, + left.m10, left.m11, 0, left.m12, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + /** + * Apply another matrix to the left of this one. + */ + public void preApply(PMatrix source) { + if (source instanceof PMatrix2D) { + preApply((PMatrix2D) source); + } else if (source instanceof PMatrix3D) { + preApply((PMatrix3D) source); + } + } + + + /** + * Apply another matrix to the left of this one. + */ + public void preApply(PMatrix3D left) { + preApply(left.m00, left.m01, left.m02, left.m03, + left.m10, left.m11, left.m12, left.m13, + left.m20, left.m21, left.m22, left.m23, + left.m30, left.m31, left.m32, left.m33); + } + + + /** + * Apply the 3D equivalent of the 2D matrix supplied to the left of this one. + */ + public void preApply(float n00, float n01, float n02, + float n10, float n11, float n12) { + preApply(n00, n01, 0, n02, + n10, n11, 0, n12, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + /** + * Apply another matrix to the left of this one. + */ + public void preApply(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + + float r00 = n00*m00 + n01*m10 + n02*m20 + n03*m30; + float r01 = n00*m01 + n01*m11 + n02*m21 + n03*m31; + float r02 = n00*m02 + n01*m12 + n02*m22 + n03*m32; + float r03 = n00*m03 + n01*m13 + n02*m23 + n03*m33; + + float r10 = n10*m00 + n11*m10 + n12*m20 + n13*m30; + float r11 = n10*m01 + n11*m11 + n12*m21 + n13*m31; + float r12 = n10*m02 + n11*m12 + n12*m22 + n13*m32; + float r13 = n10*m03 + n11*m13 + n12*m23 + n13*m33; + + float r20 = n20*m00 + n21*m10 + n22*m20 + n23*m30; + float r21 = n20*m01 + n21*m11 + n22*m21 + n23*m31; + float r22 = n20*m02 + n21*m12 + n22*m22 + n23*m32; + float r23 = n20*m03 + n21*m13 + n22*m23 + n23*m33; + + float r30 = n30*m00 + n31*m10 + n32*m20 + n33*m30; + float r31 = n30*m01 + n31*m11 + n32*m21 + n33*m31; + float r32 = n30*m02 + n31*m12 + n32*m22 + n33*m32; + float r33 = n30*m03 + n31*m13 + n32*m23 + n33*m33; + + m00 = r00; m01 = r01; m02 = r02; m03 = r03; + m10 = r10; m11 = r11; m12 = r12; m13 = r13; + m20 = r20; m21 = r21; m22 = r22; m23 = r23; + m30 = r30; m31 = r31; m32 = r32; m33 = r33; + } + + + ////////////////////////////////////////////////////////////// + + + /** + * Multiply source by this matrix, and return the result. + * The result will be stored in target if target is non-null, and target + * will then be the matrix returned. This improves performance if you reuse + * target, so it's recommended if you call this many times in draw(). + */ + public PVector mult(PVector source, PVector target) { + if (target == null) { + target = new PVector(); + } + target.set(m00*source.x + m01*source.y + m02*source.z + m03, + m10*source.x + m11*source.y + m12*source.z + m13, + m20*source.x + m21*source.y + m22*source.z + m23); +// float tw = m30*source.x + m31*source.y + m32*source.z + m33; +// if (tw != 0 && tw != 1) { +// target.div(tw); +// } + return target; + } + + + /* + public PVector cmult(PVector source, PVector target) { + if (target == null) { + target = new PVector(); + } + target.x = m00*source.x + m10*source.y + m20*source.z + m30; + target.y = m01*source.x + m11*source.y + m21*source.z + m31; + target.z = m02*source.x + m12*source.y + m22*source.z + m32; + float tw = m03*source.x + m13*source.y + m23*source.z + m33; + if (tw != 0 && tw != 1) { + target.div(tw); + } + return target; + } + */ + + + /** + * Multiply a three or four element vector against this matrix. If out is + * null or not length 3 or 4, a new float array (length 3) will be returned. + * Supplying and recycling a target array improves performance, so it's + * recommended if you call this many times in draw. + */ + public float[] mult(float[] source, float[] target) { + if (target == null || target.length < 3) { + target = new float[3]; + } + if (source == target) { + throw new RuntimeException("The source and target vectors used in " + + "PMatrix3D.mult() cannot be identical."); + } + if (target.length == 3) { + target[0] = m00*source[0] + m01*source[1] + m02*source[2] + m03; + target[1] = m10*source[0] + m11*source[1] + m12*source[2] + m13; + target[2] = m20*source[0] + m21*source[1] + m22*source[2] + m23; + //float w = m30*source[0] + m31*source[1] + m32*source[2] + m33; + //if (w != 0 && w != 1) { + // target[0] /= w; target[1] /= w; target[2] /= w; + //} + } else if (target.length > 3) { + target[0] = m00*source[0] + m01*source[1] + m02*source[2] + m03*source[3]; + target[1] = m10*source[0] + m11*source[1] + m12*source[2] + m13*source[3]; + target[2] = m20*source[0] + m21*source[1] + m22*source[2] + m23*source[3]; + target[3] = m30*source[0] + m31*source[1] + m32*source[2] + m33*source[3]; + } + return target; + } + + + /** + * Returns the x-coordinate of the result of multiplying the point (x, y) + * by this matrix. + */ + public float multX(float x, float y) { + return m00*x + m01*y + m03; + } + + + /** + * Returns the y-coordinate of the result of multiplying the point (x, y) + * by this matrix. + */ + public float multY(float x, float y) { + return m10*x + m11*y + m13; + } + + + /** + * Returns the x-coordinate of the result of multiplying the point (x, y, z) + * by this matrix. + */ + public float multX(float x, float y, float z) { + return m00*x + m01*y + m02*z + m03; + } + + + /** + * Returns the y-coordinate of the result of multiplying the point (x, y, z) + * by this matrix. + */ + public float multY(float x, float y, float z) { + return m10*x + m11*y + m12*z + m13; + } + + + /** + * Returns the z-coordinate of the result of multiplying the point (x, y, z) + * by this matrix. + */ + public float multZ(float x, float y, float z) { + return m20*x + m21*y + m22*z + m23; + } + + + /** + * Returns the fourth element of the result of multiplying the vector + * (x, y, z) by this matrix. (Acts as if w = 1 was supplied.) + */ + public float multW(float x, float y, float z) { + return m30*x + m31*y + m32*z + m33; + } + + + /** + * Returns the x-coordinate of the result of multiplying the vector + * (x, y, z, w) by this matrix. + */ + public float multX(float x, float y, float z, float w) { + return m00*x + m01*y + m02*z + m03*w; + } + + + /** + * Returns the y-coordinate of the result of multiplying the vector + * (x, y, z, w) by this matrix. + */ + public float multY(float x, float y, float z, float w) { + return m10*x + m11*y + m12*z + m13*w; + } + + + /** + * Returns the z-coordinate of the result of multiplying the vector + * (x, y, z, w) by this matrix. + */ + public float multZ(float x, float y, float z, float w) { + return m20*x + m21*y + m22*z + m23*w; + } + + + /** + * Returns the w-coordinate of the result of multiplying the vector + * (x, y, z, w) by this matrix. + */ + public float multW(float x, float y, float z, float w) { + return m30*x + m31*y + m32*z + m33*w; + } + + + /** + * Transpose this matrix; rows become columns and columns rows. + */ + public void transpose() { + float temp; + temp = m01; m01 = m10; m10 = temp; + temp = m02; m02 = m20; m20 = temp; + temp = m03; m03 = m30; m30 = temp; + temp = m12; m12 = m21; m21 = temp; + temp = m13; m13 = m31; m31 = temp; + temp = m23; m23 = m32; m32 = temp; + } + + + /** + * Invert this matrix. Will not necessarily succeed, because some matrices + * map more than one point to the same image point, and so are irreversible. + * @return true if successful + */ + public boolean invert() { + float determinant = determinant(); + if (determinant == 0) { + return false; + } + + // first row + float t00 = determinant3x3(m11, m12, m13, m21, m22, m23, m31, m32, m33); + float t01 = -determinant3x3(m10, m12, m13, m20, m22, m23, m30, m32, m33); + float t02 = determinant3x3(m10, m11, m13, m20, m21, m23, m30, m31, m33); + float t03 = -determinant3x3(m10, m11, m12, m20, m21, m22, m30, m31, m32); + + // second row + float t10 = -determinant3x3(m01, m02, m03, m21, m22, m23, m31, m32, m33); + float t11 = determinant3x3(m00, m02, m03, m20, m22, m23, m30, m32, m33); + float t12 = -determinant3x3(m00, m01, m03, m20, m21, m23, m30, m31, m33); + float t13 = determinant3x3(m00, m01, m02, m20, m21, m22, m30, m31, m32); + + // third row + float t20 = determinant3x3(m01, m02, m03, m11, m12, m13, m31, m32, m33); + float t21 = -determinant3x3(m00, m02, m03, m10, m12, m13, m30, m32, m33); + float t22 = determinant3x3(m00, m01, m03, m10, m11, m13, m30, m31, m33); + float t23 = -determinant3x3(m00, m01, m02, m10, m11, m12, m30, m31, m32); + + // fourth row + float t30 = -determinant3x3(m01, m02, m03, m11, m12, m13, m21, m22, m23); + float t31 = determinant3x3(m00, m02, m03, m10, m12, m13, m20, m22, m23); + float t32 = -determinant3x3(m00, m01, m03, m10, m11, m13, m20, m21, m23); + float t33 = determinant3x3(m00, m01, m02, m10, m11, m12, m20, m21, m22); + + // transpose and divide by the determinant + m00 = t00 / determinant; + m01 = t10 / determinant; + m02 = t20 / determinant; + m03 = t30 / determinant; + + m10 = t01 / determinant; + m11 = t11 / determinant; + m12 = t21 / determinant; + m13 = t31 / determinant; + + m20 = t02 / determinant; + m21 = t12 / determinant; + m22 = t22 / determinant; + m23 = t32 / determinant; + + m30 = t03 / determinant; + m31 = t13 / determinant; + m32 = t23 / determinant; + m33 = t33 / determinant; + + return true; + } + + + /** + * Calculate the determinant of a 3x3 matrix. + * @return result + */ + private float determinant3x3(float t00, float t01, float t02, + float t10, float t11, float t12, + float t20, float t21, float t22) { + return (t00 * (t11 * t22 - t12 * t21) + + t01 * (t12 * t20 - t10 * t22) + + t02 * (t10 * t21 - t11 * t20)); + } + + + /** + * @return the determinant of the matrix + */ + public float determinant() { + float f = + m00 + * ((m11 * m22 * m33 + m12 * m23 * m31 + m13 * m21 * m32) + - m13 * m22 * m31 + - m11 * m23 * m32 + - m12 * m21 * m33); + f -= m01 + * ((m10 * m22 * m33 + m12 * m23 * m30 + m13 * m20 * m32) + - m13 * m22 * m30 + - m10 * m23 * m32 + - m12 * m20 * m33); + f += m02 + * ((m10 * m21 * m33 + m11 * m23 * m30 + m13 * m20 * m31) + - m13 * m21 * m30 + - m10 * m23 * m31 + - m11 * m20 * m33); + f -= m03 + * ((m10 * m21 * m32 + m11 * m22 * m30 + m12 * m20 * m31) + - m12 * m21 * m30 + - m10 * m22 * m31 + - m11 * m20 * m32); + return f; + } + + + ////////////////////////////////////////////////////////////// + + // REVERSE VERSIONS OF MATRIX OPERATIONS + + // These functions should not be used, as they will be removed in the future. + + + protected void invTranslate(float tx, float ty, float tz) { + preApply(1, 0, 0, -tx, + 0, 1, 0, -ty, + 0, 0, 1, -tz, + 0, 0, 0, 1); + } + + + protected void invRotateX(float angle) { + float c = cos(-angle); + float s = sin(-angle); + preApply(1, 0, 0, 0, 0, c, -s, 0, 0, s, c, 0, 0, 0, 0, 1); + } + + + protected void invRotateY(float angle) { + float c = cos(-angle); + float s = sin(-angle); + preApply(c, 0, s, 0, 0, 1, 0, 0, -s, 0, c, 0, 0, 0, 0, 1); + } + + + protected void invRotateZ(float angle) { + float c = cos(-angle); + float s = sin(-angle); + preApply(c, -s, 0, 0, s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + } + + + protected void invRotate(float angle, float v0, float v1, float v2) { + //TODO should make sure this vector is normalized + + float c = cos(-angle); + float s = sin(-angle); + float t = 1.0f - c; + + preApply((t*v0*v0) + c, (t*v0*v1) - (s*v2), (t*v0*v2) + (s*v1), 0, + (t*v0*v1) + (s*v2), (t*v1*v1) + c, (t*v1*v2) - (s*v0), 0, + (t*v0*v2) - (s*v1), (t*v1*v2) + (s*v0), (t*v2*v2) + c, 0, + 0, 0, 0, 1); + } + + + protected void invScale(float x, float y, float z) { + preApply(1/x, 0, 0, 0, 0, 1/y, 0, 0, 0, 0, 1/z, 0, 0, 0, 0, 1); + } + + + protected boolean invApply(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + if (inverseCopy == null) { + inverseCopy = new PMatrix3D(); + } + inverseCopy.set(n00, n01, n02, n03, + n10, n11, n12, n13, + n20, n21, n22, n23, + n30, n31, n32, n33); + if (!inverseCopy.invert()) { + return false; + } + preApply(inverseCopy); + return true; + } + + + ////////////////////////////////////////////////////////////// + + + public void print() { + /* + System.out.println(m00 + " " + m01 + " " + m02 + " " + m03 + "\n" + + m10 + " " + m11 + " " + m12 + " " + m13 + "\n" + + m20 + " " + m21 + " " + m22 + " " + m23 + "\n" + + m30 + " " + m31 + " " + m32 + " " + m33 + "\n"); + */ + int big = (int) Math.abs(max(max(max(max(abs(m00), abs(m01)), + max(abs(m02), abs(m03))), + max(max(abs(m10), abs(m11)), + max(abs(m12), abs(m13)))), + max(max(max(abs(m20), abs(m21)), + max(abs(m22), abs(m23))), + max(max(abs(m30), abs(m31)), + max(abs(m32), abs(m33)))))); + + int digits = 1; + if (Float.isNaN(big) || Float.isInfinite(big)) { // avoid infinite loop + digits = 5; + } else { + while ((big /= 10) != 0) digits++; // cheap log() + } + + System.out.println(PApplet.nfs(m00, digits, 4) + " " + + PApplet.nfs(m01, digits, 4) + " " + + PApplet.nfs(m02, digits, 4) + " " + + PApplet.nfs(m03, digits, 4)); + + System.out.println(PApplet.nfs(m10, digits, 4) + " " + + PApplet.nfs(m11, digits, 4) + " " + + PApplet.nfs(m12, digits, 4) + " " + + PApplet.nfs(m13, digits, 4)); + + System.out.println(PApplet.nfs(m20, digits, 4) + " " + + PApplet.nfs(m21, digits, 4) + " " + + PApplet.nfs(m22, digits, 4) + " " + + PApplet.nfs(m23, digits, 4)); + + System.out.println(PApplet.nfs(m30, digits, 4) + " " + + PApplet.nfs(m31, digits, 4) + " " + + PApplet.nfs(m32, digits, 4) + " " + + PApplet.nfs(m33, digits, 4)); + + System.out.println(); + } + + + ////////////////////////////////////////////////////////////// + + + static private final float max(float a, float b) { + return (a > b) ? a : b; + } + + static private final float abs(float a) { + return (a < 0) ? -a : a; + } + + static private final float sin(float angle) { + return (float) Math.sin(angle); + } + + static private final float cos(float angle) { + return (float) Math.cos(angle); + } +} diff --git a/src/main/java/processing/core/PShape.java b/src/main/java/processing/core/PShape.java new file mode 100644 index 0000000..7d6f1d7 --- /dev/null +++ b/src/main/java/processing/core/PShape.java @@ -0,0 +1,3445 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2006-10 Ben Fry and Casey Reas + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License version 2.1 as published by the Free Software Foundation. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + +import java.util.HashMap; +import java.util.Map; + +import processing.core.PApplet; + + +/** + * ( begin auto-generated from PShape.xml ) + * + * Datatype for storing shapes. Processing can currently load and display + * SVG (Scalable Vector Graphics) shapes. Before a shape is used, it must + * be loaded with the loadShape() function. The shape() + * function is used to draw the shape to the display window. The + * PShape object contain a group of methods, linked below, that can + * operate on the shape data. + *

        + * The loadShape() function supports SVG files created with Inkscape + * and Adobe Illustrator. It is not a full SVG implementation, but offers + * some straightforward support for handling vector data. + * + * ( end auto-generated ) + *

        Advanced

        + * + * In-progress class to handle shape data, currently to be considered of + * alpha or beta quality. Major structural work may be performed on this class + * after the release of Processing 1.0. Such changes may include: + * + *
          + *
        • addition of proper accessors to read shape vertex and coloring data + * (this is the second most important part of having a PShape class after all). + *
        • a means of creating PShape objects ala beginShape() and endShape(). + *
        • load(), update(), and cache methods ala PImage, so that shapes can + * have renderer-specific optimizations, such as vertex arrays in OpenGL. + *
        • splitting this class into multiple classes to handle different + * varieties of shape data (primitives vs collections of vertices vs paths) + *
        • change of package declaration, for instance moving the code into + * package processing.shape (if the code grows too much). + *
        + * + *

        For the time being, this class and its shape() and loadShape() friends in + * PApplet exist as placeholders for more exciting things to come. If you'd + * like to work with this class, make a subclass (see how PShapeSVG works) + * and you can play with its internal methods all you like.

        + * + *

        Library developers are encouraged to create PShape objects when loading + * shape data, so that they can eventually hook into the bounty that will be + * the PShape interface, and the ease of loadShape() and shape().

        + * + * @webref shape + * @usage Web & Application + * @see PApplet#loadShape(String) + * @see PApplet#createShape() + * @see PApplet#shapeMode(int) + * @instanceName sh any variable of type PShape + */ +public class PShape implements PConstants { + protected String name; + protected Map nameTable; + +// /** Generic, only draws its child objects. */ +// static public final int GROUP = 0; + // GROUP now inherited from PConstants, and is still zero + + // These constants were updated in 3.0b6 so that they could be distinguished + // from others in PConstants and improve how some typos were handled. + // https://github.com/processing/processing/issues/3776 + /** A line, ellipse, arc, image, etc. */ + static public final int PRIMITIVE = 101; + /** A series of vertex, curveVertex, and bezierVertex calls. */ + static public final int PATH = 102; + /** Collections of vertices created with beginShape(). */ + static public final int GEOMETRY = 103; + /** The shape type, one of GROUP, PRIMITIVE, PATH, or GEOMETRY. */ + protected int family; + + /** ELLIPSE, LINE, QUAD; TRIANGLE_FAN, QUAD_STRIP; etc. */ + protected int kind; + + protected PMatrix matrix; + + protected int textureMode; + + /** Texture or image data associated with this shape. */ + protected PImage image; + + public static final String OUTSIDE_BEGIN_END_ERROR = + "%1$s can only be called between beginShape() and endShape()"; + + public static final String INSIDE_BEGIN_END_ERROR = + "%1$s can only be called outside beginShape() and endShape()"; + + public static final String NO_SUCH_VERTEX_ERROR = + "%1$s vertex index does not exist"; + + static public final String NO_VERTICES_ERROR = + "getVertexCount() only works with PATH or GEOMETRY shapes"; + + public static final String NOT_A_SIMPLE_VERTEX = + "%1$s can not be called on quadratic or bezier vertices"; + + static public final String PER_VERTEX_UNSUPPORTED = + "This renderer does not support %1$s for individual vertices"; + + /** + * ( begin auto-generated from PShape_width.xml ) + * + * The width of the PShape document. + * + * ( end auto-generated ) + * @webref pshape:field + * @usage web_application + * @brief Shape document width + * @see PShape#height + */ + public float width; + /** + * ( begin auto-generated from PShape_height.xml ) + * + * The height of the PShape document. + * + * ( end auto-generated ) + * @webref pshape:field + * @usage web_application + * @brief Shape document height + * @see PShape#width + */ + public float height; + + public float depth; + + PGraphics g; + + // set to false if the object is hidden in the layers palette + protected boolean visible = true; + + /** Retained shape being created with beginShape/endShape */ + protected boolean openShape = false; + + protected boolean openContour = false; + + protected boolean stroke; + protected int strokeColor; + protected float strokeWeight; // default is 1 + protected int strokeCap; + protected int strokeJoin; + + protected boolean fill; + protected int fillColor; + + protected boolean tint; + protected int tintColor; + + protected int ambientColor; + protected boolean setAmbient; + protected int specularColor; + protected int emissiveColor; + protected float shininess; + + protected int sphereDetailU, sphereDetailV; + protected int rectMode; + protected int ellipseMode; + + /** Temporary toggle for whether styles should be honored. */ + protected boolean style = true; + + /** For primitive shapes in particular, params like x/y/w/h or x1/y1/x2/y2. */ + protected float[] params; + + protected int vertexCount; + /** + * When drawing POLYGON shapes, the second param is an array of length + * VERTEX_FIELD_COUNT. When drawing PATH shapes, the second param has only + * two variables. + */ + protected float[][] vertices; + + protected PShape parent; + protected int childCount; + protected PShape[] children; + + + /** Array of VERTEX, BEZIER_VERTEX, and CURVE_VERTEX calls. */ + protected int vertexCodeCount; + protected int[] vertexCodes; + /** True if this is a closed path. */ + protected boolean close; + + // ........................................................ + + // internal color for setting/calculating + protected float calcR, calcG, calcB, calcA; + protected int calcRi, calcGi, calcBi, calcAi; + protected int calcColor; + protected boolean calcAlpha; + + /** The current colorMode */ + public int colorMode; // = RGB; + + /** Max value for red (or hue) set by colorMode */ + public float colorModeX; // = 255; + + /** Max value for green (or saturation) set by colorMode */ + public float colorModeY; // = 255; + + /** Max value for blue (or value) set by colorMode */ + public float colorModeZ; // = 255; + + /** Max value for alpha set by colorMode */ + public float colorModeA; // = 255; + + /** True if colors are not in the range 0..1 */ + boolean colorModeScale; // = true; + + /** True if colorMode(RGB, 255) */ + boolean colorModeDefault; // = true; + + /** True if contains 3D data */ + protected boolean is3D = false; + + protected boolean perVertexStyles = false; + + // should this be called vertices (consistent with PGraphics internals) + // or does that hurt flexibility? + + + // POINTS, LINES, xLINE_STRIP, xLINE_LOOP + // TRIANGLES, TRIANGLE_STRIP, TRIANGLE_FAN + // QUADS, QUAD_STRIP + // xPOLYGON +// static final int PATH = 1; // POLYGON, LINE_LOOP, LINE_STRIP +// static final int GROUP = 2; + + // how to handle rectmode/ellipsemode? + // are they bitshifted into the constant? + // CORNER, CORNERS, CENTER, (CENTER_RADIUS?) +// static final int RECT = 3; // could just be QUAD, but would be x1/y1/x2/y2 +// static final int ELLIPSE = 4; +// +// static final int VERTEX = 7; +// static final int CURVE = 5; +// static final int BEZIER = 6; + + + // fill and stroke functions will need a pointer to the parent + // PGraphics object.. may need some kind of createShape() fxn + // or maybe the values are stored until draw() is called? + + // attaching images is very tricky.. it's a different type of data + + // material parameters will be thrown out, + // except those currently supported (kinds of lights) + + // pivot point for transformations +// public float px; +// public float py; + + + /** + * @nowebref + */ + public PShape() { + this.family = GROUP; + } + + + /** + * @nowebref + */ + public PShape(int family) { + this.family = family; + } + + + /** + * @nowebref + */ + public PShape(PGraphics g, int family) { + this.g = g; + this.family = family; + + // Style parameters are retrieved from the current values in the renderer. + textureMode = g.textureMode; + + colorMode(g.colorMode, + g.colorModeX, g.colorModeY, g.colorModeZ, g.colorModeA); + + // Initial values for fill, stroke and tint colors are also imported from + // the renderer. This is particular relevant for primitive shapes, since is + // not possible to set their color separately when creating them, and their + // input vertices are actually generated at rendering time, by which the + // color configuration of the renderer might have changed. + fill = g.fill; + fillColor = g.fillColor; + + stroke = g.stroke; + strokeColor = g.strokeColor; + strokeWeight = g.strokeWeight; + strokeCap = g.strokeCap; + strokeJoin = g.strokeJoin; + + tint = g.tint; + tintColor = g.tintColor; + + setAmbient = g.setAmbient; + ambientColor = g.ambientColor; + specularColor = g.specularColor; + emissiveColor = g.emissiveColor; + shininess = g.shininess; + + sphereDetailU = g.sphereDetailU; + sphereDetailV = g.sphereDetailV; + +// bezierDetail = pg.bezierDetail; +// curveDetail = pg.curveDetail; +// curveTightness = pg.curveTightness; + + rectMode = g.rectMode; + ellipseMode = g.ellipseMode; + +// normalX = normalY = 0; +// normalZ = 1; +// +// normalMode = NORMAL_MODE_AUTO; + + // To make sure that the first vertex is marked as a break. + // Same behavior as in the immediate mode. +// breakShape = false; + + if (family == GROUP) { + // GROUP shapes are always marked as ended. +// shapeCreated = true; + // TODO why was this commented out? + } + } + + + public PShape(PGraphics g, int kind, float... params) { + this(g, PRIMITIVE); + setKind(kind); + setParams(params); + } + + + public void setFamily(int family) { + this.family = family; + } + + + public void setKind(int kind) { + this.kind = kind; + } + + + public void setName(String name) { + this.name = name; + } + + + public String getName() { + return name; + } + + /** + * ( begin auto-generated from PShape_isVisible.xml ) + * + * Returns a boolean value "true" if the image is set to be visible, + * "false" if not. This is modified with the setVisible() parameter. + *

        + * The visibility of a shape is usually controlled by whatever program + * created the SVG file. For instance, this parameter is controlled by + * showing or hiding the shape in the layers palette in Adobe Illustrator. + * + * ( end auto-generated ) + * @webref pshape:method + * @usage web_application + * @brief Returns a boolean value "true" if the image is set to be visible, "false" if not + * @see PShape#setVisible(boolean) + */ + public boolean isVisible() { + return visible; + } + + + /** + * ( begin auto-generated from PShape_setVisible.xml ) + * + * Sets the shape to be visible or invisible. This is determined by the + * value of the visible parameter. + *

        + * The visibility of a shape is usually controlled by whatever program + * created the SVG file. For instance, this parameter is controlled by + * showing or hiding the shape in the layers palette in Adobe Illustrator. + * + * ( end auto-generated ) + * @webref pshape:mathod + * @usage web_application + * @brief Sets the shape to be visible or invisible + * @param visible "false" makes the shape invisible and "true" makes it visible + * @see PShape#isVisible() + */ + public void setVisible(boolean visible) { + this.visible = visible; + } + + + /** + * ( begin auto-generated from PShape_disableStyle.xml ) + * + * Disables the shape's style data and uses Processing's current styles. + * Styles include attributes such as colors, stroke weight, and stroke + * joints. + * + * ( end auto-generated ) + *

        Advanced

        + * Overrides this shape's style information and uses PGraphics styles and + * colors. Identical to ignoreStyles(true). Also disables styles for all + * child shapes. + * @webref pshape:method + * @usage web_application + * @brief Disables the shape's style data and uses Processing styles + * @see PShape#enableStyle() + */ + public void disableStyle() { + style = false; + + for (int i = 0; i < childCount; i++) { + children[i].disableStyle(); + } + } + + + /** + * ( begin auto-generated from PShape_enableStyle.xml ) + * + * Enables the shape's style data and ignores Processing's current styles. + * Styles include attributes such as colors, stroke weight, and stroke + * joints. + * + * ( end auto-generated ) + * + * @webref pshape:method + * @usage web_application + * @brief Enables the shape's style data and ignores the Processing styles + * @see PShape#disableStyle() + */ + public void enableStyle() { + style = true; + + for (int i = 0; i < childCount; i++) { + children[i].enableStyle(); + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + +// protected void checkBounds() { +// if (width == 0 || height == 0) { +// // calculate bounds here (also take kids into account) +// width = 1; +// height = 1; +// } +// } + + + /** + * Get the width of the drawing area (not necessarily the shape boundary). + */ + public float getWidth() { + //checkBounds(); + return width; + } + + + /** + * Get the height of the drawing area (not necessarily the shape boundary). + */ + public float getHeight() { + //checkBounds(); + return height; + } + + + /** + * Get the depth of the shape area (not necessarily the shape boundary). Only makes sense for 3D PShape subclasses, + * such as PShape3D. + */ + public float getDepth() { + //checkBounds(); + return depth; + } + + + + /* + // TODO unapproved + protected PVector getTop() { + return getTop(null); + } + + + protected PVector getTop(PVector top) { + if (top == null) { + top = new PVector(); + } + return top; + } + + + protected PVector getBottom() { + return getBottom(null); + } + + + protected PVector getBottom(PVector bottom) { + if (bottom == null) { + bottom = new PVector(); + } + return bottom; + } + */ + + + /** + * Return true if this shape is 2D. Defaults to true. + */ + public boolean is2D() { + return !is3D; + } + + + /** + * Return true if this shape is 3D. Defaults to false. + */ + public boolean is3D() { + return is3D; + } + + + public void set3D(boolean val) { + is3D = val; + } + + +// /** +// * Return true if this shape requires rendering through OpenGL. Defaults to false. +// */ +// // TODO unapproved +// public boolean isGL() { +// return false; +// } + + + /////////////////////////////////////////////////////////// + + // + + // Drawing methods + + public void textureMode(int mode) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "textureMode()"); + return; + } + + textureMode = mode; + } + + public void texture(PImage tex) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "texture()"); + return; + } + + image = tex; + } + + public void noTexture() { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "noTexture()"); + return; + } + + image = null; + } + + + // TODO unapproved + protected void solid(boolean solid) { + } + + + /** + * @webref shape:vertex + * @brief Starts a new contour + * @see PShape#endContour() + */ + public void beginContour() { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "beginContour()"); + return; + } + + if (family == GROUP) { + PGraphics.showWarning("Cannot begin contour in GROUP shapes"); + return; + } + + if (openContour) { + PGraphics.showWarning("Already called beginContour()."); + return; + } + openContour = true; + beginContourImpl(); + } + + + protected void beginContourImpl() { + if (vertexCodes == null) { + vertexCodes = new int[10]; + } else if (vertexCodes.length == vertexCodeCount) { + vertexCodes = PApplet.expand(vertexCodes); + } + vertexCodes[vertexCodeCount++] = BREAK; + } + + + /** + * @webref shape:vertex + * @brief Ends a contour + * @see PShape#beginContour() + */ + public void endContour() { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "endContour()"); + return; + } + + if (family == GROUP) { + PGraphics.showWarning("Cannot end contour in GROUP shapes"); + return; + } + + if (!openContour) { + PGraphics.showWarning("Need to call beginContour() first."); + return; + } + endContourImpl(); + openContour = false; + } + + + protected void endContourImpl() { + } + + + public void vertex(float x, float y) { + if (vertices == null) { + vertices = new float[10][2]; + } else if (vertices.length == vertexCount) { + vertices = (float[][]) PApplet.expand(vertices); + } + vertices[vertexCount++] = new float[] { x, y }; + + if (vertexCodes == null) { + vertexCodes = new int[10]; + } else if (vertexCodes.length == vertexCodeCount) { + vertexCodes = PApplet.expand(vertexCodes); + } + vertexCodes[vertexCodeCount++] = VERTEX; + + if (x > width) { + width = x; + } + if (y > height) { + height = y; + } + } + + + public void vertex(float x, float y, float u, float v) { + } + + + public void vertex(float x, float y, float z) { + vertex(x, y); // maybe? maybe not? + } + + + public void vertex(float x, float y, float z, float u, float v) { + } + + + public void normal(float nx, float ny, float nz) { + } + + + public void attribPosition(String name, float x, float y, float z) { + } + + public void attribNormal(String name, float nx, float ny, float nz) { + } + + + public void attribColor(String name, int color) { + } + + + public void attrib(String name, float... values) { + } + + + public void attrib(String name, int... values) { + } + + + public void attrib(String name, boolean... values) { + } + + + /** + * @webref pshape:method + * @brief Starts the creation of a new PShape + * @see PApplet#endShape() + */ + public void beginShape() { + beginShape(POLYGON); + } + + + public void beginShape(int kind) { + this.kind = kind; + openShape = true; + } + + /** + * @webref pshape:method + * @brief Finishes the creation of a new PShape + * @see PApplet#beginShape() + */ + public void endShape() { + endShape(OPEN); + } + + + public void endShape(int mode) { + if (family == GROUP) { + PGraphics.showWarning("Cannot end GROUP shape"); + return; + } + + if (!openShape) { + PGraphics.showWarning("Need to call beginShape() first"); + return; + } + + close = (mode==CLOSE); + + // this is the state of the shape + openShape = false; + } + + + ////////////////////////////////////////////////////////////// + + // STROKE CAP/JOIN/WEIGHT + + + public void strokeWeight(float weight) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "strokeWeight()"); + return; + } + + strokeWeight = weight; + } + + public void strokeJoin(int join) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "strokeJoin()"); + return; + } + + strokeJoin = join; + } + + public void strokeCap(int cap) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "strokeCap()"); + return; + } + + strokeCap = cap; + } + + + ////////////////////////////////////////////////////////////// + + // FILL COLOR + + + public void noFill() { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "noFill()"); + return; + } + + fill = false; + fillColor = 0x0; + + if (!setAmbient) { + ambientColor = fillColor; + } + } + + + public void fill(int rgb) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "fill()"); + return; + } + + fill = true; + colorCalc(rgb); + fillColor = calcColor; + + if (!setAmbient) { + ambientColor = fillColor; + } + } + + + public void fill(int rgb, float alpha) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "fill()"); + return; + } + + fill = true; + colorCalc(rgb, alpha); + fillColor = calcColor; + + if (!setAmbient) { + ambientColor = fillColor; + } + } + + + public void fill(float gray) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "fill()"); + return; + } + + fill = true; + colorCalc(gray); + fillColor = calcColor; + + if (!setAmbient) { + ambientColor = fillColor; + } + } + + + public void fill(float gray, float alpha) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "fill()"); + return; + } + + fill = true; + colorCalc(gray, alpha); + fillColor = calcColor; + + if (!setAmbient) { + ambient(fillColor); + setAmbient = false; + } + + if (!setAmbient) { + ambientColor = fillColor; + } + } + + + public void fill(float x, float y, float z) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "fill()"); + return; + } + + fill = true; + colorCalc(x, y, z); + fillColor = calcColor; + + if (!setAmbient) { + ambientColor = fillColor; + } + } + + + public void fill(float x, float y, float z, float a) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "fill()"); + return; + } + + fill = true; + colorCalc(x, y, z, a); + fillColor = calcColor; + + if (!setAmbient) { + ambientColor = fillColor; + } + } + + + ////////////////////////////////////////////////////////////// + + // STROKE COLOR + + + public void noStroke() { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "noStroke()"); + return; + } + + stroke = false; + } + + + public void stroke(int rgb) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "stroke()"); + return; + } + + stroke = true; + colorCalc(rgb); + strokeColor = calcColor; + } + + + public void stroke(int rgb, float alpha) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "stroke()"); + return; + } + + stroke = true; + colorCalc(rgb, alpha); + strokeColor = calcColor; + } + + + public void stroke(float gray) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "stroke()"); + return; + } + + stroke = true; + colorCalc(gray); + strokeColor = calcColor; + } + + + public void stroke(float gray, float alpha) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "stroke()"); + return; + } + + stroke = true; + colorCalc(gray, alpha); + strokeColor = calcColor; + } + + + public void stroke(float x, float y, float z) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "stroke()"); + return; + } + + stroke = true; + colorCalc(x, y, z); + strokeColor = calcColor; + } + + + public void stroke(float x, float y, float z, float alpha) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "stroke()"); + return; + } + + stroke = true; + colorCalc(x, y, z, alpha); + strokeColor = calcColor; + } + + + ////////////////////////////////////////////////////////////// + + // TINT COLOR + + + public void noTint() { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "noTint()"); + return; + } + + tint = false; + } + + + public void tint(int rgb) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "tint()"); + return; + } + + tint = true; + colorCalc(rgb); + tintColor = calcColor; + } + + + public void tint(int rgb, float alpha) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "tint()"); + return; + } + + tint = true; + colorCalc(rgb, alpha); + tintColor = calcColor; + } + + + public void tint(float gray) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "tint()"); + return; + } + + tint = true; + colorCalc(gray); + tintColor = calcColor; + } + + + public void tint(float gray, float alpha) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "tint()"); + return; + } + + tint = true; + colorCalc(gray, alpha); + tintColor = calcColor; + } + + + public void tint(float x, float y, float z) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "tint()"); + return; + } + + tint = true; + colorCalc(x, y, z); + tintColor = calcColor; + } + + + public void tint(float x, float y, float z, float alpha) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "tint()"); + return; + } + + tint = true; + colorCalc(x, y, z, alpha); + tintColor = calcColor; + } + + + ////////////////////////////////////////////////////////////// + + // Ambient set/update + + public void ambient(int rgb) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "ambient()"); + return; + } + + setAmbient = true; + colorCalc(rgb); + ambientColor = calcColor; + } + + + public void ambient(float gray) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "ambient()"); + return; + } + + setAmbient = true; + colorCalc(gray); + ambientColor = calcColor; + } + + + public void ambient(float x, float y, float z) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "ambient()"); + return; + } + + setAmbient = true; + colorCalc(x, y, z); + ambientColor = calcColor; + } + + + ////////////////////////////////////////////////////////////// + + // Specular set/update + + public void specular(int rgb) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "specular()"); + return; + } + + colorCalc(rgb); + specularColor = calcColor; + } + + + public void specular(float gray) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "specular()"); + return; + } + + colorCalc(gray); + specularColor = calcColor; + } + + + public void specular(float x, float y, float z) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "specular()"); + return; + } + + colorCalc(x, y, z); + specularColor = calcColor; + } + + + ////////////////////////////////////////////////////////////// + + // Emissive set/update + + public void emissive(int rgb) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "emissive()"); + return; + } + + colorCalc(rgb); + emissiveColor = calcColor; + } + + + public void emissive(float gray) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "emissive()"); + return; + } + + colorCalc(gray); + emissiveColor = calcColor; + } + + + public void emissive(float x, float y, float z) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "emissive()"); + return; + } + + colorCalc(x, y, z); + emissiveColor = calcColor; + } + + + ////////////////////////////////////////////////////////////// + + // Shininess set/update + + public void shininess(float shine) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "shininess()"); + return; + } + + shininess = shine; + } + + /////////////////////////////////////////////////////////// + + // + + // Bezier curves + + + public void bezierDetail(int detail) { + } + + + public void bezierVertex(float x2, float y2, + float x3, float y3, + float x4, float y4) { + if (vertices == null) { + vertices = new float[10][]; + } else if (vertexCount + 2 >= vertices.length) { + vertices = (float[][]) PApplet.expand(vertices); + } + vertices[vertexCount++] = new float[] { x2, y2 }; + vertices[vertexCount++] = new float[] { x3, y3 }; + vertices[vertexCount++] = new float[] { x4, y4 }; + + // vertexCodes must be allocated because a vertex() call is required + if (vertexCodes.length == vertexCodeCount) { + vertexCodes = PApplet.expand(vertexCodes); + } + vertexCodes[vertexCodeCount++] = BEZIER_VERTEX; + + if (x4 > width) { + width = x4; + } + if (y4 > height) { + height = y4; + } + } + + + public void bezierVertex(float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + } + + + public void quadraticVertex(float cx, float cy, + float x3, float y3) { + if (vertices == null) { + vertices = new float[10][]; + } else if (vertexCount + 1 >= vertices.length) { + vertices = (float[][]) PApplet.expand(vertices); + } + vertices[vertexCount++] = new float[] { cx, cy }; + vertices[vertexCount++] = new float[] { x3, y3 }; + + // vertexCodes must be allocated because a vertex() call is required + if (vertexCodes.length == vertexCodeCount) { + vertexCodes = PApplet.expand(vertexCodes); + } + vertexCodes[vertexCodeCount++] = QUADRATIC_VERTEX; + + if (x3 > width) { + width = x3; + } + if (y3 > height) { + height = y3; + } + } + + + public void quadraticVertex(float cx, float cy, float cz, + float x3, float y3, float z3) { + } + + + /////////////////////////////////////////////////////////// + + // + + // Catmull-Rom curves + + public void curveDetail(int detail) { + } + + public void curveTightness(float tightness) { + } + + public void curveVertex(float x, float y) { + } + + public void curveVertex(float x, float y, float z) { + } + + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /* + boolean strokeSaved; + int strokeColorSaved; + float strokeWeightSaved; + int strokeCapSaved; + int strokeJoinSaved; + + boolean fillSaved; + int fillColorSaved; + + int rectModeSaved; + int ellipseModeSaved; + int shapeModeSaved; + */ + + + protected void pre(PGraphics g) { + if (matrix != null) { + g.pushMatrix(); + g.applyMatrix(matrix); + } + + /* + strokeSaved = g.stroke; + strokeColorSaved = g.strokeColor; + strokeWeightSaved = g.strokeWeight; + strokeCapSaved = g.strokeCap; + strokeJoinSaved = g.strokeJoin; + + fillSaved = g.fill; + fillColorSaved = g.fillColor; + + rectModeSaved = g.rectMode; + ellipseModeSaved = g.ellipseMode; + shapeModeSaved = g.shapeMode; + */ + if (style) { + g.pushStyle(); + styles(g); + } + } + + + protected void styles(PGraphics g) { + // should not be necessary because using only the int version of color + //parent.colorMode(PConstants.RGB, 255); + + if (stroke) { + g.stroke(strokeColor); + g.strokeWeight(strokeWeight); + g.strokeCap(strokeCap); + g.strokeJoin(strokeJoin); + } else { + g.noStroke(); + } + + if (fill) { + //System.out.println("filling " + PApplet.hex(fillColor)); + g.fill(fillColor); + } else { + g.noFill(); + } + } + + + protected void post(PGraphics g) { +// for (int i = 0; i < childCount; i++) { +// children[i].draw(g); +// } + + /* + // TODO this is not sufficient, since not saving fillR et al. + g.stroke = strokeSaved; + g.strokeColor = strokeColorSaved; + g.strokeWeight = strokeWeightSaved; + g.strokeCap = strokeCapSaved; + g.strokeJoin = strokeJoinSaved; + + g.fill = fillSaved; + g.fillColor = fillColorSaved; + + g.ellipseMode = ellipseModeSaved; + */ + + if (matrix != null) { + g.popMatrix(); + } + + if (style) { + g.popStyle(); + } + } + + + //////////////////////////////////////////////////////////////////////// + // + // Shape copy + + + // TODO unapproved + static protected PShape createShape(PApplet parent, PShape src) { + PShape dest = null; + if (src.family == GROUP) { + dest = parent.createShape(GROUP); + PShape.copyGroup(parent, src, dest); + } else if (src.family == PRIMITIVE) { + dest = parent.createShape(src.kind, src.params); + PShape.copyPrimitive(src, dest); + } else if (src.family == GEOMETRY) { + dest = parent.createShape(src.kind); + PShape.copyGeometry(src, dest); + } else if (src.family == PATH) { + dest = parent.createShape(PATH); + PShape.copyPath(src, dest); + } + dest.setName(src.name); + return dest; + } + + + // TODO unapproved + static protected void copyGroup(PApplet parent, PShape src, PShape dest) { + copyMatrix(src, dest); + copyStyles(src, dest); + copyImage(src, dest); + for (int i = 0; i < src.childCount; i++) { + PShape c = PShape.createShape(parent, src.children[i]); + dest.addChild(c); + } + } + + + // TODO unapproved + static protected void copyPrimitive(PShape src, PShape dest) { + copyMatrix(src, dest); + copyStyles(src, dest); + copyImage(src, dest); + } + + + // TODO unapproved + static protected void copyGeometry(PShape src, PShape dest) { + dest.beginShape(src.getKind()); + + copyMatrix(src, dest); + copyStyles(src, dest); + copyImage(src, dest); + + if (src.style) { + for (int i = 0; i < src.vertexCount; i++) { + float[] vert = src.vertices[i]; + + dest.fill((int)(vert[PGraphics.A] * 255) << 24 | + (int)(vert[PGraphics.R] * 255) << 16 | + (int)(vert[PGraphics.G] * 255) << 8 | + (int)(vert[PGraphics.B] * 255)); + + // Do we need to copy these as well? +// dest.ambient(vert[PGraphics.AR] * 255, vert[PGraphics.AG] * 255, vert[PGraphics.AB] * 255); +// dest.specular(vert[PGraphics.SPR] * 255, vert[PGraphics.SPG] * 255, vert[PGraphics.SPB] * 255); +// dest.emissive(vert[PGraphics.ER] * 255, vert[PGraphics.EG] * 255, vert[PGraphics.EB] * 255); +// dest.shininess(vert[PGraphics.SHINE]); + + if (0 < PApplet.dist(vert[PGraphics.NX], + vert[PGraphics.NY], + vert[PGraphics.NZ], 0, 0, 0)) { + dest.normal(vert[PGraphics.NX], + vert[PGraphics.NY], + vert[PGraphics.NZ]); + } + dest.vertex(vert[X], vert[Y], vert[Z], + vert[PGraphics.U], + vert[PGraphics.V]); + } + } else { + for (int i = 0; i < src.vertexCount; i++) { + float[] vert = src.vertices[i]; + if (vert[Z] == 0) { + dest.vertex(vert[X], vert[Y]); + } else { + dest.vertex(vert[X], vert[Y], vert[Z]); + } + } + } + + dest.endShape(); + } + + + // TODO unapproved + static protected void copyPath(PShape src, PShape dest) { + copyMatrix(src, dest); + copyStyles(src, dest); + copyImage(src, dest); + dest.close = src.close; + dest.setPath(src.vertexCount, src.vertices, src.vertexCodeCount, src.vertexCodes); + } + + + // TODO unapproved + static protected void copyMatrix(PShape src, PShape dest) { + if (src.matrix != null) { + dest.applyMatrix(src.matrix); + } + } + + + // TODO unapproved + static protected void copyStyles(PShape src, PShape dest) { + dest.ellipseMode = src.ellipseMode; + dest.rectMode = src.rectMode; + + if (src.stroke) { + dest.stroke = true; + dest.strokeColor = src.strokeColor; + dest.strokeWeight = src.strokeWeight; + dest.strokeCap = src.strokeCap; + dest.strokeJoin = src.strokeJoin; + } else { + dest.stroke = false; + } + + if (src.fill) { + dest.fill = true; + dest.fillColor = src.fillColor; + } else { + dest.fill = false; + } + } + + + // TODO unapproved + static protected void copyImage(PShape src, PShape dest) { + if (src.image != null) { + dest.texture(src.image); + } + } + + + + //////////////////////////////////////////////////////////////////////// + + + /** + * Called by the following (the shape() command adds the g) + * PShape s = loadShape("blah.svg"); + * shape(s); + */ + public void draw(PGraphics g) { + if (visible) { + pre(g); + drawImpl(g); + post(g); + } + } + + + /** + * Draws the SVG document. + */ + protected void drawImpl(PGraphics g) { + if (family == GROUP) { + drawGroup(g); + } else if (family == PRIMITIVE) { + drawPrimitive(g); + } else if (family == GEOMETRY) { + // Not same as path: `kind` matters. +// drawPath(g); + drawGeometry(g); + } else if (family == PATH) { + drawPath(g); + } + } + + + protected void drawGroup(PGraphics g) { + for (int i = 0; i < childCount; i++) { + children[i].draw(g); + } + } + + + protected void drawPrimitive(PGraphics g) { + if (kind == POINT) { + g.point(params[0], params[1]); + + } else if (kind == LINE) { + if (params.length == 4) { // 2D + g.line(params[0], params[1], + params[2], params[3]); + } else { // 3D + g.line(params[0], params[1], params[2], + params[3], params[4], params[5]); + } + + } else if (kind == TRIANGLE) { + g.triangle(params[0], params[1], + params[2], params[3], + params[4], params[5]); + + } else if (kind == QUAD) { + g.quad(params[0], params[1], + params[2], params[3], + params[4], params[5], + params[6], params[7]); + + } else if (kind == RECT) { + if (image != null) { + int oldMode = g.imageMode; + g.imageMode(CORNER); + g.image(image, params[0], params[1], params[2], params[3]); + g.imageMode(oldMode); + } else { + int oldMode = g.rectMode; + g.rectMode(rectMode); + if (params.length == 4) { + g.rect(params[0], params[1], + params[2], params[3]); + } else if (params.length == 5) { + g.rect(params[0], params[1], + params[2], params[3], + params[4]); + } else if (params.length == 8) { + g.rect(params[0], params[1], + params[2], params[3], + params[4], params[5], + params[6], params[7]); + } + g.rectMode(oldMode); + } + } else if (kind == ELLIPSE) { + int oldMode = g.ellipseMode; + g.ellipseMode(ellipseMode); + g.ellipse(params[0], params[1], + params[2], params[3]); + g.ellipseMode(oldMode); + + } else if (kind == ARC) { + int oldMode = g.ellipseMode; + g.ellipseMode(ellipseMode); + if (params.length == 6) { + g.arc(params[0], params[1], + params[2], params[3], + params[4], params[5]); + } else if (params.length == 7) { + g.arc(params[0], params[1], + params[2], params[3], + params[4], params[5], + (int) params[6]); + } + g.ellipseMode(oldMode); + + } else if (kind == BOX) { + if (params.length == 1) { + g.box(params[0]); + } else { + g.box(params[0], params[1], params[2]); + } + + } else if (kind == SPHERE) { + g.sphere(params[0]); + } + } + + + protected void drawGeometry(PGraphics g) { + // get cache object using g. + g.beginShape(kind); + if (style) { + for (int i = 0; i < vertexCount; i++) { + g.vertex(vertices[i]); + } + } else { + for (int i = 0; i < vertexCount; i++) { + float[] vert = vertices[i]; + if (vert[Z] == 0) { + g.vertex(vert[X], vert[Y]); + } else { + g.vertex(vert[X], vert[Y], vert[Z]); + } + } + } + g.endShape(close ? CLOSE : OPEN); + } + + + /* + protected void drawPath(PGraphics g) { + g.beginShape(); + for (int j = 0; j < childCount; j++) { + if (j > 0) g.breakShape(); + int count = children[j].vertexCount; + float[][] vert = children[j].vertices; + int[] code = children[j].vertexCodes; + + for (int i = 0; i < count; i++) { + if (style) { + if (children[j].fill) { + g.fill(vert[i][R], vert[i][G], vert[i][B]); + } else { + g.noFill(); + } + if (children[j].stroke) { + g.stroke(vert[i][R], vert[i][G], vert[i][B]); + } else { + g.noStroke(); + } + } + g.edge(vert[i][EDGE] == 1); + + if (code[i] == VERTEX) { + g.vertex(vert[i]); + + } else if (code[i] == BEZIER_VERTEX) { + float z0 = vert[i+0][Z]; + float z1 = vert[i+1][Z]; + float z2 = vert[i+2][Z]; + if (z0 == 0 && z1 == 0 && z2 == 0) { + g.bezierVertex(vert[i+0][X], vert[i+0][Y], z0, + vert[i+1][X], vert[i+1][Y], z1, + vert[i+2][X], vert[i+2][Y], z2); + } else { + g.bezierVertex(vert[i+0][X], vert[i+0][Y], + vert[i+1][X], vert[i+1][Y], + vert[i+2][X], vert[i+2][Y]); + } + } else if (code[i] == CURVE_VERTEX) { + float z = vert[i][Z]; + if (z == 0) { + g.curveVertex(vert[i][X], vert[i][Y]); + } else { + g.curveVertex(vert[i][X], vert[i][Y], z); + } + } + } + } + g.endShape(); + } + */ + + protected void drawPath(PGraphics g) { + // Paths might be empty (go figure) + // http://dev.processing.org/bugs/show_bug.cgi?id=982 + if (vertices == null) return; + + boolean insideContour = false; + g.beginShape(); + + if (vertexCodeCount == 0) { // each point is a simple vertex + if (vertices[0].length == 2) { // drawing 2D vertices + for (int i = 0; i < vertexCount; i++) { + g.vertex(vertices[i][X], vertices[i][Y]); + } + } else { // drawing 3D vertices + for (int i = 0; i < vertexCount; i++) { + g.vertex(vertices[i][X], vertices[i][Y], vertices[i][Z]); + } + } + + } else { // coded set of vertices + int index = 0; + + if (vertices[0].length == 2) { // drawing a 2D path + for (int j = 0; j < vertexCodeCount; j++) { + switch (vertexCodes[j]) { + + case VERTEX: + g.vertex(vertices[index][X], vertices[index][Y]); + index++; + break; + + case QUADRATIC_VERTEX: + g.quadraticVertex(vertices[index+0][X], vertices[index+0][Y], + vertices[index+1][X], vertices[index+1][Y]); + index += 2; + break; + + case BEZIER_VERTEX: + g.bezierVertex(vertices[index+0][X], vertices[index+0][Y], + vertices[index+1][X], vertices[index+1][Y], + vertices[index+2][X], vertices[index+2][Y]); + index += 3; + break; + + case CURVE_VERTEX: + g.curveVertex(vertices[index][X], vertices[index][Y]); + index++; + break; + + case BREAK: + if (insideContour) { + g.endContour(); + } + g.beginContour(); + insideContour = true; + } + } + } else { // drawing a 3D path + for (int j = 0; j < vertexCodeCount; j++) { + switch (vertexCodes[j]) { + + case VERTEX: + g.vertex(vertices[index][X], vertices[index][Y], vertices[index][Z]); + index++; + break; + + case QUADRATIC_VERTEX: + g.quadraticVertex(vertices[index+0][X], vertices[index+0][Y], vertices[index+0][Z], + vertices[index+1][X], vertices[index+1][Y], vertices[index+0][Z]); + index += 2; + break; + + + case BEZIER_VERTEX: + g.bezierVertex(vertices[index+0][X], vertices[index+0][Y], vertices[index+0][Z], + vertices[index+1][X], vertices[index+1][Y], vertices[index+1][Z], + vertices[index+2][X], vertices[index+2][Y], vertices[index+2][Z]); + index += 3; + break; + + case CURVE_VERTEX: + g.curveVertex(vertices[index][X], vertices[index][Y], vertices[index][Z]); + index++; + break; + + case BREAK: + if (insideContour) { + g.endContour(); + } + g.beginContour(); + insideContour = true; + } + } + } + } + if (insideContour) { + g.endContour(); + } + g.endShape(close ? CLOSE : OPEN); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public PShape getParent() { + return parent; + } + + /** + * @webref + * @brief Returns the number of children + */ + public int getChildCount() { + return childCount; + } + + + /** Resize the children[] array to be in line with childCount */ + protected void crop() { + // https://github.com/processing/processing/issues/3347 + if (children.length != childCount) { + children = (PShape[]) PApplet.subset(children, 0, childCount); + } + } + + + public PShape[] getChildren() { + crop(); + return children; + } + + /** + * ( begin auto-generated from PShape_getChild.xml ) + * + * Extracts a child shape from a parent shape. Specify the name of the + * shape with the target parameter. The shape is returned as a + * PShape object, or null is returned if there is an error. + * + * ( end auto-generated ) + * @webref pshape:method + * @usage web_application + * @brief Returns a child element of a shape as a PShape object + * @param index the layer position of the shape to get + * @see PShape#addChild(PShape) + */ + public PShape getChild(int index) { + crop(); + return children[index]; + } + + /** + * @param target the name of the shape to get + */ + public PShape getChild(String target) { + if (name != null && name.equals(target)) { + return this; + } + if (nameTable != null) { + PShape found = nameTable.get(target); + if (found != null) return found; + } + for (int i = 0; i < childCount; i++) { + PShape found = children[i].getChild(target); + if (found != null) return found; + } + return null; + } + + + /** + * Same as getChild(name), except that it first walks all the way up the + * hierarchy to the eldest grandparent, so that children can be found anywhere. + */ + public PShape findChild(String target) { + if (parent == null) { + return getChild(target); + + } else { + return parent.findChild(target); + } + } + + + // can't be just 'add' because that suggests additive geometry + /** + * @webref pshape:method + * @brief Adds a new child + * @param who any variable of type PShape + * @see PShape#getChild(int) + */ + public void addChild(PShape who) { + if (children == null) { + children = new PShape[1]; + } + if (childCount == children.length) { + children = (PShape[]) PApplet.expand(children); + } + children[childCount++] = who; + who.parent = this; + + if (who.getName() != null) { + addName(who.getName(), who); + } + } + + + // adds child who exactly at position idx in the array of children. + /** + * @param idx the layer position in which to insert the new child + */ + public void addChild(PShape who, int idx) { + if (idx < childCount) { + if (childCount == children.length) { + children = (PShape[]) PApplet.expand(children); + } + + // Copy [idx, childCount - 1] to [idx + 1, childCount] + for (int i = childCount - 1; i >= idx; i--) { + children[i + 1] = children[i]; + } + childCount++; + + children[idx] = who; + + who.parent = this; + + if (who.getName() != null) { + addName(who.getName(), who); + } + } + } + + + /** + * Remove the child shape with index idx. + */ + public void removeChild(int idx) { + if (idx < childCount) { + PShape child = children[idx]; + + // Copy [idx + 1, childCount - 1] to [idx, childCount - 2] + for (int i = idx; i < childCount - 1; i++) { + children[i] = children[i + 1]; + } + childCount--; + + if (child.getName() != null && nameTable != null) { + nameTable.remove(child.getName()); + } + } + } + + + /** + * Add a shape to the name lookup table. + */ + public void addName(String nom, PShape shape) { + if (parent != null) { + parent.addName(nom, shape); + } else { + if (nameTable == null) { + nameTable = new HashMap(); + } + nameTable.put(nom, shape); + } + } + + + /** + * Returns the index of child who. + */ + public int getChildIndex(PShape who) { + for (int i = 0; i < childCount; i++) { + if (children[i] == who) { + return i; + } + } + return -1; + } + + + public PShape getTessellation() { + return null; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** The shape type, one of GROUP, PRIMITIVE, PATH, or GEOMETRY. */ + public int getFamily() { + return family; + } + + + public int getKind() { + return kind; + } + + + public float[] getParams() { + return getParams(null); + } + + + public float[] getParams(float[] target) { + if (target == null || target.length != params.length) { + target = new float[params.length]; + } + PApplet.arrayCopy(params, target); + return target; + } + + + public float getParam(int index) { + return params[index]; + } + + + protected void setParams(float[] source) { + if (params == null) { + params = new float[source.length]; + } + if (source.length != params.length) { + PGraphics.showWarning("Wrong number of parameters"); + return; + } + PApplet.arrayCopy(source, params); + } + + + public void setPath(int vcount, float[][] verts) { + setPath(vcount, verts, 0, null); + } + + + protected void setPath(int vcount, float[][] verts, int ccount, int[] codes) { + if (verts == null || verts.length < vcount) return; + if (0 < ccount && (codes == null || codes.length < ccount)) return; + + int ndim = verts[0].length; + vertexCount = vcount; + vertices = new float[vertexCount][ndim]; + for (int i = 0; i < vertexCount; i++) { + PApplet.arrayCopy(verts[i], vertices[i]); + } + + vertexCodeCount = ccount; + if (0 < vertexCodeCount) { + vertexCodes = new int[vertexCodeCount]; + PApplet.arrayCopy(codes, vertexCodes, vertexCodeCount); + } + } + + /** + * @webref pshape:method + * @brief Returns the total number of vertices as an int + * @see PShape#getVertex(int) + * @see PShape#setVertex(int, float, float) + */ + public int getVertexCount() { + if (family == GROUP || family == PRIMITIVE) { + PGraphics.showWarning(NO_VERTICES_ERROR); + } + return vertexCount; + } + + + /** + * @webref pshape:method + * @brief Returns the vertex at the index position + * @param index the location of the vertex + * @see PShape#setVertex(int, float, float) + * @see PShape#getVertexCount() + */ + public PVector getVertex(int index) { + return getVertex(index, null); + } + + + /** + * @param vec PVector to assign the data to + */ + public PVector getVertex(int index, PVector vec) { + if (vec == null) { + vec = new PVector(); + } + float[] vert = vertices[index]; + vec.x = vert[X]; + vec.y = vert[Y]; + if (vert.length > 2) { + vec.z = vert[Z]; + } else { + vec.z = 0; // in case this isn't a new vector + } + return vec; + } + + + public float getVertexX(int index) { + return vertices[index][X]; + } + + + public float getVertexY(int index) { + return vertices[index][Y]; + } + + + public float getVertexZ(int index) { + return vertices[index][Z]; + } + + + /** + * @webref pshape:method + * @brief Sets the vertex at the index position + * @param index the location of the vertex + * @param x the x value for the vertex + * @param y the y value for the vertex + * @see PShape#getVertex(int) + * @see PShape#getVertexCount() + */ + public void setVertex(int index, float x, float y) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setVertex()"); + return; + } + + vertices[index][X] = x; + vertices[index][Y] = y; + } + + + /** + * @param z the z value for the vertex + */ + public void setVertex(int index, float x, float y, float z) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setVertex()"); + return; + } + + vertices[index][X] = x; + vertices[index][Y] = y; + vertices[index][Z] = z; + } + + + /** + * @param vec the PVector to define the x, y, z coordinates + */ + public void setVertex(int index, PVector vec) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setVertex()"); + return; + } + + vertices[index][X] = vec.x; + vertices[index][Y] = vec.y; + + if (vertices[index].length > 2) { + vertices[index][Z] = vec.z; + } else if (vec.z != 0 && vec.z == vec.z) { + throw new IllegalArgumentException("Cannot set a z-coordinate on a 2D shape"); + } + } + + + public PVector getNormal(int index) { + return getNormal(index, null); + } + + + public PVector getNormal(int index, PVector vec) { + if (vec == null) { + vec = new PVector(); + } + vec.x = vertices[index][PGraphics.NX]; + vec.y = vertices[index][PGraphics.NY]; + vec.z = vertices[index][PGraphics.NZ]; + return vec; + } + + + public float getNormalX(int index) { + return vertices[index][PGraphics.NX]; + } + + + public float getNormalY(int index) { + return vertices[index][PGraphics.NY]; + } + + + public float getNormalZ(int index) { + return vertices[index][PGraphics.NZ]; + } + + + public void setNormal(int index, float nx, float ny, float nz) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setNormal()"); + return; + } + + vertices[index][PGraphics.NX] = nx; + vertices[index][PGraphics.NY] = ny; + vertices[index][PGraphics.NZ] = nz; + } + + + + public void setAttrib(String name, int index, float... values) { + } + + + public void setAttrib(String name, int index, int... values) { + } + + + public void setAttrib(String name, int index, boolean... values) { + } + + + public float getTextureU(int index) { + return vertices[index][PGraphics.U]; + } + + + public float getTextureV(int index) { + return vertices[index][PGraphics.V]; + } + + + public void setTextureUV(int index, float u, float v) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setTextureUV()"); + return; + } + + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || + index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "setTextureUV()"); + return; + } + + + vertices[index][PGraphics.U] = u; + vertices[index][PGraphics.V] = v; + } + + + public void setTextureMode(int mode) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setTextureMode()"); + return; + } + + textureMode = mode; + } + + + public void setTexture(PImage tex) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setTexture()"); + return; + } + + image = tex; + } + + + public int getFill(int index) { + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || + index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "getFill()"); + return fillColor; + } + + if (image == null) { + int a = (int) (vertices[index][PGraphics.A] * 255); + int r = (int) (vertices[index][PGraphics.R] * 255); + int g = (int) (vertices[index][PGraphics.G] * 255); + int b = (int) (vertices[index][PGraphics.B] * 255); + return (a << 24) | (r << 16) | (g << 8) | b; + } else { + return 0; + } + } + + /** + * @nowebref + */ + public void setFill(boolean fill) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setFill()"); + return; + } + + this.fill = fill; + } + + /** + * ( begin auto-generated from PShape_setFill.xml ) + * + * The setFill() method defines the fill color of a PShape. + * This method is used after shapes are created or when a shape is defined explicitly + * (e.g. createShape(RECT, 20, 20, 80, 80)) as shown in the above example. + * When a shape is created with beginShape() and endShape(), its + * attributes may be changed with fill() and stroke() within + * beginShape() and endShape(). However, after the shape is + * created, only the setFill() method can define a new fill value for + * the PShape. + * + * ( end auto-generated ) + * + * @webref + * @param fill + * @brief Set the fill value + */ + public void setFill(int fill) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setFill()"); + return; + } + + this.fillColor = fill; + + if (vertices != null && perVertexStyles) { + for (int i = 0; i < vertexCount; i++) { + setFill(i, fill); + } + } + } + + /** + * @nowebref + */ + public void setFill(int index, int fill) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setFill()"); + return; + } + + if (!perVertexStyles) { + PGraphics.showWarning(PER_VERTEX_UNSUPPORTED, "setFill()"); + return; + } + + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "getFill()"); + return; + } + + if (image == null) { + vertices[index][PGraphics.A] = ((fill >> 24) & 0xFF) / 255.0f; + vertices[index][PGraphics.R] = ((fill >> 16) & 0xFF) / 255.0f; + vertices[index][PGraphics.G] = ((fill >> 8) & 0xFF) / 255.0f; + vertices[index][PGraphics.B] = ((fill >> 0) & 0xFF) / 255.0f; + } + } + + + public int getTint(int index) { + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "getTint()"); + return this.tintColor; + } + + if (image != null) { + int a = (int) (vertices[index][PGraphics.A] * 255); + int r = (int) (vertices[index][PGraphics.R] * 255); + int g = (int) (vertices[index][PGraphics.G] * 255); + int b = (int) (vertices[index][PGraphics.B] * 255); + return (a << 24) | (r << 16) | (g << 8) | b; + } else { + return 0; + } + } + + + public void setTint(boolean tint) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setTint()"); + return; + } + + this.tint = tint; + } + + + public void setTint(int fill) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setTint()"); + return; + } + + tintColor = fill; + + if (vertices != null) { + for (int i = 0; i < vertices.length; i++) { + setFill(i, fill); + } + } + } + + + public void setTint(int index, int tint) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setTint()"); + return; + } + + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || + index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "setTint()"); + return; + } + + if (image != null) { + vertices[index][PGraphics.A] = ((tint >> 24) & 0xFF) / 255.0f; + vertices[index][PGraphics.R] = ((tint >> 16) & 0xFF) / 255.0f; + vertices[index][PGraphics.G] = ((tint >> 8) & 0xFF) / 255.0f; + vertices[index][PGraphics.B] = ((tint >> 0) & 0xFF) / 255.0f; + } + } + + + public int getStroke(int index) { + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || + index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "getStroke()"); + return strokeColor; + } + + int a = (int) (vertices[index][PGraphics.SA] * 255); + int r = (int) (vertices[index][PGraphics.SR] * 255); + int g = (int) (vertices[index][PGraphics.SG] * 255); + int b = (int) (vertices[index][PGraphics.SB] * 255); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + /** + * @nowebref + */ + public void setStroke(boolean stroke) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStroke()"); + return; + } + + this.stroke = stroke; + } + + /** + * ( begin auto-generated from PShape_setStroke.xml ) + * + * The setStroke() method defines the outline color of a PShape. + * This method is used after shapes are created or when a shape is defined + * explicitly (e.g. createShape(RECT, 20, 20, 80, 80)) as shown in + * the above example. When a shape is created with beginShape() and + * endShape(), its attributes may be changed with fill() and + * stroke() within beginShape() and endShape(). + * However, after the shape is created, only the setStroke() method + * can define a new stroke value for the PShape. + * + * ( end auto-generated ) + * + * @webref + * @param stroke + * @brief Set the stroke value + */ + public void setStroke(int stroke) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStroke()"); + return; + } + + strokeColor = stroke; + + if (vertices != null && perVertexStyles) { + for (int i = 0; i < vertices.length; i++) { + setStroke(i, stroke); + } + } + } + + /** + * @nowebref + */ + public void setStroke(int index, int stroke) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStroke()"); + return; + } + + if (!perVertexStyles) { + PGraphics.showWarning(PER_VERTEX_UNSUPPORTED, "setStroke()"); + return; + } + + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "setStroke()"); + return; + } + + vertices[index][PGraphics.SA] = ((stroke >> 24) & 0xFF) / 255.0f; + vertices[index][PGraphics.SR] = ((stroke >> 16) & 0xFF) / 255.0f; + vertices[index][PGraphics.SG] = ((stroke >> 8) & 0xFF) / 255.0f; + vertices[index][PGraphics.SB] = ((stroke >> 0) & 0xFF) / 255.0f; + } + + + public float getStrokeWeight(int index) { + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "getStrokeWeight()"); + return strokeWeight; + } + + return vertices[index][PGraphics.SW]; + } + + + public void setStrokeWeight(float weight) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStrokeWeight()"); + return; + } + + strokeWeight = weight; + + if (vertices != null && perVertexStyles) { + for (int i = 0; i < vertexCount; i++) { + setStrokeWeight(i, weight); + } + } + } + + + public void setStrokeWeight(int index, float weight) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStrokeWeight()"); + return; + } + + if (!perVertexStyles) { + PGraphics.showWarning(PER_VERTEX_UNSUPPORTED, "setStrokeWeight()"); + return; + } + + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "setStrokeWeight()"); + return; + } + + vertices[index][PGraphics.SW] = weight; + } + + + public void setStrokeJoin(int join) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStrokeJoin()"); + return; + } + + strokeJoin = join; + } + + + public void setStrokeCap(int cap) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStrokeCap()"); + return; + } + + strokeCap = cap; + } + + + public int getAmbient(int index) { + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "getAmbient()"); + return ambientColor; + } + + int r = (int) (vertices[index][PGraphics.AR] * 255); + int g = (int) (vertices[index][PGraphics.AG] * 255); + int b = (int) (vertices[index][PGraphics.AB] * 255); + return 0xff000000 | (r << 16) | (g << 8) | b; + } + + + public void setAmbient(int ambient) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setAmbient()"); + return; + } + + ambientColor = ambient; + + if (vertices != null) { + for (int i = 0; i < vertices.length; i++) { + setAmbient(i, ambient); + } + } + } + + + public void setAmbient(int index, int ambient) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setAmbient()"); + return; + } + + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "setAmbient()"); + return; + } + + vertices[index][PGraphics.AR] = ((ambient >> 16) & 0xFF) / 255.0f; + vertices[index][PGraphics.AG] = ((ambient >> 8) & 0xFF) / 255.0f; + vertices[index][PGraphics.AB] = ((ambient >> 0) & 0xFF) / 255.0f; + } + + + public int getSpecular(int index) { + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "getSpecular()"); + return specularColor; + } + + int r = (int) (vertices[index][PGraphics.SPR] * 255); + int g = (int) (vertices[index][PGraphics.SPG] * 255); + int b = (int) (vertices[index][PGraphics.SPB] * 255); + return 0xff000000 | (r << 16) | (g << 8) | b; + } + + + public void setSpecular(int specular) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setSpecular()"); + return; + } + + specularColor = specular; + + if (vertices != null) { + for (int i = 0; i < vertices.length; i++) { + setSpecular(i, specular); + } + } + } + + + public void setSpecular(int index, int specular) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setSpecular()"); + return; + } + + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "setSpecular()"); + return; + } + + vertices[index][PGraphics.SPR] = ((specular >> 16) & 0xFF) / 255.0f; + vertices[index][PGraphics.SPG] = ((specular >> 8) & 0xFF) / 255.0f; + vertices[index][PGraphics.SPB] = ((specular >> 0) & 0xFF) / 255.0f; + } + + + public int getEmissive(int index) { + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "getEmissive()"); + return emissiveColor; + } + + int r = (int) (vertices[index][PGraphics.ER] * 255); + int g = (int) (vertices[index][PGraphics.EG] * 255); + int b = (int) (vertices[index][PGraphics.EB] * 255); + return 0xff000000 | (r << 16) | (g << 8) | b; + } + + + public void setEmissive(int emissive) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setEmissive()"); + return; + } + + emissiveColor = emissive; + + if (vertices != null) { + for (int i = 0; i < vertices.length; i++) { + setEmissive(i, emissive); + } + } + } + + + public void setEmissive(int index, int emissive) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setEmissive()"); + return; + } + + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || + index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "setEmissive()"); + return; + } + + vertices[index][PGraphics.ER] = ((emissive >> 16) & 0xFF) / 255.0f; + vertices[index][PGraphics.EG] = ((emissive >> 8) & 0xFF) / 255.0f; + vertices[index][PGraphics.EB] = ((emissive >> 0) & 0xFF) / 255.0f; + } + + + public float getShininess(int index) { + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || + index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "getShininess()"); + return shininess; + } + + return vertices[index][PGraphics.SHINE]; + } + + + public void setShininess(float shine) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setShininess()"); + return; + } + + shininess = shine; + + if (vertices != null) { + for (int i = 0; i < vertices.length; i++) { + setShininess(i, shine); + } + } + } + + + public void setShininess(int index, float shine) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setShininess()"); + return; + } + + // make sure we allocated the vertices array and that vertex exists + if (vertices == null || + index >= vertices.length) { + PGraphics.showWarning(NO_SUCH_VERTEX_ERROR + " (" + index + ")", "setShininess()"); + return; + } + + + vertices[index][PGraphics.SHINE] = shine; + } + + + public int[] getVertexCodes() { + if (vertexCodes == null) { + return null; + } + if (vertexCodes.length != vertexCodeCount) { + vertexCodes = PApplet.subset(vertexCodes, 0, vertexCodeCount); + } + return vertexCodes; + } + + + public int getVertexCodeCount() { + return vertexCodeCount; + } + + + /** + * One of VERTEX, BEZIER_VERTEX, CURVE_VERTEX, or BREAK. + */ + public int getVertexCode(int index) { + return vertexCodes[index]; + } + + + public boolean isClosed() { + return close; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html + public boolean contains(float x, float y) { + if (family == PATH) { + boolean c = false; + for (int i = 0, j = vertexCount-1; i < vertexCount; j = i++) { + if (((vertices[i][Y] > y) != (vertices[j][Y] > y)) && + (x < + (vertices[j][X]-vertices[i][X]) * + (y-vertices[i][Y]) / + (vertices[j][1]-vertices[i][Y]) + + vertices[i][X])) { + c = !c; + } + } + return c; + } else { + throw new IllegalArgumentException("The contains() method is only implemented for paths."); + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + // translate, rotate, scale, apply (no push/pop) + // these each call matrix.translate, etc + // if matrix is null when one is called, + // it is created and set to identity + + +/** + * ( begin auto-generated from PShape_translate.xml ) + * + * Specifies an amount to displace the shape. The x parameter + * specifies left/right translation, the y parameter specifies + * up/down translation, and the z parameter specifies translations + * toward/away from the screen. Subsequent calls to the method accumulates + * the effect. For example, calling translate(50, 0) and then + * translate(20, 0) is the same as translate(70, 0). This + * transformation is applied directly to the shape, it's not refreshed each + * time draw() is run. + *

        + * Using this method with the z parameter requires using the P3D + * parameter in combination with size. + * + * ( end auto-generated ) + * @webref pshape:method + * @usage web_application + * @brief Displaces the shape + * @param x left/right translation + * @param y up/down translation + * @see PShape#rotate(float) + * @see PShape#scale(float) + * @see PShape#resetMatrix() + */ + public void translate(float x, float y) { + checkMatrix(2); + matrix.translate(x, y); + } + + /** + * @param z forward/back translation + */ + public void translate(float x, float y, float z) { + checkMatrix(3); + matrix.translate(x, y, z); + } + + /** + * ( begin auto-generated from PShape_rotateX.xml ) + * + * Rotates a shape around the x-axis the amount specified by the + * angle parameter. Angles should be specified in radians (values + * from 0 to TWO_PI) or converted to radians with the radians() method. + *

        + * Shapes are always rotated around the upper-left corner of their bounding + * box. Positive numbers rotate objects in a clockwise direction. + * Subsequent calls to the method accumulates the effect. For example, + * calling rotateX(HALF_PI) and then rotateX(HALF_PI) is the + * same as rotateX(PI). This transformation is applied directly to + * the shape, it's not refreshed each time draw() is run. + *

        + * This method requires a 3D renderer. You need to use P3D as a third + * parameter for the size() function as shown in the example above. + * + * ( end auto-generated ) + * @webref pshape:method + * @usage web_application + * @brief Rotates the shape around the x-axis + * @param angle angle of rotation specified in radians + * @see PShape#rotate(float) + * @see PShape#rotateY(float) + * @see PShape#rotateZ(float) + * @see PShape#scale(float) + * @see PShape#translate(float, float) + * @see PShape#resetMatrix() + */ + public void rotateX(float angle) { + rotate(angle, 1, 0, 0); + } + + /** + * ( begin auto-generated from PShape_rotateY.xml ) + * + * Rotates a shape around the y-axis the amount specified by the + * angle parameter. Angles should be specified in radians (values + * from 0 to TWO_PI) or converted to radians with the radians() method. + *

        + * Shapes are always rotated around the upper-left corner of their bounding + * box. Positive numbers rotate objects in a clockwise direction. + * Subsequent calls to the method accumulates the effect. For example, + * calling rotateY(HALF_PI) and then rotateY(HALF_PI) is the + * same as rotateY(PI). This transformation is applied directly to + * the shape, it's not refreshed each time draw() is run. + *

        + * This method requires a 3D renderer. You need to use P3D as a third + * parameter for the size() function as shown in the example above. + * + * ( end auto-generated ) + * + * @webref pshape:method + * @usage web_application + * @brief Rotates the shape around the y-axis + * @param angle angle of rotation specified in radians + * @see PShape#rotate(float) + * @see PShape#rotateX(float) + * @see PShape#rotateZ(float) + * @see PShape#scale(float) + * @see PShape#translate(float, float) + * @see PShape#resetMatrix() + */ + public void rotateY(float angle) { + rotate(angle, 0, 1, 0); + } + + + /** + * ( begin auto-generated from PShape_rotateZ.xml ) + * + * Rotates a shape around the z-axis the amount specified by the + * angle parameter. Angles should be specified in radians (values + * from 0 to TWO_PI) or converted to radians with the radians() method. + *

        + * Shapes are always rotated around the upper-left corner of their bounding + * box. Positive numbers rotate objects in a clockwise direction. + * Subsequent calls to the method accumulates the effect. For example, + * calling rotateZ(HALF_PI) and then rotateZ(HALF_PI) is the + * same as rotateZ(PI). This transformation is applied directly to + * the shape, it's not refreshed each time draw() is run. + *

        + * This method requires a 3D renderer. You need to use P3D as a third + * parameter for the size() function as shown in the example above. + * + * ( end auto-generated ) + * @webref pshape:method + * @usage web_application + * @brief Rotates the shape around the z-axis + * @param angle angle of rotation specified in radians + * @see PShape#rotate(float) + * @see PShape#rotateX(float) + * @see PShape#rotateY(float) + * @see PShape#scale(float) + * @see PShape#translate(float, float) + * @see PShape#resetMatrix() + */ + public void rotateZ(float angle) { + rotate(angle, 0, 0, 1); + } + + /** + * ( begin auto-generated from PShape_rotate.xml ) + * + * Rotates a shape the amount specified by the angle parameter. + * Angles should be specified in radians (values from 0 to TWO_PI) or + * converted to radians with the radians() method. + *

        + * Shapes are always rotated around the upper-left corner of their bounding + * box. Positive numbers rotate objects in a clockwise direction. + * Transformations apply to everything that happens after and subsequent + * calls to the method accumulates the effect. For example, calling + * rotate(HALF_PI) and then rotate(HALF_PI) is the same as + * rotate(PI). This transformation is applied directly to the shape, + * it's not refreshed each time draw() is run. + * + * ( end auto-generated ) + * @webref pshape:method + * @usage web_application + * @brief Rotates the shape + * @param angle angle of rotation specified in radians + * @see PShape#rotateX(float) + * @see PShape#rotateY(float) + * @see PShape#rotateZ(float) + * @see PShape#scale(float) + * @see PShape#translate(float, float) + * @see PShape#resetMatrix() + */ + public void rotate(float angle) { + checkMatrix(2); // at least 2... + matrix.rotate(angle); + } + +/** + * @nowebref + */ + public void rotate(float angle, float v0, float v1, float v2) { + checkMatrix(3); + float norm2 = v0 * v0 + v1 * v1 + v2 * v2; + if (Math.abs(norm2 - 1) > EPSILON) { + // The rotation vector is not normalized. + float norm = PApplet.sqrt(norm2); + v0 /= norm; + v1 /= norm; + v2 /= norm; + } + matrix.rotate(angle, v0, v1, v2); + } + + + // + + /** + * ( begin auto-generated from PShape_scale.xml ) + * + * Increases or decreases the size of a shape by expanding and contracting + * vertices. Shapes always scale from the relative origin of their bounding + * box. Scale values are specified as decimal percentages. For example, the + * method call scale(2.0) increases the dimension of a shape by + * 200%. Subsequent calls to the method multiply the effect. For example, + * calling scale(2.0) and then scale(1.5) is the same as + * scale(3.0). This transformation is applied directly to the shape, + * it's not refreshed each time draw() is run. + *

        + * Using this method with the z parameter requires using the P3D + * parameter in combination with size. + * + * ( end auto-generated ) + * @webref pshape:method + * @usage web_application + * @brief Increases and decreases the size of a shape + * @param s percentate to scale the object + * @see PShape#rotate(float) + * @see PShape#translate(float, float) + * @see PShape#resetMatrix() + */ + public void scale(float s) { + checkMatrix(2); // at least 2... + matrix.scale(s); + } + + + public void scale(float x, float y) { + checkMatrix(2); + matrix.scale(x, y); + } + +/** + * @param x percentage to scale the object in the x-axis + * @param y percentage to scale the object in the y-axis + * @param z percentage to scale the object in the z-axis + */ + public void scale(float x, float y, float z) { + checkMatrix(3); + matrix.scale(x, y, z); + } + + + // + +/** + * ( begin auto-generated from PShape_resetMatrix.xml ) + * + * Replaces the current matrix of a shape with the identity matrix. The + * equivalent function in OpenGL is glLoadIdentity(). + * + * ( end auto-generated ) + * @webref pshape:method + * @brief Replaces the current matrix of a shape with the identity matrix + * @usage web_application + * @see PShape#rotate(float) + * @see PShape#scale(float) + * @see PShape#translate(float, float) + */ + public void resetMatrix() { + checkMatrix(2); + matrix.reset(); + } + + + public void applyMatrix(PMatrix source) { + if (source instanceof PMatrix2D) { + applyMatrix((PMatrix2D) source); + } else if (source instanceof PMatrix3D) { + applyMatrix((PMatrix3D) source); + } + } + + + public void applyMatrix(PMatrix2D source) { + applyMatrix(source.m00, source.m01, 0, source.m02, + source.m10, source.m11, 0, source.m12, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + public void applyMatrix(float n00, float n01, float n02, + float n10, float n11, float n12) { + checkMatrix(2); + matrix.apply(n00, n01, n02, + n10, n11, n12); + } + + + public void applyMatrix(PMatrix3D source) { + applyMatrix(source.m00, source.m01, source.m02, source.m03, + source.m10, source.m11, source.m12, source.m13, + source.m20, source.m21, source.m22, source.m23, + source.m30, source.m31, source.m32, source.m33); + } + + + public void applyMatrix(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + checkMatrix(3); + matrix.apply(n00, n01, n02, n03, + n10, n11, n12, n13, + n20, n21, n22, n23, + n30, n31, n32, n33); + } + + + // + + + /** + * Make sure that the shape's matrix is 1) not null, and 2) has a matrix + * that can handle at least the specified number of dimensions. + */ + protected void checkMatrix(int dimensions) { + if (matrix == null) { + if (dimensions == 2) { + matrix = new PMatrix2D(); + } else { + matrix = new PMatrix3D(); + } + } else if (dimensions == 3 && (matrix instanceof PMatrix2D)) { + // time for an upgrayedd for a double dose of my pimpin' + matrix = new PMatrix3D(matrix); + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Center the shape based on its bounding box. Can't assume + * that the bounding box is 0, 0, width, height. Common case will be + * opening a letter size document in Illustrator, and drawing something + * in the middle, then reading it in as an svg file. + * This will also need to flip the y axis (scale(1, -1)) in cases + * like Adobe Illustrator where the coordinates start at the bottom. + */ +// public void center() { +// } + + + /** + * Set the pivot point for all transformations. + */ +// public void pivot(float x, float y) { +// px = x; +// py = y; +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public void colorMode(int mode) { + colorMode(mode, colorModeX, colorModeY, colorModeZ, colorModeA); + } + + /** + * @param max range for all color elements + */ + public void colorMode(int mode, float max) { + colorMode(mode, max, max, max, max); + } + + + /** + * @param maxX range for the red or hue depending on the current color mode + * @param maxY range for the green or saturation depending on the current color mode + * @param maxZ range for the blue or brightness depending on the current color mode + */ + public void colorMode(int mode, float maxX, float maxY, float maxZ) { + colorMode(mode, maxX, maxY, maxZ, colorModeA); + } + +/** + * @param maxA range for the alpha + */ + public void colorMode(int mode, + float maxX, float maxY, float maxZ, float maxA) { + colorMode = mode; + + colorModeX = maxX; // still needs to be set for hsb + colorModeY = maxY; + colorModeZ = maxZ; + colorModeA = maxA; + + // if color max values are all 1, then no need to scale + colorModeScale = + ((maxA != 1) || (maxX != maxY) || (maxY != maxZ) || (maxZ != maxA)); + + // if color is rgb/0..255 this will make it easier for the + // red() green() etc functions + colorModeDefault = (colorMode == RGB) && + (colorModeA == 255) && (colorModeX == 255) && + (colorModeY == 255) && (colorModeZ == 255); + } + + + protected void colorCalc(int rgb) { + if (((rgb & 0xff000000) == 0) && (rgb <= colorModeX)) { + colorCalc((float) rgb); + + } else { + colorCalcARGB(rgb, colorModeA); + } + } + + + protected void colorCalc(int rgb, float alpha) { + if (((rgb & 0xff000000) == 0) && (rgb <= colorModeX)) { // see above + colorCalc((float) rgb, alpha); + + } else { + colorCalcARGB(rgb, alpha); + } + } + + + protected void colorCalc(float gray) { + colorCalc(gray, colorModeA); + } + + + protected void colorCalc(float gray, float alpha) { + if (gray > colorModeX) gray = colorModeX; + if (alpha > colorModeA) alpha = colorModeA; + + if (gray < 0) gray = 0; + if (alpha < 0) alpha = 0; + + calcR = colorModeScale ? (gray / colorModeX) : gray; + calcG = calcR; + calcB = calcR; + calcA = colorModeScale ? (alpha / colorModeA) : alpha; + + calcRi = (int)(calcR*255); calcGi = (int)(calcG*255); + calcBi = (int)(calcB*255); calcAi = (int)(calcA*255); + calcColor = (calcAi << 24) | (calcRi << 16) | (calcGi << 8) | calcBi; + calcAlpha = (calcAi != 255); + } + + + protected void colorCalc(float x, float y, float z) { + colorCalc(x, y, z, colorModeA); + } + + + protected void colorCalc(float x, float y, float z, float a) { + if (x > colorModeX) x = colorModeX; + if (y > colorModeY) y = colorModeY; + if (z > colorModeZ) z = colorModeZ; + if (a > colorModeA) a = colorModeA; + + if (x < 0) x = 0; + if (y < 0) y = 0; + if (z < 0) z = 0; + if (a < 0) a = 0; + + switch (colorMode) { + case RGB: + if (colorModeScale) { + calcR = x / colorModeX; + calcG = y / colorModeY; + calcB = z / colorModeZ; + calcA = a / colorModeA; + } else { + calcR = x; calcG = y; calcB = z; calcA = a; + } + break; + + case HSB: + x /= colorModeX; // h + y /= colorModeY; // s + z /= colorModeZ; // b + + calcA = colorModeScale ? (a/colorModeA) : a; + + if (y == 0) { // saturation == 0 + calcR = calcG = calcB = z; + + } else { + float which = (x - (int)x) * 6.0f; + float f = which - (int)which; + float p = z * (1.0f - y); + float q = z * (1.0f - y * f); + float t = z * (1.0f - (y * (1.0f - f))); + + switch ((int)which) { + case 0: calcR = z; calcG = t; calcB = p; break; + case 1: calcR = q; calcG = z; calcB = p; break; + case 2: calcR = p; calcG = z; calcB = t; break; + case 3: calcR = p; calcG = q; calcB = z; break; + case 4: calcR = t; calcG = p; calcB = z; break; + case 5: calcR = z; calcG = p; calcB = q; break; + } + } + break; + } + calcRi = (int)(255*calcR); calcGi = (int)(255*calcG); + calcBi = (int)(255*calcB); calcAi = (int)(255*calcA); + calcColor = (calcAi << 24) | (calcRi << 16) | (calcGi << 8) | calcBi; + calcAlpha = (calcAi != 255); + } + + + protected void colorCalcARGB(int argb, float alpha) { + if (alpha == colorModeA) { + calcAi = (argb >> 24) & 0xff; + calcColor = argb; + } else { + calcAi = (int) (((argb >> 24) & 0xff) * (alpha / colorModeA)); + calcColor = (calcAi << 24) | (argb & 0xFFFFFF); + } + calcRi = (argb >> 16) & 0xff; + calcGi = (argb >> 8) & 0xff; + calcBi = argb & 0xff; + calcA = calcAi / 255.0f; + calcR = calcRi / 255.0f; + calcG = calcGi / 255.0f; + calcB = calcBi / 255.0f; + calcAlpha = (calcAi != 255); + } + +} diff --git a/src/main/java/processing/core/PShapeOBJ.java b/src/main/java/processing/core/PShapeOBJ.java new file mode 100644 index 0000000..a46e6ab --- /dev/null +++ b/src/main/java/processing/core/PShapeOBJ.java @@ -0,0 +1,469 @@ +package processing.core; + +import java.io.BufferedReader; +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * This class is not part of the Processing API and should not be used + * directly. Instead, use loadShape() and methods like it, which will make + * use of this class. Using this class directly will cause your code to break + * when combined with future versions of Processing. + *

        + * OBJ loading implemented using code from Saito's OBJLoader library: + * http://code.google.com/p/saitoobjloader/ + * and OBJReader from Ahmet Kizilay + * http://www.openprocessing.org/visuals/?visualID=191 + * + */ +public class PShapeOBJ extends PShape { + + /** + * Initializes a new OBJ Object with the given filename. + */ + public PShapeOBJ(PApplet parent, String filename) { + this(parent, parent.createReader(filename), getBasePath(parent, filename)); + } + + public PShapeOBJ(PApplet parent, BufferedReader reader) { + this(parent, reader, ""); + } + + public PShapeOBJ(PApplet parent, BufferedReader reader, String basePath) { + ArrayList faces = new ArrayList(); + ArrayList materials = new ArrayList(); + ArrayList coords = new ArrayList(); + ArrayList normals = new ArrayList(); + ArrayList texcoords = new ArrayList(); + parseOBJ(parent, basePath, reader, + faces, materials, coords, normals, texcoords); + + // The OBJ geometry is stored with each face in a separate child shape. + parent = null; + family = GROUP; + addChildren(faces, materials, coords, normals, texcoords); + } + + + protected PShapeOBJ(OBJFace face, OBJMaterial mtl, + ArrayList coords, + ArrayList normals, + ArrayList texcoords) { + family = GEOMETRY; + if (face.vertIdx.size() == 3) { + kind = TRIANGLES; + } else if (face.vertIdx.size() == 4) { + kind = QUADS; + } else { + kind = POLYGON; + } + + stroke = false; + fill = true; + + // Setting material properties for the new face + fillColor = rgbaValue(mtl.kd); + ambientColor = rgbaValue(mtl.ka); + specularColor = rgbaValue(mtl.ks); + shininess = mtl.ns; + if (mtl.kdMap != null) { + // If current material is textured, then tinting the texture using the + // diffuse color. + tintColor = rgbaValue(mtl.kd, mtl.d); + } + + vertexCount = face.vertIdx.size(); + vertices = new float[vertexCount][12]; + for (int j = 0; j < face.vertIdx.size(); j++){ + int vertIdx, normIdx, texIdx; + PVector vert, norms, tex; + + vert = norms = tex = null; + + vertIdx = face.vertIdx.get(j).intValue() - 1; + vert = coords.get(vertIdx); + + if (j < face.normIdx.size()) { + normIdx = face.normIdx.get(j).intValue() - 1; + if (-1 < normIdx) { + norms = normals.get(normIdx); + } + } + + if (j < face.texIdx.size()) { + texIdx = face.texIdx.get(j).intValue() - 1; + if (-1 < texIdx) { + tex = texcoords.get(texIdx); + } + } + + vertices[j][X] = vert.x; + vertices[j][Y] = vert.y; + vertices[j][Z] = vert.z; + + vertices[j][PGraphics.R] = mtl.kd.x; + vertices[j][PGraphics.G] = mtl.kd.y; + vertices[j][PGraphics.B] = mtl.kd.z; + vertices[j][PGraphics.A] = 1; + + if (norms != null) { + vertices[j][PGraphics.NX] = norms.x; + vertices[j][PGraphics.NY] = norms.y; + vertices[j][PGraphics.NZ] = norms.z; + } + + if (tex != null) { + vertices[j][PGraphics.U] = tex.x; + vertices[j][PGraphics.V] = tex.y; + } + + if (mtl != null && mtl.kdMap != null) { + image = mtl.kdMap; + } + } + } + + + protected void addChildren(ArrayList faces, + ArrayList materials, + ArrayList coords, + ArrayList normals, + ArrayList texcoords) { + int mtlIdxCur = -1; + OBJMaterial mtl = null; + for (int i = 0; i < faces.size(); i++) { + OBJFace face = faces.get(i); + + // Getting current material. + if (mtlIdxCur != face.matIdx || face.matIdx == -1) { + // To make sure that at least we get the default material + mtlIdxCur = PApplet.max(0, face.matIdx); + mtl = materials.get(mtlIdxCur); + } + + // Creating child shape for current face. + PShape child = new PShapeOBJ(face, mtl, coords, normals, texcoords); + addChild(child); + } + } + + + static protected void parseOBJ(PApplet parent, String path, + BufferedReader reader, + ArrayList faces, + ArrayList materials, + ArrayList coords, + ArrayList normals, + ArrayList texcoords) { + Map mtlTable = new HashMap(); + int mtlIdxCur = -1; + boolean readv, readvn, readvt; + try { + + readv = readvn = readvt = false; + String line; + String gname = "object"; + while ((line = reader.readLine()) != null) { + // Parse the line. + line = line.trim(); + if (line.equals("") || line.indexOf('#') == 0) { + // Empty line of comment, ignore line + continue; + } + + // The below patch/hack comes from Carlos Tomas Marti and is a + // fix for single backslashes in Rhino obj files + + // BEGINNING OF RHINO OBJ FILES HACK + // Statements can be broken in multiple lines using '\' at the + // end of a line. + // In regular expressions, the backslash is also an escape + // character. + // The regular expression \\ matches a single backslash. This + // regular expression as a Java string, becomes "\\\\". + // That's right: 4 backslashes to match a single one. + while (line.contains("\\")) { + line = line.split("\\\\")[0]; + final String s = reader.readLine(); + if (s != null) + line += s; + } + // END OF RHINO OBJ FILES HACK + + String[] parts = line.split("\\s+"); + // if not a blank line, process the line. + if (parts.length > 0) { + if (parts[0].equals("v")) { + // vertex + PVector tempv = new PVector(Float.valueOf(parts[1]).floatValue(), + Float.valueOf(parts[2]).floatValue(), + Float.valueOf(parts[3]).floatValue()); + coords.add(tempv); + readv = true; + } else if (parts[0].equals("vn")) { + // normal + PVector tempn = new PVector(Float.valueOf(parts[1]).floatValue(), + Float.valueOf(parts[2]).floatValue(), + Float.valueOf(parts[3]).floatValue()); + normals.add(tempn); + readvn = true; + } else if (parts[0].equals("vt")) { + // uv, inverting v to take into account Processing's inverted Y axis + // with respect to OpenGL. + PVector tempv = new PVector(Float.valueOf(parts[1]).floatValue(), + 1 - Float.valueOf(parts[2]). + floatValue()); + texcoords.add(tempv); + readvt = true; + } else if (parts[0].equals("o")) { + // Object name is ignored, for now. + } else if (parts[0].equals("mtllib")) { + if (parts[1] != null) { + String fn = parts[1]; + if (fn.indexOf(File.separator) == -1 && !path.equals("")) { + // Relative file name, adding the base path. + fn = path + File.separator + fn; + } + BufferedReader mreader = parent.createReader(fn); + if (mreader != null) { + parseMTL(parent, fn, path, mreader, materials, mtlTable); + mreader.close(); + } + } + } else if (parts[0].equals("g")) { + gname = 1 < parts.length ? parts[1] : ""; + } else if (parts[0].equals("usemtl")) { + // Getting index of current active material (will be applied on + // all subsequent faces). + if (parts[1] != null) { + String mtlname = parts[1]; + if (mtlTable.containsKey(mtlname)) { + Integer tempInt = mtlTable.get(mtlname); + mtlIdxCur = tempInt.intValue(); + } else { + mtlIdxCur = -1; + } + } + } else if (parts[0].equals("f")) { + // Face setting + OBJFace face = new OBJFace(); + face.matIdx = mtlIdxCur; + face.name = gname; + + for (int i = 1; i < parts.length; i++) { + String seg = parts[i]; + + if (seg.indexOf("/") > 0) { + String[] forder = seg.split("/"); + + if (forder.length > 2) { + // Getting vertex and texture and normal indexes. + if (forder[0].length() > 0 && readv) { + face.vertIdx.add(Integer.valueOf(forder[0])); + } + + if (forder[1].length() > 0 && readvt) { + face.texIdx.add(Integer.valueOf(forder[1])); + } + + if (forder[2].length() > 0 && readvn) { + face.normIdx.add(Integer.valueOf(forder[2])); + } + } else if (forder.length > 1) { + // Getting vertex and texture/normal indexes. + if (forder[0].length() > 0 && readv) { + face.vertIdx.add(Integer.valueOf(forder[0])); + } + + if (forder[1].length() > 0) { + if (readvt) { + face.texIdx.add(Integer.valueOf(forder[1])); + } else if (readvn) { + face.normIdx.add(Integer.valueOf(forder[1])); + } + + } + + } else if (forder.length > 0) { + // Getting vertex index only. + if (forder[0].length() > 0 && readv) { + face.vertIdx.add(Integer.valueOf(forder[0])); + } + } + } else { + // Getting vertex index only. + if (seg.length() > 0 && readv) { + face.vertIdx.add(Integer.valueOf(seg)); + } + } + } + + faces.add(face); + } + } + } + + if (materials.size() == 0) { + // No materials definition so far. Adding one default material. + OBJMaterial defMtl = new OBJMaterial(); + materials.add(defMtl); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + + static protected void parseMTL(PApplet parent, String mtlfn, String path, + BufferedReader reader, + ArrayList materials, + Map materialsHash) { + try { + String line; + OBJMaterial currentMtl = null; + while ((line = reader.readLine()) != null) { + // Parse the line + line = line.trim(); + String parts[] = line.split("\\s+"); + if (parts.length > 0) { + // Extract the material data. + if (parts[0].equals("newmtl")) { + // Starting new material. + String mtlname = parts[1]; + currentMtl = addMaterial(mtlname, materials, materialsHash); + } else { + if (currentMtl == null) { + currentMtl = addMaterial("material" + materials.size(), + materials, materialsHash); + } + if (parts[0].equals("map_Kd") && parts.length > 1) { + // Loading texture map. + String texname = parts[1]; + if (texname.indexOf(File.separator) == -1 && !path.equals("")) { + // Relative file name, adding the base path. + texname = path + File.separator + texname; + } + + File file = new File(parent.dataPath(texname)); + if (file.exists()) { + currentMtl.kdMap = parent.loadImage(texname); + } else { + System.err.println("The texture map \"" + texname + "\" " + + "in the materials definition file \"" + mtlfn + "\" " + + "is missing or inaccessible, make sure " + + "the URL is valid or that the file has been " + + "added to your sketch and is readable."); + } + } else if (parts[0].equals("Ka") && parts.length > 3) { + // The ambient color of the material + currentMtl.ka.x = Float.valueOf(parts[1]).floatValue(); + currentMtl.ka.y = Float.valueOf(parts[2]).floatValue(); + currentMtl.ka.z = Float.valueOf(parts[3]).floatValue(); + } else if (parts[0].equals("Kd") && parts.length > 3) { + // The diffuse color of the material + currentMtl.kd.x = Float.valueOf(parts[1]).floatValue(); + currentMtl.kd.y = Float.valueOf(parts[2]).floatValue(); + currentMtl.kd.z = Float.valueOf(parts[3]).floatValue(); + } else if (parts[0].equals("Ks") && parts.length > 3) { + // The specular color weighted by the specular coefficient + currentMtl.ks.x = Float.valueOf(parts[1]).floatValue(); + currentMtl.ks.y = Float.valueOf(parts[2]).floatValue(); + currentMtl.ks.z = Float.valueOf(parts[3]).floatValue(); + } else if ((parts[0].equals("d") || + parts[0].equals("Tr")) && parts.length > 1) { + // Reading the alpha transparency. + currentMtl.d = Float.valueOf(parts[1]).floatValue(); + } else if (parts[0].equals("Ns") && parts.length > 1) { + // The specular component of the Phong shading model + currentMtl.ns = Float.valueOf(parts[1]).floatValue(); + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + protected static OBJMaterial addMaterial(String mtlname, + ArrayList materials, + Map materialsHash) { + OBJMaterial currentMtl = new OBJMaterial(mtlname); + materialsHash.put(mtlname, Integer.valueOf(materials.size())); + materials.add(currentMtl); + return currentMtl; + } + + protected static int rgbaValue(PVector color) { + return 0xFF000000 | ((int)(color.x * 255) << 16) | + ((int)(color.y * 255) << 8) | + (int)(color.z * 255); + } + + + protected static int rgbaValue(PVector color, float alpha) { + return ((int)(alpha * 255) << 24) | + ((int)(color.x * 255) << 16) | + ((int)(color.y * 255) << 8) | + (int)(color.z * 255); + } + + + // Stores a face from an OBJ file + static protected class OBJFace { + ArrayList vertIdx; + ArrayList texIdx; + ArrayList normIdx; + int matIdx; + String name; + + OBJFace() { + vertIdx = new ArrayList(); + texIdx = new ArrayList(); + normIdx = new ArrayList(); + matIdx = -1; + name = ""; + } + } + + + static protected String getBasePath(PApplet parent, String filename) { + // Obtaining the path + File file = new File(parent.dataPath(filename)); + if (!file.exists()) { + file = parent.sketchFile(filename); + } + String absolutePath = file.getAbsolutePath(); + return absolutePath.substring(0, + absolutePath.lastIndexOf(File.separator)); + } + + + // Stores a material defined in an MTL file. + static protected class OBJMaterial { + String name; + PVector ka; + PVector kd; + PVector ks; + float d; + float ns; + PImage kdMap; + + OBJMaterial() { + this("default"); + } + + OBJMaterial(String name) { + this.name = name; + ka = new PVector(0.5f, 0.5f, 0.5f); + kd = new PVector(0.5f, 0.5f, 0.5f); + ks = new PVector(0.5f, 0.5f, 0.5f); + d = 1.0f; + ns = 0.0f; + kdMap = null; + } + } +} diff --git a/src/main/java/processing/core/PShapeSVG.java b/src/main/java/processing/core/PShapeSVG.java new file mode 100644 index 0000000..bf5cdc2 --- /dev/null +++ b/src/main/java/processing/core/PShapeSVG.java @@ -0,0 +1,1787 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2006-12 Ben Fry and Casey Reas + Copyright (c) 2004-06 Michael Chang + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License version 2.1 as published by the Free Software Foundation. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + +import processing.data.*; + +// TODO replace these with PMatrix2D +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; + +import java.util.Map; +import java.util.HashMap; + + +/** + * This class is not part of the Processing API and should not be used + * directly. Instead, use loadShape() and methods like it, which will make + * use of this class. Using this class directly will cause your code to break + * when combined with future versions of Processing. + *

        + * SVG stands for Scalable Vector Graphics, a portable graphics format. + * It is a vector format so it allows for "infinite" resolution and relatively + * small file sizes. Most modern media software can view SVG files, including + * Adobe products, Firefox, etc. Illustrator and Inkscape can edit SVG files. + * View the SVG specification here. + *

        + * We have no intention of turning this into a full-featured SVG library. + * The goal of this project is a basic shape importer that originally was small + * enough to be included with applets, meaning that its download size should be + * in the neighborhood of 25-30 Kb. Though we're far less limited nowadays on + * size constraints, we remain extremely limited in terms of time, and do not + * have volunteers who are available to maintain a larger SVG library. + *

        + * For more sophisticated import/export, consider the + * Batik + * library from the Apache Software Foundation. + *

        + * Batik is used in the SVG Export library in Processing 3, however using it + * for full SVG import is still a considerable amount of work. Wiring it to + * Java2D wouldn't be too bad, but using it with OpenGL, JavaFX, and features + * like begin/endRecord() and begin/endRaw() would be considerable effort. + *

        + * Future improvements to this library may focus on this properly supporting + * a specific subset of SVG, for instance the simpler SVG profiles known as + * SVG Tiny or Basic, + * although we still would not support the interactivity options. + * + *


        + * + * A minimal example program using SVG: + * (assuming a working moo.svg is in your data folder) + * + *

        + * PShape moo;
        + *
        + * void setup() {
        + *   size(400, 400);
        + *   moo = loadShape("moo.svg");
        + * }
        + * void draw() {
        + *   background(255);
        + *   shape(moo, mouseX, mouseY);
        + * }
        + * 
        + */ +public class PShapeSVG extends PShape { + XML element; + + /// Values between 0 and 1. + protected float opacity; + float strokeOpacity; + float fillOpacity; + + /** Width of containing SVG (used for percentages). */ + protected float svgWidth; + + /** Height of containing SVG (used for percentages). */ + protected float svgHeight; + + /** √((w² + h²)/2) of containing SVG (used for percentages). */ + protected float svgSizeXY; + + protected Gradient strokeGradient; + String strokeName; // id of another object, gradients only? + + protected Gradient fillGradient; + String fillName; // id of another object + + + /** + * Initializes a new SVG object from the given XML object. + */ + public PShapeSVG(XML svg) { + this(null, svg, true); + + if (!svg.getName().equals("svg")) { + if (svg.getName().toLowerCase().equals("html")) { + // Common case is that files aren't downloaded properly + throw new RuntimeException("This appears to be a web page, not an SVG file."); + } else { + throw new RuntimeException("The root node is not , it's <" + svg.getName() + ">"); + } + } + } + + + protected PShapeSVG(PShapeSVG parent, XML properties, boolean parseKids) { + setParent(parent); + + // Need to get width/height in early. + if (properties.getName().equals("svg")) { + String unitWidth = properties.getString("width"); + String unitHeight = properties.getString("height"); + + // Can't handle width/height as percentages easily. I'm just going + // to put in 100 as a dummy value, beacuse this means that it will + // come out as a reasonable value. + if (unitWidth != null) width = parseUnitSize(unitWidth, 100); + if (unitHeight != null) height = parseUnitSize(unitHeight, 100); + + String viewBoxStr = properties.getString("viewBox"); + if (viewBoxStr != null) { + float[] viewBox = PApplet.parseFloat(PApplet.splitTokens(viewBoxStr)); + if (unitWidth == null || unitHeight == null) { + // Not proper parsing of the viewBox, but will cover us for cases where + // the width and height of the object is not specified. + width = viewBox[2]; + height = viewBox[3]; + } else { + // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute + // TODO: preserveAspectRatio. + if (matrix == null) matrix = new PMatrix2D(); + matrix.scale(width/viewBox[2], height/viewBox[3]); + matrix.translate(-viewBox[0], -viewBox[1]); + } + } + + // Negative size is illegal. + if (width < 0 || height < 0) + throw new RuntimeException(": width (" + width + + ") and height (" + height + ") must not be negative."); + + // It's technically valid to have width or height == 0. Not specified at + // all is what to test for. + if ((unitWidth == null || unitHeight == null) && viewBoxStr == null) { + //throw new RuntimeException("width/height not specified"); + PGraphics.showWarning("The width and/or height is not " + + "readable in the tag of this file."); + // For the spec, the default is 100% and 100%. For purposes + // here, insert a dummy value because this is prolly just a + // font or something for which the w/h doesn't matter. + width = 1; + height = 1; + } + + svgWidth = width; + svgHeight = height; + svgSizeXY = PApplet.sqrt((svgWidth*svgWidth + svgHeight*svgHeight)/2.0f); + } + + element = properties; + name = properties.getString("id"); + // @#$(* adobe illustrator mangles names of objects when re-saving + if (name != null) { + while (true) { + String[] m = PApplet.match(name, "_x([A-Za-z0-9]{2})_"); + if (m == null) break; + char repair = (char) PApplet.unhex(m[1]); + name = name.replace(m[0], "" + repair); + } + } + + String displayStr = properties.getString("display", "inline"); + visible = !displayStr.equals("none"); + + String transformStr = properties.getString("transform"); + if (transformStr != null) { + if (matrix == null) { + matrix = parseTransform(transformStr); + } else { + matrix.preApply(parseTransform(transformStr)); + } + } + + if (parseKids) { + parseColors(properties); + parseChildren(properties); + } + } + + + // Broken out so that subclasses can copy any additional variables + // (i.e. fillGradientPaint and strokeGradientPaint) + protected void setParent(PShapeSVG parent) { + // Need to set this so that findChild() works. + // Otherwise 'parent' is null until addChild() is called later. + this.parent = parent; + + if (parent == null) { + // set values to their defaults according to the SVG spec + stroke = false; + strokeColor = 0xff000000; + strokeWeight = 1; + strokeCap = PConstants.SQUARE; // equivalent to BUTT in svg spec + strokeJoin = PConstants.MITER; + strokeGradient = null; +// strokeGradientPaint = null; + strokeName = null; + + fill = true; + fillColor = 0xff000000; + fillGradient = null; +// fillGradientPaint = null; + fillName = null; + + //hasTransform = false; + //transformation = null; //new float[] { 1, 0, 0, 1, 0, 0 }; + + // svgWidth, svgHeight, and svgXYSize done below. + + strokeOpacity = 1; + fillOpacity = 1; + opacity = 1; + + } else { + stroke = parent.stroke; + strokeColor = parent.strokeColor; + strokeWeight = parent.strokeWeight; + strokeCap = parent.strokeCap; + strokeJoin = parent.strokeJoin; + strokeGradient = parent.strokeGradient; +// strokeGradientPaint = parent.strokeGradientPaint; + strokeName = parent.strokeName; + + fill = parent.fill; + fillColor = parent.fillColor; + fillGradient = parent.fillGradient; +// fillGradientPaint = parent.fillGradientPaint; + fillName = parent.fillName; + + svgWidth = parent.svgWidth; + svgHeight = parent.svgHeight; + svgSizeXY = parent.svgSizeXY; + + opacity = parent.opacity; + } + + // The rect and ellipse modes are set to CORNER since it is the expected + // mode for svg shapes. + rectMode = CORNER; + ellipseMode = CORNER; + } + + + /** Factory method for subclasses. */ + protected PShapeSVG createShape(PShapeSVG parent, XML properties, boolean parseKids) { + return new PShapeSVG(parent, properties, parseKids); + } + + + protected void parseChildren(XML graphics) { + XML[] elements = graphics.getChildren(); + children = new PShape[elements.length]; + childCount = 0; + + for (XML elem : elements) { + PShape kid = parseChild(elem); + if (kid != null) addChild(kid); + } + children = (PShape[]) PApplet.subset(children, 0, childCount); + } + + + /** + * Parse a child XML element. + * Override this method to add parsing for more SVG elements. + */ + protected PShape parseChild(XML elem) { +// System.err.println("parsing child in pshape " + elem.getName()); + String name = elem.getName(); + PShapeSVG shape = null; + + + if (name == null) { + // just some whitespace that can be ignored (hopefully) + + } else if (name.equals("g")) { + shape = createShape(this, elem, true); + + } else if (name.equals("defs")) { + // generally this will contain gradient info, so may + // as well just throw it into a group element for parsing + shape = createShape(this, elem, true); + + } else if (name.equals("line")) { + shape = createShape(this, elem, true); + shape.parseLine(); + + } else if (name.equals("circle")) { + shape = createShape(this, elem, true); + shape.parseEllipse(true); + + } else if (name.equals("ellipse")) { + shape = createShape(this, elem, true); + shape.parseEllipse(false); + + } else if (name.equals("rect")) { + shape = createShape(this, elem, true); + shape.parseRect(); + + } else if (name.equals("polygon")) { + shape = createShape(this, elem, true); + shape.parsePoly(true); + + } else if (name.equals("polyline")) { + shape = createShape(this, elem, true); + shape.parsePoly(false); + + } else if (name.equals("path")) { + shape = createShape(this, elem, true); + shape.parsePath(); + + } else if (name.equals("radialGradient")) { + return new RadialGradient(this, elem); + + } else if (name.equals("linearGradient")) { + return new LinearGradient(this, elem); + + } else if (name.equals("font")) { + return new Font(this, elem); + +// } else if (name.equals("font-face")) { +// return new FontFace(this, elem); + +// } else if (name.equals("glyph") || name.equals("missing-glyph")) { +// return new FontGlyph(this, elem); + + } else if (name.equals("text")) { // || name.equals("font")) { + PGraphics.showWarning("Text and fonts in SVG files are " + + "not currently supported, convert text to outlines instead."); + + } else if (name.equals("filter")) { + PGraphics.showWarning("Filters are not supported."); + + } else if (name.equals("mask")) { + PGraphics.showWarning("Masks are not supported."); + + } else if (name.equals("pattern")) { + PGraphics.showWarning("Patterns are not supported."); + + } else if (name.equals("stop")) { + // stop tag is handled by gradient parser, so don't warn about it + + } else if (name.equals("sodipodi:namedview")) { + // these are always in Inkscape files, the warnings get tedious + + } else if (name.equals("metadata") + || name.equals("title") || name.equals("desc")) { + // fontforge just stuffs in as a comment. + // All harmless stuff, irrelevant to rendering. + return null; + + } else if (!name.startsWith("#")) { + PGraphics.showWarning("Ignoring <" + name + "> tag."); +// new Exception().printStackTrace(); + } + return shape; + } + + + protected void parseLine() { + kind = LINE; + family = PRIMITIVE; + params = new float[] { + getFloatWithUnit(element, "x1", svgWidth), + getFloatWithUnit(element, "y1", svgHeight), + getFloatWithUnit(element, "x2", svgWidth), + getFloatWithUnit(element, "y2", svgHeight) + }; + } + + + /** + * Handles parsing ellipse and circle tags. + * @param circle true if this is a circle and not an ellipse + */ + protected void parseEllipse(boolean circle) { + kind = ELLIPSE; + family = PRIMITIVE; + params = new float[4]; + + params[0] = getFloatWithUnit(element, "cx", svgWidth); + params[1] = getFloatWithUnit(element, "cy", svgHeight); + + float rx, ry; + if (circle) { + rx = ry = getFloatWithUnit(element, "r", svgSizeXY); + } else { + rx = getFloatWithUnit(element, "rx", svgWidth); + ry = getFloatWithUnit(element, "ry", svgHeight); + } + params[0] -= rx; + params[1] -= ry; + + params[2] = rx*2; + params[3] = ry*2; + } + + + protected void parseRect() { + kind = RECT; + family = PRIMITIVE; + params = new float[] { + getFloatWithUnit(element, "x", svgWidth), + getFloatWithUnit(element, "y", svgHeight), + getFloatWithUnit(element, "width", svgWidth), + getFloatWithUnit(element, "height", svgHeight) + }; + } + + + /** + * Parse a polyline or polygon from an SVG file. + * Syntax defined at http://www.w3.org/TR/SVG/shapes.html#PointsBNF + * @param close true if shape is closed (polygon), false if not (polyline) + */ + protected void parsePoly(boolean close) { + family = PATH; + this.close = close; + + String pointsAttr = element.getString("points"); + if (pointsAttr != null) { + String[] pointsBuffer = PApplet.splitTokens(pointsAttr); + vertexCount = pointsBuffer.length; + vertices = new float[vertexCount][2]; + for (int i = 0; i < vertexCount; i++) { + String pb[] = PApplet.splitTokens(pointsBuffer[i], ", \t\r\n"); + vertices[i][X] = Float.parseFloat(pb[0]); + vertices[i][Y] = Float.parseFloat(pb[1]); + } + } + } + + + protected void parsePath() { + family = PATH; + kind = 0; + + String pathData = element.getString("d"); + if (pathData == null || PApplet.trim(pathData).length() == 0) { + return; + } + char[] pathDataChars = pathData.toCharArray(); + + StringBuilder pathBuffer = new StringBuilder(); + boolean lastSeparate = false; + + for (int i = 0; i < pathDataChars.length; i++) { + char c = pathDataChars[i]; + boolean separate = false; + + if (c == 'M' || c == 'm' || + c == 'L' || c == 'l' || + c == 'H' || c == 'h' || + c == 'V' || c == 'v' || + c == 'C' || c == 'c' || // beziers + c == 'S' || c == 's' || + c == 'Q' || c == 'q' || // quadratic beziers + c == 'T' || c == 't' || + c == 'A' || c == 'a' || // elliptical arc + c == 'Z' || c == 'z' || // closepath + c == ',') { + separate = true; + if (i != 0) { + pathBuffer.append("|"); + } + } + if (c == 'Z' || c == 'z') { + separate = false; + } + if (c == '-' && !lastSeparate) { + // allow for 'e' notation in numbers, e.g. 2.10e-9 + // http://dev.processing.org/bugs/show_bug.cgi?id=1408 + if (i == 0 || pathDataChars[i-1] != 'e') { + pathBuffer.append("|"); + } + } + if (c != ',') { + pathBuffer.append(c); //"" + pathDataBuffer.charAt(i)); + } + if (separate && c != ',' && c != '-') { + pathBuffer.append("|"); + } + lastSeparate = separate; + } + + // use whitespace constant to get rid of extra spaces and CR or LF + String[] pathTokens = + PApplet.splitTokens(pathBuffer.toString(), "|" + WHITESPACE); + vertices = new float[pathTokens.length][2]; + vertexCodes = new int[pathTokens.length]; + + float cx = 0; + float cy = 0; + int i = 0; + + char implicitCommand = '\0'; +// char prevCommand = '\0'; + boolean prevCurve = false; + float ctrlX, ctrlY; + // store values for closepath so that relative coords work properly + float movetoX = 0; + float movetoY = 0; + + while (i < pathTokens.length) { + char c = pathTokens[i].charAt(0); + if (((c >= '0' && c <= '9') || (c == '-')) && implicitCommand != '\0') { + c = implicitCommand; + i--; + } else { + implicitCommand = c; + } + switch (c) { + + case 'M': // M - move to (absolute) + cx = PApplet.parseFloat(pathTokens[i + 1]); + cy = PApplet.parseFloat(pathTokens[i + 2]); + movetoX = cx; + movetoY = cy; + parsePathMoveto(cx, cy); + implicitCommand = 'L'; + i += 3; + break; + + case 'm': // m - move to (relative) + cx = cx + PApplet.parseFloat(pathTokens[i + 1]); + cy = cy + PApplet.parseFloat(pathTokens[i + 2]); + movetoX = cx; + movetoY = cy; + parsePathMoveto(cx, cy); + implicitCommand = 'l'; + i += 3; + break; + + case 'L': + cx = PApplet.parseFloat(pathTokens[i + 1]); + cy = PApplet.parseFloat(pathTokens[i + 2]); + parsePathLineto(cx, cy); + i += 3; + break; + + case 'l': + cx = cx + PApplet.parseFloat(pathTokens[i + 1]); + cy = cy + PApplet.parseFloat(pathTokens[i + 2]); + parsePathLineto(cx, cy); + i += 3; + break; + + // horizontal lineto absolute + case 'H': + cx = PApplet.parseFloat(pathTokens[i + 1]); + parsePathLineto(cx, cy); + i += 2; + break; + + // horizontal lineto relative + case 'h': + cx = cx + PApplet.parseFloat(pathTokens[i + 1]); + parsePathLineto(cx, cy); + i += 2; + break; + + case 'V': + cy = PApplet.parseFloat(pathTokens[i + 1]); + parsePathLineto(cx, cy); + i += 2; + break; + + case 'v': + cy = cy + PApplet.parseFloat(pathTokens[i + 1]); + parsePathLineto(cx, cy); + i += 2; + break; + + // C - curve to (absolute) + case 'C': { + float ctrlX1 = PApplet.parseFloat(pathTokens[i + 1]); + float ctrlY1 = PApplet.parseFloat(pathTokens[i + 2]); + float ctrlX2 = PApplet.parseFloat(pathTokens[i + 3]); + float ctrlY2 = PApplet.parseFloat(pathTokens[i + 4]); + float endX = PApplet.parseFloat(pathTokens[i + 5]); + float endY = PApplet.parseFloat(pathTokens[i + 6]); + parsePathCurveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY); + cx = endX; + cy = endY; + i += 7; + prevCurve = true; + } + break; + + // c - curve to (relative) + case 'c': { + float ctrlX1 = cx + PApplet.parseFloat(pathTokens[i + 1]); + float ctrlY1 = cy + PApplet.parseFloat(pathTokens[i + 2]); + float ctrlX2 = cx + PApplet.parseFloat(pathTokens[i + 3]); + float ctrlY2 = cy + PApplet.parseFloat(pathTokens[i + 4]); + float endX = cx + PApplet.parseFloat(pathTokens[i + 5]); + float endY = cy + PApplet.parseFloat(pathTokens[i + 6]); + parsePathCurveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY); + cx = endX; + cy = endY; + i += 7; + prevCurve = true; + } + break; + + // S - curve to shorthand (absolute) + // Draws a cubic Bézier curve from the current point to (x,y). The first + // control point is assumed to be the reflection of the second control + // point on the previous command relative to the current point. + // (x2,y2) is the second control point (i.e., the control point + // at the end of the curve). S (uppercase) indicates that absolute + // coordinates will follow; s (lowercase) indicates that relative + // coordinates will follow. Multiple sets of coordinates may be specified + // to draw a polybézier. At the end of the command, the new current point + // becomes the final (x,y) coordinate pair used in the polybézier. + case 'S': { + // (If there is no previous command or if the previous command was not + // an C, c, S or s, assume the first control point is coincident with + // the current point.) + if (!prevCurve) { + ctrlX = cx; + ctrlY = cy; + } else { + float ppx = vertices[vertexCount-2][X]; + float ppy = vertices[vertexCount-2][Y]; + float px = vertices[vertexCount-1][X]; + float py = vertices[vertexCount-1][Y]; + ctrlX = px + (px - ppx); + ctrlY = py + (py - ppy); + } + float ctrlX2 = PApplet.parseFloat(pathTokens[i + 1]); + float ctrlY2 = PApplet.parseFloat(pathTokens[i + 2]); + float endX = PApplet.parseFloat(pathTokens[i + 3]); + float endY = PApplet.parseFloat(pathTokens[i + 4]); + parsePathCurveto(ctrlX, ctrlY, ctrlX2, ctrlY2, endX, endY); + cx = endX; + cy = endY; + i += 5; + prevCurve = true; + } + break; + + // s - curve to shorthand (relative) + case 's': { + if (!prevCurve) { + ctrlX = cx; + ctrlY = cy; + } else { + float ppx = vertices[vertexCount-2][X]; + float ppy = vertices[vertexCount-2][Y]; + float px = vertices[vertexCount-1][X]; + float py = vertices[vertexCount-1][Y]; + ctrlX = px + (px - ppx); + ctrlY = py + (py - ppy); + } + float ctrlX2 = cx + PApplet.parseFloat(pathTokens[i + 1]); + float ctrlY2 = cy + PApplet.parseFloat(pathTokens[i + 2]); + float endX = cx + PApplet.parseFloat(pathTokens[i + 3]); + float endY = cy + PApplet.parseFloat(pathTokens[i + 4]); + parsePathCurveto(ctrlX, ctrlY, ctrlX2, ctrlY2, endX, endY); + cx = endX; + cy = endY; + i += 5; + prevCurve = true; + } + break; + + // Q - quadratic curve to (absolute) + // Draws a quadratic Bézier curve from the current point to (x,y) using + // (x1,y1) as the control point. Q (uppercase) indicates that absolute + // coordinates will follow; q (lowercase) indicates that relative + // coordinates will follow. Multiple sets of coordinates may be specified + // to draw a polybézier. At the end of the command, the new current point + // becomes the final (x,y) coordinate pair used in the polybézier. + case 'Q': { + ctrlX = PApplet.parseFloat(pathTokens[i + 1]); + ctrlY = PApplet.parseFloat(pathTokens[i + 2]); + float endX = PApplet.parseFloat(pathTokens[i + 3]); + float endY = PApplet.parseFloat(pathTokens[i + 4]); + //parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY); + parsePathQuadto(ctrlX, ctrlY, endX, endY); + cx = endX; + cy = endY; + i += 5; + prevCurve = true; + } + break; + + // q - quadratic curve to (relative) + case 'q': { + ctrlX = cx + PApplet.parseFloat(pathTokens[i + 1]); + ctrlY = cy + PApplet.parseFloat(pathTokens[i + 2]); + float endX = cx + PApplet.parseFloat(pathTokens[i + 3]); + float endY = cy + PApplet.parseFloat(pathTokens[i + 4]); + //parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY); + parsePathQuadto(ctrlX, ctrlY, endX, endY); + cx = endX; + cy = endY; + i += 5; + prevCurve = true; + } + break; + + // T - quadratic curveto shorthand (absolute) + // The control point is assumed to be the reflection of the control + // point on the previous command relative to the current point. + case 'T': { + // If there is no previous command or if the previous command was + // not a Q, q, T or t, assume the control point is coincident + // with the current point. + if (!prevCurve) { + ctrlX = cx; + ctrlY = cy; + } else { + float ppx = vertices[vertexCount-2][X]; + float ppy = vertices[vertexCount-2][Y]; + float px = vertices[vertexCount-1][X]; + float py = vertices[vertexCount-1][Y]; + ctrlX = px + (px - ppx); + ctrlY = py + (py - ppy); + } + float endX = PApplet.parseFloat(pathTokens[i + 1]); + float endY = PApplet.parseFloat(pathTokens[i + 2]); + //parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY); + parsePathQuadto(ctrlX, ctrlY, endX, endY); + cx = endX; + cy = endY; + i += 3; + prevCurve = true; + } + break; + + // t - quadratic curveto shorthand (relative) + case 't': { + if (!prevCurve) { + ctrlX = cx; + ctrlY = cy; + } else { + float ppx = vertices[vertexCount-2][X]; + float ppy = vertices[vertexCount-2][Y]; + float px = vertices[vertexCount-1][X]; + float py = vertices[vertexCount-1][Y]; + ctrlX = px + (px - ppx); + ctrlY = py + (py - ppy); + } + float endX = cx + PApplet.parseFloat(pathTokens[i + 1]); + float endY = cy + PApplet.parseFloat(pathTokens[i + 2]); + //parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY); + parsePathQuadto(ctrlX, ctrlY, endX, endY); + cx = endX; + cy = endY; + i += 3; + prevCurve = true; + } + break; + + // A - elliptical arc to (absolute) + case 'A': { + float rx = PApplet.parseFloat(pathTokens[i + 1]); + float ry = PApplet.parseFloat(pathTokens[i + 2]); + float angle = PApplet.parseFloat(pathTokens[i + 3]); + boolean fa = PApplet.parseFloat(pathTokens[i + 4]) != 0; + boolean fs = PApplet.parseFloat(pathTokens[i + 5]) != 0; + float endX = PApplet.parseFloat(pathTokens[i + 6]); + float endY = PApplet.parseFloat(pathTokens[i + 7]); + parsePathArcto(cx, cy, rx, ry, angle, fa, fs, endX, endY); + cx = endX; + cy = endY; + i += 8; + prevCurve = true; + } + break; + + // a - elliptical arc to (relative) + case 'a': { + float rx = PApplet.parseFloat(pathTokens[i + 1]); + float ry = PApplet.parseFloat(pathTokens[i + 2]); + float angle = PApplet.parseFloat(pathTokens[i + 3]); + boolean fa = PApplet.parseFloat(pathTokens[i + 4]) != 0; + boolean fs = PApplet.parseFloat(pathTokens[i + 5]) != 0; + float endX = cx + PApplet.parseFloat(pathTokens[i + 6]); + float endY = cy + PApplet.parseFloat(pathTokens[i + 7]); + parsePathArcto(cx, cy, rx, ry, angle, fa, fs, endX, endY); + cx = endX; + cy = endY; + i += 8; + prevCurve = true; + } + break; + + case 'Z': + case 'z': + // since closing the path, the 'current' point needs + // to return back to the last moveto location. + // http://code.google.com/p/processing/issues/detail?id=1058 + cx = movetoX; + cy = movetoY; + close = true; + i++; + break; + + default: + String parsed = + PApplet.join(PApplet.subset(pathTokens, 0, i), ","); + String unparsed = + PApplet.join(PApplet.subset(pathTokens, i), ","); + System.err.println("parsed: " + parsed); + System.err.println("unparsed: " + unparsed); + throw new RuntimeException("shape command not handled: " + pathTokens[i]); + } +// prevCommand = c; + } + } + + +// private void parsePathCheck(int num) { +// if (vertexCount + num-1 >= vertices.length) { +// //vertices = (float[][]) PApplet.expand(vertices); +// float[][] temp = new float[vertexCount << 1][2]; +// System.arraycopy(vertices, 0, temp, 0, vertexCount); +// vertices = temp; +// } +// } + + private void parsePathVertex(float x, float y) { + if (vertexCount == vertices.length) { + //vertices = (float[][]) PApplet.expand(vertices); + float[][] temp = new float[vertexCount << 1][2]; + System.arraycopy(vertices, 0, temp, 0, vertexCount); + vertices = temp; + } + vertices[vertexCount][X] = x; + vertices[vertexCount][Y] = y; + vertexCount++; + } + + + private void parsePathCode(int what) { + if (vertexCodeCount == vertexCodes.length) { + vertexCodes = PApplet.expand(vertexCodes); + } + vertexCodes[vertexCodeCount++] = what; + } + + + private void parsePathMoveto(float px, float py) { + if (vertexCount > 0) { + parsePathCode(BREAK); + } + parsePathCode(VERTEX); + parsePathVertex(px, py); + } + + + private void parsePathLineto(float px, float py) { + parsePathCode(VERTEX); + parsePathVertex(px, py); + } + + + private void parsePathCurveto(float x1, float y1, + float x2, float y2, + float x3, float y3) { + parsePathCode(BEZIER_VERTEX); + parsePathVertex(x1, y1); + parsePathVertex(x2, y2); + parsePathVertex(x3, y3); + } + +// private void parsePathQuadto(float x1, float y1, +// float cx, float cy, +// float x2, float y2) { +// //System.out.println("quadto: " + x1 + "," + y1 + " " + cx + "," + cy + " " + x2 + "," + y2); +//// parsePathCode(BEZIER_VERTEX); +// parsePathCode(QUAD_BEZIER_VERTEX); +// // x1/y1 already covered by last moveto, lineto, or curveto +// +// parsePathVertex(x1 + ((cx-x1)*2/3.0f), y1 + ((cy-y1)*2/3.0f)); +// parsePathVertex(x2 + ((cx-x2)*2/3.0f), y2 + ((cy-y2)*2/3.0f)); +// parsePathVertex(x2, y2); +// } + + private void parsePathQuadto(float cx, float cy, + float x2, float y2) { + //System.out.println("quadto: " + x1 + "," + y1 + " " + cx + "," + cy + " " + x2 + "," + y2); +// parsePathCode(BEZIER_VERTEX); + parsePathCode(QUADRATIC_VERTEX); + // x1/y1 already covered by last moveto, lineto, or curveto + parsePathVertex(cx, cy); + parsePathVertex(x2, y2); + } + + + // Approximates elliptical arc by several bezier segments. + // Meets SVG standard requirements from: + // http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands + // http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes + // Based on arc to bezier curve equations from: + // http://www.spaceroots.org/documents/ellipse/node22.html + private void parsePathArcto(float x1, float y1, + float rx, float ry, + float angle, + boolean fa, boolean fs, + float x2, float y2) { + if (x1 == x2 && y1 == y2) return; + if (rx == 0 || ry == 0) { parsePathLineto(x2, y2); return; } + + rx = PApplet.abs(rx); ry = PApplet.abs(ry); + + float phi = PApplet.radians(((angle % 360) + 360) % 360); + float cosPhi = PApplet.cos(phi), sinPhi = PApplet.sin(phi); + + float x1r = ( cosPhi * (x1 - x2) + sinPhi * (y1 - y2)) / 2; + float y1r = (-sinPhi * (x1 - x2) + cosPhi * (y1 - y2)) / 2; + + float cxr, cyr; + { + float A = (x1r*x1r) / (rx*rx) + (y1r*y1r) / (ry*ry); + if (A > 1) { + // No solution, scale ellipse up according to SVG standard + float sqrtA = PApplet.sqrt(A); + rx *= sqrtA; cxr = 0; + ry *= sqrtA; cyr = 0; + } else { + float k = ((fa == fs) ? -1f : 1f) * + PApplet.sqrt((rx*rx * ry*ry) / ((rx*rx * y1r*y1r) + (ry*ry * x1r*x1r)) - 1f); + cxr = k * rx * y1r / ry; + cyr = -k * ry * x1r / rx; + } + } + + float cx = cosPhi * cxr - sinPhi * cyr + (x1 + x2) / 2; + float cy = sinPhi * cxr + cosPhi * cyr + (y1 + y2) / 2; + + float phi1, phiDelta; + { + float sx = ( x1r - cxr) / rx, sy = ( y1r - cyr) / ry; + float tx = (-x1r - cxr) / rx, ty = (-y1r - cyr) / ry; + phi1 = PApplet.atan2(sy, sx); + phiDelta = (((PApplet.atan2(ty, tx) - phi1) % TWO_PI) + TWO_PI) % TWO_PI; + if (!fs) phiDelta -= TWO_PI; + } + + // One segment can not cover more that PI, less than PI/2 is + // recommended to avoid visible inaccuracies caused by rounding errors + int segmentCount = PApplet.ceil(PApplet.abs(phiDelta) / TWO_PI * 4); + + float inc = phiDelta / segmentCount; + float a = PApplet.sin(inc) * + (PApplet.sqrt(4 + 3 * PApplet.sq(PApplet.tan(inc / 2))) - 1) / 3; + + float sinPhi1 = PApplet.sin(phi1), cosPhi1 = PApplet.cos(phi1); + + float p1x = x1; + float p1y = y1; + float relq1x = a * (-rx * cosPhi * sinPhi1 - ry * sinPhi * cosPhi1); + float relq1y = a * (-rx * sinPhi * sinPhi1 + ry * cosPhi * cosPhi1); + + for (int i = 0; i < segmentCount; i++) { + float eta = phi1 + (i + 1) * inc; + float sinEta = PApplet.sin(eta), cosEta = PApplet.cos(eta); + + float p2x = cx + rx * cosPhi * cosEta - ry * sinPhi * sinEta; + float p2y = cy + rx * sinPhi * cosEta + ry * cosPhi * sinEta; + float relq2x = a * (-rx * cosPhi * sinEta - ry * sinPhi * cosEta); + float relq2y = a * (-rx * sinPhi * sinEta + ry * cosPhi * cosEta); + + if (i == segmentCount - 1) { p2x = x2; p2y = y2; } + + parsePathCode(BEZIER_VERTEX); + parsePathVertex(p1x + relq1x, p1y + relq1y); + parsePathVertex(p2x - relq2x, p2y - relq2y); + parsePathVertex(p2x, p2y); + + p1x = p2x; relq1x = relq2x; + p1y = p2y; relq1y = relq2y; + } + } + + + /** + * Parse the specified SVG matrix into a PMatrix2D. Note that PMatrix2D + * is rotated relative to the SVG definition, so parameters are rearranged + * here. More about the transformation matrices in + * this section + * of the SVG documentation. + * @param matrixStr text of the matrix param. + * @return a good old-fashioned PMatrix2D + */ + static protected PMatrix2D parseTransform(String matrixStr) { + matrixStr = matrixStr.trim(); + PMatrix2D outgoing = null; + int start = 0; + int stop = -1; + while ((stop = matrixStr.indexOf(')', start)) != -1) { + PMatrix2D m = parseSingleTransform(matrixStr.substring(start, stop+1)); + if (outgoing == null) { + outgoing = m; + } else { + outgoing.apply(m); + } + start = stop + 1; + } + return outgoing; + } + + + static protected PMatrix2D parseSingleTransform(String matrixStr) { + //String[] pieces = PApplet.match(matrixStr, "^\\s*(\\w+)\\((.*)\\)\\s*$"); + String[] pieces = PApplet.match(matrixStr, "[,\\s]*(\\w+)\\((.*)\\)"); + if (pieces == null) { + System.err.println("Could not parse transform " + matrixStr); + return null; + } + float[] m = PApplet.parseFloat(PApplet.splitTokens(pieces[2], ", ")); + if (pieces[1].equals("matrix")) { + return new PMatrix2D(m[0], m[2], m[4], m[1], m[3], m[5]); + + } else if (pieces[1].equals("translate")) { + float tx = m[0]; + float ty = (m.length == 2) ? m[1] : m[0]; + return new PMatrix2D(1, 0, tx, 0, 1, ty); + + } else if (pieces[1].equals("scale")) { + float sx = m[0]; + float sy = (m.length == 2) ? m[1] : m[0]; + return new PMatrix2D(sx, 0, 0, 0, sy, 0); + + } else if (pieces[1].equals("rotate")) { + float angle = m[0]; + + if (m.length == 1) { + float c = PApplet.cos(angle); + float s = PApplet.sin(angle); + // SVG version is cos(a) sin(a) -sin(a) cos(a) 0 0 + return new PMatrix2D(c, -s, 0, s, c, 0); + + } else if (m.length == 3) { + PMatrix2D mat = new PMatrix2D(0, 1, m[1], 1, 0, m[2]); + mat.rotate(m[0]); + mat.translate(-m[1], -m[2]); + return mat; + } + + } else if (pieces[1].equals("skewX")) { + return new PMatrix2D(1, 0, 1, PApplet.tan(m[0]), 0, 0); + + } else if (pieces[1].equals("skewY")) { + return new PMatrix2D(1, 0, 1, 0, PApplet.tan(m[0]), 0); + } + return null; + } + + + protected void parseColors(XML properties) { + if (properties.hasAttribute("opacity")) { + String opacityText = properties.getString("opacity"); + setOpacity(opacityText); + } + + if (properties.hasAttribute("stroke")) { + String strokeText = properties.getString("stroke"); + setColor(strokeText, false); + } + + if (properties.hasAttribute("stroke-opacity")) { + String strokeOpacityText = properties.getString("stroke-opacity"); + setStrokeOpacity(strokeOpacityText); + } + + if (properties.hasAttribute("stroke-width")) { + // if NaN (i.e. if it's 'inherit') then default back to the inherit setting + String lineweight = properties.getString("stroke-width"); + setStrokeWeight(lineweight); + } + + if (properties.hasAttribute("stroke-linejoin")) { + String linejoin = properties.getString("stroke-linejoin"); + setStrokeJoin(linejoin); + } + + if (properties.hasAttribute("stroke-linecap")) { + String linecap = properties.getString("stroke-linecap"); + setStrokeCap(linecap); + } + + // fill defaults to black (though stroke defaults to "none") + // http://www.w3.org/TR/SVG/painting.html#FillProperties + if (properties.hasAttribute("fill")) { + String fillText = properties.getString("fill"); + setColor(fillText, true); + } + + if (properties.hasAttribute("fill-opacity")) { + String fillOpacityText = properties.getString("fill-opacity"); + setFillOpacity(fillOpacityText); + } + + if (properties.hasAttribute("style")) { + String styleText = properties.getString("style"); + String[] styleTokens = PApplet.splitTokens(styleText, ";"); + + //PApplet.println(styleTokens); + for (int i = 0; i < styleTokens.length; i++) { + String[] tokens = PApplet.splitTokens(styleTokens[i], ":"); + //PApplet.println(tokens); + + tokens[0] = PApplet.trim(tokens[0]); + + if (tokens[0].equals("fill")) { + setColor(tokens[1], true); + + } else if(tokens[0].equals("fill-opacity")) { + setFillOpacity(tokens[1]); + + } else if(tokens[0].equals("stroke")) { + setColor(tokens[1], false); + + } else if(tokens[0].equals("stroke-width")) { + setStrokeWeight(tokens[1]); + + } else if(tokens[0].equals("stroke-linecap")) { + setStrokeCap(tokens[1]); + + } else if(tokens[0].equals("stroke-linejoin")) { + setStrokeJoin(tokens[1]); + + } else if(tokens[0].equals("stroke-opacity")) { + setStrokeOpacity(tokens[1]); + + } else if(tokens[0].equals("opacity")) { + setOpacity(tokens[1]); + + } else { + // Other attributes are not yet implemented + } + } + } + } + + + void setOpacity(String opacityText) { + opacity = PApplet.parseFloat(opacityText); + strokeColor = ((int) (opacity * 255)) << 24 | strokeColor & 0xFFFFFF; + fillColor = ((int) (opacity * 255)) << 24 | fillColor & 0xFFFFFF; + } + + + void setStrokeWeight(String lineweight) { + strokeWeight = parseUnitSize(lineweight, svgSizeXY); + } + + + void setStrokeOpacity(String opacityText) { + strokeOpacity = PApplet.parseFloat(opacityText); + strokeColor = ((int) (strokeOpacity * 255)) << 24 | strokeColor & 0xFFFFFF; + } + + + void setStrokeJoin(String linejoin) { + if (linejoin.equals("inherit")) { + // do nothing, will inherit automatically + + } else if (linejoin.equals("miter")) { + strokeJoin = PConstants.MITER; + + } else if (linejoin.equals("round")) { + strokeJoin = PConstants.ROUND; + + } else if (linejoin.equals("bevel")) { + strokeJoin = PConstants.BEVEL; + } + } + + + void setStrokeCap(String linecap) { + if (linecap.equals("inherit")) { + // do nothing, will inherit automatically + + } else if (linecap.equals("butt")) { + strokeCap = PConstants.SQUARE; + + } else if (linecap.equals("round")) { + strokeCap = PConstants.ROUND; + + } else if (linecap.equals("square")) { + strokeCap = PConstants.PROJECT; + } + } + + + void setFillOpacity(String opacityText) { + fillOpacity = PApplet.parseFloat(opacityText); + fillColor = ((int) (fillOpacity * 255)) << 24 | fillColor & 0xFFFFFF; + } + + + void setColor(String colorText, boolean isFill) { + colorText = colorText.trim(); + int opacityMask = fillColor & 0xFF000000; + boolean visible = true; + int color = 0; + String name = ""; +// String lColorText = colorText.toLowerCase(); + Gradient gradient = null; +// Object paint = null; + if (colorText.equals("none")) { + visible = false; + } else if (colorText.startsWith("url(#")) { + name = colorText.substring(5, colorText.length() - 1); + Object object = findChild(name); + if (object instanceof Gradient) { + gradient = (Gradient) object; + // in 3.0a11, do this on first draw inside PShapeJava2D +// paint = calcGradientPaint(gradient); //, opacity); + } else { +// visible = false; + System.err.println("url " + name + " refers to unexpected data: " + object); + } + } else { + // Prints errors itself. + color = opacityMask | parseSimpleColor(colorText); + } + if (isFill) { + fill = visible; + fillColor = color; + fillName = name; + fillGradient = gradient; +// fillGradientPaint = paint; + } else { + stroke = visible; + strokeColor = color; + strokeName = name; + strokeGradient = gradient; +// strokeGradientPaint = paint; + } + } + + + /** + * Parses the "color" datatype only, and prints an error if it is not of this form. + * http://www.w3.org/TR/SVG/types.html#DataTypeColor + * @return 0xRRGGBB (no alpha). Zero on error. + */ + static protected int parseSimpleColor(String colorText) { + colorText = colorText.toLowerCase().trim(); + //if (colorNames.containsKey(colorText)) { + if (colorNames.hasKey(colorText)) { + return colorNames.get(colorText); + } else if (colorText.startsWith("#")) { + if (colorText.length() == 4) { + // Short form: #ABC, transform to long form #AABBCC + colorText = colorText.replaceAll("^#(.)(.)(.)$", "#$1$1$2$2$3$3"); + } + return (Integer.parseInt(colorText.substring(1), 16)) & 0xFFFFFF; + //System.out.println("hex for fill is " + PApplet.hex(fillColor)); + } else if (colorText.startsWith("rgb")) { + return parseRGB(colorText); + } else { + System.err.println("Cannot parse \"" + colorText + "\"."); + return 0; + } + } + + + /** + * Deliberately conforms to the HTML 4.01 color spec + en-gb grey, rather + * than the (unlikely to be useful) entire 147-color system used in SVG. + */ + static protected IntDict colorNames = new IntDict(new Object[][] { + { "aqua", 0x00ffff }, + { "black", 0x000000 }, + { "blue", 0x0000ff }, + { "fuchsia", 0xff00ff }, + { "gray", 0x808080 }, + { "grey", 0x808080 }, + { "green", 0x008000 }, + { "lime", 0x00ff00 }, + { "maroon", 0x800000 }, + { "navy", 0x000080 }, + { "olive", 0x808000 }, + { "purple", 0x800080 }, + { "red", 0xff0000 }, + { "silver", 0xc0c0c0 }, + { "teal", 0x008080 }, + { "white", 0xffffff }, + { "yellow", 0xffff00 } + }); + + /* + static protected Map colorNames; + static { + colorNames = new HashMap(); + colorNames.put("aqua", 0x00ffff); + colorNames.put("black", 0x000000); + colorNames.put("blue", 0x0000ff); + colorNames.put("fuchsia", 0xff00ff); + colorNames.put("gray", 0x808080); + colorNames.put("grey", 0x808080); + colorNames.put("green", 0x008000); + colorNames.put("lime", 0x00ff00); + colorNames.put("maroon", 0x800000); + colorNames.put("navy", 0x000080); + colorNames.put("olive", 0x808000); + colorNames.put("purple", 0x800080); + colorNames.put("red", 0xff0000); + colorNames.put("silver", 0xc0c0c0); + colorNames.put("teal", 0x008080); + colorNames.put("white", 0xffffff); + colorNames.put("yellow", 0xffff00); + } + */ + + static protected int parseRGB(String what) { + int leftParen = what.indexOf('(') + 1; + int rightParen = what.indexOf(')'); + String sub = what.substring(leftParen, rightParen); + String[] values = PApplet.splitTokens(sub, ", "); + int rgbValue = 0; + if (values.length == 3) { + // Color spec allows for rgb values to be percentages. + for (int i = 0; i < 3; i++) { + rgbValue <<= 8; + if (values[i].endsWith("%")) { + rgbValue |= (int)(PApplet.constrain(255*parseFloatOrPercent(values[i]), 0, 255)); + } else { + rgbValue |= PApplet.constrain(PApplet.parseInt(values[i]), 0, 255); + } + } + } else System.err.println("Could not read color \"" + what + "\"."); + + return rgbValue; + } + + + //static protected Map parseStyleAttributes(String style) { + static protected StringDict parseStyleAttributes(String style) { + //Map table = new HashMap(); + StringDict table = new StringDict(); +// if (style == null) return table; + if (style != null) { + String[] pieces = style.split(";"); + for (int i = 0; i < pieces.length; i++) { + String[] parts = pieces[i].split(":"); + //table.put(parts[0], parts[1]); + table.set(parts[0], parts[1]); + } + } + return table; + } + + + /** + * Used in place of element.getFloatAttribute(a) because we can + * have a unit suffix (length or coordinate). + * @param element what to parse + * @param attribute name of the attribute to get + * @param relativeTo (float) Used for %. When relative to viewbox, should + * be svgWidth for horizontal dimentions, svgHeight for vertical, and + * svgXYSize for anything else. + * @return unit-parsed version of the data + */ + static protected float getFloatWithUnit(XML element, String attribute, float relativeTo) { + String val = element.getString(attribute); + return (val == null) ? 0 : parseUnitSize(val, relativeTo); + } + + + /** + * Parse a size that may have a suffix for its units. + * This assumes 90dpi, which implies, as given in the + * units spec: + *
          + *
        • "1pt" equals "1.25px" (and therefore 1.25 user units) + *
        • "1pc" equals "15px" (and therefore 15 user units) + *
        • "1mm" would be "3.543307px" (3.543307 user units) + *
        • "1cm" equals "35.43307px" (and therefore 35.43307 user units) + *
        • "1in" equals "90px" (and therefore 90 user units) + *
        + * @param relativeTo (float) Used for %. When relative to viewbox, should + * be svgWidth for horizontal dimentions, svgHeight for vertical, and + * svgXYSize for anything else. + */ + static protected float parseUnitSize(String text, float relativeTo) { + int len = text.length() - 2; + + if (text.endsWith("pt")) { + return PApplet.parseFloat(text.substring(0, len)) * 1.25f; + } else if (text.endsWith("pc")) { + return PApplet.parseFloat(text.substring(0, len)) * 15; + } else if (text.endsWith("mm")) { + return PApplet.parseFloat(text.substring(0, len)) * 3.543307f; + } else if (text.endsWith("cm")) { + return PApplet.parseFloat(text.substring(0, len)) * 35.43307f; + } else if (text.endsWith("in")) { + return PApplet.parseFloat(text.substring(0, len)) * 90; + } else if (text.endsWith("px")) { + return PApplet.parseFloat(text.substring(0, len)); + } else if (text.endsWith("%")) { + return relativeTo * parseFloatOrPercent(text); + } else { + return PApplet.parseFloat(text); + } + } + + + static protected float parseFloatOrPercent(String text) { + text = text.trim(); + if (text.endsWith("%")) { + return Float.parseFloat(text.substring(0, text.length() - 1)) / 100.0f; + } else { + return Float.parseFloat(text); + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + static public class Gradient extends PShapeSVG { + AffineTransform transform; + + public float[] offset; + public int[] color; + public int count; + + public Gradient(PShapeSVG parent, XML properties) { + super(parent, properties, true); + + XML elements[] = properties.getChildren(); + offset = new float[elements.length]; + color = new int[elements.length]; + + // + for (int i = 0; i < elements.length; i++) { + XML elem = elements[i]; + String name = elem.getName(); + if (name.equals("stop")) { + String offsetAttr = elem.getString("offset"); + offset[count] = parseFloatOrPercent(offsetAttr); + + String style = elem.getString("style"); + //Map styles = parseStyleAttributes(style); + StringDict styles = parseStyleAttributes(style); + + String colorStr = styles.get("stop-color"); + if (colorStr == null) { + colorStr = elem.getString("stop-color"); + if (colorStr == null) colorStr = "#000000"; + } + String opacityStr = styles.get("stop-opacity"); + if (opacityStr == null) { + opacityStr = elem.getString("stop-opacity"); + if (opacityStr == null) opacityStr = "1"; + } + int tupacity = PApplet.constrain( + (int)(PApplet.parseFloat(opacityStr) * 255), 0, 255); + color[count] = (tupacity << 24) | parseSimpleColor(colorStr); + count++; + } + } + offset = PApplet.subset(offset, 0, count); + color = PApplet.subset(color, 0, count); + } + } + + + public class LinearGradient extends Gradient { + public float x1, y1, x2, y2; + + public LinearGradient(PShapeSVG parent, XML properties) { + super(parent, properties); + + this.x1 = getFloatWithUnit(properties, "x1", svgWidth); + this.y1 = getFloatWithUnit(properties, "y1", svgHeight); + this.x2 = getFloatWithUnit(properties, "x2", svgWidth); + this.y2 = getFloatWithUnit(properties, "y2", svgHeight); + + String transformStr = + properties.getString("gradientTransform"); + + if (transformStr != null) { + float t[] = parseTransform(transformStr).get(null); + this.transform = new AffineTransform(t[0], t[3], t[1], t[4], t[2], t[5]); + + Point2D t1 = transform.transform(new Point2D.Float(x1, y1), null); + Point2D t2 = transform.transform(new Point2D.Float(x2, y2), null); + + this.x1 = (float) t1.getX(); + this.y1 = (float) t1.getY(); + this.x2 = (float) t2.getX(); + this.y2 = (float) t2.getY(); + } + } + } + + + public class RadialGradient extends Gradient { + public float cx, cy, r; + + public RadialGradient(PShapeSVG parent, XML properties) { + super(parent, properties); + + this.cx = getFloatWithUnit(properties, "cx", svgWidth); + this.cy = getFloatWithUnit(properties, "cy", svgHeight); + this.r = getFloatWithUnit(properties, "r", svgSizeXY); + + String transformStr = + properties.getString("gradientTransform"); + + if (transformStr != null) { + float t[] = parseTransform(transformStr).get(null); + this.transform = new AffineTransform(t[0], t[3], t[1], t[4], t[2], t[5]); + + Point2D t1 = transform.transform(new Point2D.Float(cx, cy), null); + Point2D t2 = transform.transform(new Point2D.Float(cx + r, cy), null); + + this.cx = (float) t1.getX(); + this.cy = (float) t1.getY(); + this.r = (float) (t2.getX() - t1.getX()); + } + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public static class Font extends PShapeSVG { + public FontFace face; + + public Map namedGlyphs; + public Map unicodeGlyphs; + + public int glyphCount; + public FontGlyph[] glyphs; + public FontGlyph missingGlyph; + + int horizAdvX; + + + public Font(PShapeSVG parent, XML properties) { + super(parent, properties, false); +// handle(parent, properties); + + XML[] elements = properties.getChildren(); + + horizAdvX = properties.getInt("horiz-adv-x", 0); + + namedGlyphs = new HashMap(); + unicodeGlyphs = new HashMap(); + glyphCount = 0; + glyphs = new FontGlyph[elements.length]; + + for (int i = 0; i < elements.length; i++) { + String name = elements[i].getName(); + XML elem = elements[i]; + if (name == null) { + // skip it + } else if (name.equals("glyph")) { + FontGlyph fg = new FontGlyph(this, elem, this); + if (fg.isLegit()) { + if (fg.name != null) { + namedGlyphs.put(fg.name, fg); + } + if (fg.unicode != 0) { + unicodeGlyphs.put(Character.valueOf(fg.unicode), fg); + } + } + glyphs[glyphCount++] = fg; + + } else if (name.equals("missing-glyph")) { +// System.out.println("got missing glyph inside "); + missingGlyph = new FontGlyph(this, elem, this); + } else if (name.equals("font-face")) { + face = new FontFace(this, elem); + } else { + System.err.println("Ignoring " + name + " inside "); + } + } + } + + + protected void drawShape() { + // does nothing for fonts + } + + + public void drawString(PGraphics g, String str, float x, float y, float size) { + // 1) scale by the 1.0/unitsPerEm + // 2) scale up by a font size + g.pushMatrix(); + float s = size / face.unitsPerEm; + //System.out.println("scale is " + s); + // swap y coord at the same time, since fonts have y=0 at baseline + g.translate(x, y); + g.scale(s, -s); + char[] c = str.toCharArray(); + for (int i = 0; i < c.length; i++) { + // call draw on each char (pulling it w/ the unicode table) + FontGlyph fg = unicodeGlyphs.get(Character.valueOf(c[i])); + if (fg != null) { + fg.draw(g); + // add horizAdvX/unitsPerEm to the x coordinate along the way + g.translate(fg.horizAdvX, 0); + } else { + System.err.println("'" + c[i] + "' not available."); + } + } + g.popMatrix(); + } + + + public void drawChar(PGraphics g, char c, float x, float y, float size) { + g.pushMatrix(); + float s = size / face.unitsPerEm; + g.translate(x, y); + g.scale(s, -s); + FontGlyph fg = unicodeGlyphs.get(Character.valueOf(c)); + if (fg != null) g.shape(fg); + g.popMatrix(); + } + + + public float textWidth(String str, float size) { + float w = 0; + char[] c = str.toCharArray(); + for (int i = 0; i < c.length; i++) { + // call draw on each char (pulling it w/ the unicode table) + FontGlyph fg = unicodeGlyphs.get(Character.valueOf(c[i])); + if (fg != null) { + w += (float) fg.horizAdvX / face.unitsPerEm; + } + } + return w * size; + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + static class FontFace extends PShapeSVG { + int horizOriginX; // dflt 0 + int horizOriginY; // dflt 0 +// int horizAdvX; // no dflt? + int vertOriginX; // dflt horizAdvX/2 + int vertOriginY; // dflt ascent + int vertAdvY; // dflt 1em (unitsPerEm value) + + String fontFamily; + int fontWeight; // can also be normal or bold (also comma separated) + String fontStretch; + int unitsPerEm; // dflt 1000 + int[] panose1; // dflt "0 0 0 0 0 0 0 0 0 0" + int ascent; + int descent; + int[] bbox; // spec says comma separated, tho not w/ forge + int underlineThickness; + int underlinePosition; + //String unicodeRange; // gonna ignore for now + + + public FontFace(PShapeSVG parent, XML properties) { + super(parent, properties, true); + + unitsPerEm = properties.getInt("units-per-em", 1000); + } + + + protected void drawShape() { + // nothing to draw in the font face attribute + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public static class FontGlyph extends PShapeSVG { // extends Path + public String name; + char unicode; + int horizAdvX; + + public FontGlyph(PShapeSVG parent, XML properties, Font font) { + super(parent, properties, true); + super.parsePath(); // ?? + + name = properties.getString("glyph-name"); + String u = properties.getString("unicode"); + unicode = 0; + if (u != null) { + if (u.length() == 1) { + unicode = u.charAt(0); + //System.out.println("unicode for " + name + " is " + u); + } else { + System.err.println("unicode for " + name + + " is more than one char: " + u); + } + } + if (properties.hasAttribute("horiz-adv-x")) { + horizAdvX = properties.getInt("horiz-adv-x"); + } else { + horizAdvX = font.horizAdvX; + } + } + + + protected boolean isLegit() { // TODO need a better way to handle this... + return vertexCount != 0; + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Get a particular element based on its SVG ID. When editing SVG by hand, + * this is the id="" tag on any SVG element. When editing from Illustrator, + * these IDs can be edited by expanding the layers palette. The names used + * in the layers palette, both for the layers or the shapes and groups + * beneath them can be used here. + *
        +   * // This code grabs "Layer 3" and the shapes beneath it.
        +   * PShape layer3 = svg.getChild("Layer 3");
        +   * 
        + */ + @Override + public PShape getChild(String name) { + PShape found = super.getChild(name); + if (found == null) { + // Otherwise try with underscores instead of spaces + // (this is how Illustrator handles spaces in the layer names). + found = super.getChild(name.replace(' ', '_')); + } + // Set bounding box based on the parent bounding box + if (found != null) { +// found.x = this.x; +// found.y = this.y; + found.width = this.width; + found.height = this.height; + } + return found; + } + + + /** + * Prints out the SVG document. Useful for parsing. + */ + public void print() { + PApplet.println(element.toString()); + } +} diff --git a/src/main/java/processing/core/PStyle.java b/src/main/java/processing/core/PStyle.java new file mode 100644 index 0000000..7b27695 --- /dev/null +++ b/src/main/java/processing/core/PStyle.java @@ -0,0 +1,63 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2008 Ben Fry and Casey Reas + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + + +public class PStyle implements PConstants { + public int imageMode; + public int rectMode; + public int ellipseMode; + public int shapeMode; + + public int blendMode; + + public int colorMode; + public float colorModeX; + public float colorModeY; + public float colorModeZ; + public float colorModeA; + + public boolean tint; + public int tintColor; + public boolean fill; + public int fillColor; + public boolean stroke; + public int strokeColor; + public float strokeWeight; + public int strokeCap; + public int strokeJoin; + + // TODO these fellas are inconsistent, and may need to go elsewhere + public float ambientR, ambientG, ambientB; + public float specularR, specularG, specularB; + public float emissiveR, emissiveG, emissiveB; + public float shininess; + + public PFont textFont; + public int textAlign; + public int textAlignY; + public int textMode; + public float textSize; + public float textLeading; +} diff --git a/src/main/java/processing/core/PSurface.java b/src/main/java/processing/core/PSurface.java new file mode 100644 index 0000000..c4f3580 --- /dev/null +++ b/src/main/java/processing/core/PSurface.java @@ -0,0 +1,161 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2014-15 The Processing Foundation + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + + +public interface PSurface { + /** + * Minimum dimensions for the window holding an applet. This varies between + * platforms, Mac OS X 10.3 (confirmed with 10.7 and Java 6) can do any + * height but requires at least 128 pixels width. Windows XP has another + * set of limitations. And for all I know, Linux probably allows window + * sizes to be negative numbers. + */ + static public final int MIN_WINDOW_WIDTH = 128; + static public final int MIN_WINDOW_HEIGHT = 128; + + // renderer that doesn't draw to the screen + public void initOffscreen(PApplet sketch); + + // considering removal in favor of separate Component classes for appropriate renderers + // (i.e. for Java2D or a generic Image surface, but not PDF, debatable for GL or FX) + //public Component initComponent(PApplet sketch); + + //public Frame initFrame(PApplet sketch, Color backgroundColor, +// public void initFrame(PApplet sketch, int backgroundColor, +// int deviceIndex, boolean fullScreen, boolean spanDisplays); + public void initFrame(PApplet sketch); + + /** + * Get the native window object associated with this drawing surface. + * For Java2D, this will be an AWT Frame object. For OpenGL, the window. + * The data returned here is subject to the whims of the renderer, + * and using this method means you're willing to deal with underlying + * implementation changes and that you won't throw a fit like a toddler + * if your code breaks sometime in the future. + */ + public Object getNative(); + + // + + // Just call these on an AWT Frame object stored in PApplet. + // Silly, but prevents a lot of rewrite and extra methods for little benefit. + // However, maybe prevents us from having to document the 'frame' variable? + + /** Set the window (and dock, or whatever necessary) title. */ + public void setTitle(String title); + + /** Show or hide the window. */ + public void setVisible(boolean visible); + + /** Set true if we want to resize things (default is not resizable) */ + public void setResizable(boolean resizable); + + /** Dumb name, but inherited from Frame and no better ideas. */ + public void setAlwaysOnTop(boolean always); + + public void setIcon(PImage icon); + + // + +// public void placeWindow(int[] location); + + public void placeWindow(int[] location, int[] editorLocation); + + //public void placeFullScreen(boolean hideStop); + public void placePresent(int stopColor); + + // Sketch is running from the PDE, set up messaging back to the PDE + public void setupExternalMessages(); + + // + + // sets displayWidth/Height inside PApplet + //public void checkDisplaySize(); + + public void setLocation(int x, int y); + + public void setSize(int width, int height); + +// /** +// * Called by {@link PApplet#createGraphics} to initialize the +// * {@link PGraphics#image} object with an image that's compatible with this +// * drawing surface/display/hardware. +// * @param gr PGraphics object whose image will be set +// * @param wide +// * @param high +// */ + // create pixel buffer (pulled out for offscreen graphics) + //public void initImage(PGraphics gr, int wide, int high); + // create pixel buffer, called from allocate() to produce a compatible image for rendering efficiently +// public void initImage(PGraphics gr); + + //public Component getComponent(); + +// /** +// * Sometimes smoothing must be set at the drawing surface level +// * not just inside the renderer itself. +// */ +// public void setSmooth(int level); + + public void setFrameRate(float fps); + +// // called on the first frame so that the now-visible drawing surface can +// // receive key and mouse events +// public void requestFocus(); + +// // finish rendering to the screen (called by PApplet) +// public void blit(); + + // + + public void setCursor(int kind); + + public void setCursor(PImage image, int hotspotX, int hotspotY); + + public void showCursor(); + + public void hideCursor(); + + // + + /** Start the animation thread */ + public void startThread(); + + /** + * On the next trip through the animation thread, things should go sleepy-bye. + * Does not pause the thread immediately because that needs to happen on the + * animation thread itself, so fires on the next trip through draw(). + */ + public void pauseThread(); + + public void resumeThread(); + + /** + * Stop the animation thread (set it null) + * @return false if already stopped + */ + public boolean stopThread(); + + public boolean isStopped(); +} diff --git a/src/main/java/processing/core/PSurfaceNone.java b/src/main/java/processing/core/PSurfaceNone.java new file mode 100644 index 0000000..cb90384 --- /dev/null +++ b/src/main/java/processing/core/PSurfaceNone.java @@ -0,0 +1,374 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2015 The Processing Foundation + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.core; + + +/** + * Surface that's not really visible. Used for PDF and friends, or as a base + * class for other drawing surfaces. It includes the standard rendering loop. + */ +public class PSurfaceNone implements PSurface { + protected PApplet sketch; + protected PGraphics graphics; + + protected Thread thread; + protected boolean paused; + protected Object pauseObject = new Object(); + + protected float frameRateTarget = 60; + protected long frameRatePeriod = 1000000000L / 60L; + + + public PSurfaceNone(PGraphics graphics) { + this.graphics = graphics; + } + + + @Override + public void initOffscreen(PApplet sketch) { + this.sketch = sketch; + + setSize(sketch.sketchWidth(), sketch.sketchHeight()); + } + + +// public Component initComponent(PApplet sketch) { +// return null; +// } + + + @Override + public void initFrame(PApplet sketch) {/*, int backgroundColor, + int deviceIndex, boolean fullScreen, + boolean spanDisplays) {*/ + //this.sketch = sketch; + throw new IllegalStateException("initFrame() not available with " + + getClass().getSimpleName()); + } + + + public Object getNative() { + return null; + } + + + /** Set the window (and dock, or whatever necessary) title. */ + @Override + public void setTitle(String title) { + // You're in a utopian PSurface implementation where titles don't exist. + } + + + @Override + public void setIcon(PImage image) { + // I ain't visible, man. + } + + + /** Show or hide the window. */ + @Override + public void setVisible(boolean visible) { + // I'm always invisible. You can't catch me. + } + + + /** Set true if we want to resize things (default is not resizable) */ + @Override + public void setResizable(boolean resizable) { + // I don't need size to know my worth. + } + + + @Override + public void placeWindow(int[] location, int[] editorLocation) { } + + + @Override + public void placePresent(int stopColor) { } + + + @Override + public void setupExternalMessages() { } + + + @Override + public void setAlwaysOnTop(boolean always) { } + + + // + + + @Override + public void setLocation(int x, int y) { + // I'm everywhere, because I'm nowhere. + } + + + @Override + public void setSize(int wide, int high) { + if (PApplet.DEBUG) { + //System.out.format("frame visible %b, setSize(%d, %d) %n", frame.isVisible(), wide, high); + new Exception(String.format("setSize(%d, %d)", wide, high)).printStackTrace(System.out); + } + + //if (wide == sketchWidth && high == sketchHeight) { // doesn't work on launch + if (wide == sketch.width && high == sketch.height) { + if (PApplet.DEBUG) { + new Exception("w/h unchanged " + wide + " " + high).printStackTrace(System.out); + } + return; // unchanged, don't rebuild everything + } + + //throw new RuntimeException("implement me, see readme.md"); + sketch.width = wide; + sketch.height = high; + + // set PGraphics variables for width/height/pixelWidth/pixelHeight + graphics.setSize(wide, high); + } + + +// public void initImage(PGraphics graphics) { +// // TODO Auto-generated method stub +// +// } + +// public Component getComponent() { +// return null; +// } + + +// public void setSmooth(int level) { +// // TODO Auto-generated method stub +// } + +// void requestFocus() { +// } + +// public void blit() { +// // TODO Auto-generated method stub +// } + + public void setCursor(int kind) { } + + public void setCursor(PImage image, int hotspotX, int hotspotY) { } + + public void showCursor() { } + + public void hideCursor() { } + + + // + + + public Thread createThread() { + return new AnimationThread(); + } + + + public void startThread() { + if (thread == null) { + thread = createThread(); + thread.start(); + } else { + throw new IllegalStateException("Thread already started in " + + getClass().getSimpleName()); + } + } + + + public boolean stopThread() { + if (thread == null) { + return false; + } + thread = null; + return true; + } + + + public boolean isStopped() { + return thread == null || !thread.isAlive(); + } + + + // sets a flag to pause the thread when ready + public void pauseThread() { + PApplet.debug("PApplet.run() paused, calling object wait..."); + paused = true; + } + + + // halts the animation thread if the pause flag is set + protected void checkPause() { + if (paused) { + synchronized (pauseObject) { + try { + pauseObject.wait(); +// PApplet.debug("out of wait"); + } catch (InterruptedException e) { + // waiting for this interrupt on a start() (resume) call + } + } + } +// PApplet.debug("done with pause"); + } + + + public void resumeThread() { + paused = false; + synchronized (pauseObject) { + pauseObject.notifyAll(); // wake up the animation thread + } + } + + + public void setFrameRate(float fps) { + frameRateTarget = fps; + frameRatePeriod = (long) (1000000000.0 / frameRateTarget); + //g.setFrameRate(fps); + } + + + public class AnimationThread extends Thread { + + public AnimationThread() { + super("Animation Thread"); + } + + // broken out so it can be overridden by Danger et al + public void callDraw() { + sketch.handleDraw(); + } + + /** + * Main method for the primary animation thread. + * Painting in AWT and Swing + */ + @Override + public void run() { // not good to make this synchronized, locks things up + long beforeTime = System.nanoTime(); + long overSleepTime = 0L; + + int noDelays = 0; + // Number of frames with a delay of 0 ms before the + // animation thread yields to other running threads. + final int NO_DELAYS_PER_YIELD = 15; + + /* + // If size un-initialized, might be a Canvas. Call setSize() here since + // we now have a parent object that this Canvas can use as a peer. + if (graphics.image == null) { +// System.out.format("it's null, sketchW/H already set to %d %d%n", sketchWidth, sketchHeight); + try { + EventQueue.invokeAndWait(new Runnable() { + public void run() { + setSize(sketchWidth, sketchHeight); + } + }); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } catch (InvocationTargetException ite) { + ite.printStackTrace(); + } +// System.out.format(" but now, sketchW/H changed to %d %d%n", sketchWidth, sketchHeight); + } + */ + + // un-pause the sketch and get rolling + sketch.start(); + + while ((Thread.currentThread() == thread) && !sketch.finished) { + checkPause(); + + // Don't resize the renderer from the EDT (i.e. from a ComponentEvent), + // otherwise it may attempt a resize mid-render. +// Dimension currentSize = canvas.getSize(); +// if (currentSize.width != sketchWidth || currentSize.height != sketchHeight) { +// System.err.format("need to resize from %s to %d, %d%n", currentSize, sketchWidth, sketchHeight); +// } + + // render a single frame +// try { +// EventQueue.invokeAndWait(new Runnable() { +// public void run() { +// System.out.println("calling draw, finished = " + sketch.finished); + //System.out.println("calling draw, looping = " + sketch.looping + ", frameCount = " + sketch.frameCount); + callDraw(); + +// EventQueue.invokeLater(new Runnable() { +// public void run() { +// if (sketch.frameCount == 1) { +// requestFocus(); +// } +// } +// }); + +// } +// }); +// } catch (InterruptedException ie) { +// ie.printStackTrace(); +// } catch (InvocationTargetException ite) { +// ite.getTargetException().printStackTrace(); +// } + + // wait for update & paint to happen before drawing next frame + // this is necessary since the drawing is sometimes in a + // separate thread, meaning that the next frame will start + // before the update/paint is completed + + long afterTime = System.nanoTime(); + long timeDiff = afterTime - beforeTime; + //System.out.println("time diff is " + timeDiff); + long sleepTime = (frameRatePeriod - timeDiff) - overSleepTime; + + if (sleepTime > 0) { // some time left in this cycle + try { + Thread.sleep(sleepTime / 1000000L, (int) (sleepTime % 1000000L)); + noDelays = 0; // Got some sleep, not delaying anymore + } catch (InterruptedException ex) { } + + overSleepTime = (System.nanoTime() - afterTime) - sleepTime; + + } else { // sleepTime <= 0; the frame took longer than the period + overSleepTime = 0L; + noDelays++; + + if (noDelays > NO_DELAYS_PER_YIELD) { + Thread.yield(); // give another thread a chance to run + noDelays = 0; + } + } + + beforeTime = System.nanoTime(); + } + + sketch.dispose(); // call to shutdown libs? + + // If the user called the exit() function, the window should close, + // rather than the sketch just halting. + if (sketch.exitCalled) { + sketch.exitActual(); + } + } + } +} diff --git a/src/main/java/processing/core/PVector.java b/src/main/java/processing/core/PVector.java new file mode 100644 index 0000000..51d461f --- /dev/null +++ b/src/main/java/processing/core/PVector.java @@ -0,0 +1,1063 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-17 The Processing Foundation + Copyright (c) 2008-12 Ben Fry and Casey Reas + Copyright (c) 2008 Dan Shiffman + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License version 2.1 as published by the Free Software Foundation. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA + */ + +package processing.core; + +import java.io.Serializable; + +import processing.core.PApplet; +import processing.core.PConstants; + +/** + * ( begin auto-generated from PVector.xml ) + * + * A class to describe a two or three dimensional vector. This datatype + * stores two or three variables that are commonly used as a position, + * velocity, and/or acceleration. Technically, position is a point + * and velocity and acceleration are vectors, but this is + * often simplified to consider all three as vectors. For example, if you + * consider a rectangle moving across the screen, at any given instant it + * has a position (the object's location, expressed as a point.), a + * velocity (the rate at which the object's position changes per time unit, + * expressed as a vector), and acceleration (the rate at which the object's + * velocity changes per time unit, expressed as a vector). Since vectors + * represent groupings of values, we cannot simply use traditional + * addition/multiplication/etc. Instead, we'll need to do some "vector" + * math, which is made easy by the methods inside the PVector + * class.
        + *
        + * The methods for this class are extensive. For a complete list, visit the + * developer's reference. + * + * ( end auto-generated ) + * + * A class to describe a two or three dimensional vector. + *

        + * The result of all functions are applied to the vector itself, with the + * exception of cross(), which returns a new PVector (or writes to a specified + * 'target' PVector). That is, add() will add the contents of one vector to + * this one. Using add() with additional parameters allows you to put the + * result into a new PVector. Functions that act on multiple vectors also + * include static versions. Because creating new objects can be computationally + * expensive, most functions include an optional 'target' PVector, so that a + * new PVector object is not created with each operation. + *

        + * Initially based on the Vector3D class by Dan Shiffman. + * + * @webref math + */ +public class PVector implements Serializable { + /** + * ( begin auto-generated from PVector_x.xml ) + * + * The x component of the vector. This field (variable) can be used to both + * get and set the value (see above example.) + * + * ( end auto-generated ) + * + * @webref pvector:field + * @usage web_application + * @brief The x component of the vector + */ + public float x; + + /** + * ( begin auto-generated from PVector_y.xml ) + * + * The y component of the vector. This field (variable) can be used to both + * get and set the value (see above example.) + * + * ( end auto-generated ) + * + * @webref pvector:field + * @usage web_application + * @brief The y component of the vector + */ + public float y; + + /** + * ( begin auto-generated from PVector_z.xml ) + * + * The z component of the vector. This field (variable) can be used to both + * get and set the value (see above example.) + * + * ( end auto-generated ) + * + * @webref pvector:field + * @usage web_application + * @brief The z component of the vector + */ + public float z; + + /** Array so that this can be temporarily used in an array context */ + transient protected float[] array; + + + /** + * Constructor for an empty vector: x, y, and z are set to 0. + */ + public PVector() { + } + + + /** + * Constructor for a 3D vector. + * + * @param x the x coordinate. + * @param y the y coordinate. + * @param z the z coordinate. + */ + public PVector(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + + /** + * Constructor for a 2D vector: z coordinate is set to 0. + */ + public PVector(float x, float y) { + this.x = x; + this.y = y; + } + + + /** + * ( begin auto-generated from PVector_set.xml ) + * + * Sets the x, y, and z component of the vector using two or three separate + * variables, the data from a PVector, or the values from a float array. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @param x the x component of the vector + * @param y the y component of the vector + * @param z the z component of the vector + * @brief Set the components of the vector + */ + public PVector set(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + return this; + } + + + /** + * @param x the x component of the vector + * @param y the y component of the vector + */ + public PVector set(float x, float y) { + this.x = x; + this.y = y; + this.z = 0; + return this; + } + + + /** + * @param v any variable of type PVector + */ + public PVector set(PVector v) { + x = v.x; + y = v.y; + z = v.z; + return this; + } + + + /** + * Set the x, y (and maybe z) coordinates using a float[] array as the source. + * @param source array to copy from + */ + public PVector set(float[] source) { + if (source.length >= 2) { + x = source[0]; + y = source[1]; + } + if (source.length >= 3) { + z = source[2]; + } else { + z = 0; + } + return this; + } + + + /** + * ( begin auto-generated from PVector_random2D.xml ) + * + * Make a new 2D unit vector with a random direction. If you pass in "this" + * as an argument, it will use the PApplet's random number generator. You can + * also pass in a target PVector to fill. + * + * @webref pvector:method + * @usage web_application + * @return the random PVector + * @brief Make a new 2D unit vector with a random direction. + * @see PVector#random3D() + */ + static public PVector random2D() { + return random2D(null, null); + } + + + /** + * Make a new 2D unit vector with a random direction + * using Processing's current random number generator + * @param parent current PApplet instance + * @return the random PVector + */ + static public PVector random2D(PApplet parent) { + return random2D(null, parent); + } + + /** + * Set a 2D vector to a random unit vector with a random direction + * @param target the target vector (if null, a new vector will be created) + * @return the random PVector + */ + static public PVector random2D(PVector target) { + return random2D(target, null); + } + + + /** + * Make a new 2D unit vector with a random direction. Pass in the parent + * PApplet if you want randomSeed() to work (and be predictable). Or leave + * it null and be... random. + * @return the random PVector + */ + static public PVector random2D(PVector target, PApplet parent) { + return (parent == null) ? + fromAngle((float) (Math.random() * Math.PI*2), target) : + fromAngle(parent.random(PConstants.TAU), target); + } + + + /** + * ( begin auto-generated from PVector_random3D.xml ) + * + * Make a new 3D unit vector with a random direction. If you pass in "this" + * as an argument, it will use the PApplet's random number generator. You can + * also pass in a target PVector to fill. + * + * @webref pvector:method + * @usage web_application + * @return the random PVector + * @brief Make a new 3D unit vector with a random direction. + * @see PVector#random2D() + */ + static public PVector random3D() { + return random3D(null, null); + } + + + /** + * Make a new 3D unit vector with a random direction + * using Processing's current random number generator + * @param parent current PApplet instance + * @return the random PVector + */ + static public PVector random3D(PApplet parent) { + return random3D(null, parent); + } + + + /** + * Set a 3D vector to a random unit vector with a random direction + * @param target the target vector (if null, a new vector will be created) + * @return the random PVector + */ + static public PVector random3D(PVector target) { + return random3D(target, null); + } + + + /** + * Make a new 3D unit vector with a random direction + * @return the random PVector + */ + static public PVector random3D(PVector target, PApplet parent) { + float angle; + float vz; + if (parent == null) { + angle = (float) (Math.random()*Math.PI*2); + vz = (float) (Math.random()*2-1); + } else { + angle = parent.random(PConstants.TWO_PI); + vz = parent.random(-1,1); + } + float vx = (float) (Math.sqrt(1-vz*vz)*Math.cos(angle)); + float vy = (float) (Math.sqrt(1-vz*vz)*Math.sin(angle)); + if (target == null) { + target = new PVector(vx, vy, vz); + //target.normalize(); // Should be unnecessary + } else { + target.set(vx,vy,vz); + } + return target; + } + + + /** + * ( begin auto-generated from PVector_sub.xml ) + * + * Make a new 2D unit vector from an angle. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @brief Make a new 2D unit vector from an angle + * @param angle the angle in radians + * @return the new unit PVector + */ + static public PVector fromAngle(float angle) { + return fromAngle(angle,null); + } + + + /** + * Make a new 2D unit vector from an angle + * + * @param target the target vector (if null, a new vector will be created) + * @return the PVector + */ + static public PVector fromAngle(float angle, PVector target) { + if (target == null) { + target = new PVector((float)Math.cos(angle),(float)Math.sin(angle),0); + } else { + target.set((float)Math.cos(angle),(float)Math.sin(angle),0); + } + return target; + } + + + /** + * ( begin auto-generated from PVector_copy.xml ) + * + * Gets a copy of the vector, returns a PVector object. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @brief Get a copy of the vector + */ + public PVector copy() { + return new PVector(x, y, z); + } + + + @Deprecated + public PVector get() { + return copy(); + } + + + /** + * @param target + */ + public float[] get(float[] target) { + if (target == null) { + return new float[] { x, y, z }; + } + if (target.length >= 2) { + target[0] = x; + target[1] = y; + } + if (target.length >= 3) { + target[2] = z; + } + return target; + } + + + /** + * ( begin auto-generated from PVector_mag.xml ) + * + * Calculates the magnitude (length) of the vector and returns the result + * as a float (this is simply the equation sqrt(x*x + y*y + z*z).) + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @brief Calculate the magnitude of the vector + * @return magnitude (length) of the vector + * @see PVector#magSq() + */ + public float mag() { + return (float) Math.sqrt(x*x + y*y + z*z); + } + + + /** + * ( begin auto-generated from PVector_mag.xml ) + * + * Calculates the squared magnitude of the vector and returns the result + * as a float (this is simply the equation (x*x + y*y + z*z).) + * Faster if the real length is not required in the + * case of comparing vectors, etc. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @brief Calculate the magnitude of the vector, squared + * @return squared magnitude of the vector + * @see PVector#mag() + */ + public float magSq() { + return (x*x + y*y + z*z); + } + + + /** + * ( begin auto-generated from PVector_add.xml ) + * + * Adds x, y, and z components to a vector, adds one vector to another, or + * adds two independent vectors together. The version of the method that + * adds two vectors together is a static method and returns a PVector, the + * others have no return value -- they act directly on the vector. See the + * examples for more context. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @param v the vector to be added + * @brief Adds x, y, and z components to a vector, one vector to another, or two independent vectors + */ + public PVector add(PVector v) { + x += v.x; + y += v.y; + z += v.z; + return this; + } + + + /** + * @param x x component of the vector + * @param y y component of the vector + */ + public PVector add(float x, float y) { + this.x += x; + this.y += y; + return this; + } + + + /** + * @param z z component of the vector + */ + public PVector add(float x, float y, float z) { + this.x += x; + this.y += y; + this.z += z; + return this; + } + + + /** + * Add two vectors + * @param v1 a vector + * @param v2 another vector + */ + static public PVector add(PVector v1, PVector v2) { + return add(v1, v2, null); + } + + + /** + * Add two vectors into a target vector + * @param target the target vector (if null, a new vector will be created) + */ + static public PVector add(PVector v1, PVector v2, PVector target) { + if (target == null) { + target = new PVector(v1.x + v2.x,v1.y + v2.y, v1.z + v2.z); + } else { + target.set(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); + } + return target; + } + + + /** + * ( begin auto-generated from PVector_sub.xml ) + * + * Subtracts x, y, and z components from a vector, subtracts one vector + * from another, or subtracts two independent vectors. The version of the + * method that subtracts two vectors is a static method and returns a + * PVector, the others have no return value -- they act directly on the + * vector. See the examples for more context. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @param v any variable of type PVector + * @brief Subtract x, y, and z components from a vector, one vector from another, or two independent vectors + */ + public PVector sub(PVector v) { + x -= v.x; + y -= v.y; + z -= v.z; + return this; + } + + + /** + * @param x the x component of the vector + * @param y the y component of the vector + */ + public PVector sub(float x, float y) { + this.x -= x; + this.y -= y; + return this; + } + + + /** + * @param z the z component of the vector + */ + public PVector sub(float x, float y, float z) { + this.x -= x; + this.y -= y; + this.z -= z; + return this; + } + + + /** + * Subtract one vector from another + * @param v1 the x, y, and z components of a PVector object + * @param v2 the x, y, and z components of a PVector object + */ + static public PVector sub(PVector v1, PVector v2) { + return sub(v1, v2, null); + } + + + /** + * Subtract one vector from another and store in another vector + * @param target PVector in which to store the result + */ + static public PVector sub(PVector v1, PVector v2, PVector target) { + if (target == null) { + target = new PVector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); + } else { + target.set(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); + } + return target; + } + + + /** + * ( begin auto-generated from PVector_mult.xml ) + * + * Multiplies a vector by a scalar or multiplies one vector by another. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @brief Multiply a vector by a scalar + * @param n the number to multiply with the vector + */ + public PVector mult(float n) { + x *= n; + y *= n; + z *= n; + return this; + } + + + /** + * @param v the vector to multiply by the scalar + */ + static public PVector mult(PVector v, float n) { + return mult(v, n, null); + } + + + /** + * Multiply a vector by a scalar, and write the result into a target PVector. + * @param target PVector in which to store the result + */ + static public PVector mult(PVector v, float n, PVector target) { + if (target == null) { + target = new PVector(v.x*n, v.y*n, v.z*n); + } else { + target.set(v.x*n, v.y*n, v.z*n); + } + return target; + } + + + /** + * ( begin auto-generated from PVector_div.xml ) + * + * Divides a vector by a scalar or divides one vector by another. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @brief Divide a vector by a scalar + * @param n the number by which to divide the vector + */ + public PVector div(float n) { + x /= n; + y /= n; + z /= n; + return this; + } + + + /** + * Divide a vector by a scalar and return the result in a new vector. + * @param v the vector to divide by the scalar + * @return a new vector that is v1 / n + */ + static public PVector div(PVector v, float n) { + return div(v, n, null); + } + + + /** + * Divide a vector by a scalar and store the result in another vector. + * @param target PVector in which to store the result + */ + static public PVector div(PVector v, float n, PVector target) { + if (target == null) { + target = new PVector(v.x/n, v.y/n, v.z/n); + } else { + target.set(v.x/n, v.y/n, v.z/n); + } + return target; + } + + + /** + * ( begin auto-generated from PVector_dist.xml ) + * + * Calculates the Euclidean distance between two points (considering a + * point as a vector object). + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @param v the x, y, and z coordinates of a PVector + * @brief Calculate the distance between two points + */ + public float dist(PVector v) { + float dx = x - v.x; + float dy = y - v.y; + float dz = z - v.z; + return (float) Math.sqrt(dx*dx + dy*dy + dz*dz); + } + + + /** + * @param v1 any variable of type PVector + * @param v2 any variable of type PVector + * @return the Euclidean distance between v1 and v2 + */ + static public float dist(PVector v1, PVector v2) { + float dx = v1.x - v2.x; + float dy = v1.y - v2.y; + float dz = v1.z - v2.z; + return (float) Math.sqrt(dx*dx + dy*dy + dz*dz); + } + + + /** + * ( begin auto-generated from PVector_dot.xml ) + * + * Calculates the dot product of two vectors. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @param v any variable of type PVector + * @return the dot product + * @brief Calculate the dot product of two vectors + */ + public float dot(PVector v) { + return x*v.x + y*v.y + z*v.z; + } + + + /** + * @param x x component of the vector + * @param y y component of the vector + * @param z z component of the vector + */ + public float dot(float x, float y, float z) { + return this.x*x + this.y*y + this.z*z; + } + + + /** + * @param v1 any variable of type PVector + * @param v2 any variable of type PVector + */ + static public float dot(PVector v1, PVector v2) { + return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z; + } + + + /** + * ( begin auto-generated from PVector_cross.xml ) + * + * Calculates and returns a vector composed of the cross product between + * two vectors. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @param v the vector to calculate the cross product + * @brief Calculate and return the cross product + */ + public PVector cross(PVector v) { + return cross(v, null); + } + + + /** + * @param v any variable of type PVector + * @param target PVector to store the result + */ + public PVector cross(PVector v, PVector target) { + float crossX = y * v.z - v.y * z; + float crossY = z * v.x - v.z * x; + float crossZ = x * v.y - v.x * y; + + if (target == null) { + target = new PVector(crossX, crossY, crossZ); + } else { + target.set(crossX, crossY, crossZ); + } + return target; + } + + + /** + * @param v1 any variable of type PVector + * @param v2 any variable of type PVector + * @param target PVector to store the result + */ + static public PVector cross(PVector v1, PVector v2, PVector target) { + float crossX = v1.y * v2.z - v2.y * v1.z; + float crossY = v1.z * v2.x - v2.z * v1.x; + float crossZ = v1.x * v2.y - v2.x * v1.y; + + if (target == null) { + target = new PVector(crossX, crossY, crossZ); + } else { + target.set(crossX, crossY, crossZ); + } + return target; + } + + + /** + * ( begin auto-generated from PVector_normalize.xml ) + * + * Normalize the vector to length 1 (make it a unit vector). + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @brief Normalize the vector to a length of 1 + */ + public PVector normalize() { + float m = mag(); + if (m != 0 && m != 1) { + div(m); + } + return this; + } + + + /** + * @param target Set to null to create a new vector + * @return a new vector (if target was null), or target + */ + public PVector normalize(PVector target) { + if (target == null) { + target = new PVector(); + } + float m = mag(); + if (m > 0) { + target.set(x/m, y/m, z/m); + } else { + target.set(x, y, z); + } + return target; + } + + + /** + * ( begin auto-generated from PVector_limit.xml ) + * + * Limit the magnitude of this vector to the value used for the max parameter. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @param max the maximum magnitude for the vector + * @brief Limit the magnitude of the vector + */ + public PVector limit(float max) { + if (magSq() > max*max) { + normalize(); + mult(max); + } + return this; + } + + + /** + * ( begin auto-generated from PVector_setMag.xml ) + * + * Set the magnitude of this vector to the value used for the len parameter. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @param len the new length for this vector + * @brief Set the magnitude of the vector + */ + public PVector setMag(float len) { + normalize(); + mult(len); + return this; + } + + + /** + * Sets the magnitude of this vector, storing the result in another vector. + * @param target Set to null to create a new vector + * @param len the new length for the new vector + * @return a new vector (if target was null), or target + */ + public PVector setMag(PVector target, float len) { + target = normalize(target); + target.mult(len); + return target; + } + + + /** + * ( begin auto-generated from PVector_setMag.xml ) + * + * Calculate the angle of rotation for this vector (only 2D vectors) + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @return the angle of rotation + * @brief Calculate the angle of rotation for this vector + */ + public float heading() { + float angle = (float) Math.atan2(y, x); + return angle; + } + + + @Deprecated + public float heading2D() { + return heading(); + } + + + /** + * ( begin auto-generated from PVector_rotate.xml ) + * + * Rotate the vector by an angle (only 2D vectors), magnitude remains the same + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @brief Rotate the vector by an angle (2D only) + * @param theta the angle of rotation + */ + public PVector rotate(float theta) { + float temp = x; + // Might need to check for rounding errors like with angleBetween function? + x = x*PApplet.cos(theta) - y*PApplet.sin(theta); + y = temp*PApplet.sin(theta) + y*PApplet.cos(theta); + return this; + } + + + /** + * ( begin auto-generated from PVector_rotate.xml ) + * + * Linear interpolate the vector to another vector + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @brief Linear interpolate the vector to another vector + * @param v the vector to lerp to + * @param amt The amount of interpolation; some value between 0.0 (old vector) and 1.0 (new vector). 0.1 is very near the old vector; 0.5 is halfway in between. + * @see PApplet#lerp(float, float, float) + */ + public PVector lerp(PVector v, float amt) { + x = PApplet.lerp(x, v.x, amt); + y = PApplet.lerp(y, v.y, amt); + z = PApplet.lerp(z, v.z, amt); + return this; + } + + + /** + * Linear interpolate between two vectors (returns a new PVector object) + * @param v1 the vector to start from + * @param v2 the vector to lerp to + */ + public static PVector lerp(PVector v1, PVector v2, float amt) { + PVector v = v1.copy(); + v.lerp(v2, amt); + return v; + } + + + /** + * Linear interpolate the vector to x,y,z values + * @param x the x component to lerp to + * @param y the y component to lerp to + * @param z the z component to lerp to + */ + public PVector lerp(float x, float y, float z, float amt) { + this.x = PApplet.lerp(this.x, x, amt); + this.y = PApplet.lerp(this.y, y, amt); + this.z = PApplet.lerp(this.z, z, amt); + return this; + } + + + /** + * ( begin auto-generated from PVector_angleBetween.xml ) + * + * Calculates and returns the angle (in radians) between two vectors. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage web_application + * @param v1 the x, y, and z components of a PVector + * @param v2 the x, y, and z components of a PVector + * @brief Calculate and return the angle between two vectors + */ + static public float angleBetween(PVector v1, PVector v2) { + + // We get NaN if we pass in a zero vector which can cause problems + // Zero seems like a reasonable angle between a (0,0,0) vector and something else + if (v1.x == 0 && v1.y == 0 && v1.z == 0 ) return 0.0f; + if (v2.x == 0 && v2.y == 0 && v2.z == 0 ) return 0.0f; + + double dot = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; + double v1mag = Math.sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z); + double v2mag = Math.sqrt(v2.x * v2.x + v2.y * v2.y + v2.z * v2.z); + // This should be a number between -1 and 1, since it's "normalized" + double amt = dot / (v1mag * v2mag); + // But if it's not due to rounding error, then we need to fix it + // http://code.google.com/p/processing/issues/detail?id=340 + // Otherwise if outside the range, acos() will return NaN + // http://www.cppreference.com/wiki/c/math/acos + if (amt <= -1) { + return PConstants.PI; + } else if (amt >= 1) { + // http://code.google.com/p/processing/issues/detail?id=435 + return 0; + } + return (float) Math.acos(amt); + } + + + @Override + public String toString() { + return "[ " + x + ", " + y + ", " + z + " ]"; + } + + + /** + * ( begin auto-generated from PVector_array.xml ) + * + * Return a representation of this vector as a float array. This is only + * for temporary use. If used in any other fashion, the contents should be + * copied by using the PVector.get() method to copy into your own array. + * + * ( end auto-generated ) + * + * @webref pvector:method + * @usage: web_application + * @brief Return a representation of the vector as a float array + */ + public float[] array() { + if (array == null) { + array = new float[3]; + } + array[0] = x; + array[1] = y; + array[2] = z; + return array; + } + + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PVector)) { + return false; + } + final PVector p = (PVector) obj; + return x == p.x && y == p.y && z == p.z; + } + + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + Float.floatToIntBits(x); + result = 31 * result + Float.floatToIntBits(y); + result = 31 * result + Float.floatToIntBits(z); + return result; + } +} diff --git a/src/main/java/processing/core/ThinkDifferent.java b/src/main/java/processing/core/ThinkDifferent.java new file mode 100644 index 0000000..8596696 --- /dev/null +++ b/src/main/java/processing/core/ThinkDifferent.java @@ -0,0 +1,96 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-2014 The Processing Foundation + Copyright (c) 2007-2012 Ben Fry and Casey Reas + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + version 2, as published by the Free Software Foundation. + + 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, write to the Free Software Foundation, + Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +package processing.core; + +import java.awt.Image; + +import com.apple.eawt.AppEvent.QuitEvent; +import com.apple.eawt.Application; +import com.apple.eawt.QuitHandler; +import com.apple.eawt.QuitResponse; + + +/** + * Deal with issues related to thinking differently. + * + * We have to register a quit handler to safely shut down the sketch, + * otherwise OS X will just kill the sketch when a user hits Cmd-Q. + * In addition, we have a method to set the dock icon image so we look more + * like a native application. + * + * This is a stripped-down version of what's in processing.app.platform to fix + * 3301. + */ +public class ThinkDifferent { + + // http://developer.apple.com/documentation/Java/Reference/1.4.2/appledoc/api/com/apple/eawt/Application.html + private static Application application; + + // True if user has tried to quit once. Prevents us from canceling the quit + // call if the sketch is held up for some reason, like an exception that's + // managed to put the sketch in a bad state. + static boolean attemptedQuit; + + + static public void init(final PApplet sketch) { + if (application == null) { + application = Application.getApplication(); + } + + application.setQuitHandler(new QuitHandler() { + public void handleQuitRequestWith(QuitEvent event, QuitResponse response) { + sketch.exit(); + if (PApplet.uncaughtThrowable == null && // no known crash + !attemptedQuit) { // haven't tried yet + response.cancelQuit(); // tell OS X we'll handle this + attemptedQuit = true; + } else { + response.performQuit(); // just force it this time + } + } + }); + } + + static public void cleanup() { + if (application == null) { + application = Application.getApplication(); + } + application.setQuitHandler(null); + } + + // Called via reflection from PSurfaceAWT and others + static public void setIconImage(Image image) { + // When already set, is a sun.awt.image.MultiResolutionCachedImage on OS X +// Image current = application.getDockIconImage(); +// System.out.println("current dock icon image is " + current); +// System.out.println("changing to " + image); + + application.setDockIconImage(image); + } + + + // Instead, just use Application.getApplication() inside your app +// static public Application getApplication() { +// return application; +// } +} diff --git a/src/main/java/processing/data/FloatDict.java b/src/main/java/processing/data/FloatDict.java new file mode 100644 index 0000000..fba7d4f --- /dev/null +++ b/src/main/java/processing/data/FloatDict.java @@ -0,0 +1,829 @@ +package processing.data; + +import java.io.*; +import java.util.HashMap; +import java.util.Iterator; + +import processing.core.PApplet; + + +/** + * A simple table class to use a String as a lookup for an float value. + * + * @webref data:composite + * @see IntDict + * @see StringDict + */ +public class FloatDict { + + /** Number of elements in the table */ + protected int count; + + protected String[] keys; + protected float[] values; + + /** Internal implementation for faster lookups */ + private HashMap indices = new HashMap<>(); + + + public FloatDict() { + count = 0; + keys = new String[10]; + values = new float[10]; + } + + + /** + * Create a new lookup with a specific size. This is more efficient than not + * specifying a size. Use it when you know the rough size of the thing you're creating. + * + * @nowebref + */ + public FloatDict(int length) { + count = 0; + keys = new String[length]; + values = new float[length]; + } + + + /** + * Read a set of entries from a Reader that has each key/value pair on + * a single line, separated by a tab. + * + * @nowebref + */ + public FloatDict(BufferedReader reader) { + String[] lines = PApplet.loadStrings(reader); + keys = new String[lines.length]; + values = new float[lines.length]; + + for (int i = 0; i < lines.length; i++) { + String[] pieces = PApplet.split(lines[i], '\t'); + if (pieces.length == 2) { + keys[count] = pieces[0]; + values[count] = PApplet.parseFloat(pieces[1]); + indices.put(pieces[0], count); + count++; + } + } + } + + + /** + * @nowebref + */ + public FloatDict(String[] keys, float[] values) { + if (keys.length != values.length) { + throw new IllegalArgumentException("key and value arrays must be the same length"); + } + this.keys = keys; + this.values = values; + count = keys.length; + for (int i = 0; i < count; i++) { + indices.put(keys[i], i); + } + } + + + /** + * Constructor to allow (more intuitive) inline initialization, e.g.: + *

        +   * new FloatDict(new Object[][] {
        +   *   { "key1", 1 },
        +   *   { "key2", 2 }
        +   * });
        +   * 
        + */ + public FloatDict(Object[][] pairs) { + count = pairs.length; + this.keys = new String[count]; + this.values = new float[count]; + for (int i = 0; i < count; i++) { + keys[i] = (String) pairs[i][0]; + values[i] = (Float) pairs[i][1]; + indices.put(keys[i], i); + } + } + + + /** + * @webref floatdict:method + * @brief Returns the number of key/value pairs + */ + public int size() { + return count; + } + + + /** + * Resize the internal data, this can only be used to shrink the list. + * Helpful for situations like sorting and then grabbing the top 50 entries. + */ + public void resize(int length) { + if (length == count) return; + + if (length > count) { + throw new IllegalArgumentException("resize() can only be used to shrink the dictionary"); + } + if (length < 1) { + throw new IllegalArgumentException("resize(" + length + ") is too small, use 1 or higher"); + } + + String[] newKeys = new String[length]; + float[] newValues = new float[length]; + PApplet.arrayCopy(keys, newKeys, length); + PApplet.arrayCopy(values, newValues, length); + keys = newKeys; + values = newValues; + count = length; + resetIndices(); + } + + + /** + * Remove all entries. + * + * @webref floatdict:method + * @brief Remove all entries + */ + public void clear() { + count = 0; + indices = new HashMap<>(); + } + + + private void resetIndices() { + indices = new HashMap<>(count); + for (int i = 0; i < count; i++) { + indices.put(keys[i], i); + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public class Entry { + public String key; + public float value; + + Entry(String key, float value) { + this.key = key; + this.value = value; + } + } + + + public Iterable entries() { + return new Iterable() { + + public Iterator iterator() { + return entryIterator(); + } + }; + } + + + public Iterator entryIterator() { + return new Iterator() { + int index = -1; + + public void remove() { + removeIndex(index); + index--; + } + + public Entry next() { + ++index; + Entry e = new Entry(keys[index], values[index]); + return e; + } + + public boolean hasNext() { + return index+1 < size(); + } + }; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public String key(int index) { + return keys[index]; + } + + + protected void crop() { + if (count != keys.length) { + keys = PApplet.subset(keys, 0, count); + values = PApplet.subset(values, 0, count); + } + } + + + public Iterable keys() { + return new Iterable() { + + @Override + public Iterator iterator() { + return keyIterator(); + } + }; + } + + + // Use this to iterate when you want to be able to remove elements along the way + public Iterator keyIterator() { + return new Iterator() { + int index = -1; + + public void remove() { + removeIndex(index); + index--; + } + + public String next() { + return key(++index); + } + + public boolean hasNext() { + return index+1 < size(); + } + }; + } + + + /** + * Return a copy of the internal keys array. This array can be modified. + * + * @webref floatdict:method + * @brief Return a copy of the internal keys array + */ + public String[] keyArray() { + crop(); + return keyArray(null); + } + + + public String[] keyArray(String[] outgoing) { + if (outgoing == null || outgoing.length != count) { + outgoing = new String[count]; + } + System.arraycopy(keys, 0, outgoing, 0, count); + return outgoing; + } + + + public float value(int index) { + return values[index]; + } + + + /** + * @webref floatdict:method + * @brief Return the internal array being used to store the values + */ + public Iterable values() { + return new Iterable() { + + @Override + public Iterator iterator() { + return valueIterator(); + } + }; + } + + + public Iterator valueIterator() { + return new Iterator() { + int index = -1; + + public void remove() { + removeIndex(index); + index--; + } + + public Float next() { + return value(++index); + } + + public boolean hasNext() { + return index+1 < size(); + } + }; + } + + + /** + * Create a new array and copy each of the values into it. + * + * @webref floatdict:method + * @brief Create a new array and copy each of the values into it + */ + public float[] valueArray() { + crop(); + return valueArray(null); + } + + + /** + * Fill an already-allocated array with the values (more efficient than + * creating a new array each time). If 'array' is null, or not the same + * size as the number of values, a new array will be allocated and returned. + */ + public float[] valueArray(float[] array) { + if (array == null || array.length != size()) { + array = new float[count]; + } + System.arraycopy(values, 0, array, 0, count); + return array; + } + + + /** + * Return a value for the specified key. + * + * @webref floatdict:method + * @brief Return a value for the specified key + */ + public float get(String key) { + int index = index(key); + if (index == -1) { + throw new IllegalArgumentException("No key named '" + key + "'"); + } + return values[index]; + } + + + public float get(String key, float alternate) { + int index = index(key); + if (index == -1) { + return alternate; + } + return values[index]; + } + + + /** + * @webref floatdict:method + * @brief Create a new key/value pair or change the value of one + */ + public void set(String key, float amount) { + int index = index(key); + if (index == -1) { + create(key, amount); + } else { + values[index] = amount; + } + } + + + public void setIndex(int index, String key, float value) { + if (index < 0 || index >= count) { + throw new ArrayIndexOutOfBoundsException(index); + } + keys[index] = key; + values[index] = value; + } + + + /** + * @webref floatdict:method + * @brief Check if a key is a part of the data structure + */ + public boolean hasKey(String key) { + return index(key) != -1; + } + + + /** + * @webref floatdict:method + * @brief Add to a value + */ + public void add(String key, float amount) { + int index = index(key); + if (index == -1) { + create(key, amount); + } else { + values[index] += amount; + } + } + + + /** + * @webref floatdict:method + * @brief Subtract from a value + */ + public void sub(String key, float amount) { + add(key, -amount); + } + + + /** + * @webref floatdict:method + * @brief Multiply a value + */ + public void mult(String key, float amount) { + int index = index(key); + if (index != -1) { + values[index] *= amount; + } + } + + + /** + * @webref floatdict:method + * @brief Divide a value + */ + public void div(String key, float amount) { + int index = index(key); + if (index != -1) { + values[index] /= amount; + } + } + + + private void checkMinMax(String functionName) { + if (count == 0) { + String msg = + String.format("Cannot use %s() on an empty %s.", + functionName, getClass().getSimpleName()); + throw new RuntimeException(msg); + } + } + + + /** + * @webref floatlist:method + * @brief Return the smallest value + */ + public int minIndex() { + //checkMinMax("minIndex"); + if (count == 0) return -1; + + // Will still return NaN if there are 1 or more entries, and they're all NaN + float m = Float.NaN; + int mi = -1; + for (int i = 0; i < count; i++) { + // find one good value to start + if (values[i] == values[i]) { + m = values[i]; + mi = i; + + // calculate the rest + for (int j = i+1; j < count; j++) { + float d = values[j]; + if ((d == d) && (d < m)) { + m = values[j]; + mi = j; + } + } + break; + } + } + return mi; + } + + + // return the key for the minimum value + public String minKey() { + checkMinMax("minKey"); + int index = minIndex(); + if (index == -1) { + return null; + } + return keys[index]; + } + + + // return the minimum value, or throw an error if there are no values + public float minValue() { + checkMinMax("minValue"); + int index = minIndex(); + if (index == -1) { + return Float.NaN; + } + return values[index]; + } + + + /** + * @webref floatlist:method + * @brief Return the largest value + */ + // The index of the entry that has the max value. Reference above is incorrect. + public int maxIndex() { + //checkMinMax("maxIndex"); + if (count == 0) { + return -1; + } + // Will still return NaN if there is 1 or more entries, and they're all NaN + float m = Float.NaN; + int mi = -1; + for (int i = 0; i < count; i++) { + // find one good value to start + if (values[i] == values[i]) { + m = values[i]; + mi = i; + + // calculate the rest + for (int j = i+1; j < count; j++) { + float d = values[j]; + if (!Float.isNaN(d) && (d > m)) { + m = values[j]; + mi = j; + } + } + break; + } + } + return mi; + } + + + /** The key for a max value; null if empty or everything is NaN (no max). */ + public String maxKey() { + //checkMinMax("maxKey"); + int index = maxIndex(); + if (index == -1) { + return null; + } + return keys[index]; + } + + + /** The max value. (Or NaN if no entries or they're all NaN.) */ + public float maxValue() { + //checkMinMax("maxValue"); + int index = maxIndex(); + if (index == -1) { + return Float.NaN; + } + return values[index]; + } + + + public float sum() { + double amount = sumDouble(); + if (amount > Float.MAX_VALUE) { + throw new RuntimeException("sum() exceeds " + Float.MAX_VALUE + ", use sumDouble()"); + } + if (amount < -Float.MAX_VALUE) { + throw new RuntimeException("sum() lower than " + -Float.MAX_VALUE + ", use sumDouble()"); + } + return (float) amount; + } + + + public double sumDouble() { + double sum = 0; + for (int i = 0; i < count; i++) { + sum += values[i]; + } + return sum; + } + + + public int index(String what) { + Integer found = indices.get(what); + return (found == null) ? -1 : found.intValue(); + } + + + protected void create(String what, float much) { + if (count == keys.length) { + keys = PApplet.expand(keys); + values = PApplet.expand(values); + } + indices.put(what, Integer.valueOf(count)); + keys[count] = what; + values[count] = much; + count++; + } + + + /** + * @webref floatdict:method + * @brief Remove a key/value pair + */ + public int remove(String key) { + int index = index(key); + if (index != -1) { + removeIndex(index); + } + return index; + } + + + public String removeIndex(int index) { + if (index < 0 || index >= count) { + throw new ArrayIndexOutOfBoundsException(index); + } + String key = keys[index]; + //System.out.println("index is " + which + " and " + keys[which]); + indices.remove(keys[index]); + for (int i = index; i < count-1; i++) { + keys[i] = keys[i+1]; + values[i] = values[i+1]; + indices.put(keys[i], i); + } + count--; + keys[count] = null; + values[count] = 0; + return key; + } + + + public void swap(int a, int b) { + String tkey = keys[a]; + float tvalue = values[a]; + keys[a] = keys[b]; + values[a] = values[b]; + keys[b] = tkey; + values[b] = tvalue; + +// indices.put(keys[a], Integer.valueOf(a)); +// indices.put(keys[b], Integer.valueOf(b)); + } + + + /** + * Sort the keys alphabetically (ignoring case). Uses the value as a + * tie-breaker (only really possible with a key that has a case change). + * + * @webref floatdict:method + * @brief Sort the keys alphabetically + */ + public void sortKeys() { + sortImpl(true, false, true); + } + + + /** + * @webref floatdict:method + * @brief Sort the keys alphabetically in reverse + */ + public void sortKeysReverse() { + sortImpl(true, true, true); + } + + + /** + * Sort by values in descending order (largest value will be at [0]). + * + * @webref floatdict:method + * @brief Sort by values in ascending order + */ + public void sortValues() { + sortValues(true); + } + + + /** + * Set true to ensure that the order returned is identical. Slightly + * slower because the tie-breaker for identical values compares the keys. + * @param stable + */ + public void sortValues(boolean stable) { + sortImpl(false, false, stable); + } + + + /** + * @webref floatdict:method + * @brief Sort by values in descending order + */ + public void sortValuesReverse() { + sortValuesReverse(true); + } + + + public void sortValuesReverse(boolean stable) { + sortImpl(false, true, stable); + } + + + protected void sortImpl(final boolean useKeys, final boolean reverse, + final boolean stable) { + Sort s = new Sort() { + @Override + public int size() { + if (useKeys) { + return count; // don't worry about NaN values + + } else if (count == 0) { // skip the NaN check, it'll AIOOBE + return 0; + + } else { // first move NaN values to the end of the list + int right = count - 1; + while (values[right] != values[right]) { + right--; + if (right == -1) { + return 0; // all values are NaN + } + } + for (int i = right; i >= 0; --i) { + if (Float.isNaN(values[i])) { + swap(i, right); + --right; + } + } + return right + 1; + } + } + + @Override + public float compare(int a, int b) { + float diff = 0; + if (useKeys) { + diff = keys[a].compareToIgnoreCase(keys[b]); + if (diff == 0) { + diff = values[a] - values[b]; + } + } else { // sort values + diff = values[a] - values[b]; + if (diff == 0 && stable) { + diff = keys[a].compareToIgnoreCase(keys[b]); + } + } + return reverse ? -diff : diff; + } + + @Override + public void swap(int a, int b) { + FloatDict.this.swap(a, b); + } + }; + s.run(); + + // Set the indices after sort/swaps (performance fix 160411) + resetIndices(); + } + + + /** + * Sum all of the values in this dictionary, then return a new FloatDict of + * each key, divided by the total sum. The total for all values will be ~1.0. + * @return a FloatDict with the original keys, mapped to their pct of the total + */ + public FloatDict getPercent() { + double sum = sum(); + FloatDict outgoing = new FloatDict(); + for (int i = 0; i < size(); i++) { + double percent = value(i) / sum; + outgoing.set(key(i), (float) percent); + } + return outgoing; + } + + + /** Returns a duplicate copy of this object. */ + public FloatDict copy() { + FloatDict outgoing = new FloatDict(count); + System.arraycopy(keys, 0, outgoing.keys, 0, count); + System.arraycopy(values, 0, outgoing.values, 0, count); + for (int i = 0; i < count; i++) { + outgoing.indices.put(keys[i], i); + } + outgoing.count = count; + return outgoing; + } + + + public void print() { + for (int i = 0; i < size(); i++) { + System.out.println(keys[i] + " = " + values[i]); + } + } + + + /** + * Write tab-delimited entries out to + * @param writer + */ + public void write(PrintWriter writer) { + for (int i = 0; i < count; i++) { + writer.println(keys[i] + "\t" + values[i]); + } + writer.flush(); + } + + + /** + * Return this dictionary as a String in JSON format. + */ + public String toJSON() { + StringList items = new StringList(); + for (int i = 0; i < count; i++) { + items.append(JSONObject.quote(keys[i])+ ": " + values[i]); + } + return "{ " + items.join(", ") + " }"; + } + + + @Override + public String toString() { + return getClass().getSimpleName() + " size=" + size() + " " + toJSON(); + } +} diff --git a/src/main/java/processing/data/FloatList.java b/src/main/java/processing/data/FloatList.java new file mode 100644 index 0000000..a8d190e --- /dev/null +++ b/src/main/java/processing/data/FloatList.java @@ -0,0 +1,912 @@ +package processing.data; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Random; + +import processing.core.PApplet; + + +/** + * Helper class for a list of floats. Lists are designed to have some of the + * features of ArrayLists, but to maintain the simplicity and efficiency of + * working with arrays. + * + * Functions like sort() and shuffle() always act on the list itself. To get + * a sorted copy, use list.copy().sort(). + * + * @webref data:composite + * @see IntList + * @see StringList + */ +public class FloatList implements Iterable { + int count; + float[] data; + + + public FloatList() { + data = new float[10]; + } + + + /** + * @nowebref + */ + public FloatList(int length) { + data = new float[length]; + } + + + /** + * @nowebref + */ + public FloatList(float[] list) { + count = list.length; + data = new float[count]; + System.arraycopy(list, 0, data, 0, count); + } + + + /** + * Construct an FloatList from an iterable pile of objects. + * For instance, a float array, an array of strings, who knows). + * Un-parseable or null values will be set to NaN. + * @nowebref + */ + public FloatList(Iterable iter) { + this(10); + for (Object o : iter) { + if (o == null) { + append(Float.NaN); + } else if (o instanceof Number) { + append(((Number) o).floatValue()); + } else { + append(PApplet.parseFloat(o.toString().trim())); + } + } + crop(); + } + + + /** + * Construct an FloatList from a random pile of objects. + * Un-parseable or null values will be set to NaN. + */ + public FloatList(Object... items) { + // nuts, no good way to pass missingValue to this fn (varargs must be last) + final float missingValue = Float.NaN; + + count = items.length; + data = new float[count]; + int index = 0; + for (Object o : items) { + float value = missingValue; + if (o != null) { + if (o instanceof Number) { + value = ((Number) o).floatValue(); + } else { + value = PApplet.parseFloat(o.toString().trim(), missingValue); + } + } + data[index++] = value; + } + } + + + /** + * Improve efficiency by removing allocated but unused entries from the + * internal array used to store the data. Set to private, though it could + * be useful to have this public if lists are frequently making drastic + * size changes (from very large to very small). + */ + private void crop() { + if (count != data.length) { + data = PApplet.subset(data, 0, count); + } + } + + + /** + * Get the length of the list. + * + * @webref floatlist:method + * @brief Get the length of the list + */ + public int size() { + return count; + } + + + public void resize(int length) { + if (length > data.length) { + float[] temp = new float[length]; + System.arraycopy(data, 0, temp, 0, count); + data = temp; + + } else if (length > count) { + Arrays.fill(data, count, length, 0); + } + count = length; + } + + + /** + * Remove all entries from the list. + * + * @webref floatlist:method + * @brief Remove all entries from the list + */ + public void clear() { + count = 0; + } + + + /** + * Get an entry at a particular index. + * + * @webref floatlist:method + * @brief Get an entry at a particular index + */ + public float get(int index) { + if (index >= count) { + throw new ArrayIndexOutOfBoundsException(index); + } + return data[index]; + } + + + /** + * Set the entry at a particular index. If the index is past the length of + * the list, it'll expand the list to accommodate, and fill the intermediate + * entries with 0s. + * + * @webref floatlist:method + * @brief Set the entry at a particular index + */ + public void set(int index, float what) { + if (index >= count) { + data = PApplet.expand(data, index+1); + for (int i = count; i < index; i++) { + data[i] = 0; + } + count = index+1; + } + data[index] = what; + } + + + /** Just an alias for append(), but matches pop() */ + public void push(float value) { + append(value); + } + + + public float pop() { + if (count == 0) { + throw new RuntimeException("Can't call pop() on an empty list"); + } + float value = get(count-1); + count--; + return value; + } + + + /** + * Remove an element from the specified index. + * + * @webref floatlist:method + * @brief Remove an element from the specified index + */ + public float remove(int index) { + if (index < 0 || index >= count) { + throw new ArrayIndexOutOfBoundsException(index); + } + float entry = data[index]; +// int[] outgoing = new int[count - 1]; +// System.arraycopy(data, 0, outgoing, 0, index); +// count--; +// System.arraycopy(data, index + 1, outgoing, 0, count - index); +// data = outgoing; + // For most cases, this actually appears to be faster + // than arraycopy() on an array copying into itself. + for (int i = index; i < count-1; i++) { + data[i] = data[i+1]; + } + count--; + return entry; + } + + + // Remove the first instance of a particular value, + // and return the index at which it was found. + public int removeValue(int value) { + int index = index(value); + if (index != -1) { + remove(index); + return index; + } + return -1; + } + + + // Remove all instances of a particular value, + // and return the number of values found and removed + public int removeValues(int value) { + int ii = 0; + if (Float.isNaN(value)) { + for (int i = 0; i < count; i++) { + if (!Float.isNaN(data[i])) { + data[ii++] = data[i]; + } + } + } else { + for (int i = 0; i < count; i++) { + if (data[i] != value) { + data[ii++] = data[i]; + } + } + } + int removed = count - ii; + count = ii; + return removed; + } + + + /** Replace the first instance of a particular value */ + public boolean replaceValue(float value, float newValue) { + if (Float.isNaN(value)) { + for (int i = 0; i < count; i++) { + if (Float.isNaN(data[i])) { + data[i] = newValue; + return true; + } + } + } else { + int index = index(value); + if (index != -1) { + data[index] = newValue; + return true; + } + } + return false; + } + + + /** Replace all instances of a particular value */ + public boolean replaceValues(float value, float newValue) { + boolean changed = false; + if (Float.isNaN(value)) { + for (int i = 0; i < count; i++) { + if (Float.isNaN(data[i])) { + data[i] = newValue; + changed = true; + } + } + } else { + for (int i = 0; i < count; i++) { + if (data[i] == value) { + data[i] = newValue; + changed = true; + } + } + } + return changed; + } + + + + /** + * Add a new entry to the list. + * + * @webref floatlist:method + * @brief Add a new entry to the list + */ + public void append(float value) { + if (count == data.length) { + data = PApplet.expand(data); + } + data[count++] = value; + } + + + public void append(float[] values) { + for (float v : values) { + append(v); + } + } + + + public void append(FloatList list) { + for (float v : list.values()) { // will concat the list... + append(v); + } + } + + + /** Add this value, but only if it's not already in the list. */ + public void appendUnique(float value) { + if (!hasValue(value)) { + append(value); + } + } + + +// public void insert(int index, int value) { +// if (index+1 > count) { +// if (index+1 < data.length) { +// } +// } +// if (index >= data.length) { +// data = PApplet.expand(data, index+1); +// data[index] = value; +// count = index+1; +// +// } else if (count == data.length) { +// if (index >= count) { +// //int[] temp = new int[count << 1]; +// System.arraycopy(data, 0, temp, 0, index); +// temp[index] = value; +// System.arraycopy(data, index, temp, index+1, count - index); +// data = temp; +// +// } else { +// // data[] has room to grow +// // for() loop believed to be faster than System.arraycopy over itself +// for (int i = count; i > index; --i) { +// data[i] = data[i-1]; +// } +// data[index] = value; +// count++; +// } +// } + + + public void insert(int index, float value) { + insert(index, new float[] { value }); + } + + + // same as splice + public void insert(int index, float[] values) { + if (index < 0) { + throw new IllegalArgumentException("insert() index cannot be negative: it was " + index); + } + if (index >= data.length) { + throw new IllegalArgumentException("insert() index " + index + " is past the end of this list"); + } + + float[] temp = new float[count + values.length]; + + // Copy the old values, but not more than already exist + System.arraycopy(data, 0, temp, 0, Math.min(count, index)); + + // Copy the new values into the proper place + System.arraycopy(values, 0, temp, index, values.length); + +// if (index < count) { + // The index was inside count, so it's a true splice/insert + System.arraycopy(data, index, temp, index+values.length, count - index); + count = count + values.length; +// } else { +// // The index was past 'count', so the new count is weirder +// count = index + values.length; +// } + data = temp; + } + + + public void insert(int index, FloatList list) { + insert(index, list.values()); + } + + + // below are aborted attempts at more optimized versions of the code + // that are harder to read and debug... + +// if (index + values.length >= count) { +// // We're past the current 'count', check to see if we're still allocated +// // index 9, data.length = 10, values.length = 1 +// if (index + values.length < data.length) { +// // There's still room for these entries, even though it's past 'count'. +// // First clear out the entries leading up to it, however. +// for (int i = count; i < index; i++) { +// data[i] = 0; +// } +// data[index] = +// } +// if (index >= data.length) { +// int length = index + values.length; +// int[] temp = new int[length]; +// System.arraycopy(data, 0, temp, 0, count); +// System.arraycopy(values, 0, temp, index, values.length); +// data = temp; +// count = data.length; +// } else { +// +// } +// +// } else if (count == data.length) { +// int[] temp = new int[count << 1]; +// System.arraycopy(data, 0, temp, 0, index); +// temp[index] = value; +// System.arraycopy(data, index, temp, index+1, count - index); +// data = temp; +// +// } else { +// // data[] has room to grow +// // for() loop believed to be faster than System.arraycopy over itself +// for (int i = count; i > index; --i) { +// data[i] = data[i-1]; +// } +// data[index] = value; +// count++; +// } + + + /** Return the first index of a particular value. */ + public int index(float what) { + /* + if (indexCache != null) { + try { + return indexCache.get(what); + } catch (Exception e) { // not there + return -1; + } + } + */ + for (int i = 0; i < count; i++) { + if (data[i] == what) { + return i; + } + } + return -1; + } + + + /** + * @webref floatlist:method + * @brief Check if a number is a part of the list + */ + public boolean hasValue(float value) { + if (Float.isNaN(value)) { + for (int i = 0; i < count; i++) { + if (Float.isNaN(data[i])) { + return true; + } + } + } else { + for (int i = 0; i < count; i++) { + if (data[i] == value) { + return true; + } + } + } + return false; + } + + + private void boundsProblem(int index, String method) { + final String msg = String.format("The list size is %d. " + + "You cannot %s() to element %d.", count, method, index); + throw new ArrayIndexOutOfBoundsException(msg); + } + + + /** + * @webref floatlist:method + * @brief Add to a value + */ + public void add(int index, float amount) { + if (index < count) { + data[index] += amount; + } else { + boundsProblem(index, "add"); + } + } + + + /** + * @webref floatlist:method + * @brief Subtract from a value + */ + public void sub(int index, float amount) { + if (index < count) { + data[index] -= amount; + } else { + boundsProblem(index, "sub"); + } + } + + + /** + * @webref floatlist:method + * @brief Multiply a value + */ + public void mult(int index, float amount) { + if (index < count) { + data[index] *= amount; + } else { + boundsProblem(index, "mult"); + } + } + + + /** + * @webref floatlist:method + * @brief Divide a value + */ + public void div(int index, float amount) { + if (index < count) { + data[index] /= amount; + } else { + boundsProblem(index, "div"); + } + } + + + private void checkMinMax(String functionName) { + if (count == 0) { + String msg = + String.format("Cannot use %s() on an empty %s.", + functionName, getClass().getSimpleName()); + throw new RuntimeException(msg); + } + } + + + /** + * @webref floatlist:method + * @brief Return the smallest value + */ + public float min() { + checkMinMax("min"); + int index = minIndex(); + return index == -1 ? Float.NaN : data[index]; + } + + + public int minIndex() { + checkMinMax("minIndex"); + float m = Float.NaN; + int mi = -1; + for (int i = 0; i < count; i++) { + // find one good value to start + if (data[i] == data[i]) { + m = data[i]; + mi = i; + + // calculate the rest + for (int j = i+1; j < count; j++) { + float d = data[j]; + if (!Float.isNaN(d) && (d < m)) { + m = data[j]; + mi = j; + } + } + break; + } + } + return mi; + } + + + /** + * @webref floatlist:method + * @brief Return the largest value + */ + public float max() { + checkMinMax("max"); + int index = maxIndex(); + return index == -1 ? Float.NaN : data[index]; + } + + + public int maxIndex() { + checkMinMax("maxIndex"); + float m = Float.NaN; + int mi = -1; + for (int i = 0; i < count; i++) { + // find one good value to start + if (data[i] == data[i]) { + m = data[i]; + mi = i; + + // calculate the rest + for (int j = i+1; j < count; j++) { + float d = data[j]; + if (!Float.isNaN(d) && (d > m)) { + m = data[j]; + mi = j; + } + } + break; + } + } + return mi; + } + + + public float sum() { + double amount = sumDouble(); + if (amount > Float.MAX_VALUE) { + throw new RuntimeException("sum() exceeds " + Float.MAX_VALUE + ", use sumDouble()"); + } + if (amount < -Float.MAX_VALUE) { + throw new RuntimeException("sum() lower than " + -Float.MAX_VALUE + ", use sumDouble()"); + } + return (float) amount; + } + + + public double sumDouble() { + double sum = 0; + for (int i = 0; i < count; i++) { + sum += data[i]; + } + return sum; + } + + + /** + * Sorts the array in place. + * + * @webref floatlist:method + * @brief Sorts an array, lowest to highest + */ + public void sort() { + Arrays.sort(data, 0, count); + } + + + /** + * Reverse sort, orders values from highest to lowest + * + * @webref floatlist:method + * @brief Reverse sort, orders values from highest to lowest + */ + public void sortReverse() { + new Sort() { + @Override + public int size() { + // if empty, don't even mess with the NaN check, it'll AIOOBE + if (count == 0) { + return 0; + } + // move NaN values to the end of the list and don't sort them + int right = count - 1; + while (data[right] != data[right]) { + right--; + if (right == -1) { // all values are NaN + return 0; + } + } + for (int i = right; i >= 0; --i) { + float v = data[i]; + if (v != v) { + data[i] = data[right]; + data[right] = v; + --right; + } + } + return right + 1; + } + + @Override + public float compare(int a, int b) { + return data[b] - data[a]; + } + + @Override + public void swap(int a, int b) { + float temp = data[a]; + data[a] = data[b]; + data[b] = temp; + } + }.run(); + } + + + // use insert() +// public void splice(int index, int value) { +// } + + +// public void subset(int start) { +// subset(start, count - start); +// } + + +// public void subset(int start, int num) { +// for (int i = 0; i < num; i++) { +// data[i] = data[i+start]; +// } +// count = num; +// } + + + /** + * @webref floatlist:method + * @brief Reverse the order of the list elements + */ + public void reverse() { + int ii = count - 1; + for (int i = 0; i < count/2; i++) { + float t = data[i]; + data[i] = data[ii]; + data[ii] = t; + --ii; + } + } + + + /** + * Randomize the order of the list elements. Note that this does not + * obey the randomSeed() function in PApplet. + * + * @webref floatlist:method + * @brief Randomize the order of the list elements + */ + public void shuffle() { + Random r = new Random(); + int num = count; + while (num > 1) { + int value = r.nextInt(num); + num--; + float temp = data[num]; + data[num] = data[value]; + data[value] = temp; + } + } + + + /** + * Randomize the list order using the random() function from the specified + * sketch, allowing shuffle() to use its current randomSeed() setting. + */ + public void shuffle(PApplet sketch) { + int num = count; + while (num > 1) { + int value = (int) sketch.random(num); + num--; + float temp = data[num]; + data[num] = data[value]; + data[value] = temp; + } + } + + + public FloatList copy() { + FloatList outgoing = new FloatList(data); + outgoing.count = count; + return outgoing; + } + + + /** + * Returns the actual array being used to store the data. For advanced users, + * this is the fastest way to access a large list. Suitable for iterating + * with a for() loop, but modifying the list will have terrible consequences. + */ + public float[] values() { + crop(); + return data; + } + + + /** Implemented this way so that we can use a FloatList in a for loop. */ + @Override + public Iterator iterator() { +// } +// +// +// public Iterator valueIterator() { + return new Iterator() { + int index = -1; + + public void remove() { + FloatList.this.remove(index); + index--; + } + + public Float next() { + return data[++index]; + } + + public boolean hasNext() { + return index+1 < count; + } + }; + } + + + /** + * Create a new array with a copy of all the values. + * @return an array sized by the length of the list with each of the values. + * @webref floatlist:method + * @brief Create a new array with a copy of all the values + */ + public float[] array() { + return array(null); + } + + + /** + * Copy values into the specified array. If the specified array is null or + * not the same size, a new array will be allocated. + * @param array + */ + public float[] array(float[] array) { + if (array == null || array.length != count) { + array = new float[count]; + } + System.arraycopy(data, 0, array, 0, count); + return array; + } + + + /** + * Returns a normalized version of this array. Called getPercent() for + * consistency with the Dict classes. It's a getter method because it needs + * to returns a new list (because IntList/Dict can't do percentages or + * normalization in place on int values). + */ + public FloatList getPercent() { + double sum = 0; + for (float value : array()) { + sum += value; + } + FloatList outgoing = new FloatList(count); + for (int i = 0; i < count; i++) { + double percent = data[i] / sum; + outgoing.set(i, (float) percent); + } + return outgoing; + } + + + public FloatList getSubset(int start) { + return getSubset(start, count - start); + } + + + public FloatList getSubset(int start, int num) { + float[] subset = new float[num]; + System.arraycopy(data, start, subset, 0, num); + return new FloatList(subset); + } + + + public String join(String separator) { + if (count == 0) { + return ""; + } + StringBuilder sb = new StringBuilder(); + sb.append(data[0]); + for (int i = 1; i < count; i++) { + sb.append(separator); + sb.append(data[i]); + } + return sb.toString(); + } + + + public void print() { + for (int i = 0; i < count; i++) { + System.out.format("[%d] %f%n", i, data[i]); + } + } + + + /** + * Return this dictionary as a String in JSON format. + */ + public String toJSON() { + return "[ " + join(", ") + " ]"; + } + + + @Override + public String toString() { + return getClass().getSimpleName() + " size=" + size() + " " + toJSON(); + } +} diff --git a/src/main/java/processing/data/IntDict.java b/src/main/java/processing/data/IntDict.java new file mode 100644 index 0000000..4b0c0b4 --- /dev/null +++ b/src/main/java/processing/data/IntDict.java @@ -0,0 +1,796 @@ +package processing.data; + +import java.io.*; +import java.util.HashMap; +import java.util.Iterator; + +import processing.core.PApplet; + + +/** + * A simple class to use a String as a lookup for an int value. + * + * @webref data:composite + * @see FloatDict + * @see StringDict + */ +public class IntDict { + + /** Number of elements in the table */ + protected int count; + + protected String[] keys; + protected int[] values; + + /** Internal implementation for faster lookups */ + private HashMap indices = new HashMap<>(); + + + public IntDict() { + count = 0; + keys = new String[10]; + values = new int[10]; + } + + + /** + * Create a new lookup with a specific size. This is more efficient than not + * specifying a size. Use it when you know the rough size of the thing you're creating. + * + * @nowebref + */ + public IntDict(int length) { + count = 0; + keys = new String[length]; + values = new int[length]; + } + + + /** + * Read a set of entries from a Reader that has each key/value pair on + * a single line, separated by a tab. + * + * @nowebref + */ + public IntDict(BufferedReader reader) { + String[] lines = PApplet.loadStrings(reader); + keys = new String[lines.length]; + values = new int[lines.length]; + + for (int i = 0; i < lines.length; i++) { + String[] pieces = PApplet.split(lines[i], '\t'); + if (pieces.length == 2) { + keys[count] = pieces[0]; + values[count] = PApplet.parseInt(pieces[1]); + indices.put(pieces[0], count); + count++; + } + } + } + + /** + * @nowebref + */ + public IntDict(String[] keys, int[] values) { + if (keys.length != values.length) { + throw new IllegalArgumentException("key and value arrays must be the same length"); + } + this.keys = keys; + this.values = values; + count = keys.length; + for (int i = 0; i < count; i++) { + indices.put(keys[i], i); + } + } + + + /** + * Constructor to allow (more intuitive) inline initialization, e.g.: + *
        +   * new FloatDict(new Object[][] {
        +   *   { "key1", 1 },
        +   *   { "key2", 2 }
        +   * });
        +   * 
        + */ + public IntDict(Object[][] pairs) { + count = pairs.length; + this.keys = new String[count]; + this.values = new int[count]; + for (int i = 0; i < count; i++) { + keys[i] = (String) pairs[i][0]; + values[i] = (Integer) pairs[i][1]; + indices.put(keys[i], i); + } + } + + + /** + * Returns the number of key/value pairs + * + * @webref intdict:method + * @brief Returns the number of key/value pairs + */ + public int size() { + return count; + } + + + /** + * Resize the internal data, this can only be used to shrink the list. + * Helpful for situations like sorting and then grabbing the top 50 entries. + */ + public void resize(int length) { + if (length > count) { + throw new IllegalArgumentException("resize() can only be used to shrink the dictionary"); + } + if (length < 1) { + throw new IllegalArgumentException("resize(" + length + ") is too small, use 1 or higher"); + } + + String[] newKeys = new String[length]; + int[] newValues = new int[length]; + PApplet.arrayCopy(keys, newKeys, length); + PApplet.arrayCopy(values, newValues, length); + keys = newKeys; + values = newValues; + count = length; + resetIndices(); + } + + + /** + * Remove all entries. + * + * @webref intdict:method + * @brief Remove all entries + */ + public void clear() { + count = 0; + indices = new HashMap<>(); + } + + + private void resetIndices() { + indices = new HashMap<>(count); + for (int i = 0; i < count; i++) { + indices.put(keys[i], i); + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public class Entry { + public String key; + public int value; + + Entry(String key, int value) { + this.key = key; + this.value = value; + } + } + + + public Iterable entries() { + return new Iterable() { + + public Iterator iterator() { + return entryIterator(); + } + }; + } + + + public Iterator entryIterator() { + return new Iterator() { + int index = -1; + + public void remove() { + removeIndex(index); + index--; + } + + public Entry next() { + ++index; + Entry e = new Entry(keys[index], values[index]); + return e; + } + + public boolean hasNext() { + return index+1 < size(); + } + }; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public String key(int index) { + return keys[index]; + } + + + protected void crop() { + if (count != keys.length) { + keys = PApplet.subset(keys, 0, count); + values = PApplet.subset(values, 0, count); + } + } + + + public Iterable keys() { + return new Iterable() { + + @Override + public Iterator iterator() { + return keyIterator(); + } + }; + } + + + // Use this to iterate when you want to be able to remove elements along the way + public Iterator keyIterator() { + return new Iterator() { + int index = -1; + + public void remove() { + removeIndex(index); + index--; + } + + public String next() { + return key(++index); + } + + public boolean hasNext() { + return index+1 < size(); + } + }; + } + + + /** + * Return a copy of the internal keys array. This array can be modified. + * + * @webref intdict:method + * @brief Return a copy of the internal keys array + */ + public String[] keyArray() { + crop(); + return keyArray(null); + } + + + public String[] keyArray(String[] outgoing) { + if (outgoing == null || outgoing.length != count) { + outgoing = new String[count]; + } + System.arraycopy(keys, 0, outgoing, 0, count); + return outgoing; + } + + + public int value(int index) { + return values[index]; + } + + + /** + * @webref intdict:method + * @brief Return the internal array being used to store the values + */ + public Iterable values() { + return new Iterable() { + + @Override + public Iterator iterator() { + return valueIterator(); + } + }; + } + + + public Iterator valueIterator() { + return new Iterator() { + int index = -1; + + public void remove() { + removeIndex(index); + index--; + } + + public Integer next() { + return value(++index); + } + + public boolean hasNext() { + return index+1 < size(); + } + }; + } + + + /** + * Create a new array and copy each of the values into it. + * + * @webref intdict:method + * @brief Create a new array and copy each of the values into it + */ + public int[] valueArray() { + crop(); + return valueArray(null); + } + + + /** + * Fill an already-allocated array with the values (more efficient than + * creating a new array each time). If 'array' is null, or not the same + * size as the number of values, a new array will be allocated and returned. + * + * @param array values to copy into the array + */ + public int[] valueArray(int[] array) { + if (array == null || array.length != size()) { + array = new int[count]; + } + System.arraycopy(values, 0, array, 0, count); + return array; + } + + + /** + * Return a value for the specified key. + * + * @webref intdict:method + * @brief Return a value for the specified key + */ + public int get(String key) { + int index = index(key); + if (index == -1) { + throw new IllegalArgumentException("No key named '" + key + "'"); + } + return values[index]; + } + + + public int get(String key, int alternate) { + int index = index(key); + if (index == -1) return alternate; + return values[index]; + } + + + /** + * Create a new key/value pair or change the value of one. + * + * @webref intdict:method + * @brief Create a new key/value pair or change the value of one + */ + public void set(String key, int amount) { + int index = index(key); + if (index == -1) { + create(key, amount); + } else { + values[index] = amount; + } + } + + + public void setIndex(int index, String key, int value) { + if (index < 0 || index >= count) { + throw new ArrayIndexOutOfBoundsException(index); + } + keys[index] = key; + values[index] = value; + } + + + /** + * @webref intdict:method + * @brief Check if a key is a part of the data structure + */ + public boolean hasKey(String key) { + return index(key) != -1; + } + + + /** + * Increase the value associated with a specific key by 1. + * + * @webref intdict:method + * @brief Increase the value of a specific key value by 1 + */ + public void increment(String key) { + add(key, 1); + } + + + /** + * Merge another dictionary into this one. Calling this increment() + * since it doesn't make sense in practice for the other dictionary types, + * even though it's technically an add(). + */ + public void increment(IntDict dict) { + for (int i = 0; i < dict.count; i++) { + add(dict.key(i), dict.value(i)); + } + } + + + /** + * @webref intdict:method + * @brief Add to a value + */ + public void add(String key, int amount) { + int index = index(key); + if (index == -1) { + create(key, amount); + } else { + values[index] += amount; + } + } + + + /** + * @webref intdict:method + * @brief Subtract from a value + */ + public void sub(String key, int amount) { + add(key, -amount); + } + + + /** + * @webref intdict:method + * @brief Multiply a value + */ + public void mult(String key, int amount) { + int index = index(key); + if (index != -1) { + values[index] *= amount; + } + } + + + /** + * @webref intdict:method + * @brief Divide a value + */ + public void div(String key, int amount) { + int index = index(key); + if (index != -1) { + values[index] /= amount; + } + } + + + private void checkMinMax(String functionName) { + if (count == 0) { + String msg = + String.format("Cannot use %s() on an empty %s.", + functionName, getClass().getSimpleName()); + throw new RuntimeException(msg); + } + } + + + // return the index of the minimum value + public int minIndex() { + //checkMinMax("minIndex"); + if (count == 0) return -1; + + int index = 0; + int value = values[0]; + for (int i = 1; i < count; i++) { + if (values[i] < value) { + index = i; + value = values[i]; + } + } + return index; + } + + + // return the key for the minimum value + public String minKey() { + checkMinMax("minKey"); + int index = minIndex(); + if (index == -1) { + return null; + } + return keys[index]; + } + + + // return the minimum value, or throw an error if there are no values + public int minValue() { + checkMinMax("minValue"); + return values[minIndex()]; + } + + + // return the index of the max value + public int maxIndex() { + //checkMinMax("maxIndex"); + if (count == 0) { + return -1; + } + int index = 0; + int value = values[0]; + for (int i = 1; i < count; i++) { + if (values[i] > value) { + index = i; + value = values[i]; + } + } + return index; + } + + + /** return the key corresponding to the maximum value or null if no entries */ + public String maxKey() { + //checkMinMax("maxKey"); + int index = maxIndex(); + if (index == -1) { + return null; + } + return keys[index]; + } + + + // return the maximum value or throw an error if zero length + public int maxValue() { + checkMinMax("maxIndex"); + return values[maxIndex()]; + } + + + public int sum() { + long amount = sumLong(); + if (amount > Integer.MAX_VALUE) { + throw new RuntimeException("sum() exceeds " + Integer.MAX_VALUE + ", use sumLong()"); + } + if (amount < Integer.MIN_VALUE) { + throw new RuntimeException("sum() less than " + Integer.MIN_VALUE + ", use sumLong()"); + } + return (int) amount; + } + + + public long sumLong() { + long sum = 0; + for (int i = 0; i < count; i++) { + sum += values[i]; + } + return sum; + } + + + public int index(String what) { + Integer found = indices.get(what); + return (found == null) ? -1 : found.intValue(); + } + + + protected void create(String what, int much) { + if (count == keys.length) { + keys = PApplet.expand(keys); + values = PApplet.expand(values); + } + indices.put(what, Integer.valueOf(count)); + keys[count] = what; + values[count] = much; + count++; + } + + /** + * @webref intdict:method + * @brief Remove a key/value pair + */ + public int remove(String key) { + int index = index(key); + if (index != -1) { + removeIndex(index); + } + return index; + } + + + public String removeIndex(int index) { + if (index < 0 || index >= count) { + throw new ArrayIndexOutOfBoundsException(index); + } + //System.out.println("index is " + which + " and " + keys[which]); + String key = keys[index]; + indices.remove(keys[index]); + for (int i = index; i < count-1; i++) { + keys[i] = keys[i+1]; + values[i] = values[i+1]; + indices.put(keys[i], i); + } + count--; + keys[count] = null; + values[count] = 0; + return key; + } + + + public void swap(int a, int b) { + String tkey = keys[a]; + int tvalue = values[a]; + keys[a] = keys[b]; + values[a] = values[b]; + keys[b] = tkey; + values[b] = tvalue; + +// indices.put(keys[a], Integer.valueOf(a)); +// indices.put(keys[b], Integer.valueOf(b)); + } + + + /** + * Sort the keys alphabetically (ignoring case). Uses the value as a + * tie-breaker (only really possible with a key that has a case change). + * + * @webref intdict:method + * @brief Sort the keys alphabetically + */ + public void sortKeys() { + sortImpl(true, false, true); + } + + /** + * Sort the keys alphabetically in reverse (ignoring case). Uses the value as a + * tie-breaker (only really possible with a key that has a case change). + * + * @webref intdict:method + * @brief Sort the keys alphabetically in reverse + */ + public void sortKeysReverse() { + sortImpl(true, true, true); + } + + + /** + * Sort by values in ascending order. The smallest value will be at [0]. + * + * @webref intdict:method + * @brief Sort by values in ascending order + */ + public void sortValues() { + sortValues(true); + } + + + /** + * Set true to ensure that the order returned is identical. Slightly + * slower because the tie-breaker for identical values compares the keys. + * @param stable + */ + public void sortValues(boolean stable) { + sortImpl(false, false, stable); + } + + + /** + * Sort by values in descending order. The largest value will be at [0]. + * + * @webref intdict:method + * @brief Sort by values in descending order + */ + public void sortValuesReverse() { + sortValuesReverse(true); + } + + + public void sortValuesReverse(boolean stable) { + sortImpl(false, true, stable); + } + + + protected void sortImpl(final boolean useKeys, final boolean reverse, + final boolean stable) { + Sort s = new Sort() { + @Override + public int size() { + return count; + } + + @Override + public float compare(int a, int b) { + int diff = 0; + if (useKeys) { + diff = keys[a].compareToIgnoreCase(keys[b]); + if (diff == 0) { + diff = values[a] - values[b]; + } + } else { // sort values + diff = values[a] - values[b]; + if (diff == 0 && stable) { + diff = keys[a].compareToIgnoreCase(keys[b]); + } + } + return reverse ? -diff : diff; + } + + @Override + public void swap(int a, int b) { + IntDict.this.swap(a, b); + } + }; + s.run(); + + // Set the indices after sort/swaps (performance fix 160411) + resetIndices(); + } + + + /** + * Sum all of the values in this dictionary, then return a new FloatDict of + * each key, divided by the total sum. The total for all values will be ~1.0. + * @return an IntDict with the original keys, mapped to their pct of the total + */ + public FloatDict getPercent() { + double sum = sum(); // a little more accuracy + FloatDict outgoing = new FloatDict(); + for (int i = 0; i < size(); i++) { + double percent = value(i) / sum; + outgoing.set(key(i), (float) percent); + } + return outgoing; + } + + + /** Returns a duplicate copy of this object. */ + public IntDict copy() { + IntDict outgoing = new IntDict(count); + System.arraycopy(keys, 0, outgoing.keys, 0, count); + System.arraycopy(values, 0, outgoing.values, 0, count); + for (int i = 0; i < count; i++) { + outgoing.indices.put(keys[i], i); + } + outgoing.count = count; + return outgoing; + } + + + public void print() { + for (int i = 0; i < size(); i++) { + System.out.println(keys[i] + " = " + values[i]); + } + } + + + /** + * Write tab-delimited entries out to + * @param writer + */ + public void write(PrintWriter writer) { + for (int i = 0; i < count; i++) { + writer.println(keys[i] + "\t" + values[i]); + } + writer.flush(); + } + + + /** + * Return this dictionary as a String in JSON format. + */ + public String toJSON() { + StringList items = new StringList(); + for (int i = 0; i < count; i++) { + items.append(JSONObject.quote(keys[i])+ ": " + values[i]); + } + return "{ " + items.join(", ") + " }"; + } + + + @Override + public String toString() { + return getClass().getSimpleName() + " size=" + size() + " " + toJSON(); + } +} diff --git a/src/main/java/processing/data/IntList.java b/src/main/java/processing/data/IntList.java new file mode 100644 index 0000000..627187d --- /dev/null +++ b/src/main/java/processing/data/IntList.java @@ -0,0 +1,913 @@ +package processing.data; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Random; + +import processing.core.PApplet; + + +// splice, slice, subset, concat, reverse + +// trim, join for String versions + + +/** + * Helper class for a list of ints. Lists are designed to have some of the + * features of ArrayLists, but to maintain the simplicity and efficiency of + * working with arrays. + * + * Functions like sort() and shuffle() always act on the list itself. To get + * a sorted copy, use list.copy().sort(). + * + * @webref data:composite + * @see FloatList + * @see StringList + */ +public class IntList implements Iterable { + protected int count; + protected int[] data; + + + public IntList() { + data = new int[10]; + } + + + /** + * @nowebref + */ + public IntList(int length) { + data = new int[length]; + } + + + /** + * @nowebref + */ + public IntList(int[] source) { + count = source.length; + data = new int[count]; + System.arraycopy(source, 0, data, 0, count); + } + + + /** + * Construct an IntList from an iterable pile of objects. + * For instance, a float array, an array of strings, who knows). + * Un-parseable or null values will be set to 0. + * @nowebref + */ + public IntList(Iterable iter) { + this(10); + for (Object o : iter) { + if (o == null) { + append(0); // missing value default + } else if (o instanceof Number) { + append(((Number) o).intValue()); + } else { + append(PApplet.parseInt(o.toString().trim())); + } + } + crop(); + } + + + /** + * Construct an IntList from a random pile of objects. + * Un-parseable or null values will be set to zero. + */ + public IntList(Object... items) { + final int missingValue = 0; // nuts, can't be last/final/second arg + + count = items.length; + data = new int[count]; + int index = 0; + for (Object o : items) { + int value = missingValue; + if (o != null) { + if (o instanceof Number) { + value = ((Number) o).intValue(); + } else { + value = PApplet.parseInt(o.toString().trim(), missingValue); + } + } + data[index++] = value; + } + } + + + static public IntList fromRange(int stop) { + return fromRange(0, stop); + } + + + static public IntList fromRange(int start, int stop) { + int count = stop - start; + IntList newbie = new IntList(count); + for (int i = 0; i < count; i++) { + newbie.set(i, start+i); + } + return newbie; + } + + + /** + * Improve efficiency by removing allocated but unused entries from the + * internal array used to store the data. Set to private, though it could + * be useful to have this public if lists are frequently making drastic + * size changes (from very large to very small). + */ + private void crop() { + if (count != data.length) { + data = PApplet.subset(data, 0, count); + } + } + + + /** + * Get the length of the list. + * + * @webref intlist:method + * @brief Get the length of the list + */ + public int size() { + return count; + } + + + public void resize(int length) { + if (length > data.length) { + int[] temp = new int[length]; + System.arraycopy(data, 0, temp, 0, count); + data = temp; + + } else if (length > count) { + Arrays.fill(data, count, length, 0); + } + count = length; + } + + + /** + * Remove all entries from the list. + * + * @webref intlist:method + * @brief Remove all entries from the list + */ + public void clear() { + count = 0; + } + + + /** + * Get an entry at a particular index. + * + * @webref intlist:method + * @brief Get an entry at a particular index + */ + public int get(int index) { + if (index >= this.count) { + throw new ArrayIndexOutOfBoundsException(index); + } + return data[index]; + } + + + /** + * Set the entry at a particular index. If the index is past the length of + * the list, it'll expand the list to accommodate, and fill the intermediate + * entries with 0s. + * + * @webref intlist:method + * @brief Set the entry at a particular index + */ + public void set(int index, int what) { + if (index >= count) { + data = PApplet.expand(data, index+1); + for (int i = count; i < index; i++) { + data[i] = 0; + } + count = index+1; + } + data[index] = what; + } + + + /** Just an alias for append(), but matches pop() */ + public void push(int value) { + append(value); + } + + + public int pop() { + if (count == 0) { + throw new RuntimeException("Can't call pop() on an empty list"); + } + int value = get(count-1); + count--; + return value; + } + + + /** + * Remove an element from the specified index + * + * @webref intlist:method + * @brief Remove an element from the specified index + */ + public int remove(int index) { + if (index < 0 || index >= count) { + throw new ArrayIndexOutOfBoundsException(index); + } + int entry = data[index]; +// int[] outgoing = new int[count - 1]; +// System.arraycopy(data, 0, outgoing, 0, index); +// count--; +// System.arraycopy(data, index + 1, outgoing, 0, count - index); +// data = outgoing; + // For most cases, this actually appears to be faster + // than arraycopy() on an array copying into itself. + for (int i = index; i < count-1; i++) { + data[i] = data[i+1]; + } + count--; + return entry; + } + + + // Remove the first instance of a particular value, + // and return the index at which it was found. + public int removeValue(int value) { + int index = index(value); + if (index != -1) { + remove(index); + return index; + } + return -1; + } + + + // Remove all instances of a particular value, + // and return the number of values found and removed + public int removeValues(int value) { + int ii = 0; + for (int i = 0; i < count; i++) { + if (data[i] != value) { + data[ii++] = data[i]; + } + } + int removed = count - ii; + count = ii; + return removed; + } + + + /** + * Add a new entry to the list. + * + * @webref intlist:method + * @brief Add a new entry to the list + */ + public void append(int value) { + if (count == data.length) { + data = PApplet.expand(data); + } + data[count++] = value; + } + + + public void append(int[] values) { + for (int v : values) { + append(v); + } + } + + + public void append(IntList list) { + for (int v : list.values()) { // will concat the list... + append(v); + } + } + + + /** Add this value, but only if it's not already in the list. */ + public void appendUnique(int value) { + if (!hasValue(value)) { + append(value); + } + } + + +// public void insert(int index, int value) { +// if (index+1 > count) { +// if (index+1 < data.length) { +// } +// } +// if (index >= data.length) { +// data = PApplet.expand(data, index+1); +// data[index] = value; +// count = index+1; +// +// } else if (count == data.length) { +// if (index >= count) { +// //int[] temp = new int[count << 1]; +// System.arraycopy(data, 0, temp, 0, index); +// temp[index] = value; +// System.arraycopy(data, index, temp, index+1, count - index); +// data = temp; +// +// } else { +// // data[] has room to grow +// // for() loop believed to be faster than System.arraycopy over itself +// for (int i = count; i > index; --i) { +// data[i] = data[i-1]; +// } +// data[index] = value; +// count++; +// } +// } + + + public void insert(int index, int value) { + insert(index, new int[] { value }); + } + + + // same as splice + public void insert(int index, int[] values) { + if (index < 0) { + throw new IllegalArgumentException("insert() index cannot be negative: it was " + index); + } + if (index >= data.length) { + throw new IllegalArgumentException("insert() index " + index + " is past the end of this list"); + } + + int[] temp = new int[count + values.length]; + + // Copy the old values, but not more than already exist + System.arraycopy(data, 0, temp, 0, Math.min(count, index)); + + // Copy the new values into the proper place + System.arraycopy(values, 0, temp, index, values.length); + +// if (index < count) { + // The index was inside count, so it's a true splice/insert + System.arraycopy(data, index, temp, index+values.length, count - index); + count = count + values.length; +// } else { +// // The index was past 'count', so the new count is weirder +// count = index + values.length; +// } + data = temp; + } + + + public void insert(int index, IntList list) { + insert(index, list.values()); + } + + + // below are aborted attempts at more optimized versions of the code + // that are harder to read and debug... + +// if (index + values.length >= count) { +// // We're past the current 'count', check to see if we're still allocated +// // index 9, data.length = 10, values.length = 1 +// if (index + values.length < data.length) { +// // There's still room for these entries, even though it's past 'count'. +// // First clear out the entries leading up to it, however. +// for (int i = count; i < index; i++) { +// data[i] = 0; +// } +// data[index] = +// } +// if (index >= data.length) { +// int length = index + values.length; +// int[] temp = new int[length]; +// System.arraycopy(data, 0, temp, 0, count); +// System.arraycopy(values, 0, temp, index, values.length); +// data = temp; +// count = data.length; +// } else { +// +// } +// +// } else if (count == data.length) { +// int[] temp = new int[count << 1]; +// System.arraycopy(data, 0, temp, 0, index); +// temp[index] = value; +// System.arraycopy(data, index, temp, index+1, count - index); +// data = temp; +// +// } else { +// // data[] has room to grow +// // for() loop believed to be faster than System.arraycopy over itself +// for (int i = count; i > index; --i) { +// data[i] = data[i-1]; +// } +// data[index] = value; +// count++; +// } + + + /** Return the first index of a particular value. */ + public int index(int what) { + /* + if (indexCache != null) { + try { + return indexCache.get(what); + } catch (Exception e) { // not there + return -1; + } + } + */ + for (int i = 0; i < count; i++) { + if (data[i] == what) { + return i; + } + } + return -1; + } + + + // !!! TODO this is not yet correct, because it's not being reset when + // the rest of the entries are changed +// protected void cacheIndices() { +// indexCache = new HashMap(); +// for (int i = 0; i < count; i++) { +// indexCache.put(data[i], i); +// } +// } + + /** + * @webref intlist:method + * @brief Check if a number is a part of the list + */ + public boolean hasValue(int value) { +// if (indexCache == null) { +// cacheIndices(); +// } +// return index(what) != -1; + for (int i = 0; i < count; i++) { + if (data[i] == value) { + return true; + } + } + return false; + } + + /** + * @webref intlist:method + * @brief Add one to a value + */ + public void increment(int index) { + if (count <= index) { + resize(index + 1); + } + data[index]++; + } + + + private void boundsProblem(int index, String method) { + final String msg = String.format("The list size is %d. " + + "You cannot %s() to element %d.", count, method, index); + throw new ArrayIndexOutOfBoundsException(msg); + } + + + /** + * @webref intlist:method + * @brief Add to a value + */ + public void add(int index, int amount) { + if (index < count) { + data[index] += amount; + } else { + boundsProblem(index, "add"); + } + } + + /** + * @webref intlist:method + * @brief Subtract from a value + */ + public void sub(int index, int amount) { + if (index < count) { + data[index] -= amount; + } else { + boundsProblem(index, "sub"); + } + } + + /** + * @webref intlist:method + * @brief Multiply a value + */ + public void mult(int index, int amount) { + if (index < count) { + data[index] *= amount; + } else { + boundsProblem(index, "mult"); + } + } + + /** + * @webref intlist:method + * @brief Divide a value + */ + public void div(int index, int amount) { + if (index < count) { + data[index] /= amount; + } else { + boundsProblem(index, "div"); + } + } + + + private void checkMinMax(String functionName) { + if (count == 0) { + String msg = + String.format("Cannot use %s() on an empty %s.", + functionName, getClass().getSimpleName()); + throw new RuntimeException(msg); + } + } + + + /** + * @webref intlist:method + * @brief Return the smallest value + */ + public int min() { + checkMinMax("min"); + int outgoing = data[0]; + for (int i = 1; i < count; i++) { + if (data[i] < outgoing) outgoing = data[i]; + } + return outgoing; + } + + + // returns the index of the minimum value. + // if there are ties, it returns the first one found. + public int minIndex() { + checkMinMax("minIndex"); + int value = data[0]; + int index = 0; + for (int i = 1; i < count; i++) { + if (data[i] < value) { + value = data[i]; + index = i; + } + } + return index; + } + + + /** + * @webref intlist:method + * @brief Return the largest value + */ + public int max() { + checkMinMax("max"); + int outgoing = data[0]; + for (int i = 1; i < count; i++) { + if (data[i] > outgoing) outgoing = data[i]; + } + return outgoing; + } + + + // returns the index of the maximum value. + // if there are ties, it returns the first one found. + public int maxIndex() { + checkMinMax("maxIndex"); + int value = data[0]; + int index = 0; + for (int i = 1; i < count; i++) { + if (data[i] > value) { + value = data[i]; + index = i; + } + } + return index; + } + + + public int sum() { + long amount = sumLong(); + if (amount > Integer.MAX_VALUE) { + throw new RuntimeException("sum() exceeds " + Integer.MAX_VALUE + ", use sumLong()"); + } + if (amount < Integer.MIN_VALUE) { + throw new RuntimeException("sum() less than " + Integer.MIN_VALUE + ", use sumLong()"); + } + return (int) amount; + } + + + public long sumLong() { + long sum = 0; + for (int i = 0; i < count; i++) { + sum += data[i]; + } + return sum; + } + + + /** + * Sorts the array in place. + * + * @webref intlist:method + * @brief Sorts the array, lowest to highest + */ + public void sort() { + Arrays.sort(data, 0, count); + } + + + /** + * Reverse sort, orders values from highest to lowest. + * + * @webref intlist:method + * @brief Reverse sort, orders values from highest to lowest + */ + public void sortReverse() { + new Sort() { + @Override + public int size() { + return count; + } + + @Override + public float compare(int a, int b) { + return data[b] - data[a]; + } + + @Override + public void swap(int a, int b) { + int temp = data[a]; + data[a] = data[b]; + data[b] = temp; + } + }.run(); + } + + + // use insert() +// public void splice(int index, int value) { +// } + + +// public void subset(int start) { +// subset(start, count - start); +// } +// +// +// public void subset(int start, int num) { +// for (int i = 0; i < num; i++) { +// data[i] = data[i+start]; +// } +// count = num; +// } + + /** + * @webref intlist:method + * @brief Reverse the order of the list elements + */ + public void reverse() { + int ii = count - 1; + for (int i = 0; i < count/2; i++) { + int t = data[i]; + data[i] = data[ii]; + data[ii] = t; + --ii; + } + } + + + /** + * Randomize the order of the list elements. Note that this does not + * obey the randomSeed() function in PApplet. + * + * @webref intlist:method + * @brief Randomize the order of the list elements + */ + public void shuffle() { + Random r = new Random(); + int num = count; + while (num > 1) { + int value = r.nextInt(num); + num--; + int temp = data[num]; + data[num] = data[value]; + data[value] = temp; + } + } + + + /** + * Randomize the list order using the random() function from the specified + * sketch, allowing shuffle() to use its current randomSeed() setting. + */ + public void shuffle(PApplet sketch) { + int num = count; + while (num > 1) { + int value = (int) sketch.random(num); + num--; + int temp = data[num]; + data[num] = data[value]; + data[value] = temp; + } + } + + + public IntList copy() { + IntList outgoing = new IntList(data); + outgoing.count = count; + return outgoing; + } + + + /** + * Returns the actual array being used to store the data. For advanced users, + * this is the fastest way to access a large list. Suitable for iterating + * with a for() loop, but modifying the list will have terrible consequences. + */ + public int[] values() { + crop(); + return data; + } + + + @Override + public Iterator iterator() { +// public Iterator valueIterator() { + return new Iterator() { + int index = -1; + + public void remove() { + IntList.this.remove(index); + index--; + } + + public Integer next() { + return data[++index]; + } + + public boolean hasNext() { + return index+1 < count; + } + }; + } + + + /** + * Create a new array with a copy of all the values. + * + * @return an array sized by the length of the list with each of the values. + * @webref intlist:method + * @brief Create a new array with a copy of all the values + */ + public int[] array() { + return array(null); + } + + + /** + * Copy values into the specified array. If the specified array is null or + * not the same size, a new array will be allocated. + * @param array + */ + public int[] array(int[] array) { + if (array == null || array.length != count) { + array = new int[count]; + } + System.arraycopy(data, 0, array, 0, count); + return array; + } + + +// public int[] toIntArray() { +// int[] outgoing = new int[count]; +// for (int i = 0; i < count; i++) { +// outgoing[i] = (int) data[i]; +// } +// return outgoing; +// } + + +// public long[] toLongArray() { +// long[] outgoing = new long[count]; +// for (int i = 0; i < count; i++) { +// outgoing[i] = (long) data[i]; +// } +// return outgoing; +// } + + +// public float[] toFloatArray() { +// float[] outgoing = new float[count]; +// System.arraycopy(data, 0, outgoing, 0, count); +// return outgoing; +// } + + +// public double[] toDoubleArray() { +// double[] outgoing = new double[count]; +// for (int i = 0; i < count; i++) { +// outgoing[i] = data[i]; +// } +// return outgoing; +// } + + +// public String[] toStringArray() { +// String[] outgoing = new String[count]; +// for (int i = 0; i < count; i++) { +// outgoing[i] = String.valueOf(data[i]); +// } +// return outgoing; +// } + + + /** + * Returns a normalized version of this array. Called getPercent() for + * consistency with the Dict classes. It's a getter method because it needs + * to returns a new list (because IntList/Dict can't do percentages or + * normalization in place on int values). + */ + public FloatList getPercent() { + double sum = 0; + for (float value : array()) { + sum += value; + } + FloatList outgoing = new FloatList(count); + for (int i = 0; i < count; i++) { + double percent = data[i] / sum; + outgoing.set(i, (float) percent); + } + return outgoing; + } + + +// /** +// * Count the number of times each entry is found in this list. +// * Converts each entry to a String so it can be used as a key. +// */ +// public IntDict getTally() { +// IntDict outgoing = new IntDict(); +// for (int i = 0; i < count; i++) { +// outgoing.increment(String.valueOf(data[i])); +// } +// return outgoing; +// } + + + public IntList getSubset(int start) { + return getSubset(start, count - start); + } + + + public IntList getSubset(int start, int num) { + int[] subset = new int[num]; + System.arraycopy(data, start, subset, 0, num); + return new IntList(subset); + } + + + public String join(String separator) { + if (count == 0) { + return ""; + } + StringBuilder sb = new StringBuilder(); + sb.append(data[0]); + for (int i = 1; i < count; i++) { + sb.append(separator); + sb.append(data[i]); + } + return sb.toString(); + } + + + public void print() { + for (int i = 0; i < count; i++) { + System.out.format("[%d] %d%n", i, data[i]); + } + } + + + /** + * Return this dictionary as a String in JSON format. + */ + public String toJSON() { + return "[ " + join(", ") + " ]"; + } + + + @Override + public String toString() { + return getClass().getSimpleName() + " size=" + size() + " " + toJSON(); + } +} diff --git a/src/main/java/processing/data/JSONArray.java b/src/main/java/processing/data/JSONArray.java new file mode 100644 index 0000000..ea8276b --- /dev/null +++ b/src/main/java/processing/data/JSONArray.java @@ -0,0 +1,1260 @@ +package processing.data; + +// This code has been modified heavily to more closely match the rest of the +// Processing API. In the spirit of the rest of the project, where we try to +// keep the API as simple as possible, we have erred on the side of being +// conservative in choosing which functions to include, since we haven't yet +// decided what's truly necessary. Power users looking for a full-featured +// version can use the original version from json.org, or one of the many +// other APIs that are available. As with all Processing API, if there's a +// function that should be added, please let use know, and have others vote: +// http://code.google.com/p/processing/issues/list + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Array; +import java.util.ArrayList; + +import processing.core.PApplet; + +/** + * A JSONArray is an ordered sequence of values. Its external text form is a + * string wrapped in square brackets with commas separating the values. The + * internal form is an object having get and opt + * methods for accessing the values by index, and put methods for + * adding or replacing values. The values can be any of these types: + * Boolean, JSONArray, JSONObject, + * Number, String, or the + * JSONObject.NULL object. + *

        + * The constructor can convert a JSON text into a Java object. The + * toString method converts to JSON text. + *

        + * A get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

        + * The generic get() and opt() methods return an + * object which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. + *

        + * The texts produced by the toString methods strictly conform to + * JSON syntax rules. The constructors are more forgiving in the texts they will + * accept: + *

          + *
        • An extra , (comma) may appear just + * before the closing bracket.
        • + *
        • The null value will be inserted when there is , + *  (comma) elision.
        • + *
        • Strings may be quoted with ' (single + * quote).
        • + *
        • Strings do not need to be quoted at all if they do not begin with a quote + * or single quote, and if they do not contain leading or trailing spaces, and + * if they do not contain any of these characters: + * { } [ ] / \ : , = ; # and if they do not look like numbers and + * if they are not the reserved words true, false, or + * null.
        • + *
        • Values can be separated by ; (semicolon) as + * well as by , (comma).
        • + *
        + * + * @author JSON.org + * @version 2012-11-13 + * @webref data:composite + * @see JSONObject + * @see PApplet#loadJSONObject(String) + * @see PApplet#loadJSONArray(String) + * @see PApplet#saveJSONObject(JSONObject, String) + * @see PApplet#saveJSONArray(JSONArray, String) + */ +public class JSONArray { + + /** + * The arrayList where the JSONArray's properties are kept. + */ + private final ArrayList myArrayList; + + + /** + * Construct an empty JSONArray. + */ + public JSONArray() { + this.myArrayList = new ArrayList<>(); + } + + + /** + * @nowebref + */ + public JSONArray(Reader reader) { + this(new JSONTokener(reader)); + } + + + /** + * Construct a JSONArray from a JSONTokener. + * + * @param x A JSONTokener + * @throws RuntimeException If there is a syntax error. + * @nowebref + */ + protected JSONArray(JSONTokener x) { + this(); + if (x.nextClean() != '[') { + throw new RuntimeException("A JSONArray text must start with '['"); + } + if (x.nextClean() != ']') { + x.back(); + for (;;) { + if (x.nextClean() == ',') { + x.back(); + myArrayList.add(JSONObject.NULL); + } else { + x.back(); + myArrayList.add(x.nextValue()); + } + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == ']') { + return; + } + x.back(); + break; + case ']': + return; + default: + throw new RuntimeException("Expected a ',' or ']'"); + } + } + } + } + + + /** + * @nowebref + */ + public JSONArray(IntList list) { + myArrayList = new ArrayList<>(); + for (int item : list.values()) { + myArrayList.add(Integer.valueOf(item)); + } + } + + + /** + * @nowebref + */ + public JSONArray(FloatList list) { + myArrayList = new ArrayList<>(); + for (float item : list.values()) { + myArrayList.add(Float.valueOf(item)); + } + } + + + /** + * @nowebref + */ + public JSONArray(StringList list) { + myArrayList = new ArrayList<>(); + for (String item : list.values()) { + myArrayList.add(item); + } + } + + + /** + * Construct a JSONArray from a source JSON text. + * @param source A string that begins with + * [ (left bracket) + * and ends with ] (right bracket). + * @return {@code null} if there is a syntax error. + */ + static public JSONArray parse(String source) { + try { + return new JSONArray(new JSONTokener(source)); + } catch (Exception e) { + return null; + } + } + + +// /** +// * Construct a JSONArray from a Collection. +// * @param collection A Collection. +// */ +// public JSONArray(Collection collection) { +// myArrayList = new ArrayList(); +// if (collection != null) { +// Iterator iter = collection.iterator(); +// while (iter.hasNext()) { +// myArrayList.add(JSONObject.wrap(iter.next())); +// } +// } +// } + + + // TODO not decided whether we keep this one, but used heavily by JSONObject + /** + * Construct a JSONArray from an array + * @throws RuntimeException If not an array. + */ + protected JSONArray(Object array) { + this(); + if (array.getClass().isArray()) { + int length = Array.getLength(array); + for (int i = 0; i < length; i += 1) { + this.append(JSONObject.wrap(Array.get(array, i))); + } + } else { + throw new RuntimeException("JSONArray initial value should be a string or collection or array."); + } + } + + + /** + * Get the optional object value associated with an index. + * @param index must be between 0 and length() - 1 + * @return An object value, or null if there is no + * object at that index. + */ + private Object opt(int index) { + if (index < 0 || index >= this.size()) { + return null; + } + return myArrayList.get(index); + } + + + /** + * Get the object value associated with an index. + * @param index must be between 0 and length() - 1 + * @return An object value. + * @throws RuntimeException If there is no value for the index. + */ + public Object get(int index) { + Object object = opt(index); + if (object == null) { + throw new RuntimeException("JSONArray[" + index + "] not found."); + } + return object; + } + + + /** + * Get the string associated with an index. + * + * @webref jsonarray:method + * @brief Gets the String value associated with an index + * @param index must be between 0 and length() - 1 + * @return A string value. + * @throws RuntimeException If there is no string value for the index. + * @see JSONArray#getInt(int) + * @see JSONArray#getFloat(int) + * @see JSONArray#getBoolean(int) + */ + public String getString(int index) { + Object object = this.get(index); + if (object instanceof String) { + return (String)object; + } + throw new RuntimeException("JSONArray[" + index + "] not a string."); + } + + + /** + * Get the optional string associated with an index. + * The defaultValue is returned if the key is not found. + * + * @param index The index must be between 0 and length() - 1. + * @param defaultValue The default value. + * @return A String value. + */ + public String getString(int index, String defaultValue) { + Object object = this.opt(index); + return JSONObject.NULL.equals(object) ? defaultValue : object.toString(); + } + + + /** + * Get the int value associated with an index. + * + * @webref jsonarray:method + * @brief Gets the int value associated with an index + * @param index must be between 0 and length() - 1 + * @return The value. + * @throws RuntimeException If the key is not found or if the value is not a number. + * @see JSONArray#getFloat(int) + * @see JSONArray#getString(int) + * @see JSONArray#getBoolean(int) + */ + public int getInt(int index) { + Object object = this.get(index); + try { + return object instanceof Number + ? ((Number)object).intValue() + : Integer.parseInt((String)object); + } catch (Exception e) { + throw new RuntimeException("JSONArray[" + index + "] is not a number."); + } + } + + + /** + * Get the optional int value associated with an index. + * The defaultValue is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * @param index The index must be between 0 and length() - 1. + * @param defaultValue The default value. + * @return The value. + */ + public int getInt(int index, int defaultValue) { + try { + return getInt(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the long value associated with an index. + * + * @param index The index must be between 0 and length() - 1 + * @return The value. + * @throws RuntimeException If the key is not found or if the value cannot + * be converted to a number. + */ + public long getLong(int index) { + Object object = this.get(index); + try { + return object instanceof Number + ? ((Number)object).longValue() + : Long.parseLong((String)object); + } catch (Exception e) { + throw new RuntimeException("JSONArray[" + index + "] is not a number."); + } + } + + + /** + * Get the optional long value associated with an index. + * The defaultValue is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * @param index The index must be between 0 and length() - 1. + * @param defaultValue The default value. + * @return The value. + */ + public long getLong(int index, long defaultValue) { + try { + return this.getLong(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get a value from an index as a float. JSON uses 'double' values + * internally, so this is simply getDouble() cast to a float. + * + * @webref jsonarray:method + * @brief Gets the float value associated with an index + * @param index must be between 0 and length() - 1 + * @see JSONArray#getInt(int) + * @see JSONArray#getString(int) + * @see JSONArray#getBoolean(int) + */ + public float getFloat(int index) { + return (float) getDouble(index); + } + + + public float getFloat(int index, float defaultValue) { + try { + return getFloat(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the double value associated with an index. + * + * @param index must be between 0 and length() - 1 + * @return The value. + * @throws RuntimeException If the key is not found or if the value cannot + * be converted to a number. + */ + public double getDouble(int index) { + Object object = this.get(index); + try { + return object instanceof Number + ? ((Number)object).doubleValue() + : Double.parseDouble((String)object); + } catch (Exception e) { + throw new RuntimeException("JSONArray[" + index + "] is not a number."); + } + } + + + /** + * Get the optional double value associated with an index. + * The defaultValue is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * + * @param index subscript + * @param defaultValue The default value. + * @return The value. + */ + public double getDouble(int index, double defaultValue) { + try { + return this.getDouble(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the boolean value associated with an index. + * The string values "true" and "false" are converted to boolean. + * + * @webref jsonarray:method + * @brief Gets the boolean value associated with an index + * @param index must be between 0 and length() - 1 + * @return The truth. + * @throws RuntimeException If there is no value for the index or if the + * value is not convertible to boolean. + * @see JSONArray#getInt(int) + * @see JSONArray#getFloat(int) + * @see JSONArray#getString(int) + */ + public boolean getBoolean(int index) { + Object object = this.get(index); + if (object.equals(Boolean.FALSE) || + (object instanceof String && + ((String)object).equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) || + (object instanceof String && + ((String)object).equalsIgnoreCase("true"))) { + return true; + } + throw new RuntimeException("JSONArray[" + index + "] is not a boolean."); + } + + + /** + * Get the optional boolean value associated with an index. + * It returns the defaultValue if there is no value at that index or if + * it is not a Boolean or the String "true" or "false" (case insensitive). + * + * @param index The index must be between 0 and length() - 1. + * @param defaultValue A boolean default. + * @return The truth. + */ + public boolean getBoolean(int index, boolean defaultValue) { + try { + return getBoolean(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the JSONArray associated with an index. + * + * @webref jsonobject:method + * @brief Gets the JSONArray associated with an index value + * @param index must be between 0 and length() - 1 + * @return A JSONArray value. + * @throws RuntimeException If there is no value for the index. or if the + * value is not a JSONArray + * @see JSONArray#getJSONObject(int) + * @see JSONArray#setJSONObject(int, JSONObject) + * @see JSONArray#setJSONArray(int, JSONArray) + */ + public JSONArray getJSONArray(int index) { + Object object = this.get(index); + if (object instanceof JSONArray) { + return (JSONArray)object; + } + throw new RuntimeException("JSONArray[" + index + "] is not a JSONArray."); + } + + + public JSONArray getJSONArray(int index, JSONArray defaultValue) { + try { + return getJSONArray(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the JSONObject associated with an index. + * + * @webref jsonobject:method + * @brief Gets the JSONObject associated with an index value + * @param index the index value of the object to get + * @return A JSONObject value. + * @throws RuntimeException If there is no value for the index or if the + * value is not a JSONObject + * @see JSONArray#getJSONArray(int) + * @see JSONArray#setJSONObject(int, JSONObject) + * @see JSONArray#setJSONArray(int, JSONArray) + */ + public JSONObject getJSONObject(int index) { + Object object = this.get(index); + if (object instanceof JSONObject) { + return (JSONObject)object; + } + throw new RuntimeException("JSONArray[" + index + "] is not a JSONObject."); + } + + + public JSONObject getJSONObject(int index, JSONObject defaultValue) { + try { + return getJSONObject(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get this entire array as a String array. + * + * @webref jsonarray:method + * @brief Gets the entire array as an array of Strings + * @see JSONArray#getIntArray() + */ + public String[] getStringArray() { + String[] outgoing = new String[size()]; + for (int i = 0; i < size(); i++) { + outgoing[i] = getString(i); + } + return outgoing; + } + + + /** + * Get this entire array as an int array. Everything must be an int. + * + * @webref jsonarray:method + * @brief Gets the entire array as array of ints + * @see JSONArray#getStringArray() + */ + public int[] getIntArray() { + int[] outgoing = new int[size()]; + for (int i = 0; i < size(); i++) { + outgoing[i] = getInt(i); + } + return outgoing; + } + + + /** Get this entire array as a long array. Everything must be an long. */ + public long[] getLongArray() { + long[] outgoing = new long[size()]; + for (int i = 0; i < size(); i++) { + outgoing[i] = getLong(i); + } + return outgoing; + } + + + /** Get this entire array as a float array. Everything must be an float. */ + public float[] getFloatArray() { + float[] outgoing = new float[size()]; + for (int i = 0; i < size(); i++) { + outgoing[i] = getFloat(i); + } + return outgoing; + } + + + /** Get this entire array as a double array. Everything must be an double. */ + public double[] getDoubleArray() { + double[] outgoing = new double[size()]; + for (int i = 0; i < size(); i++) { + outgoing[i] = getDouble(i); + } + return outgoing; + } + + + /** Get this entire array as a boolean array. Everything must be a boolean. */ + public boolean[] getBooleanArray() { + boolean[] outgoing = new boolean[size()]; + for (int i = 0; i < size(); i++) { + outgoing[i] = getBoolean(i); + } + return outgoing; + } + + +// /** +// * Get the optional boolean value associated with an index. +// * It returns false if there is no value at that index, +// * or if the value is not Boolean.TRUE or the String "true". +// * +// * @param index The index must be between 0 and length() - 1. +// * @return The truth. +// */ +// public boolean optBoolean(int index) { +// return this.optBoolean(index, false); +// } +// +// +// /** +// * Get the optional double value associated with an index. +// * NaN is returned if there is no value for the index, +// * or if the value is not a number and cannot be converted to a number. +// * +// * @param index The index must be between 0 and length() - 1. +// * @return The value. +// */ +// public double optDouble(int index) { +// return this.optDouble(index, Double.NaN); +// } +// +// +// /** +// * Get the optional int value associated with an index. +// * Zero is returned if there is no value for the index, +// * or if the value is not a number and cannot be converted to a number. +// * +// * @param index The index must be between 0 and length() - 1. +// * @return The value. +// */ +// public int optInt(int index) { +// return this.optInt(index, 0); +// } +// +// +// /** +// * Get the optional long value associated with an index. +// * Zero is returned if there is no value for the index, +// * or if the value is not a number and cannot be converted to a number. +// * +// * @param index The index must be between 0 and length() - 1. +// * @return The value. +// */ +// public long optLong(int index) { +// return this.optLong(index, 0); +// } +// +// +// /** +// * Get the optional string value associated with an index. It returns an +// * empty string if there is no value at that index. If the value +// * is not a string and is not null, then it is coverted to a string. +// * +// * @param index The index must be between 0 and length() - 1. +// * @return A String value. +// */ +// public String optString(int index) { +// return this.optString(index, ""); +// } + + + /** + * Append an String value. This increases the array's length by one. + * + * @webref jsonarray:method + * @brief Appends a value, increasing the array's length by one + * @param value a String value + * @return this. + * @see JSONArray#size() + * @see JSONArray#remove(int) + */ + public JSONArray append(String value) { + this.append((Object)value); + return this; + } + + + /** + * Append an int value. This increases the array's length by one. + * + * @param value an int value + * @return this. + */ + public JSONArray append(int value) { + this.append(Integer.valueOf(value)); + return this; + } + + + /** + * Append an long value. This increases the array's length by one. + * + * @nowebref + * @param value A long value. + * @return this. + */ + public JSONArray append(long value) { + this.append(Long.valueOf(value)); + return this; + } + + + /** + * Append a float value. This increases the array's length by one. + * This will store the value as a double, since there are no floats in JSON. + * + * @param value a float value + * @throws RuntimeException if the value is not finite. + * @return this. + */ + public JSONArray append(float value) { + return append((double) value); + } + + + /** + * Append a double value. This increases the array's length by one. + * + * @nowebref + * @param value A double value. + * @throws RuntimeException if the value is not finite. + * @return this. + */ + public JSONArray append(double value) { + Double d = value; + JSONObject.testValidity(d); + this.append(d); + return this; + } + + + /** + * Append a boolean value. This increases the array's length by one. + * + * @param value a boolean value + * @return this. + */ + public JSONArray append(boolean value) { + this.append(value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + +// /** +// * Put a value in the JSONArray, where the value will be a +// * JSONArray which is produced from a Collection. +// * @param value A Collection value. +// * @return this. +// */ +// public JSONArray append(Collection value) { +// this.append(new JSONArray(value)); +// return this; +// } + + +// /** +// * Put a value in the JSONArray, where the value will be a +// * JSONObject which is produced from a Map. +// * @param value A Map value. +// * @return this. +// */ +// public JSONArray append(Map value) { +// this.append(new JSONObject(value)); +// return this; +// } + + + /** + * @param value a JSONArray value + */ + public JSONArray append(JSONArray value) { + myArrayList.add(value); + return this; + } + + + /** + * @param value a JSONObject value + */ + public JSONArray append(JSONObject value) { + myArrayList.add(value); + return this; + } + + + /** + * Append an object value. This increases the array's length by one. + * @param value An object value. The value should be a + * Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the + * JSONObject.NULL object. + * @return this. + */ + protected JSONArray append(Object value) { + myArrayList.add(value); + return this; + } + + +// /** +// * Put a value in the JSONArray, where the value will be a +// * JSONArray which is produced from a Collection. +// * @param index The subscript. +// * @param value A Collection value. +// * @return this. +// * @throws RuntimeException If the index is negative or if the value is +// * not finite. +// */ +// public JSONArray set(int index, Collection value) { +// this.set(index, new JSONArray(value)); +// return this; +// } + + + /** + * Put or replace a String value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad + * it out. + * + * @webref jsonarray:method + * @brief Put a String value in the JSONArray + * @param index an index value + * @param value the value to assign + * @return this. + * @throws RuntimeException If the index is negative. + * @see JSONArray#setInt(int, int) + * @see JSONArray#setFloat(int, float) + * @see JSONArray#setBoolean(int, boolean) + */ + public JSONArray setString(int index, String value) { + this.set(index, value); + return this; + } + + + /** + * Put or replace an int value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad + * it out. + * + * @webref jsonarray:method + * @brief Put an int value in the JSONArray + * @param index an index value + * @param value the value to assign + * @return this. + * @throws RuntimeException If the index is negative. + * @see JSONArray#setFloat(int, float) + * @see JSONArray#setString(int, String) + * @see JSONArray#setBoolean(int, boolean) + */ + public JSONArray setInt(int index, int value) { + this.set(index, Integer.valueOf(value)); + return this; + } + + + /** + * Put or replace a long value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad + * it out. + * @param index The subscript. + * @param value A long value. + * @return this. + * @throws RuntimeException If the index is negative. + */ + public JSONArray setLong(int index, long value) { + return set(index, Long.valueOf(value)); + } + + + /** + * Put or replace a float value. If the index is greater than the length + * of the JSONArray, then null elements will be added as necessary to pad + * it out. There are no 'double' values in JSON, so this is passed to + * setDouble(value). + * + * @webref jsonarray:method + * @brief Put a float value in the JSONArray + * @param index an index value + * @param value the value to assign + * @return this. + * @throws RuntimeException If the index is negative or if the value is + * not finite. + * @see JSONArray#setInt(int, int) + * @see JSONArray#setString(int, String) + * @see JSONArray#setBoolean(int, boolean) + */ + public JSONArray setFloat(int index, float value) { + return setDouble(index, value); + } + + + /** + * Put or replace a double value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad + * it out. + * @param index The subscript. + * @param value A double value. + * @return this. + * @throws RuntimeException If the index is negative or if the value is + * not finite. + */ + public JSONArray setDouble(int index, double value) { + return set(index, Double.valueOf(value)); + } + + + /** + * Put or replace a boolean value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * + * @webref jsonarray:method + * @brief Put a boolean value in the JSONArray + * @param index an index value + * @param value the value to assign + * @return this. + * @throws RuntimeException If the index is negative. + * @see JSONArray#setInt(int, int) + * @see JSONArray#setFloat(int, float) + * @see JSONArray#setString(int, String) + */ + public JSONArray setBoolean(int index, boolean value) { + return set(index, value ? Boolean.TRUE : Boolean.FALSE); + } + + +// /** +// * Put a value in the JSONArray, where the value will be a +// * JSONObject that is produced from a Map. +// * @param index The subscript. +// * @param value The Map value. +// * @return this. +// * @throws RuntimeException If the index is negative or if the the value is +// * an invalid number. +// */ +// public JSONArray set(int index, Map value) { +// this.set(index, new JSONObject(value)); +// return this; +// } + + /** + * @webref jsonarray:method + * @brief Sets the JSONArray value associated with an index value + * @param index the index value to target + * @param value the value to assign + * @see JSONArray#setJSONObject(int, JSONObject) + * @see JSONArray#getJSONObject(int) + * @see JSONArray#getJSONArray(int) + */ + public JSONArray setJSONArray(int index, JSONArray value) { + set(index, value); + return this; + } + + /** + * @webref jsonarray:method + * @brief Sets the JSONObject value associated with an index value + * @param index the index value to target + * @param value the value to assign + * @see JSONArray#setJSONArray(int, JSONArray) + * @see JSONArray#getJSONObject(int) + * @see JSONArray#getJSONArray(int) + */ + public JSONArray setJSONObject(int index, JSONObject value) { + set(index, value); + return this; + } + + + /** + * Put or replace an object value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * @param index The subscript. + * @param value The value to put into the array. The value should be a + * Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the + * JSONObject.NULL object. + * @return this. + * @throws RuntimeException If the index is negative or if the the value is + * an invalid number. + */ + private JSONArray set(int index, Object value) { + JSONObject.testValidity(value); + if (index < 0) { + throw new RuntimeException("JSONArray[" + index + "] not found."); + } + if (index < this.size()) { + this.myArrayList.set(index, value); + } else { + while (index != this.size()) { + this.append(JSONObject.NULL); + } + this.append(value); + } + return this; + } + + + /** + * Get the number of elements in the JSONArray, included nulls. + * + * @webref jsonarray:method + * @brief Gets the number of elements in the JSONArray + * @return The length (or size). + * @see JSONArray#append(String) + * @see JSONArray#remove(int) + */ + public int size() { + return myArrayList.size(); + } + + + /** + * Determine if the value is null. + * @webref + * @param index must be between 0 and length() - 1 + * @return true if the value at the index is null, or if there is no value. + */ + public boolean isNull(int index) { + return JSONObject.NULL.equals(this.opt(index)); + } + + + /** + * Remove an index and close the hole. + * + * @webref jsonarray:method + * @brief Removes an element + * @param index the index value of the element to be removed + * @return The value that was associated with the index, or null if there was no value. + * @see JSONArray#size() + * @see JSONArray#append(String) + */ + public Object remove(int index) { + Object o = this.opt(index); + this.myArrayList.remove(index); + return o; + } + + +// /** +// * Produce a JSONObject by combining a JSONArray of names with the values +// * of this JSONArray. +// * @param names A JSONArray containing a list of key strings. These will be +// * paired with the values. +// * @return A JSONObject, or null if there are no names or if this JSONArray +// * has no values. +// * @throws JSONException If any of the names are null. +// */ +// public JSON toJSONObject(JSONArray names) { +// if (names == null || names.length() == 0 || this.length() == 0) { +// return null; +// } +// JSON jo = new JSON(); +// for (int i = 0; i < names.length(); i += 1) { +// jo.put(names.getString(i), this.opt(i)); +// } +// return jo; +// } + + +// protected boolean save(OutputStream output) { +// return write(PApplet.createWriter(output), null); +// } + + + public boolean save(File file, String options) { + PrintWriter writer = PApplet.createWriter(file); + boolean success = write(writer, options); + writer.close(); + return success; + } + + + public boolean write(PrintWriter output) { + return write(output, null); + } + + + public boolean write(PrintWriter output, String options) { + int indentFactor = 2; + if (options != null) { + String[] opts = PApplet.split(options, ','); + for (String opt : opts) { + if (opt.equals("compact")) { + indentFactor = -1; + } else if (opt.startsWith("indent=")) { + indentFactor = PApplet.parseInt(opt.substring(7), -2); + if (indentFactor == -2) { + throw new IllegalArgumentException("Could not read a number from " + opt); + } + } else { + System.err.println("Ignoring " + opt); + } + } + } + output.print(format(indentFactor)); + output.flush(); + return true; + } + + + /** + * Return the JSON data formatted with two spaces for indents. + * Chosen to do this since it's the most common case (e.g. with println()). + * Same as format(2). Use the format() function for more options. + */ + @Override + public String toString() { + try { + return format(2); + } catch (Exception e) { + return null; + } + } + + + /** + * Make a pretty-printed JSON text of this JSONArray. + * Warning: This method assumes that the data structure is acyclical. + * @param indentFactor The number of spaces to add to each level of + * indentation. Use -1 to specify no indentation and no newlines. + * @return a printable, displayable, transmittable + * representation of the object, beginning + * with [ (left bracket) and ending + * with ] (right bracket). + */ + public String format(int indentFactor) { + StringWriter sw = new StringWriter(); + synchronized (sw.getBuffer()) { + return this.writeInternal(sw, indentFactor, 0).toString(); + } + } + + +// /** +// * Write the contents of the JSONArray as JSON text to a writer. For +// * compactness, no whitespace is added. +// *

        +// * Warning: This method assumes that the data structure is acyclic. +// * +// * @return The writer. +// */ +// protected Writer write(Writer writer) { +// return this.write(writer, -1, 0); +// } + + + /** + * Write the contents of the JSONArray as JSON text to a writer. + *

        + * Warning: This method assumes that the data structure is acyclic. + * + * @param indentFactor + * The number of spaces to add to each level of indentation. + * Use -1 to specify no indentation and no newlines. + * @param indent + * The indention of the top level. + * @return The writer. + * @throws RuntimeException + */ + protected Writer writeInternal(Writer writer, int indentFactor, int indent) { + try { + boolean commanate = false; + int length = this.size(); + writer.write('['); + + // Use -1 to signify 'no indent' + int thisFactor = (indentFactor == -1) ? 0 : indentFactor; + + if (length == 1) { + JSONObject.writeValue(writer, this.myArrayList.get(0), + indentFactor, indent); +// thisFactor, indent); + } else if (length != 0) { + final int newIndent = indent + thisFactor; + + for (int i = 0; i < length; i += 1) { + if (commanate) { + writer.write(','); + } + if (indentFactor != -1) { + writer.write('\n'); + } + JSONObject.indent(writer, newIndent); +// JSONObject.writeValue(writer, this.myArrayList.get(i), +// thisFactor, newIndent); + JSONObject.writeValue(writer, this.myArrayList.get(i), + indentFactor, newIndent); + commanate = true; + } + if (indentFactor != -1) { + writer.write('\n'); + } + JSONObject.indent(writer, indent); + } + writer.write(']'); + return writer; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + /** + * Make a string from the contents of this JSONArray. The + * separator string is inserted between each element. + * Warning: This method assumes that the data structure is acyclic. + * @param separator A string that will be inserted between the elements. + * @return a string. + * @throws RuntimeException If the array contains an invalid number. + */ + public String join(String separator) { + int len = this.size(); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < len; i += 1) { + if (i > 0) { + sb.append(separator); + } + sb.append(JSONObject.valueToString(this.myArrayList.get(i))); + } + return sb.toString(); + } +} diff --git a/src/main/java/processing/data/JSONObject.java b/src/main/java/processing/data/JSONObject.java new file mode 100644 index 0000000..f76900a --- /dev/null +++ b/src/main/java/processing/data/JSONObject.java @@ -0,0 +1,2282 @@ +package processing.data; + +// This code has been modified heavily to more closely match the rest of the +// Processing API. In the spirit of the rest of the project, where we try to +// keep the API as simple as possible, we have erred on the side of being +// conservative in choosing which functions to include, since we haven't yet +// decided what's truly necessary. Power users looking for a full-featured +// version can use the original version from json.org, or one of the many +// other APIs that are available. As with all Processing API, if there's a +// function that should be added, please let use know, and have others vote: +// http://code.google.com/p/processing/issues/list + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import processing.core.PApplet; + +/** + * A JSONObject is an unordered collection of name/value pairs. Its external + * form is a string wrapped in curly braces with colons between the names and + * values, and commas between the values and names. The internal form is an + * object having get and opt methods for accessing the + * values by name, and put methods for adding or replacing values + * by name. The values can be any of these types: Boolean, + * JSONArray, JSONObject, Number, + * String, or the JSONObject.NULL object. A JSONObject + * constructor can be used to convert an external form JSON text into an + * internal form whose values can be retrieved with the get and + * opt methods, or to convert values into a JSON text using the + * put and toString methods. A get method + * returns a value if one can be found, and throws an exception if one cannot be + * found. An opt method returns a default value instead of throwing + * an exception, and so is useful for obtaining optional values. + *

        + * The generic get() and opt() methods return an + * object, which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. The opt methods differ from the get methods in that they do + * not throw. Instead, they return a specified value, such as null. + *

        + * The put methods add or replace values in an object. For example, + * + *

        + * myString = new JSONObject().put("JSON", "Hello, World!").toString();
        + * 
        + * + * produces the string {"JSON": "Hello, World"}. + *

        + * The texts produced by the toString methods strictly conform to + * the JSON syntax rules. The constructors are more forgiving in the texts they + * will accept: + *

          + *
        • An extra , (comma) may appear just + * before the closing brace.
        • + *
        • Strings may be quoted with ' (single + * quote).
        • + *
        • Strings do not need to be quoted at all if they do not begin with a quote + * or single quote, and if they do not contain leading or trailing spaces, and + * if they do not contain any of these characters: + * { } [ ] / \ : , = ; # and if they do not look like numbers and + * if they are not the reserved words true, false, or + * null.
        • + *
        • Keys can be followed by = or {@code =>} as well as by + * :.
        • + *
        • Values can be followed by ; (semicolon) as + * well as by , (comma).
        • + *
        + * + * @author JSON.org + * @version 2012-12-01 + * @webref data:composite + * @see JSONArray + * @see PApplet#loadJSONObject(String) + * @see PApplet#loadJSONArray(String) + * @see PApplet#saveJSONObject(JSONObject, String) + * @see PApplet#saveJSONArray(JSONArray, String) + */ +public class JSONObject { + /** + * The maximum number of keys in the key pool. + */ + private static final int keyPoolSize = 100; + + /** + * Key pooling is like string interning, but without permanently tying up + * memory. To help conserve memory, storage of duplicated key strings in + * JSONObjects will be avoided by using a key pool to manage unique key + * string objects. This is used by JSONObject.put(string, object). + */ + private static HashMap keyPool = + new HashMap<>(keyPoolSize); + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * JSONObject.NULL is equivalent to the value that JavaScript calls null, + * whilst Java's null is equivalent to the value that JavaScript calls + * undefined. + */ + private static final class Null { + /** + * There is only intended to be a single instance of the NULL object, + * so the clone method returns itself. + * @return NULL. + */ + @Override + protected final Object clone() { + return this; + } + + /** + * A Null object is equal to the null value and to itself. + * @param object An object to test for nullness. + * @return true if the object parameter is the JSONObject.NULL object + * or null. + */ + @Override + public boolean equals(Object object) { + return object == null || object == this; + } + + /** + * Get the "null" string value. + * @return The string "null". + */ + @Override + public String toString() { + return "null"; + } + + @Override + public int hashCode() { + // TODO Auto-generated method stub + return super.hashCode(); + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * The map where the JSONObject's properties are kept. + */ +// private final Map map; + private final HashMap map; + + + /** + * It is sometimes more convenient and less ambiguous to have a + * NULL object than to use Java's null value. + * JSONObject.NULL.equals(null) returns true. + * JSONObject.NULL.toString() returns "null". + */ + public static final Object NULL = new Null(); + + + /** + * Construct an empty JSONObject. + * @nowebref + */ + public JSONObject() { + this.map = new HashMap<>(); + } + + +// /** +// * Construct a JSONObject from a subset of another JSONObject. +// * An array of strings is used to identify the keys that should be copied. +// * Missing keys are ignored. +// * @param jo A JSONObject. +// * @param names An array of strings. +// */ +// public JSONObject(JSONObject jo, String[] names) { +// this(); +// for (int i = 0; i < names.length; i += 1) { +// try { +// this.putOnce(names[i], jo.opt(names[i])); +// } catch (Exception ignore) { +// } +// } +// } + + + /** + * @nowebref + */ + public JSONObject(Reader reader) { + this(new JSONTokener(reader)); + } + + + /** + * Construct a JSONObject from a JSONTokener. + * @param x A JSONTokener object containing the source string. + * @throws RuntimeException If there is a syntax error in the source string + * or a duplicated key. + */ + protected JSONObject(JSONTokener x) { + this(); + char c; + String key; + + if (x.nextClean() != '{') { + throw new RuntimeException("A JSONObject text must begin with '{'"); + } + for (;;) { + c = x.nextClean(); + switch (c) { + case 0: + throw new RuntimeException("A JSONObject text must end with '}'"); + case '}': + return; + default: + x.back(); + key = x.nextValue().toString(); + } + + // The key is followed by ':'. We will also tolerate '=' or '=>'. + + c = x.nextClean(); + if (c == '=') { + if (x.next() != '>') { + x.back(); + } + } else if (c != ':') { + throw new RuntimeException("Expected a ':' after a key"); + } + this.putOnce(key, x.nextValue()); + + // Pairs are separated by ','. We will also tolerate ';'. + + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == '}') { + return; + } + x.back(); + break; + case '}': + return; + default: + throw new RuntimeException("Expected a ',' or '}'"); + } + } + } + + + /** + * Construct a JSONObject from a Map. + * + * @param map A map object that can be used to initialize the contents of + * the JSONObject. + */ + protected JSONObject(HashMap map) { + this.map = new HashMap<>(); + if (map != null) { + Iterator i = map.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry e = (Map.Entry) i.next(); + Object value = e.getValue(); + if (value != null) { + map.put((String) e.getKey(), wrap(value)); + } + } + } + } + + + /** + * @nowebref + */ + public JSONObject(IntDict dict) { + map = new HashMap<>(); + for (int i = 0; i < dict.size(); i++) { + setInt(dict.key(i), dict.value(i)); + } + } + + + /** + * @nowebref + */ + public JSONObject(FloatDict dict) { + map = new HashMap<>(); + for (int i = 0; i < dict.size(); i++) { + setFloat(dict.key(i), dict.value(i)); + } + } + + + /** + * @nowebref + */ + public JSONObject(StringDict dict) { + map = new HashMap<>(); + for (int i = 0; i < dict.size(); i++) { + setString(dict.key(i), dict.value(i)); + } + } + + + /** + * Construct a JSONObject from an Object using bean getters. + * It reflects on all of the public methods of the object. + * For each of the methods with no parameters and a name starting + * with "get" or "is" followed by an uppercase letter, + * the method is invoked, and a key and the value returned from the getter method + * are put into the new JSONObject. + * + * The key is formed by removing the "get" or "is" prefix. + * If the second remaining character is not upper case, then the first + * character is converted to lower case. + * + * For example, if an object has a method named "getName", and + * if the result of calling object.getName() is "Larry Fine", + * then the JSONObject will contain "name": "Larry Fine". + * + * @param bean An object that has getter methods that should be used + * to make a JSONObject. + */ + protected JSONObject(Object bean) { + this(); + this.populateMap(bean); + } + + + // holding off on this method until we decide on how to handle reflection +// /** +// * Construct a JSONObject from an Object, using reflection to find the +// * public members. The resulting JSONObject's keys will be the strings +// * from the names array, and the values will be the field values associated +// * with those keys in the object. If a key is not found or not visible, +// * then it will not be copied into the new JSONObject. +// * @param object An object that has fields that should be used to make a +// * JSONObject. +// * @param names An array of strings, the names of the fields to be obtained +// * from the object. +// */ +// public JSONObject(Object object, String names[]) { +// this(); +// Class c = object.getClass(); +// for (int i = 0; i < names.length; i += 1) { +// String name = names[i]; +// try { +// this.putOpt(name, c.getField(name).get(object)); +// } catch (Exception ignore) { +// } +// } +// } + + + /** + * Construct a JSONObject from a source JSON text string. + * This is the most commonly used JSONObject constructor. + * @param source A string beginning + * with { (left brace) and ending + * with } (right brace). + * @exception RuntimeException If there is a syntax error in the source + * string or a duplicated key. + */ + static public JSONObject parse(String source) { + return new JSONObject(new JSONTokener(source)); + } + + +// /** +// * Construct a JSONObject from a ResourceBundle. +// * @param baseName The ResourceBundle base name. +// * @param locale The Locale to load the ResourceBundle for. +// * @throws JSONException If any JSONExceptions are detected. +// */ +// public JSON(String baseName, Locale locale) { +// this(); +// ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale, +// Thread.currentThread().getContextClassLoader()); +// +// // Iterate through the keys in the bundle. +// +// Enumeration keys = bundle.getKeys(); +// while (keys.hasMoreElements()) { +// Object key = keys.nextElement(); +// if (key instanceof String) { +// +// // Go through the path, ensuring that there is a nested JSONObject for each +// // segment except the last. Add the value using the last segment's name into +// // the deepest nested JSONObject. +// +// String[] path = ((String)key).split("\\."); +// int last = path.length - 1; +// JSON target = this; +// for (int i = 0; i < last; i += 1) { +// String segment = path[i]; +// JSON nextTarget = target.optJSONObject(segment); +// if (nextTarget == null) { +// nextTarget = new JSON(); +// target.put(segment, nextTarget); +// } +// target = nextTarget; +// } +// target.put(path[last], bundle.getString((String)key)); +// } +// } +// } + + +// /** +// * Accumulate values under a key. It is similar to the put method except +// * that if there is already an object stored under the key then a +// * JSONArray is stored under the key to hold all of the accumulated values. +// * If there is already a JSONArray, then the new value is appended to it. +// * In contrast, the put method replaces the previous value. +// * +// * If only one value is accumulated that is not a JSONArray, then the +// * result will be the same as using put. But if multiple values are +// * accumulated, then the result will be like append. +// * @param key A key string. +// * @param value An object to be accumulated under the key. +// * @return this. +// * @throws JSONException If the value is an invalid number +// * or if the key is null. +// */ +// public JSONObject accumulate( +// String key, +// Object value +// ) throws JSONException { +// testValidity(value); +// Object object = this.opt(key); +// if (object == null) { +// this.put(key, value instanceof JSONArray +// ? new JSONArray().put(value) +// : value); +// } else if (object instanceof JSONArray) { +// ((JSONArray)object).put(value); +// } else { +// this.put(key, new JSONArray().put(object).put(value)); +// } +// return this; +// } + + +// /** +// * Append values to the array under a key. If the key does not exist in the +// * JSONObject, then the key is put in the JSONObject with its value being a +// * JSONArray containing the value parameter. If the key was already +// * associated with a JSONArray, then the value parameter is appended to it. +// * @param key A key string. +// * @param value An object to be accumulated under the key. +// * @return this. +// * @throws JSONException If the key is null or if the current value +// * associated with the key is not a JSONArray. +// */ +// public JSONObject append(String key, Object value) throws JSONException { +// testValidity(value); +// Object object = this.opt(key); +// if (object == null) { +// this.put(key, new JSONArray().put(value)); +// } else if (object instanceof JSONArray) { +// this.put(key, ((JSONArray)object).put(value)); +// } else { +// throw new JSONException("JSONObject[" + key + +// "] is not a JSONArray."); +// } +// return this; +// } + + + /** + * Produce a string from a double. The string "null" will be returned if + * the number is not finite. + * @param d A double. + * @return A String. + */ + static protected String doubleToString(double d) { + if (Double.isInfinite(d) || Double.isNaN(d)) { + return "null"; + } + + // Shave off trailing zeros and decimal point, if possible. + + String string = Double.toString(d); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && + string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + + /** + * Get the value object associated with a key. + * + * @param key A key string. + * @return The object associated with the key. + * @throws RuntimeException if the key is not found. + */ + public Object get(String key) { + if (key == null) { + throw new RuntimeException("JSONObject.get(null) called"); + } + Object object = this.opt(key); + if (object == null) { + // Adding for rev 0257 in line with other p5 api + return null; + } + if (object == null) { + throw new RuntimeException("JSONObject[" + quote(key) + "] not found"); + } + return object; + } + + + /** + * Gets the String associated with a key + * + * @webref jsonobject:method + * @brief Gets the string value associated with a key + * @param key a key string + * @return A string which is the value. + * @throws RuntimeException if there is no string value for the key. + * @see JSONObject#getInt(String) + * @see JSONObject#getFloat(String) + * @see JSONObject#getBoolean(String) + */ + public String getString(String key) { + Object object = this.get(key); + if (object == null) { + // Adding for rev 0257 in line with other p5 api + return null; + } + if (object instanceof String) { + return (String)object; + } + throw new RuntimeException("JSONObject[" + quote(key) + "] is not a string"); + } + + + /** + * Get an optional string associated with a key. + * It returns the defaultValue if there is no such key. + * + * @param key A key string. + * @param defaultValue The default. + * @return A string which is the value. + */ + public String getString(String key, String defaultValue) { + Object object = this.opt(key); + return NULL.equals(object) ? defaultValue : object.toString(); + } + + + /** + * Gets the int value associated with a key + * + * @webref jsonobject:method + * @brief Gets the int value associated with a key + * @param key A key string. + * @return The integer value. + * @throws RuntimeException if the key is not found or if the value cannot + * be converted to an integer. + * @see JSONObject#getFloat(String) + * @see JSONObject#getString(String) + * @see JSONObject#getBoolean(String) + */ + public int getInt(String key) { + Object object = this.get(key); + if (object == null) { + throw new RuntimeException("JSONObject[" + quote(key) + "] not found"); + } + try { + return object instanceof Number ? + ((Number)object).intValue() : Integer.parseInt((String)object); + } catch (Exception e) { + throw new RuntimeException("JSONObject[" + quote(key) + "] is not an int."); + } + } + + + /** + * Get an optional int value associated with a key, + * or the default if there is no such key or if the value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public int getInt(String key, int defaultValue) { + try { + return this.getInt(key); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the long value associated with a key. + * + * @param key A key string. + * @return The long value. + * @throws RuntimeException if the key is not found or if the value cannot + * be converted to a long. + */ + public long getLong(String key) { + Object object = this.get(key); + try { + return object instanceof Number + ? ((Number)object).longValue() + : Long.parseLong((String)object); + } catch (Exception e) { + throw new RuntimeException("JSONObject[" + quote(key) + "] is not a long.", e); + } + } + + + /** + * Get an optional long value associated with a key, + * or the default if there is no such key or if the value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public long getLong(String key, long defaultValue) { + try { + return this.getLong(key); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * @webref jsonobject:method + * @brief Gets the float value associated with a key + * @param key a key string + * @see JSONObject#getInt(String) + * @see JSONObject#getString(String) + * @see JSONObject#getBoolean(String) + */ + public float getFloat(String key) { + return (float) getDouble(key); + } + + + public float getFloat(String key, float defaultValue) { + try { + return getFloat(key); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the double value associated with a key. + * @param key A key string. + * @return The numeric value. + * @throws RuntimeException if the key is not found or + * if the value is not a Number object and cannot be converted to a number. + */ + public double getDouble(String key) { + Object object = this.get(key); + try { + return object instanceof Number + ? ((Number)object).doubleValue() + : Double.parseDouble((String)object); + } catch (Exception e) { + throw new RuntimeException("JSONObject[" + quote(key) + "] is not a number."); + } + } + + + /** + * Get an optional double associated with a key, or the + * defaultValue if there is no such key or if its value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public double getDouble(String key, double defaultValue) { + try { + return this.getDouble(key); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the boolean value associated with a key. + * + * @webref jsonobject:method + * @brief Gets the boolean value associated with a key + * @param key a key string + * @return The truth. + * @throws RuntimeException if the value is not a Boolean or the String "true" or "false". + * @see JSONObject#getInt(String) + * @see JSONObject#getFloat(String) + * @see JSONObject#getString(String) + */ + public boolean getBoolean(String key) { + Object object = this.get(key); + if (object.equals(Boolean.FALSE) || + (object instanceof String && + ((String)object).equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) || + (object instanceof String && + ((String)object).equalsIgnoreCase("true"))) { + return true; + } + throw new RuntimeException("JSONObject[" + quote(key) + "] is not a Boolean."); + } + + + /** + * Get an optional boolean associated with a key. + * It returns the defaultValue if there is no such key, or if it is not + * a Boolean or the String "true" or "false" (case insensitive). + * + * @param key A key string. + * @param defaultValue The default. + * @return The truth. + */ + public boolean getBoolean(String key, boolean defaultValue) { + try { + return this.getBoolean(key); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the JSONArray value associated with a key. + * + * @webref jsonobject:method + * @brief Gets the JSONArray value associated with a key + * @param key a key string + * @return A JSONArray which is the value, or null if not present + * @throws RuntimeException if the value is not a JSONArray. + * @see JSONObject#getJSONObject(String) + * @see JSONObject#setJSONObject(String, JSONObject) + * @see JSONObject#setJSONArray(String, JSONArray) + */ + public JSONArray getJSONArray(String key) { + Object object = this.get(key); + if (object == null) { + return null; + } + if (object instanceof JSONArray) { + return (JSONArray)object; + } + throw new RuntimeException("JSONObject[" + quote(key) + "] is not a JSONArray."); + } + + + /** + * Get the JSONObject value associated with a key. + * + * @webref jsonobject:method + * @brief Gets the JSONObject value associated with a key + * @param key a key string + * @return A JSONObject which is the value or null if not available. + * @throws RuntimeException if the value is not a JSONObject. + * @see JSONObject#getJSONArray(String) + * @see JSONObject#setJSONObject(String, JSONObject) + * @see JSONObject#setJSONArray(String, JSONArray) + */ + public JSONObject getJSONObject(String key) { + Object object = this.get(key); + if (object == null) { + return null; + } + if (object instanceof JSONObject) { + return (JSONObject)object; + } + throw new RuntimeException("JSONObject[" + quote(key) + "] is not a JSONObject."); + } + + +// /** +// * Get an array of field names from a JSONObject. +// * +// * @return An array of field names, or null if there are no names. +// */ +// public static String[] getNames(JSONObject jo) { +// int length = jo.length(); +// if (length == 0) { +// return null; +// } +// Iterator iterator = jo.keys(); +// String[] names = new String[length]; +// int i = 0; +// while (iterator.hasNext()) { +// names[i] = (String)iterator.next(); +// i += 1; +// } +// return names; +// } +// +// +// /** +// * Get an array of field names from an Object. +// * +// * @return An array of field names, or null if there are no names. +// */ +// public static String[] getNames(Object object) { +// if (object == null) { +// return null; +// } +// Class klass = object.getClass(); +// Field[] fields = klass.getFields(); +// int length = fields.length; +// if (length == 0) { +// return null; +// } +// String[] names = new String[length]; +// for (int i = 0; i < length; i += 1) { +// names[i] = fields[i].getName(); +// } +// return names; +// } + + + /** + * Determine if the JSONObject contains a specific key. + * @param key A key string. + * @return true if the key exists in the JSONObject. + */ + public boolean hasKey(String key) { + return map.containsKey(key); + } + + +// /** +// * Increment a property of a JSONObject. If there is no such property, +// * create one with a value of 1. If there is such a property, and if +// * it is an Integer, Long, Double, or Float, then add one to it. +// * @param key A key string. +// * @return this. +// * @throws JSONException If there is already a property with this name +// * that is not an Integer, Long, Double, or Float. +// */ +// public JSON increment(String key) { +// Object value = this.opt(key); +// if (value == null) { +// this.put(key, 1); +// } else if (value instanceof Integer) { +// this.put(key, ((Integer)value).intValue() + 1); +// } else if (value instanceof Long) { +// this.put(key, ((Long)value).longValue() + 1); +// } else if (value instanceof Double) { +// this.put(key, ((Double)value).doubleValue() + 1); +// } else if (value instanceof Float) { +// this.put(key, ((Float)value).floatValue() + 1); +// } else { +// throw new RuntimeException("Unable to increment [" + quote(key) + "]."); +// } +// return this; +// } + + + /** + * Determine if the value associated with the key is null or if there is + * no value. + * + * @webref + * @param key A key string. + * @return true if there is no value associated with the key or if + * the value is the JSONObject.NULL object. + */ + public boolean isNull(String key) { + return JSONObject.NULL.equals(this.opt(key)); + } + + + /** + * Get an enumeration of the keys of the JSONObject. + * + * @return An iterator of the keys. + */ + public Iterator keyIterator() { +// return this.keySet().iterator(); + return map.keySet().iterator(); + } + + + /** + * Get a set of keys of the JSONObject. + * + * @return A keySet. + */ + public Set keys() { + return this.map.keySet(); + } + + + /** + * Get the number of keys stored in the JSONObject. + * + * @return The number of keys in the JSONObject. + */ + public int size() { + return this.map.size(); + } + + +// /** +// * Produce a JSONArray containing the names of the elements of this +// * JSONObject. +// * @return A JSONArray containing the key strings, or null if the JSONObject +// * is empty. +// */ +// public JSONArray names() { +// JSONArray ja = new JSONArray(); +// Iterator keys = this.keys(); +// while (keys.hasNext()) { +// ja.append(keys.next()); +// } +// return ja.size() == 0 ? null : ja; +// } + + + /** + * Produce a string from a Number. + * @param number A Number + * @return A String. + * @throws RuntimeException If number is null or a non-finite number. + */ + private static String numberToString(Number number) { + if (number == null) { + throw new RuntimeException("Null pointer"); + } + testValidity(number); + + // Shave off trailing zeros and decimal point, if possible. + + String string = number.toString(); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && + string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + + /** + * Get an optional value associated with a key. + * @param key A key string. + * @return An object which is the value, or null if there is no value. + */ + private Object opt(String key) { + return key == null ? null : this.map.get(key); + } + + +// /** +// * Get an optional boolean associated with a key. +// * It returns false if there is no such key, or if the value is not +// * Boolean.TRUE or the String "true". +// * +// * @param key A key string. +// * @return The truth. +// */ +// private boolean optBoolean(String key) { +// return this.optBoolean(key, false); +// } + + +// /** +// * Get an optional double associated with a key, +// * or NaN if there is no such key or if its value is not a number. +// * If the value is a string, an attempt will be made to evaluate it as +// * a number. +// * +// * @param key A string which is the key. +// * @return An object which is the value. +// */ +// private double optDouble(String key) { +// return this.optDouble(key, Double.NaN); +// } + + +// /** +// * Get an optional int value associated with a key, +// * or zero if there is no such key or if the value is not a number. +// * If the value is a string, an attempt will be made to evaluate it as +// * a number. +// * +// * @param key A key string. +// * @return An object which is the value. +// */ +// private int optInt(String key) { +// return this.optInt(key, 0); +// } + + +// /** +// * Get an optional JSONArray associated with a key. +// * It returns null if there is no such key, or if its value is not a +// * JSONArray. +// * +// * @param key A key string. +// * @return A JSONArray which is the value. +// */ +// private JSONArray optJSONArray(String key) { +// Object o = this.opt(key); +// return o instanceof JSONArray ? (JSONArray)o : null; +// } + + +// /** +// * Get an optional JSONObject associated with a key. +// * It returns null if there is no such key, or if its value is not a +// * JSONObject. +// * +// * @param key A key string. +// * @return A JSONObject which is the value. +// */ +// private JSONObject optJSONObject(String key) { +// Object object = this.opt(key); +// return object instanceof JSONObject ? (JSONObject)object : null; +// } + + +// /** +// * Get an optional long value associated with a key, +// * or zero if there is no such key or if the value is not a number. +// * If the value is a string, an attempt will be made to evaluate it as +// * a number. +// * +// * @param key A key string. +// * @return An object which is the value. +// */ +// public long optLong(String key) { +// return this.optLong(key, 0); +// } + + +// /** +// * Get an optional string associated with a key. +// * It returns an empty string if there is no such key. If the value is not +// * a string and is not null, then it is converted to a string. +// * +// * @param key A key string. +// * @return A string which is the value. +// */ +// public String optString(String key) { +// return this.optString(key, ""); +// } + + + private void populateMap(Object bean) { + Class klass = bean.getClass(); + + // If klass is a System class then set includeSuperClass to false. + + boolean includeSuperClass = klass.getClassLoader() != null; + + Method[] methods = includeSuperClass + ? klass.getMethods() + : klass.getDeclaredMethods(); + for (int i = 0; i < methods.length; i += 1) { + try { + Method method = methods[i]; + if (Modifier.isPublic(method.getModifiers())) { + String name = method.getName(); + String key = ""; + if (name.startsWith("get")) { + if ("getClass".equals(name) || + "getDeclaringClass".equals(name)) { + key = ""; + } else { + key = name.substring(3); + } + } else if (name.startsWith("is")) { + key = name.substring(2); + } + if (key.length() > 0 && + Character.isUpperCase(key.charAt(0)) && + method.getParameterTypes().length == 0) { + if (key.length() == 1) { + key = key.toLowerCase(); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase() + + key.substring(1); + } + + Object result = method.invoke(bean, (Object[])null); + if (result != null) { + this.map.put(key, wrap(result)); + } + } + } + } catch (Exception ignore) { + } + } + } + + + /** + * @webref jsonobject:method + * @brief Put a key/String pair in the JSONObject + * @param key a key string + * @param value the value to assign + * @see JSONObject#setInt(String, int) + * @see JSONObject#setFloat(String, float) + * @see JSONObject#setBoolean(String, boolean) + */ + public JSONObject setString(String key, String value) { + return put(key, value); + } + + + /** + * Put a key/int pair in the JSONObject. + * + * @webref jsonobject:method + * @brief Put a key/int pair in the JSONObject + * @param key a key string + * @param value the value to assign + * @return this. + * @throws RuntimeException If the key is null. + * @see JSONObject#setFloat(String, float) + * @see JSONObject#setString(String, String) + * @see JSONObject#setBoolean(String, boolean) + */ + public JSONObject setInt(String key, int value) { + this.put(key, Integer.valueOf(value)); + return this; + } + + + /** + * Put a key/long pair in the JSONObject. + * + * @param key A key string. + * @param value A long which is the value. + * @return this. + * @throws RuntimeException If the key is null. + */ + public JSONObject setLong(String key, long value) { + this.put(key, Long.valueOf(value)); + return this; + } + + /** + * @webref jsonobject:method + * @brief Put a key/float pair in the JSONObject + * @param key a key string + * @param value the value to assign + * @throws RuntimeException If the key is null or if the number is NaN or infinite. + * @see JSONObject#setInt(String, int) + * @see JSONObject#setString(String, String) + * @see JSONObject#setBoolean(String, boolean) + */ + public JSONObject setFloat(String key, float value) { + this.put(key, Double.valueOf(value)); + return this; + } + + + /** + * Put a key/double pair in the JSONObject. + * + * @param key A key string. + * @param value A double which is the value. + * @return this. + * @throws RuntimeException If the key is null or if the number is NaN or infinite. + */ + public JSONObject setDouble(String key, double value) { + this.put(key, Double.valueOf(value)); + return this; + } + + + /** + * Put a key/boolean pair in the JSONObject. + * + * @webref jsonobject:method + * @brief Put a key/boolean pair in the JSONObject + * @param key a key string + * @param value the value to assign + * @return this. + * @throws RuntimeException If the key is null. + * @see JSONObject#setInt(String, int) + * @see JSONObject#setFloat(String, float) + * @see JSONObject#setString(String, String) + */ + public JSONObject setBoolean(String key, boolean value) { + this.put(key, value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + /** + * @webref jsonobject:method + * @brief Sets the JSONObject value associated with a key + * @param key a key string + * @param value value to assign + * @see JSONObject#setJSONArray(String, JSONArray) + * @see JSONObject#getJSONObject(String) + * @see JSONObject#getJSONArray(String) + */ + public JSONObject setJSONObject(String key, JSONObject value) { + return put(key, value); + } + + /** + * @webref jsonobject:method + * @brief Sets the JSONArray value associated with a key + * @param key a key string + * @param value value to assign + * @see JSONObject#setJSONObject(String, JSONObject) + * @see JSONObject#getJSONObject(String) + * @see JSONObject#getJSONArray(String) + */ + public JSONObject setJSONArray(String key, JSONArray value) { + return put(key, value); + } + + +// /** +// * Put a key/value pair in the JSONObject, where the value will be a +// * JSONArray which is produced from a Collection. +// * @param key A key string. +// * @param value A Collection value. +// * @return this. +// * @throws JSONException +// */ +// public JSONObject put(String key, Collection value) { +// this.put(key, new JSONArray(value)); +// return this; +// } + + +// /** +// * Put a key/value pair in the JSONObject, where the value will be a +// * JSONObject which is produced from a Map. +// * @param key A key string. +// * @param value A Map value. +// * @return this. +// * @throws JSONException +// */ +// //public JSONObject put(String key, HashMap value) { +// public JSONObject put(String key, Map value) { +// this.put(key, new JSONObject(value)); +// return this; +// } + + + /** + * Put a key/value pair in the JSONObject. If the value is null, + * then the key will be removed from the JSONObject if it is present. + * @param key A key string. + * @param value An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, + * or the JSONObject.NULL object. + * @return this. + * @throws RuntimeException If the value is non-finite number + * or if the key is null. + */ + public JSONObject put(String key, Object value) { + String pooled; + if (key == null) { + throw new RuntimeException("Null key."); + } + if (value != null) { + testValidity(value); + pooled = (String)keyPool.get(key); + if (pooled == null) { + if (keyPool.size() >= keyPoolSize) { + keyPool = new HashMap<>(keyPoolSize); + } + keyPool.put(key, key); + } else { + key = pooled; + } + this.map.put(key, value); + } else { + this.remove(key); + } + return this; + } + + + /** + * Put a key/value pair in the JSONObject, but only if the key and the + * value are both non-null, and only if there is not already a member + * with that name. + * @param key + * @param value + * @return {@code this}. + * @throws RuntimeException if the key is a duplicate, or if + * {@link #put(String,Object)} throws. + */ + private JSONObject putOnce(String key, Object value) { + if (key != null && value != null) { + if (this.opt(key) != null) { + throw new RuntimeException("Duplicate key \"" + key + "\""); + } + this.put(key, value); + } + return this; + } + + +// /** +// * Put a key/value pair in the JSONObject, but only if the +// * key and the value are both non-null. +// * @param key A key string. +// * @param value An object which is the value. It should be of one of these +// * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, +// * or the JSONObject.NULL object. +// * @return this. +// * @throws JSONException If the value is a non-finite number. +// */ +// public JSONObject putOpt(String key, Object value) { +// if (key != null && value != null) { +// this.put(key, value); +// } +// return this; +// } + + + /** + * Produce a string in double quotes with backslash sequences in all the + * right places. A backslash will be inserted within = '\u0080' && c < '\u00a0') + || (c >= '\u2000' && c < '\u2100')) { + w.write("\\u"); + hhhh = Integer.toHexString(c); + w.write("0000", 0, 4 - hhhh.length()); + w.write(hhhh); + } else { + w.write(c); + } + } + } + w.write('"'); + return w; + } + + + /** + * Remove a name and its value, if present. + * @param key The name to be removed. + * @return The value that was associated with the name, + * or null if there was no value. + */ + public Object remove(String key) { + return this.map.remove(key); + } + + + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. + * @param string A String. + * @return A simple JSON value. + */ + static protected Object stringToValue(String string) { + Double d; + if (string.equals("")) { + return string; + } + if (string.equalsIgnoreCase("true")) { + return Boolean.TRUE; + } + if (string.equalsIgnoreCase("false")) { + return Boolean.FALSE; + } + if (string.equalsIgnoreCase("null")) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. + * If a number cannot be produced, then the value will just + * be a string. Note that the plus and implied string + * conventions are non-standard. A JSON parser may accept + * non-JSON forms as long as it accepts all correct JSON forms. + */ + + char b = string.charAt(0); + if ((b >= '0' && b <= '9') || b == '.' || b == '-' || b == '+') { + try { + if (string.indexOf('.') > -1 || + string.indexOf('e') > -1 || string.indexOf('E') > -1) { + d = Double.valueOf(string); + if (!d.isInfinite() && !d.isNaN()) { + return d; + } + } else { + Long myLong = Long.valueOf(string); + if (myLong.longValue() == myLong.intValue()) { + return Integer.valueOf(myLong.intValue()); + } else { + return myLong; + } + } + } catch (Exception ignore) { + } + } + return string; + } + + + /** + * Throw an exception if the object is a NaN or infinite number. + * @param o The object to test. If not Float or Double, accepted without + * exceptions. + * @throws RuntimeException If o is infinite or NaN. + */ + static protected void testValidity(Object o) { + if (o != null) { + if (o instanceof Double) { + if (((Double)o).isInfinite() || ((Double)o).isNaN()) { + throw new RuntimeException( + "JSON does not allow non-finite numbers."); + } + } else if (o instanceof Float) { + if (((Float)o).isInfinite() || ((Float)o).isNaN()) { + throw new RuntimeException( + "JSON does not allow non-finite numbers."); + } + } + } + } + + +// /** +// * Produce a JSONArray containing the values of the members of this +// * JSONObject. +// * @param names A JSONArray containing a list of key strings. This +// * determines the sequence of the values in the result. +// * @return A JSONArray of values. +// * @throws JSONException If any of the values are non-finite numbers. +// */ +// public JSONArray toJSONArray(JSONArray names) { +// if (names == null || names.size() == 0) { +// return null; +// } +// JSONArray ja = new JSONArray(); +// for (int i = 0; i < names.size(); i += 1) { +// ja.append(this.opt(names.getString(i))); +// } +// return ja; +// } + + +// protected boolean save(OutputStream output) { +// return save(PApplet.createWriter(output)); +// } + + + public boolean save(File file, String options) { + PrintWriter writer = PApplet.createWriter(file); + boolean success = write(writer, options); + writer.close(); + return success; + } + + + public boolean write(PrintWriter output) { + return write(output, null); + } + + + public boolean write(PrintWriter output, String options) { + int indentFactor = 2; + if (options != null) { + String[] opts = PApplet.split(options, ','); + for (String opt : opts) { + if (opt.equals("compact")) { + indentFactor = -1; + } else if (opt.startsWith("indent=")) { + indentFactor = PApplet.parseInt(opt.substring(7), -2); + if (indentFactor == -2) { + throw new IllegalArgumentException("Could not read a number from " + opt); + } + } else { + System.err.println("Ignoring " + opt); + } + } + } + output.print(format(indentFactor)); + output.flush(); + return true; + } + + + /** + * Return the JSON data formatted with two spaces for indents. + * Chosen to do this since it's the most common case (e.g. with println()). + * Same as format(2). Use the format() function for more options. + */ + @Override + public String toString() { + try { + return format(2); + } catch (Exception e) { + return null; + } + } + + + /** + * Make a prettyprinted JSON text of this JSONObject. + *

        + * Warning: This method assumes that the data structure is acyclical. + * @param indentFactor The number of spaces to add to each level of + * indentation. + * @return a printable, displayable, portable, transmittable + * representation of the object, beginning + * with { (left brace) and ending + * with } (right brace). + * @throws RuntimeException If the object contains an invalid number. + */ + public String format(int indentFactor) { + StringWriter w = new StringWriter(); + synchronized (w.getBuffer()) { + return this.writeInternal(w, indentFactor, 0).toString(); + } + } + + /** + * Make a JSON text of an Object value. If the object has an + * value.toJSONString() method, then that method will be used to produce + * the JSON text. The method is required to produce a strictly + * conforming text. If the object does not contain a toJSONString + * method (which is the most common case), then a text will be + * produced by other means. If the value is an array or Collection, + * then a JSONArray will be made from it and its toJSONString method + * will be called. If the value is a MAP, then a JSONObject will be made + * from it and its toJSONString method will be called. Otherwise, the + * value's toString method will be called, and the result will be quoted. + * + *

        + * Warning: This method assumes that the data structure is acyclical. + * @param value The value to be serialized. + * @return a printable, displayable, transmittable + * representation of the object, beginning + * with { (left brace) and ending + * with } (right brace). + * @throws RuntimeException If the value is or contains an invalid number. + */ + static protected String valueToString(Object value) { + if (value == null || value.equals(null)) { + return "null"; + } +// if (value instanceof JSONString) { +// Object object; +// try { +// object = ((JSONString)value).toJSONString(); +// } catch (Exception e) { +// throw new RuntimeException(e); +// } +// if (object instanceof String) { +// return (String)object; +// } +// throw new RuntimeException("Bad value from toJSONString: " + object); +// } + if (value instanceof Number) { + return numberToString((Number) value); + } + if (value instanceof Boolean || value instanceof JSONObject || + value instanceof JSONArray) { + return value.toString(); + } + if (value instanceof Map) { + return new JSONObject(value).toString(); + } + if (value instanceof Collection) { + return new JSONArray(value).toString(); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(); + } + return quote(value.toString()); + } + + /** + * Wrap an object, if necessary. If the object is null, return the NULL + * object. If it is an array or collection, wrap it in a JSONArray. If + * it is a map, wrap it in a JSONObject. If it is a standard property + * (Double, String, et al) then it is already wrapped. Otherwise, if it + * comes from one of the java packages, turn it into a string. And if + * it doesn't, try to wrap it in a JSONObject. If the wrapping fails, + * then null is returned. + * + * @param object The object to wrap + * @return The wrapped value + */ + static protected Object wrap(Object object) { + try { + if (object == null) { + return NULL; + } + if (object instanceof JSONObject || object instanceof JSONArray || + NULL.equals(object) || /*object instanceof JSONString ||*/ + object instanceof Byte || object instanceof Character || + object instanceof Short || object instanceof Integer || + object instanceof Long || object instanceof Boolean || + object instanceof Float || object instanceof Double || + object instanceof String) { + return object; + } + + if (object instanceof Collection) { + return new JSONArray(object); + } + if (object.getClass().isArray()) { + return new JSONArray(object); + } + if (object instanceof Map) { + return new JSONObject(object); + } + Package objectPackage = object.getClass().getPackage(); + String objectPackageName = objectPackage != null + ? objectPackage.getName() + : ""; + if ( + objectPackageName.startsWith("java.") || + objectPackageName.startsWith("javax.") || + object.getClass().getClassLoader() == null + ) { + return object.toString(); + } + return new JSONObject(object); + } catch(Exception exception) { + return null; + } + } + + +// /** +// * Write the contents of the JSONObject as JSON text to a writer. +// * For compactness, no whitespace is added. +// *

        +// * Warning: This method assumes that the data structure is acyclical. +// * +// * @return The writer. +// * @throws JSONException +// */ +// protected Writer write(Writer writer) { +// return this.write(writer, 0, 0); +// } + + + static final Writer writeValue(Writer writer, Object value, + int indentFactor, int indent) throws IOException { + if (value == null || value.equals(null)) { + writer.write("null"); + } else if (value instanceof JSONObject) { + ((JSONObject) value).writeInternal(writer, indentFactor, indent); + } else if (value instanceof JSONArray) { + ((JSONArray) value).writeInternal(writer, indentFactor, indent); + } else if (value instanceof Map) { + new JSONObject(value).writeInternal(writer, indentFactor, indent); + } else if (value instanceof Collection) { + new JSONArray(value).writeInternal(writer, indentFactor, + indent); + } else if (value.getClass().isArray()) { + new JSONArray(value).writeInternal(writer, indentFactor, indent); + } else if (value instanceof Number) { + writer.write(numberToString((Number) value)); + } else if (value instanceof Boolean) { + writer.write(value.toString()); + /* + } else if (value instanceof JSONString) { + Object o; + try { + o = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + writer.write(o != null ? o.toString() : quote(value.toString())); + */ + } else { + quote(value.toString(), writer); + } + return writer; + } + + + static final void indent(Writer writer, int indent) throws IOException { + for (int i = 0; i < indent; i += 1) { + writer.write(' '); + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. + *

        + * Warning: This method assumes that the data structure is acyclical. + * + * @return The writer. + * @throws RuntimeException + */ + protected Writer writeInternal(Writer writer, int indentFactor, int indent) { + try { + boolean commanate = false; + final int length = this.size(); + Iterator keys = this.keyIterator(); + writer.write('{'); + + int actualFactor = (indentFactor == -1) ? 0 : indentFactor; + + if (length == 1) { + Object key = keys.next(); + writer.write(quote(key.toString())); + writer.write(':'); + if (actualFactor > 0) { + writer.write(' '); + } + //writeValue(writer, this.map.get(key), actualFactor, indent); + writeValue(writer, this.map.get(key), indentFactor, indent); + } else if (length != 0) { + final int newIndent = indent + actualFactor; + while (keys.hasNext()) { + Object key = keys.next(); + if (commanate) { + writer.write(','); + } + if (indentFactor != -1) { + writer.write('\n'); + } + indent(writer, newIndent); + writer.write(quote(key.toString())); + writer.write(':'); + if (actualFactor > 0) { + writer.write(' '); + } + //writeValue(writer, this.map.get(key), actualFactor, newIndent); + writeValue(writer, this.map.get(key), indentFactor, newIndent); + commanate = true; + } + if (indentFactor != -1) { + writer.write('\n'); + } + indent(writer, indent); + } + writer.write('}'); + return writer; + } catch (IOException exception) { + throw new RuntimeException(exception); + } + } + + +// // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +// +// +// class JSONException extends RuntimeException { +// +// public JSONException(String message) { +// super(message); +// } +// +// public JSONException(Throwable throwable) { +// super(throwable); +// } +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + +// /** +// * Get the hex value of a character (base16). +// * @param c A character between '0' and '9' or between 'A' and 'F' or +// * between 'a' and 'f'. +// * @return An int between 0 and 15, or -1 if c was not a hex digit. +// */ +// static protected int dehexchar(char c) { +// if (c >= '0' && c <= '9') { +// return c - '0'; +// } +// if (c >= 'A' && c <= 'F') { +// return c - ('A' - 10); +// } +// if (c >= 'a' && c <= 'f') { +// return c - ('a' - 10); +// } +// return -1; +// } + + +// static class JSONTokener { +// private long character; +// private boolean eof; +// private long index; +// private long line; +// private char previous; +// private Reader reader; +// private boolean usePrevious; +// +// +// /** +// * Construct a JSONTokener from a Reader. +// * +// * @param reader A reader. +// */ +// public JSONTokener(Reader reader) { +// this.reader = reader.markSupported() +// ? reader +// : new BufferedReader(reader); +// this.eof = false; +// this.usePrevious = false; +// this.previous = 0; +// this.index = 0; +// this.character = 1; +// this.line = 1; +// } +// +// +// /** +// * Construct a JSONTokener from an InputStream. +// */ +// public JSONTokener(InputStream inputStream) { +// this(new InputStreamReader(inputStream)); +// } +// +// +// /** +// * Construct a JSONTokener from a string. +// * +// * @param s A source string. +// */ +// public JSONTokener(String s) { +// this(new StringReader(s)); +// } +// +// +// /** +// * Back up one character. This provides a sort of lookahead capability, +// * so that you can test for a digit or letter before attempting to parse +// * the next number or identifier. +// */ +// public void back() { +// if (this.usePrevious || this.index <= 0) { +// throw new RuntimeException("Stepping back two steps is not supported"); +// } +// this.index -= 1; +// this.character -= 1; +// this.usePrevious = true; +// this.eof = false; +// } +// +// +// public boolean end() { +// return this.eof && !this.usePrevious; +// } +// +// +// /** +// * Determine if the source string still contains characters that next() +// * can consume. +// * @return true if not yet at the end of the source. +// */ +// public boolean more() { +// this.next(); +// if (this.end()) { +// return false; +// } +// this.back(); +// return true; +// } +// +// +// /** +// * Get the next character in the source string. +// * +// * @return The next character, or 0 if past the end of the source string. +// */ +// public char next() { +// int c; +// if (this.usePrevious) { +// this.usePrevious = false; +// c = this.previous; +// } else { +// try { +// c = this.reader.read(); +// } catch (IOException exception) { +// throw new RuntimeException(exception); +// } +// +// if (c <= 0) { // End of stream +// this.eof = true; +// c = 0; +// } +// } +// this.index += 1; +// if (this.previous == '\r') { +// this.line += 1; +// this.character = c == '\n' ? 0 : 1; +// } else if (c == '\n') { +// this.line += 1; +// this.character = 0; +// } else { +// this.character += 1; +// } +// this.previous = (char) c; +// return this.previous; +// } +// +// +// /** +// * Consume the next character, and check that it matches a specified +// * character. +// * @param c The character to match. +// * @return The character. +// * @throws JSONException if the character does not match. +// */ +// public char next(char c) { +// char n = this.next(); +// if (n != c) { +// throw new RuntimeException("Expected '" + c + "' and instead saw '" + n + "'"); +// } +// return n; +// } +// +// +// /** +// * Get the next n characters. +// * +// * @param n The number of characters to take. +// * @return A string of n characters. +// * @throws JSONException +// * Substring bounds error if there are not +// * n characters remaining in the source string. +// */ +// public String next(int n) { +// if (n == 0) { +// return ""; +// } +// +// char[] chars = new char[n]; +// int pos = 0; +// +// while (pos < n) { +// chars[pos] = this.next(); +// if (this.end()) { +// throw new RuntimeException("Substring bounds error"); +// } +// pos += 1; +// } +// return new String(chars); +// } +// +// +// /** +// * Get the next char in the string, skipping whitespace. +// * @throws JSONException +// * @return A character, or 0 if there are no more characters. +// */ +// public char nextClean() { +// for (;;) { +// char c = this.next(); +// if (c == 0 || c > ' ') { +// return c; +// } +// } +// } +// +// +// /** +// * Return the characters up to the next close quote character. +// * Backslash processing is done. The formal JSON format does not +// * allow strings in single quotes, but an implementation is allowed to +// * accept them. +// * @param quote The quoting character, either +// * " (double quote) or +// * ' (single quote). +// * @return A String. +// * @throws JSONException Unterminated string. +// */ +// public String nextString(char quote) { +// char c; +// StringBuffer sb = new StringBuffer(); +// for (;;) { +// c = this.next(); +// switch (c) { +// case 0: +// case '\n': +// case '\r': +// throw new RuntimeException("Unterminated string"); +// case '\\': +// c = this.next(); +// switch (c) { +// case 'b': +// sb.append('\b'); +// break; +// case 't': +// sb.append('\t'); +// break; +// case 'n': +// sb.append('\n'); +// break; +// case 'f': +// sb.append('\f'); +// break; +// case 'r': +// sb.append('\r'); +// break; +// case 'u': +// sb.append((char)Integer.parseInt(this.next(4), 16)); +// break; +// case '"': +// case '\'': +// case '\\': +// case '/': +// sb.append(c); +// break; +// default: +// throw new RuntimeException("Illegal escape."); +// } +// break; +// default: +// if (c == quote) { +// return sb.toString(); +// } +// sb.append(c); +// } +// } +// } +// +// +// /** +// * Get the text up but not including the specified character or the +// * end of line, whichever comes first. +// * @param delimiter A delimiter character. +// * @return A string. +// */ +// public String nextTo(char delimiter) { +// StringBuffer sb = new StringBuffer(); +// for (;;) { +// char c = this.next(); +// if (c == delimiter || c == 0 || c == '\n' || c == '\r') { +// if (c != 0) { +// this.back(); +// } +// return sb.toString().trim(); +// } +// sb.append(c); +// } +// } +// +// +// /** +// * Get the text up but not including one of the specified delimiter +// * characters or the end of line, whichever comes first. +// * @param delimiters A set of delimiter characters. +// * @return A string, trimmed. +// */ +// public String nextTo(String delimiters) { +// char c; +// StringBuffer sb = new StringBuffer(); +// for (;;) { +// c = this.next(); +// if (delimiters.indexOf(c) >= 0 || c == 0 || +// c == '\n' || c == '\r') { +// if (c != 0) { +// this.back(); +// } +// return sb.toString().trim(); +// } +// sb.append(c); +// } +// } +// +// +// /** +// * Get the next value. The value can be a Boolean, Double, Integer, +// * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object. +// * @throws JSONException If syntax error. +// * +// * @return An object. +// */ +// public Object nextValue() { +// char c = this.nextClean(); +// String string; +// +// switch (c) { +// case '"': +// case '\'': +// return this.nextString(c); +// case '{': +// this.back(); +// return new JSONObject(this); +// case '[': +// this.back(); +// return new JSONArray(this); +// } +// +// /* +// * Handle unquoted text. This could be the values true, false, or +// * null, or it can be a number. An implementation (such as this one) +// * is allowed to also accept non-standard forms. +// * +// * Accumulate characters until we reach the end of the text or a +// * formatting character. +// */ +// +// StringBuffer sb = new StringBuffer(); +// while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { +// sb.append(c); +// c = this.next(); +// } +// this.back(); +// +// string = sb.toString().trim(); +// if ("".equals(string)) { +// throw new RuntimeException("Missing value"); +// } +// return JSONObject.stringToValue(string); +// } +// +// +// /** +// * Skip characters until the next character is the requested character. +// * If the requested character is not found, no characters are skipped. +// * @param to A character to skip to. +// * @return The requested character, or zero if the requested character +// * is not found. +// */ +// public char skipTo(char to) { +// char c; +// try { +// long startIndex = this.index; +// long startCharacter = this.character; +// long startLine = this.line; +// this.reader.mark(1000000); +// do { +// c = this.next(); +// if (c == 0) { +// this.reader.reset(); +// this.index = startIndex; +// this.character = startCharacter; +// this.line = startLine; +// return c; +// } +// } while (c != to); +// } catch (IOException exc) { +// throw new RuntimeException(exc); +// } +// +// this.back(); +// return c; +// } +// +// +// /** +// * Make a printable string of this JSONTokener. +// * +// * @return " at {index} [character {character} line {line}]" +// */ +// @Override +// public String toString() { +// return " at " + this.index + " [character " + this.character + " line " + +// this.line + "]"; +// } +// } +} diff --git a/src/main/java/processing/data/JSONTokener.java b/src/main/java/processing/data/JSONTokener.java new file mode 100644 index 0000000..5301a5e --- /dev/null +++ b/src/main/java/processing/data/JSONTokener.java @@ -0,0 +1,435 @@ +package processing.data; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +/** + * A JSONTokener takes a source string and extracts characters and tokens from + * it. It is used by the JSONObject and JSONArray constructors to parse + * JSON source strings. + * @author JSON.org + * @version 2012-02-16 + */ +class JSONTokener { + + private long character; + private boolean eof; + private long index; + private long line; + private char previous; + private Reader reader; + private boolean usePrevious; + + + /** + * Construct a JSONTokener from a Reader. + * + * @param reader A reader. + */ + public JSONTokener(Reader reader) { + this.reader = reader.markSupported() + ? reader + : new BufferedReader(reader); + this.eof = false; + this.usePrevious = false; + this.previous = 0; + this.index = 0; + this.character = 1; + this.line = 1; + } + + + /** + * Construct a JSONTokener from an InputStream. + */ + public JSONTokener(InputStream inputStream) { + this(new InputStreamReader(inputStream)); + } + + + /** + * Construct a JSONTokener from a string. + * + * @param s A source string. + */ + public JSONTokener(String s) { + this(new StringReader(s)); + } + + + /** + * Back up one character. This provides a sort of lookahead capability, + * so that you can test for a digit or letter before attempting to parse + * the next number or identifier. + */ + public void back() { + if (this.usePrevious || this.index <= 0) { + throw new RuntimeException("Stepping back two steps is not supported"); + } + this.index -= 1; + this.character -= 1; + this.usePrevious = true; + this.eof = false; + } + + + /** + * Get the hex value of a character (base16). + * @param c A character between '0' and '9' or between 'A' and 'F' or + * between 'a' and 'f'. + * @return An int between 0 and 15, or -1 if c was not a hex digit. + */ + public static int dehexchar(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - ('A' - 10); + } + if (c >= 'a' && c <= 'f') { + return c - ('a' - 10); + } + return -1; + } + + public boolean end() { + return this.eof && !this.usePrevious; + } + + + /** + * Determine if the source string still contains characters that next() + * can consume. + * @return true if not yet at the end of the source. + */ + public boolean more() { + this.next(); + if (this.end()) { + return false; + } + this.back(); + return true; + } + + + /** + * Get the next character in the source string. + * + * @return The next character, or 0 if past the end of the source string. + */ + public char next() { + int c; + if (this.usePrevious) { + this.usePrevious = false; + c = this.previous; + } else { + try { + c = this.reader.read(); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + + if (c <= 0) { // End of stream + this.eof = true; + c = 0; + } + } + this.index += 1; + if (this.previous == '\r') { + this.line += 1; + this.character = c == '\n' ? 0 : 1; + } else if (c == '\n') { + this.line += 1; + this.character = 0; + } else { + this.character += 1; + } + this.previous = (char) c; + return this.previous; + } + + + /** + * Consume the next character, and check that it matches a specified + * character. + * @param c The character to match. + * @return The character. + * @throws JSONException if the character does not match. + */ + public char next(char c) { + char n = this.next(); + if (n != c) { + throw new RuntimeException("Expected '" + c + "' and instead saw '" + n + "'"); + } + return n; + } + + + /** + * Get the next n characters. + * + * @param n The number of characters to take. + * @return A string of n characters. + * @throws JSONException + * Substring bounds error if there are not + * n characters remaining in the source string. + */ + public String next(int n) { + if (n == 0) { + return ""; + } + + char[] chars = new char[n]; + int pos = 0; + + while (pos < n) { + chars[pos] = this.next(); + if (this.end()) { + throw new RuntimeException("Substring bounds error"); + } + pos += 1; + } + return new String(chars); + } + + + /** + * Get the next char in the string, skipping whitespace. + * @throws JSONException + * @return A character, or 0 if there are no more characters. + */ + public char nextClean() { + for (;;) { + char c = this.next(); + if (c == 0 || c > ' ') { + return c; + } + } + } + + + /** + * Return the characters up to the next close quote character. + * Backslash processing is done. The formal JSON format does not + * allow strings in single quotes, but an implementation is allowed to + * accept them. + * @param quote The quoting character, either + * " (double quote) or + * ' (single quote). + * @return A String. + * @throws JSONException Unterminated string. + */ + public String nextString(char quote) { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + switch (c) { + case 0: + case '\n': + case '\r': + throw new RuntimeException("Unterminated string"); + case '\\': + c = this.next(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + sb.append((char)Integer.parseInt(this.next(4), 16)); + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw new RuntimeException("Illegal escape."); + } + break; + default: + if (c == quote) { + return sb.toString(); + } + sb.append(c); + } + } + } + + + /** + * Get the text up but not including the specified character or the + * end of line, whichever comes first. + * @param delimiter A delimiter character. + * @return A string. + */ + public String nextTo(char delimiter) { + StringBuilder sb = new StringBuilder(); + for (;;) { + char c = this.next(); + if (c == delimiter || c == 0 || c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the text up but not including one of the specified delimiter + * characters or the end of line, whichever comes first. + * @param delimiters A set of delimiter characters. + * @return A string, trimmed. + */ + public String nextTo(String delimiters) { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + if (delimiters.indexOf(c) >= 0 || c == 0 || + c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the next value. The value can be a Boolean, Double, Integer, + * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object. + * @throws JSONException If syntax error. + * + * @return An object. + */ + public Object nextValue() { + char c = this.nextClean(); + String string; + + switch (c) { + case '"': + case '\'': + return this.nextString(c); + case '{': + this.back(); + return new JSONObject(this); + case '[': + this.back(); + return new JSONArray(this); + } + + /* + * Handle unquoted text. This could be the values true, false, or + * null, or it can be a number. An implementation (such as this one) + * is allowed to also accept non-standard forms. + * + * Accumulate characters until we reach the end of the text or a + * formatting character. + */ + + StringBuilder sb = new StringBuilder(); + while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { + sb.append(c); + c = this.next(); + } + this.back(); + + string = sb.toString().trim(); + if ("".equals(string)) { + throw new RuntimeException("Missing value"); + } + return JSONObject.stringToValue(string); + } + + + /** + * Skip characters until the next character is the requested character. + * If the requested character is not found, no characters are skipped. + * @param to A character to skip to. + * @return The requested character, or zero if the requested character + * is not found. + */ + public char skipTo(char to) { + char c; + try { + long startIndex = this.index; + long startCharacter = this.character; + long startLine = this.line; + this.reader.mark(1000000); + do { + c = this.next(); + if (c == 0) { + this.reader.reset(); + this.index = startIndex; + this.character = startCharacter; + this.line = startLine; + return c; + } + } while (c != to); + } catch (IOException exc) { + throw new RuntimeException(exc); + } + + this.back(); + return c; + } + + + /** + * Make a printable string of this JSONTokener. + * + * @return " at {index} [character {character} line {line}]" + */ + @Override + public String toString() { + return " at " + this.index + " [character " + this.character + " line " + + this.line + "]"; + } +} diff --git a/src/main/java/processing/data/Sort.java b/src/main/java/processing/data/Sort.java new file mode 100644 index 0000000..b42e0f1 --- /dev/null +++ b/src/main/java/processing/data/Sort.java @@ -0,0 +1,46 @@ +package processing.data; + + +/** + * Internal sorter used by several data classes. + * Advanced users only, not official API. + */ +public abstract class Sort implements Runnable { + + public Sort() { } + + + public void run() { + int c = size(); + if (c > 1) { + sort(0, c - 1); + } + } + + + protected void sort(int i, int j) { + int pivotIndex = (i+j)/2; + swap(pivotIndex, j); + int k = partition(i-1, j); + swap(k, j); + if ((k-i) > 1) sort(i, k-1); + if ((j-k) > 1) sort(k+1, j); + } + + + protected int partition(int left, int right) { + int pivot = right; + do { + while (compare(++left, pivot) < 0) { } + while ((right != 0) && (compare(--right, pivot) > 0)) { } + swap(left, right); + } while (left < right); + swap(left, right); + return left; + } + + + abstract public int size(); + abstract public float compare(int a, int b); + abstract public void swap(int a, int b); +} \ No newline at end of file diff --git a/src/main/java/processing/data/StringDict.java b/src/main/java/processing/data/StringDict.java new file mode 100644 index 0000000..eebffaa --- /dev/null +++ b/src/main/java/processing/data/StringDict.java @@ -0,0 +1,601 @@ +package processing.data; + +import java.io.*; +import java.util.HashMap; +import java.util.Iterator; + +import processing.core.PApplet; + + +/** + * A simple table class to use a String as a lookup for another String value. + * + * @webref data:composite + * @see IntDict + * @see FloatDict + */ +public class StringDict { + + /** Number of elements in the table */ + protected int count; + + protected String[] keys; + protected String[] values; + + /** Internal implementation for faster lookups */ + private HashMap indices = new HashMap<>(); + + + public StringDict() { + count = 0; + keys = new String[10]; + values = new String[10]; + } + + + /** + * Create a new lookup pre-allocated to a specific length. This will not + * change the size(), but is more efficient than not specifying a length. + * Use it when you know the rough size of the thing you're creating. + * + * @nowebref + */ + public StringDict(int length) { + count = 0; + keys = new String[length]; + values = new String[length]; + } + + + /** + * Read a set of entries from a Reader that has each key/value pair on + * a single line, separated by a tab. + * + * @nowebref + */ + public StringDict(BufferedReader reader) { + String[] lines = PApplet.loadStrings(reader); + keys = new String[lines.length]; + values = new String[lines.length]; + + for (int i = 0; i < lines.length; i++) { + String[] pieces = PApplet.split(lines[i], '\t'); + if (pieces.length == 2) { + keys[count] = pieces[0]; + values[count] = pieces[1]; + indices.put(keys[count], count); + count++; + } + } + } + + + /** + * @nowebref + */ + public StringDict(String[] keys, String[] values) { + if (keys.length != values.length) { + throw new IllegalArgumentException("key and value arrays must be the same length"); + } + this.keys = keys; + this.values = values; + count = keys.length; + for (int i = 0; i < count; i++) { + indices.put(keys[i], i); + } + } + + + /** + * Constructor to allow (more intuitive) inline initialization, e.g.: + *

        +   * new StringDict(new String[][] {
        +   *   { "key1", "value1" },
        +   *   { "key2", "value2" }
        +   * });
        +   * 
        + * It's no Python, but beats a static { } block with HashMap.put() statements. + */ + public StringDict(String[][] pairs) { + count = pairs.length; + this.keys = new String[count]; + this.values = new String[count]; + for (int i = 0; i < count; i++) { + keys[i] = pairs[i][0]; + values[i] = pairs[i][1]; + indices.put(keys[i], i); + } + } + + + /** + * Create a dictionary that maps between column titles and cell entries + * in a TableRow. If two columns have the same name, the later column's + * values will override the earlier values. + */ + public StringDict(TableRow row) { + this(row.getColumnCount()); + + String[] titles = row.getColumnTitles(); + if (titles == null) { + titles = new StringList(IntList.fromRange(row.getColumnCount())).array(); + } + for (int col = 0; col < row.getColumnCount(); col++) { + set(titles[col], row.getString(col)); + } + // remove unused and overwritten entries + crop(); + } + + + /** + * @webref stringdict:method + * @brief Returns the number of key/value pairs + */ + public int size() { + return count; + } + + + /** + * Resize the internal data, this can only be used to shrink the list. + * Helpful for situations like sorting and then grabbing the top 50 entries. + */ + public void resize(int length) { + if (length > count) { + throw new IllegalArgumentException("resize() can only be used to shrink the dictionary"); + } + if (length < 1) { + throw new IllegalArgumentException("resize(" + length + ") is too small, use 1 or higher"); + } + + String[] newKeys = new String[length]; + String[] newValues = new String[length]; + PApplet.arrayCopy(keys, newKeys, length); + PApplet.arrayCopy(values, newValues, length); + keys = newKeys; + values = newValues; + count = length; + resetIndices(); + } + + + /** + * Remove all entries. + * + * @webref stringdict:method + * @brief Remove all entries + */ + public void clear() { + count = 0; + indices = new HashMap<>(); + } + + + private void resetIndices() { + indices = new HashMap<>(count); + for (int i = 0; i < count; i++) { + indices.put(keys[i], i); + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public class Entry { + public String key; + public String value; + + Entry(String key, String value) { + this.key = key; + this.value = value; + } + } + + + public Iterable entries() { + return new Iterable() { + + public Iterator iterator() { + return entryIterator(); + } + }; + } + + + public Iterator entryIterator() { + return new Iterator() { + int index = -1; + + public void remove() { + removeIndex(index); + index--; + } + + public Entry next() { + ++index; + Entry e = new Entry(keys[index], values[index]); + return e; + } + + public boolean hasNext() { + return index+1 < size(); + } + }; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public String key(int index) { + return keys[index]; + } + + + protected void crop() { + if (count != keys.length) { + keys = PApplet.subset(keys, 0, count); + values = PApplet.subset(values, 0, count); + } + } + + + public Iterable keys() { + return new Iterable() { + + @Override + public Iterator iterator() { + return keyIterator(); + } + }; + } + + + // Use this to iterate when you want to be able to remove elements along the way + public Iterator keyIterator() { + return new Iterator() { + int index = -1; + + public void remove() { + removeIndex(index); + index--; + } + + public String next() { + return key(++index); + } + + public boolean hasNext() { + return index+1 < size(); + } + }; + } + + + /** + * Return a copy of the internal keys array. This array can be modified. + * + * @webref stringdict:method + * @brief Return a copy of the internal keys array + */ + public String[] keyArray() { + crop(); + return keyArray(null); + } + + + public String[] keyArray(String[] outgoing) { + if (outgoing == null || outgoing.length != count) { + outgoing = new String[count]; + } + System.arraycopy(keys, 0, outgoing, 0, count); + return outgoing; + } + + + public String value(int index) { + return values[index]; + } + + /** + * @webref stringdict:method + * @brief Return the internal array being used to store the values + */ + public Iterable values() { + return new Iterable() { + + @Override + public Iterator iterator() { + return valueIterator(); + } + }; + } + + + public Iterator valueIterator() { + return new Iterator() { + int index = -1; + + public void remove() { + removeIndex(index); + index--; + } + + public String next() { + return value(++index); + } + + public boolean hasNext() { + return index+1 < size(); + } + }; + } + + + /** + * Create a new array and copy each of the values into it. + * + * @webref stringdict:method + * @brief Create a new array and copy each of the values into it + */ + public String[] valueArray() { + crop(); + return valueArray(null); + } + + + /** + * Fill an already-allocated array with the values (more efficient than + * creating a new array each time). If 'array' is null, or not the same + * size as the number of values, a new array will be allocated and returned. + */ + public String[] valueArray(String[] array) { + if (array == null || array.length != size()) { + array = new String[count]; + } + System.arraycopy(values, 0, array, 0, count); + return array; + } + + + /** + * Return a value for the specified key. + * + * @webref stringdict:method + * @brief Return a value for the specified key + */ + public String get(String key) { + int index = index(key); + if (index == -1) return null; + return values[index]; + } + + + public String get(String key, String alternate) { + int index = index(key); + if (index == -1) return alternate; + return values[index]; + } + + + /** + * @webref stringdict:method + * @brief Create a new key/value pair or change the value of one + */ + public void set(String key, String value) { + int index = index(key); + if (index == -1) { + create(key, value); + } else { + values[index] = value; + } + } + + + public void setIndex(int index, String key, String value) { + if (index < 0 || index >= count) { + throw new ArrayIndexOutOfBoundsException(index); + } + keys[index] = key; + values[index] = value; + } + + + public int index(String what) { + Integer found = indices.get(what); + return (found == null) ? -1 : found.intValue(); + } + + + /** + * @webref stringdict:method + * @brief Check if a key is a part of the data structure + */ + public boolean hasKey(String key) { + return index(key) != -1; + } + + + protected void create(String key, String value) { + if (count == keys.length) { + keys = PApplet.expand(keys); + values = PApplet.expand(values); + } + indices.put(key, Integer.valueOf(count)); + keys[count] = key; + values[count] = value; + count++; + } + + /** + * @webref stringdict:method + * @brief Remove a key/value pair + */ + public int remove(String key) { + int index = index(key); + if (index != -1) { + removeIndex(index); + } + return index; + } + + + public String removeIndex(int index) { + if (index < 0 || index >= count) { + throw new ArrayIndexOutOfBoundsException(index); + } + //System.out.println("index is " + which + " and " + keys[which]); + String key = keys[index]; + indices.remove(key); + for (int i = index; i < count-1; i++) { + keys[i] = keys[i+1]; + values[i] = values[i+1]; + indices.put(keys[i], i); + } + count--; + keys[count] = null; + values[count] = null; + return key; + } + + + public void swap(int a, int b) { + String tkey = keys[a]; + String tvalue = values[a]; + keys[a] = keys[b]; + values[a] = values[b]; + keys[b] = tkey; + values[b] = tvalue; + +// indices.put(keys[a], Integer.valueOf(a)); +// indices.put(keys[b], Integer.valueOf(b)); + } + + + /** + * Sort the keys alphabetically (ignoring case). Uses the value as a + * tie-breaker (only really possible with a key that has a case change). + * + * @webref stringdict:method + * @brief Sort the keys alphabetically + */ + public void sortKeys() { + sortImpl(true, false); + } + + /** + * @webref stringdict:method + * @brief Sort the keys alphabetically in reverse + */ + public void sortKeysReverse() { + sortImpl(true, true); + } + + + /** + * Sort by values in descending order (largest value will be at [0]). + * + * @webref stringdict:method + * @brief Sort by values in ascending order + */ + public void sortValues() { + sortImpl(false, false); + } + + + /** + * @webref stringdict:method + * @brief Sort by values in descending order + */ + public void sortValuesReverse() { + sortImpl(false, true); + } + + + protected void sortImpl(final boolean useKeys, final boolean reverse) { + Sort s = new Sort() { + @Override + public int size() { + return count; + } + + @Override + public float compare(int a, int b) { + int diff = 0; + if (useKeys) { + diff = keys[a].compareToIgnoreCase(keys[b]); + if (diff == 0) { + diff = values[a].compareToIgnoreCase(values[b]); + } + } else { // sort values + diff = values[a].compareToIgnoreCase(values[b]); + if (diff == 0) { + diff = keys[a].compareToIgnoreCase(keys[b]); + } + } + return reverse ? -diff : diff; + } + + @Override + public void swap(int a, int b) { + StringDict.this.swap(a, b); + } + }; + s.run(); + + // Set the indices after sort/swaps (performance fix 160411) + resetIndices(); + } + + + /** Returns a duplicate copy of this object. */ + public StringDict copy() { + StringDict outgoing = new StringDict(count); + System.arraycopy(keys, 0, outgoing.keys, 0, count); + System.arraycopy(values, 0, outgoing.values, 0, count); + for (int i = 0; i < count; i++) { + outgoing.indices.put(keys[i], i); + } + outgoing.count = count; + return outgoing; + } + + + public void print() { + for (int i = 0; i < size(); i++) { + System.out.println(keys[i] + " = " + values[i]); + } + } + + + /** + * Write tab-delimited entries out to + * @param writer + */ + public void write(PrintWriter writer) { + for (int i = 0; i < count; i++) { + writer.println(keys[i] + "\t" + values[i]); + } + writer.flush(); + } + + + /** + * Return this dictionary as a String in JSON format. + */ + public String toJSON() { + StringList items = new StringList(); + for (int i = 0; i < count; i++) { + items.append(JSONObject.quote(keys[i])+ ": " + JSONObject.quote(values[i])); + } + return "{ " + items.join(", ") + " }"; + } + + + @Override + public String toString() { + return getClass().getSimpleName() + " size=" + size() + " " + toJSON(); + } +} diff --git a/src/main/java/processing/data/StringList.java b/src/main/java/processing/data/StringList.java new file mode 100644 index 0000000..9f21f3c --- /dev/null +++ b/src/main/java/processing/data/StringList.java @@ -0,0 +1,775 @@ +package processing.data; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Random; + +import processing.core.PApplet; + +/** + * Helper class for a list of Strings. Lists are designed to have some of the + * features of ArrayLists, but to maintain the simplicity and efficiency of + * working with arrays. + * + * Functions like sort() and shuffle() always act on the list itself. To get + * a sorted copy, use list.copy().sort(). + * + * @webref data:composite + * @see IntList + * @see FloatList + */ +public class StringList implements Iterable { + int count; + String[] data; + + + public StringList() { + this(10); + } + + /** + * @nowebref + */ + public StringList(int length) { + data = new String[length]; + } + + /** + * @nowebref + */ + public StringList(String[] list) { + count = list.length; + data = new String[count]; + System.arraycopy(list, 0, data, 0, count); + } + + + /** + * Construct a StringList from a random pile of objects. Null values will + * stay null, but all the others will be converted to String values. + */ + public StringList(Object... items) { + count = items.length; + data = new String[count]; + int index = 0; + for (Object o : items) { +// // Not gonna go with null values staying that way because perhaps +// // the most common case here is to immediately call join() or similar. +// data[index++] = String.valueOf(o); + // Keep null values null (because join() will make non-null anyway) + if (o != null) { // leave null values null + data[index] = o.toString(); + } + index++; + } + } + + + /** + * Create from something iterable, for instance: + * StringList list = new StringList(hashMap.keySet()); + * + * @nowebref + */ + public StringList(Iterable iter) { + this(10); + for (String s : iter) { + append(s); + } + } + + + /** + * Improve efficiency by removing allocated but unused entries from the + * internal array used to store the data. Set to private, though it could + * be useful to have this public if lists are frequently making drastic + * size changes (from very large to very small). + */ + private void crop() { + if (count != data.length) { + data = PApplet.subset(data, 0, count); + } + } + + + /** + * Get the length of the list. + * + * @webref stringlist:method + * @brief Get the length of the list + */ + public int size() { + return count; + } + + + public void resize(int length) { + if (length > data.length) { + String[] temp = new String[length]; + System.arraycopy(data, 0, temp, 0, count); + data = temp; + + } else if (length > count) { + Arrays.fill(data, count, length, 0); + } + count = length; + } + + + /** + * Remove all entries from the list. + * + * @webref stringlist:method + * @brief Remove all entries from the list + */ + public void clear() { + count = 0; + } + + + /** + * Get an entry at a particular index. + * + * @webref stringlist:method + * @brief Get an entry at a particular index + */ + public String get(int index) { + if (index >= count) { + throw new ArrayIndexOutOfBoundsException(index); + } + return data[index]; + } + + + /** + * Set the entry at a particular index. If the index is past the length of + * the list, it'll expand the list to accommodate, and fill the intermediate + * entries with 0s. + * + * @webref stringlist:method + * @brief Set an entry at a particular index + */ + public void set(int index, String what) { + if (index >= count) { + data = PApplet.expand(data, index+1); + for (int i = count; i < index; i++) { + data[i] = null; + } + count = index+1; + } + data[index] = what; + } + + + /** Just an alias for append(), but matches pop() */ + public void push(String value) { + append(value); + } + + + public String pop() { + if (count == 0) { + throw new RuntimeException("Can't call pop() on an empty list"); + } + String value = get(count-1); + data[--count] = null; // avoid leak + return value; + } + + + /** + * Remove an element from the specified index. + * + * @webref stringlist:method + * @brief Remove an element from the specified index + */ + public String remove(int index) { + if (index < 0 || index >= count) { + throw new ArrayIndexOutOfBoundsException(index); + } + String entry = data[index]; +// int[] outgoing = new int[count - 1]; +// System.arraycopy(data, 0, outgoing, 0, index); +// count--; +// System.arraycopy(data, index + 1, outgoing, 0, count - index); +// data = outgoing; + for (int i = index; i < count-1; i++) { + data[i] = data[i+1]; + } + count--; + return entry; + } + + + // Remove the first instance of a particular value and return its index. + public int removeValue(String value) { + if (value == null) { + for (int i = 0; i < count; i++) { + if (data[i] == null) { + remove(i); + return i; + } + } + } else { + int index = index(value); + if (index != -1) { + remove(index); + return index; + } + } + return -1; + } + + + // Remove all instances of a particular value and return the count removed. + public int removeValues(String value) { + int ii = 0; + if (value == null) { + for (int i = 0; i < count; i++) { + if (data[i] != null) { + data[ii++] = data[i]; + } + } + } else { + for (int i = 0; i < count; i++) { + if (!value.equals(data[i])) { + data[ii++] = data[i]; + } + } + } + int removed = count - ii; + count = ii; + return removed; + } + + + // replace the first value that matches, return the index that was replaced + public int replaceValue(String value, String newValue) { + if (value == null) { + for (int i = 0; i < count; i++) { + if (data[i] == null) { + data[i] = newValue; + return i; + } + } + } else { + for (int i = 0; i < count; i++) { + if (value.equals(data[i])) { + data[i] = newValue; + return i; + } + } + } + return -1; + } + + + // replace all values that match, return the count of those replaced + public int replaceValues(String value, String newValue) { + int changed = 0; + if (value == null) { + for (int i = 0; i < count; i++) { + if (data[i] == null) { + data[i] = newValue; + changed++; + } + } + } else { + for (int i = 0; i < count; i++) { + if (value.equals(data[i])) { + data[i] = newValue; + changed++; + } + } + } + return changed; + } + + + /** + * Add a new entry to the list. + * + * @webref stringlist:method + * @brief Add a new entry to the list + */ + public void append(String value) { + if (count == data.length) { + data = PApplet.expand(data); + } + data[count++] = value; + } + + + public void append(String[] values) { + for (String v : values) { + append(v); + } + } + + + public void append(StringList list) { + for (String v : list.values()) { // will concat the list... + append(v); + } + } + + + /** Add this value, but only if it's not already in the list. */ + public void appendUnique(String value) { + if (!hasValue(value)) { + append(value); + } + } + + +// public void insert(int index, int value) { +// if (index+1 > count) { +// if (index+1 < data.length) { +// } +// } +// if (index >= data.length) { +// data = PApplet.expand(data, index+1); +// data[index] = value; +// count = index+1; +// +// } else if (count == data.length) { +// if (index >= count) { +// //int[] temp = new int[count << 1]; +// System.arraycopy(data, 0, temp, 0, index); +// temp[index] = value; +// System.arraycopy(data, index, temp, index+1, count - index); +// data = temp; +// +// } else { +// // data[] has room to grow +// // for() loop believed to be faster than System.arraycopy over itself +// for (int i = count; i > index; --i) { +// data[i] = data[i-1]; +// } +// data[index] = value; +// count++; +// } +// } + + + public void insert(int index, String value) { + insert(index, new String[] { value }); + } + + + // same as splice + public void insert(int index, String[] values) { + if (index < 0) { + throw new IllegalArgumentException("insert() index cannot be negative: it was " + index); + } + if (index >= data.length) { + throw new IllegalArgumentException("insert() index " + index + " is past the end of this list"); + } + + String[] temp = new String[count + values.length]; + + // Copy the old values, but not more than already exist + System.arraycopy(data, 0, temp, 0, Math.min(count, index)); + + // Copy the new values into the proper place + System.arraycopy(values, 0, temp, index, values.length); + +// if (index < count) { + // The index was inside count, so it's a true splice/insert + System.arraycopy(data, index, temp, index+values.length, count - index); + count = count + values.length; +// } else { +// // The index was past 'count', so the new count is weirder +// count = index + values.length; +// } + data = temp; + } + + + public void insert(int index, StringList list) { + insert(index, list.values()); + } + + + // below are aborted attempts at more optimized versions of the code + // that are harder to read and debug... + +// if (index + values.length >= count) { +// // We're past the current 'count', check to see if we're still allocated +// // index 9, data.length = 10, values.length = 1 +// if (index + values.length < data.length) { +// // There's still room for these entries, even though it's past 'count'. +// // First clear out the entries leading up to it, however. +// for (int i = count; i < index; i++) { +// data[i] = 0; +// } +// data[index] = +// } +// if (index >= data.length) { +// int length = index + values.length; +// int[] temp = new int[length]; +// System.arraycopy(data, 0, temp, 0, count); +// System.arraycopy(values, 0, temp, index, values.length); +// data = temp; +// count = data.length; +// } else { +// +// } +// +// } else if (count == data.length) { +// int[] temp = new int[count << 1]; +// System.arraycopy(data, 0, temp, 0, index); +// temp[index] = value; +// System.arraycopy(data, index, temp, index+1, count - index); +// data = temp; +// +// } else { +// // data[] has room to grow +// // for() loop believed to be faster than System.arraycopy over itself +// for (int i = count; i > index; --i) { +// data[i] = data[i-1]; +// } +// data[index] = value; +// count++; +// } + + + /** Return the first index of a particular value. */ + public int index(String what) { + if (what == null) { + for (int i = 0; i < count; i++) { + if (data[i] == null) { + return i; + } + } + } else { + for (int i = 0; i < count; i++) { + if (what.equals(data[i])) { + return i; + } + } + } + return -1; + } + + + // !!! TODO this is not yet correct, because it's not being reset when + // the rest of the entries are changed +// protected void cacheIndices() { +// indexCache = new HashMap(); +// for (int i = 0; i < count; i++) { +// indexCache.put(data[i], i); +// } +// } + + /** + * @webref stringlist:method + * @brief Check if a value is a part of the list + */ + public boolean hasValue(String value) { + if (value == null) { + for (int i = 0; i < count; i++) { + if (data[i] == null) { + return true; + } + } + } else { + for (int i = 0; i < count; i++) { + if (value.equals(data[i])) { + return true; + } + } + } + return false; + } + + + /** + * Sorts the array in place. + * + * @webref stringlist:method + * @brief Sorts the array in place + */ + public void sort() { + sortImpl(false); + } + + + /** + * Reverse sort, orders values from highest to lowest. + * + * @webref stringlist:method + * @brief Reverse sort, orders values from highest to lowest + */ + public void sortReverse() { + sortImpl(true); + } + + + private void sortImpl(final boolean reverse) { + new Sort() { + @Override + public int size() { + return count; + } + + @Override + public float compare(int a, int b) { + float diff = data[a].compareToIgnoreCase(data[b]); + return reverse ? -diff : diff; + } + + @Override + public void swap(int a, int b) { + String temp = data[a]; + data[a] = data[b]; + data[b] = temp; + } + }.run(); + } + + + // use insert() +// public void splice(int index, int value) { +// } + + +// public void subset(int start) { +// subset(start, count - start); +// } +// +// +// public void subset(int start, int num) { +// for (int i = 0; i < num; i++) { +// data[i] = data[i+start]; +// } +// count = num; +// } + + /** + * @webref stringlist:method + * @brief Reverse the order of the list elements + */ + public void reverse() { + int ii = count - 1; + for (int i = 0; i < count/2; i++) { + String t = data[i]; + data[i] = data[ii]; + data[ii] = t; + --ii; + } + } + + + /** + * Randomize the order of the list elements. Note that this does not + * obey the randomSeed() function in PApplet. + * + * @webref stringlist:method + * @brief Randomize the order of the list elements + */ + public void shuffle() { + Random r = new Random(); + int num = count; + while (num > 1) { + int value = r.nextInt(num); + num--; + String temp = data[num]; + data[num] = data[value]; + data[value] = temp; + } + } + + + /** + * Randomize the list order using the random() function from the specified + * sketch, allowing shuffle() to use its current randomSeed() setting. + */ + public void shuffle(PApplet sketch) { + int num = count; + while (num > 1) { + int value = (int) sketch.random(num); + num--; + String temp = data[num]; + data[num] = data[value]; + data[value] = temp; + } + } + + + /** + * Make the entire list lower case. + * + * @webref stringlist:method + * @brief Make the entire list lower case + */ + public void lower() { + for (int i = 0; i < count; i++) { + if (data[i] != null) { + data[i] = data[i].toLowerCase(); + } + } + } + + + /** + * Make the entire list upper case. + * + * @webref stringlist:method + * @brief Make the entire list upper case + */ + public void upper() { + for (int i = 0; i < count; i++) { + if (data[i] != null) { + data[i] = data[i].toUpperCase(); + } + } + } + + + public StringList copy() { + StringList outgoing = new StringList(data); + outgoing.count = count; + return outgoing; + } + + + /** + * Returns the actual array being used to store the data. Suitable for + * iterating with a for() loop, but modifying the list could cause terrible + * things to happen. + */ + public String[] values() { + crop(); + return data; + } + + + @Override + public Iterator iterator() { +// return valueIterator(); +// } +// +// +// public Iterator valueIterator() { + return new Iterator() { + int index = -1; + + public void remove() { + StringList.this.remove(index); + index--; + } + + public String next() { + return data[++index]; + } + + public boolean hasNext() { + return index+1 < count; + } + }; + } + + + /** + * Create a new array with a copy of all the values. + * + * @return an array sized by the length of the list with each of the values. + * @webref stringlist:method + * @brief Create a new array with a copy of all the values + */ + public String[] array() { + return array(null); + } + + + /** + * Copy values into the specified array. If the specified array is null or + * not the same size, a new array will be allocated. + * @param array + */ + public String[] array(String[] array) { + if (array == null || array.length != count) { + array = new String[count]; + } + System.arraycopy(data, 0, array, 0, count); + return array; + } + + + public StringList getSubset(int start) { + return getSubset(start, count - start); + } + + + public StringList getSubset(int start, int num) { + String[] subset = new String[num]; + System.arraycopy(data, start, subset, 0, num); + return new StringList(subset); + } + + + /** Get a list of all unique entries. */ + public String[] getUnique() { + return getTally().keyArray(); + } + + + /** Count the number of times each String entry is found in this list. */ + public IntDict getTally() { + IntDict outgoing = new IntDict(); + for (int i = 0; i < count; i++) { + outgoing.increment(data[i]); + } + return outgoing; + } + + + /** Create a dictionary associating each entry in this list to its index. */ + public IntDict getOrder() { + IntDict outgoing = new IntDict(); + for (int i = 0; i < count; i++) { + outgoing.set(data[i], i); + } + return outgoing; + } + + + public String join(String separator) { + if (count == 0) { + return ""; + } + StringBuilder sb = new StringBuilder(); + sb.append(data[0]); + for (int i = 1; i < count; i++) { + sb.append(separator); + sb.append(data[i]); + } + return sb.toString(); + } + + + public void print() { + for (int i = 0; i < count; i++) { + System.out.format("[%d] %s%n", i, data[i]); + } + } + + + /** + * Return this dictionary as a String in JSON format. + */ + public String toJSON() { + StringList temp = new StringList(); + for (String item : this) { + temp.append(JSONObject.quote(item)); + } + return "[ " + temp.join(", ") + " ]"; + } + + + @Override + public String toString() { + return getClass().getSimpleName() + " size=" + size() + " " + toJSON(); + } +} diff --git a/src/main/java/processing/data/Table.java b/src/main/java/processing/data/Table.java new file mode 100644 index 0000000..fa0e0c5 --- /dev/null +++ b/src/main/java/processing/data/Table.java @@ -0,0 +1,4923 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2011-13 Ben Fry and Casey Reas + Copyright (c) 2006-11 Ben Fry + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License version 2.1 as published by the Free Software Foundation. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA + */ + +package processing.data; + +import java.io.*; +import java.lang.reflect.*; +import java.nio.charset.Charset; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +import processing.core.PApplet; +import processing.core.PConstants; + + +/** + *

        Generic class for handling tabular data, typically from a CSV, TSV, or + * other sort of spreadsheet file.

        + *

        CSV files are + * comma separated values, + * often with the data in quotes. TSV files use tabs as separators, and usually + * don't bother with the quotes.

        + *

        File names should end with .csv if they're comma separated.

        + *

        A rough "spec" for CSV can be found here.

        + * + * @webref data:composite + * @see PApplet#loadTable(String) + * @see PApplet#saveTable(Table, String) + * @see TableRow + */ +public class Table { + protected int rowCount; + protected int allocCount; + +// protected boolean skipEmptyRows = true; +// protected boolean skipCommentLines = true; +// protected String extension = null; +// protected boolean commaSeparatedValues = false; +// protected boolean awfulCSV = false; + + protected String missingString = null; + protected int missingInt = 0; + protected long missingLong = 0; + protected float missingFloat = Float.NaN; + protected double missingDouble = Double.NaN; + protected int missingCategory = -1; + + String[] columnTitles; + HashMapBlows[] columnCategories; + HashMap columnIndices; + + protected Object[] columns; // [column] + + // accessible for advanced users + static public final int STRING = 0; + static public final int INT = 1; + static public final int LONG = 2; + static public final int FLOAT = 3; + static public final int DOUBLE = 4; + static public final int CATEGORY = 5; + int[] columnTypes; + + protected RowIterator rowIterator; + + // 0 for doubling each time, otherwise the number of rows to increment on + // each expansion. + protected int expandIncrement; + + + /** + * Creates a new, empty table. Use addRow() to add additional rows. + */ + public Table() { + init(); + } + + /** + * @nowebref + */ + public Table(File file) throws IOException { + this(file, null); + } + + + /** + * version that uses a File object; future releases (or data types) + * may include additional optimizations here + * + * @nowebref + */ + public Table(File file, String options) throws IOException { + // uses createInput() to handle .gz (and eventually .bz2) files + init(); + parse(PApplet.createInput(file), + extensionOptions(true, file.getName(), options)); + } + + /** + * @nowebref + */ + public Table(InputStream input) throws IOException { + this(input, null); + } + + + /** + * Read the table from a stream. Possible options include: + *
          + *
        • csv - parse the table as comma-separated values + *
        • tsv - parse the table as tab-separated values + *
        • newlines - this CSV file contains newlines inside individual cells + *
        • header - this table has a header (title) row + *
        + * + * @nowebref + * @param input + * @param options + * @throws IOException + */ + public Table(InputStream input, String options) throws IOException { + init(); + parse(input, options); + } + + + public Table(Iterable rows) { + init(); + + int row = 0; + int alloc = 10; + + for (TableRow incoming : rows) { + if (row == 0) { + setColumnTypes(incoming.getColumnTypes()); + setColumnTitles(incoming.getColumnTitles()); + // Do this after setting types, otherwise it'll attempt to parse the + // allocated but empty rows, and drive CATEGORY columns nutso. + setRowCount(alloc); + // sometimes more columns than titles (and types?) + setColumnCount(incoming.getColumnCount()); + + } else if (row == alloc) { + // Far more efficient than re-allocating all columns and doing a copy + alloc *= 2; + setRowCount(alloc); + } + + //addRow(row); +// try { + setRow(row++, incoming); +// } catch (ArrayIndexOutOfBoundsException aioobe) { +// for (int i = 0; i < incoming.getColumnCount(); i++) { +// System.out.format("[%d] %s%n", i, incoming.getString(i)); +// } +// throw aioobe; +// } + } + // Shrink the table to only the rows that were used + if (row != alloc) { + setRowCount(row); + } + } + + + /** + * @nowebref + */ + public Table(ResultSet rs) { + init(); + try { + ResultSetMetaData rsmd = rs.getMetaData(); + + int columnCount = rsmd.getColumnCount(); + setColumnCount(columnCount); + + for (int col = 0; col < columnCount; col++) { + setColumnTitle(col, rsmd.getColumnName(col + 1)); + + int type = rsmd.getColumnType(col + 1); + switch (type) { // TODO these aren't tested. nor are they complete. + case Types.INTEGER: + case Types.TINYINT: + case Types.SMALLINT: + setColumnType(col, INT); + break; + case Types.BIGINT: + setColumnType(col, LONG); + break; + case Types.FLOAT: + setColumnType(col, FLOAT); + break; + case Types.DECIMAL: + case Types.DOUBLE: + case Types.REAL: + setColumnType(col, DOUBLE); + break; + } + } + + int row = 0; + while (rs.next()) { + for (int col = 0; col < columnCount; col++) { + switch (columnTypes[col]) { + case STRING: setString(row, col, rs.getString(col+1)); break; + case INT: setInt(row, col, rs.getInt(col+1)); break; + case LONG: setLong(row, col, rs.getLong(col+1)); break; + case FLOAT: setFloat(row, col, rs.getFloat(col+1)); break; + case DOUBLE: setDouble(row, col, rs.getDouble(col+1)); break; + default: throw new IllegalArgumentException("column type " + columnTypes[col] + " not supported."); + } + } + row++; +// String[] row = new String[columnCount]; +// for (int col = 0; col < columnCount; col++) { +// row[col] = rs.get(col + 1); +// } +// addRow(row); + } + + } catch (SQLException s) { + throw new RuntimeException(s); + } + } + + + public Table typedParse(InputStream input, String options) throws IOException { + Table table = new Table(); + table.setColumnTypes(this); + table.parse(input, options); + return table; + } + + + protected void init() { + columns = new Object[0]; + columnTypes = new int[0]; + columnCategories = new HashMapBlows[0]; + } + + + /* + protected String checkOptions(File file, String options) throws IOException { + String extension = null; + String filename = file.getName(); + int dotIndex = filename.lastIndexOf('.'); + if (dotIndex != -1) { + extension = filename.substring(dotIndex + 1).toLowerCase(); + if (!extension.equals("csv") && + !extension.equals("tsv") && + !extension.equals("html") && + !extension.equals("bin")) { + // ignore extension + extension = null; + } + } + if (extension == null) { + if (options == null) { + throw new IOException("This table filename has no extension, and no options are set."); + } + } else { // extension is not null + if (options == null) { + options = extension; + } else { + // prepend the extension, it will be overridden if there's an option for it. + options = extension + "," + options; + } + } + return options; + } + */ + + + static final String[] loadExtensions = { "csv", "tsv", "ods", "bin" }; + static final String[] saveExtensions = { "csv", "tsv", "ods", "bin", "html" }; + + static public String extensionOptions(boolean loading, String filename, String options) { + String extension = PApplet.checkExtension(filename); + if (extension != null) { + for (String possible : loading ? loadExtensions : saveExtensions) { + if (extension.equals(possible)) { + if (options == null) { + return extension; + } else { + // prepend the extension to the options (will be replaced by other + // options that override it later in the load loop) + return extension + "," + options; + } + } + } + } + return options; + } + + + protected void parse(InputStream input, String options) throws IOException { +// boolean awfulCSV = false; + boolean header = false; + String extension = null; + boolean binary = false; + String encoding = "UTF-8"; + + String worksheet = null; + final String sheetParam = "worksheet="; + + String[] opts = null; + if (options != null) { + opts = PApplet.trim(PApplet.split(options, ',')); + for (String opt : opts) { + if (opt.equals("tsv")) { + extension = "tsv"; + } else if (opt.equals("csv")) { + extension = "csv"; + } else if (opt.equals("ods")) { + extension = "ods"; + } else if (opt.equals("newlines")) { + //awfulCSV = true; + //extension = "csv"; + throw new IllegalArgumentException("The 'newlines' option is no longer necessary."); + } else if (opt.equals("bin")) { + binary = true; + extension = "bin"; + } else if (opt.equals("header")) { + header = true; + } else if (opt.startsWith(sheetParam)) { + worksheet = opt.substring(sheetParam.length()); + } else if (opt.startsWith("dictionary=")) { + // ignore option, this is only handled by PApplet + } else if (opt.startsWith("encoding=")) { + encoding = opt.substring(9); + } else { + throw new IllegalArgumentException("'" + opt + "' is not a valid option for loading a Table"); + } + } + } + + if (extension == null) { + throw new IllegalArgumentException("No extension specified for this Table"); + } + + if (binary) { + loadBinary(input); + + } else if (extension.equals("ods")) { + odsParse(input, worksheet, header); + + } else { + InputStreamReader isr = new InputStreamReader(input, encoding); + BufferedReader reader = new BufferedReader(isr); + + // strip out the Unicode BOM, if present + reader.mark(1); + int c = reader.read(); + // if not the BOM, back up to the beginning again + if (c != '\uFEFF') { + reader.reset(); + } + + /* + if (awfulCSV) { + parseAwfulCSV(reader, header); + } else if ("tsv".equals(extension)) { + parseBasic(reader, header, true); + } else if ("csv".equals(extension)) { + parseBasic(reader, header, false); + } + */ + parseBasic(reader, header, "tsv".equals(extension)); + } + } + + + protected void parseBasic(BufferedReader reader, + boolean header, boolean tsv) throws IOException { + String line = null; + int row = 0; + if (rowCount == 0) { + setRowCount(10); + } + //int prev = 0; //-1; + try { + while ((line = reader.readLine()) != null) { + if (row == getRowCount()) { + setRowCount(row << 1); + } + if (row == 0 && header) { + setColumnTitles(tsv ? PApplet.split(line, '\t') : splitLineCSV(line, reader)); + header = false; + } else { + setRow(row, tsv ? PApplet.split(line, '\t') : splitLineCSV(line, reader)); + row++; + } + + if (row % 10000 == 0) { + /* + // this is problematic unless we're going to calculate rowCount first + if (row < rowCount) { + int pct = (100 * row) / rowCount; + if (pct != prev) { // also prevents "0%" from showing up + System.out.println(pct + "%"); + prev = pct; + } + } + */ + try { + // Sleep this thread so that the GC can catch up + Thread.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } catch (Exception e) { + throw new RuntimeException("Error reading table on line " + row, e); + } + // shorten or lengthen based on what's left + if (row != getRowCount()) { + setRowCount(row); + } + } + + +// public void convertTSV(BufferedReader reader, File outputFile) throws IOException { +// convertBasic(reader, true, outputFile); +// } + + + /* + protected void parseAwfulCSV(BufferedReader reader, + boolean header) throws IOException { + char[] c = new char[100]; + int count = 0; + boolean insideQuote = false; + + int alloc = 100; + setRowCount(100); + + int row = 0; + int col = 0; + int ch; + while ((ch = reader.read()) != -1) { + if (insideQuote) { + if (ch == '\"') { + // this is either the end of a quoted entry, or a quote character + reader.mark(1); + if (reader.read() == '\"') { + // it's "", which means a quote character + if (count == c.length) { + c = PApplet.expand(c); + } + c[count++] = '\"'; + } else { + // nope, just the end of a quoted csv entry + reader.reset(); + insideQuote = false; + // TODO nothing here that prevents bad csv data from showing up + // after the quote and before the comma... +// set(row, col, new String(c, 0, count)); +// count = 0; +// col++; +// insideQuote = false; + } + } else { // inside a quote, but the character isn't a quote + if (count == c.length) { + c = PApplet.expand(c); + } + c[count++] = (char) ch; + } + } else { // not inside a quote + if (ch == '\"') { + insideQuote = true; + + } else if (ch == '\r' || ch == '\n') { + if (ch == '\r') { + // check to see if next is a '\n' + reader.mark(1); + if (reader.read() != '\n') { + reader.reset(); + } + } + setString(row, col, new String(c, 0, count)); + count = 0; + row++; + if (row == 1 && header) { + // Use internal row removal (efficient because only one row). + removeTitleRow(); + // Un-set the header variable so that next time around, we don't + // just get stuck into a loop, removing the 0th row repeatedly. + header = false; + // Reset the number of rows (removeTitleRow() won't reset our local 'row' counter) + row = 0; + } +// if (row % 1000 == 0) { +// PApplet.println(PApplet.nfc(row)); +// } + if (row == alloc) { + alloc *= 2; + setRowCount(alloc); + } + col = 0; + + } else if (ch == ',') { + setString(row, col, new String(c, 0, count)); + count = 0; + // starting a new column, make sure we have room + col++; + ensureColumn(col); + + } else { // just a regular character, add it + if (count == c.length) { + c = PApplet.expand(c); + } + c[count++] = (char) ch; + } + } + } + // catch any leftovers + if (count > 0) { + setString(row, col, new String(c, 0, count)); + } + row++; // set row to row count (the current row index + 1) + if (alloc != row) { + setRowCount(row); // shrink to the actual size + } + } + */ + + + static class CommaSeparatedLine { + char[] c; + String[] pieces; + int pieceCount; + +// int offset; + int start; //, stop; + + String[] handle(String line, BufferedReader reader) throws IOException { +// PApplet.println("handle() called for: " + line); + start = 0; + pieceCount = 0; + c = line.toCharArray(); + + // get tally of number of columns and allocate the array + int cols = 1; // the first comma indicates the second column + boolean quote = false; + for (int i = 0; i < c.length; i++) { + if (!quote && (c[i] == ',')) { + cols++; + } else if (c[i] == '\"') { + // double double quotes (escaped quotes like "") will simply toggle + // this back and forth, so it should remain accurate + quote = !quote; + } + } + pieces = new String[cols]; + +// while (offset < c.length) { +// start = offset; + while (start < c.length) { + boolean enough = ingest(); + while (!enough) { + // found a newline inside the quote, grab another line + String nextLine = reader.readLine(); +// System.out.println("extending to " + nextLine); + if (nextLine == null) { +// System.err.println(line); + throw new IOException("Found a quoted line that wasn't terminated properly."); + } + // for simplicity, not bothering to skip what's already been read + // from c (and reset the offset to 0), opting to make a bigger array + // with both lines. + char[] temp = new char[c.length + 1 + nextLine.length()]; + PApplet.arrayCopy(c, temp, c.length); + // NOTE: we're converting to \n here, which isn't perfect + temp[c.length] = '\n'; + nextLine.getChars(0, nextLine.length(), temp, c.length + 1); +// c = temp; + return handle(new String(temp), reader); + //System.out.println(" full line is now " + new String(c)); + //stop = nextComma(c, offset); + //System.out.println("stop is now " + stop); + //enough = ingest(); + } + } + + // Make any remaining entries blanks instead of nulls. Empty columns from + // CSV are always "" not null, so this handles successive commas in a line + for (int i = pieceCount; i < pieces.length; i++) { + pieces[i] = ""; + } +// PApplet.printArray(pieces); + return pieces; + } + + protected void addPiece(int start, int stop, boolean quotes) { + if (quotes) { + int dest = start; + for (int i = start; i < stop; i++) { + if (c[i] == '\"') { + ++i; // step over the quote + } + if (i != dest) { + c[dest] = c[i]; + } + dest++; + } + pieces[pieceCount++] = new String(c, start, dest - start); + + } else { + pieces[pieceCount++] = new String(c, start, stop - start); + } + } + + /** + * Returns the next comma (not inside a quote) in the specified array. + * @param c array to search + * @param index offset at which to start looking + * @return index of the comma, or -1 if line ended inside an unclosed quote + */ + protected boolean ingest() { + boolean hasEscapedQuotes = false; + // not possible +// if (index == c.length) { // we're already at the end +// return c.length; +// } + boolean quoted = c[start] == '\"'; + if (quoted) { + start++; // step over the quote + } + int i = start; + while (i < c.length) { +// PApplet.println(c[i] + " i=" + i); + if (c[i] == '\"') { + // if this fella started with a quote + if (quoted) { + if (i == c.length-1) { + // closing quote for field; last field on the line + addPiece(start, i, hasEscapedQuotes); + start = c.length; + return true; + + } else if (c[i+1] == '\"') { + // an escaped quote inside a quoted field, step over it + hasEscapedQuotes = true; + i += 2; + + } else if (c[i+1] == ',') { + // that was our closing quote, get outta here + addPiece(start, i, hasEscapedQuotes); + start = i+2; + return true; + + } else { + // This is a lone-wolf quote, occasionally seen in exports. + // It's a single quote in the middle of some other text, + // and not escaped properly. Pray for the best! + i++; + } + + } else { // not a quoted line + if (i == c.length-1) { + // we're at the end of the line, can't have an unescaped quote + throw new RuntimeException("Unterminated quote at end of line"); + + } else if (c[i+1] == '\"') { + // step over this crummy quote escape + hasEscapedQuotes = true; + i += 2; + + } else { + throw new RuntimeException("Unterminated quoted field mid-line"); + } + } + } else if (!quoted && c[i] == ',') { + addPiece(start, i, hasEscapedQuotes); + start = i+1; + return true; + + } else if (!quoted && i == c.length-1) { + addPiece(start, c.length, hasEscapedQuotes); + start = c.length; + return true; + + } else { // nothing all that interesting + i++; + } + } +// if (!quote && (c[i] == ',')) { +// // found a comma, return this location +// return i; +// } else if (c[i] == '\"') { +// // if it's a quote, then either the next char is another quote, +// // or if this is a quoted entry, it better be a comma +// quote = !quote; +// } +// } + + // if still inside a quote, indicate that another line should be read + if (quoted) { + return false; + } + +// // made it to the end of the array with no new comma +// return c.length; + + throw new RuntimeException("not sure how..."); + } + } + + + CommaSeparatedLine csl; + + /** + * Parse a line of text as comma-separated values, returning each value as + * one entry in an array of String objects. Remove quotes from entries that + * begin and end with them, and convert 'escaped' quotes to actual quotes. + * @param line line of text to be parsed + * @return an array of the individual values formerly separated by commas + */ + protected String[] splitLineCSV(String line, BufferedReader reader) throws IOException { + if (csl == null) { + csl = new CommaSeparatedLine(); + } + return csl.handle(line, reader); + } + + + /** + * Returns the next comma (not inside a quote) in the specified array. + * @param c array to search + * @param index offset at which to start looking + * @return index of the comma, or -1 if line ended inside an unclosed quote + */ + /* + static protected int nextComma(char[] c, int index) { + if (index == c.length) { // we're already at the end + return c.length; + } + boolean quoted = c[index] == '\"'; + if (quoted) { + index++; // step over the quote + } + for (int i = index; i < c.length; i++) { + if (c[i] == '\"') { + // if this fella started with a quote + if (quoted) { + if (i == c.length-1) { + //return -1; // ran out of chars + // closing quote for field; last field on the line + return c.length; + } else if (c[i+1] == '\"') { + // an escaped quote inside a quoted field, step over it + i++; + } else if (c[i+1] == ',') { + // that's our closing quote, get outta here + return i+1; + } + + } else { // not a quoted line + if (i == c.length-1) { + // we're at the end of the line, can't have an unescaped quote + //return -1; // ran out of chars + throw new RuntimeException("Unterminated quoted field at end of line"); + } else if (c[i+1] == '\"') { + // step over this crummy quote escape + ++i; + } else { + throw new RuntimeException("Unterminated quoted field mid-line"); + } + } + } else if (!quoted && c[i] == ',') { + return i; + } + if (!quote && (c[i] == ',')) { + // found a comma, return this location + return i; + } else if (c[i] == '\"') { + // if it's a quote, then either the next char is another quote, + // or if this is a quoted entry, it better be a comma + quote = !quote; + } + } + // if still inside a quote, indicate that another line should be read + if (quote) { + return -1; + } + // made it to the end of the array with no new comma + return c.length; + } + */ + + + /** + * Read a .ods (OpenDoc spreadsheet) zip file from an InputStream, and + * return the InputStream for content.xml contained inside. + */ + private InputStream odsFindContentXML(InputStream input) { + ZipInputStream zis = new ZipInputStream(input); + ZipEntry entry = null; + try { + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equals("content.xml")) { + return zis; + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + + protected void odsParse(InputStream input, String worksheet, boolean header) { + try { + InputStream contentStream = odsFindContentXML(input); + XML xml = new XML(contentStream); + + // table files will have multiple sheets.. + // + // + // + XML[] sheets = + xml.getChildren("office:body/office:spreadsheet/table:table"); + + boolean found = false; + for (XML sheet : sheets) { +// System.out.println(sheet.getAttribute("table:name")); + if (worksheet == null || worksheet.equals(sheet.getString("table:name"))) { + odsParseSheet(sheet, header); + found = true; + if (worksheet == null) { + break; // only read the first sheet + } + } + } + if (!found) { + if (worksheet == null) { + throw new RuntimeException("No worksheets found in the ODS file."); + } else { + throw new RuntimeException("No worksheet named " + worksheet + + " found in the ODS file."); + } + } + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (ParserConfigurationException e) { + e.printStackTrace(); + } catch (SAXException e) { + e.printStackTrace(); + } + } + + + /** + * Parses a single sheet of XML from this file. + * @param The XML object for a single worksheet from the ODS file + */ + private void odsParseSheet(XML sheet, boolean header) { + // Extra

        or tags inside the text tag for the cell will be stripped. + // Different from showing formulas, and not quite the same as 'save as + // displayed' option when saving from inside OpenOffice. Only time we + // wouldn't want this would be so that we could parse hyperlinks and + // styling information intact, but that's out of scope for the p5 version. + final boolean ignoreTags = true; + + XML[] rows = sheet.getChildren("table:table-row"); + //xml.getChildren("office:body/office:spreadsheet/table:table/table:table-row"); + + int rowIndex = 0; + for (XML row : rows) { + int rowRepeat = row.getInt("table:number-rows-repeated", 1); +// if (rowRepeat != 1) { +// System.out.println(rowRepeat + " " + rowCount + " " + (rowCount + rowRepeat)); +// } + boolean rowNotNull = false; + XML[] cells = row.getChildren(); + int columnIndex = 0; + + for (XML cell : cells) { + int cellRepeat = cell.getInt("table:number-columns-repeated", 1); + +// +// 4150.00 +// + + String cellData = ignoreTags ? cell.getString("office:value") : null; + + // if there's an office:value in the cell, just roll with that + if (cellData == null) { + int cellKids = cell.getChildCount(); + if (cellKids != 0) { + XML[] paragraphElements = cell.getChildren("text:p"); + if (paragraphElements.length != 1) { + for (XML el : paragraphElements) { + System.err.println(el.toString()); + } + throw new RuntimeException("found more than one text:p element"); + } + XML textp = paragraphElements[0]; + String textpContent = textp.getContent(); + // if there are sub-elements, the content shows up as a child element + // (for which getName() returns null.. which seems wrong) + if (textpContent != null) { + cellData = textpContent; // nothing fancy, the text is in the text:p element + } else { + XML[] textpKids = textp.getChildren(); + StringBuilder cellBuffer = new StringBuilder(); + for (XML kid : textpKids) { + String kidName = kid.getName(); + if (kidName == null) { + odsAppendNotNull(kid, cellBuffer); + + } else if (kidName.equals("text:s")) { + int spaceCount = kid.getInt("text:c", 1); + for (int space = 0; space < spaceCount; space++) { + cellBuffer.append(' '); + } + } else if (kidName.equals("text:span")) { + odsAppendNotNull(kid, cellBuffer); + + } else if (kidName.equals("text:a")) { + // blah.com + if (ignoreTags) { + cellBuffer.append(kid.getString("xlink:href")); + } else { + odsAppendNotNull(kid, cellBuffer); + } + + } else { + odsAppendNotNull(kid, cellBuffer); + System.err.println(getClass().getName() + ": don't understand: " + kid); + //throw new RuntimeException("I'm not used to this."); + } + } + cellData = cellBuffer.toString(); + } + //setString(rowIndex, columnIndex, c); //text[0].getContent()); + //columnIndex++; + } + } + for (int r = 0; r < cellRepeat; r++) { + if (cellData != null) { + //System.out.println("setting " + rowIndex + "," + columnIndex + " to " + cellData); + setString(rowIndex, columnIndex, cellData); + } + columnIndex++; + if (cellData != null) { +// if (columnIndex > columnMax) { +// columnMax = columnIndex; +// } + rowNotNull = true; + } + } + } + if (header) { + removeTitleRow(); // efficient enough on the first row + header = false; // avoid infinite loop + + } else { + if (rowNotNull && rowRepeat > 1) { + String[] rowStrings = getStringRow(rowIndex); + for (int r = 1; r < rowRepeat; r++) { + addRow(rowStrings); + } + } + rowIndex += rowRepeat; + } + } + } + + + private void odsAppendNotNull(XML kid, StringBuilder buffer) { + String content = kid.getContent(); + if (content != null) { + buffer.append(content); + } + } + + + // A 'Class' object is used here, so the syntax for this function is: + // Table t = loadTable("cars3.tsv", "header"); + // Record[] records = (Record[]) t.parse(Record.class); + // While t.parse("Record") might be nicer, the class is likely to be an + // inner class (another tab in a PDE sketch) or even inside a package, + // so additional information would be needed to locate it. The name of the + // inner class would be "SketchName$Record" which isn't acceptable syntax + // to make people use. Better to just introduce the '.class' syntax. + + // Unlike the Table class itself, this accepts char and boolean fields in + // the target class, since they're much more prevalent, and don't require + // a zillion extra methods and special cases in the rest of the class here. + + // since this is likely an inner class, needs a reference to its parent, + // because that's passed to the constructor parameter (inserted by the + // compiler) of an inner class by the runtime. + + /** incomplete, do not use */ + public void parseInto(Object enclosingObject, String fieldName) { + Class target = null; + Object outgoing = null; + Field targetField = null; + try { + // Object targetObject, + // Class target -> get this from the type of fieldName +// Class sketchClass = sketch.getClass(); + Class sketchClass = enclosingObject.getClass(); + targetField = sketchClass.getDeclaredField(fieldName); +// PApplet.println("found " + targetField); + Class targetArray = targetField.getType(); + if (!targetArray.isArray()) { + // fieldName is not an array + } else { + target = targetArray.getComponentType(); + outgoing = Array.newInstance(target, getRowCount()); + } + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (SecurityException e) { + e.printStackTrace(); + } + +// Object enclosingObject = sketch; +// PApplet.println("enclosing obj is " + enclosingObject); + Class enclosingClass = target.getEnclosingClass(); + Constructor con = null; + + try { + if (enclosingClass == null) { + con = target.getDeclaredConstructor(); //new Class[] { }); +// PApplet.println("no enclosing class"); + } else { + con = target.getDeclaredConstructor(new Class[] { enclosingClass }); +// PApplet.println("enclosed by " + enclosingClass.getName()); + } + if (!con.isAccessible()) { +// System.out.println("setting constructor to public"); + con.setAccessible(true); + } + } catch (SecurityException e) { + e.printStackTrace(); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + + Field[] fields = target.getDeclaredFields(); + ArrayList inuse = new ArrayList<>(); + for (Field field : fields) { + String name = field.getName(); + if (getColumnIndex(name, false) != -1) { +// System.out.println("found field " + name); + if (!field.isAccessible()) { +// PApplet.println(" changing field access"); + field.setAccessible(true); + } + inuse.add(field); + } else { +// System.out.println("skipping field " + name); + } + } + + int index = 0; + try { + for (TableRow row : rows()) { + Object item = null; + if (enclosingClass == null) { + //item = target.newInstance(); + item = con.newInstance(); + } else { + item = con.newInstance(new Object[] { enclosingObject }); + } + //Object item = defaultCons.newInstance(new Object[] { }); + for (Field field : inuse) { + String name = field.getName(); + //PApplet.println("gonna set field " + name); + + if (field.getType() == String.class) { + field.set(item, row.getString(name)); + + } else if (field.getType() == Integer.TYPE) { + field.setInt(item, row.getInt(name)); + + } else if (field.getType() == Long.TYPE) { + field.setLong(item, row.getLong(name)); + + } else if (field.getType() == Float.TYPE) { + field.setFloat(item, row.getFloat(name)); + + } else if (field.getType() == Double.TYPE) { + field.setDouble(item, row.getDouble(name)); + + } else if (field.getType() == Boolean.TYPE) { + String content = row.getString(name); + if (content != null) { + // Only bother setting if it's true, + // otherwise false by default anyway. + if (content.toLowerCase().equals("true") || + content.equals("1")) { + field.setBoolean(item, true); + } + } +// if (content == null) { +// field.setBoolean(item, false); // necessary? +// } else if (content.toLowerCase().equals("true")) { +// field.setBoolean(item, true); +// } else if (content.equals("1")) { +// field.setBoolean(item, true); +// } else { +// field.setBoolean(item, false); // necessary? +// } + } else if (field.getType() == Character.TYPE) { + String content = row.getString(name); + if (content != null && content.length() > 0) { + // Otherwise set to \0 anyway + field.setChar(item, content.charAt(0)); + } + } + } +// list.add(item); + Array.set(outgoing, index++, item); + } + if (!targetField.isAccessible()) { +// PApplet.println("setting target field to public"); + targetField.setAccessible(true); + } + // Set the array in the sketch +// targetField.set(sketch, outgoing); + targetField.set(enclosingObject, outgoing); + + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } + + + public boolean save(File file, String options) throws IOException { + return save(PApplet.createOutput(file), + Table.extensionOptions(false, file.getName(), options)); + } + + + public boolean save(OutputStream output, String options) { + PrintWriter writer = PApplet.createWriter(output); + String extension = null; + if (options == null) { + throw new IllegalArgumentException("No extension specified for saving this Table"); + } + + String[] opts = PApplet.trim(PApplet.split(options, ',')); + // Only option for save is the extension, so we can safely grab the last + extension = opts[opts.length - 1]; + boolean found = false; + for (String ext : saveExtensions) { + if (extension.equals(ext)) { + found = true; + break; + } + } + // Not providing a fallback; let's make users specify an extension + if (!found) { + throw new IllegalArgumentException("'" + extension + "' not available for Table"); + } + + if (extension.equals("csv")) { + writeCSV(writer); + } else if (extension.equals("tsv")) { + writeTSV(writer); + } else if (extension.equals("ods")) { + try { + saveODS(output); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } else if (extension.equals("html")) { + writeHTML(writer); + } else if (extension.equals("bin")) { + try { + saveBinary(output); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + writer.flush(); + writer.close(); + return true; + } + + + protected void writeTSV(PrintWriter writer) { + if (columnTitles != null) { + for (int col = 0; col < columns.length; col++) { + if (col != 0) { + writer.print('\t'); + } + if (columnTitles[col] != null) { + writer.print(columnTitles[col]); + } + } + writer.println(); + } + for (int row = 0; row < rowCount; row++) { + for (int col = 0; col < getColumnCount(); col++) { + if (col != 0) { + writer.print('\t'); + } + String entry = getString(row, col); + // just write null entries as blanks, rather than spewing 'null' + // all over the spreadsheet file. + if (entry != null) { + writer.print(entry); + } + } + writer.println(); + } + writer.flush(); + } + + + protected void writeCSV(PrintWriter writer) { + if (columnTitles != null) { + for (int col = 0; col < getColumnCount(); col++) { + if (col != 0) { + writer.print(','); + } + try { + if (columnTitles[col] != null) { // col < columnTitles.length && + writeEntryCSV(writer, columnTitles[col]); + } + } catch (ArrayIndexOutOfBoundsException e) { + PApplet.printArray(columnTitles); + PApplet.printArray(columns); + throw e; + } + } + writer.println(); + } + for (int row = 0; row < rowCount; row++) { + for (int col = 0; col < getColumnCount(); col++) { + if (col != 0) { + writer.print(','); + } + String entry = getString(row, col); + // just write null entries as blanks, rather than spewing 'null' + // all over the spreadsheet file. + if (entry != null) { + writeEntryCSV(writer, entry); + } + } + // Prints the newline for the row, even if it's missing + writer.println(); + } + writer.flush(); + } + + + protected void writeEntryCSV(PrintWriter writer, String entry) { + if (entry != null) { + if (entry.indexOf('\"') != -1) { // convert quotes to double quotes + char[] c = entry.toCharArray(); + writer.print('\"'); + for (int i = 0; i < c.length; i++) { + if (c[i] == '\"') { + writer.print("\"\""); + } else { + writer.print(c[i]); + } + } + writer.print('\"'); + + // add quotes if commas or CR/LF are in the entry + } else if (entry.indexOf(',') != -1 || + entry.indexOf('\n') != -1 || + entry.indexOf('\r') != -1) { + writer.print('\"'); + writer.print(entry); + writer.print('\"'); + + + // add quotes if leading or trailing space + } else if ((entry.length() > 0) && + (entry.charAt(0) == ' ' || + entry.charAt(entry.length() - 1) == ' ')) { + writer.print('\"'); + writer.print(entry); + writer.print('\"'); + + } else { + writer.print(entry); + } + } + } + + + protected void writeHTML(PrintWriter writer) { + writer.println(""); +// writer.println(""); +// writer.println(""); + + writer.println(""); + writer.println(""); + writer.println(" "); + writer.println(""); + + writer.println(""); + writer.println(" "); + + if (hasColumnTitles()) { + writer.println(" "); + for (String entry : getColumnTitles()) { + writer.print(" "); + } + writer.println(" "); + } + + for (int row = 0; row < getRowCount(); row++) { + writer.println(" "); + for (int col = 0; col < getColumnCount(); col++) { + String entry = getString(row, col); + writer.print(" "); + } + writer.println(" "); + } + writer.println("
        "); + if (entry != null) { + writeEntryHTML(writer, entry); + } + writer.println("
        "); + if (entry != null) { + // probably not a great idea to mess w/ the export +// if (entry.startsWith("<") && entry.endsWith(">")) { +// writer.print(entry); +// } else { + writeEntryHTML(writer, entry); +// } + } + writer.println("
        "); + writer.println(""); + + writer.println(""); + writer.flush(); + } + + + protected void writeEntryHTML(PrintWriter writer, String entry) { + //char[] chars = entry.toCharArray(); + for (char c : entry.toCharArray()) { //chars) { + if (c == '<') { + writer.print("<"); + } else if (c == '>') { + writer.print(">"); + } else if (c == '&') { + writer.print("&"); +// } else if (c == '\'') { // only in XML +// writer.print("'"); + } else if (c == '"') { + writer.print("""); + + } else if (c < 32 || c > 127) { // keep in ASCII or Tidy complains + writer.print("&#"); + writer.print((int) c); + writer.print(';'); + + } else { + writer.print(c); + } + } + } + + + protected void saveODS(OutputStream os) throws IOException { + ZipOutputStream zos = new ZipOutputStream(os); + + final String xmlHeader = ""; + + ZipEntry entry = new ZipEntry("META-INF/manifest.xml"); + String[] lines = new String[] { + xmlHeader, + "", + " ", + " ", + " ", + " ", + " ", + "" + }; + zos.putNextEntry(entry); + zos.write(PApplet.join(lines, "\n").getBytes()); + zos.closeEntry(); + + /* + entry = new ZipEntry("meta.xml"); + lines = new String[] { + xmlHeader, + "" + }; + zos.putNextEntry(entry); + zos.write(PApplet.join(lines, "\n").getBytes()); + zos.closeEntry(); + + entry = new ZipEntry("meta.xml"); + lines = new String[] { + xmlHeader, + "" + }; + zos.putNextEntry(entry); + zos.write(PApplet.join(lines, "\n").getBytes()); + zos.closeEntry(); + + entry = new ZipEntry("settings.xml"); + lines = new String[] { + xmlHeader, + "" + }; + zos.putNextEntry(entry); + zos.write(PApplet.join(lines, "\n").getBytes()); + zos.closeEntry(); + + entry = new ZipEntry("styles.xml"); + lines = new String[] { + xmlHeader, + "" + }; + zos.putNextEntry(entry); + zos.write(PApplet.join(lines, "\n").getBytes()); + zos.closeEntry(); + */ + + final String[] dummyFiles = new String[] { + "meta.xml", "settings.xml", "styles.xml" + }; + lines = new String[] { + xmlHeader, + "" + }; + byte[] dummyBytes = PApplet.join(lines, "\n").getBytes(); + for (String filename : dummyFiles) { + entry = new ZipEntry(filename); + zos.putNextEntry(entry); + zos.write(dummyBytes); + zos.closeEntry(); + } + + // + + entry = new ZipEntry("mimetype"); + zos.putNextEntry(entry); + zos.write("application/vnd.oasis.opendocument.spreadsheet".getBytes()); + zos.closeEntry(); + + // + + entry = new ZipEntry("content.xml"); + zos.putNextEntry(entry); + //lines = new String[] { + writeUTF(zos, new String[] { + xmlHeader, + "", + " ", + " ", + " " + }); + //zos.write(PApplet.join(lines, "\n").getBytes()); + + byte[] rowStart = " \n".getBytes(); + byte[] rowStop = " \n".getBytes(); + + if (hasColumnTitles()) { + zos.write(rowStart); + for (int i = 0; i < getColumnCount(); i++) { + saveStringODS(zos, columnTitles[i]); + } + zos.write(rowStop); + } + + for (TableRow row : rows()) { + zos.write(rowStart); + for (int i = 0; i < getColumnCount(); i++) { + if (columnTypes[i] == STRING || columnTypes[i] == CATEGORY) { + saveStringODS(zos, row.getString(i)); + } else { + saveNumberODS(zos, row.getString(i)); + } + } + zos.write(rowStop); + } + + //lines = new String[] { + writeUTF(zos, new String[] { + " ", + " ", + " ", + "" + }); + //zos.write(PApplet.join(lines, "\n").getBytes()); + zos.closeEntry(); + + zos.flush(); + zos.close(); + } + + + void saveStringODS(OutputStream output, String text) throws IOException { + // At this point, I should have just used the XML library. But this does + // save us from having to create the entire document in memory again before + // writing to the file. So while it's dorky, the outcome is still useful. + StringBuilder sanitized = new StringBuilder(); + if (text != null) { + char[] array = text.toCharArray(); + for (char c : array) { + if (c == '&') { + sanitized.append("&"); + } else if (c == '\'') { + sanitized.append("'"); + } else if (c == '"') { + sanitized.append("""); + } else if (c == '<') { + sanitized.append("<"); + } else if (c == '>') { + sanitized.append("&rt;"); + } else if (c < 32 || c > 127) { + sanitized.append("&#" + ((int) c) + ";"); + } else { + sanitized.append(c); + } + } + } + + writeUTF(output, + " ", + " " + sanitized + "", + " "); + } + + + void saveNumberODS(OutputStream output, String text) throws IOException { + writeUTF(output, + " ", + " " + text + "", + " "); + } + + + static Charset utf8; + + static void writeUTF(OutputStream output, String... lines) throws IOException { + if (utf8 == null) { + utf8 = Charset.forName("UTF-8"); + } + for (String str : lines) { + output.write(str.getBytes(utf8)); + output.write('\n'); + } + } + + + protected void saveBinary(OutputStream os) throws IOException { + DataOutputStream output = new DataOutputStream(new BufferedOutputStream(os)); + output.writeInt(0x9007AB1E); // version + output.writeInt(getRowCount()); + output.writeInt(getColumnCount()); + if (columnTitles != null) { + output.writeBoolean(true); + for (String title : columnTitles) { + output.writeUTF(title); + } + } else { + output.writeBoolean(false); + } + for (int i = 0; i < getColumnCount(); i++) { + //System.out.println(i + " is " + columnTypes[i]); + output.writeInt(columnTypes[i]); + } + + for (int i = 0; i < getColumnCount(); i++) { + if (columnTypes[i] == CATEGORY) { + columnCategories[i].write(output); + } + } + if (missingString == null) { + output.writeBoolean(false); + } else { + output.writeBoolean(true); + output.writeUTF(missingString); + } + output.writeInt(missingInt); + output.writeLong(missingLong); + output.writeFloat(missingFloat); + output.writeDouble(missingDouble); + output.writeInt(missingCategory); + + for (TableRow row : rows()) { + for (int col = 0; col < getColumnCount(); col++) { + switch (columnTypes[col]) { + case STRING: + String str = row.getString(col); + if (str == null) { + output.writeBoolean(false); + } else { + output.writeBoolean(true); + output.writeUTF(str); + } + break; + case INT: + output.writeInt(row.getInt(col)); + break; + case LONG: + output.writeLong(row.getLong(col)); + break; + case FLOAT: + output.writeFloat(row.getFloat(col)); + break; + case DOUBLE: + output.writeDouble(row.getDouble(col)); + break; + case CATEGORY: + String peace = row.getString(col); + if (peace.equals(missingString)) { + output.writeInt(missingCategory); + } else { + output.writeInt(columnCategories[col].index(peace)); + } + break; + } + } + } + + output.flush(); + output.close(); + } + + + protected void loadBinary(InputStream is) throws IOException { + DataInputStream input = new DataInputStream(new BufferedInputStream(is)); + + int magic = input.readInt(); + if (magic != 0x9007AB1E) { + throw new IOException("Not a compatible binary table (magic was " + PApplet.hex(magic) + ")"); + } + int rowCount = input.readInt(); + setRowCount(rowCount); + int columnCount = input.readInt(); + setColumnCount(columnCount); + + boolean hasTitles = input.readBoolean(); + if (hasTitles) { + columnTitles = new String[getColumnCount()]; + for (int i = 0; i < columnCount; i++) { + //columnTitles[i] = input.readUTF(); + setColumnTitle(i, input.readUTF()); + } + } + for (int column = 0; column < columnCount; column++) { + int newType = input.readInt(); + columnTypes[column] = newType; + switch (newType) { + case INT: + columns[column] = new int[rowCount]; + break; + case LONG: + columns[column] = new long[rowCount];; + break; + case FLOAT: + columns[column] = new float[rowCount];; + break; + case DOUBLE: + columns[column] = new double[rowCount];; + break; + case STRING: + columns[column] = new String[rowCount];; + break; + case CATEGORY: + columns[column] = new int[rowCount];; + break; + default: + throw new IllegalArgumentException(newType + " is not a valid column type."); + } + } + + for (int i = 0; i < columnCount; i++) { + if (columnTypes[i] == CATEGORY) { + columnCategories[i] = new HashMapBlows(input); + } + } + + if (input.readBoolean()) { + missingString = input.readUTF(); + } else { + missingString = null; + } + missingInt = input.readInt(); + missingLong = input.readLong(); + missingFloat = input.readFloat(); + missingDouble = input.readDouble(); + missingCategory = input.readInt(); + + for (int row = 0; row < rowCount; row++) { + for (int col = 0; col < columnCount; col++) { + switch (columnTypes[col]) { + case STRING: + String str = null; + if (input.readBoolean()) { + str = input.readUTF(); + } + setString(row, col, str); + break; + case INT: + setInt(row, col, input.readInt()); + break; + case LONG: + setLong(row, col, input.readLong()); + break; + case FLOAT: + setFloat(row, col, input.readFloat()); + break; + case DOUBLE: + setDouble(row, col, input.readDouble()); + break; + case CATEGORY: + int index = input.readInt(); + //String name = columnCategories[col].key(index); + setInt(row, col, index); + break; + } + } + } + + input.close(); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * @webref table:method + * @brief Adds a new column to a table + * @see Table#removeColumn(String) + */ + public void addColumn() { + addColumn(null, STRING); + } + + + /** + * @param title the title to be used for the new column + */ + public void addColumn(String title) { + addColumn(title, STRING); + } + + + /** + * @param type the type to be used for the new column: INT, LONG, FLOAT, DOUBLE, or STRING + */ + public void addColumn(String title, int type) { + insertColumn(columns.length, title, type); + } + + + public void insertColumn(int index) { + insertColumn(index, null, STRING); + } + + + public void insertColumn(int index, String title) { + insertColumn(index, title, STRING); + } + + + public void insertColumn(int index, String title, int type) { + if (title != null && columnTitles == null) { + columnTitles = new String[columns.length]; + } + if (columnTitles != null) { + columnTitles = PApplet.splice(columnTitles, title, index); + columnIndices = null; + } + columnTypes = PApplet.splice(columnTypes, type, index); + +// columnCategories = (HashMapBlows[]) +// PApplet.splice(columnCategories, new HashMapBlows(), index); + HashMapBlows[] catTemp = new HashMapBlows[columns.length + 1]; + // Faster than arrayCopy for a dozen or so entries + for (int i = 0; i < index; i++) { + catTemp[i] = columnCategories[i]; + } + catTemp[index] = new HashMapBlows(); + for (int i = index; i < columns.length; i++) { + catTemp[i+1] = columnCategories[i]; + } + columnCategories = catTemp; + + Object[] temp = new Object[columns.length + 1]; + System.arraycopy(columns, 0, temp, 0, index); + System.arraycopy(columns, index, temp, index+1, columns.length - index); + columns = temp; + + switch (type) { + case INT: columns[index] = new int[rowCount]; break; + case LONG: columns[index] = new long[rowCount]; break; + case FLOAT: columns[index] = new float[rowCount]; break; + case DOUBLE: columns[index] = new double[rowCount]; break; + case STRING: columns[index] = new String[rowCount]; break; + case CATEGORY: columns[index] = new int[rowCount]; break; + } + } + + /** + * @webref table:method + * @brief Removes a column from a table + * @param columnName the title of the column to be removed + * @see Table#addColumn() + */ + public void removeColumn(String columnName) { + removeColumn(getColumnIndex(columnName)); + } + + /** + * @param column the index number of the column to be removed + */ + public void removeColumn(int column) { + int newCount = columns.length - 1; + + Object[] columnsTemp = new Object[newCount]; + HashMapBlows[] catTemp = new HashMapBlows[newCount]; + + for (int i = 0; i < column; i++) { + columnsTemp[i] = columns[i]; + catTemp[i] = columnCategories[i]; + } + for (int i = column; i < newCount; i++) { + columnsTemp[i] = columns[i+1]; + catTemp[i] = columnCategories[i+1]; + } + + columns = columnsTemp; + columnCategories = catTemp; + + if (columnTitles != null) { + String[] titlesTemp = new String[newCount]; + for (int i = 0; i < column; i++) { + titlesTemp[i] = columnTitles[i]; + } + for (int i = column; i < newCount; i++) { + titlesTemp[i] = columnTitles[i+1]; + } + columnTitles = titlesTemp; + columnIndices = null; + } + } + + + /** + * @webref table:method + * @brief Gets the number of columns in a table + * @see Table#getRowCount() + */ + public int getColumnCount() { + return columns.length; + } + + + /** + * Change the number of columns in this table. Resizes all rows to ensure + * the same number of columns in each row. Entries in the additional (empty) + * columns will be set to null. + * @param newCount + */ + public void setColumnCount(int newCount) { + int oldCount = columns.length; + if (oldCount != newCount) { + columns = (Object[]) PApplet.expand(columns, newCount); + // create new columns, default to String as the data type + for (int c = oldCount; c < newCount; c++) { + columns[c] = new String[rowCount]; + } + + if (columnTitles != null) { + columnTitles = PApplet.expand(columnTitles, newCount); + } + columnTypes = PApplet.expand(columnTypes, newCount); + columnCategories = (HashMapBlows[]) + PApplet.expand(columnCategories, newCount); + } + } + + + public void setColumnType(String columnName, String columnType) { + setColumnType(checkColumnIndex(columnName), columnType); + } + + + static int parseColumnType(String columnType) { + columnType = columnType.toLowerCase(); + int type = -1; + if (columnType.equals("string")) { + type = STRING; + } else if (columnType.equals("int")) { + type = INT; + } else if (columnType.equals("long")) { + type = LONG; + } else if (columnType.equals("float")) { + type = FLOAT; + } else if (columnType.equals("double")) { + type = DOUBLE; + } else if (columnType.equals("category")) { + type = CATEGORY; + } else { + throw new IllegalArgumentException("'" + columnType + "' is not a valid column type."); + } + return type; + } + + + /** + * Set the data type for a column so that using it is more efficient. + * @param column the column to change + * @param columnType One of int, long, float, double, string, or category. + */ + public void setColumnType(int column, String columnType) { + setColumnType(column, parseColumnType(columnType)); + } + + + public void setColumnType(String columnName, int newType) { + setColumnType(checkColumnIndex(columnName), newType); + } + + + /** + * Sets the column type. If data already exists, then it'll be converted to + * the new type. + * @param column the column whose type should be changed + * @param newType something fresh, maybe try an int or a float for size? + */ + public void setColumnType(int column, int newType) { + switch (newType) { + case INT: { + int[] intData = new int[rowCount]; + for (int row = 0; row < rowCount; row++) { + String s = getString(row, column); + intData[row] = (s == null) ? missingInt : PApplet.parseInt(s, missingInt); + } + columns[column] = intData; + break; + } + case LONG: { + long[] longData = new long[rowCount]; + for (int row = 0; row < rowCount; row++) { + String s = getString(row, column); + try { + longData[row] = (s == null) ? missingLong : Long.parseLong(s); + } catch (NumberFormatException nfe) { + longData[row] = missingLong; + } + } + columns[column] = longData; + break; + } + case FLOAT: { + float[] floatData = new float[rowCount]; + for (int row = 0; row < rowCount; row++) { + String s = getString(row, column); + floatData[row] = (s == null) ? missingFloat : PApplet.parseFloat(s, missingFloat); + } + columns[column] = floatData; + break; + } + case DOUBLE: { + double[] doubleData = new double[rowCount]; + for (int row = 0; row < rowCount; row++) { + String s = getString(row, column); + try { + doubleData[row] = (s == null) ? missingDouble : Double.parseDouble(s); + } catch (NumberFormatException nfe) { + doubleData[row] = missingDouble; + } + } + columns[column] = doubleData; + break; + } + case STRING: { + if (columnTypes[column] != STRING) { + String[] stringData = new String[rowCount]; + for (int row = 0; row < rowCount; row++) { + stringData[row] = getString(row, column); + } + columns[column] = stringData; + } + break; + } + case CATEGORY: { + int[] indexData = new int[rowCount]; + HashMapBlows categories = new HashMapBlows(); + for (int row = 0; row < rowCount; row++) { + String s = getString(row, column); + indexData[row] = categories.index(s); + } + columnCategories[column] = categories; + columns[column] = indexData; + break; + } + default: { + throw new IllegalArgumentException("That's not a valid column type."); + } + } +// System.out.println("new type is " + newType); + columnTypes[column] = newType; + } + + + /** + * Set the entire table to a specific data type. + */ + public void setTableType(String type) { + for (int col = 0; col < getColumnCount(); col++) { + setColumnType(col, type); + } + } + + + public void setColumnTypes(int[] types) { + ensureColumn(types.length - 1); + for (int col = 0; col < types.length; col++) { + setColumnType(col, types[col]); + } + } + + + /** + * Set the titles (and if a second column is present) the data types for + * this table based on a file loaded separately. This will look for the + * title in column 0, and the type in column 1. Better yet, specify a + * column named "title" and another named "type" in the dictionary table + * to future-proof the code. + * @param dictionary + */ + public void setColumnTypes(final Table dictionary) { + ensureColumn(dictionary.getRowCount() - 1); + int titleCol = 0; + int typeCol = 1; + if (dictionary.hasColumnTitles()) { + titleCol = dictionary.getColumnIndex("title", true); + typeCol = dictionary.getColumnIndex("type", true); + } + setColumnTitles(dictionary.getStringColumn(titleCol)); + final String[] typeNames = dictionary.getStringColumn(typeCol); + + if (dictionary.getColumnCount() > 1) { + if (getRowCount() > 1000) { + int proc = Runtime.getRuntime().availableProcessors(); + ExecutorService pool = Executors.newFixedThreadPool(proc/2); + for (int i = 0; i < dictionary.getRowCount(); i++) { + final int col = i; + pool.execute(new Runnable() { + public void run() { + setColumnType(col, typeNames[col]); + } + }); + } + pool.shutdown(); + while (!pool.isTerminated()) { + Thread.yield(); + } + + } else { + for (int col = 0; col < dictionary.getRowCount(); col++) { +// setColumnType(i, dictionary.getString(i, typeCol)); + setColumnType(col, typeNames[col]); + } + } + } + } + + + public int getColumnType(String columnName) { + return getColumnType(getColumnIndex(columnName)); + } + + + /** Returns one of Table.STRING, Table.INT, etc... */ + public int getColumnType(int column) { + return columnTypes[column]; + } + + + public int[] getColumnTypes() { + return columnTypes; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Remove the first row from the data set, and use it as the column titles. + * Use loadTable("table.csv", "header") instead. + */ + @Deprecated + public String[] removeTitleRow() { + String[] titles = getStringRow(0); + removeRow(0); + setColumnTitles(titles); + return titles; + } + + + public void setColumnTitles(String[] titles) { + if (titles != null) { + ensureColumn(titles.length - 1); + } + columnTitles = titles; + columnIndices = null; // remove the cache + } + + + public void setColumnTitle(int column, String title) { + ensureColumn(column); + if (columnTitles == null) { + columnTitles = new String[getColumnCount()]; + } + columnTitles[column] = title; + columnIndices = null; // reset these fellas + } + + + public boolean hasColumnTitles() { + return columnTitles != null; + } + + + public String[] getColumnTitles() { + return columnTitles; + } + + + public String getColumnTitle(int col) { + return (columnTitles == null) ? null : columnTitles[col]; + } + + + public int getColumnIndex(String columnName) { + return getColumnIndex(columnName, true); + } + + + /** + * Get the index of a column. + * @param name Name of the column. + * @param report Whether to throw an exception if the column wasn't found. + * @return index of the found column, or -1 if not found. + */ + protected int getColumnIndex(String name, boolean report) { + if (columnTitles == null) { + if (report) { + throw new IllegalArgumentException("This table has no header, so no column titles are set."); + } + return -1; + } + // only create this on first get(). subsequent calls to set the title will + // also update this array, but only if it exists. + if (columnIndices == null) { + columnIndices = new HashMap<>(); + for (int col = 0; col < columns.length; col++) { + columnIndices.put(columnTitles[col], col); + } + } + Integer index = columnIndices.get(name); + if (index == null) { + if (report) { + // Throws an exception here because the name is known and therefore most useful. + // (Rather than waiting for it to fail inside, say, getInt()) + throw new IllegalArgumentException("This table has no column named '" + name + "'"); + } + return -1; + } + return index.intValue(); + } + + + /** + * Same as getColumnIndex(), but creates the column if it doesn't exist. + * Named this way to not conflict with checkColumn(), an internal function + * used to ensure that a columns exists, and also to denote that it returns + * an int for the column index. + * @param title column title + * @return index of a new or previously existing column + */ + public int checkColumnIndex(String title) { + int index = getColumnIndex(title, false); + if (index != -1) { + return index; + } + addColumn(title); + return getColumnCount() - 1; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + /** + * @webref table:method + * @brief Gets the number of rows in a table + * @see Table#getColumnCount() + */ + public int getRowCount() { + return rowCount; + } + + + public int lastRowIndex() { + return getRowCount() - 1; + } + + + /** + * @webref table:method + * @brief Removes all rows from a table + * @see Table#addRow() + * @see Table#removeRow(int) + */ + public void clearRows() { + setRowCount(0); + } + + + public void setRowCount(int newCount) { + if (newCount != rowCount) { + if (newCount > 1000000) { + System.out.print("Note: setting maximum row count to " + PApplet.nfc(newCount)); + } + long t = System.currentTimeMillis(); + for (int col = 0; col < columns.length; col++) { + switch (columnTypes[col]) { + case INT: columns[col] = PApplet.expand((int[]) columns[col], newCount); break; + case LONG: columns[col] = PApplet.expand((long[]) columns[col], newCount); break; + case FLOAT: columns[col] = PApplet.expand((float[]) columns[col], newCount); break; + case DOUBLE: columns[col] = PApplet.expand((double[]) columns[col], newCount); break; + case STRING: columns[col] = PApplet.expand((String[]) columns[col], newCount); break; + case CATEGORY: columns[col] = PApplet.expand((int[]) columns[col], newCount); break; + } + if (newCount > 1000000) { + try { + Thread.sleep(10); // gc time! + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + if (newCount > 1000000) { + int ms = (int) (System.currentTimeMillis() - t); + System.out.println(" (resize took " + PApplet.nfc(ms) + " ms)"); + } + } + rowCount = newCount; + } + + + /** + * @webref table:method + * @brief Adds a row to a table + * @see Table#removeRow(int) + * @see Table#clearRows() + */ + public TableRow addRow() { + //if (rowIncrement == 0) { + setRowCount(rowCount + 1); + return new RowPointer(this, rowCount - 1); + } + + + /** + * @param source a reference to the original row to be duplicated + */ + public TableRow addRow(TableRow source) { + return setRow(rowCount, source); + } + + + public TableRow setRow(int row, TableRow source) { + // Make sure there are enough columns to add this data + ensureBounds(row, source.getColumnCount() - 1); + + for (int col = 0; col < Math.min(source.getColumnCount(), columns.length); col++) { + switch (columnTypes[col]) { + case INT: + setInt(row, col, source.getInt(col)); + break; + case LONG: + setLong(row, col, source.getLong(col)); + break; + case FLOAT: + setFloat(row, col, source.getFloat(col)); + break; + case DOUBLE: + setDouble(row, col, source.getDouble(col)); + break; + case STRING: + setString(row, col, source.getString(col)); + break; + case CATEGORY: + int index = source.getInt(col); + setInt(row, col, index); + if (!columnCategories[col].hasCategory(index)) { + columnCategories[col].setCategory(index, source.getString(col)); + } + break; + + default: + throw new RuntimeException("no types"); + } + } + return new RowPointer(this, row); + } + + + /** + * @nowebref + */ + public TableRow addRow(Object[] columnData) { + setRow(getRowCount(), columnData); + return new RowPointer(this, rowCount - 1); + } + + + public void addRows(Table source) { + int index = getRowCount(); + setRowCount(index + source.getRowCount()); + for (TableRow row : source.rows()) { + setRow(index++, row); + } + } + + + public void insertRow(int insert, Object[] columnData) { + for (int col = 0; col < columns.length; col++) { + switch (columnTypes[col]) { + case CATEGORY: + case INT: { + int[] intTemp = new int[rowCount+1]; + System.arraycopy(columns[col], 0, intTemp, 0, insert); + System.arraycopy(columns[col], insert, intTemp, insert+1, rowCount - insert); + columns[col] = intTemp; + break; + } + case LONG: { + long[] longTemp = new long[rowCount+1]; + System.arraycopy(columns[col], 0, longTemp, 0, insert); + System.arraycopy(columns[col], insert, longTemp, insert+1, rowCount - insert); + columns[col] = longTemp; + break; + } + case FLOAT: { + float[] floatTemp = new float[rowCount+1]; + System.arraycopy(columns[col], 0, floatTemp, 0, insert); + System.arraycopy(columns[col], insert, floatTemp, insert+1, rowCount - insert); + columns[col] = floatTemp; + break; + } + case DOUBLE: { + double[] doubleTemp = new double[rowCount+1]; + System.arraycopy(columns[col], 0, doubleTemp, 0, insert); + System.arraycopy(columns[col], insert, doubleTemp, insert+1, rowCount - insert); + columns[col] = doubleTemp; + break; + } + case STRING: { + String[] stringTemp = new String[rowCount+1]; + System.arraycopy(columns[col], 0, stringTemp, 0, insert); + System.arraycopy(columns[col], insert, stringTemp, insert+1, rowCount - insert); + columns[col] = stringTemp; + break; + } + } + } + // Need to increment before setRow(), because it calls ensureBounds() + // https://github.com/processing/processing/issues/5406 + ++rowCount; + setRow(insert, columnData); + } + + + /** + * @webref table:method + * @brief Removes a row from a table + * @param row ID number of the row to remove + * @see Table#addRow() + * @see Table#clearRows() + */ + public void removeRow(int row) { + for (int col = 0; col < columns.length; col++) { + switch (columnTypes[col]) { + case CATEGORY: + case INT: { + int[] intTemp = new int[rowCount-1]; +// int[] intData = (int[]) columns[col]; +// System.arraycopy(intData, 0, intTemp, 0, dead); +// System.arraycopy(intData, dead+1, intTemp, dead, (rowCount - dead) + 1); + System.arraycopy(columns[col], 0, intTemp, 0, row); + System.arraycopy(columns[col], row+1, intTemp, row, (rowCount - row) - 1); + columns[col] = intTemp; + break; + } + case LONG: { + long[] longTemp = new long[rowCount-1]; +// long[] longData = (long[]) columns[col]; +// System.arraycopy(longData, 0, longTemp, 0, dead); +// System.arraycopy(longData, dead+1, longTemp, dead, (rowCount - dead) + 1); + System.arraycopy(columns[col], 0, longTemp, 0, row); + System.arraycopy(columns[col], row+1, longTemp, row, (rowCount - row) - 1); + columns[col] = longTemp; + break; + } + case FLOAT: { + float[] floatTemp = new float[rowCount-1]; +// float[] floatData = (float[]) columns[col]; +// System.arraycopy(floatData, 0, floatTemp, 0, dead); +// System.arraycopy(floatData, dead+1, floatTemp, dead, (rowCount - dead) + 1); + System.arraycopy(columns[col], 0, floatTemp, 0, row); + System.arraycopy(columns[col], row+1, floatTemp, row, (rowCount - row) - 1); + columns[col] = floatTemp; + break; + } + case DOUBLE: { + double[] doubleTemp = new double[rowCount-1]; +// double[] doubleData = (double[]) columns[col]; +// System.arraycopy(doubleData, 0, doubleTemp, 0, dead); +// System.arraycopy(doubleData, dead+1, doubleTemp, dead, (rowCount - dead) + 1); + System.arraycopy(columns[col], 0, doubleTemp, 0, row); + System.arraycopy(columns[col], row+1, doubleTemp, row, (rowCount - row) - 1); + columns[col] = doubleTemp; + break; + } + case STRING: { + String[] stringTemp = new String[rowCount-1]; + System.arraycopy(columns[col], 0, stringTemp, 0, row); + System.arraycopy(columns[col], row+1, stringTemp, row, (rowCount - row) - 1); + columns[col] = stringTemp; + } + } + } + rowCount--; + } + + + /* + public void setRow(int row, String[] pieces) { + checkSize(row, pieces.length - 1); + // pieces.length may be less than columns.length, so loop over pieces + for (int col = 0; col < pieces.length; col++) { + setRowCol(row, col, pieces[col]); + } + } + + + protected void setRowCol(int row, int col, String piece) { + switch (columnTypes[col]) { + case STRING: + String[] stringData = (String[]) columns[col]; + stringData[row] = piece; + break; + case INT: + int[] intData = (int[]) columns[col]; + intData[row] = PApplet.parseInt(piece, missingInt); + break; + case LONG: + long[] longData = (long[]) columns[col]; + try { + longData[row] = Long.parseLong(piece); + } catch (NumberFormatException nfe) { + longData[row] = missingLong; + } + break; + case FLOAT: + float[] floatData = (float[]) columns[col]; + floatData[row] = PApplet.parseFloat(piece, missingFloat); + break; + case DOUBLE: + double[] doubleData = (double[]) columns[col]; + try { + doubleData[row] = Double.parseDouble(piece); + } catch (NumberFormatException nfe) { + doubleData[row] = missingDouble; + } + break; + case CATEGORY: + int[] indexData = (int[]) columns[col]; + indexData[row] = columnCategories[col].index(piece); + break; + default: + throw new IllegalArgumentException("That's not a valid column type."); + } + } + */ + + + public void setRow(int row, Object[] pieces) { + ensureBounds(row, pieces.length - 1); + // pieces.length may be less than columns.length, so loop over pieces + for (int col = 0; col < pieces.length; col++) { + setRowCol(row, col, pieces[col]); + } + } + + + protected void setRowCol(int row, int col, Object piece) { + switch (columnTypes[col]) { + case STRING: + String[] stringData = (String[]) columns[col]; + if (piece == null) { + stringData[row] = null; +// } else if (piece instanceof String) { +// stringData[row] = (String) piece; + } else { + // Calls toString() on the object, which is 'return this' for String + stringData[row] = String.valueOf(piece); + } + break; + case INT: + int[] intData = (int[]) columns[col]; + //intData[row] = PApplet.parseInt(piece, missingInt); + if (piece == null) { + intData[row] = missingInt; + } else if (piece instanceof Integer) { + intData[row] = (Integer) piece; + } else { + intData[row] = PApplet.parseInt(String.valueOf(piece), missingInt); + } + break; + case LONG: + long[] longData = (long[]) columns[col]; + if (piece == null) { + longData[row] = missingLong; + } else if (piece instanceof Long) { + longData[row] = (Long) piece; + } else { + try { + longData[row] = Long.parseLong(String.valueOf(piece)); + } catch (NumberFormatException nfe) { + longData[row] = missingLong; + } + } + break; + case FLOAT: + float[] floatData = (float[]) columns[col]; + if (piece == null) { + floatData[row] = missingFloat; + } else if (piece instanceof Float) { + floatData[row] = (Float) piece; + } else { + floatData[row] = PApplet.parseFloat(String.valueOf(piece), missingFloat); + } + break; + case DOUBLE: + double[] doubleData = (double[]) columns[col]; + if (piece == null) { + doubleData[row] = missingDouble; + } else if (piece instanceof Double) { + doubleData[row] = (Double) piece; + } else { + try { + doubleData[row] = Double.parseDouble(String.valueOf(piece)); + } catch (NumberFormatException nfe) { + doubleData[row] = missingDouble; + } + } + break; + case CATEGORY: + int[] indexData = (int[]) columns[col]; + if (piece == null) { + indexData[row] = missingCategory; + } else { + String peace = String.valueOf(piece); + if (peace.equals(missingString)) { // missingString might be null + indexData[row] = missingCategory; + } else { + indexData[row] = columnCategories[col].index(peace); + } + } + break; + default: + throw new IllegalArgumentException("That's not a valid column type."); + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + /** + * @webref table:method + * @brief Gets a row from a table + * @param row ID number of the row to get + * @see Table#rows() + * @see Table#findRow(String, int) + * @see Table#findRows(String, int) + * @see Table#matchRow(String, int) + * @see Table#matchRows(String, int) + */ + public TableRow getRow(int row) { + return new RowPointer(this, row); + } + + + /** + * Note that this one iterator instance is shared by any calls to iterate + * the rows of this table. This is very efficient, but not thread-safe. + * If you want to iterate in a multi-threaded manner, don't use the iterator. + * + * @webref table:method + * @brief Gets multiple rows from a table + * @see Table#getRow(int) + * @see Table#findRow(String, int) + * @see Table#findRows(String, int) + * @see Table#matchRow(String, int) + * @see Table#matchRows(String, int) + */ + public Iterable rows() { + return new Iterable() { + public Iterator iterator() { + if (rowIterator == null) { + rowIterator = new RowIterator(Table.this); + } else { + rowIterator.reset(); + } + return rowIterator; + } + }; + } + + /** + * @nowebref + */ + public Iterable rows(final int[] indices) { + return new Iterable() { + public Iterator iterator() { + return new RowIndexIterator(Table.this, indices); + } + }; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + static class RowPointer implements TableRow { + Table table; + int row; + + public RowPointer(Table table, int row) { + this.table = table; + this.row = row; + } + + public void setRow(int row) { + this.row = row; + } + + public String getString(int column) { + return table.getString(row, column); + } + + public String getString(String columnName) { + return table.getString(row, columnName); + } + + public int getInt(int column) { + return table.getInt(row, column); + } + + public int getInt(String columnName) { + return table.getInt(row, columnName); + } + + public long getLong(int column) { + return table.getLong(row, column); + } + + public long getLong(String columnName) { + return table.getLong(row, columnName); + } + + public float getFloat(int column) { + return table.getFloat(row, column); + } + + public float getFloat(String columnName) { + return table.getFloat(row, columnName); + } + + public double getDouble(int column) { + return table.getDouble(row, column); + } + + public double getDouble(String columnName) { + return table.getDouble(row, columnName); + } + + public void setString(int column, String value) { + table.setString(row, column, value); + } + + public void setString(String columnName, String value) { + table.setString(row, columnName, value); + } + + public void setInt(int column, int value) { + table.setInt(row, column, value); + } + + public void setInt(String columnName, int value) { + table.setInt(row, columnName, value); + } + + public void setLong(int column, long value) { + table.setLong(row, column, value); + } + + public void setLong(String columnName, long value) { + table.setLong(row, columnName, value); + } + + public void setFloat(int column, float value) { + table.setFloat(row, column, value); + } + + public void setFloat(String columnName, float value) { + table.setFloat(row, columnName, value); + } + + public void setDouble(int column, double value) { + table.setDouble(row, column, value); + } + + public void setDouble(String columnName, double value) { + table.setDouble(row, columnName, value); + } + + public int getColumnCount() { + return table.getColumnCount(); + } + + public int getColumnType(String columnName) { + return table.getColumnType(columnName); + } + + public int getColumnType(int column) { + return table.getColumnType(column); + } + + public int[] getColumnTypes() { + return table.getColumnTypes(); + } + + public String getColumnTitle(int column) { + return table.getColumnTitle(column); + } + + public String[] getColumnTitles() { + return table.getColumnTitles(); + } + + public void print() { + write(new PrintWriter(System.out)); + } + + public void write(PrintWriter writer) { + for (int i = 0 ; i < getColumnCount(); i++) { + if (i != 0) { + writer.print('\t'); + } + writer.print(getString(i)); + } + } + } + + + static class RowIterator implements Iterator { + Table table; + RowPointer rp; + int row; + + public RowIterator(Table table) { + this.table = table; + row = -1; + rp = new RowPointer(table, row); + } + + public void remove() { + table.removeRow(row); + } + + public TableRow next() { + rp.setRow(++row); + return rp; + } + + public boolean hasNext() { + return row+1 < table.getRowCount(); + } + + public void reset() { + row = -1; + } + } + + + static class RowIndexIterator implements Iterator { + Table table; + RowPointer rp; + int[] indices; + int index; + + public RowIndexIterator(Table table, int[] indices) { + this.table = table; + this.indices = indices; + index = -1; + // just set to something arbitrary + rp = new RowPointer(table, -1); + } + + public void remove() { + table.removeRow(indices[index]); + } + + public TableRow next() { + rp.setRow(indices[++index]); + return rp; + } + + public boolean hasNext() { + //return row+1 < table.getRowCount(); + return index + 1 < indices.length; + } + + public void reset() { + index = -1; + } + } + + + /* + static public Iterator createIterator(final ResultSet rs) { + return new Iterator() { + boolean already; + + public boolean hasNext() { + already = true; + try { + return rs.next(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + + public TableRow next() { + if (!already) { + try { + rs.next(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } else { + already = false; + } + + return new TableRow() { + public double getDouble(int column) { + try { + return rs.getDouble(column); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public double getDouble(String columnName) { + try { + return rs.getDouble(columnName); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public float getFloat(int column) { + try { + return rs.getFloat(column); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public float getFloat(String columnName) { + try { + return rs.getFloat(columnName); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public int getInt(int column) { + try { + return rs.getInt(column); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public int getInt(String columnName) { + try { + return rs.getInt(columnName); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public long getLong(int column) { + try { + return rs.getLong(column); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public long getLong(String columnName) { + try { + return rs.getLong(columnName); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public String getString(int column) { + try { + return rs.getString(column); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public String getString(String columnName) { + try { + return rs.getString(columnName); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public void setString(int column, String value) { immutable(); } + public void setString(String columnName, String value) { immutable(); } + public void setInt(int column, int value) { immutable(); } + public void setInt(String columnName, int value) { immutable(); } + public void setLong(int column, long value) { immutable(); } + public void setLong(String columnName, long value) { immutable(); } + public void setFloat(int column, float value) { immutable(); } + public void setFloat(String columnName, float value) { immutable(); } + public void setDouble(int column, double value) { immutable(); } + public void setDouble(String columnName, double value) { immutable(); } + + private void immutable() { + throw new IllegalArgumentException("This TableRow cannot be modified."); + } + + public int getColumnCount() { + try { + return rs.getMetaData().getColumnCount(); + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } + + + public int getColumnType(String columnName) { + // unimplemented + } + + + public int getColumnType(int column) { + // unimplemented + } + + }; + } + + public void remove() { + throw new IllegalArgumentException("remove() not supported"); + } + }; + } + */ + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * @webref table:method + * @brief Get an integer value from the specified row and column + * @param row ID number of the row to reference + * @param column ID number of the column to reference + * @see Table#getFloat(int, int) + * @see Table#getString(int, int) + * @see Table#getStringColumn(String) + * @see Table#setInt(int, int, int) + * @see Table#setFloat(int, int, float) + * @see Table#setString(int, int, String) + */ + public int getInt(int row, int column) { + checkBounds(row, column); + if (columnTypes[column] == INT || + columnTypes[column] == CATEGORY) { + int[] intData = (int[]) columns[column]; + return intData[row]; + } + String str = getString(row, column); + return (str == null || str.equals(missingString)) ? + missingInt : PApplet.parseInt(str, missingInt); + } + + /** + * @param columnName title of the column to reference + */ + public int getInt(int row, String columnName) { + return getInt(row, getColumnIndex(columnName)); + } + + + public void setMissingInt(int value) { + missingInt = value; + } + + + /** + * @webref table:method + * @brief Store an integer value in the specified row and column + * @param row ID number of the target row + * @param column ID number of the target column + * @param value value to assign + * @see Table#setFloat(int, int, float) + * @see Table#setString(int, int, String) + * @see Table#getInt(int, int) + * @see Table#getFloat(int, int) + * @see Table#getString(int, int) + * @see Table#getStringColumn(String) + */ + public void setInt(int row, int column, int value) { + if (columnTypes[column] == STRING) { + setString(row, column, String.valueOf(value)); + + } else { + ensureBounds(row, column); + if (columnTypes[column] != INT && + columnTypes[column] != CATEGORY) { + throw new IllegalArgumentException("Column " + column + " is not an int column."); + } + int[] intData = (int[]) columns[column]; + intData[row] = value; + } + } + + /** + * @param columnName title of the target column + */ + public void setInt(int row, String columnName, int value) { + setInt(row, getColumnIndex(columnName), value); + } + + + + public int[] getIntColumn(String name) { + int col = getColumnIndex(name); + return (col == -1) ? null : getIntColumn(col); + } + + + public int[] getIntColumn(int col) { + int[] outgoing = new int[rowCount]; + for (int row = 0; row < rowCount; row++) { + outgoing[row] = getInt(row, col); + } + return outgoing; + } + + + public int[] getIntRow(int row) { + int[] outgoing = new int[columns.length]; + for (int col = 0; col < columns.length; col++) { + outgoing[col] = getInt(row, col); + } + return outgoing; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public long getLong(int row, int column) { + checkBounds(row, column); + if (columnTypes[column] == LONG) { + long[] longData = (long[]) columns[column]; + return longData[row]; + } + String str = getString(row, column); + if (str == null || str.equals(missingString)) { + return missingLong; + } + try { + return Long.parseLong(str); + } catch (NumberFormatException nfe) { + return missingLong; + } + } + + + public long getLong(int row, String columnName) { + return getLong(row, getColumnIndex(columnName)); + } + + + public void setMissingLong(long value) { + missingLong = value; + } + + + public void setLong(int row, int column, long value) { + if (columnTypes[column] == STRING) { + setString(row, column, String.valueOf(value)); + + } else { + ensureBounds(row, column); + if (columnTypes[column] != LONG) { + throw new IllegalArgumentException("Column " + column + " is not a 'long' column."); + } + long[] longData = (long[]) columns[column]; + longData[row] = value; + } + } + + + public void setLong(int row, String columnName, long value) { + setLong(row, getColumnIndex(columnName), value); + } + + + public long[] getLongColumn(String name) { + int col = getColumnIndex(name); + return (col == -1) ? null : getLongColumn(col); + } + + + public long[] getLongColumn(int col) { + long[] outgoing = new long[rowCount]; + for (int row = 0; row < rowCount; row++) { + outgoing[row] = getLong(row, col); + } + return outgoing; + } + + + public long[] getLongRow(int row) { + long[] outgoing = new long[columns.length]; + for (int col = 0; col < columns.length; col++) { + outgoing[col] = getLong(row, col); + } + return outgoing; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Get a float value from the specified row and column. If the value is null + * or not parseable as a float, the "missing" value is returned. By default, + * this is Float.NaN, but can be controlled with setMissingFloat(). + * + * @webref table:method + * @brief Get a float value from the specified row and column + * @param row ID number of the row to reference + * @param column ID number of the column to reference + * @see Table#getInt(int, int) + * @see Table#getString(int, int) + * @see Table#getStringColumn(String) + * @see Table#setInt(int, int, int) + * @see Table#setFloat(int, int, float) + * @see Table#setString(int, int, String) + */ + public float getFloat(int row, int column) { + checkBounds(row, column); + if (columnTypes[column] == FLOAT) { + float[] floatData = (float[]) columns[column]; + return floatData[row]; + } + String str = getString(row, column); + if (str == null || str.equals(missingString)) { + return missingFloat; + } + return PApplet.parseFloat(str, missingFloat); + } + + /** + * @param columnName title of the column to reference + */ + public float getFloat(int row, String columnName) { + return getFloat(row, getColumnIndex(columnName)); + } + + + public void setMissingFloat(float value) { + missingFloat = value; + } + + + /** + * @webref table:method + * @brief Store a float value in the specified row and column + * @param row ID number of the target row + * @param column ID number of the target column + * @param value value to assign + * @see Table#setInt(int, int, int) + * @see Table#setString(int, int, String) + * @see Table#getInt(int, int) + * @see Table#getFloat(int, int) + * @see Table#getString(int, int) + * @see Table#getStringColumn(String) + */ + public void setFloat(int row, int column, float value) { + if (columnTypes[column] == STRING) { + setString(row, column, String.valueOf(value)); + + } else { + ensureBounds(row, column); + if (columnTypes[column] != FLOAT) { + throw new IllegalArgumentException("Column " + column + " is not a float column."); + } + float[] longData = (float[]) columns[column]; + longData[row] = value; + } + } + + /** + * @param columnName title of the target column + */ + public void setFloat(int row, String columnName, float value) { + setFloat(row, getColumnIndex(columnName), value); + } + + + public float[] getFloatColumn(String name) { + int col = getColumnIndex(name); + return (col == -1) ? null : getFloatColumn(col); + } + + + public float[] getFloatColumn(int col) { + float[] outgoing = new float[rowCount]; + for (int row = 0; row < rowCount; row++) { + outgoing[row] = getFloat(row, col); + } + return outgoing; + } + + + public float[] getFloatRow(int row) { + float[] outgoing = new float[columns.length]; + for (int col = 0; col < columns.length; col++) { + outgoing[col] = getFloat(row, col); + } + return outgoing; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public double getDouble(int row, int column) { + checkBounds(row, column); + if (columnTypes[column] == DOUBLE) { + double[] doubleData = (double[]) columns[column]; + return doubleData[row]; + } + String str = getString(row, column); + if (str == null || str.equals(missingString)) { + return missingDouble; + } + try { + return Double.parseDouble(str); + } catch (NumberFormatException nfe) { + return missingDouble; + } + } + + + public double getDouble(int row, String columnName) { + return getDouble(row, getColumnIndex(columnName)); + } + + + public void setMissingDouble(double value) { + missingDouble = value; + } + + + public void setDouble(int row, int column, double value) { + if (columnTypes[column] == STRING) { + setString(row, column, String.valueOf(value)); + + } else { + ensureBounds(row, column); + if (columnTypes[column] != DOUBLE) { + throw new IllegalArgumentException("Column " + column + " is not a 'double' column."); + } + double[] doubleData = (double[]) columns[column]; + doubleData[row] = value; + } + } + + + public void setDouble(int row, String columnName, double value) { + setDouble(row, getColumnIndex(columnName), value); + } + + + public double[] getDoubleColumn(String name) { + int col = getColumnIndex(name); + return (col == -1) ? null : getDoubleColumn(col); + } + + + public double[] getDoubleColumn(int col) { + double[] outgoing = new double[rowCount]; + for (int row = 0; row < rowCount; row++) { + outgoing[row] = getDouble(row, col); + } + return outgoing; + } + + + public double[] getDoubleRow(int row) { + double[] outgoing = new double[columns.length]; + for (int col = 0; col < columns.length; col++) { + outgoing[col] = getDouble(row, col); + } + return outgoing; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + +//public long getTimestamp(String rowName, int column) { +//return getTimestamp(getRowIndex(rowName), column); +//} + + + /** + * Returns the time in milliseconds by parsing a SQL Timestamp at this cell. + */ +// public long getTimestamp(int row, int column) { +// String str = get(row, column); +// java.sql.Timestamp timestamp = java.sql.Timestamp.valueOf(str); +// return timestamp.getTime(); +// } + + +// public long getExcelTimestamp(int row, int column) { +// return parseExcelTimestamp(get(row, column)); +// } + + +// static protected DateFormat excelDateFormat; + +// static public long parseExcelTimestamp(String timestamp) { +// if (excelDateFormat == null) { +// excelDateFormat = new SimpleDateFormat("MM/dd/yy HH:mm"); +// } +// try { +// return excelDateFormat.parse(timestamp).getTime(); +// } catch (ParseException e) { +// e.printStackTrace(); +// return -1; +// } +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + +// public void setObject(int row, int column, Object value) { +// if (value == null) { +// data[row][column] = null; +// } else if (value instanceof String) { +// set(row, column, (String) value); +// } else if (value instanceof Float) { +// setFloat(row, column, ((Float) value).floatValue()); +// } else if (value instanceof Integer) { +// setInt(row, column, ((Integer) value).intValue()); +// } else { +// set(row, column, value.toString()); +// } +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Get a String value from the table. If the row is longer than the table + * + * @webref table:method + * @brief Get an String value from the specified row and column + * @param row ID number of the row to reference + * @param column ID number of the column to reference + * @see Table#getInt(int, int) + * @see Table#getFloat(int, int) + * @see Table#getStringColumn(String) + * @see Table#setInt(int, int, int) + * @see Table#setFloat(int, int, float) + * @see Table#setString(int, int, String) + */ + public String getString(int row, int column) { + checkBounds(row, column); + if (columnTypes[column] == STRING) { + String[] stringData = (String[]) columns[column]; + return stringData[row]; + } else if (columnTypes[column] == CATEGORY) { + int cat = getInt(row, column); + if (cat == missingCategory) { + return missingString; + } + return columnCategories[column].key(cat); + } else if (columnTypes[column] == FLOAT) { + if (Float.isNaN(getFloat(row, column))) { + return null; + } + } else if (columnTypes[column] == DOUBLE) { + if (Double.isNaN(getFloat(row, column))) { + return null; + } + } + return String.valueOf(Array.get(columns[column], row)); + } + + + /** + * @param columnName title of the column to reference + */ + public String getString(int row, String columnName) { + return getString(row, getColumnIndex(columnName)); + } + + + /** + * Treat entries with this string as "missing". Also used for categorial. + */ + public void setMissingString(String value) { + missingString = value; + } + + + /** + * @webref table:method + * @brief Store a String value in the specified row and column + * @param row ID number of the target row + * @param column ID number of the target column + * @param value value to assign + * @see Table#setInt(int, int, int) + * @see Table#setFloat(int, int, float) + * @see Table#getInt(int, int) + * @see Table#getFloat(int, int) + * @see Table#getString(int, int) + * @see Table#getStringColumn(String) + */ + public void setString(int row, int column, String value) { + ensureBounds(row, column); + if (columnTypes[column] != STRING) { + throw new IllegalArgumentException("Column " + column + " is not a String column."); + } + String[] stringData = (String[]) columns[column]; + stringData[row] = value; + } + + /** + * @param columnName title of the target column + */ + public void setString(int row, String columnName, String value) { + int column = checkColumnIndex(columnName); + setString(row, column, value); + } + + /** + * @webref table:method + * @brief Gets all values in the specified column + * @param columnName title of the column to search + * @see Table#getInt(int, int) + * @see Table#getFloat(int, int) + * @see Table#getString(int, int) + * @see Table#setInt(int, int, int) + * @see Table#setFloat(int, int, float) + * @see Table#setString(int, int, String) + */ + public String[] getStringColumn(String columnName) { + int col = getColumnIndex(columnName); + return (col == -1) ? null : getStringColumn(col); + } + + + /** + * @param column ID number of the column to search + */ + public String[] getStringColumn(int column) { + String[] outgoing = new String[rowCount]; + for (int i = 0; i < rowCount; i++) { + outgoing[i] = getString(i, column); + } + return outgoing; + } + + + public String[] getStringRow(int row) { + String[] outgoing = new String[columns.length]; + for (int col = 0; col < columns.length; col++) { + outgoing[col] = getString(row, col); + } + return outgoing; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Return the row that contains the first String that matches. + * @param value the String to match + * @param column ID number of the column to search + */ + public int findRowIndex(String value, int column) { + checkColumn(column); + if (columnTypes[column] == STRING) { + String[] stringData = (String[]) columns[column]; + if (value == null) { + for (int row = 0; row < rowCount; row++) { + if (stringData[row] == null) return row; + } + } else { + for (int row = 0; row < rowCount; row++) { + if (stringData[row] != null && stringData[row].equals(value)) { + return row; + } + } + } + } else { // less efficient, includes conversion as necessary + for (int row = 0; row < rowCount; row++) { + String str = getString(row, column); + if (str == null) { + if (value == null) { + return row; + } + } else if (str.equals(value)) { + return row; + } + } + } + return -1; + } + + + /** + * Return the row that contains the first String that matches. + * @param value the String to match + * @param columnName title of the column to search + */ + public int findRowIndex(String value, String columnName) { + return findRowIndex(value, getColumnIndex(columnName)); + } + + + /** + * Return a list of rows that contain the String passed in. If there are no + * matches, a zero length array will be returned (not a null array). + * @param value the String to match + * @param column ID number of the column to search + */ + public int[] findRowIndices(String value, int column) { + int[] outgoing = new int[rowCount]; + int count = 0; + + checkColumn(column); + if (columnTypes[column] == STRING) { + String[] stringData = (String[]) columns[column]; + if (value == null) { + for (int row = 0; row < rowCount; row++) { + if (stringData[row] == null) { + outgoing[count++] = row; + } + } + } else { + for (int row = 0; row < rowCount; row++) { + if (stringData[row] != null && stringData[row].equals(value)) { + outgoing[count++] = row; + } + } + } + } else { // less efficient, includes conversion as necessary + for (int row = 0; row < rowCount; row++) { + String str = getString(row, column); + if (str == null) { + if (value == null) { + outgoing[count++] = row; + } + } else if (str.equals(value)) { + outgoing[count++] = row; + } + } + } + return PApplet.subset(outgoing, 0, count); + } + + + /** + * Return a list of rows that contain the String passed in. If there are no + * matches, a zero length array will be returned (not a null array). + * @param value the String to match + * @param columnName title of the column to search + */ + public int[] findRowIndices(String value, String columnName) { + return findRowIndices(value, getColumnIndex(columnName)); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + /** + * @webref table:method + * @brief Finds a row that contains the given value + * @param value the value to match + * @param column ID number of the column to search + * @see Table#getRow(int) + * @see Table#rows() + * @see Table#findRows(String, int) + * @see Table#matchRow(String, int) + * @see Table#matchRows(String, int) + */ + public TableRow findRow(String value, int column) { + int row = findRowIndex(value, column); + return (row == -1) ? null : new RowPointer(this, row); + } + + + /** + * @param columnName title of the column to search + */ + public TableRow findRow(String value, String columnName) { + return findRow(value, getColumnIndex(columnName)); + } + + + /** + * @webref table:method + * @brief Finds multiple rows that contain the given value + * @param value the value to match + * @param column ID number of the column to search + * @see Table#getRow(int) + * @see Table#rows() + * @see Table#findRow(String, int) + * @see Table#matchRow(String, int) + * @see Table#matchRows(String, int) + */ + public Iterable findRows(final String value, final int column) { + return new Iterable() { + public Iterator iterator() { + return findRowIterator(value, column); + } + }; + } + + + /** + * @param columnName title of the column to search + */ + public Iterable findRows(final String value, final String columnName) { + return findRows(value, getColumnIndex(columnName)); + } + + + /** + * @brief Finds multiple rows that contain the given value + * @param value the value to match + * @param column ID number of the column to search + */ + public Iterator findRowIterator(String value, int column) { + return new RowIndexIterator(this, findRowIndices(value, column)); + } + + + /** + * @param columnName title of the column to search + */ + public Iterator findRowIterator(String value, String columnName) { + return findRowIterator(value, getColumnIndex(columnName)); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Return the row that contains the first String that matches. + * @param regexp the String to match + * @param column ID number of the column to search + */ + public int matchRowIndex(String regexp, int column) { + checkColumn(column); + if (columnTypes[column] == STRING) { + String[] stringData = (String[]) columns[column]; + for (int row = 0; row < rowCount; row++) { + if (stringData[row] != null && + PApplet.match(stringData[row], regexp) != null) { + return row; + } + } + } else { // less efficient, includes conversion as necessary + for (int row = 0; row < rowCount; row++) { + String str = getString(row, column); + if (str != null && + PApplet.match(str, regexp) != null) { + return row; + } + } + } + return -1; + } + + + /** + * Return the row that contains the first String that matches. + * @param what the String to match + * @param columnName title of the column to search + */ + public int matchRowIndex(String what, String columnName) { + return matchRowIndex(what, getColumnIndex(columnName)); + } + + + /** + * Return a list of rows that contain the String passed in. If there are no + * matches, a zero length array will be returned (not a null array). + * @param regexp the String to match + * @param column ID number of the column to search + */ + public int[] matchRowIndices(String regexp, int column) { + int[] outgoing = new int[rowCount]; + int count = 0; + + checkColumn(column); + if (columnTypes[column] == STRING) { + String[] stringData = (String[]) columns[column]; + for (int row = 0; row < rowCount; row++) { + if (stringData[row] != null && + PApplet.match(stringData[row], regexp) != null) { + outgoing[count++] = row; + } + } + } else { // less efficient, includes conversion as necessary + for (int row = 0; row < rowCount; row++) { + String str = getString(row, column); + if (str != null && + PApplet.match(str, regexp) != null) { + outgoing[count++] = row; + } + } + } + return PApplet.subset(outgoing, 0, count); + } + + + /** + * Return a list of rows that match the regex passed in. If there are no + * matches, a zero length array will be returned (not a null array). + * @param what the String to match + * @param columnName title of the column to search + */ + public int[] matchRowIndices(String what, String columnName) { + return matchRowIndices(what, getColumnIndex(columnName)); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + /** + * @webref table:method + * @brief Finds a row that matches the given expression + * @param regexp the regular expression to match + * @param column ID number of the column to search + * @see Table#getRow(int) + * @see Table#rows() + * @see Table#findRow(String, int) + * @see Table#findRows(String, int) + * @see Table#matchRows(String, int) + */ + public TableRow matchRow(String regexp, int column) { + int row = matchRowIndex(regexp, column); + return (row == -1) ? null : new RowPointer(this, row); + } + + + /** + * @param columnName title of the column to search + */ + public TableRow matchRow(String regexp, String columnName) { + return matchRow(regexp, getColumnIndex(columnName)); + } + + + /** + * @webref table:method + * @brief Finds multiple rows that match the given expression + * @param regexp the regular expression to match + * @param column ID number of the column to search + * @see Table#getRow(int) + * @see Table#rows() + * @see Table#findRow(String, int) + * @see Table#findRows(String, int) + * @see Table#matchRow(String, int) + */ + public Iterable matchRows(final String regexp, final int column) { + return new Iterable() { + public Iterator iterator() { + return matchRowIterator(regexp, column); + } + }; + } + + + /** + * @param columnName title of the column to search + */ + public Iterable matchRows(String regexp, String columnName) { + return matchRows(regexp, getColumnIndex(columnName)); + } + + + /** + * @webref table:method + * @brief Finds multiple rows that match the given expression + * @param value the regular expression to match + * @param column ID number of the column to search + */ + public Iterator matchRowIterator(String value, int column) { + return new RowIndexIterator(this, matchRowIndices(value, column)); + } + + + /** + * @param columnName title of the column to search + */ + public Iterator matchRowIterator(String value, String columnName) { + return matchRowIterator(value, getColumnIndex(columnName)); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Replace a String with another. Set empty entries null by using + * replace("", null) or use replace(null, "") to go the other direction. + * If this is a typed table, only String columns will be modified. + * @param orig + * @param replacement + */ + public void replace(String orig, String replacement) { + for (int col = 0; col < columns.length; col++) { + replace(orig, replacement, col); + } + } + + + public void replace(String orig, String replacement, int col) { + if (columnTypes[col] == STRING) { + String[] stringData = (String[]) columns[col]; + + if (orig != null) { + for (int row = 0; row < rowCount; row++) { + if (orig.equals(stringData[row])) { + stringData[row] = replacement; + } + } + } else { // null is a special case (and faster anyway) + for (int row = 0; row < rowCount; row++) { + if (stringData[row] == null) { + stringData[row] = replacement; + } + } + } + } + } + + + public void replace(String orig, String replacement, String colName) { + replace(orig, replacement, getColumnIndex(colName)); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public void replaceAll(String regex, String replacement) { + for (int col = 0; col < columns.length; col++) { + replaceAll(regex, replacement, col); + } + } + + + public void replaceAll(String regex, String replacement, int column) { + checkColumn(column); + if (columnTypes[column] == STRING) { + String[] stringData = (String[]) columns[column]; + for (int row = 0; row < rowCount; row++) { + if (stringData[row] != null) { + stringData[row] = stringData[row].replaceAll(regex, replacement); + } + } + } else { + throw new IllegalArgumentException("replaceAll() can only be used on String columns"); + } + } + + + /** + * Run String.replaceAll() on all entries in a column. + * Only works with columns that are already String values. + * @param regex the String to match + * @param columnName title of the column to search + */ + public void replaceAll(String regex, String replacement, String columnName) { + replaceAll(regex, replacement, getColumnIndex(columnName)); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Remove any of the specified characters from the entire table. + * + * @webref table:method + * @brief Removes characters from the table + * @param tokens a list of individual characters to be removed + * @see Table#trim() + */ + public void removeTokens(String tokens) { + for (int col = 0; col < getColumnCount(); col++) { + removeTokens(tokens, col); + } + } + + + /** + * Removed any of the specified characters from a column. For instance, + * the following code removes dollar signs and commas from column 2: + *

        +   * table.removeTokens(",$", 2);
        +   * 
        + * + * @param column ID number of the column to process + */ + public void removeTokens(String tokens, int column) { + for (int row = 0; row < rowCount; row++) { + String s = getString(row, column); + if (s != null) { + char[] c = s.toCharArray(); + int index = 0; + for (int j = 0; j < c.length; j++) { + if (tokens.indexOf(c[j]) == -1) { + if (index != j) { + c[index] = c[j]; + } + index++; + } + } + if (index != c.length) { + setString(row, column, new String(c, 0, index)); + } + } + } + } + + /** + * @param columnName title of the column to process + */ + public void removeTokens(String tokens, String columnName) { + removeTokens(tokens, getColumnIndex(columnName)); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * @webref table:method + * @brief Trims whitespace from values + * @see Table#removeTokens(String) + */ + public void trim() { + columnTitles = PApplet.trim(columnTitles); + for (int col = 0; col < getColumnCount(); col++) { + trim(col); + } + // remove empty columns + int lastColumn = getColumnCount() - 1; + //while (isEmptyColumn(lastColumn) && lastColumn >= 0) { + while (isEmptyArray(getStringColumn(lastColumn)) && lastColumn >= 0) { + lastColumn--; + } + setColumnCount(lastColumn + 1); + + // trim() works from both sides + while (getColumnCount() > 0 && isEmptyArray(getStringColumn(0))) { + removeColumn(0); + } + + // remove empty rows (starting from the end) + int lastRow = lastRowIndex(); + //while (isEmptyRow(lastRow) && lastRow >= 0) { + while (isEmptyArray(getStringRow(lastRow)) && lastRow >= 0) { + lastRow--; + } + setRowCount(lastRow + 1); + + while (getRowCount() > 0 && isEmptyArray(getStringRow(0))) { + removeRow(0); + } + } + + + protected boolean isEmptyArray(String[] contents) { + for (String entry : contents) { + if (entry != null && entry.length() > 0) { + return false; + } + } + return true; + } + + + /* + protected boolean isEmptyColumn(int column) { + String[] contents = getStringColumn(column); + for (String entry : contents) { + if (entry != null && entry.length() > 0) { + return false; + } + } + return true; + } + + + protected boolean isEmptyRow(int row) { + String[] contents = getStringRow(row); + for (String entry : contents) { + if (entry != null && entry.length() > 0) { + return false; + } + } + return true; + } + */ + + + /** + * @param column ID number of the column to trim + */ + public void trim(int column) { + if (columnTypes[column] == STRING) { + String[] stringData = (String[]) columns[column]; + for (int row = 0; row < rowCount; row++) { + if (stringData[row] != null) { + stringData[row] = PApplet.trim(stringData[row]); + } + } + } + } + + /** + * @param columnName title of the column to trim + */ + public void trim(String columnName) { + trim(getColumnIndex(columnName)); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** Make sure this is a legit column, and if not, expand the table. */ + protected void ensureColumn(int col) { + if (col >= columns.length) { + setColumnCount(col + 1); + } + } + + + /** Make sure this is a legit row, and if not, expand the table. */ + protected void ensureRow(int row) { + if (row >= rowCount) { + setRowCount(row + 1); + } + } + + + /** Make sure this is a legit row and column. If not, expand the table. */ + protected void ensureBounds(int row, int col) { + ensureRow(row); + ensureColumn(col); + } + + + /** Throw an error if this row doesn't exist. */ + protected void checkRow(int row) { + if (row < 0 || row >= rowCount) { + throw new ArrayIndexOutOfBoundsException("Row " + row + " does not exist."); + } + } + + + /** Throw an error if this column doesn't exist. */ + protected void checkColumn(int column) { + if (column < 0 || column >= columns.length) { + throw new ArrayIndexOutOfBoundsException("Column " + column + " does not exist."); + } + } + + + /** Throw an error if this entry is out of bounds. */ + protected void checkBounds(int row, int column) { + checkRow(row); + checkColumn(column); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + static class HashMapBlows { + HashMap dataToIndex = new HashMap<>(); + ArrayList indexToData = new ArrayList<>(); + + HashMapBlows() { } + + HashMapBlows(DataInputStream input) throws IOException { + read(input); + } + + /** gets the index, and creates one if it doesn't already exist. */ + int index(String key) { + Integer value = dataToIndex.get(key); + if (value != null) { + return value; + } + + int v = dataToIndex.size(); + dataToIndex.put(key, v); + indexToData.add(key); + return v; + } + + String key(int index) { + return indexToData.get(index); + } + + boolean hasCategory(int index) { + return index < size() && indexToData.get(index) != null; + } + + void setCategory(int index, String name) { + while (indexToData.size() <= index) { + indexToData.add(null); + } + indexToData.set(index, name); + dataToIndex.put(name, index); + } + + int size() { + return dataToIndex.size(); + } + + void write(DataOutputStream output) throws IOException { + output.writeInt(size()); + for (String str : indexToData) { + output.writeUTF(str); + } + } + + private void writeln(PrintWriter writer) throws IOException { + for (String str : indexToData) { + writer.println(str); + } + writer.flush(); + writer.close(); + } + + void read(DataInputStream input) throws IOException { + int count = input.readInt(); + //System.out.println("found " + count + " entries in category map"); + dataToIndex = new HashMap<>(count); + for (int i = 0; i < count; i++) { + String str = input.readUTF(); + //System.out.println(i + " " + str); + dataToIndex.put(str, i); + indexToData.add(str); + } + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + +// class HashMapSucks extends HashMap { +// +// void increment(String what) { +// Integer value = get(what); +// if (value == null) { +// put(what, 1); +// } else { +// put(what, value + 1); +// } +// } +// +// void check(String what) { +// if (get(what) == null) { +// put(what, 0); +// } +// } +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + /** + * Sorts (orders) a table based on the values in a column. + * + * @webref table:method + * @brief Orders a table based on the values in a column + * @param columnName the name of the column to sort + * @see Table#trim() + */ + public void sort(String columnName) { + sort(getColumnIndex(columnName), false); + } + + /** + * @param column the column ID, e.g. 0, 1, 2 + */ + public void sort(int column) { + sort(column, false); + } + + + public void sortReverse(String columnName) { + sort(getColumnIndex(columnName), true); + } + + + public void sortReverse(int column) { + sort(column, true); + } + + + protected void sort(final int column, final boolean reverse) { + final int[] order = IntList.fromRange(getRowCount()).array(); + Sort s = new Sort() { + + @Override + public int size() { + return getRowCount(); + } + + @Override + public float compare(int index1, int index2) { + int a = reverse ? order[index2] : order[index1]; + int b = reverse ? order[index1] : order[index2]; + + switch (getColumnType(column)) { + case INT: + return getInt(a, column) - getInt(b, column); + case LONG: + return getLong(a, column) - getLong(b, column); + case FLOAT: + return getFloat(a, column) - getFloat(b, column); + case DOUBLE: + return (float) (getDouble(a, column) - getDouble(b, column)); + case STRING: + return getString(a, column).compareToIgnoreCase(getString(b, column)); + case CATEGORY: + return getInt(a, column) - getInt(b, column); + default: + throw new IllegalArgumentException("Invalid column type: " + getColumnType(column)); + } + } + + @Override + public void swap(int a, int b) { + int temp = order[a]; + order[a] = order[b]; + order[b] = temp; + } + + }; + s.run(); + + //Object[] newColumns = new Object[getColumnCount()]; + for (int col = 0; col < getColumnCount(); col++) { + switch (getColumnType(col)) { + case INT: + case CATEGORY: + int[] oldInt = (int[]) columns[col]; + int[] newInt = new int[rowCount]; + for (int row = 0; row < getRowCount(); row++) { + newInt[row] = oldInt[order[row]]; + } + columns[col] = newInt; + break; + case LONG: + long[] oldLong = (long[]) columns[col]; + long[] newLong = new long[rowCount]; + for (int row = 0; row < getRowCount(); row++) { + newLong[row] = oldLong[order[row]]; + } + columns[col] = newLong; + break; + case FLOAT: + float[] oldFloat = (float[]) columns[col]; + float[] newFloat = new float[rowCount]; + for (int row = 0; row < getRowCount(); row++) { + newFloat[row] = oldFloat[order[row]]; + } + columns[col] = newFloat; + break; + case DOUBLE: + double[] oldDouble = (double[]) columns[col]; + double[] newDouble = new double[rowCount]; + for (int row = 0; row < getRowCount(); row++) { + newDouble[row] = oldDouble[order[row]]; + } + columns[col] = newDouble; + break; + case STRING: + String[] oldString = (String[]) columns[col]; + String[] newString = new String[rowCount]; + for (int row = 0; row < getRowCount(); row++) { + newString[row] = oldString[order[row]]; + } + columns[col] = newString; + break; + } + } + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public String[] getUnique(String columnName) { + return getUnique(getColumnIndex(columnName)); + } + + + public String[] getUnique(int column) { + StringList list = new StringList(getStringColumn(column)); + return list.getUnique(); + } + + + public IntDict getTally(String columnName) { + return getTally(getColumnIndex(columnName)); + } + + + public IntDict getTally(int column) { + StringList list = new StringList(getStringColumn(column)); + return list.getTally(); + } + + + public IntDict getOrder(String columnName) { + return getOrder(getColumnIndex(columnName)); + } + + + public IntDict getOrder(int column) { + StringList list = new StringList(getStringColumn(column)); + return list.getOrder(); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public IntList getIntList(String columnName) { + return new IntList(getIntColumn(columnName)); + } + + + public IntList getIntList(int column) { + return new IntList(getIntColumn(column)); + } + + + public FloatList getFloatList(String columnName) { + return new FloatList(getFloatColumn(columnName)); + } + + + public FloatList getFloatList(int column) { + return new FloatList(getFloatColumn(column)); + } + + + public StringList getStringList(String columnName) { + return new StringList(getStringColumn(columnName)); + } + + + public StringList getStringList(int column) { + return new StringList(getStringColumn(column)); + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + public IntDict getIntDict(String keyColumnName, String valueColumnName) { + return new IntDict(getStringColumn(keyColumnName), + getIntColumn(valueColumnName)); + } + + + public IntDict getIntDict(int keyColumn, int valueColumn) { + return new IntDict(getStringColumn(keyColumn), + getIntColumn(valueColumn)); + } + + + public FloatDict getFloatDict(String keyColumnName, String valueColumnName) { + return new FloatDict(getStringColumn(keyColumnName), + getFloatColumn(valueColumnName)); + } + + + public FloatDict getFloatDict(int keyColumn, int valueColumn) { + return new FloatDict(getStringColumn(keyColumn), + getFloatColumn(valueColumn)); + } + + + public StringDict getStringDict(String keyColumnName, String valueColumnName) { + return new StringDict(getStringColumn(keyColumnName), + getStringColumn(valueColumnName)); + } + + + public StringDict getStringDict(int keyColumn, int valueColumn) { + return new StringDict(getStringColumn(keyColumn), + getStringColumn(valueColumn)); + } + + + public Map getRowMap(String columnName) { + int col = getColumnIndex(columnName); + return (col == -1) ? null : getRowMap(col); + } + + + /** + * Return a mapping that connects the entry from a column back to the row + * from which it came. For instance: + *
        +   * Table t = loadTable("country-data.tsv", "header");
        +   * // use the contents of the 'country' column to index the table
        +   * Map lookup = t.getRowMap("country");
        +   * // get the row that has "us" in the "country" column:
        +   * TableRow usRow = lookup.get("us");
        +   * // get an entry from the 'population' column
        +   * int population = usRow.getInt("population");
        +   * 
        + */ + public Map getRowMap(int column) { + Map outgoing = new HashMap<>(); + for (int row = 0; row < getRowCount(); row++) { + String id = getString(row, column); + outgoing.put(id, new RowPointer(this, row)); + } +// for (TableRow row : rows()) { +// String id = row.getString(column); +// outgoing.put(id, row); +// } + return outgoing; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + +// /** +// * Return an object that maps the String values in one column back to the +// * row from which they came. For instance, if the "name" of each row is +// * found in the first column, getColumnRowLookup(0) would return an object +// * that would map each name back to its row. +// */ +// protected HashMap getRowLookup(int col) { +// HashMap outgoing = new HashMap(); +// for (int row = 0; row < getRowCount(); row++) { +// outgoing.put(getString(row, col), row); +// } +// return outgoing; +// } + + + // incomplete, basically this is silly to write all this repetitive code when + // it can be implemented in ~3 lines of code... +// /** +// * Return an object that maps the data from one column to the data of found +// * in another column. +// */ +// public HashMap getLookup(int col1, int col2) { +// HashMap outgoing = null; +// +// switch (columnTypes[col1]) { +// case INT: { +// if (columnTypes[col2] == INT) { +// outgoing = new HashMap(); +// for (int row = 0; row < getRowCount(); row++) { +// outgoing.put(getInt(row, col1), getInt(row, col2)); +// } +// } else if (columnTypes[col2] == LONG) { +// outgoing = new HashMap(); +// for (int row = 0; row < getRowCount(); row++) { +// outgoing.put(getInt(row, col1), getLong(row, col2)); +// } +// } else if (columnTypes[col2] == FLOAT) { +// outgoing = new HashMap(); +// for (int row = 0; row < getRowCount(); row++) { +// outgoing.put(getInt(row, col1), getFloat(row, col2)); +// } +// } else if (columnTypes[col2] == DOUBLE) { +// outgoing = new HashMap(); +// for (int row = 0; row < getRowCount(); row++) { +// outgoing.put(getInt(row, col1), getDouble(row, col2)); +// } +// } else if (columnTypes[col2] == STRING) { +// outgoing = new HashMap(); +// for (int row = 0; row < getRowCount(); row++) { +// outgoing.put(getInt(row, col1), get(row, col2)); +// } +// } +// break; +// } +// } +// return outgoing; +// } + + + // public StringIntPairs getColumnRowLookup(int col) { +// StringIntPairs sc = new StringIntPairs(); +// String[] column = getStringColumn(col); +// for (int i = 0; i < column.length; i++) { +// sc.set(column[i], i); +// } +// return sc; +// } + + +// public String[] getUniqueEntries(int column) { +//// HashMap indices = new HashMap(); +//// for (int row = 0; row < rowCount; row++) { +//// indices.put(data[row][column], this); // 'this' is a dummy +//// } +// StringIntPairs sc = getStringCount(column); +// return sc.keys(); +// } +// +// +// public StringIntPairs getStringCount(String columnName) { +// return getStringCount(getColumnIndex(columnName)); +// } +// +// +// public StringIntPairs getStringCount(int column) { +// StringIntPairs outgoing = new StringIntPairs(); +// for (int row = 0; row < rowCount; row++) { +// String entry = data[row][column]; +// if (entry != null) { +// outgoing.increment(entry); +// } +// } +// return outgoing; +// } +// +// +// /** +// * Return an object that maps the String values in one column back to the +// * row from which they came. For instance, if the "name" of each row is +// * found in the first column, getColumnRowLookup(0) would return an object +// * that would map each name back to its row. +// */ +// public StringIntPairs getColumnRowLookup(int col) { +// StringIntPairs sc = new StringIntPairs(); +// String[] column = getStringColumn(col); +// for (int i = 0; i < column.length; i++) { +// sc.set(column[i], i); +// } +// return sc; +// } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + // TODO naming/whether to include + protected Table createSubset(int[] rowSubset) { + Table newbie = new Table(); + newbie.setColumnTitles(columnTitles); // also sets columns.length + newbie.columnTypes = columnTypes; + newbie.setRowCount(rowSubset.length); + + for (int i = 0; i < rowSubset.length; i++) { + int row = rowSubset[i]; + for (int col = 0; col < columns.length; col++) { + switch (columnTypes[col]) { + case STRING: newbie.setString(i, col, getString(row, col)); break; + case INT: newbie.setInt(i, col, getInt(row, col)); break; + case LONG: newbie.setLong(i, col, getLong(row, col)); break; + case FLOAT: newbie.setFloat(i, col, getFloat(row, col)); break; + case DOUBLE: newbie.setDouble(i, col, getDouble(row, col)); break; + } + } + } + return newbie; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /** + * Searches the entire table for float values. + * Returns missing float (Float.NaN by default) if no valid numbers found. + */ + protected float getMaxFloat() { + boolean found = false; + float max = PConstants.MIN_FLOAT; + for (int row = 0; row < getRowCount(); row++) { + for (int col = 0; col < getColumnCount(); col++) { + float value = getFloat(row, col); + if (!Float.isNaN(value)) { // TODO no, this should be comparing to the missing value + if (!found) { + max = value; + found = true; + } else if (value > max) { + max = value; + } + } + } + } + return found ? max : missingFloat; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + // converts a TSV or CSV file to binary.. do not use + protected void convertBasic(BufferedReader reader, boolean tsv, + File outputFile) throws IOException { + FileOutputStream fos = new FileOutputStream(outputFile); + BufferedOutputStream bos = new BufferedOutputStream(fos, 16384); + DataOutputStream output = new DataOutputStream(bos); + output.writeInt(0); // come back for row count + output.writeInt(getColumnCount()); + if (columnTitles != null) { + output.writeBoolean(true); + for (String title : columnTitles) { + output.writeUTF(title); + } + } else { + output.writeBoolean(false); + } + for (int type : columnTypes) { + output.writeInt(type); + } + + String line = null; + //setRowCount(1); + int prev = -1; + int row = 0; + while ((line = reader.readLine()) != null) { + convertRow(output, tsv ? PApplet.split(line, '\t') : splitLineCSV(line, reader)); + row++; + + if (row % 10000 == 0) { + if (row < rowCount) { + int pct = (100 * row) / rowCount; + if (pct != prev) { + System.out.println(pct + "%"); + prev = pct; + } + } +// try { +// Thread.sleep(5); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } + } + } + // shorten or lengthen based on what's left +// if (row != getRowCount()) { +// setRowCount(row); +// } + + // has to come afterwards, since these tables get built out during the conversion + int col = 0; + for (HashMapBlows hmb : columnCategories) { + if (hmb == null) { + output.writeInt(0); + } else { + hmb.write(output); + hmb.writeln(PApplet.createWriter(new File(columnTitles[col] + ".categories"))); +// output.writeInt(hmb.size()); +// for (Map.Entry e : hmb.entrySet()) { +// output.writeUTF(e.getKey()); +// output.writeInt(e.getValue()); +// } + } + col++; + } + + output.flush(); + output.close(); + + // come back and write the row count + RandomAccessFile raf = new RandomAccessFile(outputFile, "rw"); + raf.writeInt(rowCount); + raf.close(); + } + + + protected void convertRow(DataOutputStream output, String[] pieces) throws IOException { + if (pieces.length > getColumnCount()) { + throw new IllegalArgumentException("Row with too many columns: " + + PApplet.join(pieces, ",")); + } + // pieces.length may be less than columns.length, so loop over pieces + for (int col = 0; col < pieces.length; col++) { + switch (columnTypes[col]) { + case STRING: + output.writeUTF(pieces[col]); + break; + case INT: + output.writeInt(PApplet.parseInt(pieces[col], missingInt)); + break; + case LONG: + try { + output.writeLong(Long.parseLong(pieces[col])); + } catch (NumberFormatException nfe) { + output.writeLong(missingLong); + } + break; + case FLOAT: + output.writeFloat(PApplet.parseFloat(pieces[col], missingFloat)); + break; + case DOUBLE: + try { + output.writeDouble(Double.parseDouble(pieces[col])); + } catch (NumberFormatException nfe) { + output.writeDouble(missingDouble); + } + break; + case CATEGORY: + String peace = pieces[col]; + if (peace.equals(missingString)) { + output.writeInt(missingCategory); + } else { + output.writeInt(columnCategories[col].index(peace)); + } + break; + } + } + for (int col = pieces.length; col < getColumnCount(); col++) { + switch (columnTypes[col]) { + case STRING: + output.writeUTF(""); + break; + case INT: + output.writeInt(missingInt); + break; + case LONG: + output.writeLong(missingLong); + break; + case FLOAT: + output.writeFloat(missingFloat); + break; + case DOUBLE: + output.writeDouble(missingDouble); + break; + case CATEGORY: + output.writeInt(missingCategory); + break; + + } + } + } + + + /* + private void convertRowCol(DataOutputStream output, int row, int col, String piece) { + switch (columnTypes[col]) { + case STRING: + String[] stringData = (String[]) columns[col]; + stringData[row] = piece; + break; + case INT: + int[] intData = (int[]) columns[col]; + intData[row] = PApplet.parseInt(piece, missingInt); + break; + case LONG: + long[] longData = (long[]) columns[col]; + try { + longData[row] = Long.parseLong(piece); + } catch (NumberFormatException nfe) { + longData[row] = missingLong; + } + break; + case FLOAT: + float[] floatData = (float[]) columns[col]; + floatData[row] = PApplet.parseFloat(piece, missingFloat); + break; + case DOUBLE: + double[] doubleData = (double[]) columns[col]; + try { + doubleData[row] = Double.parseDouble(piece); + } catch (NumberFormatException nfe) { + doubleData[row] = missingDouble; + } + break; + default: + throw new IllegalArgumentException("That's not a valid column type."); + } + } + */ + + + /** Make a copy of the current table */ + public Table copy() { + return new Table(rows()); + } + + + public void write(PrintWriter writer) { + writeTSV(writer); + } + + + public void print() { + writeTSV(new PrintWriter(System.out)); + } +} diff --git a/src/main/java/processing/data/TableRow.java b/src/main/java/processing/data/TableRow.java new file mode 100644 index 0000000..3ac59fe --- /dev/null +++ b/src/main/java/processing/data/TableRow.java @@ -0,0 +1,198 @@ +package processing.data; + +import java.io.PrintWriter; + +/** + * @webref data:composite + * @see Table + * @see Table#addRow() + * @see Table#removeRow(int) + * @see Table#clearRows() + * @see Table#getRow(int) + * @see Table#rows() + */ +public interface TableRow { + + /** + * @webref tablerow:method + * @brief Get an String value from the specified column + * @param column ID number of the column to reference + * @see TableRow#getInt(int) + * @see TableRow#getFloat(int) + */ + public String getString(int column); + + /** + * @param columnName title of the column to reference + */ + public String getString(String columnName); + + /** + * @webref tablerow:method + * @brief Get an integer value from the specified column + * @param column ID number of the column to reference + * @see TableRow#getFloat(int) + * @see TableRow#getString(int) + */ + public int getInt(int column); + + /** + * @param columnName title of the column to reference + */ + public int getInt(String columnName); + + /** + * @brief Get a long value from the specified column + * @param column ID number of the column to reference + * @see TableRow#getFloat(int) + * @see TableRow#getString(int) + */ + + public long getLong(int column); + + /** + * @param columnName title of the column to reference + */ + public long getLong(String columnName); + + /** + * @webref tablerow:method + * @brief Get a float value from the specified column + * @param column ID number of the column to reference + * @see TableRow#getInt(int) + * @see TableRow#getString(int) + */ + public float getFloat(int column); + + /** + * @param columnName title of the column to reference + */ + public float getFloat(String columnName); + + /** + * @brief Get a double value from the specified column + * @param column ID number of the column to reference + * @see TableRow#getInt(int) + * @see TableRow#getString(int) + */ + public double getDouble(int column); + + /** + * @param columnName title of the column to reference + */ + public double getDouble(String columnName); + + /** + * @webref tablerow:method + * @brief Store a String value in the specified column + * @param column ID number of the target column + * @param value value to assign + * @see TableRow#setInt(int, int) + * @see TableRow#setFloat(int, float) + */ + public void setString(int column, String value); + /** + * @param columnName title of the target column + */ + public void setString(String columnName, String value); + + /** + * @webref tablerow:method + * @brief Store an integer value in the specified column + * @param column ID number of the target column + * @param value value to assign + * @see TableRow#setFloat(int, float) + * @see TableRow#setString(int, String) + */ + public void setInt(int column, int value); + + /** + * @param columnName title of the target column + */ + public void setInt(String columnName, int value); + + /** + * @brief Store a long value in the specified column + * @param column ID number of the target column + * @param value value to assign + * @see TableRow#setFloat(int, float) + * @see TableRow#setString(int, String) + */ + public void setLong(int column, long value); + + /** + * @param columnName title of the target column + */ + public void setLong(String columnName, long value); + + /** + * @webref tablerow:method + * @brief Store a float value in the specified column + * @param column ID number of the target column + * @param value value to assign + * @see TableRow#setInt(int, int) + * @see TableRow#setString(int, String) + */ + public void setFloat(int column, float value); + + /** + * @param columnName title of the target column + */ + public void setFloat(String columnName, float value); + + /** + * @brief Store a double value in the specified column + * @param column ID number of the target column + * @param value value to assign + * @see TableRow#setFloat(int, float) + * @see TableRow#setString(int, String) + */ + public void setDouble(int column, double value); + + /** + * @param columnName title of the target column + */ + public void setDouble(String columnName, double value); + + /** + * @webref tablerow:method + * @brief Get the column count. + * @return count of all columns + */ + public int getColumnCount(); + + /** + * @brief Get the column type. + * @param columnName title of the target column + * @return type of the column + */ + public int getColumnType(String columnName); + + /** + * @param column ID number of the target column + */ + public int getColumnType(int column); + + /** + * @brief Get the all column types + * @return list of all column types + */ + public int[] getColumnTypes(); + + /** + * @webref tablerow:method + * @brief Get the column title. + * @param column ID number of the target column + * @return title of the column + */ + public String getColumnTitle(int column); + + /** + * @brief Get the all column titles + * @return list of all column titles + */ + public String[] getColumnTitles(); + + public void write(PrintWriter writer); + public void print(); +} diff --git a/src/main/java/processing/data/XML.java b/src/main/java/processing/data/XML.java new file mode 100644 index 0000000..12b35cd --- /dev/null +++ b/src/main/java/processing/data/XML.java @@ -0,0 +1,1149 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012 The Processing Foundation + Copyright (c) 2009-12 Ben Fry and Casey Reas + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2. + + This library 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.data; + +import java.io.*; + +import javax.xml.parsers.*; + +import org.w3c.dom.*; +import org.xml.sax.*; + +import javax.xml.transform.*; +import javax.xml.transform.dom.*; +import javax.xml.transform.stream.*; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; + +import processing.core.PApplet; + + +/** + * This is the base class used for the Processing XML library, + * representing a single node of an XML tree. + * + * @webref data:composite + * @see PApplet#loadXML(String) + * @see PApplet#parseXML(String) + * @see PApplet#saveXML(XML, String) + */ +public class XML implements Serializable { + + /** The internal representation, a DOM node. */ + protected Node node; + +// /** Cached locally because it's used often. */ +// protected String name; + + /** The parent element. */ + protected XML parent; + + /** Child elements, once loaded. */ + protected XML[] children; + + /** + * @nowebref + */ + protected XML() { } + + +// /** +// * Begin parsing XML data passed in from a PApplet. This code +// * wraps exception handling, for more advanced exception handling, +// * use the constructor that takes a Reader or InputStream. +// * +// * @throws SAXException +// * @throws ParserConfigurationException +// * @throws IOException +// */ +// public XML(PApplet parent, String filename) throws IOException, ParserConfigurationException, SAXException { +// this(parent.createReader(filename)); +// } + + + /** + * Advanced users only; use loadXML() in PApplet. This is not a supported + * function and is subject to change. It is available simply for users that + * would like to handle the exceptions in a particular way. + * + * @nowebref + */ + public XML(File file) throws IOException, ParserConfigurationException, SAXException { + this(file, null); + } + + + /** + * Advanced users only; use loadXML() in PApplet. + * + * @nowebref + */ + public XML(File file, String options) throws IOException, ParserConfigurationException, SAXException { + this(PApplet.createReader(file), options); + } + + /** + * @nowebref + */ + public XML(InputStream input) throws IOException, ParserConfigurationException, SAXException { + this(input, null); + } + + + /** + * Unlike the loadXML() method in PApplet, this version works with files + * that are not in UTF-8 format. + * + * @nowebref + */ + public XML(InputStream input, String options) throws IOException, ParserConfigurationException, SAXException { + //this(PApplet.createReader(input), options); // won't handle non-UTF8 + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + + try { + // Prevent 503 errors from www.w3.org + factory.setAttribute("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + } catch (IllegalArgumentException e) { + // ignore this; Android doesn't like it + } + + factory.setExpandEntityReferences(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(new InputSource(input)); + node = document.getDocumentElement(); + } + + + /** + * Advanced users only; use loadXML() in PApplet. + * + * @nowebref + */ + public XML(Reader reader) throws IOException, ParserConfigurationException, SAXException { + this(reader, null); + } + + + /** + * Advanced users only; use loadXML() in PApplet. + * + * Added extra code to handle \u2028 (Unicode NLF), which is sometimes + * inserted by web browsers (Safari?) and not distinguishable from a "real" + * LF (or CRLF) in some text editors (i.e. TextEdit on OS X). Only doing + * this for XML (and not all Reader objects) because LFs are essential. + * https://github.com/processing/processing/issues/2100 + * + * @nowebref + */ + public XML(final Reader reader, String options) throws IOException, ParserConfigurationException, SAXException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + + // Prevent 503 errors from www.w3.org + try { + factory.setAttribute("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + } catch (IllegalArgumentException e) { + // ignore this; Android doesn't like it + } + + // without a validating DTD, this doesn't do anything since it doesn't know what is ignorable +// factory.setIgnoringElementContentWhitespace(true); + + factory.setExpandEntityReferences(false); +// factory.setExpandEntityReferences(true); + +// factory.setCoalescing(true); +// builderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + DocumentBuilder builder = factory.newDocumentBuilder(); +// builder.setEntityResolver() + +// SAXParserFactory spf = SAXParserFactory.newInstance(); +// spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); +// SAXParser p = spf.newSAXParser(); + + // builder = DocumentBuilderFactory.newDocumentBuilder(); + // builder = new SAXBuilder(); + // builder.setValidation(validating); + + Document document = builder.parse(new InputSource(new Reader() { + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + int count = reader.read(cbuf, off, len); + for (int i = 0; i < count; i++) { + if (cbuf[off+i] == '\u2028') { + cbuf[off+i] = '\n'; + } + } + return count; + } + + @Override + public void close() throws IOException { + reader.close(); + } + })); + node = document.getDocumentElement(); + } + + + /** + * @param name creates a node with this name + * + */ + public XML(String name) { + try { + // TODO is there a more efficient way of doing this? wow. + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.newDocument(); + node = document.createElement(name); + this.parent = null; + + } catch (ParserConfigurationException pce) { + throw new RuntimeException(pce); + } + } + + /** + * @nowebref + */ + protected XML(XML parent, Node node) { + this.node = node; + this.parent = parent; + + for (String attr : parent.listAttributes()) { + if (attr.startsWith("xmlns")) { + // Copy namespace attributes to the kids, otherwise this XML + // can no longer be printed (or manipulated in most ways). + // Only do this when it's an Element, otherwise it's trying to set + // attributes on text notes (interstitial content). + if (node instanceof Element) { + setString(attr, parent.getString(attr)); + } + } + } + } + + + /** + * @webref xml:method + * @brief Converts String content to an XML object + * @param data the content to be parsed as XML + * @return an XML object, or null + * @throws SAXException + * @throws ParserConfigurationException + * @throws IOException + * @nowebref + */ + static public XML parse(String data) throws IOException, ParserConfigurationException, SAXException { + return XML.parse(data, null); + } + + /** + * @nowebref + */ + static public XML parse(String data, String options) throws IOException, ParserConfigurationException, SAXException { + return new XML(new StringReader(data), null); + } + + +// protected boolean save(OutputStream output) { +// return write(PApplet.createWriter(output)); +// } + + + public boolean save(File file) { + return save(file, null); + } + + + public boolean save(File file, String options) { + PrintWriter writer = PApplet.createWriter(file); + boolean result = write(writer); + writer.flush(); + writer.close(); + return result; + } + + + // Sends this object and its kids to a Writer with an indent of 2 spaces, + // including the declaration at the top so that the output will be valid XML. + public boolean write(PrintWriter output) { + output.print(format(2)); + output.flush(); + return true; + } + + + /** + * Returns the parent element. This method returns null for the root + * element. + * + * @webref xml:method + * @brief Gets a copy of the element's parent + */ + public XML getParent() { + return this.parent; + } + + /** + * Internal function; not included in reference. + */ + protected Object getNative() { + return node; + } + + + /** + * Returns the full name (i.e. the name including an eventual namespace + * prefix) of the element. + * + * @webref xml:method + * @brief Gets the element's full name + * @return the name, or null if the element only contains #PCDATA. + */ + public String getName() { +// return name; + return node.getNodeName(); + } + + /** + * @webref xml:method + * @brief Sets the element's name + */ + public void setName(String newName) { + Document document = node.getOwnerDocument(); + node = document.renameNode(node, null, newName); +// name = node.getNodeName(); + } + + + /** + * Returns the name of the element (without namespace prefix). + * + * Internal function; not included in reference. + */ + public String getLocalName() { + return node.getLocalName(); + } + + + /** + * Honey, can you just check on the kids? Thanks. + * + * Internal function; not included in reference. + */ + protected void checkChildren() { + if (children == null) { + NodeList kids = node.getChildNodes(); + int childCount = kids.getLength(); + children = new XML[childCount]; + for (int i = 0; i < childCount; i++) { + children[i] = new XML(this, kids.item(i)); + } + } + } + + + /** + * Returns the number of children. + * + * @webref xml:method + * @brief Returns the element's number of children + * @return the count. + */ + public int getChildCount() { + checkChildren(); + return children.length; + } + + + /** + * Returns a boolean of whether or not there are children. + * + * @webref xml:method + * @brief Checks whether or not an element has any children + */ + public boolean hasChildren() { + checkChildren(); + return children.length > 0; + } + + + /** + * Put the names of all children into an array. Same as looping through + * each child and calling getName() on each XMLElement. + * + * @webref xml:method + * @brief Returns the names of all children as an array + */ + public String[] listChildren() { +// NodeList children = node.getChildNodes(); +// int childCount = children.getLength(); +// String[] outgoing = new String[childCount]; +// for (int i = 0; i < childCount; i++) { +// Node kid = children.item(i); +// if (kid.getNodeType() == Node.ELEMENT_NODE) { +// outgoing[i] = kid.getNodeName(); +// } // otherwise just leave him null +// } + checkChildren(); + String[] outgoing = new String[children.length]; + for (int i = 0; i < children.length; i++) { + outgoing[i] = children[i].getName(); + } + return outgoing; + } + + + /** + * Returns an array containing all the child elements. + * + * @webref xml:method + * @brief Returns an array containing all child elements + */ + public XML[] getChildren() { +// NodeList children = node.getChildNodes(); +// int childCount = children.getLength(); +// XMLElement[] kids = new XMLElement[childCount]; +// for (int i = 0; i < childCount; i++) { +// Node kid = children.item(i); +// kids[i] = new XMLElement(this, kid); +// } +// return kids; + checkChildren(); + return children; + } + + + /** + * Quick accessor for an element at a particular index. + * + * @webref xml:method + * @brief Returns the child element with the specified index value or path + */ + public XML getChild(int index) { + checkChildren(); + return children[index]; + } + + + /** + * Get a child by its name or path. + * + * @param name element name or path/to/element + * @return the first matching element or null if no match + */ + public XML getChild(String name) { + if (name.length() > 0 && name.charAt(0) == '/') { + throw new IllegalArgumentException("getChild() should not begin with a slash"); + } + if (name.indexOf('/') != -1) { + return getChildRecursive(PApplet.split(name, '/'), 0); + } + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + XML kid = getChild(i); + String kidName = kid.getName(); + if (kidName != null && kidName.equals(name)) { + return kid; + } + } + return null; + } + + + /** + * Internal helper function for getChild(String). + * + * @param items result of splitting the query on slashes + * @param offset where in the items[] array we're currently looking + * @return matching element or null if no match + * @author processing.org + */ + protected XML getChildRecursive(String[] items, int offset) { + // if it's a number, do an index instead + if (Character.isDigit(items[offset].charAt(0))) { + XML kid = getChild(Integer.parseInt(items[offset])); + if (offset == items.length-1) { + return kid; + } else { + return kid.getChildRecursive(items, offset+1); + } + } + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + XML kid = getChild(i); + String kidName = kid.getName(); + if (kidName != null && kidName.equals(items[offset])) { + if (offset == items.length-1) { + return kid; + } else { + return kid.getChildRecursive(items, offset+1); + } + } + } + return null; + } + + + /** + * Get any children that match this name or path. Similar to getChild(), + * but will grab multiple matches rather than only the first. + * + * @param name element name or path/to/element + * @return array of child elements that match + * @author processing.org + */ + public XML[] getChildren(String name) { + if (name.length() > 0 && name.charAt(0) == '/') { + throw new IllegalArgumentException("getChildren() should not begin with a slash"); + } + if (name.indexOf('/') != -1) { + return getChildrenRecursive(PApplet.split(name, '/'), 0); + } + // if it's a number, do an index instead + // (returns a single element array, since this will be a single match + if (Character.isDigit(name.charAt(0))) { + return new XML[] { getChild(Integer.parseInt(name)) }; + } + int childCount = getChildCount(); + XML[] matches = new XML[childCount]; + int matchCount = 0; + for (int i = 0; i < childCount; i++) { + XML kid = getChild(i); + String kidName = kid.getName(); + if (kidName != null && kidName.equals(name)) { + matches[matchCount++] = kid; + } + } + return (XML[]) PApplet.subset(matches, 0, matchCount); + } + + + protected XML[] getChildrenRecursive(String[] items, int offset) { + if (offset == items.length-1) { + return getChildren(items[offset]); + } + XML[] matches = getChildren(items[offset]); + XML[] outgoing = new XML[0]; + for (int i = 0; i < matches.length; i++) { + XML[] kidMatches = matches[i].getChildrenRecursive(items, offset+1); + outgoing = (XML[]) PApplet.concat(outgoing, kidMatches); + } + return outgoing; + } + + + /** + * @webref xml:method + * @brief Appends a new child to the element + */ + public XML addChild(String tag) { + Document document = node.getOwnerDocument(); + Node newChild = document.createElement(tag); + return appendChild(newChild); + } + + + public XML addChild(XML child) { + Document document = node.getOwnerDocument(); + Node newChild = document.importNode((Node) child.getNative(), true); + return appendChild(newChild); + } + + + /** Internal handler to add the node structure. */ + protected XML appendChild(Node newNode) { + node.appendChild(newNode); + XML newbie = new XML(this, newNode); + if (children != null) { + children = (XML[]) PApplet.concat(children, new XML[] { newbie }); + } + return newbie; + } + + + /** + * @webref xml:method + * @brief Removes the specified child + */ + public void removeChild(XML kid) { + node.removeChild(kid.node); + children = null; // TODO not efficient + } + + + public void trim() { + try { + XPathFactory xpathFactory = XPathFactory.newInstance(); + XPathExpression xpathExp = + xpathFactory.newXPath().compile("//text()[normalize-space(.) = '']"); + NodeList emptyTextNodes = (NodeList) + xpathExp.evaluate(node, XPathConstants.NODESET); + + // Remove each empty text node from document. + for (int i = 0; i < emptyTextNodes.getLength(); i++) { + Node emptyTextNode = emptyTextNodes.item(i); + emptyTextNode.getParentNode().removeChild(emptyTextNode); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + +// /** Remove whitespace nodes. */ +// public void trim() { +////// public static boolean isWhitespace(XML xml) { +////// if (xml.node.getNodeType() != Node.TEXT_NODE) +////// return false; +////// Matcher m = whitespace.matcher(xml.node.getNodeValue()); +////// return m.matches(); +////// } +//// trim(this); +//// } +// +// checkChildren(); +// int index = 0; +// for (int i = 0; i < children.length; i++) { +// if (i != index) { +// children[index] = children[i]; +// } +// Node childNode = (Node) children[i].getNative(); +// if (childNode.getNodeType() != Node.TEXT_NODE || +// children[i].getContent().trim().length() > 0) { +// children[i].trim(); +// index++; +// } +// } +// if (index != children.length) { +// children = (XML[]) PApplet.subset(children, 0, index); +// } +// +// // possibility, but would have to re-parse the object +//// helpdesk.objects.com.au/java/how-do-i-remove-whitespace-from-an-xml-document +//// TransformerFactory factory = TransformerFactory.newInstance(); +//// Transformer transformer = factory.newTransformer(new StreamSource("strip-space.xsl")); +//// DOMSource source = new DOMSource(document); +//// StreamResult result = new StreamResult(System.out); +//// transformer.transform(source, result); +// +//// +//// +//// +//// +//// +//// +//// +//// +//// +// } + + + /** + * Returns the number of attributes. + * + * @webref xml:method + * @brief Counts the specified element's number of attributes + */ + public int getAttributeCount() { + return node.getAttributes().getLength(); + } + + + /** + * Get a list of the names for all of the attributes for this node. + * + * @webref xml:method + * @brief Returns a list of names of all attributes as an array + */ + public String[] listAttributes() { + NamedNodeMap nnm = node.getAttributes(); + String[] outgoing = new String[nnm.getLength()]; + for (int i = 0; i < outgoing.length; i++) { + outgoing[i] = nnm.item(i).getNodeName(); + } + return outgoing; + } + + /** + * Returns whether an attribute exists. + * + * @webref xml:method + * @brief Checks whether or not an element has the specified attribute + */ + public boolean hasAttribute(String name) { + return (node.getAttributes().getNamedItem(name) != null); + } + + + /** + * Returns the value of an attribute. + * + * @param name the non-null name of the attribute. + * @return the value, or null if the attribute does not exist. + */ +// public String getAttribute(String name) { +// return this.getAttribute(name, null); +// } + + + /** + * Returns the value of an attribute. + * + * @param name the non-null full name of the attribute. + * @param defaultValue the default value of the attribute. + * @return the value, or defaultValue if the attribute does not exist. + */ +// public String getAttribute(String name, String defaultValue) { +// Node attr = node.getAttributes().getNamedItem(name); +// return (attr == null) ? defaultValue : attr.getNodeValue(); +// } + + + /** + * @webref xml:method + * @brief Gets the content of an attribute as a String + */ + public String getString(String name) { + return getString(name, null); + } + + + public String getString(String name, String defaultValue) { + NamedNodeMap attrs = node.getAttributes(); + if (attrs != null) { + Node attr = attrs.getNamedItem(name); + if (attr != null) { + return attr.getNodeValue(); + } + } + return defaultValue; + } + + + /** + * @webref xml:method + * @brief Sets the content of an attribute as a String + */ + public void setString(String name, String value) { + ((Element) node).setAttribute(name, value); + } + + + /** + * @webref xml:method + * @brief Gets the content of an attribute as an int + */ + public int getInt(String name) { + return getInt(name, 0); + } + + + /** + * @webref xml:method + * @brief Sets the content of an attribute as an int + */ + public void setInt(String name, int value) { + setString(name, String.valueOf(value)); + } + + + /** + * Returns the value of an attribute. + * + * @param name the non-null full name of the attribute + * @param defaultValue the default value of the attribute + * @return the value, or defaultValue if the attribute does not exist + */ + public int getInt(String name, int defaultValue) { + String value = getString(name); + return (value == null) ? defaultValue : Integer.parseInt(value); + } + + + /** + * @webref xml:method + * @brief Sets the content of an element as an int + */ + public void setLong(String name, long value) { + setString(name, String.valueOf(value)); + } + + + /** + * Returns the value of an attribute. + * + * @param name the non-null full name of the attribute. + * @param defaultValue the default value of the attribute. + * @return the value, or defaultValue if the attribute does not exist. + */ + public long getLong(String name, long defaultValue) { + String value = getString(name); + return (value == null) ? defaultValue : Long.parseLong(value); + } + + + /** + * Returns the value of an attribute, or zero if not present. + * + * @webref xml:method + * @brief Gets the content of an attribute as a float + */ + public float getFloat(String name) { + return getFloat(name, 0); + } + + + /** + * Returns the value of an attribute. + * + * @param name the non-null full name of the attribute. + * @param defaultValue the default value of the attribute. + * @return the value, or defaultValue if the attribute does not exist. + */ + public float getFloat(String name, float defaultValue) { + String value = getString(name); + return (value == null) ? defaultValue : Float.parseFloat(value); + } + + + /** + * @webref xml:method + * @brief Sets the content of an attribute as a float + */ + public void setFloat(String name, float value) { + setString(name, String.valueOf(value)); + } + + + public double getDouble(String name) { + return getDouble(name, 0); + } + + + /** + * Returns the value of an attribute. + * + * @param name the non-null full name of the attribute + * @param defaultValue the default value of the attribute + * @return the value, or defaultValue if the attribute does not exist + */ + public double getDouble(String name, double defaultValue) { + String value = getString(name); + return (value == null) ? defaultValue : Double.parseDouble(value); + } + + + public void setDouble(String name, double value) { + setString(name, String.valueOf(value)); + } + + + /** + * Return the #PCDATA content of the element. If the element has a + * combination of #PCDATA content and child elements, the #PCDATA + * sections can be retrieved as unnamed child objects. In this case, + * this method returns null. + * + * @webref xml:method + * @brief Gets the content of an element + * @return the content. + * @see XML#getIntContent() + * @see XML#getFloatContent() + */ + public String getContent() { + return node.getTextContent(); + } + + + public String getContent(String defaultValue) { + String s = node.getTextContent(); + return (s != null) ? s : defaultValue; + } + + + /** + * @webref xml:method + * @brief Gets the content of an element as an int + * @return the content. + * @see XML#getContent() + * @see XML#getFloatContent() + */ + public int getIntContent() { + return getIntContent(0); + } + + + /** + * @param defaultValue the default value of the attribute + */ + public int getIntContent(int defaultValue) { + return PApplet.parseInt(node.getTextContent(), defaultValue); + } + + + /** + * @webref xml:method + * @brief Gets the content of an element as a float + * @return the content. + * @see XML#getContent() + * @see XML#getIntContent() + */ + public float getFloatContent() { + return getFloatContent(0); + } + + + /** + * @param defaultValue the default value of the attribute + */ + public float getFloatContent(float defaultValue) { + return PApplet.parseFloat(node.getTextContent(), defaultValue); + } + + + public long getLongContent() { + return getLongContent(0); + } + + + public long getLongContent(long defaultValue) { + String c = node.getTextContent(); + if (c != null) { + try { + return Long.parseLong(c); + } catch (NumberFormatException nfe) { } + } + return defaultValue; + } + + + public double getDoubleContent() { + return getDoubleContent(0); + } + + + public double getDoubleContent(double defaultValue) { + String c = node.getTextContent(); + if (c != null) { + try { + return Double.parseDouble(c); + } catch (NumberFormatException nfe) { } + } + return defaultValue; + } + + + /** + * @webref xml:method + * @brief Sets the content of an element + */ + public void setContent(String text) { + node.setTextContent(text); + } + + + public void setIntContent(int value) { + setContent(String.valueOf(value)); + } + + + public void setFloatContent(float value) { + setContent(String.valueOf(value)); + } + + + public void setLongContent(long value) { + setContent(String.valueOf(value)); + } + + + public void setDoubleContent(double value) { + setContent(String.valueOf(value)); + } + + + /** + * Format this XML data as a String. + * + * @webref xml:method + * @brief Formats XML data as a String + * @param indent -1 for a single line (and no declaration), >= 0 for indents and newlines + * @return the content + * @see XML#toString() + */ + public String format(int indent) { + try { + // entities = doctype.getEntities() + boolean useIndentAmount = false; + TransformerFactory factory = TransformerFactory.newInstance(); + if (indent != -1) { + try { + factory.setAttribute("indent-number", indent); + } catch (IllegalArgumentException e) { + useIndentAmount = true; + } + } + Transformer transformer = factory.newTransformer(); + + // Add the XML declaration at the top if this node is the root and we're + // not writing to a single line (indent = -1 means single line). + if (indent == -1 || parent == null) { + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + } else { + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); + } + +// transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "sample.dtd"); + + transformer.setOutputProperty(OutputKeys.METHOD, "xml"); + +// transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "yes"); // huh? + +// transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, +// "-//W3C//DTD XHTML 1.0 Transitional//EN"); +// transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, +// "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"); + + // For Android, because (at least 2.3.3) doesn't like indent-number + if (useIndentAmount) { + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(indent)); + } + +// transformer.setOutputProperty(OutputKeys.ENCODING,"ISO-8859-1"); +// transformer.setOutputProperty(OutputKeys.ENCODING,"UTF8"); + transformer.setOutputProperty(OutputKeys.ENCODING,"UTF-8"); +// transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS + + // Always indent, otherwise the XML declaration will just be jammed + // onto the first line with the XML code as well. + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + +// Properties p = transformer.getOutputProperties(); +// for (Object key : p.keySet()) { +// System.out.println(key + " -> " + p.get(key)); +// } + + // If you smell something, that's because this code stinks. No matter + // the settings of the Transformer object, if the XML document already + // has whitespace elements, it won't bother re-indenting/re-formatting. + // So instead, transform the data once into a single line string. + // If indent is -1, then we're done. Otherwise re-run and the settings + // of the factory will kick in. If you know a better way to do this, + // please contribute. I've wasted too much of my Sunday on it. But at + // least the Giants are getting blown out by the Falcons. + + final String decl = ""; + final String sep = System.getProperty("line.separator"); + + StringWriter tempWriter = new StringWriter(); + StreamResult tempResult = new StreamResult(tempWriter); + transformer.transform(new DOMSource(node), tempResult); + String[] tempLines = PApplet.split(tempWriter.toString(), sep); +// PApplet.println(tempLines); + if (tempLines[0].startsWith("") + 2; + //if (tempLines[0].length() == decl.length()) { + if (tempLines[0].length() == declEnd) { + // If it's all the XML declaration, remove it +// PApplet.println("removing first line"); + tempLines = PApplet.subset(tempLines, 1); + } else { +// PApplet.println("removing part of first line"); + // If the first node has been moved to this line, be more careful + //tempLines[0] = tempLines[0].substring(decl.length()); + tempLines[0] = tempLines[0].substring(declEnd); + } + } + String singleLine = PApplet.join(PApplet.trim(tempLines), ""); + if (indent == -1) { + return singleLine; + } + + // Might just be whitespace, which won't be valid XML for parsing below. + // https://github.com/processing/processing/issues/1796 + // Since indent is not -1, that means they want valid XML, + // so we'll give them the single line plus the decl... Lame? sure. + if (singleLine.trim().length() == 0) { + // You want whitespace? I've got your whitespace right here. + return decl + sep + singleLine; + } + + // Since the indent is not -1, bring back the XML declaration + //transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); + + StringWriter stringWriter = new StringWriter(); + StreamResult xmlOutput = new StreamResult(stringWriter); +// DOMSource source = new DOMSource(node); + Source source = new StreamSource(new StringReader(singleLine)); + transformer.transform(source, xmlOutput); + String outgoing = stringWriter.toString(); + + // Add the XML declaration to the top if it's not there already + if (outgoing.startsWith(decl)) { + int declen = decl.length(); + int seplen = sep.length(); + if (outgoing.length() > declen + seplen && + !outgoing.substring(declen, declen + seplen).equals(sep)) { + // make sure there's a line break between the XML decl and the code + return outgoing.substring(0, decl.length()) + + sep + outgoing.substring(decl.length()); + } + return outgoing; + } else { + return decl + sep + outgoing; + } + + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + + public void print() { + PApplet.println(format(2)); + } + + + /** + * Return the XML document formatted with two spaces for indents. + * Chosen to do this since it's the most common case (e.g. with println()). + * Same as format(2). Use the format() function for more options. + * + * @webref xml:method + * @brief Gets XML data as a String using default formatting + * @return the content + * @see XML#format(int) + */ + @Override + public String toString() { + //return format(2); + return format(-1); + } +} diff --git a/src/main/java/processing/event/Event.java b/src/main/java/processing/event/Event.java new file mode 100644 index 0000000..32bf175 --- /dev/null +++ b/src/main/java/processing/event/Event.java @@ -0,0 +1,125 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012 The Processing Foundation + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.event; + + +public class Event { + protected Object nativeObject; + + protected long millis; + protected int action; + + // These correspond to the java.awt.Event modifiers (not to be confused with + // the newer getModifiersEx), though they're not guaranteed to in the future. + static public final int SHIFT = 1 << 0; + static public final int CTRL = 1 << 1; + static public final int META = 1 << 2; + static public final int ALT = 1 << 3; + protected int modifiers; + + // Types of events. As with all constants in Processing, brevity's preferred. + static public final int KEY = 1; + static public final int MOUSE = 2; + static public final int TOUCH = 3; + protected int flavor; + + + public Event(Object nativeObject, long millis, int action, int modifiers) { + this.nativeObject = nativeObject; + this.millis = millis; + this.action = action; + this.modifiers = modifiers; + } + + + public int getFlavor() { + return flavor; + } + + + /** + * Get the platform-native event object. This might be the java.awt event + * on the desktop, though if you're using OpenGL on the desktop it'll be a + * NEWT event that JOGL uses. Android events are something else altogether. + * Bottom line, use this only if you know what you're doing, and don't make + * assumptions about the class type. + */ + public Object getNative() { + return nativeObject; + } + + +// public void setNative(Object nativeObject) { +// this.nativeObject = nativeObject; +// } + + + public long getMillis() { + return millis; + } + + +// public void setMillis(long millis) { +// this.millis = millis; +// } + + + public int getAction() { + return action; + } + + +// public void setAction(int action) { +// this.action = action; +// } + + + public int getModifiers() { + return modifiers; + } + + +// public void setModifiers(int modifiers) { +// this.modifiers = modifiers; +// } + + + public boolean isShiftDown() { + return (modifiers & SHIFT) != 0; + } + + + public boolean isControlDown() { + return (modifiers & CTRL) != 0; + } + + + public boolean isMetaDown() { + return (modifiers & META) != 0; + } + + + public boolean isAltDown() { + return (modifiers & ALT) != 0; + } +} \ No newline at end of file diff --git a/src/main/java/processing/event/KeyEvent.java b/src/main/java/processing/event/KeyEvent.java new file mode 100644 index 0000000..5c4ccdd --- /dev/null +++ b/src/main/java/processing/event/KeyEvent.java @@ -0,0 +1,70 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012 The Processing Foundation + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.event; + + +public class KeyEvent extends Event { + static public final int PRESS = 1; + static public final int RELEASE = 2; + static public final int TYPE = 3; + + char key; + int keyCode; + + boolean isAutoRepeat; + + + public KeyEvent(Object nativeObject, + long millis, int action, int modifiers, + char key, int keyCode) { + super(nativeObject, millis, action, modifiers); + this.flavor = KEY; + this.key = key; + this.keyCode = keyCode; + } + + public KeyEvent(Object nativeObject, + long millis, int action, int modifiers, + char key, int keyCode, boolean isAutoRepeat) { + super(nativeObject, millis, action, modifiers); + this.flavor = KEY; + this.key = key; + this.keyCode = keyCode; + this.isAutoRepeat = isAutoRepeat; + } + + + public char getKey() { + return key; + } + + + public int getKeyCode() { + return keyCode; + } + + + public boolean isAutoRepeat() { + return isAutoRepeat; + } +} \ No newline at end of file diff --git a/src/main/java/processing/event/MouseEvent.java b/src/main/java/processing/event/MouseEvent.java new file mode 100644 index 0000000..bde328f --- /dev/null +++ b/src/main/java/processing/event/MouseEvent.java @@ -0,0 +1,149 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012 The Processing Foundation + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.event; + +//import processing.core.PConstants; + + +public class MouseEvent extends Event { + static public final int PRESS = 1; + static public final int RELEASE = 2; + static public final int CLICK = 3; + static public final int DRAG = 4; + static public final int MOVE = 5; + static public final int ENTER = 6; + static public final int EXIT = 7; + static public final int WHEEL = 8; + + protected int x, y; + protected int button; +// protected int clickCount; +// protected float amount; + protected int count; + + +// public MouseEvent(int x, int y) { +// this(null, +// System.currentTimeMillis(), PRESSED, 0, +// x, y, PConstants.LEFT, 1); +// } + + + public MouseEvent(Object nativeObject, + long millis, int action, int modifiers, + int x, int y, int button, int count) { //float amount) { //int clickCount) { + super(nativeObject, millis, action, modifiers); + this.flavor = MOUSE; + this.x = x; + this.y = y; + this.button = button; + //this.clickCount = clickCount; + //this.amount = amount; + this.count = count; + } + + + public int getX() { + return x; + } + + + public int getY() { + return y; + } + + + /** Which button was pressed, either LEFT, CENTER, or RIGHT. */ + public int getButton() { + return button; + } + + +// public void setButton(int button) { +// this.button = button; +// } + + + /** Do not use, getCount() is the correct method. */ + @Deprecated + public int getClickCount() { + //return (int) amount; //clickCount; + return count; + } + + + /** Do not use, getCount() is the correct method. */ + @Deprecated + public float getAmount() { + //return amount; + return count; + } + + + /** + * Number of clicks for mouse button events, or the number of steps (positive + * or negative depending on direction) for a mouse wheel event. + * Wheel events follow Java (see
        here), so + * getAmount() will return "negative values if the mouse wheel was rotated + * up or away from the user" and positive values in the other direction. + * On Mac OS X, this will be reversed when "natural" scrolling is enabled + * in System Preferences &rarr Mouse. + */ + public int getCount() { + return count; + } + + +// public void setClickCount(int clickCount) { +// this.clickCount = clickCount; +// } + + private String actionString() { + switch (action) { + default: + return "UNKNOWN"; + case CLICK: + return "CLICK"; + case DRAG: + return "DRAG"; + case ENTER: + return "ENTER"; + case EXIT: + return "EXIT"; + case MOVE: + return "MOVE"; + case PRESS: + return "PRESS"; + case RELEASE: + return "RELEASE"; + case WHEEL: + return "WHEEL"; + } + } + + @Override + public String toString() { + return String.format("", + actionString(), x, y, count, button); + } +} diff --git a/src/main/java/processing/event/TouchEvent.java b/src/main/java/processing/event/TouchEvent.java new file mode 100644 index 0000000..7c4b9d0 --- /dev/null +++ b/src/main/java/processing/event/TouchEvent.java @@ -0,0 +1,57 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012 The Processing Foundation + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.event; + + +// PLACEHOLDER CLASS: DO NOT USE. IT HAS NOT EVEN DECIDED WHETHER +// THIS WILL BE CALLED TOUCHEVENT ONCE IT'S FINISHED. + +/* +http://developer.android.com/guide/topics/ui/ui-events.html +http://developer.android.com/reference/android/view/MotionEvent.html +http://developer.apple.com/library/safari/#documentation/UserExperience/Reference/TouchEventClassReference/TouchEvent/TouchEvent.html +http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIGestureRecognizer_Class/Reference/Reference.html#//apple_ref/occ/cl/UIGestureRecognizer + +Apple's high-level gesture names: +tap +pinch +rotate +swipe +pan +longpress + +W3C touch events +http://www.w3.org/TR/touch-events/ +http://www.w3.org/TR/2011/WD-touch-events-20110913/ + +Pointer and gesture events (Windows) +http://msdn.microsoft.com/en-US/library/ie/hh673557.aspx + +*/ +public class TouchEvent extends Event { + + public TouchEvent(Object nativeObject, long millis, int action, int modifiers) { + super(nativeObject, millis, action, modifiers); + this.flavor = TOUCH; + } +} diff --git a/src/main/java/processing/javafx/PGraphicsFX2D.java b/src/main/java/processing/javafx/PGraphicsFX2D.java new file mode 100644 index 0000000..74758ce --- /dev/null +++ b/src/main/java/processing/javafx/PGraphicsFX2D.java @@ -0,0 +1,2330 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2015 The Processing Foundation + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.javafx; + +import com.sun.javafx.geom.Path2D; +import com.sun.javafx.geom.PathIterator; +import com.sun.javafx.geom.Shape; + +import java.nio.IntBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; + +import javafx.scene.SnapshotParameters; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.effect.BlendMode; +import javafx.scene.image.PixelFormat; +import javafx.scene.image.PixelReader; +import javafx.scene.image.PixelWriter; +import javafx.scene.image.WritableImage; +import javafx.scene.image.WritablePixelFormat; +import javafx.scene.paint.Color; +import javafx.scene.shape.ArcType; +import javafx.scene.shape.StrokeLineCap; +import javafx.scene.shape.StrokeLineJoin; +import javafx.scene.text.Font; +import javafx.scene.text.Text; +import javafx.scene.transform.Affine; +import javafx.scene.transform.Transform; + +import processing.core.*; + + +public class PGraphicsFX2D extends PGraphics { + GraphicsContext context; + + static final WritablePixelFormat argbFormat = + PixelFormat.getIntArgbInstance(); + + WritableImage snapshotImage; + + Path2D workPath = new Path2D(); + Path2D auxPath = new Path2D(); + boolean openContour; + boolean adjustedForThinLines; + /// break the shape at the next vertex (next vertex() call is a moveto()) + boolean breakShape; + + private float pathCoordsBuffer[] = new float[6]; + + /// coordinates for internal curve calculation + float[] curveCoordX; + float[] curveCoordY; + float[] curveDrawX; + float[] curveDrawY; + + int transformCount; + Affine transformStack[] = new Affine[MATRIX_STACK_DEPTH]; + +// Line2D.Float line = new Line2D.Float(); +// Ellipse2D.Float ellipse = new Ellipse2D.Float(); +// Rectangle2D.Float rect = new Rectangle2D.Float(); +// Arc2D.Float arc = new Arc2D.Float(); +// +// protected Color tintColorObject; +// +// protected Color fillColorObject; +// public boolean fillGradient; +// public Paint fillGradientObject; +// +// protected Color strokeColorObject; +// public boolean strokeGradient; +// public Paint strokeGradientObject; + + + + ////////////////////////////////////////////////////////////// + + // INTERNAL + + + public PGraphicsFX2D() { } + + + //public void setParent(PApplet parent) + + + //public void setPrimary(boolean primary) + + + //public void setPath(String path) + + + //public void setSize(int width, int height) + + + //public void dispose() + + + @Override + public PSurface createSurface() { + return surface = new PSurfaceFX(this); + } + + + /** Returns the javafx.scene.canvas.GraphicsContext used by this renderer. */ + @Override + public Object getNative() { + return context; + } + + + ////////////////////////////////////////////////////////////// + + // FRAME + + +// @Override +// public boolean canDraw() { +// return true; +// } + + + @Override + public void beginDraw() { + checkSettings(); + resetMatrix(); // reset model matrix + vertexCount = 0; + } + + + @Override + public void endDraw() { + flush(); + + if (!primaryGraphics) { + // TODO this is probably overkill for most tasks... + loadPixels(); + } + } + + + + ////////////////////////////////////////////////////////////// + + // SETTINGS + + + //protected void checkSettings() + + + //protected void defaultSettings() + + + //protected void reapplySettings() + + + + ////////////////////////////////////////////////////////////// + + // HINT + + + //public void hint(int which) + + + + ////////////////////////////////////////////////////////////// + + // SHAPE CREATION + + + //protected PShape createShapeFamily(int type) + + + //protected PShape createShapePrimitive(int kind, float... p) + + + + ////////////////////////////////////////////////////////////// + + // SHAPE + + + @Override + public void beginShape(int kind) { + shape = kind; + vertexCount = 0; + curveVertexCount = 0; + + workPath.reset(); + auxPath.reset(); + + flushPixels(); + + if (drawingThinLines()) { + adjustedForThinLines = true; + translate(0.5f, 0.5f); + } + } + + + //public boolean edge(boolean e) + + + //public void normal(float nx, float ny, float nz) { + + + //public void textureMode(int mode) + + + @Override + public void texture(PImage image) { + showMethodWarning("texture"); + } + + + @Override + public void vertex(float x, float y) { + if (vertexCount == vertices.length) { + float temp[][] = new float[vertexCount<<1][VERTEX_FIELD_COUNT]; + System.arraycopy(vertices, 0, temp, 0, vertexCount); + vertices = temp; + //message(CHATTER, "allocating more vertices " + vertices.length); + } + // not everyone needs this, but just easier to store rather + // than adding another moving part to the code... + vertices[vertexCount][X] = x; + vertices[vertexCount][Y] = y; + vertexCount++; + + switch (shape) { + + case POINTS: + point(x, y); + break; + + case LINES: + if ((vertexCount % 2) == 0) { + line(vertices[vertexCount-2][X], + vertices[vertexCount-2][Y], x, y); + } + break; + + case TRIANGLES: + if ((vertexCount % 3) == 0) { + triangle(vertices[vertexCount - 3][X], + vertices[vertexCount - 3][Y], + vertices[vertexCount - 2][X], + vertices[vertexCount - 2][Y], + x, y); + } + break; + + case TRIANGLE_STRIP: + if (vertexCount >= 3) { + triangle(vertices[vertexCount - 2][X], + vertices[vertexCount - 2][Y], + vertices[vertexCount - 1][X], + vertices[vertexCount - 1][Y], + vertices[vertexCount - 3][X], + vertices[vertexCount - 3][Y]); + } + break; + + case TRIANGLE_FAN: + if (vertexCount >= 3) { + // This is an unfortunate implementation because the stroke for an + // adjacent triangle will be repeated. However, if the stroke is not + // redrawn, it will replace the adjacent line (when it lines up + // perfectly) or show a faint line (when off by a small amount). + // The alternative would be to wait, then draw the shape as a + // polygon fill, followed by a series of vertices. But that's a + // poor method when used with PDF, DXF, or other recording objects, + // since discrete triangles would likely be preferred. + triangle(vertices[0][X], + vertices[0][Y], + vertices[vertexCount - 2][X], + vertices[vertexCount - 2][Y], + x, y); + } + break; + + case QUAD: + case QUADS: + if ((vertexCount % 4) == 0) { + quad(vertices[vertexCount - 4][X], + vertices[vertexCount - 4][Y], + vertices[vertexCount - 3][X], + vertices[vertexCount - 3][Y], + vertices[vertexCount - 2][X], + vertices[vertexCount - 2][Y], + x, y); + } + break; + + case QUAD_STRIP: + // 0---2---4 + // | | | + // 1---3---5 + if ((vertexCount >= 4) && ((vertexCount % 2) == 0)) { + quad(vertices[vertexCount - 4][X], + vertices[vertexCount - 4][Y], + vertices[vertexCount - 2][X], + vertices[vertexCount - 2][Y], + x, y, + vertices[vertexCount - 3][X], + vertices[vertexCount - 3][Y]); + } + break; + + case POLYGON: + if (workPath.getNumCommands() == 0 || breakShape) { + workPath.moveTo(x, y); + breakShape = false; + } else { + workPath.lineTo(x, y); + } + break; + } + } + + + @Override + public void vertex(float x, float y, float z) { + showDepthWarningXYZ("vertex"); + } + + + @Override + public void vertex(float[] v) { + vertex(v[X], v[Y]); + } + + + @Override + public void vertex(float x, float y, float u, float v) { + showVariationWarning("vertex(x, y, u, v)"); + } + + + @Override + public void vertex(float x, float y, float z, float u, float v) { + showDepthWarningXYZ("vertex"); + } + + + @Override + public void beginContour() { + if (openContour) { + PGraphics.showWarning("Already called beginContour()"); + return; + } + + // draw contours to auxiliary path so main path can be closed later + Path2D contourPath = auxPath; + auxPath = workPath; + workPath = contourPath; + + if (contourPath.getNumCommands() > 0) { // first contour does not break + breakShape = true; + } + + openContour = true; + } + + + @Override + public void endContour() { + if (!openContour) { + PGraphics.showWarning("Need to call beginContour() first"); + return; + } + + if (workPath.getNumCommands() > 0) workPath.closePath(); + + Path2D temp = workPath; + workPath = auxPath; + auxPath = temp; + + openContour = false; + } + + + @Override + public void endShape(int mode) { + if (openContour) { // correct automagically, notify user + endContour(); + PGraphics.showWarning("Missing endContour() before endShape()"); + } + if (workPath.getNumCommands() > 0) { + if (shape == POLYGON) { + if (mode == CLOSE) { + workPath.closePath(); + } + if (auxPath.getNumCommands() > 0) { + workPath.append(auxPath, false); + } + drawShape(workPath); + } + } + shape = 0; + if (adjustedForThinLines) { + adjustedForThinLines = false; + translate(-0.5f, -0.5f); + } + loaded = false; + } + + + private void drawShape(Shape s) { + context.beginPath(); + PathIterator pi = s.getPathIterator(null); + while (!pi.isDone()) { + int pitype = pi.currentSegment(pathCoordsBuffer); + switch (pitype) { + case PathIterator.SEG_MOVETO: + context.moveTo(pathCoordsBuffer[0], pathCoordsBuffer[1]); + break; + case PathIterator.SEG_LINETO: + context.lineTo(pathCoordsBuffer[0], pathCoordsBuffer[1]); + break; + case PathIterator.SEG_QUADTO: + context.quadraticCurveTo(pathCoordsBuffer[0], pathCoordsBuffer[1], + pathCoordsBuffer[2], pathCoordsBuffer[3]); + break; + case PathIterator.SEG_CUBICTO: + context.bezierCurveTo(pathCoordsBuffer[0], pathCoordsBuffer[1], + pathCoordsBuffer[2], pathCoordsBuffer[3], + pathCoordsBuffer[4], pathCoordsBuffer[5]); + break; + case PathIterator.SEG_CLOSE: + context.closePath(); + break; + default: + showWarning("Unknown segment type " + pitype); + } + pi.next(); + } + if (fill) context.fill(); + if (stroke) context.stroke(); + } + + + + ////////////////////////////////////////////////////////////// + + // CLIPPING + + + @Override + protected void clipImpl(float x1, float y1, float x2, float y2) { + //g2.setClip(new Rectangle2D.Float(x1, y1, x2 - x1, y2 - y1)); + showTodoWarning("clip()", 3274); + } + + + @Override + public void noClip() { + //g2.setClip(null); + showTodoWarning("noClip()", 3274); + } + + + + ////////////////////////////////////////////////////////////// + + // BLEND + + + @Override + protected void blendModeImpl() { + BlendMode mode = BlendMode.SRC_OVER; + switch (blendMode) { + case REPLACE: showWarning("blendMode(REPLACE) is not supported"); break; + case BLEND: break; // this is SRC_OVER, the default + case ADD: mode = BlendMode.ADD; break; // everyone's favorite + case SUBTRACT: showWarning("blendMode(SUBTRACT) is not supported"); break; + case LIGHTEST: mode = BlendMode.LIGHTEN; break; + case DARKEST: mode = BlendMode.DARKEN; break; + case DIFFERENCE: mode = BlendMode.DIFFERENCE; break; + case EXCLUSION: mode = BlendMode.EXCLUSION; break; + case MULTIPLY: mode = BlendMode.MULTIPLY; break; + case SCREEN: mode = BlendMode.SCREEN; break; + case OVERLAY: mode = BlendMode.OVERLAY; break; + case HARD_LIGHT: mode = BlendMode.HARD_LIGHT; break; + case SOFT_LIGHT: mode = BlendMode.SOFT_LIGHT; break; + case DODGE: mode = BlendMode.COLOR_DODGE; break; + case BURN: mode = BlendMode.COLOR_BURN; break; + } + context.setGlobalBlendMode(mode); + } + + + + ////////////////////////////////////////////////////////////// + + // BEZIER VERTICES + + + @Override + protected void bezierVertexCheck() { + if (shape == 0 || shape != POLYGON) { + throw new RuntimeException("beginShape() or beginShape(POLYGON) " + + "must be used before bezierVertex() or quadraticVertex()"); + } + if (workPath.getNumCommands() == 0) { + throw new RuntimeException("vertex() must be used at least once " + + "before bezierVertex() or quadraticVertex()"); + } + } + + @Override + public void bezierVertex(float x1, float y1, + float x2, float y2, + float x3, float y3) { + bezierVertexCheck(); + workPath.curveTo(x1, y1, x2, y2, x3, y3); + } + + + @Override + public void bezierVertex(float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + showDepthWarningXYZ("bezierVertex"); + } + + + + ////////////////////////////////////////////////////////////// + + // QUADRATIC BEZIER VERTICES + + + @Override + public void quadraticVertex(float ctrlX, float ctrlY, + float endX, float endY) { + bezierVertexCheck(); + workPath.quadTo(ctrlX, ctrlY, endX, endY); + } + + + @Override + public void quadraticVertex(float x2, float y2, float z2, + float x4, float y4, float z4) { + showDepthWarningXYZ("quadVertex"); + } + + + + ////////////////////////////////////////////////////////////// + + // CURVE VERTICES + + + @Override + protected void curveVertexSegment(float x1, float y1, + float x2, float y2, + float x3, float y3, + float x4, float y4) { + if (curveCoordX == null) { + curveCoordX = new float[4]; + curveCoordY = new float[4]; + curveDrawX = new float[4]; + curveDrawY = new float[4]; + } + + curveCoordX[0] = x1; + curveCoordY[0] = y1; + + curveCoordX[1] = x2; + curveCoordY[1] = y2; + + curveCoordX[2] = x3; + curveCoordY[2] = y3; + + curveCoordX[3] = x4; + curveCoordY[3] = y4; + + curveToBezierMatrix.mult(curveCoordX, curveDrawX); + curveToBezierMatrix.mult(curveCoordY, curveDrawY); + + // since the paths are continuous, + // only the first point needs the actual moveto + if (workPath.getNumCommands() == 0) { + workPath.moveTo(curveDrawX[0], curveDrawY[0]); + breakShape = false; + } + + workPath.curveTo(curveDrawX[1], curveDrawY[1], + curveDrawX[2], curveDrawY[2], + curveDrawX[3], curveDrawY[3]); + } + + + @Override + public void curveVertex(float x, float y, float z) { + showDepthWarningXYZ("curveVertex"); + } + + + + ////////////////////////////////////////////////////////////// + + // RENDERER + + @Override + public void flush() { + flushPixels(); + } + + + protected void flushPixels() { + boolean hasPixels = modified && pixels != null; + if (hasPixels) { + // If the user has been manipulating individual pixels, + // the changes need to be copied to the screen before + // drawing any new geometry. + int mx1 = getModifiedX1(); + int mx2 = getModifiedX2(); + int my1 = getModifiedY1(); + int my2 = getModifiedY2(); + int mw = mx2 - mx1; + int mh = my2 - my1; + + if (pixelDensity == 1) { + PixelWriter pw = context.getPixelWriter(); + pw.setPixels(mx1, my1, mw, mh, argbFormat, pixels, + mx1 + my1 * pixelWidth, pixelWidth); + } else { + // The only way to push all the pixels is to draw a scaled-down image + if (snapshotImage == null || + snapshotImage.getWidth() != pixelWidth || + snapshotImage.getHeight() != pixelHeight) { + snapshotImage = new WritableImage(pixelWidth, pixelHeight); + } + + PixelWriter pw = snapshotImage.getPixelWriter(); + pw.setPixels(mx1, my1, mw, mh, argbFormat, pixels, + mx1 + my1 * pixelWidth, pixelWidth); + context.save(); + resetMatrix(); + context.scale(1d / pixelDensity, 1d / pixelDensity); + context.drawImage(snapshotImage, mx1, my1, mw, mh, mx1, my1, mw, mh); + context.restore(); + } + } + + modified = false; + } + + + protected void beforeContextDraw() { + flushPixels(); + loaded = false; + } + + + ////////////////////////////////////////////////////////////// + + // POINT, LINE, TRIANGLE, QUAD + + + @Override + public void point(float x, float y) { + if (stroke) { +// if (strokeWeight > 1) { + line(x, y, x + EPSILON, y + EPSILON); +// } else { +// set((int) screenX(x, y), (int) screenY(x, y), strokeColor); +// } + } + } + + + @Override + public void line(float x1, float y1, float x2, float y2) { + beforeContextDraw(); + if (drawingThinLines()) { + x1 += 0.5f; + x2 += 0.5f; + y1 += 0.5f; + y2 += 0.5f; + } + context.strokeLine(x1, y1, x2, y2); + } + + + @Override + public void triangle(float x1, float y1, float x2, float y2, + float x3, float y3) { + beforeContextDraw(); + if (drawingThinLines()) { + x1 += 0.5f; + x2 += 0.5f; + x3 += 0.5f; + y1 += 0.5f; + y2 += 0.5f; + y3 += 0.5f; + } + context.beginPath(); + context.moveTo(x1, y1); + context.lineTo(x2, y2); + context.lineTo(x3, y3); + context.closePath(); + if (fill) context.fill(); + if (stroke) context.stroke(); + } + + + @Override + public void quad(float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4) { + beforeContextDraw(); + if (drawingThinLines()) { + x1 += 0.5f; + x2 += 0.5f; + x3 += 0.5f; + x4 += 0.5f; + y1 += 0.5f; + y2 += 0.5f; + y3 += 0.5f; + y4 += 0.5f; + } + context.beginPath(); + context.moveTo(x1, y1); + context.lineTo(x2, y2); + context.lineTo(x3, y3); + context.lineTo(x4, y4); + context.closePath(); + if (fill) context.fill(); + if (stroke) context.stroke(); + } + + + + ////////////////////////////////////////////////////////////// + + // RECT + + + //public void rectMode(int mode) + + + //public void rect(float a, float b, float c, float d) + + + @Override + protected void rectImpl(float x1, float y1, float x2, float y2) { + beforeContextDraw(); + if (drawingThinLines()) { + x1 += 0.5f; + x2 += 0.5f; + y1 += 0.5f; + y2 += 0.5f; + } + if (fill) context.fillRect(x1, y1, x2 - x1, y2 - y1); + if (stroke) context.strokeRect(x1, y1, x2 - x1, y2 - y1); + } + + + + ////////////////////////////////////////////////////////////// + + // ELLIPSE + + + //public void ellipseMode(int mode) + + + //public void ellipse(float a, float b, float c, float d) + + + @Override + protected void ellipseImpl(float x, float y, float w, float h) { + beforeContextDraw(); + if (drawingThinLines()) { + x += 0.5f; + y += 0.5f; + } + if (fill) context.fillOval(x, y, w, h); + if (stroke) context.strokeOval(x, y, w, h); + } + + + + ////////////////////////////////////////////////////////////// + + // ARC + + + //public void arc(float a, float b, float c, float d, + // float start, float stop) + + + @Override + protected void arcImpl(float x, float y, float w, float h, + float start, float stop, int mode) { + beforeContextDraw(); + + if (drawingThinLines()) { + x += 0.5f; + y += 0.5f; + } + + // 0 to 90 in java would be 0 to -90 for p5 renderer + // but that won't work, so -90 to 0? + start = -start; + stop = -stop; + + float sweep = stop - start; + + // The defaults, before 2.0b7, were to stroke as Arc2D.OPEN, and then fill + // using Arc2D.PIE. That's a little wonky, but it's here for compatability. + ArcType fillMode = ArcType.ROUND; // Arc2D.PIE + ArcType strokeMode = ArcType.OPEN; + + if (mode == OPEN) { + fillMode = ArcType.OPEN; + + } else if (mode == PIE) { + strokeMode = ArcType.ROUND; // PIE + + } else if (mode == CHORD) { + fillMode = ArcType.CHORD; + strokeMode = ArcType.CHORD; + } + + if (fill) { + context.fillArc(x, y, w, h, PApplet.degrees(start), PApplet.degrees(sweep), fillMode); + } + if (stroke) { + context.strokeArc(x, y, w, h, PApplet.degrees(start), PApplet.degrees(sweep), strokeMode); + } + } + + + + ////////////////////////////////////////////////////////////// + + // BOX + + + //public void box(float size) + + + @Override + public void box(float w, float h, float d) { + showMethodWarning("box"); + } + + + + ////////////////////////////////////////////////////////////// + + // SPHERE + + + //public void sphereDetail(int res) + + + //public void sphereDetail(int ures, int vres) + + + @Override + public void sphere(float r) { + showMethodWarning("sphere"); + } + + + + ////////////////////////////////////////////////////////////// + + // BEZIER + + + //public float bezierPoint(float a, float b, float c, float d, float t) + + + //public float bezierTangent(float a, float b, float c, float d, float t) + + + //protected void bezierInitCheck() + + + //protected void bezierInit() + + + /** Ignored (not needed) by this renderer. */ + @Override + public void bezierDetail(int detail) { } + + + //public void bezier(float x1, float y1, + // float x2, float y2, + // float x3, float y3, + // float x4, float y4) + + + //public void bezier(float x1, float y1, float z1, + // float x2, float y2, float z2, + // float x3, float y3, float z3, + // float x4, float y4, float z4) + + + + ////////////////////////////////////////////////////////////// + + // CURVE + + + //public float curvePoint(float a, float b, float c, float d, float t) + + + //public float curveTangent(float a, float b, float c, float d, float t) + + + /** Ignored (not needed) by this renderer. */ + @Override + public void curveDetail(int detail) { } + + + //public void curveTightness(float tightness) + + + //protected void curveInitCheck() + + + //protected void curveInit() + + + //public void curve(float x1, float y1, + // float x2, float y2, + // float x3, float y3, + // float x4, float y4) + + + //public void curve(float x1, float y1, float z1, + // float x2, float y2, float z2, + // float x3, float y3, float z3, + // float x4, float y4, float z4) + + + + ////////////////////////////////////////////////////////////// + + // SMOOTH + + +// @Override +// public void smooth() { +// smooth = true; +// +// if (quality == 0) { +// quality = 4; // change back to bicubic +// } +// } + + +// @Override +// public void smooth(int quality) { +//// this.quality = quality; +//// if (quality == 0) { +//// noSmooth(); +//// } else { +//// smooth(); +//// } +// showMissingWarning("smooth"); +// } +// +// +// @Override +// public void noSmooth() { +// showMissingWarning("noSmooth"); +// } + + + + ////////////////////////////////////////////////////////////// + + // IMAGE + + + //public void imageMode(int mode) + + + //public void image(PImage image, float x, float y) + + + //public void image(PImage image, float x, float y, float c, float d) + + + //public void image(PImage image, + // float a, float b, float c, float d, + // int u1, int v1, int u2, int v2) + + + /** + * Handle renderer-specific image drawing. + */ + @Override + protected void imageImpl(PImage who, + float x1, float y1, float x2, float y2, + int u1, int v1, int u2, int v2) { + // Image not ready yet, or an error + if (who.width <= 0 || who.height <= 0) return; + + ImageCache cash = (ImageCache) getCache(who); + + // Nuke the cache if the image was resized + if (cash != null) { + if (who.pixelWidth != cash.image.getWidth() || + who.pixelHeight != cash.image.getHeight()) { + cash = null; + } + } + + if (cash == null) { + //System.out.println("making new image cache"); + cash = new ImageCache(); //who); + setCache(who, cash); + who.updatePixels(); // mark the whole thing for update + who.setModified(); + } + + // If image previously was tinted, or the color changed + // or the image was tinted, and tint is now disabled + if ((tint && !cash.tinted) || + (tint && (cash.tintedColor != tintColor)) || + (!tint && cash.tinted)) { + // For tint change, mark all pixels as needing update. + who.updatePixels(); + } + + if (who.isModified()) { + if (who.pixels == null) { + // This might be a PGraphics that hasn't been drawn to yet. + // Can't just bail because the cache has been created above. + // https://github.com/processing/processing/issues/2208 + who.pixels = new int[who.pixelWidth * who.pixelHeight]; + } + cash.update(who, tint, tintColor); + who.setModified(false); + } + + u1 *= who.pixelDensity; + v1 *= who.pixelDensity; + u2 *= who.pixelDensity; + v2 *= who.pixelDensity; + + context.drawImage(((ImageCache) getCache(who)).image, + u1, v1, u2-u1, v2-v1, + x1, y1, x2-x1, y2-y1); + } + + + static class ImageCache { + boolean tinted; + int tintedColor; + int[] tintedTemp; // one row of tinted pixels + //BufferedImage image; + WritableImage image; + + /** + * Update the pixels of the cache image. Already determined that the tint + * has changed, or the pixels have changed, so should just go through + * with the update without further checks. + */ + public void update(PImage source, boolean tint, int tintColor) { + //int bufferType = BufferedImage.TYPE_INT_ARGB; + int targetType = ARGB; + boolean opaque = (tintColor & 0xFF000000) == 0xFF000000; + if (source.format == RGB) { + if (!tint || (tint && opaque)) { + //bufferType = BufferedImage.TYPE_INT_RGB; + targetType = RGB; + } + } +// boolean wrongType = (image != null) && (image.getType() != bufferType); +// if ((image == null) || wrongType) { +// image = new BufferedImage(source.width, source.height, bufferType); +// } + // Must always use an ARGB image, otherwise will write zeros + // in the alpha channel when drawn to the screen. + // https://github.com/processing/processing/issues/2030 +// if (image == null) { +// image = new BufferedImage(source.width, source.height, +// BufferedImage.TYPE_INT_ARGB); +// } + if (image == null) { + image = new WritableImage(source.pixelWidth, source.pixelHeight); + } + + //WritableRaster wr = image.getRaster(); + PixelWriter pw = image.getPixelWriter(); + if (tint) { + if (tintedTemp == null || tintedTemp.length != source.pixelWidth) { + tintedTemp = new int[source.pixelWidth]; + } + int a2 = (tintColor >> 24) & 0xff; +// System.out.println("tint color is " + a2); +// System.out.println("source.pixels[0] alpha is " + (source.pixels[0] >>> 24)); + int r2 = (tintColor >> 16) & 0xff; + int g2 = (tintColor >> 8) & 0xff; + int b2 = (tintColor) & 0xff; + + //if (bufferType == BufferedImage.TYPE_INT_RGB) { + if (targetType == RGB) { + // The target image is opaque, meaning that the source image has no + // alpha (is not ARGB), and the tint has no alpha. + int index = 0; + for (int y = 0; y < source.pixelHeight; y++) { + for (int x = 0; x < source.pixelWidth; x++) { + int argb1 = source.pixels[index++]; + int r1 = (argb1 >> 16) & 0xff; + int g1 = (argb1 >> 8) & 0xff; + int b1 = (argb1) & 0xff; + + // Prior to 2.1, the alpha channel was commented out here, + // but can't remember why (just thought unnecessary b/c of RGB?) + // https://github.com/processing/processing/issues/2030 + tintedTemp[x] = 0xFF000000 | + (((r2 * r1) & 0xff00) << 8) | + ((g2 * g1) & 0xff00) | + (((b2 * b1) & 0xff00) >> 8); + } + //wr.setDataElements(0, y, source.width, 1, tintedTemp); + pw.setPixels(0, y, source.pixelWidth, 1, argbFormat, tintedTemp, 0, source.pixelWidth); + } + // could this be any slower? +// float[] scales = { tintR, tintG, tintB }; +// float[] offsets = new float[3]; +// RescaleOp op = new RescaleOp(scales, offsets, null); +// op.filter(image, image); + + //} else if (bufferType == BufferedImage.TYPE_INT_ARGB) { + } else if (targetType == ARGB) { + if (source.format == RGB && + (tintColor & 0xffffff) == 0xffffff) { + int hi = tintColor & 0xff000000; + int index = 0; + for (int y = 0; y < source.pixelHeight; y++) { + for (int x = 0; x < source.pixelWidth; x++) { + tintedTemp[x] = hi | (source.pixels[index++] & 0xFFFFFF); + } + //wr.setDataElements(0, y, source.width, 1, tintedTemp); + pw.setPixels(0, y, source.pixelWidth, 1, argbFormat, tintedTemp, 0, source.pixelWidth); + } + } else { + int index = 0; + for (int y = 0; y < source.pixelHeight; y++) { + if (source.format == RGB) { + int alpha = tintColor & 0xFF000000; + for (int x = 0; x < source.pixelWidth; x++) { + int argb1 = source.pixels[index++]; + int r1 = (argb1 >> 16) & 0xff; + int g1 = (argb1 >> 8) & 0xff; + int b1 = (argb1) & 0xff; + tintedTemp[x] = alpha | + (((r2 * r1) & 0xff00) << 8) | + ((g2 * g1) & 0xff00) | + (((b2 * b1) & 0xff00) >> 8); + } + } else if (source.format == ARGB) { + for (int x = 0; x < source.pixelWidth; x++) { + int argb1 = source.pixels[index++]; + int a1 = (argb1 >> 24) & 0xff; + int r1 = (argb1 >> 16) & 0xff; + int g1 = (argb1 >> 8) & 0xff; + int b1 = (argb1) & 0xff; + tintedTemp[x] = + (((a2 * a1) & 0xff00) << 16) | + (((r2 * r1) & 0xff00) << 8) | + ((g2 * g1) & 0xff00) | + (((b2 * b1) & 0xff00) >> 8); + } + } else if (source.format == ALPHA) { + int lower = tintColor & 0xFFFFFF; + for (int x = 0; x < source.pixelWidth; x++) { + int a1 = source.pixels[index++]; + tintedTemp[x] = + (((a2 * a1) & 0xff00) << 16) | lower; + } + } + //wr.setDataElements(0, y, source.width, 1, tintedTemp); + pw.setPixels(0, y, source.pixelWidth, 1, argbFormat, tintedTemp, 0, source.pixelWidth); + } + } + // Not sure why ARGB images take the scales in this order... +// float[] scales = { tintR, tintG, tintB, tintA }; +// float[] offsets = new float[4]; +// RescaleOp op = new RescaleOp(scales, offsets, null); +// op.filter(image, image); + } + } else { // !tint + if (targetType == RGB && (source.pixels[0] >> 24 == 0)) { + // If it's an RGB image and the high bits aren't set, need to set + // the high bits to opaque because we're drawing ARGB images. + source.filter(OPAQUE); + // Opting to just manipulate the image here, since it shouldn't + // affect anything else (and alpha(get(x, y)) should return 0xff). + // Wel also make no guarantees about the values of the pixels array + // in a PImage and how the high bits will be set. + } + // If no tint, just shove the pixels on in there verbatim + //wr.setDataElements(0, 0, source.width, source.height, source.pixels); + //System.out.println("moving the big one"); + pw.setPixels(0, 0, source.pixelWidth, source.pixelHeight, + argbFormat, source.pixels, 0, source.pixelWidth); + } + this.tinted = tint; + this.tintedColor = tintColor; + +// GraphicsConfiguration gc = parent.getGraphicsConfiguration(); +// compat = gc.createCompatibleImage(image.getWidth(), +// image.getHeight(), +// Transparency.TRANSLUCENT); +// +// Graphics2D g = compat.createGraphics(); +// g.drawImage(image, 0, 0, null); +// g.dispose(); + } + } + + + + ////////////////////////////////////////////////////////////// + + // SHAPE + + + //public void shapeMode(int mode) + + + //public void shape(PShape shape) + + + //public void shape(PShape shape, float x, float y) + + + //public void shape(PShape shape, float x, float y, float c, float d) + + + ////////////////////////////////////////////////////////////// + + // SHAPE I/O + + + @Override + public PShape loadShape(String filename) { + return loadShape(filename, null); + } + + + @Override + public PShape loadShape(String filename, String options) { + String extension = PApplet.getExtension(filename); + if (extension.equals("svg") || extension.equals("svgz")) { + return new PShapeSVG(parent.loadXML(filename)); + } + PGraphics.showWarning("Unsupported format: " + filename); + return null; + } + + + + ////////////////////////////////////////////////////////////// + + // TEXT ATTRIBTUES + + + protected FontCache fontCache = new FontCache(); + + // Is initialized when defaultFontOrDeath() is called + // and mirrors PGraphics.textFont field + protected FontInfo textFontInfo; + + + @Override + protected PFont createFont(String name, float size, + boolean smooth, char[] charset) { + PFont font = super.createFont(name, size, smooth, charset); + if (font.isStream()) { + fontCache.nameToFilename.put(font.getName(), name); + } + return font; + } + + + @Override + protected void defaultFontOrDeath(String method, float size) { + super.defaultFontOrDeath(method, size); + handleTextFont(textFont, size); + } + + + @Override + protected boolean textModeCheck(int mode) { + return mode == MODEL; + } + + + @Override + public float textAscent() { + if (textFont == null) { + defaultFontOrDeath("textAscent"); + } + if (textFontInfo.font == null) { + return super.textAscent(); + } + return textFontInfo.ascent; + } + + + @Override + public float textDescent() { + if (textFont == null) { + defaultFontOrDeath("textDescent"); + } + if (textFontInfo.font == null) { + return super.textDescent(); + } + return textFontInfo.descent; + } + + + static final class FontInfo { + // TODO: this should be based on memory consumption + // this should be enough e.g. for all grays and alpha combos + static final int MAX_CACHED_COLORS_PER_FONT = 1 << 16; + + // used only when there is native font + Font font; + float ascent; + float descent; + + // used only when there is no native font + // maps 32-bit color to the arrays of tinted glyph images + Map tintCache; + } + + + static final class FontCache { + static final int MAX_CACHE_SIZE = 512; + + // keeps track of filenames of fonts loaded from ttf and otf files + Map nameToFilename = new HashMap<>(); + + // keeps track of fonts which should be rendered as pictures + // so we don't go through native font search process every time + final HashSet nonNativeNames = new HashSet<>(); + + // keeps all created fonts for reuse up to MAX_CACHE_SIZE limit + // when the limit is reached, the least recently used font is removed + // TODO: this should be based on memory consumtion + final LinkedHashMap cache = + new LinkedHashMap(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_CACHE_SIZE; + } + }; + + // key for retrieving fonts from cache; don't use for insertion, + // every font has to have its own new Key instance + final Key retrievingKey = new Key(); + + // text node used for measuring sizes of text + final Text measuringText = new Text(); + + FontInfo get(String name, float size) { + if (nonNativeNames.contains(name)) { + // Don't have native font, using glyph images. + // Size is set to zero, because all sizes of this font + // should share one FontInfo with one tintCache. + size = 0; + } + retrievingKey.name = name; + retrievingKey.size = size; + return cache.get(retrievingKey); + } + + void put(String name, float size, FontInfo fontInfo) { + if (fontInfo.font == null) { + // Don't have native font, using glyph images. + // Size is set to zero, because all sizes of this font + // should share one FontInfo with one tintCache. + nonNativeNames.add(name); + size = 0; + } + Key key = new Key(); + key.name = name; + key.size = size; + cache.put(key, fontInfo); + } + + FontInfo createFontInfo(Font font) { + FontInfo result = new FontInfo(); + result.font = font; + if (font != null) { + // measure ascent and descent + measuringText.setFont(result.font); + measuringText.setText(" "); + float lineHeight = (float) measuringText.getLayoutBounds().getHeight(); + result.ascent = (float) measuringText.getBaselineOffset(); + result.descent = lineHeight - result.ascent; + } + return result; + } + + static final class Key { + String name; + float size; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Key that = (Key) o; + if (Float.compare(that.size, size) != 0) return false; + return name.equals(that.name); + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + (size != +0.0f ? Float.floatToIntBits(size) : 0); + return result; + } + } + } + + + /////////////////////////////////////////////////////////////// + + // TEXT + + // None of the variations of text() are overridden from PGraphics. + + + + ////////////////////////////////////////////////////////////// + + // TEXT IMPL + + + @Override + protected void textFontImpl(PFont which, float size) { + handleTextFont(which, size); + handleTextSize(size); + } + + + @Override + protected void textSizeImpl(float size) { + handleTextFont(textFont, size); + handleTextSize(size); + } + + + /** + * FX specific. When setting font or size, new font has to + * be created. Both textFontImpl and textSizeImpl call this one. + * @param which font to be set, not null + * @param size size to be set, greater than zero + */ + protected void handleTextFont(PFont which, float size) { + textFont = which; + + String fontName = which.getName(); + String fontPsName = which.getPostScriptName(); + + textFontInfo = fontCache.get(fontName, size); + if (textFontInfo == null) { + Font font = null; + + if (which.isStream()) { + // Load from ttf or otf file + String filename = fontCache.nameToFilename.get(fontName); + font = Font.loadFont(parent.createInput(filename), size); + } + + if (font == null) { + // Look up font name + font = new Font(fontName, size); + if (!fontName.equalsIgnoreCase(font.getName())) { + // Look up font postscript name + font = new Font(fontPsName, size); + if (!fontPsName.equalsIgnoreCase(font.getName())) { + font = null; // Done with it + } + } + } + + if (font == null && which.getNative() != null) { + // Ain't got nothing, but AWT has something, so glyph images are not + // going to be used for this font; go with the default font then + font = new Font(size); + } + + textFontInfo = fontCache.createFontInfo(font); + fontCache.put(fontName, size, textFontInfo); + } + + context.setFont(textFontInfo.font); + } + + + @Override + protected void textLineImpl(char[] buffer, int start, int stop, float x, float y) { + if (textFontInfo.font == null) { + super.textLineImpl(buffer, start, stop, x, y); + } else { + context.fillText(new String(buffer, start, stop - start), x, y); + } + } + + + protected PImage getTintedGlyphImage(PFont.Glyph glyph, int tintColor) { + if (textFontInfo.tintCache == null) { + textFontInfo.tintCache = new LinkedHashMap(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > FontInfo.MAX_CACHED_COLORS_PER_FONT; + } + }; + } + PImage[] tintedGlyphs = textFontInfo.tintCache.get(tintColor); + int index = glyph.index; + if (tintedGlyphs == null || tintedGlyphs.length <= index) { + PImage[] newArray = new PImage[textFont.getGlyphCount()]; + if (tintedGlyphs != null) { + System.arraycopy(tintedGlyphs, 0, newArray, 0, tintedGlyphs.length); + } + tintedGlyphs = newArray; + textFontInfo.tintCache.put(tintColor, tintedGlyphs); + } + PImage tintedGlyph = tintedGlyphs[index]; + if (tintedGlyph == null) { + tintedGlyph = glyph.image.copy(); + tintedGlyphs[index] = tintedGlyph; + } + return tintedGlyph; + } + + + @Override + protected void textCharImpl(char ch, float x, float y) { //, float z) { + PFont.Glyph glyph = textFont.getGlyph(ch); + if (glyph != null) { + if (textMode == MODEL) { + float high = glyph.height / (float) textFont.getSize(); + float bwidth = glyph.width / (float) textFont.getSize(); + float lextent = glyph.leftExtent / (float) textFont.getSize(); + float textent = glyph.topExtent / (float) textFont.getSize(); + + float x1 = x + lextent * textSize; + float y1 = y - textent * textSize; + float x2 = x1 + bwidth * textSize; + float y2 = y1 + high * textSize; + + PImage glyphImage = (fillColor == 0xFFFFFFFF) ? + glyph.image : getTintedGlyphImage(glyph, fillColor); + + textCharModelImpl(glyphImage, + x1, y1, x2, y2, + glyph.width, glyph.height); + } + } else if (ch != ' ' && ch != 127) { + showWarning("No glyph found for the " + ch + + " (\\u" + PApplet.hex(ch, 4) + ") character"); + } + } + + + @Override + protected float textWidthImpl(char[] buffer, int start, int stop) { + if (textFont == null) { + defaultFontOrDeath("textWidth"); + } + + if (textFontInfo.font == null) { + return super.textWidthImpl(buffer, start, stop); + } + + fontCache.measuringText.setFont(textFontInfo.font); + fontCache.measuringText.setText(new String(buffer, start, stop - start)); + return (float) fontCache.measuringText.getLayoutBounds().getWidth(); + } + + + + ////////////////////////////////////////////////////////////// + + // MATRIX STACK + + + @Override + public void pushMatrix() { + if (transformCount == transformStack.length) { + throw new RuntimeException("pushMatrix() cannot use push more than " + + transformStack.length + " times"); + } + transformStack[transformCount] = context.getTransform(transformStack[transformCount]); + transformCount++; + } + + + @Override + public void popMatrix() { + if (transformCount == 0) { + throw new RuntimeException("missing a pushMatrix() " + + "to go with that popMatrix()"); + } + transformCount--; + context.setTransform(transformStack[transformCount]); + } + + + + ////////////////////////////////////////////////////////////// + + // MATRIX TRANSFORMS + + + @Override + public void translate(float tx, float ty) { + context.translate(tx, ty); + } + + + //public void translate(float tx, float ty, float tz) + + + @Override + public void rotate(float angle) { + context.rotate(PApplet.degrees(angle)); + } + + + @Override + public void rotateX(float angle) { + showDepthWarning("rotateX"); + } + + + @Override + public void rotateY(float angle) { + showDepthWarning("rotateY"); + } + + + @Override + public void rotateZ(float angle) { + showDepthWarning("rotateZ"); + } + + + @Override + public void rotate(float angle, float vx, float vy, float vz) { + showVariationWarning("rotate"); + } + + + @Override + public void scale(float s) { + context.scale(s, s); + } + + + @Override + public void scale(float sx, float sy) { + context.scale(sx, sy); + } + + + @Override + public void scale(float sx, float sy, float sz) { + showDepthWarningXYZ("scale"); + } + + + @Override + public void shearX(float angle) { + Affine temp = new Affine(); + temp.appendShear(Math.tan(angle), 0); + context.transform(temp); + } + + + @Override + public void shearY(float angle) { + Affine temp = new Affine(); + temp.appendShear(0, Math.tan(angle)); + context.transform(temp); + } + + + + ////////////////////////////////////////////////////////////// + + // MATRIX MORE + + + @Override + public void resetMatrix() { + context.setTransform(new Affine()); + } + + + //public void applyMatrix(PMatrix2D source) + + + @Override + public void applyMatrix(float n00, float n01, float n02, + float n10, float n11, float n12) { + context.transform(n00, n10, n01, n11, n02, n12); + } + + + //public void applyMatrix(PMatrix3D source) + + + @Override + public void applyMatrix(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + showVariationWarning("applyMatrix"); + } + + + + ////////////////////////////////////////////////////////////// + + // MATRIX GET/SET + + + @Override + public PMatrix getMatrix() { + return getMatrix((PMatrix2D) null); + } + + + @Override + public PMatrix2D getMatrix(PMatrix2D target) { + if (target == null) { + target = new PMatrix2D(); + } + //double[] transform = new double[6]; + // TODO This is not tested; apparently Affine is a full 3x4 + Affine t = context.getTransform(); //.getMatrix(transform); +// target.set((float) transform[0], (float) transform[2], (float) transform[4], +// (float) transform[1], (float) transform[3], (float) transform[5]); + target.set((float) t.getMxx(), (float) t.getMxy(), (float) t.getTx(), + (float) t.getMyx(), (float) t.getMyy(), (float) t.getTy()); + return target; + } + + + @Override + public PMatrix3D getMatrix(PMatrix3D target) { + showVariationWarning("getMatrix"); + return target; + } + + + //public void setMatrix(PMatrix source) + + + @Override + public void setMatrix(PMatrix2D source) { + context.setTransform(source.m00, source.m10, + source.m01, source.m11, + source.m02, source.m12); + } + + + @Override + public void setMatrix(PMatrix3D source) { + showVariationWarning("setMatrix"); + } + + + @Override + public void printMatrix() { + getMatrix((PMatrix2D) null).print(); + } + + + +// ////////////////////////////////////////////////////////////// +// +// // CAMERA and PROJECTION +// +// // Inherit the plaintive warnings from PGraphics +// +// +// //public void beginCamera() +// //public void endCamera() +// //public void camera() +// //public void camera(float eyeX, float eyeY, float eyeZ, +// // float centerX, float centerY, float centerZ, +// // float upX, float upY, float upZ) +// //public void printCamera() +// +// //public void ortho() +// //public void ortho(float left, float right, +// // float bottom, float top, +// // float near, float far) +// //public void perspective() +// //public void perspective(float fov, float aspect, float near, float far) +// //public void frustum(float left, float right, +// // float bottom, float top, +// // float near, float far) +// //public void printProjection() + + + + ////////////////////////////////////////////////////////////// + + // SCREEN and MODEL transforms + + + @Override + public float screenX(float x, float y) { + return (float) context.getTransform().transform(x, y).getX(); + } + + + @Override + public float screenY(float x, float y) { + return (float) context.getTransform().transform(x, y).getY(); + } + + + @Override + public float screenX(float x, float y, float z) { + showDepthWarningXYZ("screenX"); + return 0; + } + + + @Override + public float screenY(float x, float y, float z) { + showDepthWarningXYZ("screenY"); + return 0; + } + + + @Override + public float screenZ(float x, float y, float z) { + showDepthWarningXYZ("screenZ"); + return 0; + } + + + //public float modelX(float x, float y, float z) + + + //public float modelY(float x, float y, float z) + + + //public float modelZ(float x, float y, float z) + + + +// ////////////////////////////////////////////////////////////// +// +// // STYLE +// +// // pushStyle(), popStyle(), style() and getStyle() inherited. + + + + ////////////////////////////////////////////////////////////// + + // STROKE CAP/JOIN/WEIGHT + + + @Override + public void strokeCap(int cap) { + super.strokeCap(cap); + if (strokeCap == ROUND) { + context.setLineCap(StrokeLineCap.ROUND); + } else if (strokeCap == PROJECT) { + context.setLineCap(StrokeLineCap.SQUARE); + } else { + context.setLineCap(StrokeLineCap.BUTT); + } + } + + + @Override + public void strokeJoin(int join) { + super.strokeJoin(join); + if (strokeJoin == MITER) { + context.setLineJoin(StrokeLineJoin.MITER); + } else if (strokeJoin == ROUND) { + context.setLineJoin(StrokeLineJoin.ROUND); + } else { + context.setLineJoin(StrokeLineJoin.BEVEL); + } + } + + + @Override + public void strokeWeight(float weight) { + super.strokeWeight(weight); + context.setLineWidth(weight); + } + + + + ////////////////////////////////////////////////////////////// + + // STROKE + + // noStroke() and stroke() inherited from PGraphics. + + + @Override + protected void strokeFromCalc() { + super.strokeFromCalc(); + context.setStroke(new Color(strokeR, strokeG, strokeB, strokeA)); + } + + + protected boolean drawingThinLines() { + // align strokes to pixel centers when drawing thin lines + return stroke && strokeWeight == 1; + } + + + + ////////////////////////////////////////////////////////////// + + // TINT + + // noTint() and tint() inherited from PGraphics. + + + + ////////////////////////////////////////////////////////////// + + // FILL + + // noFill() and fill() inherited from PGraphics. + + + @Override + protected void fillFromCalc() { + super.fillFromCalc(); + context.setFill(new Color(fillR, fillG, fillB, fillA)); + } + + + +// ////////////////////////////////////////////////////////////// +// +// // MATERIAL PROPERTIES +// +// +// //public void ambient(int rgb) +// //public void ambient(float gray) +// //public void ambient(float x, float y, float z) +// //protected void ambientFromCalc() +// //public void specular(int rgb) +// //public void specular(float gray) +// //public void specular(float x, float y, float z) +// //protected void specularFromCalc() +// //public void shininess(float shine) +// //public void emissive(int rgb) +// //public void emissive(float gray) +// //public void emissive(float x, float y, float z ) +// //protected void emissiveFromCalc() +// +// +// +// ////////////////////////////////////////////////////////////// +// +// // LIGHTS +// +// +// //public void lights() +// //public void noLights() +// //public void ambientLight(float red, float green, float blue) +// //public void ambientLight(float red, float green, float blue, +// // float x, float y, float z) +// //public void directionalLight(float red, float green, float blue, +// // float nx, float ny, float nz) +// //public void pointLight(float red, float green, float blue, +// // float x, float y, float z) +// //public void spotLight(float red, float green, float blue, +// // float x, float y, float z, +// // float nx, float ny, float nz, +// // float angle, float concentration) +// //public void lightFalloff(float constant, float linear, float quadratic) +// //public void lightSpecular(float x, float y, float z) +// //protected void lightPosition(int num, float x, float y, float z) +// //protected void lightDirection(int num, float x, float y, float z) + + + + ////////////////////////////////////////////////////////////// + + // BACKGROUND + + + @Override + public void backgroundImpl() { + + // if pixels are modified, we don't flush them (just mark them flushed) + // because they would be immediatelly overwritten by the background anyway + modified = false; + loaded = false; + + // Save drawing context (transform, fill, blend mode, etc.) + context.save(); + + // Reset transform to identity + context.setTransform(new Affine()); + + // This only takes into account cases where this is the primary surface. + // Not sure what we do with offscreen anyway. + context.setFill(new Color(backgroundR, backgroundG, backgroundB, backgroundA)); + context.setGlobalBlendMode(BlendMode.SRC_OVER); + context.fillRect(0, 0, width, height); + + // Restore drawing context (transform, fill, blend mode, etc.) + context.restore(); + } + + + +// ////////////////////////////////////////////////////////////// +// +// // COLOR MODE +// +// // All colorMode() variations are inherited from PGraphics. +// +// +// +// ////////////////////////////////////////////////////////////// +// +// // COLOR CALC +// +// // colorCalc() and colorCalcARGB() inherited from PGraphics. +// +// +// +// ////////////////////////////////////////////////////////////// +// +// // COLOR DATATYPE STUFFING +// +// // final color() variations inherited. +// +// +// +// ////////////////////////////////////////////////////////////// +// +// // COLOR DATATYPE EXTRACTION +// +// // final methods alpha, red, green, blue, +// // hue, saturation, and brightness all inherited. +// +// +// +// ////////////////////////////////////////////////////////////// +// +// // COLOR DATATYPE INTERPOLATION +// +// // both lerpColor variants inherited. +// +// +// +// ////////////////////////////////////////////////////////////// +// +// // BEGIN/END RAW +// +// +// @Override +// public void beginRaw(PGraphics recorderRaw) { +// showMethodWarning("beginRaw"); +// } +// +// +// @Override +// public void endRaw() { +// showMethodWarning("endRaw"); +// } +// +// +// +// ////////////////////////////////////////////////////////////// +// +// // WARNINGS and EXCEPTIONS +// +// // showWarning and showException inherited. +// +// +// +// ////////////////////////////////////////////////////////////// +// +// // RENDERER SUPPORT QUERIES +// +// +// //public boolean displayable() // true +// +// +// //public boolean is2D() // true +// +// +// //public boolean is3D() // false + + + + ////////////////////////////////////////////////////////////// + + // PIMAGE METHODS + + + @Override + public void loadPixels() { + if ((pixels == null) || (pixels.length != pixelWidth * pixelHeight)) { + pixels = new int[pixelWidth * pixelHeight]; + loaded = false; + } + + if (!loaded) { + if (snapshotImage == null || + snapshotImage.getWidth() != pixelWidth || + snapshotImage.getHeight() != pixelHeight) { + snapshotImage = new WritableImage(pixelWidth, pixelHeight); + } + + SnapshotParameters sp = null; + if (pixelDensity != 1) { + sp = new SnapshotParameters(); + sp.setTransform(Transform.scale(pixelDensity, pixelDensity)); + } + snapshotImage = ((PSurfaceFX) surface).canvas.snapshot(sp, snapshotImage); + PixelReader pr = snapshotImage.getPixelReader(); + pr.getPixels(0, 0, pixelWidth, pixelHeight, argbFormat, pixels, 0, pixelWidth); + + loaded = true; + modified = false; + } + } + + + + ////////////////////////////////////////////////////////////// + + // GET/SET PIXELS + + + @Override + public int get(int x, int y) { + loadPixels(); + return super.get(x, y); + } + + + @Override + protected void getImpl(int sourceX, int sourceY, + int sourceWidth, int sourceHeight, + PImage target, int targetX, int targetY) { + loadPixels(); + super.getImpl(sourceX, sourceY, sourceWidth, sourceHeight, + target, targetX, targetY); + } + + + @Override + public void set(int x, int y, int argb) { + loadPixels(); + super.set(x, y, argb); + } + + + @Override + protected void setImpl(PImage sourceImage, + int sourceX, int sourceY, + int sourceWidth, int sourceHeight, + int targetX, int targetY) { + sourceImage.loadPixels(); + + int sourceOffset = sourceX + sourceImage.pixelWidth * sourceY; + + PixelWriter pw = context.getPixelWriter(); + pw.setPixels(targetX, targetY, sourceWidth, sourceHeight, + argbFormat, + sourceImage.pixels, + sourceOffset, + sourceImage.pixelWidth); + + // Let's keep them loaded + if (loaded) { + int sourceStride = sourceImage.pixelWidth; + int targetStride = pixelWidth; + int targetOffset = targetX + targetY * targetStride; + for (int i = 0; i < sourceHeight; i++) { + System.arraycopy(sourceImage.pixels, sourceOffset + i * sourceStride, + pixels, targetOffset + i * targetStride, sourceWidth); + } + } + } + + + ////////////////////////////////////////////////////////////// + + // MASK + + + static final String MASK_WARNING = + "mask() cannot be used on the main drawing surface"; + + + @Override + public void mask(PImage alpha) { + showWarning(MASK_WARNING); + } + + + + ////////////////////////////////////////////////////////////// + + // FILTER + + // Because the PImage versions call loadPixels() and + // updatePixels(), no need to override anything here. + + + //public void filter(int kind) + + + //public void filter(int kind, float param) + + + + ////////////////////////////////////////////////////////////// + + // COPY + + +// @Override +// public void copy(int sx, int sy, int sw, int sh, +// int dx, int dy, int dw, int dh) { +// if ((sw != dw) || (sh != dh)) { +// g2.drawImage(image, dx, dy, dx + dw, dy + dh, sx, sy, sx + sw, sy + sh, null); +// +// } else { +// dx = dx - sx; // java2d's "dx" is the delta, not dest +// dy = dy - sy; +// g2.copyArea(sx, sy, sw, sh, dx, dy); +// } +// } + + +// @Override +// public void copy(PImage src, +// int sx, int sy, int sw, int sh, +// int dx, int dy, int dw, int dh) { +// g2.drawImage((Image) src.getNative(), +// dx, dy, dx + dw, dy + dh, +// sx, sy, sx + sw, sy + sh, null); +// } + + + + ////////////////////////////////////////////////////////////// + + // BLEND + + + //static public int blendColor(int c1, int c2, int mode) + + + //public void blend(int sx, int sy, int sw, int sh, + // int dx, int dy, int dw, int dh, int mode) + + + //public void blend(PImage src, + // int sx, int sy, int sw, int sh, + // int dx, int dy, int dw, int dh, int mode) + + + + ////////////////////////////////////////////////////////////// + + // SAVE + + + //public void save(String filename) + + + + ////////////////////////////////////////////////////////////// + + /** + * Display a warning that the specified method is simply unavailable. + */ + static public void showTodoWarning(String method, int issue) { + showWarning(method + "() is not yet available: " + + "https://github.com/processing/processing/issues/" + issue); + } +} diff --git a/src/main/java/processing/javafx/PSurfaceFX.java b/src/main/java/processing/javafx/PSurfaceFX.java new file mode 100644 index 0000000..e9f8243 --- /dev/null +++ b/src/main/java/processing/javafx/PSurfaceFX.java @@ -0,0 +1,1069 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2015 The Processing Foundation + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.javafx; + +import com.sun.glass.ui.Screen; + +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Rectangle; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.SynchronousQueue; + +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.event.EventType; +import javafx.scene.Cursor; +import javafx.scene.ImageCursor; +import javafx.scene.Scene; +import javafx.scene.SceneAntialiasing; +import javafx.scene.canvas.Canvas; +import javafx.scene.image.Image; +import javafx.scene.image.PixelFormat; +import javafx.scene.image.WritableImage; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.ScrollEvent; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.stage.WindowEvent; +import javafx.util.Duration; +import processing.core.*; + + +public class PSurfaceFX implements PSurface { + PApplet sketch; + + PGraphicsFX2D fx; + Stage stage; + Canvas canvas; + + final Animation animation; + float frameRate = 60; + + private SynchronousQueue drawExceptionQueue = new SynchronousQueue<>(); + + public PSurfaceFX(PGraphicsFX2D graphics) { + fx = graphics; + canvas = new ResizableCanvas(); + + // set up main drawing loop + KeyFrame keyFrame = new KeyFrame(Duration.millis(1000), + new EventHandler() { + public void handle(ActionEvent event) { + long startNanoTime = System.nanoTime(); + try { + sketch.handleDraw(); + } catch (Throwable e) { + // Let exception handler thread crash with our exception + drawExceptionQueue.offer(e); + // Stop animating right now so nothing runs afterwards + // and crash frame can be for example traced by println() + animation.stop(); + return; + } + long drawNanos = System.nanoTime() - startNanoTime; + + if (sketch.exitCalled()) { + // using Platform.runLater() didn't work +// Platform.runLater(new Runnable() { +// public void run() { + // instead of System.exit(), safely shut down JavaFX this way + Platform.exit(); +// } +// }); + } + if (sketch.frameCount > 5) { + animation.setRate(-PApplet.min(1e9f / drawNanos, frameRate)); + } + } + }); + animation = new Timeline(keyFrame); + animation.setCycleCount(Animation.INDEFINITE); + + // key frame has duration of 1 second, so the rate of the animation + // should be set to frames per second + + // setting rate to negative so that event fires at the start of + // the key frame and first frame is drawn immediately + animation.setRate(-frameRate); + } + + + public Object getNative() { + return canvas; + } + + + class ResizableCanvas extends Canvas { + + public ResizableCanvas() { + widthProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue value, + Number oldWidth, Number newWidth) { +// sketch.width = newWidth.intValue(); + sketch.setSize(newWidth.intValue(), sketch.height); +// draw(); + fx.setSize(sketch.width, sketch.height); + } + }); + heightProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue value, + Number oldHeight, Number newHeight) { +// sketch.height = newHeight.intValue(); + sketch.setSize(sketch.width, newHeight.intValue()); +// draw(); + fx.setSize(sketch.width, sketch.height); + } + }); + + //addEventHandler(eventType, eventHandler); + + EventHandler mouseHandler = new EventHandler() { + public void handle(MouseEvent e) { + fxMouseEvent(e); + } + }; + + setOnMousePressed(mouseHandler); + setOnMouseReleased(mouseHandler); + setOnMouseClicked(mouseHandler); + setOnMouseEntered(mouseHandler); + setOnMouseExited(mouseHandler); + + setOnMouseDragged(mouseHandler); + setOnMouseMoved(mouseHandler); + + setOnScroll(new EventHandler() { + public void handle(ScrollEvent e) { + fxScrollEvent(e); + } + }); + + EventHandler keyHandler = new EventHandler() { + public void handle(KeyEvent e) { + fxKeyEvent(e); + } + }; + + setOnKeyPressed(keyHandler); + setOnKeyReleased(keyHandler); + setOnKeyTyped(keyHandler); + + setFocusTraversable(false); // prevent tab from de-focusing + + focusedProperty().addListener(new ChangeListener() { + public void changed(ObservableValue value, + Boolean oldValue, Boolean newValue) { + if (newValue.booleanValue()) { + sketch.focused = true; + sketch.focusGained(); + } else { + sketch.focused = false; + sketch.focusLost(); + } + } + }); + } + + public Stage getStage() { + return stage; + } + + @Override + public boolean isResizable() { + return true; + } + + @Override + public double prefWidth(double height) { + return getWidth(); + } + + @Override + public double prefHeight(double width) { + return getHeight(); + } + } + + + public void initOffscreen(PApplet sketch) { + } + + +// public Component initComponent(PApplet sketch) { +// return null; +// } + + + static public class PApplicationFX extends Application { + static public PSurfaceFX surface; +// static String title; // title set at launch +// static boolean resizable; // set at launch + + public PApplicationFX() { } + + @Override + public void start(final Stage stage) { +// if (title != null) { +// stage.setTitle(title); +// } + + PApplet sketch = surface.sketch; + + float renderScale = Screen.getMainScreen().getRenderScale(); + if (PApplet.platform == PConstants.MACOSX) { + for (Screen s : Screen.getScreens()) { + renderScale = Math.max(renderScale, s.getRenderScale()); + } + } + float uiScale = Screen.getMainScreen().getUIScale(); + if (sketch.pixelDensity == 2 && renderScale < 2) { + sketch.pixelDensity = 1; + sketch.g.pixelDensity = 1; + System.err.println("pixelDensity(2) is not available for this display"); + } + + // Use AWT display code, because FX orders screens in different way + GraphicsDevice displayDevice = null; + + GraphicsEnvironment environment = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + + int displayNum = sketch.sketchDisplay(); + if (displayNum > 0) { // if -1, use the default device + GraphicsDevice[] devices = environment.getScreenDevices(); + if (displayNum <= devices.length) { + displayDevice = devices[displayNum - 1]; + } else { + System.err.format("Display %d does not exist, " + + "using the default display instead.%n", displayNum); + for (int i = 0; i < devices.length; i++) { + System.err.format("Display %d is %s%n", (i+1), devices[i]); + } + } + } + if (displayDevice == null) { + displayDevice = environment.getDefaultScreenDevice(); + } + + boolean fullScreen = sketch.sketchFullScreen(); + boolean spanDisplays = sketch.sketchDisplay() == PConstants.SPAN; + + Rectangle primaryScreenRect = displayDevice.getDefaultConfiguration().getBounds(); + Rectangle screenRect = primaryScreenRect; + if (fullScreen || spanDisplays) { + double minX = screenRect.getMinX(); + double maxX = screenRect.getMaxX(); + double minY = screenRect.getMinY(); + double maxY = screenRect.getMaxY(); + if (spanDisplays) { + for (GraphicsDevice s : environment.getScreenDevices()) { + Rectangle bounds = s.getDefaultConfiguration().getBounds(); + minX = Math.min(minX, bounds.getMinX()); + maxX = Math.max(maxX, bounds.getMaxX()); + minY = Math.min(minY, bounds.getMinY()); + maxY = Math.max(maxY, bounds.getMaxY()); + } + } + screenRect = new Rectangle((int) minX, (int) minY, + (int) (maxX - minX), (int) (maxY - minY)); + } + + // Set the displayWidth/Height variables inside PApplet, so that they're + // usable and can even be returned by the sketchWidth()/Height() methods. + sketch.displayWidth = (int) screenRect.getWidth(); + sketch.displayHeight = (int) screenRect.getHeight(); + + int sketchWidth = sketch.sketchWidth(); + int sketchHeight = sketch.sketchHeight(); + + if (fullScreen || spanDisplays) { + sketchWidth = (int) (screenRect.getWidth() / uiScale); + sketchHeight = (int) (screenRect.getHeight() / uiScale); + + stage.initStyle(StageStyle.UNDECORATED); + stage.setX(screenRect.getMinX() / uiScale); + stage.setY(screenRect.getMinY() / uiScale); + stage.setWidth(screenRect.getWidth() / uiScale); + stage.setHeight(screenRect.getHeight() / uiScale); + } + + Canvas canvas = surface.canvas; + surface.fx.context = canvas.getGraphicsContext2D(); + StackPane stackPane = new StackPane(); + stackPane.getChildren().add(canvas); + canvas.widthProperty().bind(stackPane.widthProperty()); + canvas.heightProperty().bind(stackPane.heightProperty()); + + int width = sketchWidth; + int height = sketchHeight; + int smooth = sketch.sketchSmooth(); + + // Workaround for https://bugs.openjdk.java.net/browse/JDK-8136495 + // https://github.com/processing/processing/issues/3823 + if ((PApplet.platform == PConstants.MACOSX || + PApplet.platform == PConstants.LINUX) && + PApplet.javaVersionName.compareTo("1.8.0_60") >= 0 && + PApplet.javaVersionName.compareTo("1.8.0_72") < 0) { + System.err.println("smooth() disabled for JavaFX with this Java version due to Oracle bug"); + System.err.println("https://github.com/processing/processing/issues/3795"); + smooth = 0; + } + + SceneAntialiasing sceneAntialiasing = (smooth == 0) ? + SceneAntialiasing.DISABLED : SceneAntialiasing.BALANCED; + + stage.setScene(new Scene(stackPane, width, height, false, sceneAntialiasing)); + + // initFrame in different thread is waiting for + // the stage, assign it only when it is all set up + surface.stage = stage; + } + + @Override + public void stop() throws Exception { + surface.sketch.dispose(); + } + } + + + //public Frame initFrame(PApplet sketch, java.awt.Color backgroundColor, + public void initFrame(PApplet sketch) {/*, int backgroundColor, + int deviceIndex, boolean fullScreen, + boolean spanDisplays) {*/ + this.sketch = sketch; + PApplicationFX.surface = this; + //Frame frame = new DummyFrame(); + new Thread(new Runnable() { + public void run() { + Application.launch(PApplicationFX.class); + } + }).start(); + + // wait for stage to be initialized on its own thread before continuing + while (stage == null) { + try { + //System.out.println("waiting for launch"); + Thread.sleep(5); + } catch (InterruptedException e) { } + } + + startExceptionHandlerThread(); + + setProcessingIcon(stage); + } + + + private void startExceptionHandlerThread() { + Thread exceptionHandlerThread = new Thread(() -> { + Throwable drawException; + try { + drawException = drawExceptionQueue.take(); + } catch (InterruptedException e) { + return; + } + // Adapted from PSurfaceJOGL + if (drawException != null) { + if (drawException instanceof ThreadDeath) { +// System.out.println("caught ThreadDeath"); +// throw (ThreadDeath)cause; + } else if (drawException instanceof RuntimeException) { + throw (RuntimeException) drawException; + } else if (drawException instanceof UnsatisfiedLinkError) { + throw new UnsatisfiedLinkError(drawException.getMessage()); + } else { + throw new RuntimeException(drawException); + } + } + }); + exceptionHandlerThread.setDaemon(true); + exceptionHandlerThread.setName("Processing-FX-ExceptionHandler"); + exceptionHandlerThread.start(); + } + + + /** Set the window (and dock, or whatever necessary) title. */ + public void setTitle(String title) { +// PApplicationFX.title = title; // store this in case the stage still null +// if (stage != null) { + stage.setTitle(title); +// } + } + + + /** Show or hide the window. */ + @Override + public void setVisible(final boolean visible) { + Platform.runLater(new Runnable() { + public void run() { + if (visible) { + stage.show(); + canvas.requestFocus(); + } else { + stage.hide(); + } + } + }); + } + + + /** Set true if we want to resize things (default is not resizable) */ + public void setResizable(boolean resizable) { +// PApplicationFX.resizable = resizable; +// if (stage != null) { + stage.setResizable(resizable); +// } + } + + + public void setIcon(PImage icon) { + int w = icon.pixelWidth; + int h = icon.pixelHeight; + WritableImage im = new WritableImage(w, h); + im.getPixelWriter().setPixels(0, 0, w, h, + PixelFormat.getIntArgbInstance(), + icon.pixels, + 0, w); + + Stage stage = (Stage) canvas.getScene().getWindow(); + stage.getIcons().clear(); + stage.getIcons().add(im); + } + + + List iconImages; + + protected void setProcessingIcon(Stage stage) { + // Adapted from PSurfaceAWT + // Note: FX chooses wrong icon size, should be fixed in Java 9, see: + // https://bugs.openjdk.java.net/browse/JDK-8091186 + // Removing smaller sizes helps a bit, but big ones are downsized + try { + if (iconImages == null) { + iconImages = new ArrayList<>(); + final int[] sizes = { 48, 64, 128, 256, 512 }; + + for (int sz : sizes) { + URL url = PApplet.class.getResource("/icon/icon-" + sz + ".png"); + Image image = new Image(url.toString()); + iconImages.add(image); + } + } + List icons = stage.getIcons(); + icons.clear(); + icons.addAll(iconImages); + } catch (Exception e) { } // harmless; keep this to ourselves + } + + + @Override + public void setAlwaysOnTop(boolean always) { + stage.setAlwaysOnTop(always); + } + + + /* + @Override + public void placeWindow(int[] location) { + //setFrameSize(); + + if (location != null) { + // a specific location was received from the Runner + // (applet has been run more than once, user placed window) + stage.setX(location[0]); + stage.setY(location[1]); + + } else { // just center on screen + // Can't use frame.setLocationRelativeTo(null) because it sends the + // frame to the main display, which undermines the --display setting. +// frame.setLocation(screenRect.x + (screenRect.width - sketchWidth) / 2, +// screenRect.y + (screenRect.height - sketchHeight) / 2); + } + if (stage.getY() < 0) { + // Windows actually allows you to place frames where they can't be + // closed. Awesome. http://dev.processing.org/bugs/show_bug.cgi?id=1508 + //frame.setLocation(frameLoc.x, 30); + stage.setY(30); + } + + //setCanvasSize(); + + // TODO add window closing behavior +// frame.addWindowListener(new WindowAdapter() { +// @Override +// public void windowClosing(WindowEvent e) { +// System.exit(0); +// } +// }); + + // TODO handle frame resizing events +// setupFrameResizeListener(); + + if (sketch.getGraphics().displayable()) { + setVisible(true); + } + } + */ + + + @Override + public void placeWindow(int[] location, int[] editorLocation) { + if (sketch.sketchFullScreen()) { + PApplet.hideMenuBar(); + return; + } + + int wide = sketch.width; // stage.getWidth() is NaN here + //int high = sketch.height; // stage.getHeight() + + if (location != null) { + // a specific location was received from the Runner + // (applet has been run more than once, user placed window) + stage.setX(location[0]); + stage.setY(location[1]); + + } else if (editorLocation != null) { + int locationX = editorLocation[0] - 20; + int locationY = editorLocation[1]; + + if (locationX - wide > 10) { + // if it fits to the left of the window + stage.setX(locationX - wide); + stage.setY(locationY); + + } else { // doesn't fit + stage.centerOnScreen(); + } + } else { // just center on screen + stage.centerOnScreen(); + } + } + + + // http://download.java.net/jdk8/jfxdocs/javafx/stage/Stage.html#setFullScreenExitHint-java.lang.String- + // http://download.java.net/jdk8/jfxdocs/javafx/stage/Stage.html#setFullScreenExitKeyCombination-javafx.scene.input.KeyCombination- + public void placePresent(int stopColor) { + // TODO Auto-generated method stub + PApplet.hideMenuBar(); + } + + + @Override + public void setupExternalMessages() { + stage.xProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue value, + Number oldX, Number newX) { + sketch.frameMoved(newX.intValue(), stage.yProperty().intValue()); + } + }); + + stage.yProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue value, + Number oldY, Number newY) { + sketch.frameMoved(stage.xProperty().intValue(), newY.intValue()); + } + }); + + stage.setOnCloseRequest(new EventHandler() { + public void handle(WindowEvent we) { + sketch.exit(); + } + }); + } + + + public void setLocation(int x, int y) { + stage.setX(x); + stage.setY(y); + } + + + public void setSize(int wide, int high) { + // When the surface is set to resizable via surface.setResizable(true), + // a crash may occur if the user sets the window to size zero. + // https://github.com/processing/processing/issues/5052 + if (high <= 0) { + high = 1; + } + if (wide <= 0) { + wide = 1; + } + + //System.out.format("%s.setSize(%d, %d)%n", getClass().getSimpleName(), width, height); + Scene scene = stage.getScene(); + double decorH = stage.getWidth() - scene.getWidth(); + double decorV = stage.getHeight() - scene.getHeight(); + stage.setWidth(wide + decorH); + stage.setHeight(high + decorV); + fx.setSize(wide, high); + } + + +// public Component getComponent() { +// return null; +// } + + + public void setSmooth(int level) { + // TODO Auto-generated method stub + + } + + + public void setFrameRate(float fps) { + // setting rate to negative so that event fires at the start of + // the key frame and first frame is drawn immediately + if (fps > 0) { + frameRate = fps; + animation.setRate(-frameRate); + } + } + + +// @Override +// public void requestFocus() { +// canvas.requestFocus(); +// } + + Cursor lastCursor = Cursor.DEFAULT; + + public void setCursor(int kind) { + Cursor c; + switch (kind) { + case PConstants.ARROW: c = Cursor.DEFAULT; break; + case PConstants.CROSS: c = Cursor.CROSSHAIR; break; + case PConstants.HAND: c = Cursor.HAND; break; + case PConstants.MOVE: c = Cursor.MOVE; break; + case PConstants.TEXT: c = Cursor.TEXT; break; + case PConstants.WAIT: c = Cursor.WAIT; break; + default: c = Cursor.DEFAULT; break; + } + lastCursor = c; + canvas.getScene().setCursor(c); + } + + + public void setCursor(PImage image, int hotspotX, int hotspotY) { + int w = image.pixelWidth; + int h = image.pixelHeight; + WritableImage im = new WritableImage(w, h); + im.getPixelWriter().setPixels(0, 0, w, h, + PixelFormat.getIntArgbInstance(), + image.pixels, + 0, w); + ImageCursor c = new ImageCursor(im, hotspotX, hotspotY); + lastCursor = c; + canvas.getScene().setCursor(c); + } + + + public void showCursor() { + canvas.getScene().setCursor(lastCursor); + } + + + public void hideCursor() { + canvas.getScene().setCursor(Cursor.NONE); + } + + + public void startThread() { + animation.play(); + } + + + public void pauseThread() { + animation.pause(); + } + + + public void resumeThread() { + animation.play(); + } + + + public boolean stopThread() { + animation.stop(); + return true; + } + + + public boolean isStopped() { + return animation.getStatus() == Animation.Status.STOPPED; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + /* + protected void addListeners() { + + canvas.addMouseListener(new MouseListener() { + + public void mousePressed(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + + public void mouseReleased(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + + public void mouseClicked(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + + public void mouseEntered(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + + public void mouseExited(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + }); + + canvas.addMouseMotionListener(new MouseMotionListener() { + + public void mouseDragged(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + + public void mouseMoved(java.awt.event.MouseEvent e) { + nativeMouseEvent(e); + } + }); + + canvas.addMouseWheelListener(new MouseWheelListener() { + + public void mouseWheelMoved(MouseWheelEvent e) { + nativeMouseEvent(e); + } + }); + + canvas.addKeyListener(new KeyListener() { + + public void keyPressed(java.awt.event.KeyEvent e) { + nativeKeyEvent(e); + } + + + public void keyReleased(java.awt.event.KeyEvent e) { + nativeKeyEvent(e); + } + + + public void keyTyped(java.awt.event.KeyEvent e) { + nativeKeyEvent(e); + } + }); + + canvas.addFocusListener(new FocusListener() { + + public void focusGained(FocusEvent e) { + sketch.focused = true; + sketch.focusGained(); + } + + public void focusLost(FocusEvent e) { + sketch.focused = false; + sketch.focusLost(); + } + }); + } + */ + + + static Map, Integer> mouseMap = + new HashMap, Integer>(); + static { + mouseMap.put(MouseEvent.MOUSE_PRESSED, processing.event.MouseEvent.PRESS); + mouseMap.put(MouseEvent.MOUSE_RELEASED, processing.event.MouseEvent.RELEASE); + mouseMap.put(MouseEvent.MOUSE_CLICKED, processing.event.MouseEvent.CLICK); + mouseMap.put(MouseEvent.MOUSE_DRAGGED, processing.event.MouseEvent.DRAG); + mouseMap.put(MouseEvent.MOUSE_MOVED, processing.event.MouseEvent.MOVE); + mouseMap.put(MouseEvent.MOUSE_ENTERED, processing.event.MouseEvent.ENTER); + mouseMap.put(MouseEvent.MOUSE_EXITED, processing.event.MouseEvent.EXIT); + } + + protected void fxMouseEvent(MouseEvent fxEvent) { + // the 'amount' is the number of button clicks for a click event, + // or the number of steps/clicks on the wheel for a mouse wheel event. + int count = fxEvent.getClickCount(); + + int action = mouseMap.get(fxEvent.getEventType()); + + int modifiers = 0; + if (fxEvent.isShiftDown()) { + modifiers |= processing.event.Event.SHIFT; + } + if (fxEvent.isControlDown()) { + modifiers |= processing.event.Event.CTRL; + } + if (fxEvent.isMetaDown()) { + modifiers |= processing.event.Event.META; + } + if (fxEvent.isAltDown()) { + modifiers |= processing.event.Event.ALT; + } + + int button = 0; + switch (fxEvent.getButton()) { + case PRIMARY: + button = PConstants.LEFT; + break; + case SECONDARY: + button = PConstants.RIGHT; + break; + case MIDDLE: + button = PConstants.CENTER; + break; + case NONE: + // not currently handled + break; + } + + // If running on Mac OS, allow ctrl-click as right mouse. + // Verified to be necessary with Java 8u45. + if (PApplet.platform == PConstants.MACOSX && + fxEvent.isControlDown() && + button == PConstants.LEFT) { + button = PConstants.RIGHT; + } + + //long when = nativeEvent.getWhen(); // from AWT + long when = System.currentTimeMillis(); + int x = (int) fxEvent.getX(); // getSceneX()? + int y = (int) fxEvent.getY(); + + sketch.postEvent(new processing.event.MouseEvent(fxEvent, when, + action, modifiers, + x, y, button, count)); + } + + // https://docs.oracle.com/javase/8/javafx/api/javafx/scene/input/ScrollEvent.html + protected void fxScrollEvent(ScrollEvent fxEvent) { + // the number of steps/clicks on the wheel for a mouse wheel event. + int count = (int) -(fxEvent.getDeltaY() / fxEvent.getMultiplierY()); + + int action = processing.event.MouseEvent.WHEEL; + + int modifiers = 0; + if (fxEvent.isShiftDown()) { + modifiers |= processing.event.Event.SHIFT; + } + if (fxEvent.isControlDown()) { + modifiers |= processing.event.Event.CTRL; + } + if (fxEvent.isMetaDown()) { + modifiers |= processing.event.Event.META; + } + if (fxEvent.isAltDown()) { + modifiers |= processing.event.Event.ALT; + } + + // FX does not supply button info + int button = 0; + + long when = System.currentTimeMillis(); + int x = (int) fxEvent.getX(); // getSceneX()? + int y = (int) fxEvent.getY(); + + sketch.postEvent(new processing.event.MouseEvent(fxEvent, when, + action, modifiers, + x, y, button, count)); + } + + + protected void fxKeyEvent(javafx.scene.input.KeyEvent fxEvent) { + int action = 0; + EventType et = fxEvent.getEventType(); + if (et == KeyEvent.KEY_PRESSED) { + action = processing.event.KeyEvent.PRESS; + } else if (et == KeyEvent.KEY_RELEASED) { + action = processing.event.KeyEvent.RELEASE; + } else if (et == KeyEvent.KEY_TYPED) { + action = processing.event.KeyEvent.TYPE; + } + + int modifiers = 0; + if (fxEvent.isShiftDown()) { + modifiers |= processing.event.Event.SHIFT; + } + if (fxEvent.isControlDown()) { + modifiers |= processing.event.Event.CTRL; + } + if (fxEvent.isMetaDown()) { + modifiers |= processing.event.Event.META; + } + if (fxEvent.isAltDown()) { + modifiers |= processing.event.Event.ALT; + } + + long when = System.currentTimeMillis(); + + char keyChar = getKeyChar(fxEvent); + int keyCode = getKeyCode(fxEvent); + sketch.postEvent(new processing.event.KeyEvent(fxEvent, when, + action, modifiers, + keyChar, keyCode)); + } + + + @SuppressWarnings("deprecation") + private int getKeyCode(KeyEvent fxEvent) { + if (fxEvent.getEventType() == KeyEvent.KEY_TYPED) { + return 0; + } + + KeyCode kc = fxEvent.getCode(); + switch (kc) { + case ALT_GRAPH: + return PConstants.ALT; + default: + break; + } + return kc.impl_getCode(); + } + + + @SuppressWarnings("deprecation") + private char getKeyChar(KeyEvent fxEvent) { + KeyCode kc = fxEvent.getCode(); + + // Overriding chars for some + // KEY_PRESSED and KEY_RELEASED events + switch (kc) { + case UP: + case KP_UP: + case DOWN: + case KP_DOWN: + case LEFT: + case KP_LEFT: + case RIGHT: + case KP_RIGHT: + case ALT: + case ALT_GRAPH: + case CONTROL: + case SHIFT: + case CAPS: + case META: + case WINDOWS: + case CONTEXT_MENU: + case HOME: + case PAGE_UP: + case PAGE_DOWN: + case END: + case PAUSE: + case PRINTSCREEN: + case INSERT: + case NUM_LOCK: + case SCROLL_LOCK: + case F1: + case F2: + case F3: + case F4: + case F5: + case F6: + case F7: + case F8: + case F9: + case F10: + case F11: + case F12: + return PConstants.CODED; + case ENTER: + return '\n'; + case DIVIDE: + return '/'; + case MULTIPLY: + return '*'; + case SUBTRACT: + return '-'; + case ADD: + return '+'; + case NUMPAD0: + return '0'; + case NUMPAD1: + return '1'; + case NUMPAD2: + return '2'; + case NUMPAD3: + return '3'; + case NUMPAD4: + return '4'; + case NUMPAD5: + return '5'; + case NUMPAD6: + return '6'; + case NUMPAD7: + return '7'; + case NUMPAD8: + return '8'; + case NUMPAD9: + return '9'; + case DECIMAL: + // KEY_TYPED does not go through here and will produce + // dot or comma based on the keyboard layout. + // For KEY_PRESSED and KEY_RELEASED, let's just go with + // the dot. Users can detect the key by its keyCode. + return '.'; + case UNDEFINED: + // KEY_TYPED has KeyCode: UNDEFINED + // and falls through here + break; + default: + break; + } + + // Just go with what FX gives us for the rest of + // KEY_PRESSED and KEY_RELEASED and all of KEY_TYPED + String ch; + if (fxEvent.getEventType() == KeyEvent.KEY_TYPED) { + ch = fxEvent.getCharacter(); + } else { + ch = kc.impl_getChar(); + } + + if (ch.length() < 1) return PConstants.CODED; + if (ch.startsWith("\r")) return '\n'; // normalize enter key + return ch.charAt(0); + } +} diff --git a/src/main/java/processing/opengl/FontTexture.java b/src/main/java/processing/opengl/FontTexture.java new file mode 100644 index 0000000..af28ad9 --- /dev/null +++ b/src/main/java/processing/opengl/FontTexture.java @@ -0,0 +1,379 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.opengl; + +import processing.core.PApplet; +import processing.core.PConstants; +import processing.core.PFont; +import processing.core.PGraphics; +import processing.core.PImage; + +import java.util.HashMap; + +/** + * All the infrastructure needed for optimized font rendering + * in OpenGL. Basically, this special class is needed because + * fonts in Processing are handled by a separate PImage for each + * glyph. For performance reasons, all these glyphs should be + * stored in a single OpenGL texture (otherwise, rendering a + * string of text would involve binding and un-binding several + * textures. + * PFontTexture manages the correspondence between individual + * glyphs and the large OpenGL texture containing them. Also, + * in the case that the font size is very large, one single + * OpenGL texture might not be enough to store all the glyphs, + * so PFontTexture also takes care of spreading a single font + * over several textures. + * @author Andres Colubri + */ +class FontTexture implements PConstants { + protected PGL pgl; + protected boolean is3D; + + protected int minSize; + protected int maxSize; + protected int offsetX; + protected int offsetY; + protected int lineHeight; + protected Texture[] textures = null; + protected PImage[] images = null; + protected int lastTex; + protected TextureInfo[] glyphTexinfos; + protected HashMap texinfoMap; + + public FontTexture(PGraphicsOpenGL pg, PFont font, boolean is3D) { + pgl = pg.pgl; + this.is3D = is3D; + + initTexture(pg, font); + } + + + protected void allocate() { + // Nothing to do here: the font textures will allocate + // themselves. + } + + + protected void dispose() { + for (int i = 0; i < textures.length; i++) { + textures[i].dispose(); + } + } + + + protected void initTexture(PGraphicsOpenGL pg, PFont font) { + lastTex = -1; + + int spow = PGL.nextPowerOfTwo(font.getSize()); + minSize = PApplet.min(PGraphicsOpenGL.maxTextureSize, + PApplet.max(PGL.MIN_FONT_TEX_SIZE, spow)); + maxSize = PApplet.min(PGraphicsOpenGL.maxTextureSize, + PApplet.max(PGL.MAX_FONT_TEX_SIZE, 2 * spow)); + + if (maxSize < spow) { + PGraphics.showWarning("The font size is too large to be properly " + + "displayed with OpenGL"); + } + + addTexture(pg); + + offsetX = 0; + offsetY = 0; + lineHeight = 0; + + texinfoMap = new HashMap(); + glyphTexinfos = new TextureInfo[font.getGlyphCount()]; + addAllGlyphsToTexture(pg, font); + } + + + public boolean addTexture(PGraphicsOpenGL pg) { + int w, h; + boolean resize; + + w = maxSize; + if (-1 < lastTex && textures[lastTex].glHeight < maxSize) { + // The height of the current texture is less than the maximum, this + // means we can replace it with a larger texture. + h = PApplet.min(2 * textures[lastTex].glHeight, maxSize); + resize = true; + } else { + h = minSize; + resize = false; + } + + Texture tex; + if (is3D) { + // Bilinear sampling ensures that the texture doesn't look pixelated + // either when it is magnified or minified... + tex = new Texture(pg, w, h, + new Texture.Parameters(ARGB, Texture.BILINEAR, false)); + } else { + // ...however, the effect of bilinear sampling is to add some blurriness + // to the text in its original size. In 2D, we assume that text will be + // shown at its original size, so linear sampling is chosen instead (which + // only affects minimized text). + tex = new Texture(pg, w, h, + new Texture.Parameters(ARGB, Texture.LINEAR, false)); + } + + if (textures == null) { + textures = new Texture[1]; + textures[0] = tex; + images = new PImage[1]; + images[0] = pg.wrapTexture(tex); + lastTex = 0; + } else if (resize) { + // Replacing old smaller texture with larger one. + // But first we must copy the contents of the older + // texture into the new one. + Texture tex0 = textures[lastTex]; + tex.put(tex0); + textures[lastTex] = tex; + + pg.setCache(images[lastTex], tex); + images[lastTex].width = tex.width; + images[lastTex].height = tex.height; + } else { + // Adding new texture to the list. + lastTex = textures.length; + Texture[] tempTex = new Texture[lastTex + 1]; + PApplet.arrayCopy(textures, tempTex, textures.length); + tempTex[lastTex] = tex; + textures = tempTex; + + PImage[] tempImg = new PImage[textures.length]; + PApplet.arrayCopy(images, tempImg, images.length); + tempImg[lastTex] = pg.wrapTexture(tex); + images = tempImg; + } + + // Make sure that the current texture is bound. + tex.bind(); + + return resize; + } + + + public void begin() { + } + + + public void end() { + for (int i = 0; i < textures.length; i++) { + pgl.disableTexturing(textures[i].glTarget); + } + } + + + public PImage getTexture(TextureInfo info) { + return images[info.texIndex]; + } + + + // Add all the current glyphs to opengl texture. + public void addAllGlyphsToTexture(PGraphicsOpenGL pg, PFont font) { + // loop over current glyphs. + for (int i = 0; i < font.getGlyphCount(); i++) { + addToTexture(pg, i, font.getGlyph(i)); + } + } + + + public void updateGlyphsTexCoords() { + // loop over current glyphs. + for (int i = 0; i < glyphTexinfos.length; i++) { + TextureInfo tinfo = glyphTexinfos[i]; + if (tinfo != null && tinfo.texIndex == lastTex) { + tinfo.updateUV(); + } + } + } + + + public TextureInfo getTexInfo(PFont.Glyph glyph) { + TextureInfo info = texinfoMap.get(glyph); + return info; + } + + + public TextureInfo addToTexture(PGraphicsOpenGL pg, PFont.Glyph glyph) { + int n = glyphTexinfos.length; + if (n == 0) { + glyphTexinfos = new TextureInfo[1]; + } + addToTexture(pg, n, glyph); + return glyphTexinfos[n]; + } + + + public boolean contextIsOutdated() { + boolean outdated = false; + for (int i = 0; i < textures.length; i++) { + if (textures[i].contextIsOutdated()) { + outdated = true; + } + } + if (outdated) { + for (int i = 0; i < textures.length; i++) { + textures[i].dispose(); + } + } + return outdated; + } + +// public void draw() { +// Texture tex = textures[lastTex]; +// pgl.drawTexture(tex.glTarget, tex.glName, +// tex.glWidth, tex.glHeight, +// 0, 0, tex.glWidth, tex.glHeight); +// } + + + // Adds this glyph to the opengl texture in PFont. + protected void addToTexture(PGraphicsOpenGL pg, int idx, PFont.Glyph glyph) { + // We add one pixel to avoid issues when sampling the font texture at + // fractional screen positions. I.e.: the pixel on the screen only contains + // half of the font rectangle, so it would sample half of the color from the + // glyph area in the texture, and the other half from the contiguous pixel. + // If the later contains a portion of the neighbor glyph and the former + // doesn't, this would result in a shaded pixel when the correct output is + // blank. This is a consequence of putting all the glyphs in a common + // texture with bilinear sampling. + int w = 1 + glyph.width + 1; + int h = 1 + glyph.height + 1; + + // Converting the pixels array from the PImage into a valid RGBA array for + // OpenGL. + int[] rgba = new int[w * h]; + int t = 0; + int p = 0; + if (PGL.BIG_ENDIAN) { + java.util.Arrays.fill(rgba, 0, w, 0xFFFFFF00); // Set the first row to blank pixels. + t = w; + for (int y = 0; y < glyph.height; y++) { + rgba[t++] = 0xFFFFFF00; // Set the leftmost pixel in this row as blank + for (int x = 0; x < glyph.width; x++) { + rgba[t++] = 0xFFFFFF00 | glyph.image.pixels[p++]; + } + rgba[t++] = 0xFFFFFF00; // Set the rightmost pixel in this row as blank + } + java.util.Arrays.fill(rgba, (h - 1) * w, h * w, 0xFFFFFF00); // Set the last row to blank pixels. + } else { + java.util.Arrays.fill(rgba, 0, w, 0x00FFFFFF); // Set the first row to blank pixels. + t = w; + for (int y = 0; y < glyph.height; y++) { + rgba[t++] = 0x00FFFFFF; // Set the leftmost pixel in this row as blank + for (int x = 0; x < glyph.width; x++) { + rgba[t++] = (glyph.image.pixels[p++] << 24) | 0x00FFFFFF; + } + rgba[t++] = 0x00FFFFFF; // Set the rightmost pixel in this row as blank + } + java.util.Arrays.fill(rgba, (h - 1) * w, h * w, 0x00FFFFFF); // Set the last row to blank pixels. + } + + // Is there room for this glyph in the current line? + if (offsetX + w > textures[lastTex].glWidth) { + // No room, go to the next line: + offsetX = 0; + offsetY += lineHeight; + } + lineHeight = Math.max(lineHeight, h); + + boolean resized = false; + if (offsetY + lineHeight > textures[lastTex].glHeight) { + // We run out of space in the current texture, so we add a new texture: + resized = addTexture(pg); + if (resized) { + // Because the current texture has been resized, we need to + // update the UV coordinates of all the glyphs associated to it: + updateGlyphsTexCoords(); + } else { + // A new texture has been created. Reseting texture coordinates + // and line. + offsetX = 0; + offsetY = 0; + lineHeight = 0; + } + } + + TextureInfo tinfo = new TextureInfo(lastTex, offsetX, offsetY, w, h, rgba); + offsetX += w; + + if (idx == glyphTexinfos.length) { + TextureInfo[] temp = new TextureInfo[glyphTexinfos.length + 1]; + System.arraycopy(glyphTexinfos, 0, temp, 0, glyphTexinfos.length); + glyphTexinfos = temp; + } + + glyphTexinfos[idx] = tinfo; + texinfoMap.put(glyph, tinfo); + } + + + class TextureInfo { + int texIndex; + int width; + int height; + int[] crop; + float u0, u1; + float v0, v1; + int[] pixels; + + TextureInfo(int tidx, int cropX, int cropY, int cropW, int cropH, + int[] pix) { + texIndex = tidx; + crop = new int[4]; + // The region of the texture corresponding to the glyph is surrounded by a + // 1-pixel wide border to avoid artifacts due to bilinear sampling. This + // is why the additions and subtractions to the crop values. + crop[0] = cropX + 1; + crop[1] = cropY + 1 + cropH - 2; + crop[2] = cropW - 2; + crop[3] = -cropH + 2; + pixels = pix; + updateUV(); + updateTex(); + } + + + void updateUV() { + width = textures[texIndex].glWidth; + height = textures[texIndex].glHeight; + + u0 = (float)crop[0] / (float)width; + u1 = u0 + (float)crop[2] / (float)width; + v0 = (float)(crop[1] + crop[3]) / (float)height; + v1 = v0 - (float)crop[3] / (float)height; + } + + + void updateTex() { + textures[texIndex].setNative(pixels, crop[0] - 1, crop[1] + crop[3] - 1, + crop[2] + 2, -crop[3] + 2); + } + } +} diff --git a/src/main/java/processing/opengl/FrameBuffer.java b/src/main/java/processing/opengl/FrameBuffer.java new file mode 100644 index 0000000..e2992be --- /dev/null +++ b/src/main/java/processing/opengl/FrameBuffer.java @@ -0,0 +1,503 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.opengl; + +import processing.core.PApplet; +import processing.core.PConstants; +import processing.opengl.PGraphicsOpenGL.GLResourceFrameBuffer; + +import java.nio.IntBuffer; + +/** + * Encapsulates a Frame Buffer Object for offscreen rendering. + * When created with onscreen == true, it represents the normal + * framebuffer. Needed by the stack mechanism in OPENGL2 to return + * to onscreen rendering after a sequence of pushFramebuffer calls. + * It transparently handles the situations when the FBO extension is + * not available. + * + * By Andres Colubri. + */ + +public class FrameBuffer implements PConstants { + protected PGraphicsOpenGL pg; + protected PGL pgl; + protected int context; // The context that created this framebuffer. + + public int glFbo; + public int glDepth; + public int glStencil; + public int glDepthStencil; + public int glMultisample; + public int width; + public int height; + private GLResourceFrameBuffer glres; + + protected int depthBits; + protected int stencilBits; + protected boolean packedDepthStencil; + + protected boolean multisample; + protected int nsamples; + + protected int numColorBuffers; + protected Texture[] colorBufferTex; + + protected boolean screenFb; + protected boolean noDepth; + + protected IntBuffer pixelBuffer; + + + FrameBuffer(PGraphicsOpenGL pg) { + this.pg = pg; + pgl = pg.pgl; + context = pgl.createEmptyContext(); + } + + + FrameBuffer(PGraphicsOpenGL pg, int w, int h, int samples, int colorBuffers, + int depthBits, int stencilBits, boolean packedDepthStencil, + boolean screen) { + this(pg); + + glFbo = 0; + glDepth = 0; + glStencil = 0; + glDepthStencil = 0; + glMultisample = 0; + + if (screen) { + // If this framebuffer is used to represent a on-screen buffer, + // then it doesn't make it sense for it to have multisampling, + // color, depth or stencil buffers. + depthBits = stencilBits = samples = colorBuffers = 0; + } + + width = w; + height = h; + + if (1 < samples) { + multisample = true; + nsamples = samples; + } else { + multisample = false; + nsamples = 1; + } + + numColorBuffers = colorBuffers; + colorBufferTex = new Texture[numColorBuffers]; + for (int i = 0; i < numColorBuffers; i++) { + colorBufferTex[i] = null; + } + + if (depthBits < 1 && stencilBits < 1) { + this.depthBits = 0; + this.stencilBits = 0; + this.packedDepthStencil = false; + } else { + if (packedDepthStencil) { + // When combined depth/stencil format is required, the depth and stencil + // bits are overriden and the 24/8 combination for a 32 bits surface is + // used. + this.depthBits = 24; + this.stencilBits = 8; + this.packedDepthStencil = true; + } else { + this.depthBits = depthBits; + this.stencilBits = stencilBits; + this.packedDepthStencil = false; + } + } + + screenFb = screen; + + allocate(); + noDepth = false; + + pixelBuffer = null; + } + + + FrameBuffer(PGraphicsOpenGL pg, int w, int h) { + this(pg, w, h, 1, 1, 0, 0, false, false); + } + + + FrameBuffer(PGraphicsOpenGL pg, int w, int h, boolean screen) { + this(pg, w, h, 1, 1, 0, 0, false, screen); + } + + + public void clear() { + pg.pushFramebuffer(); + pg.setFramebuffer(this); + pgl.clearDepth(1); + pgl.clearStencil(0); + pgl.clearColor(0, 0, 0, 0); + pgl.clear(PGL.DEPTH_BUFFER_BIT | + PGL.STENCIL_BUFFER_BIT | + PGL.COLOR_BUFFER_BIT); + pg.popFramebuffer(); + } + + public void copyColor(FrameBuffer dest) { + copy(dest, PGL.COLOR_BUFFER_BIT); + } + + public void copyDepth(FrameBuffer dest) { + copy(dest, PGL.DEPTH_BUFFER_BIT); + } + + public void copyStencil(FrameBuffer dest) { + copy(dest, PGL.STENCIL_BUFFER_BIT); + } + + public void copy(FrameBuffer dest, int mask) { + pgl.bindFramebufferImpl(PGL.READ_FRAMEBUFFER, this.glFbo); + pgl.bindFramebufferImpl(PGL.DRAW_FRAMEBUFFER, dest.glFbo); + pgl.blitFramebuffer(0, 0, this.width, this.height, + 0, 0, dest.width, dest.height, mask, PGL.NEAREST); + pgl.bindFramebufferImpl(PGL.READ_FRAMEBUFFER, pg.getCurrentFB().glFbo); + pgl.bindFramebufferImpl(PGL.DRAW_FRAMEBUFFER, pg.getCurrentFB().glFbo); + } + + public void bind() { + pgl.bindFramebufferImpl(PGL.FRAMEBUFFER, glFbo); + } + + public void disableDepthTest() { + noDepth = true; + } + + public void finish() { + if (noDepth) { + // No need to clear depth buffer because depth testing was disabled. + if (pg.getHint(ENABLE_DEPTH_TEST)) { + pgl.enable(PGL.DEPTH_TEST); + } else { + pgl.disable(PGL.DEPTH_TEST); + } + } + } + + public void readPixels() { + if (pixelBuffer == null) createPixelBuffer(); + pixelBuffer.rewind(); + pgl.readPixels(0, 0, width, height, PGL.RGBA, PGL.UNSIGNED_BYTE, + pixelBuffer); + } + + public void getPixels(int[] pixels) { + if (pixelBuffer != null) { + pixelBuffer.get(pixels, 0, pixels.length); + pixelBuffer.rewind(); + } + } + + public IntBuffer getPixelBuffer() { + return pixelBuffer; + } + + public boolean hasDepthBuffer() { + return 0 < depthBits; + } + + public boolean hasStencilBuffer() { + return 0 < stencilBits; + } + + public void setFBO(int id) { + if (screenFb) { + glFbo = id; + } + } + + /////////////////////////////////////////////////////////// + + // Color buffer setters. + + + public void setColorBuffer(Texture tex) { + setColorBuffers(new Texture[] { tex }, 1); + } + + + public void setColorBuffers(Texture[] textures) { + setColorBuffers(textures, textures.length); + } + + + public void setColorBuffers(Texture[] textures, int n) { + if (screenFb) return; + + if (numColorBuffers != PApplet.min(n, textures.length)) { + throw new RuntimeException("Wrong number of textures to set the color " + + "buffers."); + } + + for (int i = 0; i < numColorBuffers; i++) { + colorBufferTex[i] = textures[i]; + } + + pg.pushFramebuffer(); + pg.setFramebuffer(this); + + // Making sure nothing is attached. + for (int i = 0; i < numColorBuffers; i++) { + pgl.framebufferTexture2D(PGL.FRAMEBUFFER, PGL.COLOR_ATTACHMENT0 + i, + PGL.TEXTURE_2D, 0, 0); + } + + for (int i = 0; i < numColorBuffers; i++) { + pgl.framebufferTexture2D(PGL.FRAMEBUFFER, PGL.COLOR_ATTACHMENT0 + i, + colorBufferTex[i].glTarget, + colorBufferTex[i].glName, 0); + } + + pgl.validateFramebuffer(); + + pg.popFramebuffer(); + } + + + public void swapColorBuffers() { + for (int i = 0; i < numColorBuffers - 1; i++) { + int i1 = (i + 1); + Texture tmp = colorBufferTex[i]; + colorBufferTex[i] = colorBufferTex[i1]; + colorBufferTex[i1] = tmp; + } + + pg.pushFramebuffer(); + pg.setFramebuffer(this); + for (int i = 0; i < numColorBuffers; i++) { + pgl.framebufferTexture2D(PGL.FRAMEBUFFER, PGL.COLOR_ATTACHMENT0 + i, + colorBufferTex[i].glTarget, + colorBufferTex[i].glName, 0); + } + pgl.validateFramebuffer(); + + pg.popFramebuffer(); + } + + + public int getDefaultReadBuffer() { + if (screenFb) { + return pgl.getDefaultReadBuffer(); + } else { + return PGL.COLOR_ATTACHMENT0; + } + } + + + public int getDefaultDrawBuffer() { + if (screenFb) { + return pgl.getDefaultDrawBuffer(); + } else { + return PGL.COLOR_ATTACHMENT0; + } + } + + + /////////////////////////////////////////////////////////// + + // Allocate/release framebuffer. + + + protected void allocate() { + dispose(); // Just in the case this object is being re-allocated. + + context = pgl.getCurrentContext(); + glres = new GLResourceFrameBuffer(this); // create the FBO resources... + + if (screenFb) { + glFbo = 0; + } else { + if (multisample) { + initColorBufferMultisample(); + } + + if (packedDepthStencil) { + initPackedDepthStencilBuffer(); + } else { + if (0 < depthBits) { + initDepthBuffer(); + } + if (0 < stencilBits) { + initStencilBuffer(); + } + } + } + } + + + protected void dispose() { + if (screenFb) return; + if (glres != null) { + glres.dispose(); + glFbo = 0; + glDepth = 0; + glStencil = 0; + glMultisample = 0; + glDepthStencil = 0; + glres = null; + } + } + + + protected boolean contextIsOutdated() { + if (screenFb) return false; + + boolean outdated = !pgl.contextIsCurrent(context); + if (outdated) { + dispose(); + for (int i = 0; i < numColorBuffers; i++) { + colorBufferTex[i] = null; + } + } + return outdated; + } + + + protected void initColorBufferMultisample() { + if (screenFb) return; + + pg.pushFramebuffer(); + pg.setFramebuffer(this); + + pgl.bindRenderbuffer(PGL.RENDERBUFFER, glMultisample); + pgl.renderbufferStorageMultisample(PGL.RENDERBUFFER, nsamples, + PGL.RGBA8, width, height); + pgl.framebufferRenderbuffer(PGL.FRAMEBUFFER, PGL.COLOR_ATTACHMENT0, + PGL.RENDERBUFFER, glMultisample); + + pg.popFramebuffer(); + } + + + protected void initPackedDepthStencilBuffer() { + if (screenFb) return; + + if (width == 0 || height == 0) { + throw new RuntimeException("PFramebuffer: size undefined."); + } + + pg.pushFramebuffer(); + pg.setFramebuffer(this); + + pgl.bindRenderbuffer(PGL.RENDERBUFFER, glDepthStencil); + + if (multisample) { + pgl.renderbufferStorageMultisample(PGL.RENDERBUFFER, nsamples, + PGL.DEPTH24_STENCIL8, width, height); + } else { + pgl.renderbufferStorage(PGL.RENDERBUFFER, PGL.DEPTH24_STENCIL8, + width, height); + } + + pgl.framebufferRenderbuffer(PGL.FRAMEBUFFER, PGL.DEPTH_ATTACHMENT, + PGL.RENDERBUFFER, glDepthStencil); + pgl.framebufferRenderbuffer(PGL.FRAMEBUFFER, PGL.STENCIL_ATTACHMENT, + PGL.RENDERBUFFER, glDepthStencil); + + pg.popFramebuffer(); + } + + + protected void initDepthBuffer() { + if (screenFb) return; + + if (width == 0 || height == 0) { + throw new RuntimeException("PFramebuffer: size undefined."); + } + + pg.pushFramebuffer(); + pg.setFramebuffer(this); + + pgl.bindRenderbuffer(PGL.RENDERBUFFER, glDepth); + + int glConst = PGL.DEPTH_COMPONENT16; + if (depthBits == 16) { + glConst = PGL.DEPTH_COMPONENT16; + } else if (depthBits == 24) { + glConst = PGL.DEPTH_COMPONENT24; + } else if (depthBits == 32) { + glConst = PGL.DEPTH_COMPONENT32; + } + + if (multisample) { + pgl.renderbufferStorageMultisample(PGL.RENDERBUFFER, nsamples, glConst, + width, height); + } else { + pgl.renderbufferStorage(PGL.RENDERBUFFER, glConst, width, height); + } + + pgl.framebufferRenderbuffer(PGL.FRAMEBUFFER, PGL.DEPTH_ATTACHMENT, + PGL.RENDERBUFFER, glDepth); + + pg.popFramebuffer(); + } + + + protected void initStencilBuffer() { + if (screenFb) return; + + if (width == 0 || height == 0) { + throw new RuntimeException("PFramebuffer: size undefined."); + } + + pg.pushFramebuffer(); + pg.setFramebuffer(this); + + pgl.bindRenderbuffer(PGL.RENDERBUFFER, glStencil); + + int glConst = PGL.STENCIL_INDEX1; + if (stencilBits == 1) { + glConst = PGL.STENCIL_INDEX1; + } else if (stencilBits == 4) { + glConst = PGL.STENCIL_INDEX4; + } else if (stencilBits == 8) { + glConst = PGL.STENCIL_INDEX8; + } + if (multisample) { + pgl.renderbufferStorageMultisample(PGL.RENDERBUFFER, nsamples, glConst, + width, height); + } else { + pgl.renderbufferStorage(PGL.RENDERBUFFER, glConst, width, height); + } + + pgl.framebufferRenderbuffer(PGL.FRAMEBUFFER, PGL.STENCIL_ATTACHMENT, + PGL.RENDERBUFFER, glStencil); + + pg.popFramebuffer(); + } + + + protected void createPixelBuffer() { + pixelBuffer = IntBuffer.allocate(width * height); + pixelBuffer.rewind(); + } +} diff --git a/src/main/java/processing/opengl/LinePath.java b/src/main/java/processing/opengl/LinePath.java new file mode 100644 index 0000000..a3705df --- /dev/null +++ b/src/main/java/processing/opengl/LinePath.java @@ -0,0 +1,623 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + * Copyright 2006 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package processing.opengl; + +import processing.core.PMatrix2D; + +/** + * The {@code LinePath} class allows to represent polygonal paths, + * potentially composed by several disjoint polygonal segments. + * It can be iterated by the {@link PathIterator} class including all + * of its segment types and winding rules + * + */ +public class LinePath { + /** + * The winding rule constant for specifying an even-odd rule + * for determining the interior of a path. + * The even-odd rule specifies that a point lies inside the + * path if a ray drawn in any direction from that point to + * infinity is crossed by path segments an odd number of times. + */ + public static final int WIND_EVEN_ODD = 0; + + /** + * The winding rule constant for specifying a non-zero rule + * for determining the interior of a path. + * The non-zero rule specifies that a point lies inside the + * path if a ray drawn in any direction from that point to + * infinity is crossed by path segments a different number + * of times in the counter-clockwise direction than the + * clockwise direction. + */ + public static final int WIND_NON_ZERO = 1; + + /** + * Starts segment at a given position. + */ + public static final byte SEG_MOVETO = 0; + + /** + * Extends segment by adding a line to a given position. + */ + public static final byte SEG_LINETO = 1; + + /** + * Closes segment at current position. + */ + public static final byte SEG_CLOSE = 2; + + /** + * Joins path segments by extending their outside edges until they meet. + */ + public final static int JOIN_MITER = 0; + + /** + * Joins path segments by rounding off the corner at a radius of half the line + * width. + */ + public final static int JOIN_ROUND = 1; + + /** + * Joins path segments by connecting the outer corners of their wide outlines + * with a straight segment. + */ + public final static int JOIN_BEVEL = 2; + + /** + * Ends unclosed subpaths and dash segments with no added decoration. + */ + public final static int CAP_BUTT = 0; + + /** + * Ends unclosed subpaths and dash segments with a round decoration that has a + * radius equal to half of the width of the pen. + */ + public final static int CAP_ROUND = 1; + + /** + * Ends unclosed subpaths and dash segments with a square projection that + * extends beyond the end of the segment to a distance equal to half of the + * line width. + */ + public final static int CAP_SQUARE = 2; + + private static PMatrix2D identity = new PMatrix2D(); + + private static float defaultMiterlimit = 10.0f; + + static final int INIT_SIZE = 20; + + static final int EXPAND_MAX = 500; + + protected byte[] pointTypes; + + protected float[] floatCoords; + + protected int[] pointColors; + + protected int numTypes; + + protected int numCoords; + + protected int windingRule; + + + /** + * Constructs a new empty single precision {@code LinePath} object with a + * default winding rule of {@link #WIND_NON_ZERO}. + */ + public LinePath() { + this(WIND_NON_ZERO, INIT_SIZE); + } + + + /** + * Constructs a new empty single precision {@code LinePath} object with the + * specified winding rule to control operations that require the interior of + * the path to be defined. + * + * @param rule + * the winding rule + * @see #WIND_EVEN_ODD + * @see #WIND_NON_ZERO + */ + public LinePath(int rule) { + this(rule, INIT_SIZE); + } + + + /** + * Constructs a new {@code LinePath} object from the given specified initial + * values. This method is only intended for internal use and should not be + * made public if the other constructors for this class are ever exposed. + * + * @param rule + * the winding rule + * @param initialCapacity + * the size to make the initial array to store the path segment types + */ + public LinePath(int rule, int initialCapacity) { + setWindingRule(rule); + this.pointTypes = new byte[initialCapacity]; + floatCoords = new float[initialCapacity * 2]; + pointColors = new int[initialCapacity]; + } + + + void needRoom(boolean needMove, int newPoints) { + if (needMove && numTypes == 0) { + throw new RuntimeException("missing initial moveto " + + "in path definition"); + } + int size = pointTypes.length; + if (numTypes >= size) { + int grow = size; + if (grow > EXPAND_MAX) { + grow = EXPAND_MAX; + } + pointTypes = copyOf(pointTypes, size + grow); + } + size = floatCoords.length; + if (numCoords + newPoints * 2 > size) { + int grow = size; + if (grow > EXPAND_MAX * 2) { + grow = EXPAND_MAX * 2; + } + if (grow < newPoints * 2) { + grow = newPoints * 2; + } + floatCoords = copyOf(floatCoords, size + grow); + } + size = pointColors.length; + if (numCoords/2 + newPoints > size) { + int grow = size; + if (grow > EXPAND_MAX) { + grow = EXPAND_MAX; + } + if (grow < newPoints) { + grow = newPoints; + } + pointColors = copyOf(pointColors, size + grow); + } + } + + + /** + * Adds a point to the path by moving to the specified coordinates specified + * in float precision. + *

        + * This method provides a single precision variant of the double precision + * {@code moveTo()} method on the base {@code LinePath} class. + * + * @param x + * the specified X coordinate + * @param y + * the specified Y coordinate + * @see LinePath#moveTo + */ + public final void moveTo(float x, float y, int c) { + if (numTypes > 0 && pointTypes[numTypes - 1] == SEG_MOVETO) { + floatCoords[numCoords - 2] = x; + floatCoords[numCoords - 1] = y; + pointColors[numCoords/2-1] = c; + } else { + needRoom(false, 1); + pointTypes[numTypes++] = SEG_MOVETO; + floatCoords[numCoords++] = x; + floatCoords[numCoords++] = y; + pointColors[numCoords/2-1] = c; + } + } + + + /** + * Adds a point to the path by drawing a straight line from the current + * coordinates to the new specified coordinates specified in float precision. + *

        + * This method provides a single precision variant of the double precision + * {@code lineTo()} method on the base {@code LinePath} class. + * + * @param x + * the specified X coordinate + * @param y + * the specified Y coordinate + * @see LinePath#lineTo + */ + public final void lineTo(float x, float y, int c) { + needRoom(true, 1); + pointTypes[numTypes++] = SEG_LINETO; + floatCoords[numCoords++] = x; + floatCoords[numCoords++] = y; + pointColors[numCoords/2-1] = c; + } + + + /** + * The iterator for this class is not multi-threaded safe, which means that + * the {@code LinePath} class does not guarantee that modifications to the + * geometry of this {@code LinePath} object do not affect any iterations of that + * geometry that are already in process. + */ + public PathIterator getPathIterator() { + return new PathIterator(this); + } + + + /** + * Closes the current subpath by drawing a straight line back to the + * coordinates of the last {@code moveTo}. If the path is already closed then + * this method has no effect. + */ + public final void closePath() { + if (numTypes == 0 || pointTypes[numTypes - 1] != SEG_CLOSE) { + needRoom(false, 0); + pointTypes[numTypes++] = SEG_CLOSE; + } + } + + + /** + * Returns the fill style winding rule. + * + * @return an integer representing the current winding rule. + * @see #WIND_EVEN_ODD + * @see #WIND_NON_ZERO + * @see #setWindingRule + */ + public final int getWindingRule() { + return windingRule; + } + + + /** + * Sets the winding rule for this path to the specified value. + * + * @param rule + * an integer representing the specified winding rule + * @exception IllegalArgumentException + * if {@code rule} is not either {@link #WIND_EVEN_ODD} or + * {@link #WIND_NON_ZERO} + * @see #getWindingRule + */ + public final void setWindingRule(int rule) { + if (rule != WIND_EVEN_ODD && rule != WIND_NON_ZERO) { + throw new IllegalArgumentException("winding rule must be " + + "WIND_EVEN_ODD or " + "WIND_NON_ZERO"); + } + windingRule = rule; + } + + + /** + * Resets the path to empty. The append position is set back to the beginning + * of the path and all coordinates and point types are forgotten. + */ + public final void reset() { + numTypes = numCoords = 0; + } + + + static public class PathIterator { + float floatCoords[]; + + int typeIdx; + + int pointIdx; + + int colorIdx; + + LinePath path; + + static final int curvecoords[] = { 2, 2, 0 }; + + PathIterator(LinePath p2df) { + this.path = p2df; + this.floatCoords = p2df.floatCoords; + pointIdx = 0; + colorIdx = 0; + } + + public int currentSegment(float[] coords) { + int type = path.pointTypes[typeIdx]; + int numCoords = curvecoords[type]; + if (numCoords > 0) { + System.arraycopy(floatCoords, pointIdx, coords, 0, numCoords); + int color = path.pointColors[colorIdx]; + coords[numCoords + 0] = (color >> 24) & 0xFF; + coords[numCoords + 1] = (color >> 16) & 0xFF; + coords[numCoords + 2] = (color >> 8) & 0xFF; + coords[numCoords + 3] = (color >> 0) & 0xFF; + } + return type; + } + + public int currentSegment(double[] coords) { + int type = path.pointTypes[typeIdx]; + int numCoords = curvecoords[type]; + if (numCoords > 0) { + for (int i = 0; i < numCoords; i++) { + coords[i] = floatCoords[pointIdx + i]; + } + int color = path.pointColors[colorIdx]; + coords[numCoords + 0] = (color >> 24) & 0xFF; + coords[numCoords + 1] = (color >> 16) & 0xFF; + coords[numCoords + 2] = (color >> 8) & 0xFF; + coords[numCoords + 3] = (color >> 0) & 0xFF; + } + return type; + } + + public int getWindingRule() { + return path.getWindingRule(); + } + + public boolean isDone() { + return (typeIdx >= path.numTypes); + } + + public void next() { + int type = path.pointTypes[typeIdx++]; + if (0 < curvecoords[type]) { + pointIdx += curvecoords[type]; + colorIdx++; + } + } + } + + + ///////////////////////////////////////////////////////////////////////////// + // + // Stroked path methods + + + static public LinePath createStrokedPath(LinePath src, float weight, + int caps, int join) { + return createStrokedPath(src, weight, caps, join, defaultMiterlimit, null); + } + + + static public LinePath createStrokedPath(LinePath src, float weight, + int caps, int join, float miterlimit) { + return createStrokedPath(src, weight, caps, join, miterlimit, null); + } + + + /** + * Constructs a solid LinePath with the specified attributes. + * + * @param src + * the original path to be stroked + * @param weight + * the weight of the stroked path + * @param caps + * the decoration of the ends of the segments in the path + * @param join + * the decoration applied where path segments meet + * @param miterlimit + * @param transform + * + */ + static public LinePath createStrokedPath(LinePath src, float weight, + int caps, int join, + float miterlimit, PMatrix2D transform) { + final LinePath dest = new LinePath(); + + strokeTo(src, weight, caps, join, miterlimit, transform, new LineStroker() { + @Override + public void moveTo(int x0, int y0, int c0) { + dest.moveTo(S15_16ToFloat(x0), S15_16ToFloat(y0), c0); + } + + @Override + public void lineJoin() { + } + + @Override + public void lineTo(int x1, int y1, int c1) { + dest.lineTo(S15_16ToFloat(x1), S15_16ToFloat(y1), c1); + } + + @Override + public void close() { + dest.closePath(); + } + + @Override + public void end() { + } + }); + + return dest; + } + + + private static void strokeTo(LinePath src, float width, int caps, int join, + float miterlimit, PMatrix2D transform, + LineStroker lsink) { + lsink = new LineStroker(lsink, FloatToS15_16(width), caps, join, + FloatToS15_16(miterlimit), + transform == null ? identity : transform); + + PathIterator pi = src.getPathIterator(); + pathTo(pi, lsink); + } + + + private static void pathTo(PathIterator pi, LineStroker lsink) { + float coords[] = new float[6]; + while (!pi.isDone()) { + int color; + switch (pi.currentSegment(coords)) { + case SEG_MOVETO: + color = ((int)coords[2]<<24) | + ((int)coords[3]<<16) | + ((int)coords[4]<< 8) | + (int)coords[5]; + lsink.moveTo(FloatToS15_16(coords[0]), FloatToS15_16(coords[1]), color); + break; + case SEG_LINETO: + color = ((int)coords[2]<<24) | + ((int)coords[3]<<16) | + ((int)coords[4]<< 8) | + (int)coords[5]; + lsink.lineJoin(); + lsink.lineTo(FloatToS15_16(coords[0]), FloatToS15_16(coords[1]), color); + break; + case SEG_CLOSE: + lsink.lineJoin(); + lsink.close(); + break; + default: + throw new InternalError("unknown flattened segment type"); + } + pi.next(); + } + lsink.end(); + } + + + ///////////////////////////////////////////////////////////////////////////// + // + // Utility methods + + + public static float[] copyOf(float[] source, int length) { + float[] target = new float[length]; + for (int i = 0; i < target.length; i++) { + if (i > source.length - 1) + target[i] = 0f; + else + target[i] = source[i]; + } + return target; + } + + + public static byte[] copyOf(byte[] source, int length) { + byte[] target = new byte[length]; + for (int i = 0; i < target.length; i++) { + if (i > source.length - 1) + target[i] = 0; + else + target[i] = source[i]; + } + return target; + } + + + public static int[] copyOf(int[] source, int length) { + int[] target = new int[length]; + for (int i = 0; i < target.length; i++) { + if (i > source.length - 1) + target[i] = 0; + else + target[i] = source[i]; + } + return target; + } + + + // From Ken Turkowski, _Fixed-Point Square Root_, In Graphics Gems V + public static int isqrt(int x) { + int fracbits = 16; + + int root = 0; + int remHi = 0; + int remLo = x; + int count = 15 + fracbits / 2; + + do { + remHi = (remHi << 2) | (remLo >>> 30); // N.B. - unsigned shift R + remLo <<= 2; + root <<= 1; + int testdiv = (root << 1) + 1; + if (remHi >= testdiv) { + remHi -= testdiv; + root++; + } + } while (count-- != 0); + + return root; + } + + + public static long lsqrt(long x) { + int fracbits = 16; + + long root = 0; + long remHi = 0; + long remLo = x; + int count = 31 + fracbits / 2; + + do { + remHi = (remHi << 2) | (remLo >>> 62); // N.B. - unsigned shift R + remLo <<= 2; + root <<= 1; + long testDiv = (root << 1) + 1; + if (remHi >= testDiv) { + remHi -= testDiv; + root++; + } + } while (count-- != 0); + + return root; + } + + + public static double hypot(double x, double y) { + return Math.sqrt(x * x + y * y); + } + + + public static int hypot(int x, int y) { + return (int) ((lsqrt((long) x * x + (long) y * y) + 128) >> 8); + } + + + public static long hypot(long x, long y) { + return (lsqrt(x * x + y * y) + 128) >> 8; + } + + + static int FloatToS15_16(float flt) { + flt = flt * 65536f + 0.5f; + if (flt <= -(65536f * 65536f)) { + return Integer.MIN_VALUE; + } else if (flt >= (65536f * 65536f)) { + return Integer.MAX_VALUE; + } else { + return (int) Math.floor(flt); + } + } + + + static float S15_16ToFloat(int fix) { + return (fix / 65536f); + } +} diff --git a/src/main/java/processing/opengl/LineStroker.java b/src/main/java/processing/opengl/LineStroker.java new file mode 100644 index 0000000..ee6b209 --- /dev/null +++ b/src/main/java/processing/opengl/LineStroker.java @@ -0,0 +1,685 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + * Copyright (c) 2007, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package processing.opengl; + +import processing.core.PMatrix2D; + +public class LineStroker { + private LineStroker output; + private int capStyle; + private int joinStyle; + private int m00, m01; + private int m10, m11; + private int lineWidth2; + private long scaledLineWidth2; + + // For any pen offset (pen_dx, pen_dy) that does not depend on + // the line orientation, the pen should be transformed so that: + // + // pen_dx' = m00*pen_dx + m01*pen_dy + // pen_dy' = m10*pen_dx + m11*pen_dy + // + // For a round pen, this means: + // + // pen_dx(r, theta) = r*cos(theta) + // pen_dy(r, theta) = r*sin(theta) + // + // pen_dx'(r, theta) = r*(m00*cos(theta) + m01*sin(theta)) + // pen_dy'(r, theta) = r*(m10*cos(theta) + m11*sin(theta)) + private int numPenSegments; + private int[] pen_dx; + private int[] pen_dy; + + private boolean[] penIncluded; + private int[] join; + private int[] offset = new int[2]; + private int[] reverse = new int[100]; + private int[] miter = new int[2]; + private long miterLimitSq; + private int prev; + private int rindex; + private boolean started; + private boolean lineToOrigin; + private boolean joinToOrigin; + private int sx0, sy0, sx1, sy1, x0, y0; + private int scolor0, pcolor0, color0; + private int mx0, my0, omx, omy; + private int px0, py0; + private double m00_2_m01_2; + private double m10_2_m11_2; + private double m00_m10_m01_m11; + + /** + * Empty constructor. setOutput and setParameters + * must be called prior to calling any other methods. + */ + public LineStroker() { + } + + /** + * Constructs a LineStroker. + * + * @param output + * an output LineStroker. + * @param lineWidth + * the desired line width in pixels, in S15.16 format. + * @param capStyle + * the desired end cap style, one of CAP_BUTT, + * CAP_ROUND or CAP_SQUARE. + * @param joinStyle + * the desired line join style, one of JOIN_MITER, + * JOIN_ROUND or JOIN_BEVEL. + * @param miterLimit + * the desired miter limit, in S15.16 format. + * @param transform + * a Transform4 object indicating the transform that has + * been previously applied to all incoming coordinates. This is + * required in order to produce consistently shaped end caps and + * joins. + */ + public LineStroker(LineStroker output, int lineWidth, int capStyle, int joinStyle, + int miterLimit, PMatrix2D transform) { + setOutput(output); + setParameters(lineWidth, capStyle, joinStyle, miterLimit, transform); + } + + /** + * Sets the output LineStroker of this LineStroker. + * + * @param output + * an output LineStroker. + */ + public void setOutput(LineStroker output) { + this.output = output; + } + + /** + * Sets the parameters of this LineStroker. + * + * @param lineWidth + * the desired line width in pixels, in S15.16 format. + * @param capStyle + * the desired end cap style, one of CAP_BUTT, + * CAP_ROUND or CAP_SQUARE. + * @param joinStyle + * the desired line join style, one of JOIN_MITER, + * JOIN_ROUND or JOIN_BEVEL. + * @param miterLimit + * the desired miter limit, in S15.16 format. + * @param transform + * a Transform4 object indicating the transform that has + * been previously applied to all incoming coordinates. This is + * required in order to produce consistently shaped end caps and + * joins. + */ + public void setParameters(int lineWidth, int capStyle, int joinStyle, + int miterLimit, PMatrix2D transform) { + this.m00 = LinePath.FloatToS15_16(transform.m00); + this.m01 = LinePath.FloatToS15_16(transform.m01); + this.m10 = LinePath.FloatToS15_16(transform.m10); + this.m11 = LinePath.FloatToS15_16(transform.m11); + + this.lineWidth2 = lineWidth >> 1; + this.scaledLineWidth2 = ((long) m00 * lineWidth2) >> 16; + this.capStyle = capStyle; + this.joinStyle = joinStyle; + + this.m00_2_m01_2 = (double) m00 * m00 + (double) m01 * m01; + this.m10_2_m11_2 = (double) m10 * m10 + (double) m11 * m11; + this.m00_m10_m01_m11 = (double) m00 * m10 + (double) m01 * m11; + + double dm00 = m00 / 65536.0; + double dm01 = m01 / 65536.0; + double dm10 = m10 / 65536.0; + double dm11 = m11 / 65536.0; + double determinant = dm00 * dm11 - dm01 * dm10; + + if (joinStyle == LinePath.JOIN_MITER) { + double limit = (miterLimit / 65536.0) * (lineWidth2 / 65536.0) + * determinant; + double limitSq = limit * limit; + this.miterLimitSq = (long) (limitSq * 65536.0 * 65536.0); + } + + this.numPenSegments = (int) (3.14159f * lineWidth / 65536.0f); + if (pen_dx == null || pen_dx.length < numPenSegments) { + this.pen_dx = new int[numPenSegments]; + this.pen_dy = new int[numPenSegments]; + this.penIncluded = new boolean[numPenSegments]; + this.join = new int[2 * numPenSegments]; + } + + for (int i = 0; i < numPenSegments; i++) { + double r = lineWidth / 2.0; + double theta = i * 2 * Math.PI / numPenSegments; + + double cos = Math.cos(theta); + double sin = Math.sin(theta); + pen_dx[i] = (int) (r * (dm00 * cos + dm01 * sin)); + pen_dy[i] = (int) (r * (dm10 * cos + dm11 * sin)); + } + + prev = LinePath.SEG_CLOSE; + rindex = 0; + started = false; + lineToOrigin = false; + } + + private void computeOffset(int x0, int y0, int x1, int y1, int[] m) { + long lx = (long) x1 - (long) x0; + long ly = (long) y1 - (long) y0; + + int dx, dy; + if (m00 > 0 && m00 == m11 && m01 == 0 & m10 == 0) { + long ilen = LinePath.hypot(lx, ly); + if (ilen == 0) { + dx = dy = 0; + } else { + dx = (int) ((ly * scaledLineWidth2) / ilen); + dy = (int) (-(lx * scaledLineWidth2) / ilen); + } + } else { + double dlx = x1 - x0; + double dly = y1 - y0; + double det = (double) m00 * m11 - (double) m01 * m10; + int sdet = (det > 0) ? 1 : -1; + double a = dly * m00 - dlx * m10; + double b = dly * m01 - dlx * m11; + double dh = LinePath.hypot(a, b); + double div = sdet * lineWidth2 / (65536.0 * dh); + double ddx = dly * m00_2_m01_2 - dlx * m00_m10_m01_m11; + double ddy = dly * m00_m10_m01_m11 - dlx * m10_2_m11_2; + dx = (int) (ddx * div); + dy = (int) (ddy * div); + } + + m[0] = dx; + m[1] = dy; + } + + private void ensureCapacity(int newrindex) { + if (reverse.length < newrindex) { + int[] tmp = new int[Math.max(newrindex, 6 * reverse.length / 5)]; + System.arraycopy(reverse, 0, tmp, 0, rindex); + this.reverse = tmp; + } + } + + private boolean isCCW(int x0, int y0, int x1, int y1, int x2, int y2) { + int dx0 = x1 - x0; + int dy0 = y1 - y0; + int dx1 = x2 - x1; + int dy1 = y2 - y1; + return (long) dx0 * dy1 < (long) dy0 * dx1; + } + + private boolean side(int x, int y, int x0, int y0, int x1, int y1) { + long lx = x; + long ly = y; + long lx0 = x0; + long ly0 = y0; + long lx1 = x1; + long ly1 = y1; + + return (ly0 - ly1) * lx + (lx1 - lx0) * ly + (lx0 * ly1 - lx1 * ly0) > 0; + } + + private int computeRoundJoin(int cx, int cy, int xa, int ya, int xb, int yb, + int side, boolean flip, int[] join) { + int px, py; + int ncoords = 0; + + boolean centerSide; + if (side == 0) { + centerSide = side(cx, cy, xa, ya, xb, yb); + } else { + centerSide = (side == 1) ? true : false; + } + for (int i = 0; i < numPenSegments; i++) { + px = cx + pen_dx[i]; + py = cy + pen_dy[i]; + + boolean penSide = side(px, py, xa, ya, xb, yb); + if (penSide != centerSide) { + penIncluded[i] = true; + } else { + penIncluded[i] = false; + } + } + + int start = -1, end = -1; + for (int i = 0; i < numPenSegments; i++) { + if (penIncluded[i] + && !penIncluded[(i + numPenSegments - 1) % numPenSegments]) { + start = i; + } + if (penIncluded[i] && !penIncluded[(i + 1) % numPenSegments]) { + end = i; + } + } + + if (end < start) { + end += numPenSegments; + } + + if (start != -1 && end != -1) { + long dxa = cx + pen_dx[start] - xa; + long dya = cy + pen_dy[start] - ya; + long dxb = cx + pen_dx[start] - xb; + long dyb = cy + pen_dy[start] - yb; + + boolean rev = (dxa * dxa + dya * dya > dxb * dxb + dyb * dyb); + int i = rev ? end : start; + int incr = rev ? -1 : 1; + while (true) { + int idx = i % numPenSegments; + px = cx + pen_dx[idx]; + py = cy + pen_dy[idx]; + join[ncoords++] = px; + join[ncoords++] = py; + if (i == (rev ? start : end)) { + break; + } + i += incr; + } + } + + return ncoords / 2; + } + + //private static final long ROUND_JOIN_THRESHOLD = 1000L; + private static final long ROUND_JOIN_THRESHOLD = 100000000L; + + private static final long ROUND_JOIN_INTERNAL_THRESHOLD = 1000000000L; + + private void drawRoundJoin(int x, int y, int omx, int omy, int mx, int my, + int side, int color, + boolean flip, boolean rev, long threshold) { + if ((omx == 0 && omy == 0) || (mx == 0 && my == 0)) { + return; + } + + long domx = (long) omx - mx; + long domy = (long) omy - my; + long len = domx * domx + domy * domy; + if (len < threshold) { + return; + } + + if (rev) { + omx = -omx; + omy = -omy; + mx = -mx; + my = -my; + } + + int bx0 = x + omx; + int by0 = y + omy; + int bx1 = x + mx; + int by1 = y + my; + + int npoints = computeRoundJoin(x, y, bx0, by0, bx1, by1, side, flip, join); + for (int i = 0; i < npoints; i++) { + emitLineTo(join[2 * i], join[2 * i + 1], color, rev); + } + } + + // Return the intersection point of the lines (ix0, iy0) -> (ix1, iy1) + // and (ix0p, iy0p) -> (ix1p, iy1p) in m[0] and m[1] + private void computeMiter(int ix0, int iy0, int ix1, int iy1, int ix0p, + int iy0p, int ix1p, int iy1p, int[] m) { + long x0 = ix0; + long y0 = iy0; + long x1 = ix1; + long y1 = iy1; + + long x0p = ix0p; + long y0p = iy0p; + long x1p = ix1p; + long y1p = iy1p; + + long x10 = x1 - x0; + long y10 = y1 - y0; + long x10p = x1p - x0p; + long y10p = y1p - y0p; + + long den = (x10 * y10p - x10p * y10) >> 16; + if (den == 0) { + m[0] = ix0; + m[1] = iy0; + return; + } + + long t = (x1p * (y0 - y0p) - x0 * y10p + x0p * (y1p - y0)) >> 16; + m[0] = (int) (x0 + (t * x10) / den); + m[1] = (int) (y0 + (t * y10) / den); + } + + private void drawMiter(int px0, int py0, int x0, int y0, int x1, int y1, + int omx, int omy, int mx, int my, int color, + boolean rev) { + if (mx == omx && my == omy) { + return; + } + if (px0 == x0 && py0 == y0) { + return; + } + if (x0 == x1 && y0 == y1) { + return; + } + + if (rev) { + omx = -omx; + omy = -omy; + mx = -mx; + my = -my; + } + + computeMiter(px0 + omx, py0 + omy, x0 + omx, y0 + omy, x0 + mx, y0 + my, x1 + + mx, y1 + my, miter); + + // Compute miter length in untransformed coordinates + long dx = (long) miter[0] - x0; + long dy = (long) miter[1] - y0; + long a = (dy * m00 - dx * m10) >> 16; + long b = (dy * m01 - dx * m11) >> 16; + long lenSq = a * a + b * b; + + if (lenSq < miterLimitSq) { + emitLineTo(miter[0], miter[1], color, rev); + } + } + + public void moveTo(int x0, int y0, int c0) { + // System.out.println("LineStroker.moveTo(" + x0/65536.0 + ", " + y0/65536.0 + ")"); + + if (lineToOrigin) { + // not closing the path, do the previous lineTo + lineToImpl(sx0, sy0, scolor0, joinToOrigin); + lineToOrigin = false; + } + + if (prev == LinePath.SEG_LINETO) { + finish(); + } + + this.sx0 = this.x0 = x0; + this.sy0 = this.y0 = y0; + this.scolor0 = this.color0 = c0; + this.rindex = 0; + this.started = false; + this.joinSegment = false; + this.prev = LinePath.SEG_MOVETO; + } + + boolean joinSegment = false; + + public void lineJoin() { + // System.out.println("LineStroker.lineJoin()"); + this.joinSegment = true; + } + + public void lineTo(int x1, int y1, int c1) { + // System.out.println("LineStroker.lineTo(" + x1/65536.0 + ", " + y1/65536.0 + ")"); + + if (lineToOrigin) { + if (x1 == sx0 && y1 == sy0) { + // staying in the starting point + return; + } + + // not closing the path, do the previous lineTo + lineToImpl(sx0, sy0, scolor0, joinToOrigin); + lineToOrigin = false; + } else if (x1 == x0 && y1 == y0) { + return; + } else if (x1 == sx0 && y1 == sy0) { + lineToOrigin = true; + joinToOrigin = joinSegment; + joinSegment = false; + return; + } + + lineToImpl(x1, y1, c1, joinSegment); + joinSegment = false; + } + + private void lineToImpl(int x1, int y1, int c1, boolean joinSegment) { + computeOffset(x0, y0, x1, y1, offset); + int mx = offset[0]; + int my = offset[1]; + + if (!started) { + emitMoveTo(x0 + mx, y0 + my, color0); + this.sx1 = x1; + this.sy1 = y1; + this.mx0 = mx; + this.my0 = my; + started = true; + } else { + boolean ccw = isCCW(px0, py0, x0, y0, x1, y1); + if (joinSegment) { + if (joinStyle == LinePath.JOIN_MITER) { + drawMiter(px0, py0, x0, y0, x1, y1, omx, omy, mx, my, color0, ccw); + } else if (joinStyle == LinePath.JOIN_ROUND) { + drawRoundJoin(x0, y0, omx, omy, mx, my, 0, color0, false, ccw, + ROUND_JOIN_THRESHOLD); + } + } else { + // Draw internal joins as round + drawRoundJoin(x0, y0, omx, omy, mx, my, 0, color0, false, ccw, + ROUND_JOIN_INTERNAL_THRESHOLD); + } + + emitLineTo(x0, y0, color0, !ccw); + } + + emitLineTo(x0 + mx, y0 + my, color0, false); + emitLineTo(x1 + mx, y1 + my, c1, false); + + emitLineTo(x0 - mx, y0 - my, color0, true); + emitLineTo(x1 - mx, y1 - my, c1, true); + + this.omx = mx; + this.omy = my; + this.px0 = x0; + this.py0 = y0; + this.pcolor0 = color0; + this.x0 = x1; + this.y0 = y1; + this.color0 = c1; + this.prev = LinePath.SEG_LINETO; + } + + public void close() { + if (lineToOrigin) { + // ignore the previous lineTo + lineToOrigin = false; + } + + if (!started) { + finish(); + return; + } + + computeOffset(x0, y0, sx0, sy0, offset); + int mx = offset[0]; + int my = offset[1]; + + // Draw penultimate join + boolean ccw = isCCW(px0, py0, x0, y0, sx0, sy0); + if (joinSegment) { + if (joinStyle == LinePath.JOIN_MITER) { + drawMiter(px0, py0, x0, y0, sx0, sy0, omx, omy, mx, my, pcolor0, ccw); + } else if (joinStyle == LinePath.JOIN_ROUND) { + drawRoundJoin(x0, y0, omx, omy, mx, my, 0, color0, false, ccw, + ROUND_JOIN_THRESHOLD); + } + } else { + // Draw internal joins as round + drawRoundJoin(x0, y0, omx, omy, mx, my, 0, color0, false, ccw, + ROUND_JOIN_INTERNAL_THRESHOLD); + } + + emitLineTo(x0 + mx, y0 + my, color0); + emitLineTo(sx0 + mx, sy0 + my, scolor0); + + ccw = isCCW(x0, y0, sx0, sy0, sx1, sy1); + + // Draw final join on the outside + if (!ccw) { + if (joinStyle == LinePath.JOIN_MITER) { + drawMiter(x0, y0, sx0, sy0, sx1, sy1, mx, my, mx0, my0, color0, false); + } else if (joinStyle == LinePath.JOIN_ROUND) { + drawRoundJoin(sx0, sy0, mx, my, mx0, my0, 0, scolor0, false, false, + ROUND_JOIN_THRESHOLD); + } + } + + emitLineTo(sx0 + mx0, sy0 + my0, scolor0); + emitLineTo(sx0 - mx0, sy0 - my0, scolor0); // same as reverse[0], reverse[1] + + // Draw final join on the inside + if (ccw) { + if (joinStyle == LinePath.JOIN_MITER) { + drawMiter(x0, y0, sx0, sy0, sx1, sy1, -mx, -my, -mx0, -my0, color0, + false); + } else if (joinStyle == LinePath.JOIN_ROUND) { + drawRoundJoin(sx0, sy0, -mx, -my, -mx0, -my0, 0, scolor0, true, false, + ROUND_JOIN_THRESHOLD); + } + } + + emitLineTo(sx0 - mx, sy0 - my, scolor0); + emitLineTo(x0 - mx, y0 - my, color0); + for (int i = rindex - 3; i >= 0; i -= 3) { + emitLineTo(reverse[i], reverse[i + 1], reverse[i + 2]); + } + + this.x0 = this.sx0; + this.y0 = this.sy0; + this.rindex = 0; + this.started = false; + this.joinSegment = false; + this.prev = LinePath.SEG_CLOSE; + emitClose(); + } + + public void end() { + if (lineToOrigin) { + // not closing the path, do the previous lineTo + lineToImpl(sx0, sy0, scolor0, joinToOrigin); + lineToOrigin = false; + } + + if (prev == LinePath.SEG_LINETO) { + finish(); + } + + output.end(); + this.joinSegment = false; + this.prev = LinePath.SEG_MOVETO; + } + + long lineLength(long ldx, long ldy) { + long ldet = ((long) m00 * m11 - (long) m01 * m10) >> 16; + long la = (ldy * m00 - ldx * m10) / ldet; + long lb = (ldy * m01 - ldx * m11) / ldet; + long llen = (int) LinePath.hypot(la, lb); + return llen; + } + + private void finish() { + if (capStyle == LinePath.CAP_ROUND) { + drawRoundJoin(x0, y0, omx, omy, -omx, -omy, 1, color0, false, false, + ROUND_JOIN_THRESHOLD); + } else if (capStyle == LinePath.CAP_SQUARE) { + long ldx = px0 - x0; + long ldy = py0 - y0; + long llen = lineLength(ldx, ldy); + if (0 < llen) { + long s = (long) lineWidth2 * 65536 / llen; + + int capx = x0 - (int) (ldx * s >> 16); + int capy = y0 - (int) (ldy * s >> 16); + + emitLineTo(capx + omx, capy + omy, color0); + emitLineTo(capx - omx, capy - omy, color0); + } + } + + for (int i = rindex - 3; i >= 0; i -= 3) { + emitLineTo(reverse[i], reverse[i + 1], reverse[i + 2]); + } + this.rindex = 0; + + if (capStyle == LinePath.CAP_ROUND) { + drawRoundJoin(sx0, sy0, -mx0, -my0, mx0, my0, 1, scolor0, false, false, + ROUND_JOIN_THRESHOLD); + } else if (capStyle == LinePath.CAP_SQUARE) { + long ldx = sx1 - sx0; + long ldy = sy1 - sy0; + long llen = lineLength(ldx, ldy); + if (0 < llen) { + long s = (long) lineWidth2 * 65536 / llen; + + int capx = sx0 - (int) (ldx * s >> 16); + int capy = sy0 - (int) (ldy * s >> 16); + + emitLineTo(capx - mx0, capy - my0, scolor0); + emitLineTo(capx + mx0, capy + my0, scolor0); + } + } + + emitClose(); + this.joinSegment = false; + } + + private void emitMoveTo(int x0, int y0, int c0) { + output.moveTo(x0, y0, c0); + } + + private void emitLineTo(int x1, int y1, int c1) { + output.lineTo(x1, y1, c1); + } + + private void emitLineTo(int x1, int y1, int c1, boolean rev) { + if (rev) { + ensureCapacity(rindex + 3); + reverse[rindex++] = x1; + reverse[rindex++] = y1; + reverse[rindex++] = c1; + } else { + emitLineTo(x1, y1, c1); + } + } + + private void emitClose() { + output.close(); + } +} diff --git a/src/main/java/processing/opengl/PGL.java b/src/main/java/processing/opengl/PGL.java new file mode 100644 index 0000000..73e5198 --- /dev/null +++ b/src/main/java/processing/opengl/PGL.java @@ -0,0 +1,3366 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.opengl; + +import java.io.IOException; +import java.net.URL; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.util.Arrays; +import java.util.regex.Pattern; + +import processing.core.PApplet; +import processing.core.PConstants; +import processing.core.PGraphics; + + +/** + * Processing-OpenGL abstraction layer. Needs to be implemented by subclasses + * using specific OpenGL-Java bindings. + * + * It includes a full GLES 2.0 interface. + * + */ +public abstract class PGL { + // ........................................................ + + // Basic fields + + /** The PGraphics and PApplet objects using this interface */ + protected PGraphicsOpenGL graphics; + protected PApplet sketch; + + /** OpenGL thread */ + protected Thread glThread; + + /** ID of the GL context associated to the surface **/ + protected int glContext; + + /** true if this is the GL interface for a primary surface PGraphics */ + public boolean primaryPGL; + + // ........................................................ + + // Parameters + + public static int REQUESTED_DEPTH_BITS = 24; + public static int REQUESTED_STENCIL_BITS = 8; + public static int REQUESTED_ALPHA_BITS = 8; + + /** Switches between the use of regular and direct buffers. */ + protected static boolean USE_DIRECT_BUFFERS = true; + protected static int MIN_DIRECT_BUFFER_SIZE = 1; + + /** Enables/disables mipmap use. */ + protected static boolean MIPMAPS_ENABLED = true; + + /** Initial sizes for arrays of input and tessellated data. */ + protected static int DEFAULT_IN_VERTICES = 64; + protected static int DEFAULT_IN_EDGES = 128; + protected static int DEFAULT_IN_TEXTURES = 64; + protected static int DEFAULT_TESS_VERTICES = 64; + protected static int DEFAULT_TESS_INDICES = 128; + + /** Maximum lights by default is 8, the minimum defined by OpenGL. */ + protected static int MAX_LIGHTS = 8; + + /** Maximum index value of a tessellated vertex. GLES restricts the vertex + * indices to be of type unsigned short. Since Java only supports signed + * shorts as primitive type we have 2^15 = 32768 as the maximum number of + * vertices that can be referred to within a single VBO. + */ + protected static int MAX_VERTEX_INDEX = 32767; + protected static int MAX_VERTEX_INDEX1 = MAX_VERTEX_INDEX + 1; + + /** Count of tessellated fill, line or point vertices that will + * trigger a flush in the immediate mode. It doesn't necessarily + * be equal to MAX_VERTEX_INDEX1, since the number of vertices can + * be effectively much large since the renderer uses offsets to + * refer to vertices beyond the MAX_VERTEX_INDEX limit. + */ + protected static int FLUSH_VERTEX_COUNT = MAX_VERTEX_INDEX1; + + /** Minimum/maximum dimensions of a texture used to hold font data. */ + protected static int MIN_FONT_TEX_SIZE = 256; + protected static int MAX_FONT_TEX_SIZE = 1024; + + /** Minimum stroke weight needed to apply the full path stroking + * algorithm that properly generates caps and joins. + */ + protected static float MIN_CAPS_JOINS_WEIGHT = 2f; + + /** Maximum length of linear paths to be stroked with the + * full algorithm that generates accurate caps and joins. + */ + protected static int MAX_CAPS_JOINS_LENGTH = 5000; + + /** Minimum array size to use arrayCopy method(). */ + protected static int MIN_ARRAYCOPY_SIZE = 2; + + /** Factor used to displace the stroke vertices towards the camera in + * order to make sure the lines are always on top of the fill geometry */ + protected static float STROKE_DISPLACEMENT = 0.999f; + + // ........................................................ + + // Variables to handle single-buffered situations (i.e.: Android) + + protected IntBuffer firstFrame; + protected static boolean SINGLE_BUFFERED = false; + + // ........................................................ + + // FBO layer + + protected boolean fboLayerEnabled = false; + protected boolean fboLayerCreated = false; + protected boolean fboLayerEnabledReq = false; + protected boolean fboLayerDisableReq = false; + protected boolean fbolayerResetReq = false; + public int reqNumSamples; + protected int numSamples; + + protected IntBuffer glColorFbo; + protected IntBuffer glColorTex; + protected IntBuffer glDepthStencil; + protected IntBuffer glDepth; + protected IntBuffer glStencil; + + protected IntBuffer glMultiFbo; + protected IntBuffer glMultiColor; + protected IntBuffer glMultiDepthStencil; + protected IntBuffer glMultiDepth; + protected IntBuffer glMultiStencil; + + protected int fboWidth, fboHeight; + protected int backTex, frontTex; + + /** Flags used to handle the creation of a separate front texture */ + protected boolean usingFrontTex = false; + protected boolean needSepFrontTex = false; + + // ........................................................ + + // Texture rendering + + protected boolean loadedTex2DShader = false; + protected int tex2DShaderProgram; + protected int tex2DVertShader; + protected int tex2DFragShader; + protected int tex2DShaderContext; + protected int tex2DVertLoc; + protected int tex2DTCoordLoc; + protected int tex2DSamplerLoc; + protected int tex2DGeoVBO; + + protected boolean loadedTexRectShader = false; + protected int texRectShaderProgram; + protected int texRectVertShader; + protected int texRectFragShader; + protected int texRectShaderContext; + protected int texRectVertLoc; + protected int texRectTCoordLoc; + protected int texRectSamplerLoc; + protected int texRectGeoVBO; + + protected float[] texCoords = { + // X, Y, U, V + -1.0f, -1.0f, 0.0f, 0.0f, + +1.0f, -1.0f, 1.0f, 0.0f, + -1.0f, +1.0f, 0.0f, 1.0f, + +1.0f, +1.0f, 1.0f, 1.0f + }; + protected FloatBuffer texData; + + protected static final String SHADER_PREPROCESSOR_DIRECTIVE = + "#ifdef GL_ES\n" + + "precision mediump float;\n" + + "precision mediump int;\n" + + "#endif\n"; + + protected static String[] texVertShaderSource = { + "attribute vec2 position;", + "attribute vec2 texCoord;", + "varying vec2 vertTexCoord;", + "void main() {", + " gl_Position = vec4(position, 0, 1);", + " vertTexCoord = texCoord;", + "}" + }; + + protected static String[] tex2DFragShaderSource = { + SHADER_PREPROCESSOR_DIRECTIVE, + "uniform sampler2D texMap;", + "varying vec2 vertTexCoord;", + "void main() {", + " gl_FragColor = texture2D(texMap, vertTexCoord.st);", + "}" + }; + + protected static String[] texRectFragShaderSource = { + SHADER_PREPROCESSOR_DIRECTIVE, + "uniform sampler2DRect texMap;", + "varying vec2 vertTexCoord;", + "void main() {", + " gl_FragColor = texture2DRect(texMap, vertTexCoord.st);", + "}" + }; + + /** Which texturing targets are enabled */ + protected boolean[] texturingTargets = { false, false }; + + /** Used to keep track of which textures are bound to each target */ + protected int maxTexUnits; + protected int activeTexUnit = 0; + protected int[][] boundTextures; + + // ........................................................ + + // Framerate handling + + protected float targetFps = 60; + protected float currentFps = 60; + protected boolean setFps = false; + + // ........................................................ + + // Utility buffers + + protected ByteBuffer byteBuffer; + protected IntBuffer intBuffer; + protected IntBuffer viewBuffer; + + protected IntBuffer colorBuffer; + protected FloatBuffer depthBuffer; + protected ByteBuffer stencilBuffer; + + //........................................................ + + // Rendering information + + /** Used to register amount of geometry rendered in each frame. */ + protected int geomCount = 0; + protected int pgeomCount; + + /** Used to register calls to background. */ + protected boolean clearColor = false; + protected boolean pclearColor; + + protected boolean clearDepth = false; + protected boolean pclearDepth; + + protected boolean clearStencil = false; + protected boolean pclearStencil; + + + // ........................................................ + + // Error messages + + public static final String WIKI = + " Read http://wiki.processing.org/w/OpenGL_Issues for help."; + + public static final String FRAMEBUFFER_ERROR = + "Framebuffer error (%1$s), rendering will probably not work as expected" + WIKI; + + public static final String MISSING_FBO_ERROR = + "Framebuffer objects are not supported by this hardware (or driver)" + WIKI; + + public static final String MISSING_GLSL_ERROR = + "GLSL shaders are not supported by this hardware (or driver)" + WIKI; + + public static final String MISSING_GLFUNC_ERROR = + "GL function %1$s is not available on this hardware (or driver)" + WIKI; + + public static final String UNSUPPORTED_GLPROF_ERROR = + "Unsupported OpenGL profile."; + + public static final String TEXUNIT_ERROR = + "Number of texture units not supported by this hardware (or driver)" + WIKI; + + public static final String NONPRIMARY_ERROR = + "The renderer is trying to call a PGL function that can only be called on a primary PGL. " + + "This is most likely due to a bug in the renderer's code, please report it with an " + + "issue on Processing's github page https://github.com/processing/processing/issues?state=open " + + "if using any of the built-in OpenGL renderers. If you are using a contributed " + + "library, contact the library's developers."; + + protected static final String DEPTH_READING_NOT_ENABLED_ERROR = + "Reading depth and stencil values from this multisampled buffer is not enabled. " + + "You can enable it by calling hint(ENABLE_DEPTH_READING) once. " + + "If your sketch becomes too slow, disable multisampling with noSmooth() instead."; + + // ........................................................ + + // Constants + + /** Size of different types in bytes */ + protected static int SIZEOF_SHORT = Short.SIZE / 8; + protected static int SIZEOF_INT = Integer.SIZE / 8; + protected static int SIZEOF_FLOAT = Float.SIZE / 8; + protected static int SIZEOF_BYTE = Byte.SIZE / 8; + protected static int SIZEOF_INDEX = SIZEOF_SHORT; + protected static int INDEX_TYPE = 0x1403; // GL_UNSIGNED_SHORT + + /** Machine Epsilon for float precision. */ + protected static float FLOAT_EPS = Float.MIN_VALUE; + // Calculation of the Machine Epsilon for float precision. From: + // http://en.wikipedia.org/wiki/Machine_epsilon#Approximation_using_Java + static { + float eps = 1.0f; + + do { + eps /= 2.0f; + } while ((float)(1.0 + (eps / 2.0)) != 1.0); + + FLOAT_EPS = eps; + } + + /** + * Set to true if the host system is big endian (PowerPC, MIPS, SPARC), false + * if little endian (x86 Intel for Mac or PC). + */ + protected static boolean BIG_ENDIAN = + ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN; + + // ........................................................ + + // Present mode + + // ........................................................ + + // Present mode + + protected boolean presentMode = false; + protected boolean showStopButton = true; + public float presentX; + public float presentY; + protected IntBuffer closeButtonTex; + protected int stopButtonColor; + protected int stopButtonWidth = 28; + protected int stopButtonHeight = 12; + protected int stopButtonX = 21; // The position of the close button is relative to the + protected int closeButtonY = 21; // lower left corner + protected static int[] closeButtonPix = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, -1, + -1, -1, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, + 0, 0, 0, -1, -1, 0, -1, -1, 0, 0, -1, -1, 0, -1, -1, 0, 0, -1, 0, 0, 0, 0, 0, + 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, + -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, + 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, + 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, -1, + -1, 0, 0, -1, -1, 0, -1, -1, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, -1, -1, -1, + 0, 0, 0, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0}; + + + /////////////////////////////////////////////////////////////// + + // Initialization, finalization + + + public PGL() { } + + + public PGL(PGraphicsOpenGL pg) { + this.graphics = pg; + if (glColorTex == null) { + glColorFbo = allocateIntBuffer(1); + glColorTex = allocateIntBuffer(2); + glDepthStencil = allocateIntBuffer(1); + glDepth = allocateIntBuffer(1); + glStencil = allocateIntBuffer(1); + + glMultiFbo = allocateIntBuffer(1); + glMultiColor = allocateIntBuffer(1); + glMultiDepthStencil = allocateIntBuffer(1); + glMultiDepth = allocateIntBuffer(1); + glMultiStencil = allocateIntBuffer(1); + } + + byteBuffer = allocateByteBuffer(1); + intBuffer = allocateIntBuffer(1); + viewBuffer = allocateIntBuffer(4); + } + + + public void dispose() { + destroyFBOLayer(); + } + + + public void setPrimary(boolean primary) { + primaryPGL = primary; + } + + + static public int smoothToSamples(int smooth) { + if (smooth == 0) { + // smooth(0) is noSmooth(), which is 1x sampling + return 1; + } else if (smooth == 1) { + // smooth(1) means "default smoothing", which is 2x for OpenGL + return 2; + } else { + // smooth(N) can be used for 4x, 8x, etc + return smooth; + } + } + + + abstract public Object getNative(); + + + abstract protected void setFrameRate(float fps); + + + abstract protected void initSurface(int antialias); + + + abstract protected void reinitSurface(); + + + abstract protected void registerListeners(); + + + protected int getReadFramebuffer() { + return fboLayerEnabled ? glColorFbo.get(0) : 0; + } + + + protected int getDrawFramebuffer() { + if (fboLayerEnabled) return 1 < numSamples ? glMultiFbo.get(0) : + glColorFbo.get(0); + else return 0; + } + + + protected int getDefaultDrawBuffer() { + return fboLayerEnabled ? COLOR_ATTACHMENT0 : BACK; + } + + + protected int getDefaultReadBuffer() { + return fboLayerEnabled ? COLOR_ATTACHMENT0 : FRONT; + } + + + protected boolean isFBOBacked() {; + return fboLayerEnabled; + } + + + @Deprecated + public void requestFBOLayer() { + enableFBOLayer(); + } + + + public void enableFBOLayer() { + fboLayerEnabledReq = true; + } + + + public void disableFBOLayer() { + fboLayerDisableReq = true; + } + + + public void resetFBOLayer() { + fbolayerResetReq = true; + } + + + protected boolean isMultisampled() { + return 1 < numSamples; + } + + + abstract protected int getDepthBits(); + + + abstract protected int getStencilBits(); + + + protected boolean getDepthTest() { + intBuffer.rewind(); + getBooleanv(DEPTH_TEST, intBuffer); + return intBuffer.get(0) == 0 ? false : true; + } + + + protected boolean getDepthWriteMask() { + intBuffer.rewind(); + getBooleanv(DEPTH_WRITEMASK, intBuffer); + return intBuffer.get(0) == 0 ? false : true; + } + + + protected Texture wrapBackTexture(Texture texture) { + if (texture == null) { + texture = new Texture(graphics); + texture.init(graphics.width, graphics.height, + glColorTex.get(backTex), TEXTURE_2D, RGBA, + fboWidth, fboHeight, NEAREST, NEAREST, + CLAMP_TO_EDGE, CLAMP_TO_EDGE); + texture.invertedY(true); + texture.colorBuffer(true); + graphics.setCache(graphics, texture); + } else { + texture.glName = glColorTex.get(backTex); + } + return texture; + } + + + protected Texture wrapFrontTexture(Texture texture) { + if (texture == null) { + texture = new Texture(graphics); + texture.init(graphics.width, graphics.height, + glColorTex.get(frontTex), TEXTURE_2D, RGBA, + fboWidth, fboHeight, NEAREST, NEAREST, + CLAMP_TO_EDGE, CLAMP_TO_EDGE); + texture.invertedY(true); + texture.colorBuffer(true); + } else { + texture.glName = glColorTex.get(frontTex); + } + return texture; + } + + + protected void bindFrontTexture() { + usingFrontTex = true; + if (!texturingIsEnabled(TEXTURE_2D)) { + enableTexturing(TEXTURE_2D); + } + bindTexture(TEXTURE_2D, glColorTex.get(frontTex)); + } + + + protected void unbindFrontTexture() { + if (textureIsBound(TEXTURE_2D, glColorTex.get(frontTex))) { + // We don't want to unbind another texture + // that might be bound instead of this one. + if (!texturingIsEnabled(TEXTURE_2D)) { + enableTexturing(TEXTURE_2D); + bindTexture(TEXTURE_2D, 0); + disableTexturing(TEXTURE_2D); + } else { + bindTexture(TEXTURE_2D, 0); + } + } + } + + + protected void syncBackTexture() { + if (usingFrontTex) needSepFrontTex = true; + if (1 < numSamples) { + bindFramebufferImpl(READ_FRAMEBUFFER, glMultiFbo.get(0)); + bindFramebufferImpl(DRAW_FRAMEBUFFER, glColorFbo.get(0)); + int mask = COLOR_BUFFER_BIT; + if (graphics.getHint(PConstants.ENABLE_BUFFER_READING)) { + mask |= DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT; + } + blitFramebuffer(0, 0, fboWidth, fboHeight, + 0, 0, fboWidth, fboHeight, + mask, NEAREST); + } + } + + + abstract protected float getPixelScale(); + + /////////////////////////////////////////////////////////// + + // Present mode + + + public void initPresentMode(float x, float y, int stopColor) { + presentMode = true; + showStopButton = stopColor != 0; + stopButtonColor = stopColor; + presentX = x; + presentY = y; + enableFBOLayer(); + } + + + public boolean presentMode() { + return presentMode; + } + + + public float presentX() { + return presentX; + } + + + public float presentY() { + return presentY; + } + + + public boolean insideStopButton(float x, float y) { + if (!showStopButton) return false; + return stopButtonX < x && x < stopButtonX + stopButtonWidth && + -(closeButtonY + stopButtonHeight) < y && y < -closeButtonY; + } + + + /////////////////////////////////////////////////////////// + + // Frame rendering + + + protected void clearDepthStencil() { + if (!pclearDepth && !pclearStencil) { + depthMask(true); + clearDepth(1); + clearStencil(0); + clear(DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT); + } else if (!pclearDepth) { + depthMask(true); + clearDepth(1); + clear(DEPTH_BUFFER_BIT); + } else if (!pclearStencil) { + clearStencil(0); + clear(STENCIL_BUFFER_BIT); + } + } + + + protected void clearBackground(float r, float g, float b, float a, + boolean depth, boolean stencil) { + clearColor(r, g, b, a); + if (depth && stencil) { + clearDepth(1); + clearStencil(0); + clear(DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT | COLOR_BUFFER_BIT); + if (0 < sketch.frameCount) { + clearDepth = true; + clearStencil = true; + } + } else if (depth) { + clearDepth(1); + clear(DEPTH_BUFFER_BIT | COLOR_BUFFER_BIT); + if (0 < sketch.frameCount) { + clearDepth = true; + } + } else if (stencil) { + clearStencil(0); + clear(STENCIL_BUFFER_BIT | COLOR_BUFFER_BIT); + if (0 < sketch.frameCount) { + clearStencil = true; + } + } else { + clear(PGL.COLOR_BUFFER_BIT); + } + if (0 < sketch.frameCount) { + clearColor = true; + } + } + + + protected void beginRender() { + if (sketch == null) { + sketch = graphics.parent; + } + + pgeomCount = geomCount; + geomCount = 0; + + pclearColor = clearColor; + clearColor = false; + + pclearDepth = clearDepth; + clearDepth = false; + + pclearStencil = clearStencil; + clearStencil = false; + + if (SINGLE_BUFFERED && sketch.frameCount == 1) { + restoreFirstFrame(); + } + + if (fboLayerEnabledReq) { + fboLayerEnabled = true; + fboLayerEnabledReq = false; + } + + if (fboLayerEnabled) { + if (fbolayerResetReq) { + destroyFBOLayer(); + fbolayerResetReq = false; + } + if (!fboLayerCreated) { + createFBOLayer(); + } + + // Draw to the back texture + bindFramebufferImpl(FRAMEBUFFER, glColorFbo.get(0)); + framebufferTexture2D(FRAMEBUFFER, COLOR_ATTACHMENT0, + TEXTURE_2D, glColorTex.get(backTex), 0); + + if (1 < numSamples) { + bindFramebufferImpl(FRAMEBUFFER, glMultiFbo.get(0)); + } + + if (sketch.frameCount == 0) { + // No need to draw back color buffer because we are in the first frame. + int argb = graphics.backgroundColor; + float ba = ((argb >> 24) & 0xff) / 255.0f; + float br = ((argb >> 16) & 0xff) / 255.0f; + float bg = ((argb >> 8) & 0xff) / 255.0f; + float bb = ((argb) & 0xff) / 255.0f; + clearColor(br, bg, bb, ba); + clear(COLOR_BUFFER_BIT); + } else if (!pclearColor || !sketch.isLooping()) { + // Render previous back texture (now is the front) as background, + // because no background() is being used ("incremental drawing") + int x = 0; + int y = 0; + if (presentMode) { + x = (int)presentX; + y = (int)presentY; + } + float scale = getPixelScale(); + drawTexture(TEXTURE_2D, glColorTex.get(frontTex), fboWidth, fboHeight, + x, y, graphics.width, graphics.height, + 0, 0, (int)(scale * graphics.width), (int)(scale * graphics.height), + 0, 0, graphics.width, graphics.height); + } + } + } + + + protected void endRender(int windowColor) { + if (fboLayerEnabled) { + syncBackTexture(); + + // Draw the contents of the back texture to the screen framebuffer. + bindFramebufferImpl(FRAMEBUFFER, 0); + + if (presentMode) { + float wa = ((windowColor >> 24) & 0xff) / 255.0f; + float wr = ((windowColor >> 16) & 0xff) / 255.0f; + float wg = ((windowColor >> 8) & 0xff) / 255.0f; + float wb = (windowColor & 0xff) / 255.0f; + clearDepth(1); + clearColor(wr, wg, wb, wa); + clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT); + + if (showStopButton) { + if (closeButtonTex == null) { + closeButtonTex = allocateIntBuffer(1); + genTextures(1, closeButtonTex); + bindTexture(TEXTURE_2D, closeButtonTex.get(0)); + texParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, NEAREST); + texParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, NEAREST); + texParameteri(TEXTURE_2D, TEXTURE_WRAP_S, CLAMP_TO_EDGE); + texParameteri(TEXTURE_2D, TEXTURE_WRAP_T, CLAMP_TO_EDGE); + texImage2D(TEXTURE_2D, 0, RGBA, stopButtonWidth, stopButtonHeight, 0, RGBA, UNSIGNED_BYTE, null); + + int[] color = new int[closeButtonPix.length]; + PApplet.arrayCopy(closeButtonPix, color); + + + // Multiply the texture by the button color + float ba = ((stopButtonColor >> 24) & 0xFF) / 255f; + float br = ((stopButtonColor >> 16) & 0xFF) / 255f; + float bg = ((stopButtonColor >> 8) & 0xFF) / 255f; + float bb = ((stopButtonColor >> 0) & 0xFF) / 255f; + for (int i = 0; i < color.length; i++) { + int c = closeButtonPix[i]; + int a = (int)(ba * ((c >> 24) & 0xFF)); + int r = (int)(br * ((c >> 16) & 0xFF)); + int g = (int)(bg * ((c >> 8) & 0xFF)); + int b = (int)(bb * ((c >> 0) & 0xFF)); + color[i] = javaToNativeARGB((a << 24) | (r << 16) | (g << 8) | b); + } + IntBuffer buf = allocateIntBuffer(color); + copyToTexture(TEXTURE_2D, RGBA, closeButtonTex.get(0), 0, 0, stopButtonWidth, stopButtonHeight, buf); + bindTexture(TEXTURE_2D, 0); + } + drawTexture(TEXTURE_2D, closeButtonTex.get(0), stopButtonWidth, stopButtonHeight, + 0, 0, stopButtonX + stopButtonWidth, closeButtonY + stopButtonHeight, + 0, stopButtonHeight, stopButtonWidth, 0, + stopButtonX, closeButtonY, stopButtonX + stopButtonWidth, closeButtonY + stopButtonHeight); + } + } else { + clearDepth(1); + clearColor(0, 0, 0, 0); + clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT); + } + + // Render current back texture to screen, without blending. + disable(BLEND); + int x = 0; + int y = 0; + if (presentMode) { + x = (int)presentX; + y = (int)presentY; + } + float scale = getPixelScale(); + drawTexture(TEXTURE_2D, glColorTex.get(backTex), + fboWidth, fboHeight, + x, y, graphics.width, graphics.height, + 0, 0, (int)(scale * graphics.width), (int)(scale * graphics.height), + 0, 0, graphics.width, graphics.height); + + // Swapping front and back textures. + int temp = frontTex; + frontTex = backTex; + backTex = temp; + + if (fboLayerDisableReq) { + fboLayerEnabled = false; + fboLayerDisableReq = false; + } + } else { + if (SINGLE_BUFFERED && sketch.frameCount == 0) { + saveFirstFrame(); + } + + if (!clearColor && 0 < sketch.frameCount || !sketch.isLooping()) { + enableFBOLayer(); + if (SINGLE_BUFFERED) { + createFBOLayer(); + } + } + } + } + + + protected abstract void getGL(PGL pgl); + + + protected abstract boolean canDraw(); + + + protected abstract void requestFocus(); + + + protected abstract void requestDraw(); + + + protected abstract void swapBuffers(); + + + public boolean threadIsCurrent() { + return Thread.currentThread() == glThread; + } + + + public void setThread(Thread thread) { + glThread = thread; + } + + + protected void beginGL() { } + + + protected void endGL() { } + + + private void createFBOLayer() { + float scale = getPixelScale(); + + if (hasNpotTexSupport()) { + fboWidth = (int)(scale * graphics.width); + fboHeight = (int)(scale * graphics.height); + } else { + fboWidth = nextPowerOfTwo((int)(scale * graphics.width)); + fboHeight = nextPowerOfTwo((int)(scale * graphics.height)); + } + + if (hasFboMultisampleSupport()) { + int maxs = maxSamples(); + numSamples = PApplet.min(reqNumSamples, maxs); + } else { + numSamples = 1; + } + boolean multisample = 1 < numSamples; + + boolean packed = hasPackedDepthStencilSupport(); + int depthBits = PApplet.min(REQUESTED_DEPTH_BITS, getDepthBits()); + int stencilBits = PApplet.min(REQUESTED_STENCIL_BITS, getStencilBits()); + + genTextures(2, glColorTex); + for (int i = 0; i < 2; i++) { + bindTexture(TEXTURE_2D, glColorTex.get(i)); + texParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, NEAREST); + texParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, NEAREST); + texParameteri(TEXTURE_2D, TEXTURE_WRAP_S, CLAMP_TO_EDGE); + texParameteri(TEXTURE_2D, TEXTURE_WRAP_T, CLAMP_TO_EDGE); + texImage2D(TEXTURE_2D, 0, RGBA, fboWidth, fboHeight, 0, + RGBA, UNSIGNED_BYTE, null); + initTexture(TEXTURE_2D, RGBA, fboWidth, fboHeight, graphics.backgroundColor); + } + bindTexture(TEXTURE_2D, 0); + + backTex = 0; + frontTex = 1; + + genFramebuffers(1, glColorFbo); + bindFramebufferImpl(FRAMEBUFFER, glColorFbo.get(0)); + framebufferTexture2D(FRAMEBUFFER, COLOR_ATTACHMENT0, TEXTURE_2D, + glColorTex.get(backTex), 0); + + if (!multisample || graphics.getHint(PConstants.ENABLE_BUFFER_READING)) { + // If not multisampled, this is the only depth and stencil buffer. + // If multisampled and depth reading enabled, these are going to + // hold downsampled depth and stencil buffers. + createDepthAndStencilBuffer(false, depthBits, stencilBits, packed); + } + + if (multisample) { + // Creating multisampled FBO + genFramebuffers(1, glMultiFbo); + bindFramebufferImpl(FRAMEBUFFER, glMultiFbo.get(0)); + + // color render buffer... + genRenderbuffers(1, glMultiColor); + bindRenderbuffer(RENDERBUFFER, glMultiColor.get(0)); + renderbufferStorageMultisample(RENDERBUFFER, numSamples, + RGBA8, fboWidth, fboHeight); + framebufferRenderbuffer(FRAMEBUFFER, COLOR_ATTACHMENT0, + RENDERBUFFER, glMultiColor.get(0)); + + // Creating multisampled depth and stencil buffers + createDepthAndStencilBuffer(true, depthBits, stencilBits, packed); + } + + int status = validateFramebuffer(); + + if (status == FRAMEBUFFER_INCOMPLETE_MULTISAMPLE && 1 < numSamples) { + System.err.println("Continuing with multisampling disabled"); + reqNumSamples = 1; + destroyFBOLayer(); + // try again + createFBOLayer(); + return; + } + + // Clear all buffers. + clearDepth(1); + clearStencil(0); + int argb = graphics.backgroundColor; + float ba = ((argb >> 24) & 0xff) / 255.0f; + float br = ((argb >> 16) & 0xff) / 255.0f; + float bg = ((argb >> 8) & 0xff) / 255.0f; + float bb = ((argb) & 0xff) / 255.0f; + clearColor(br, bg, bb, ba); + clear(DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT | COLOR_BUFFER_BIT); + + bindFramebufferImpl(FRAMEBUFFER, 0); + initFBOLayer(); + + fboLayerCreated = true; + } + + protected abstract void initFBOLayer(); + + + protected void saveFirstFrame() { + firstFrame = allocateDirectIntBuffer(graphics.width * graphics.height); + if (hasReadBuffer()) readBuffer(BACK); + readPixelsImpl(0, 0, graphics.width, graphics.height, RGBA, UNSIGNED_BYTE, firstFrame); + } + + + protected void restoreFirstFrame() { + if (firstFrame == null) return; + + IntBuffer tex = allocateIntBuffer(1); + genTextures(1, tex); + + int w, h; + float scale = getPixelScale(); + if (hasNpotTexSupport()) { + w = (int)(scale * graphics.width); + h = (int)(scale * graphics.height); + } else { + w = nextPowerOfTwo((int)(scale * graphics.width)); + h = nextPowerOfTwo((int)(scale * graphics.height)); + } + + bindTexture(TEXTURE_2D, tex.get(0)); + texParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, NEAREST); + texParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, NEAREST); + texParameteri(TEXTURE_2D, TEXTURE_WRAP_S, CLAMP_TO_EDGE); + texParameteri(TEXTURE_2D, TEXTURE_WRAP_T, CLAMP_TO_EDGE); + texImage2D(TEXTURE_2D, 0, RGBA, w, h, 0, RGBA, UNSIGNED_BYTE, null); + texSubImage2D(TEXTURE_2D, 0, 0, 0, graphics.width, graphics.height, RGBA, UNSIGNED_BYTE, firstFrame); + + drawTexture(TEXTURE_2D, tex.get(0), w, h, + 0, 0, graphics.width, graphics.height, + 0, 0, (int)(scale * graphics.width), (int)(scale * graphics.height), + 0, 0, graphics.width, graphics.height); + + deleteTextures(1, tex); + firstFrame.clear(); + firstFrame = null; + } + + protected void destroyFBOLayer() { + if (threadIsCurrent() && fboLayerCreated) { + deleteFramebuffers(1, glColorFbo); + deleteTextures(2, glColorTex); + deleteRenderbuffers(1, glDepthStencil); + deleteRenderbuffers(1, glDepth); + deleteRenderbuffers(1, glStencil); + + deleteFramebuffers(1, glMultiFbo); + deleteRenderbuffers(1, glMultiColor); + deleteRenderbuffers(1, glMultiDepthStencil); + deleteRenderbuffers(1, glMultiDepth); + deleteRenderbuffers(1, glMultiStencil); + } + fboLayerCreated = false; + } + + + private void createDepthAndStencilBuffer(boolean multisample, int depthBits, + int stencilBits, boolean packed) { + // Creating depth and stencil buffers + if (packed && depthBits == 24 && stencilBits == 8) { + // packed depth+stencil buffer + IntBuffer depthStencilBuf = + multisample ? glMultiDepthStencil : glDepthStencil; + genRenderbuffers(1, depthStencilBuf); + bindRenderbuffer(RENDERBUFFER, depthStencilBuf.get(0)); + if (multisample) { + renderbufferStorageMultisample(RENDERBUFFER, numSamples, + DEPTH24_STENCIL8, fboWidth, fboHeight); + } else { + renderbufferStorage(RENDERBUFFER, DEPTH24_STENCIL8, + fboWidth, fboHeight); + } + framebufferRenderbuffer(FRAMEBUFFER, DEPTH_ATTACHMENT, RENDERBUFFER, + depthStencilBuf.get(0)); + framebufferRenderbuffer(FRAMEBUFFER, STENCIL_ATTACHMENT, RENDERBUFFER, + depthStencilBuf.get(0)); + } else { + // separate depth and stencil buffers + if (0 < depthBits) { + int depthComponent = DEPTH_COMPONENT16; + if (depthBits == 32) { + depthComponent = DEPTH_COMPONENT32; + } else if (depthBits == 24) { + depthComponent = DEPTH_COMPONENT24; + } else if (depthBits == 16) { + depthComponent = DEPTH_COMPONENT16; + } + + IntBuffer depthBuf = multisample ? glMultiDepth : glDepth; + genRenderbuffers(1, depthBuf); + bindRenderbuffer(RENDERBUFFER, depthBuf.get(0)); + if (multisample) { + renderbufferStorageMultisample(RENDERBUFFER, numSamples, + depthComponent, fboWidth, fboHeight); + } else { + renderbufferStorage(RENDERBUFFER, depthComponent, + fboWidth, fboHeight); + } + framebufferRenderbuffer(FRAMEBUFFER, DEPTH_ATTACHMENT, + RENDERBUFFER, depthBuf.get(0)); + } + + if (0 < stencilBits) { + int stencilIndex = STENCIL_INDEX1; + if (stencilBits == 8) { + stencilIndex = STENCIL_INDEX8; + } else if (stencilBits == 4) { + stencilIndex = STENCIL_INDEX4; + } else if (stencilBits == 1) { + stencilIndex = STENCIL_INDEX1; + } + + IntBuffer stencilBuf = multisample ? glMultiStencil : glStencil; + genRenderbuffers(1, stencilBuf); + bindRenderbuffer(RENDERBUFFER, stencilBuf.get(0)); + if (multisample) { + renderbufferStorageMultisample(RENDERBUFFER, numSamples, + stencilIndex, fboWidth, fboHeight); + } else { + renderbufferStorage(RENDERBUFFER, stencilIndex, + fboWidth, fboHeight); + } + framebufferRenderbuffer(FRAMEBUFFER, STENCIL_ATTACHMENT, + RENDERBUFFER, stencilBuf.get(0)); + } + } + } + + + /////////////////////////////////////////////////////////// + + // Context interface + + + protected int createEmptyContext() { + return -1; + } + + + protected int getCurrentContext() { + return glContext; + } + + + /////////////////////////////////////////////////////////// + + // Utility functions + + + protected boolean contextIsCurrent(int other) { + return other == -1 || other == glContext; + } + + + protected void enableTexturing(int target) { + if (target == TEXTURE_2D) { + texturingTargets[0] = true; + } else if (target == TEXTURE_RECTANGLE) { + texturingTargets[1] = true; + } + } + + + protected void disableTexturing(int target) { + if (target == TEXTURE_2D) { + texturingTargets[0] = false; + } else if (target == TEXTURE_RECTANGLE) { + texturingTargets[1] = false; + } + } + + + protected boolean texturingIsEnabled(int target) { + if (target == TEXTURE_2D) { + return texturingTargets[0]; + } else if (target == TEXTURE_RECTANGLE) { + return texturingTargets[1]; + } else { + return false; + } + } + + + protected boolean textureIsBound(int target, int id) { + if (boundTextures == null) return false; + + if (target == TEXTURE_2D) { + return boundTextures[activeTexUnit][0] == id; + } else if (target == TEXTURE_RECTANGLE) { + return boundTextures[activeTexUnit][1] == id; + } else { + return false; + } + } + + + protected void initTexture(int target, int format, int width, int height) { + initTexture(target, format, width, height, 0); + } + + + protected void initTexture(int target, int format, int width, int height, + int initColor) { + int[] glcolor = new int[16 * 16]; + Arrays.fill(glcolor, javaToNativeARGB(initColor)); + IntBuffer texels = allocateDirectIntBuffer(16 * 16); + texels.put(glcolor); + texels.rewind(); + for (int y = 0; y < height; y += 16) { + int h = PApplet.min(16, height - y); + for (int x = 0; x < width; x += 16) { + int w = PApplet.min(16, width - x); + texSubImage2D(target, 0, x, y, w, h, format, UNSIGNED_BYTE, texels); + } + } + } + + + protected void copyToTexture(int target, int format, int id, int x, int y, + int w, int h, int[] buffer) { + copyToTexture(target, format, id, x, y, w, h, IntBuffer.wrap(buffer)); + + } + + protected void copyToTexture(int target, int format, int id, int x, int y, + int w, int h, IntBuffer buffer) { + activeTexture(TEXTURE0); + boolean enabledTex = false; + if (!texturingIsEnabled(target)) { + enableTexturing(target); + enabledTex = true; + } + bindTexture(target, id); + texSubImage2D(target, 0, x, y, w, h, format, UNSIGNED_BYTE, buffer); + bindTexture(target, 0); + if (enabledTex) { + disableTexturing(target); + } + } + + + /** + * Not an approved function, this will change or be removed in the future. + */ + public void drawTexture(int target, int id, int width, int height, + int X0, int Y0, int X1, int Y1) { + // If a texture is drawing on a viewport of the same size as its resolution, + // the pixel factor is 1:1, so we override the surface's pixel factor. + drawTexture(target, id, width, height, + 0, 0, width, height, 1, + X0, Y0, X1, Y1, + X0, Y0, X1, Y1); + } + + + /** + * Not an approved function, this will change or be removed in the future. + */ + public void drawTexture(int target, int id,int texW, int texH, + int viewX, int viewY, int viewW, int viewH, + int texX0, int texY0, int texX1, int texY1, + int scrX0, int scrY0, int scrX1, int scrY1) { + int viewF = (int)getPixelScale(); + drawTexture(target, id, texW, texH, + viewX, viewY, viewW, viewH, viewF, + texX0, texY0, texX1, texY1, + scrX0, scrY0, scrX1, scrY1); + } + + + public void drawTexture(int target, int id,int texW, int texH, + int viewX, int viewY, int viewW, int viewH, int viewF, + int texX0, int texY0, int texX1, int texY1, + int scrX0, int scrY0, int scrX1, int scrY1) { + if (target == TEXTURE_2D) { + drawTexture2D(id, texW, texH, + viewX, viewY, viewW, viewH, viewF, + texX0, texY0, texX1, texY1, + scrX0, scrY0, scrX1, scrY1); + } else if (target == TEXTURE_RECTANGLE) { + drawTextureRect(id, texW, texH, + viewX, viewY, viewW, viewH, viewF, + texX0, texY0, texX1, texY1, + scrX0, scrY0, scrX1, scrY1); + } + } + + + protected PGL initTex2DShader() { + PGL ppgl = primaryPGL ? this : graphics.getPrimaryPGL(); + + if (!ppgl.loadedTex2DShader || ppgl.tex2DShaderContext != ppgl.glContext) { + String[] preprocVertSrc = preprocessVertexSource(texVertShaderSource, getGLSLVersion()); + String vertSource = PApplet.join(preprocVertSrc, "\n"); + String[] preprocFragSrc = preprocessFragmentSource(tex2DFragShaderSource, getGLSLVersion()); + String fragSource = PApplet.join(preprocFragSrc, "\n"); + ppgl.tex2DVertShader = createShader(VERTEX_SHADER, vertSource); + ppgl.tex2DFragShader = createShader(FRAGMENT_SHADER, fragSource); + if (0 < ppgl.tex2DVertShader && 0 < ppgl.tex2DFragShader) { + ppgl.tex2DShaderProgram = createProgram(ppgl.tex2DVertShader, ppgl.tex2DFragShader); + } + if (0 < ppgl.tex2DShaderProgram) { + ppgl.tex2DVertLoc = getAttribLocation(ppgl.tex2DShaderProgram, "position"); + ppgl.tex2DTCoordLoc = getAttribLocation(ppgl.tex2DShaderProgram, "texCoord"); + ppgl.tex2DSamplerLoc = getUniformLocation(ppgl.tex2DShaderProgram, "texMap"); + } + ppgl.loadedTex2DShader = true; + ppgl.tex2DShaderContext = ppgl.glContext; + + genBuffers(1, intBuffer); + ppgl.tex2DGeoVBO = intBuffer.get(0); + bindBuffer(ARRAY_BUFFER, ppgl.tex2DGeoVBO); + bufferData(ARRAY_BUFFER, 16 * SIZEOF_FLOAT, null, STATIC_DRAW); + } + + if (texData == null) { + texData = allocateDirectFloatBuffer(texCoords.length); + } + + return ppgl; + } + + + protected void drawTexture2D(int id, int texW, int texH, + int viewX, int viewY, int viewW, int viewH, int viewF, + int texX0, int texY0, int texX1, int texY1, + int scrX0, int scrY0, int scrX1, int scrY1) { + PGL ppgl = initTex2DShader(); + + if (0 < ppgl.tex2DShaderProgram) { + // The texture overwrites anything drawn earlier. + boolean depthTest = getDepthTest(); + disable(DEPTH_TEST); + + // When drawing the texture we don't write to the + // depth mask, so the texture remains in the background + // and can be occluded by anything drawn later, even if + // if it is behind it. + boolean depthMask = getDepthWriteMask(); + depthMask(false); + + // Making sure that the viewport matches the provided screen dimensions + viewBuffer.rewind(); + getIntegerv(VIEWPORT, viewBuffer); + viewportImpl(viewF * viewX, viewF * viewY, viewF * viewW, viewF * viewH); + + useProgram(ppgl.tex2DShaderProgram); + + enableVertexAttribArray(ppgl.tex2DVertLoc); + enableVertexAttribArray(ppgl.tex2DTCoordLoc); + + // Vertex coordinates of the textured quad are specified + // in normalized screen space (-1, 1): + // Corner 1 + texCoords[ 0] = 2 * (float)scrX0 / viewW - 1; + texCoords[ 1] = 2 * (float)scrY0 / viewH - 1; + texCoords[ 2] = (float)texX0 / texW; + texCoords[ 3] = (float)texY0 / texH; + // Corner 2 + texCoords[ 4] = 2 * (float)scrX1 / viewW - 1; + texCoords[ 5] = 2 * (float)scrY0 / viewH - 1; + texCoords[ 6] = (float)texX1 / texW; + texCoords[ 7] = (float)texY0 / texH; + // Corner 3 + texCoords[ 8] = 2 * (float)scrX0 / viewW - 1; + texCoords[ 9] = 2 * (float)scrY1 / viewH - 1; + texCoords[10] = (float)texX0 / texW; + texCoords[11] = (float)texY1 / texH; + // Corner 4 + texCoords[12] = 2 * (float)scrX1 / viewW - 1; + texCoords[13] = 2 * (float)scrY1 / viewH - 1; + texCoords[14] = (float)texX1 / texW; + texCoords[15] = (float)texY1 / texH; + + texData.rewind(); + texData.put(texCoords); + + activeTexture(TEXTURE0); + boolean enabledTex = false; + if (!texturingIsEnabled(TEXTURE_2D)) { + enableTexturing(TEXTURE_2D); + enabledTex = true; + } + bindTexture(TEXTURE_2D, id); + uniform1i(ppgl.tex2DSamplerLoc, 0); + + texData.position(0); + bindBuffer(ARRAY_BUFFER, ppgl.tex2DGeoVBO); + bufferData(ARRAY_BUFFER, 16 * SIZEOF_FLOAT, texData, STATIC_DRAW); + + vertexAttribPointer(ppgl.tex2DVertLoc, 2, FLOAT, false, 4 * SIZEOF_FLOAT, 0); + vertexAttribPointer(ppgl.tex2DTCoordLoc, 2, FLOAT, false, 4 * SIZEOF_FLOAT, 2 * SIZEOF_FLOAT); + + drawArrays(TRIANGLE_STRIP, 0, 4); + + bindBuffer(ARRAY_BUFFER, 0); // Making sure that no VBO is bound at this point. + + bindTexture(TEXTURE_2D, 0); + if (enabledTex) { + disableTexturing(TEXTURE_2D); + } + + disableVertexAttribArray(ppgl.tex2DVertLoc); + disableVertexAttribArray(ppgl.tex2DTCoordLoc); + + useProgram(0); + + if (depthTest) { + enable(DEPTH_TEST); + } else { + disable(DEPTH_TEST); + } + depthMask(depthMask); + + viewportImpl(viewBuffer.get(0), viewBuffer.get(1), + viewBuffer.get(2), viewBuffer.get(3)); + } + } + + + protected PGL initTexRectShader() { + PGL ppgl = primaryPGL ? this : graphics.getPrimaryPGL(); + + if (!ppgl.loadedTexRectShader || ppgl.texRectShaderContext != ppgl.glContext) { + String[] preprocVertSrc = preprocessVertexSource(texVertShaderSource, getGLSLVersion()); + String vertSource = PApplet.join(preprocVertSrc, "\n"); + String[] preprocFragSrc = preprocessFragmentSource(texRectFragShaderSource, getGLSLVersion()); + String fragSource = PApplet.join(preprocFragSrc, "\n"); + ppgl.texRectVertShader = createShader(VERTEX_SHADER, vertSource); + ppgl.texRectFragShader = createShader(FRAGMENT_SHADER, fragSource); + if (0 < ppgl.texRectVertShader && 0 < ppgl.texRectFragShader) { + ppgl.texRectShaderProgram = createProgram(ppgl.texRectVertShader, + ppgl.texRectFragShader); + } + if (0 < ppgl.texRectShaderProgram) { + ppgl.texRectVertLoc = getAttribLocation(ppgl.texRectShaderProgram, "position"); + ppgl.texRectTCoordLoc = getAttribLocation(ppgl.texRectShaderProgram, "texCoord"); + ppgl.texRectSamplerLoc = getUniformLocation(ppgl.texRectShaderProgram, "texMap"); + } + ppgl.loadedTexRectShader = true; + ppgl.texRectShaderContext = ppgl.glContext; + + genBuffers(1, intBuffer); + ppgl.texRectGeoVBO = intBuffer.get(0); + bindBuffer(ARRAY_BUFFER, ppgl.texRectGeoVBO); + bufferData(ARRAY_BUFFER, 16 * SIZEOF_FLOAT, null, STATIC_DRAW); + } + + return ppgl; + } + + + protected void drawTextureRect(int id, int texW, int texH, + int viewX, int viewY, int viewW, int viewH, int viewF, + int texX0, int texY0, int texX1, int texY1, + int scrX0, int scrY0, int scrX1, int scrY1) { + PGL ppgl = initTexRectShader(); + + if (texData == null) { + texData = allocateDirectFloatBuffer(texCoords.length); + } + + if (0 < ppgl.texRectShaderProgram) { + // The texture overwrites anything drawn earlier. + boolean depthTest = getDepthTest(); + disable(DEPTH_TEST); + + // When drawing the texture we don't write to the + // depth mask, so the texture remains in the background + // and can be occluded by anything drawn later, even if + // if it is behind it. + boolean depthMask = getDepthWriteMask(); + depthMask(false); + + // Making sure that the viewport matches the provided screen dimensions + viewBuffer.rewind(); + getIntegerv(VIEWPORT, viewBuffer); + viewportImpl(viewF * viewX, viewF * viewY, viewF * viewW, viewF * viewH); + + useProgram(ppgl.texRectShaderProgram); + + enableVertexAttribArray(ppgl.texRectVertLoc); + enableVertexAttribArray(ppgl.texRectTCoordLoc); + + // Vertex coordinates of the textured quad are specified + // in normalized screen space (-1, 1): + // Corner 1 + texCoords[ 0] = 2 * (float)scrX0 / viewW - 1; + texCoords[ 1] = 2 * (float)scrY0 / viewH - 1; + texCoords[ 2] = texX0; + texCoords[ 3] = texY0; + // Corner 2 + texCoords[ 4] = 2 * (float)scrX1 / viewW - 1; + texCoords[ 5] = 2 * (float)scrY0 / viewH - 1; + texCoords[ 6] = texX1; + texCoords[ 7] = texY0; + // Corner 3 + texCoords[ 8] = 2 * (float)scrX0 / viewW - 1; + texCoords[ 9] = 2 * (float)scrY1 / viewH - 1; + texCoords[10] = texX0; + texCoords[11] = texY1; + // Corner 4 + texCoords[12] = 2 * (float)scrX1 / viewW - 1; + texCoords[13] = 2 * (float)scrY1 / viewH - 1; + texCoords[14] = texX1; + texCoords[15] = texY1; + + texData.rewind(); + texData.put(texCoords); + + activeTexture(TEXTURE0); + boolean enabledTex = false; + if (!texturingIsEnabled(TEXTURE_RECTANGLE)) { + enableTexturing(TEXTURE_RECTANGLE); + enabledTex = true; + } + bindTexture(TEXTURE_RECTANGLE, id); + uniform1i(ppgl.texRectSamplerLoc, 0); + + texData.position(0); + bindBuffer(ARRAY_BUFFER, ppgl.texRectGeoVBO); + bufferData(ARRAY_BUFFER, 16 * SIZEOF_FLOAT, texData, STATIC_DRAW); + + vertexAttribPointer(ppgl.texRectVertLoc, 2, FLOAT, false, 4 * SIZEOF_FLOAT, 0); + vertexAttribPointer(ppgl.texRectTCoordLoc, 2, FLOAT, false, 4 * SIZEOF_FLOAT, 2 * SIZEOF_FLOAT); + + drawArrays(TRIANGLE_STRIP, 0, 4); + + bindBuffer(ARRAY_BUFFER, 0); // Making sure that no VBO is bound at this point. + + bindTexture(TEXTURE_RECTANGLE, 0); + if (enabledTex) { + disableTexturing(TEXTURE_RECTANGLE); + } + + disableVertexAttribArray(ppgl.texRectVertLoc); + disableVertexAttribArray(ppgl.texRectTCoordLoc); + + useProgram(0); + + if (depthTest) { + enable(DEPTH_TEST); + } else { + disable(DEPTH_TEST); + } + depthMask(depthMask); + + viewportImpl(viewBuffer.get(0), viewBuffer.get(1), + viewBuffer.get(2), viewBuffer.get(3)); + } + } + + + protected int getColorValue(int scrX, int scrY) { + if (colorBuffer == null) { + colorBuffer = IntBuffer.allocate(1); + } + colorBuffer.rewind(); + readPixels(scrX, graphics.height - scrY - 1, 1, 1, RGBA, UNSIGNED_BYTE, + colorBuffer); + return colorBuffer.get(); + } + + + protected float getDepthValue(int scrX, int scrY) { + if (depthBuffer == null) { + depthBuffer = FloatBuffer.allocate(1); + } + depthBuffer.rewind(); + readPixels(scrX, graphics.height - scrY - 1, 1, 1, DEPTH_COMPONENT, FLOAT, + depthBuffer); + return depthBuffer.get(0); + } + + + protected byte getStencilValue(int scrX, int scrY) { + if (stencilBuffer == null) { + stencilBuffer = ByteBuffer.allocate(1); + } + stencilBuffer.rewind(); + readPixels(scrX, graphics.height - scrY - 1, 1, 1, STENCIL_INDEX, + UNSIGNED_BYTE, stencilBuffer); + return stencilBuffer.get(0); + } + + + protected static boolean isPowerOfTwo(int val) { + return (val & (val - 1)) == 0; + } + + + // bit shifting this might be more efficient + protected static int nextPowerOfTwo(int val) { + int ret = 1; + while (ret < val) ret <<= 1; + return ret; + } + + + /** + * Converts input native OpenGL value (RGBA on big endian, ABGR on little + * endian) to Java ARGB. + */ + protected static int nativeToJavaARGB(int color) { + if (BIG_ENDIAN) { // RGBA to ARGB + return (color >>> 8) | (color << 24); + } else { // ABGR to ARGB + int rb = color & 0x00FF00FF; + return (color & 0xFF00FF00) | (rb << 16) | (rb >> 16); + } + } + + + /** + * Converts input array of native OpenGL values (RGBA on big endian, ABGR on + * little endian) representing an image of width x height resolution to Java + * ARGB. It also rearranges the elements in the array so that the image is + * flipped vertically. + */ + protected static void nativeToJavaARGB(int[] pixels, int width, int height) { + int index = 0; + int yindex = (height - 1) * width; + for (int y = 0; y < height / 2; y++) { + for (int x = 0; x < width; x++) { + int pixy = pixels[yindex]; + int pixi = pixels[index]; + if (BIG_ENDIAN) { // RGBA to ARGB + pixels[index] = (pixy >>> 8) | (pixy << 24); + pixels[yindex] = (pixi >>> 8) | (pixi << 24); + } else { // ABGR to ARGB + int rbi = pixi & 0x00FF00FF; + int rby = pixy & 0x00FF00FF; + pixels[index] = (pixy & 0xFF00FF00) | (rby << 16) | (rby >> 16); + pixels[yindex] = (pixi & 0xFF00FF00) | (rbi << 16) | (rbi >> 16); + } + index++; + yindex++; + } + yindex -= width * 2; + } + + if ((height % 2) == 1) { // Converts center row + index = (height / 2) * width; + for (int x = 0; x < width; x++) { + int pixi = pixels[index]; + if (BIG_ENDIAN) { // RGBA to ARGB + pixels[index] = (pixi >>> 8) | (pixi << 24); + } else { // ABGR to ARGB + int rbi = pixi & 0x00FF00FF; + pixels[index] = (pixi & 0xFF00FF00) | (rbi << 16) | (rbi >> 16); + } + index++; + } + } + } + + + /** + * Converts input native OpenGL value (RGBA on big endian, ABGR on little + * endian) to Java RGB, so that the alpha component of the result is set + * to opaque (255). + */ + protected static int nativeToJavaRGB(int color) { + if (BIG_ENDIAN) { // RGBA to ARGB + return (color >>> 8) | 0xFF000000; + } else { // ABGR to ARGB + int rb = color & 0x00FF00FF; + return 0xFF000000 | (rb << 16) | + (color & 0x0000FF00) | (rb >> 16); + } + } + + + /** + * Converts input array of native OpenGL values (RGBA on big endian, ABGR on + * little endian) representing an image of width x height resolution to Java + * RGB, so that the alpha component of all pixels is set to opaque (255). It + * also rearranges the elements in the array so that the image is flipped + * vertically. + */ + protected static void nativeToJavaRGB(int[] pixels, int width, int height) { + int index = 0; + int yindex = (height - 1) * width; + for (int y = 0; y < height / 2; y++) { + for (int x = 0; x < width; x++) { + int pixy = pixels[yindex]; + int pixi = pixels[index]; + if (BIG_ENDIAN) { // RGBA to ARGB + pixels[index] = (pixy >>> 8) | 0xFF000000; + pixels[yindex] = (pixi >>> 8) | 0xFF000000; + } else { // ABGR to ARGB + int rbi = pixi & 0x00FF00FF; + int rby = pixy & 0x00FF00FF; + pixels[index] = 0xFF000000 | (rby << 16) | + (pixy & 0x0000FF00) | (rby >> 16); + pixels[yindex] = 0xFF000000 | (rbi << 16) | + (pixi & 0x0000FF00) | (rbi >> 16); + } + index++; + yindex++; + } + yindex -= width * 2; + } + + if ((height % 2) == 1) { // Converts center row + index = (height / 2) * width; + for (int x = 0; x < width; x++) { + int pixi = pixels[index]; + if (BIG_ENDIAN) { // RGBA to ARGB + pixels[index] = (pixi >>> 8) | 0xFF000000; + } else { // ABGR to ARGB + int rbi = pixi & 0x00FF00FF; + pixels[index] = 0xFF000000 | (rbi << 16) | + (pixi & 0x000FF00) | (rbi >> 16); + } + index++; + } + } + } + + + /** + * Converts input Java ARGB value to native OpenGL format (RGBA on big endian, + * BGRA on little endian). + */ + protected static int javaToNativeARGB(int color) { + if (BIG_ENDIAN) { // ARGB to RGBA + return (color >>> 24) | (color << 8); + } else { // ARGB to ABGR + int rb = color & 0x00FF00FF; + return (color & 0xFF00FF00) | (rb << 16) | (rb >> 16); + } + } + + + /** + * Converts input array of Java ARGB values representing an image of width x + * height resolution to native OpenGL format (RGBA on big endian, BGRA on + * little endian). It also rearranges the elements in the array so that the + * image is flipped vertically. + */ + protected static void javaToNativeARGB(int[] pixels, int width, int height) { + int index = 0; + int yindex = (height - 1) * width; + for (int y = 0; y < height / 2; y++) { + for (int x = 0; x < width; x++) { + int pixy = pixels[yindex]; + int pixi = pixels[index]; + if (BIG_ENDIAN) { // ARGB to RGBA + pixels[index] = (pixy >>> 24) | (pixy << 8); + pixels[yindex] = (pixi >>> 24) | (pixi << 8); + } else { // ARGB to ABGR + int rbi = pixi & 0x00FF00FF; + int rby = pixy & 0x00FF00FF; + pixels[index] = (pixy & 0xFF00FF00) | (rby << 16) | (rby >> 16); + pixels[yindex] = (pixi & 0xFF00FF00) | (rbi << 16) | (rbi >> 16); + } + index++; + yindex++; + } + yindex -= width * 2; + } + + if ((height % 2) == 1) { // Converts center row + index = (height / 2) * width; + for (int x = 0; x < width; x++) { + int pixi = pixels[index]; + if (BIG_ENDIAN) { // ARGB to RGBA + pixels[index] = (pixi >>> 24) | (pixi << 8); + } else { // ARGB to ABGR + int rbi = pixi & 0x00FF00FF; + pixels[index] = (pixi & 0xFF00FF00) | (rbi << 16) | (rbi >> 16); + } + index++; + } + } + } + + + /** + * Converts input Java ARGB value to native OpenGL format (RGBA on big endian, + * BGRA on little endian), setting alpha component to opaque (255). + */ + protected static int javaToNativeRGB(int color) { + if (BIG_ENDIAN) { // ARGB to RGB + return 0xFF | (color << 8); + } else { // ARGB to BGR + int rb = color & 0x00FF00FF; + return 0xFF000000 | (rb << 16) | (color & 0x0000FF00) | (rb >> 16); + } + } + + + /** + * Converts input array of Java ARGB values representing an image of width x + * height resolution to native OpenGL format (RGBA on big endian, BGRA on + * little endian), while setting alpha component of all pixels to opaque + * (255). It also rearranges the elements in the array so that the image is + * flipped vertically. + */ + protected static void javaToNativeRGB(int[] pixels, int width, int height) { + int index = 0; + int yindex = (height - 1) * width; + for (int y = 0; y < height / 2; y++) { + for (int x = 0; x < width; x++) { + int pixy = pixels[yindex]; + int pixi = pixels[index]; + if (BIG_ENDIAN) { // ARGB to RGB + pixels[index] = 0xFF | (pixy << 8); + pixels[yindex] = 0xFF | (pixi << 8); + } else { // ARGB to BGR + int rbi = pixi & 0x00FF00FF; + int rby = pixy & 0x00FF00FF; + pixels[index] = 0xFF000000 | (rby << 16) | + (pixy & 0x0000FF00) | (rby >> 16); + pixels[yindex] = 0xFF000000 | (rbi << 16) | + (pixi & 0x0000FF00) | (rbi >> 16); + } + index++; + yindex++; + } + yindex -= width * 2; + } + + if ((height % 2) == 1) { // Converts center row + index = (height / 2) * width; + for (int x = 0; x < width; x++) { + int pixi = pixels[index]; + if (BIG_ENDIAN) { // ARGB to RGB + pixels[index] = 0xFF | (pixi << 8); + } else { // ARGB to BGR + int rbi = pixi & 0x00FF00FF; + pixels[index] = 0xFF000000 | (rbi << 16) | + (pixi & 0x0000FF00) | (rbi >> 16); + } + index++; + } + } + } + + + protected static int qualityToSamples(int quality) { + if (quality <= 1) { + return 1; + } else { + // Number of samples is always an even number: + int n = 2 * (quality / 2); + return n; + } + } + + + abstract protected int getGLSLVersion(); + + + protected String[] loadVertexShader(String filename) { + return sketch.loadStrings(filename); + } + + + protected String[] loadFragmentShader(String filename) { + return sketch.loadStrings(filename); + } + + + protected String[] loadFragmentShader(URL url) { + try { + return PApplet.loadStrings(url.openStream()); + } catch (IOException e) { + PGraphics.showException("Cannot load fragment shader " + url.getFile()); + } + return null; + } + + + protected String[] loadVertexShader(URL url) { + try { + return PApplet.loadStrings(url.openStream()); + } catch (IOException e) { + PGraphics.showException("Cannot load vertex shader " + url.getFile()); + } + return null; + } + + + protected String[] loadVertexShader(String filename, int version) { + return loadVertexShader(filename); + } + + + protected String[] loadFragmentShader(String filename, int version) { + return loadFragmentShader(filename); + } + + + protected String[] loadFragmentShader(URL url, int version) { + return loadFragmentShader(url); + } + + + protected String[] loadVertexShader(URL url, int version) { + return loadVertexShader(url); + } + + + protected static String[] preprocessFragmentSource(String[] fragSrc0, + int version) { + if (containsVersionDirective(fragSrc0)) { + // The user knows what she or he is doing + return fragSrc0; + } + + String[] fragSrc; + + if (version < 130) { + Pattern[] search = { }; + String[] replace = { }; + int offset = 1; + + fragSrc = preprocessShaderSource(fragSrc0, search, replace, offset); + fragSrc[0] = "#version " + version; + } else { + // We need to replace 'texture' uniform by 'texMap' uniform and + // 'textureXXX()' functions by 'texture()' functions. Order of these + // replacements is important to prevent collisions between these two. + Pattern[] search = new Pattern[] { + Pattern.compile(String.format(GLSL_ID_REGEX, "varying|attribute")), + Pattern.compile(String.format(GLSL_ID_REGEX, "texture")), + Pattern.compile(String.format(GLSL_FN_REGEX, "texture2DRect|texture2D|texture3D|textureCube")), + Pattern.compile(String.format(GLSL_ID_REGEX, "gl_FragColor")) + }; + String[] replace = new String[] { + "in", "texMap", "texture", "_fragColor" + }; + int offset = 2; + + fragSrc = preprocessShaderSource(fragSrc0, search, replace, offset); + fragSrc[0] = "#version " + version; + fragSrc[1] = "out vec4 _fragColor;"; + } + + return fragSrc; + } + + protected static String[] preprocessVertexSource(String[] vertSrc0, + int version) { + if (containsVersionDirective(vertSrc0)) { + // The user knows what she or he is doing + return vertSrc0; + } + + String[] vertSrc; + + if (version < 130) { + Pattern[] search = { }; + String[] replace = { }; + int offset = 1; + + vertSrc = preprocessShaderSource(vertSrc0, search, replace, offset); + vertSrc[0] = "#version " + version; + } else { + // We need to replace 'texture' uniform by 'texMap' uniform and + // 'textureXXX()' functions by 'texture()' functions. Order of these + // replacements is important to prevent collisions between these two. + Pattern[] search = new Pattern[] { + Pattern.compile(String.format(GLSL_ID_REGEX, "varying")), + Pattern.compile(String.format(GLSL_ID_REGEX, "attribute")), + Pattern.compile(String.format(GLSL_ID_REGEX, "texture")), + Pattern.compile(String.format(GLSL_FN_REGEX, "texture2DRect|texture2D|texture3D|textureCube")) + }; + String[] replace = new String[] { + "out", "in", "texMap", "texture", + }; + int offset = 1; + + vertSrc = preprocessShaderSource(vertSrc0, search, replace, offset); + vertSrc[0] = "#version " + version; + } + + return vertSrc; + } + + + protected static final String GLSL_ID_REGEX = "(?= 0) { + line = line.substring(0, versionIndex); + } + for (int j = 0; j < search.length; j++) { + line = search[j].matcher(line).replaceAll(replace[j]); + } + src[i+offset] = line; + } + return src; + } + + protected static boolean containsVersionDirective(String[] shSrc) { + for (int i = 0; i < shSrc.length; i++) { + String line = shSrc[i]; + int versionIndex = line.indexOf("#version"); + if (versionIndex >= 0) { + int commentIndex = line.indexOf("//"); + if (commentIndex < 0 || versionIndex < commentIndex) { + return true; + } + } + } + return false; + } + + protected int createShader(int shaderType, String source) { + int shader = createShader(shaderType); + if (shader != 0) { + shaderSource(shader, source); + compileShader(shader); + if (!compiled(shader)) { + System.err.println("Could not compile shader " + shaderType + ":"); + System.err.println(getShaderInfoLog(shader)); + deleteShader(shader); + shader = 0; + } + } + return shader; + } + + + protected int createProgram(int vertexShader, int fragmentShader) { + int program = createProgram(); + if (program != 0) { + attachShader(program, vertexShader); + attachShader(program, fragmentShader); + linkProgram(program); + if (!linked(program)) { + System.err.println("Could not link program: "); + System.err.println(getProgramInfoLog(program)); + deleteProgram(program); + program = 0; + } + } + return program; + } + + + protected boolean compiled(int shader) { + intBuffer.rewind(); + getShaderiv(shader, COMPILE_STATUS, intBuffer); + return intBuffer.get(0) == 0 ? false : true; + } + + + protected boolean linked(int program) { + intBuffer.rewind(); + getProgramiv(program, LINK_STATUS, intBuffer); + return intBuffer.get(0) == 0 ? false : true; + } + + + protected int validateFramebuffer() { + int status = checkFramebufferStatus(FRAMEBUFFER); + if (status == FRAMEBUFFER_COMPLETE) { + return 0; + } else if (status == FRAMEBUFFER_UNDEFINED) { + System.err.println(String.format(FRAMEBUFFER_ERROR, + "framebuffer undefined")); + } else if (status == FRAMEBUFFER_INCOMPLETE_ATTACHMENT) { + System.err.println(String.format(FRAMEBUFFER_ERROR, + "incomplete attachment")); + } else if (status == FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT) { + System.err.println(String.format(FRAMEBUFFER_ERROR, + "incomplete missing attachment")); + } else if (status == FRAMEBUFFER_INCOMPLETE_DIMENSIONS) { + System.err.println(String.format(FRAMEBUFFER_ERROR, + "incomplete dimensions")); + } else if (status == FRAMEBUFFER_INCOMPLETE_FORMATS) { + System.err.println(String.format(FRAMEBUFFER_ERROR, + "incomplete formats")); + } else if (status == FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER) { + System.err.println(String.format(FRAMEBUFFER_ERROR, + "incomplete draw buffer")); + } else if (status == FRAMEBUFFER_INCOMPLETE_READ_BUFFER) { + System.err.println(String.format(FRAMEBUFFER_ERROR, + "incomplete read buffer")); + } else if (status == FRAMEBUFFER_UNSUPPORTED) { + System.err.println(String.format(FRAMEBUFFER_ERROR, + "framebuffer unsupported")); + } else if (status == FRAMEBUFFER_INCOMPLETE_MULTISAMPLE) { + System.err.println(String.format(FRAMEBUFFER_ERROR, + "incomplete multisample buffer")); + } else if (status == FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS) { + System.err.println(String.format(FRAMEBUFFER_ERROR, + "incomplete layer targets")); + } else { + System.err.println(String.format(FRAMEBUFFER_ERROR, + "unknown error " + status)); + } + return status; + } + + protected boolean isES() { + return getString(VERSION).trim().toLowerCase().contains("opengl es"); + } + + protected int[] getGLVersion() { + String version = getString(VERSION).trim().toLowerCase(); + + String ES = "opengl es"; + int esPosition = version.indexOf(ES); + if (esPosition >= 0) { + version = version.substring(esPosition + ES.length()).trim(); + } + + int[] res = {0, 0, 0}; + String[] parts = version.split(" "); + for (int i = 0; i < parts.length; i++) { + if (0 < parts[i].indexOf(".")) { + String nums[] = parts[i].split("\\."); + try { + res[0] = Integer.parseInt(nums[0]); + } catch (NumberFormatException e) { } + if (1 < nums.length) { + try { + res[1] = Integer.parseInt(nums[1]); + } catch (NumberFormatException e) { } + } + if (2 < nums.length) { + try { + res[2] = Integer.parseInt(nums[2]); + } catch (NumberFormatException e) { } + } + break; + } + } + return res; + } + + + protected boolean hasFBOs() { + // FBOs might still be available through extensions. + int major = getGLVersion()[0]; + if (major < 2) { + String ext = getString(EXTENSIONS); + return ext.indexOf("_framebuffer_object") != -1 && + ext.indexOf("_vertex_shader") != -1 && + ext.indexOf("_shader_objects") != -1 && + ext.indexOf("_shading_language") != -1; + } else { + return true; + } + } + + + protected boolean hasShaders() { + // GLSL might still be available through extensions. For instance, + // GLContext.hasGLSL() gives false for older intel integrated chipsets on + // OSX, where OpenGL is 1.4 but shaders are available. + int major = getGLVersion()[0]; + if (major < 2) { + String ext = getString(EXTENSIONS); + return ext.indexOf("_fragment_shader") != -1 && + ext.indexOf("_vertex_shader") != -1 && + ext.indexOf("_shader_objects") != -1 && + ext.indexOf("_shading_language") != -1; + } else { + return true; + } + } + + + protected boolean hasNpotTexSupport() { + int major = getGLVersion()[0]; + if (major < 3) { + String ext = getString(EXTENSIONS); + if (isES()) { + return -1 < ext.indexOf("_texture_npot"); + } else { + return -1 < ext.indexOf("_texture_non_power_of_two"); + } + } else { + return true; + } + } + + + protected boolean hasAutoMipmapGenSupport() { + int major = getGLVersion()[0]; + if (isES() && major >= 2) { + return true; + } else if (!isES() && major >= 3) { + return true; + } else { + String ext = getString(EXTENSIONS); + return -1 < ext.indexOf("_generate_mipmap"); + } + } + + + protected boolean hasFboMultisampleSupport() { + int major = getGLVersion()[0]; + if (major < 3) { + String ext = getString(EXTENSIONS); + return -1 < ext.indexOf("_framebuffer_multisample"); + } else { + return true; + } + } + + + protected boolean hasPackedDepthStencilSupport() { + int major = getGLVersion()[0]; + if (major < 3) { + String ext = getString(EXTENSIONS); + return -1 < ext.indexOf("_packed_depth_stencil"); + } else { + return true; + } + } + + + protected boolean hasAnisoSamplingSupport() { + int major = getGLVersion()[0]; + if (major < 3) { + String ext = getString(EXTENSIONS); + return -1 < ext.indexOf("_texture_filter_anisotropic"); + } else { + return true; + } + } + + + protected boolean hasSynchronization() { + int[] version = getGLVersion(); + if (isES()) { + return version[0] >= 3; + } + return (version[0] > 3) || (version[0] == 3 && version[1] >= 2); + } + + + protected boolean hasPBOs() { + int[] version = getGLVersion(); + if (isES()) { + return version[0] >= 3; + } + return (version[0] > 2) || (version[0] == 2 && version[1] >= 1); + } + + + protected boolean hasReadBuffer() { + int[] version = getGLVersion(); + if (isES()) { + return version[0] >= 3; + } + return version[0] >= 2; + } + + + protected boolean hasDrawBuffer() { + int[] version = getGLVersion(); + if (isES()) { + return version[0] >= 3; + } + return version[0] >= 2; + } + + + protected int maxSamples() { + intBuffer.rewind(); + getIntegerv(MAX_SAMPLES, intBuffer); + return intBuffer.get(0); + } + + + protected int getMaxTexUnits() { + intBuffer.rewind(); + getIntegerv(MAX_TEXTURE_IMAGE_UNITS, intBuffer); + return intBuffer.get(0); + } + + + protected static ByteBuffer allocateDirectByteBuffer(int size) { + int bytes = PApplet.max(MIN_DIRECT_BUFFER_SIZE, size) * SIZEOF_BYTE; + return ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()); + } + + + protected static ByteBuffer allocateByteBuffer(int size) { + if (USE_DIRECT_BUFFERS) { + return allocateDirectByteBuffer(size); + } else { + return ByteBuffer.allocate(size); + } + } + + + protected static ByteBuffer allocateByteBuffer(byte[] arr) { + if (USE_DIRECT_BUFFERS) { + ByteBuffer buf = allocateDirectByteBuffer(arr.length); + buf.put(arr); + buf.position(0); + return buf; + } else { + return ByteBuffer.wrap(arr); + } + } + + + protected static ByteBuffer updateByteBuffer(ByteBuffer buf, byte[] arr, + boolean wrap) { + if (USE_DIRECT_BUFFERS) { + if (buf == null || buf.capacity() < arr.length) { + buf = allocateDirectByteBuffer(arr.length); + } + buf.position(0); + buf.put(arr); + buf.rewind(); + } else { + if (wrap) { + buf = ByteBuffer.wrap(arr); + } else { + if (buf == null || buf.capacity() < arr.length) { + buf = ByteBuffer.allocate(arr.length); + } + buf.position(0); + buf.put(arr); + buf.rewind(); + } + } + return buf; + } + + + protected static void updateByteBuffer(ByteBuffer buf, byte[] arr, + int offset, int size) { + if (USE_DIRECT_BUFFERS || (buf.hasArray() && buf.array() != arr)) { + buf.position(offset); + buf.put(arr, offset, size); + buf.rewind(); + } + } + + + protected static void getByteArray(ByteBuffer buf, byte[] arr) { + if (!buf.hasArray() || buf.array() != arr) { + buf.position(0); + buf.get(arr); + buf.rewind(); + } + } + + + protected static void putByteArray(ByteBuffer buf, byte[] arr) { + if (!buf.hasArray() || buf.array() != arr) { + buf.position(0); + buf.put(arr); + buf.rewind(); + } + } + + + protected static void fillByteBuffer(ByteBuffer buf, int i0, int i1, + byte val) { + int n = i1 - i0; + byte[] temp = new byte[n]; + Arrays.fill(temp, 0, n, val); + buf.position(i0); + buf.put(temp, 0, n); + buf.rewind(); + } + + + protected static ShortBuffer allocateDirectShortBuffer(int size) { + int bytes = PApplet.max(MIN_DIRECT_BUFFER_SIZE, size) * SIZEOF_SHORT; + return ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()). + asShortBuffer(); + } + + + protected static ShortBuffer allocateShortBuffer(int size) { + if (USE_DIRECT_BUFFERS) { + return allocateDirectShortBuffer(size); + } else { + return ShortBuffer.allocate(size); + } + } + + + protected static ShortBuffer allocateShortBuffer(short[] arr) { + if (USE_DIRECT_BUFFERS) { + ShortBuffer buf = allocateDirectShortBuffer(arr.length); + buf.put(arr); + buf.position(0); + return buf; + } else { + return ShortBuffer.wrap(arr); + } + } + + + protected static ShortBuffer updateShortBuffer(ShortBuffer buf, short[] arr, + boolean wrap) { + if (USE_DIRECT_BUFFERS) { + if (buf == null || buf.capacity() < arr.length) { + buf = allocateDirectShortBuffer(arr.length); + } + buf.position(0); + buf.put(arr); + buf.rewind(); + } else { + if (wrap) { + buf = ShortBuffer.wrap(arr); + } else { + if (buf == null || buf.capacity() < arr.length) { + buf = ShortBuffer.allocate(arr.length); + } + buf.position(0); + buf.put(arr); + buf.rewind(); + } + } + return buf; + } + + + protected static void updateShortBuffer(ShortBuffer buf, short[] arr, + int offset, int size) { + if (USE_DIRECT_BUFFERS || (buf.hasArray() && buf.array() != arr)) { + buf.position(offset); + buf.put(arr, offset, size); + buf.rewind(); + } + } + + + protected static void getShortArray(ShortBuffer buf, short[] arr) { + if (!buf.hasArray() || buf.array() != arr) { + buf.position(0); + buf.get(arr); + buf.rewind(); + } + } + + + protected static void putShortArray(ShortBuffer buf, short[] arr) { + if (!buf.hasArray() || buf.array() != arr) { + buf.position(0); + buf.put(arr); + buf.rewind(); + } + } + + + protected static void fillShortBuffer(ShortBuffer buf, int i0, int i1, + short val) { + int n = i1 - i0; + short[] temp = new short[n]; + Arrays.fill(temp, 0, n, val); + buf.position(i0); + buf.put(temp, 0, n); + buf.rewind(); + } + + + protected static IntBuffer allocateDirectIntBuffer(int size) { + int bytes = PApplet.max(MIN_DIRECT_BUFFER_SIZE, size) * SIZEOF_INT; + return ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()). + asIntBuffer(); + } + + + protected static IntBuffer allocateIntBuffer(int size) { + if (USE_DIRECT_BUFFERS) { + return allocateDirectIntBuffer(size); + } else { + return IntBuffer.allocate(size); + } + } + + + protected static IntBuffer allocateIntBuffer(int[] arr) { + if (USE_DIRECT_BUFFERS) { + IntBuffer buf = allocateDirectIntBuffer(arr.length); + buf.put(arr); + buf.position(0); + return buf; + } else { + return IntBuffer.wrap(arr); + } + } + + + protected static IntBuffer updateIntBuffer(IntBuffer buf, int[] arr, + boolean wrap) { + if (USE_DIRECT_BUFFERS) { + if (buf == null || buf.capacity() < arr.length) { + buf = allocateDirectIntBuffer(arr.length); + } + buf.position(0); + buf.put(arr); + buf.rewind(); + } else { + if (wrap) { + buf = IntBuffer.wrap(arr); + } else { + if (buf == null || buf.capacity() < arr.length) { + buf = IntBuffer.allocate(arr.length); + } + buf.position(0); + buf.put(arr); + buf.rewind(); + } + } + return buf; + } + + + protected static void updateIntBuffer(IntBuffer buf, int[] arr, + int offset, int size) { + if (USE_DIRECT_BUFFERS || (buf.hasArray() && buf.array() != arr)) { + buf.position(offset); + buf.put(arr, offset, size); + buf.rewind(); + } + } + + + protected static void getIntArray(IntBuffer buf, int[] arr) { + if (!buf.hasArray() || buf.array() != arr) { + buf.position(0); + buf.get(arr); + buf.rewind(); + } + } + + + protected static void putIntArray(IntBuffer buf, int[] arr) { + if (!buf.hasArray() || buf.array() != arr) { + buf.position(0); + buf.put(arr); + buf.rewind(); + } + } + + + protected static void fillIntBuffer(IntBuffer buf, int i0, int i1, int val) { + int n = i1 - i0; + int[] temp = new int[n]; + Arrays.fill(temp, 0, n, val); + buf.position(i0); + buf.put(temp, 0, n); + buf.rewind(); + } + + + protected static FloatBuffer allocateDirectFloatBuffer(int size) { + int bytes = PApplet.max(MIN_DIRECT_BUFFER_SIZE, size) * SIZEOF_FLOAT; + return ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()). + asFloatBuffer(); + } + + + protected static FloatBuffer allocateFloatBuffer(int size) { + if (USE_DIRECT_BUFFERS) { + return allocateDirectFloatBuffer(size); + } else { + return FloatBuffer.allocate(size); + } + } + + + protected static FloatBuffer allocateFloatBuffer(float[] arr) { + if (USE_DIRECT_BUFFERS) { + FloatBuffer buf = allocateDirectFloatBuffer(arr.length); + buf.put(arr); + buf.position(0); + return buf; + } else { + return FloatBuffer.wrap(arr); + } + } + + + protected static FloatBuffer updateFloatBuffer(FloatBuffer buf, float[] arr, + boolean wrap) { + if (USE_DIRECT_BUFFERS) { + if (buf == null || buf.capacity() < arr.length) { + buf = allocateDirectFloatBuffer(arr.length); + } + buf.position(0); + buf.put(arr); + buf.rewind(); + } else { + if (wrap) { + buf = FloatBuffer.wrap(arr); + } else { + if (buf == null || buf.capacity() < arr.length) { + buf = FloatBuffer.allocate(arr.length); + } + buf.position(0); + buf.put(arr); + buf.rewind(); + } + } + return buf; + } + + + protected static void updateFloatBuffer(FloatBuffer buf, float[] arr, + int offset, int size) { + if (USE_DIRECT_BUFFERS || (buf.hasArray() && buf.array() != arr)) { + buf.position(offset); + buf.put(arr, offset, size); + buf.rewind(); + } + } + + + protected static void getFloatArray(FloatBuffer buf, float[] arr) { + if (!buf.hasArray() || buf.array() != arr) { + buf.position(0); + buf.get(arr); + buf.rewind(); + } + } + + + protected static void putFloatArray(FloatBuffer buf, float[] arr) { + if (!buf.hasArray() || buf.array() != arr) { + buf.position(0); + buf.put(arr); + buf.rewind(); + } + } + + + protected static void fillFloatBuffer(FloatBuffer buf, int i0, int i1, + float val) { + int n = i1 - i0; + float[] temp = new float[n]; + Arrays.fill(temp, 0, n, val); + buf.position(i0); + buf.put(temp, 0, n); + buf.rewind(); + } + + + // TODO: the next three functions shouldn't be here... + // Uses 'Object' so that the API can be used w/ Android Typeface objects + + abstract protected int getFontAscent(Object font); + + + abstract protected int getFontDescent(Object font); + + + abstract protected int getTextWidth(Object font, char[] buffer, int start, int stop); + + + abstract protected Object getDerivedFont(Object font, float size); + + + /////////////////////////////////////////////////////////// + + // Tessellator interface + + + protected abstract Tessellator createTessellator(TessellatorCallback callback); + + + protected interface Tessellator { + public void setCallback(int flag); + public void setWindingRule(int rule); + public void setProperty(int property, int value); + + public void beginPolygon(); + public void beginPolygon(Object data); + public void endPolygon(); + public void beginContour(); + public void endContour(); + public void addVertex(double[] v); + public void addVertex(double[] v, int n, Object data); + } + + + protected interface TessellatorCallback { + public void begin(int type); + public void end(); + public void vertex(Object data); + public void combine(double[] coords, Object[] data, + float[] weight, Object[] outData); + public void error(int errnum); + } + + + protected String tessError(int err) { + return ""; + } + + + /////////////////////////////////////////////////////////// + + // FontOutline interface + + + protected static boolean SHAPE_TEXT_SUPPORTED; + protected static int SEG_MOVETO; + protected static int SEG_LINETO; + protected static int SEG_QUADTO; + protected static int SEG_CUBICTO; + protected static int SEG_CLOSE; + + + protected abstract FontOutline createFontOutline(char ch, Object font); + + + protected interface FontOutline { + public boolean isDone(); + public int currentSegment(float coords[]); + public void next(); + } + + + ////////////////////////////////////////////////////////////////////////////// + // + // OpenGL ES 2.0 API, with a few additional functions for multisampling and + // and buffer mapping from OpenGL 2.1+. + // + // The functions are organized following the groups in the GLES 2.0 reference + // card: + // http://www.khronos.org/opengles/sdk/docs/reference_cards/OpenGL-ES-2_0-Reference-card.pdf + // + // The entire GLES 2.0 specification is available below: + // http://www.khronos.org/opengles/2_X/ + // + // Implementations of the PGL functions for specific OpenGL bindings (JOGL, + // LWJGL) should simply call the corresponding GL function in the bindings. + // readPixels(), activeTexture() and bindTexture() are special cases, please + // read their comments. + // Also, keep in mind the note about the PGL constants below. + // + ////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////// + + // Constants + // Very important note: set the GL constants in your PGL subclass by using an + // static initialization block as follows: + // static { + // FALSE = SUPER_DUPER_JAVA_OPENGL_BINDINGS.GL_FALSE; + // TRUE = SUPER_DUPER_JAVA_OPENGL_BINDINGS.GL_TRUE; + // ... + // } + // and not by re-declaring the constants, because doing so will lead to + // errors when the constants are accessed through PGL because they are not + // overridden but hidden by the new declarations, and hence they keep their + // initial values (all zeroes) when accessed through the superclass. + + public static int FALSE; + public static int TRUE; + + public static int INT; + public static int BYTE; + public static int SHORT; + public static int FLOAT; + public static int BOOL; + public static int UNSIGNED_INT; + public static int UNSIGNED_BYTE; + public static int UNSIGNED_SHORT; + + public static int RGB; + public static int RGBA; + public static int ALPHA; + public static int LUMINANCE; + public static int LUMINANCE_ALPHA; + + public static int UNSIGNED_SHORT_5_6_5; + public static int UNSIGNED_SHORT_4_4_4_4; + public static int UNSIGNED_SHORT_5_5_5_1; + + public static int RGBA4; + public static int RGB5_A1; + public static int RGB565; + public static int RGB8; + public static int RGBA8; + public static int ALPHA8; + + public static int READ_ONLY; + public static int WRITE_ONLY; + public static int READ_WRITE; + + public static int TESS_WINDING_NONZERO; + public static int TESS_WINDING_ODD; + public static int TESS_EDGE_FLAG; + + public static int GENERATE_MIPMAP_HINT; + public static int FASTEST; + public static int NICEST; + public static int DONT_CARE; + + public static int VENDOR; + public static int RENDERER; + public static int VERSION; + public static int EXTENSIONS; + public static int SHADING_LANGUAGE_VERSION; + + public static int MAX_SAMPLES; + public static int SAMPLES; + + public static int ALIASED_LINE_WIDTH_RANGE; + public static int ALIASED_POINT_SIZE_RANGE; + + public static int DEPTH_BITS; + public static int STENCIL_BITS; + + public static int CCW; + public static int CW; + + public static int VIEWPORT; + + public static int ARRAY_BUFFER; + public static int ELEMENT_ARRAY_BUFFER; + public static int PIXEL_PACK_BUFFER; + + public static int MAX_VERTEX_ATTRIBS; + + public static int STATIC_DRAW; + public static int DYNAMIC_DRAW; + public static int STREAM_DRAW; + public static int STREAM_READ; + + public static int BUFFER_SIZE; + public static int BUFFER_USAGE; + + public static int POINTS; + public static int LINE_STRIP; + public static int LINE_LOOP; + public static int LINES; + public static int TRIANGLE_FAN; + public static int TRIANGLE_STRIP; + public static int TRIANGLES; + + public static int CULL_FACE; + public static int FRONT; + public static int BACK; + public static int FRONT_AND_BACK; + + public static int POLYGON_OFFSET_FILL; + + public static int UNPACK_ALIGNMENT; + public static int PACK_ALIGNMENT; + + public static int TEXTURE_2D; + public static int TEXTURE_RECTANGLE; + + public static int TEXTURE_BINDING_2D; + public static int TEXTURE_BINDING_RECTANGLE; + + public static int MAX_TEXTURE_SIZE; + public static int TEXTURE_MAX_ANISOTROPY; + public static int MAX_TEXTURE_MAX_ANISOTROPY; + + public static int MAX_VERTEX_TEXTURE_IMAGE_UNITS; + public static int MAX_TEXTURE_IMAGE_UNITS; + public static int MAX_COMBINED_TEXTURE_IMAGE_UNITS; + + public static int NUM_COMPRESSED_TEXTURE_FORMATS; + public static int COMPRESSED_TEXTURE_FORMATS; + + public static int NEAREST; + public static int LINEAR; + public static int LINEAR_MIPMAP_NEAREST; + public static int LINEAR_MIPMAP_LINEAR; + + public static int CLAMP_TO_EDGE; + public static int REPEAT; + + public static int TEXTURE0; + public static int TEXTURE1; + public static int TEXTURE2; + public static int TEXTURE3; + public static int TEXTURE_MIN_FILTER; + public static int TEXTURE_MAG_FILTER; + public static int TEXTURE_WRAP_S; + public static int TEXTURE_WRAP_T; + public static int TEXTURE_WRAP_R; + + public static int TEXTURE_CUBE_MAP; + public static int TEXTURE_CUBE_MAP_POSITIVE_X; + public static int TEXTURE_CUBE_MAP_POSITIVE_Y; + public static int TEXTURE_CUBE_MAP_POSITIVE_Z; + public static int TEXTURE_CUBE_MAP_NEGATIVE_X; + public static int TEXTURE_CUBE_MAP_NEGATIVE_Y; + public static int TEXTURE_CUBE_MAP_NEGATIVE_Z; + + public static int VERTEX_SHADER; + public static int FRAGMENT_SHADER; + public static int INFO_LOG_LENGTH; + public static int SHADER_SOURCE_LENGTH; + public static int COMPILE_STATUS; + public static int LINK_STATUS; + public static int VALIDATE_STATUS; + public static int SHADER_TYPE; + public static int DELETE_STATUS; + + public static int FLOAT_VEC2; + public static int FLOAT_VEC3; + public static int FLOAT_VEC4; + public static int FLOAT_MAT2; + public static int FLOAT_MAT3; + public static int FLOAT_MAT4; + public static int INT_VEC2; + public static int INT_VEC3; + public static int INT_VEC4; + public static int BOOL_VEC2; + public static int BOOL_VEC3; + public static int BOOL_VEC4; + public static int SAMPLER_2D; + public static int SAMPLER_CUBE; + + public static int LOW_FLOAT; + public static int MEDIUM_FLOAT; + public static int HIGH_FLOAT; + public static int LOW_INT; + public static int MEDIUM_INT; + public static int HIGH_INT; + + public static int CURRENT_VERTEX_ATTRIB; + + public static int VERTEX_ATTRIB_ARRAY_BUFFER_BINDING; + public static int VERTEX_ATTRIB_ARRAY_ENABLED; + public static int VERTEX_ATTRIB_ARRAY_SIZE; + public static int VERTEX_ATTRIB_ARRAY_STRIDE; + public static int VERTEX_ATTRIB_ARRAY_TYPE; + public static int VERTEX_ATTRIB_ARRAY_NORMALIZED; + public static int VERTEX_ATTRIB_ARRAY_POINTER; + + public static int BLEND; + public static int ONE; + public static int ZERO; + public static int SRC_ALPHA; + public static int DST_ALPHA; + public static int ONE_MINUS_SRC_ALPHA; + public static int ONE_MINUS_DST_COLOR; + public static int ONE_MINUS_SRC_COLOR; + public static int DST_COLOR; + public static int SRC_COLOR; + + public static int SAMPLE_ALPHA_TO_COVERAGE; + public static int SAMPLE_COVERAGE; + + public static int KEEP; + public static int REPLACE; + public static int INCR; + public static int DECR; + public static int INVERT; + public static int INCR_WRAP; + public static int DECR_WRAP; + public static int NEVER; + public static int ALWAYS; + + public static int EQUAL; + public static int LESS; + public static int LEQUAL; + public static int GREATER; + public static int GEQUAL; + public static int NOTEQUAL; + + public static int FUNC_ADD; + public static int FUNC_MIN; + public static int FUNC_MAX; + public static int FUNC_REVERSE_SUBTRACT; + public static int FUNC_SUBTRACT; + + public static int DITHER; + + public static int CONSTANT_COLOR; + public static int CONSTANT_ALPHA; + public static int ONE_MINUS_CONSTANT_COLOR; + public static int ONE_MINUS_CONSTANT_ALPHA; + public static int SRC_ALPHA_SATURATE; + + public static int SCISSOR_TEST; + public static int STENCIL_TEST; + public static int DEPTH_TEST; + public static int DEPTH_WRITEMASK; + + public static int COLOR_BUFFER_BIT; + public static int DEPTH_BUFFER_BIT; + public static int STENCIL_BUFFER_BIT; + + public static int FRAMEBUFFER; + public static int COLOR_ATTACHMENT0; + public static int COLOR_ATTACHMENT1; + public static int COLOR_ATTACHMENT2; + public static int COLOR_ATTACHMENT3; + public static int RENDERBUFFER; + public static int DEPTH_ATTACHMENT; + public static int STENCIL_ATTACHMENT; + public static int READ_FRAMEBUFFER; + public static int DRAW_FRAMEBUFFER; + + public static int DEPTH24_STENCIL8; + + public static int DEPTH_COMPONENT; + public static int DEPTH_COMPONENT16; + public static int DEPTH_COMPONENT24; + public static int DEPTH_COMPONENT32; + + public static int STENCIL_INDEX; + public static int STENCIL_INDEX1; + public static int STENCIL_INDEX4; + public static int STENCIL_INDEX8; + + public static int DEPTH_STENCIL; + + public static int FRAMEBUFFER_COMPLETE; + public static int FRAMEBUFFER_UNDEFINED; + public static int FRAMEBUFFER_INCOMPLETE_ATTACHMENT; + public static int FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT; + public static int FRAMEBUFFER_INCOMPLETE_DIMENSIONS; + public static int FRAMEBUFFER_INCOMPLETE_FORMATS; + public static int FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER; + public static int FRAMEBUFFER_INCOMPLETE_READ_BUFFER; + public static int FRAMEBUFFER_UNSUPPORTED; + public static int FRAMEBUFFER_INCOMPLETE_MULTISAMPLE; + public static int FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS; + + public static int FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE; + public static int FRAMEBUFFER_ATTACHMENT_OBJECT_NAME; + public static int FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL; + public static int FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE; + + public static int RENDERBUFFER_WIDTH; + public static int RENDERBUFFER_HEIGHT; + public static int RENDERBUFFER_RED_SIZE; + public static int RENDERBUFFER_GREEN_SIZE; + public static int RENDERBUFFER_BLUE_SIZE; + public static int RENDERBUFFER_ALPHA_SIZE; + public static int RENDERBUFFER_DEPTH_SIZE; + public static int RENDERBUFFER_STENCIL_SIZE; + public static int RENDERBUFFER_INTERNAL_FORMAT; + + public static int MULTISAMPLE; + public static int LINE_SMOOTH; + public static int POLYGON_SMOOTH; + + public static int SYNC_GPU_COMMANDS_COMPLETE; + public static int ALREADY_SIGNALED; + public static int CONDITION_SATISFIED; + + /////////////////////////////////////////////////////////// + + // Special Functions + + public abstract void flush(); + public abstract void finish(); + public abstract void hint(int target, int hint); + + /////////////////////////////////////////////////////////// + + // State and State Requests + + public abstract void enable(int value); + public abstract void disable(int value); + public abstract void getBooleanv(int value, IntBuffer data); + public abstract void getIntegerv(int value, IntBuffer data); + public abstract void getFloatv(int value, FloatBuffer data); + public abstract boolean isEnabled(int value); + public abstract String getString(int name); + + /////////////////////////////////////////////////////////// + + // Error Handling + + public abstract int getError(); + public abstract String errorString(int err); + + ////////////////////////////////////////////////////////////////////////////// + + // Buffer Objects + + public abstract void genBuffers(int n, IntBuffer buffers); + public abstract void deleteBuffers(int n, IntBuffer buffers); + public abstract void bindBuffer(int target, int buffer); + public abstract void bufferData(int target, int size, Buffer data, int usage); + public abstract void bufferSubData(int target, int offset, int size, Buffer data); + public abstract void isBuffer(int buffer); + public abstract void getBufferParameteriv(int target, int value, IntBuffer data); + public abstract ByteBuffer mapBuffer(int target, int access); + public abstract ByteBuffer mapBufferRange(int target, int offset, int length, int access); + public abstract void unmapBuffer(int target); + + ////////////////////////////////////////////////////////////////////////////// + + // Synchronization + + public abstract long fenceSync(int condition, int flags); + public abstract void deleteSync(long sync); + public abstract int clientWaitSync(long sync, int flags, long timeout); + + ////////////////////////////////////////////////////////////////////////////// + + // Viewport and Clipping + + public abstract void depthRangef(float n, float f); + public abstract void viewport(int x, int y, int w, int h); + protected abstract void viewportImpl(int x, int y, int w, int h); + + ////////////////////////////////////////////////////////////////////////////// + + // Reading Pixels + // This is a special case: because the renderer might be using an FBO even on + // the main surface, some extra handling might be needed before and after + // reading the pixels. To make this transparent to the user, the actual call + // to glReadPixels() should be done in readPixelsImpl(). + + public void readPixels(int x, int y, int width, int height, int format, int type, Buffer buffer){ + boolean multisampled = isMultisampled() || graphics.offscreenMultisample; + boolean depthReadingEnabled = graphics.getHint(PConstants.ENABLE_BUFFER_READING); + boolean depthRequested = format == STENCIL_INDEX || format == DEPTH_COMPONENT || format == DEPTH_STENCIL; + + if (multisampled && depthRequested && !depthReadingEnabled) { + PGraphics.showWarning(DEPTH_READING_NOT_ENABLED_ERROR); + return; + } + + graphics.beginReadPixels(); + readPixelsImpl(x, y, width, height, format, type, buffer); + graphics.endReadPixels(); + } + + public void readPixels(int x, int y, int width, int height, int format, int type, long offset){ + boolean multisampled = isMultisampled() || graphics.offscreenMultisample; + boolean depthReadingEnabled = graphics.getHint(PConstants.ENABLE_BUFFER_READING); + boolean depthRequested = format == STENCIL_INDEX || format == DEPTH_COMPONENT || format == DEPTH_STENCIL; + + if (multisampled && depthRequested && !depthReadingEnabled) { + PGraphics.showWarning(DEPTH_READING_NOT_ENABLED_ERROR); + return; + } + + graphics.beginReadPixels(); + readPixelsImpl(x, y, width, height, format, type, offset); + graphics.endReadPixels(); + } + + protected abstract void readPixelsImpl(int x, int y, int width, int height, int format, int type, Buffer buffer); + protected abstract void readPixelsImpl(int x, int y, int width, int height, int format, int type, long offset); + + ////////////////////////////////////////////////////////////////////////////// + + // Vertices + + public abstract void vertexAttrib1f(int index, float value); + public abstract void vertexAttrib2f(int index, float value0, float value1); + public abstract void vertexAttrib3f(int index, float value0, float value1, float value2); + public abstract void vertexAttrib4f(int index, float value0, float value1, float value2, float value3); + public abstract void vertexAttrib1fv(int index, FloatBuffer values); + public abstract void vertexAttrib2fv(int index, FloatBuffer values); + public abstract void vertexAttrib3fv(int index, FloatBuffer values); + public abstract void vertexAttrib4fv(int index, FloatBuffer values); + public abstract void vertexAttribPointer(int index, int size, int type, boolean normalized, int stride, int offset); + public abstract void enableVertexAttribArray(int index); + public abstract void disableVertexAttribArray(int index); + + public void drawArrays(int mode, int first, int count) { + geomCount += count; + drawArraysImpl(mode, first, count); + } + + public abstract void drawArraysImpl(int mode, int first, int count); + + public void drawElements(int mode, int count, int type, int offset) { + geomCount += count; + drawElementsImpl(mode, count, type, offset); + } + + public abstract void drawElementsImpl(int mode, int count, int type, int offset); + + ////////////////////////////////////////////////////////////////////////////// + + // Rasterization + + public abstract void lineWidth(float width); + public abstract void frontFace(int dir); + public abstract void cullFace(int mode); + public abstract void polygonOffset(float factor, float units); + + ////////////////////////////////////////////////////////////////////////////// + + // Pixel Rectangles + + public abstract void pixelStorei(int pname, int param); + + /////////////////////////////////////////////////////////// + + // Texturing + + public abstract void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, Buffer data); + public abstract void copyTexImage2D(int target, int level, int internalFormat, int x, int y, int width, int height, int border); + public abstract void texSubImage2D(int target, int level, int xOffset, int yOffset, int width, int height, int format, int type, Buffer data); + public abstract void copyTexSubImage2D(int target, int level, int xOffset, int yOffset, int x, int y, int width, int height); + public abstract void compressedTexImage2D(int target, int level, int internalFormat, int width, int height, int border, int imageSize, Buffer data); + public abstract void compressedTexSubImage2D(int target, int level, int xOffset, int yOffset, int width, int height, int format, int imageSize, Buffer data); + public abstract void texParameteri(int target, int pname, int param); + public abstract void texParameterf(int target, int pname, float param); + public abstract void texParameteriv(int target, int pname, IntBuffer params); + public abstract void texParameterfv(int target, int pname, FloatBuffer params); + public abstract void generateMipmap(int target); + public abstract void genTextures(int n, IntBuffer textures); + public abstract void deleteTextures(int n, IntBuffer textures); + public abstract void getTexParameteriv(int target, int pname, IntBuffer params); + public abstract void getTexParameterfv(int target, int pname, FloatBuffer params); + public abstract boolean isTexture(int texture); + + // activeTexture() and bindTexture() have some extra logic to keep track of + // the bound textures, so the actual GL call should go in activeTextureImpl() + // and bindTextureImpl(). + public void activeTexture(int texture) { + activeTexUnit = texture - TEXTURE0; + activeTextureImpl(texture); + } + + protected abstract void activeTextureImpl(int texture); + + public void bindTexture(int target, int texture) { + bindTextureImpl(target, texture); + + if (boundTextures == null) { + maxTexUnits = getMaxTexUnits(); + boundTextures = new int[maxTexUnits][2]; + } + + if (maxTexUnits <= activeTexUnit) { + throw new RuntimeException(TEXUNIT_ERROR); + } + + if (target == TEXTURE_2D) { + boundTextures[activeTexUnit][0] = texture; + } else if (target == TEXTURE_RECTANGLE) { + boundTextures[activeTexUnit][1] = texture; + } + } + protected abstract void bindTextureImpl(int target, int texture); + + /////////////////////////////////////////////////////////// + + // Shaders and Programs + + public abstract int createShader(int type); + public abstract void shaderSource(int shader, String source); + public abstract void compileShader(int shader); + public abstract void releaseShaderCompiler(); + public abstract void deleteShader(int shader); + public abstract void shaderBinary(int count, IntBuffer shaders, int binaryFormat, Buffer binary, int length); + public abstract int createProgram(); + public abstract void attachShader(int program, int shader); + public abstract void detachShader(int program, int shader); + public abstract void linkProgram(int program); + public abstract void useProgram(int program); + public abstract void deleteProgram(int program); + public abstract String getActiveAttrib(int program, int index, IntBuffer size, IntBuffer type); + public abstract int getAttribLocation(int program, String name); + public abstract void bindAttribLocation(int program, int index, String name); + public abstract int getUniformLocation(int program, String name); + public abstract String getActiveUniform(int program, int index, IntBuffer size, IntBuffer type); + public abstract void uniform1i(int location, int value); + public abstract void uniform2i(int location, int value0, int value1); + public abstract void uniform3i(int location, int value0, int value1, int value2); + public abstract void uniform4i(int location, int value0, int value1, int value2, int value3); + public abstract void uniform1f(int location, float value); + public abstract void uniform2f(int location, float value0, float value1); + public abstract void uniform3f(int location, float value0, float value1, float value2); + public abstract void uniform4f(int location, float value0, float value1, float value2, float value3); + public abstract void uniform1iv(int location, int count, IntBuffer v); + public abstract void uniform2iv(int location, int count, IntBuffer v); + public abstract void uniform3iv(int location, int count, IntBuffer v); + public abstract void uniform4iv(int location, int count, IntBuffer v); + public abstract void uniform1fv(int location, int count, FloatBuffer v); + public abstract void uniform2fv(int location, int count, FloatBuffer v); + public abstract void uniform3fv(int location, int count, FloatBuffer v); + public abstract void uniform4fv(int location, int count, FloatBuffer v); + public abstract void uniformMatrix2fv(int location, int count, boolean transpose, FloatBuffer mat); + public abstract void uniformMatrix3fv(int location, int count, boolean transpose, FloatBuffer mat); + public abstract void uniformMatrix4fv(int location, int count, boolean transpose, FloatBuffer mat); + public abstract void validateProgram(int program); + public abstract boolean isShader(int shader); + public abstract void getShaderiv(int shader, int pname, IntBuffer params); + public abstract void getAttachedShaders(int program, int maxCount, IntBuffer count, IntBuffer shaders); + public abstract String getShaderInfoLog(int shader); + public abstract String getShaderSource(int shader); + public abstract void getShaderPrecisionFormat(int shaderType, int precisionType, IntBuffer range, IntBuffer precision); + public abstract void getVertexAttribfv(int index, int pname, FloatBuffer params); + public abstract void getVertexAttribiv(int index, int pname, IntBuffer params); + public abstract void getVertexAttribPointerv(int index, int pname, ByteBuffer data); + public abstract void getUniformfv(int program, int location, FloatBuffer params); + public abstract void getUniformiv(int program, int location, IntBuffer params); + public abstract boolean isProgram(int program); + public abstract void getProgramiv(int program, int pname, IntBuffer params); + public abstract String getProgramInfoLog(int program); + + /////////////////////////////////////////////////////////// + + // Per-Fragment Operations + + public abstract void scissor(int x, int y, int w, int h); + public abstract void sampleCoverage(float value, boolean invert); + public abstract void stencilFunc(int func, int ref, int mask); + public abstract void stencilFuncSeparate(int face, int func, int ref, int mask); + public abstract void stencilOp(int sfail, int dpfail, int dppass); + public abstract void stencilOpSeparate(int face, int sfail, int dpfail, int dppass); + public abstract void depthFunc(int func); + public abstract void blendEquation(int mode); + public abstract void blendEquationSeparate(int modeRGB, int modeAlpha); + public abstract void blendFunc(int src, int dst); + public abstract void blendFuncSeparate(int srcRGB, int dstRGB, int srcAlpha, int dstAlpha); + public abstract void blendColor(float red, float green, float blue, float alpha); + + /////////////////////////////////////////////////////////// + + // Whole Framebuffer Operations + + public abstract void colorMask(boolean r, boolean g, boolean b, boolean a); + public abstract void depthMask(boolean mask); + public abstract void stencilMask(int mask); + public abstract void stencilMaskSeparate(int face, int mask); + public abstract void clearColor(float r, float g, float b, float a); + public abstract void clearDepth(float d); + public abstract void clearStencil(int s); + public abstract void clear(int buf); + + /////////////////////////////////////////////////////////// + + // Framebuffers Objects + + public void bindFramebuffer(int target, int framebuffer) { + graphics.beginBindFramebuffer(target, framebuffer); + bindFramebufferImpl(target, framebuffer); + graphics.endBindFramebuffer(target, framebuffer); + } + protected abstract void bindFramebufferImpl(int target, int framebuffer); + + public abstract void deleteFramebuffers(int n, IntBuffer framebuffers); + public abstract void genFramebuffers(int n, IntBuffer framebuffers); + public abstract void bindRenderbuffer(int target, int renderbuffer); + public abstract void deleteRenderbuffers(int n, IntBuffer renderbuffers); + public abstract void genRenderbuffers(int n, IntBuffer renderbuffers); + public abstract void renderbufferStorage(int target, int internalFormat, int width, int height); + public abstract void framebufferRenderbuffer(int target, int attachment, int rendbuferfTarget, int renderbuffer); + public abstract void framebufferTexture2D(int target, int attachment, int texTarget, int texture, int level); + public abstract int checkFramebufferStatus(int target); + public abstract boolean isFramebuffer(int framebuffer); + public abstract void getFramebufferAttachmentParameteriv(int target, int attachment, int pname, IntBuffer params); + public abstract boolean isRenderbuffer(int renderbuffer); + public abstract void getRenderbufferParameteriv(int target, int pname, IntBuffer params); + public abstract void blitFramebuffer(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter); + public abstract void renderbufferStorageMultisample(int target, int samples, int format, int width, int height); + public abstract void readBuffer(int buf); + public abstract void drawBuffer(int buf); +} diff --git a/src/main/java/processing/opengl/PGraphics2D.java b/src/main/java/processing/opengl/PGraphics2D.java new file mode 100644 index 0000000..8fbdd5e --- /dev/null +++ b/src/main/java/processing/opengl/PGraphics2D.java @@ -0,0 +1,615 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.opengl; + +import processing.core.PGraphics; +import processing.core.PMatrix3D; +import processing.core.PShape; +import processing.core.PShapeSVG; + + +public class PGraphics2D extends PGraphicsOpenGL { + + public PGraphics2D() { + super(); + } + + + ////////////////////////////////////////////////////////////// + + // RENDERER SUPPORT QUERIES + + + @Override + public boolean is2D() { + return true; + } + + + @Override + public boolean is3D() { + return false; + } + + + ////////////////////////////////////////////////////////////// + + // HINTS + + + @Override + public void hint(int which) { + if (which == ENABLE_STROKE_PERSPECTIVE) { + showWarning("Strokes cannot be perspective-corrected in 2D."); + return; + } + super.hint(which); + } + + + ////////////////////////////////////////////////////////////// + + // PROJECTION + + + @Override + public void ortho() { + showMethodWarning("ortho"); + } + + + @Override + public void ortho(float left, float right, + float bottom, float top) { + showMethodWarning("ortho"); + } + + + @Override + public void ortho(float left, float right, + float bottom, float top, + float near, float far) { + showMethodWarning("ortho"); + } + + + @Override + public void perspective() { + showMethodWarning("perspective"); + } + + + @Override + public void perspective(float fov, float aspect, float zNear, float zFar) { + showMethodWarning("perspective"); + } + + + @Override + public void frustum(float left, float right, float bottom, float top, + float znear, float zfar) { + showMethodWarning("frustum"); + } + + + @Override + protected void defaultPerspective() { + super.ortho(0, width, -height, 0, -1, +1); + } + + + ////////////////////////////////////////////////////////////// + + // CAMERA + + + @Override + public void beginCamera() { + showMethodWarning("beginCamera"); + } + + + @Override + public void endCamera() { + showMethodWarning("endCamera"); + } + + + @Override + public void camera() { + showMethodWarning("camera"); + } + + + @Override + public void camera(float eyeX, float eyeY, float eyeZ, + float centerX, float centerY, float centerZ, + float upX, float upY, float upZ) { + showMethodWarning("camera"); + } + + + @Override + protected void defaultCamera() { + eyeDist = 1; + resetMatrix(); + } + + + ////////////////////////////////////////////////////////////// + + // MATRIX MORE! + + + @Override + protected void begin2D() { + pushProjection(); + defaultPerspective(); + pushMatrix(); + defaultCamera(); + } + + + @Override + protected void end2D() { + popMatrix(); + popProjection(); + } + + + ////////////////////////////////////////////////////////////// + + // SHAPE + + + @Override + public void shape(PShape shape) { + if (shape.is2D()) { + super.shape(shape); + } else { + showWarning("The shape object is not 2D, cannot be displayed with " + + "this renderer"); + } + } + + + @Override + public void shape(PShape shape, float x, float y) { + if (shape.is2D()) { + super.shape(shape, x, y); + } else { + showWarning("The shape object is not 2D, cannot be displayed with " + + "this renderer"); + } + } + + + @Override + public void shape(PShape shape, float a, float b, float c, float d) { + if (shape.is2D()) { + super.shape(shape, a, b, c, d); + } else { + showWarning("The shape object is not 2D, cannot be displayed with " + + "this renderer"); + } + } + + + @Override + public void shape(PShape shape, float x, float y, float z) { + showDepthWarningXYZ("shape"); + } + + + @Override + public void shape(PShape shape, float x, float y, float z, + float c, float d, float e) { + showDepthWarningXYZ("shape"); + } + + + + ////////////////////////////////////////////////////////////// + + // SHAPE I/O + + + static protected boolean isSupportedExtension(String extension) { + return extension.equals("svg") || extension.equals("svgz"); + } + + + static protected PShape loadShapeImpl(PGraphics pg, + String filename, String extension) { + if (extension.equals("svg") || extension.equals("svgz")) { + PShapeSVG svg = new PShapeSVG(pg.parent.loadXML(filename)); + return PShapeOpenGL.createShape((PGraphicsOpenGL) pg, svg); + } + return null; + } + + + ////////////////////////////////////////////////////////////// + + // SCREEN TRANSFORMS + + + @Override + public float modelX(float x, float y, float z) { + showDepthWarning("modelX"); + return 0; + } + + + @Override + public float modelY(float x, float y, float z) { + showDepthWarning("modelY"); + return 0; + } + + + @Override + public float modelZ(float x, float y, float z) { + showDepthWarning("modelZ"); + return 0; + } + + + ////////////////////////////////////////////////////////////// + + // SHAPE CREATION + + +// @Override +// protected PShape createShapeFamily(int type) { +// return new PShapeOpenGL(this, type); +// } +// +// +// @Override +// protected PShape createShapePrimitive(int kind, float... p) { +// return new PShapeOpenGL(this, kind, p); +// } + + + /* + @Override + public PShape createShape(PShape source) { + return PShapeOpenGL.createShape2D(this, source); + } + + + @Override + public PShape createShape() { + return createShape(PShape.GEOMETRY); + } + + + @Override + public PShape createShape(int type) { + return createShapeImpl(this, type); + } + + + @Override + public PShape createShape(int kind, float... p) { + return createShapeImpl(this, kind, p); + } + + + static protected PShapeOpenGL createShapeImpl(PGraphicsOpenGL pg, int type) { + PShapeOpenGL shape = null; + if (type == PConstants.GROUP) { + shape = new PShapeOpenGL(pg, PConstants.GROUP); + } else if (type == PShape.PATH) { + shape = new PShapeOpenGL(pg, PShape.PATH); + } else if (type == PShape.GEOMETRY) { + shape = new PShapeOpenGL(pg, PShape.GEOMETRY); + } + shape.set3D(false); + return shape; + } + + + static protected PShapeOpenGL createShapeImpl(PGraphicsOpenGL pg, + int kind, float... p) { + PShapeOpenGL shape = null; + int len = p.length; + + if (kind == POINT) { + if (len != 2) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(POINT); + } else if (kind == LINE) { + if (len != 4) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(LINE); + } else if (kind == TRIANGLE) { + if (len != 6) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(TRIANGLE); + } else if (kind == QUAD) { + if (len != 8) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(QUAD); + } else if (kind == RECT) { + if (len != 4 && len != 5 && len != 8 && len != 9) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(RECT); + } else if (kind == ELLIPSE) { + if (len != 4 && len != 5) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(ELLIPSE); + } else if (kind == ARC) { + if (len != 6 && len != 7) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(ARC); + } else if (kind == BOX) { + showWarning("Primitive not supported in 2D"); + } else if (kind == SPHERE) { + showWarning("Primitive not supported in 2D"); + } else { + showWarning("Unrecognized primitive type"); + } + + if (shape != null) { + shape.setParams(p); + } + + shape.set3D(false); + return shape; + } + */ + + + ////////////////////////////////////////////////////////////// + + // BEZIER VERTICES + + + @Override + public void bezierVertex(float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + showDepthWarningXYZ("bezierVertex"); + } + + + ////////////////////////////////////////////////////////////// + + // QUADRATIC BEZIER VERTICES + + + @Override + public void quadraticVertex(float x2, float y2, float z2, + float x4, float y4, float z4) { + showDepthWarningXYZ("quadVertex"); + } + + + ////////////////////////////////////////////////////////////// + + // CURVE VERTICES + + + @Override + public void curveVertex(float x, float y, float z) { + showDepthWarningXYZ("curveVertex"); + } + + + ////////////////////////////////////////////////////////////// + + // BOX + + + @Override + public void box(float w, float h, float d) { + showMethodWarning("box"); + } + + + ////////////////////////////////////////////////////////////// + + // SPHERE + + + @Override + public void sphere(float r) { + showMethodWarning("sphere"); + } + + + ////////////////////////////////////////////////////////////// + + // VERTEX SHAPES + + + @Override + public void vertex(float x, float y, float z) { + showDepthWarningXYZ("vertex"); + } + + @Override + public void vertex(float x, float y, float z, float u, float v) { + showDepthWarningXYZ("vertex"); + } + + ////////////////////////////////////////////////////////////// + + // MATRIX TRANSFORMATIONS + + @Override + public void translate(float tx, float ty, float tz) { + showDepthWarningXYZ("translate"); + } + + @Override + public void rotateX(float angle) { + showDepthWarning("rotateX"); + } + + @Override + public void rotateY(float angle) { + showDepthWarning("rotateY"); + } + + @Override + public void rotateZ(float angle) { + showDepthWarning("rotateZ"); + } + + @Override + public void rotate(float angle, float vx, float vy, float vz) { + showVariationWarning("rotate"); + } + + @Override + public void applyMatrix(PMatrix3D source) { + showVariationWarning("applyMatrix"); + } + + @Override + public void applyMatrix(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + showVariationWarning("applyMatrix"); + } + + @Override + public void scale(float sx, float sy, float sz) { + showDepthWarningXYZ("scale"); + } + + ////////////////////////////////////////////////////////////// + + // SCREEN AND MODEL COORDS + + @Override + public float screenX(float x, float y, float z) { + showDepthWarningXYZ("screenX"); + return 0; + } + + @Override + public float screenY(float x, float y, float z) { + showDepthWarningXYZ("screenY"); + return 0; + } + + @Override + public float screenZ(float x, float y, float z) { + showDepthWarningXYZ("screenZ"); + return 0; + } + + @Override + public PMatrix3D getMatrix(PMatrix3D target) { + showVariationWarning("getMatrix"); + return target; + } + + @Override + public void setMatrix(PMatrix3D source) { + showVariationWarning("setMatrix"); + } + + ////////////////////////////////////////////////////////////// + + // LIGHTS + + @Override + public void lights() { + showMethodWarning("lights"); + } + + @Override + public void noLights() { + showMethodWarning("noLights"); + } + + @Override + public void ambientLight(float red, float green, float blue) { + showMethodWarning("ambientLight"); + } + + @Override + public void ambientLight(float red, float green, float blue, + float x, float y, float z) { + showMethodWarning("ambientLight"); + } + + @Override + public void directionalLight(float red, float green, float blue, + float nx, float ny, float nz) { + showMethodWarning("directionalLight"); + } + + @Override + public void pointLight(float red, float green, float blue, + float x, float y, float z) { + showMethodWarning("pointLight"); + } + + @Override + public void spotLight(float red, float green, float blue, + float x, float y, float z, + float nx, float ny, float nz, + float angle, float concentration) { + showMethodWarning("spotLight"); + } + + @Override + public void lightFalloff(float constant, float linear, float quadratic) { + showMethodWarning("lightFalloff"); + } + + @Override + public void lightSpecular(float v1, float v2, float v3) { + showMethodWarning("lightSpecular"); + } +} \ No newline at end of file diff --git a/src/main/java/processing/opengl/PGraphics3D.java b/src/main/java/processing/opengl/PGraphics3D.java new file mode 100644 index 0000000..50b18b1 --- /dev/null +++ b/src/main/java/processing/opengl/PGraphics3D.java @@ -0,0 +1,281 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.opengl; + +import processing.core.PGraphics; +import processing.core.PShape; +import processing.core.PShapeOBJ; + + +public class PGraphics3D extends PGraphicsOpenGL { + + public PGraphics3D() { + super(); + } + + + ////////////////////////////////////////////////////////////// + + // RENDERER SUPPORT QUERIES + + + @Override + public boolean is2D() { + return false; + } + + + @Override + public boolean is3D() { + return true; + } + + + ////////////////////////////////////////////////////////////// + + // PROJECTION + + + @Override + protected void defaultPerspective() { + perspective(); + } + + + ////////////////////////////////////////////////////////////// + + // CAMERA + + + @Override + protected void defaultCamera() { + camera(); + } + + + ////////////////////////////////////////////////////////////// + + // MATRIX MORE! + + + @Override + protected void begin2D() { + pushProjection(); + ortho(-width/2f, width/2f, -height/2f, height/2f); + pushMatrix(); + + // Set camera for 2D rendering, it simply centers at (width/2, height/2) + float centerX = width/2f; + float centerY = height/2f; + modelview.reset(); + modelview.translate(-centerX, -centerY); + + modelviewInv.set(modelview); + modelviewInv.invert(); + + camera.set(modelview); + cameraInv.set(modelviewInv); + + updateProjmodelview(); + } + + + @Override + protected void end2D() { + popMatrix(); + popProjection(); + } + + + + ////////////////////////////////////////////////////////////// + + // SHAPE I/O + + + static protected boolean isSupportedExtension(String extension) { + return extension.equals("obj"); + } + + + static protected PShape loadShapeImpl(PGraphics pg, String filename, + String extension) { + PShapeOBJ obj = null; + + if (extension.equals("obj")) { + obj = new PShapeOBJ(pg.parent, filename); + int prevTextureMode = pg.textureMode; + pg.textureMode = NORMAL; + PShapeOpenGL p3d = PShapeOpenGL.createShape((PGraphicsOpenGL)pg, obj); + pg.textureMode = prevTextureMode; + return p3d; + } + return null; + } + + + + ////////////////////////////////////////////////////////////// + + // SHAPE CREATION + + +// @Override +// protected PShape createShapeFamily(int type) { +// PShape shape = new PShapeOpenGL(this, type); +// shape.set3D(true); +// return shape; +// } +// +// +// @Override +// protected PShape createShapePrimitive(int kind, float... p) { +// PShape shape = new PShapeOpenGL(this, kind, p); +// shape.set3D(true); +// return shape; +// } + + + /* + @Override + public PShape createShape(PShape source) { + return PShapeOpenGL.createShape3D(this, source); + } + + + @Override + public PShape createShape() { + return createShape(PShape.GEOMETRY); + } + + + @Override + public PShape createShape(int type) { + return createShapeImpl(this, type); + } + + + @Override + public PShape createShape(int kind, float... p) { + return createShapeImpl(this, kind, p); + } + + + static protected PShapeOpenGL createShapeImpl(PGraphicsOpenGL pg, int type) { + PShapeOpenGL shape = null; + if (type == PConstants.GROUP) { + shape = new PShapeOpenGL(pg, PConstants.GROUP); + } else if (type == PShape.PATH) { + shape = new PShapeOpenGL(pg, PShape.PATH); + } else if (type == PShape.GEOMETRY) { + shape = new PShapeOpenGL(pg, PShape.GEOMETRY); + } + shape.set3D(true); + return shape; + } + + + static protected PShapeOpenGL createShapeImpl(PGraphicsOpenGL pg, + int kind, float... p) { + PShapeOpenGL shape = null; + int len = p.length; + + if (kind == POINT) { + if (len != 2 && len != 3) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(POINT); + } else if (kind == LINE) { + if (len != 4 && len != 6) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(LINE); + } else if (kind == TRIANGLE) { + if (len != 6) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(TRIANGLE); + } else if (kind == QUAD) { + if (len != 8) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(QUAD); + } else if (kind == RECT) { + if (len != 4 && len != 5 && len != 8 && len != 9) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(RECT); + } else if (kind == ELLIPSE) { + if (len != 4 && len != 5) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(ELLIPSE); + } else if (kind == ARC) { + if (len != 6 && len != 7) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(ARC); + + } else if (kind == BOX) { + if (len != 1 && len != 3) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(BOX); + } else if (kind == SPHERE) { + if (len < 1 || 3 < len) { + showWarning("Wrong number of parameters"); + return null; + } + shape = new PShapeOpenGL(pg, PShape.PRIMITIVE); + shape.setKind(SPHERE); + } else { + showWarning("Unrecognized primitive type"); + } + + if (shape != null) { + shape.setParams(p); + } + + shape.set3D(true); + return shape; + } + */ +} \ No newline at end of file diff --git a/src/main/java/processing/opengl/PGraphicsOpenGL.java b/src/main/java/processing/opengl/PGraphicsOpenGL.java new file mode 100644 index 0000000..f2e6451 --- /dev/null +++ b/src/main/java/processing/opengl/PGraphicsOpenGL.java @@ -0,0 +1,13634 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.opengl; + +import processing.core.*; + +import java.io.File; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.net.URL; +import java.nio.*; +import java.util.*; + + +/** + * OpenGL renderer. + */ +public class PGraphicsOpenGL extends PGraphics { + /** Interface between Processing and OpenGL */ + public PGL pgl; + + /** The renderer currently in use. */ + public PGraphicsOpenGL currentPG; + + /** Font cache for texture objects. */ + protected WeakHashMap fontMap; + + // ........................................................ + + // Disposal of native resources + // Using the technique alternative to finalization described in: + // http://www.oracle.com/technetwork/articles/java/finalization-137655.html + private static ReferenceQueue refQueue = new ReferenceQueue<>(); + private static List> reachableWeakReferences = + new LinkedList<>(); + + static final private int MAX_DRAIN_GLRES_ITERATIONS = 10; + + static void drainRefQueueBounded() { + int iterations = 0; + while (iterations < MAX_DRAIN_GLRES_ITERATIONS) { + Disposable res = + (Disposable) refQueue.poll(); + if (res == null) { + break; + } + res.dispose(); + ++iterations; + } + } + + private static abstract class Disposable extends WeakReference { + protected Disposable(T obj) { + super(obj, refQueue); + drainRefQueueBounded(); + reachableWeakReferences.add(this); + } + + public void dispose() { + reachableWeakReferences.remove(this); + disposeNative(); + } + + abstract public void disposeNative(); + } + + // Basic rendering parameters: + + /** Whether the PGraphics object is ready to render or not. */ + public boolean initialized; + + /** Flush modes: continuously (geometry is flushed after each call to + * endShape) when-full (geometry is accumulated until a maximum size is + * reached. */ + static protected final int FLUSH_CONTINUOUSLY = 0; + static protected final int FLUSH_WHEN_FULL = 1; + + /** Type of geometry: immediate is that generated with beginShape/vertex/ + * endShape, retained is the result of creating a PShapeOpenGL object with + * createShape. */ + static protected final int IMMEDIATE = 0; + static protected final int RETAINED = 1; + + /** Current flush mode. */ + protected int flushMode = FLUSH_WHEN_FULL; + + // ........................................................ + + // VBOs for immediate rendering: + + protected VertexBuffer bufPolyVertex; + protected VertexBuffer bufPolyColor; + protected VertexBuffer bufPolyNormal; + protected VertexBuffer bufPolyTexcoord; + protected VertexBuffer bufPolyAmbient; + protected VertexBuffer bufPolySpecular; + protected VertexBuffer bufPolyEmissive; + protected VertexBuffer bufPolyShininess; + protected VertexBuffer bufPolyIndex; + protected boolean polyBuffersCreated = false; + protected int polyBuffersContext; + + protected VertexBuffer bufLineVertex; + protected VertexBuffer bufLineColor; + protected VertexBuffer bufLineAttrib; + protected VertexBuffer bufLineIndex; + protected boolean lineBuffersCreated = false; + protected int lineBuffersContext; + + protected VertexBuffer bufPointVertex; + protected VertexBuffer bufPointColor; + protected VertexBuffer bufPointAttrib; + protected VertexBuffer bufPointIndex; + protected boolean pointBuffersCreated = false; + protected int pointBuffersContext; + + // Generic vertex attributes (only for polys) + protected AttributeMap polyAttribs; + + static protected final int INIT_VERTEX_BUFFER_SIZE = 256; + static protected final int INIT_INDEX_BUFFER_SIZE = 512; + + // ........................................................ + + // GL parameters + + static protected boolean glParamsRead = false; + + /** Extensions used by Processing */ + static public boolean npotTexSupported; + static public boolean autoMipmapGenSupported; + static public boolean fboMultisampleSupported; + static public boolean packedDepthStencilSupported; + static public boolean anisoSamplingSupported; + static public boolean blendEqSupported; + static public boolean readBufferSupported; + static public boolean drawBufferSupported; + + /** Some hardware limits */ + static public int maxTextureSize; + static public int maxSamples; + static public float maxAnisoAmount; + static public int depthBits; + static public int stencilBits; + + /** OpenGL information strings */ + static public String OPENGL_VENDOR; + static public String OPENGL_RENDERER; + static public String OPENGL_VERSION; + static public String OPENGL_EXTENSIONS; + static public String GLSL_VERSION; + + // ........................................................ + + // Shaders + + static protected URL defColorShaderVertURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/ColorVert.glsl"); + static protected URL defTextureShaderVertURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/TexVert.glsl"); + static protected URL defLightShaderVertURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/LightVert.glsl"); + static protected URL defTexlightShaderVertURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/TexLightVert.glsl"); + static protected URL defColorShaderFragURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/ColorFrag.glsl"); + static protected URL defTextureShaderFragURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/TexFrag.glsl"); + static protected URL defLightShaderFragURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/LightFrag.glsl"); + static protected URL defTexlightShaderFragURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/TexLightFrag.glsl"); + + static protected URL defLineShaderVertURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/LineVert.glsl"); + static protected URL defLineShaderFragURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/LineFrag.glsl"); + static protected URL defPointShaderVertURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/PointVert.glsl"); + static protected URL defPointShaderFragURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/PointFrag.glsl"); + static protected URL maskShaderFragURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/MaskFrag.glsl"); + + protected PShader defColorShader; + protected PShader defTextureShader; + protected PShader defLightShader; + protected PShader defTexlightShader; + protected PShader defLineShader; + protected PShader defPointShader; + protected PShader maskShader; + + protected PShader polyShader; + protected PShader lineShader; + protected PShader pointShader; + + // ........................................................ + + // Tessellator, geometry + + protected InGeometry inGeo; + protected TessGeometry tessGeo; + protected TexCache texCache; + protected Tessellator tessellator; + + // ........................................................ + + // Depth sorter + + protected DepthSorter sorter; + protected boolean isDepthSortingEnabled; + + // ........................................................ + + // Async pixel reader + + protected AsyncPixelReader asyncPixelReader; + protected boolean asyncPixelReaderInitialized; + + // Keeps track of ongoing transfers so they can be finished. + // Set is copied to the List when we need to iterate it + // so that readers can remove themselves from the Set during + // iteration if they don't have any ongoing transfers. + protected static final Set + ongoingPixelTransfers = new HashSet<>(); + protected static final List + ongoingPixelTransfersIterable = new ArrayList<>(); + + // ........................................................ + + // Camera: + + /** Camera field of view. */ + public float cameraFOV; + + /** Default position of the camera. */ + public float cameraX, cameraY, cameraZ; + /** Distance of the near and far planes. */ + public float cameraNear, cameraFar; + /** Aspect ratio of camera's view. */ + public float cameraAspect; + + /** Default camera properties. */ + public float defCameraFOV; + public float defCameraX, defCameraY, defCameraZ; + public float defCameraNear, defCameraFar; + public float defCameraAspect; + + /** Distance between camera eye and center. */ + protected float eyeDist; + + /** Flag to indicate that we are inside beginCamera/endCamera block. */ + protected boolean manipulatingCamera; + + // ........................................................ + + // All the matrices required for camera and geometry transformations. + public PMatrix3D projection; + public PMatrix3D camera; + public PMatrix3D cameraInv; + public PMatrix3D modelview; + public PMatrix3D modelviewInv; + public PMatrix3D projmodelview; + + // To pass to shaders + protected float[] glProjection; + protected float[] glModelview; + protected float[] glProjmodelview; + protected float[] glNormal; + + // Useful to have around. + static protected PMatrix3D identity = new PMatrix3D(); + + /** + * Marks when changes to the size have occurred, so that the camera + * will be reset in beginDraw(). + */ + protected boolean sized; + + static protected final int MATRIX_STACK_DEPTH = 32; + + protected int modelviewStackDepth; + protected int projectionStackDepth; + + /** Modelview matrix stack **/ + protected float[][] modelviewStack = new float[MATRIX_STACK_DEPTH][16]; + + /** Inverse modelview matrix stack **/ + protected float[][] modelviewInvStack = new float[MATRIX_STACK_DEPTH][16]; + + /** Camera matrix stack **/ + protected float[][] cameraStack = new float[MATRIX_STACK_DEPTH][16]; + + /** Inverse camera matrix stack **/ + protected float[][] cameraInvStack = new float[MATRIX_STACK_DEPTH][16]; + + /** Projection matrix stack **/ + protected float[][] projectionStack = new float[MATRIX_STACK_DEPTH][16]; + + // ........................................................ + + // Lights: + + public boolean lights; + public int lightCount = 0; + + /** Light types */ + public int[] lightType; + + /** Light positions */ + public float[] lightPosition; + + /** Light direction (normalized vector) */ + public float[] lightNormal; + + /** + * Ambient colors for lights. + */ + public float[] lightAmbient; + + /** + * Diffuse colors for lights. + */ + public float[] lightDiffuse; + + /** + * Specular colors for lights. Internally these are stored as numbers between + * 0 and 1. + */ + public float[] lightSpecular; + + /** Light falloff */ + public float[] lightFalloffCoefficients; + + /** Light spot parameters: Cosine of light spot angle + * and concentration */ + public float[] lightSpotParameters; + + /** Current specular color for lighting */ + public float[] currentLightSpecular; + + /** Current light falloff */ + public float currentLightFalloffConstant; + public float currentLightFalloffLinear; + public float currentLightFalloffQuadratic; + + // ........................................................ + + // Texturing: + + protected int textureWrap = CLAMP; + protected int textureSampling = Texture.TRILINEAR; + + // ........................................................ + + // Clipping + + protected boolean clip = false; + + /** Clipping rectangle. */ + protected int[] clipRect = {0, 0, 0, 0}; + + + // ........................................................ + + // Text: + + /** Font texture of currently selected font. */ + FontTexture textTex; + + // ....................................................... + + // Framebuffer stack: + + static protected final int FB_STACK_DEPTH = 16; + + protected int fbStackDepth; + protected FrameBuffer[] fbStack; + protected FrameBuffer drawFramebuffer; + protected FrameBuffer readFramebuffer; + protected FrameBuffer currentFramebuffer; + + // ....................................................... + + // Offscreen rendering: + + protected FrameBuffer offscreenFramebuffer; + protected FrameBuffer multisampleFramebuffer; + protected boolean offscreenMultisample; + + protected boolean pixOpChangedFB; + + // ........................................................ + + // Screen surface: + + /** Texture containing the current frame */ + protected Texture texture = null; + + /** Texture containing the previous frame */ + protected Texture ptexture = null; + + /** IntBuffer wrapping the pixels array. */ + protected IntBuffer pixelBuffer; + + /** Array to store pixels in OpenGL format. */ + protected int[] nativePixels; + + /** IntBuffer wrapping the native pixels array. */ + protected IntBuffer nativePixelBuffer; + + /** texture used to apply a filter on the screen image. */ + protected Texture filterTexture = null; + + /** PImage that wraps filterTexture. */ + protected PImage filterImage; + + // ........................................................ + + // Utility variables: + + /** True if we are inside a beginDraw()/endDraw() block. */ + protected boolean drawing = false; + + /** Used to detect continuous use of the smooth/noSmooth functions */ + protected boolean smoothDisabled = false; + protected int smoothCallCount = 0; + protected int lastSmoothCall = -10; + + /** Used to avoid flushing the geometry when blendMode() is called with the + * same blend mode as the last */ + protected int lastBlendMode = -1; + + /** Type of pixels operation. */ + static protected final int OP_NONE = 0; + static protected final int OP_READ = 1; + static protected final int OP_WRITE = 2; + protected int pixelsOp = OP_NONE; + + /** Viewport dimensions. */ + protected IntBuffer viewport; + + protected boolean openContour = false; + protected boolean breakShape = false; + protected boolean defaultEdges = false; + + static protected final int EDGE_MIDDLE = 0; + static protected final int EDGE_START = 1; + static protected final int EDGE_STOP = 2; + static protected final int EDGE_SINGLE = 3; + static protected final int EDGE_CLOSE = -1; + + /** Used in round point and ellipse tessellation. The + * number of subdivisions per round point or ellipse is + * calculated with the following formula: + * n = min(M, max(N, (TWO_PI * size / F))) + * where size is a measure of the dimensions of the circle + * when projected on screen coordinates. F just sets the + * minimum number of subdivisions, while a smaller F + * would allow to have more detailed circles. + * N = MIN_POINT_ACCURACY + * M = MAX_POINT_ACCURACY + * F = POINT_ACCURACY_FACTOR + */ + final static protected int MIN_POINT_ACCURACY = 20; + final static protected int MAX_POINT_ACCURACY = 200; + final static protected float POINT_ACCURACY_FACTOR = 10.0f; + + /** Used in quad point tessellation. */ + final static protected float[][] QUAD_POINT_SIGNS = + { {-1, +1}, {-1, -1}, {+1, -1}, {+1, +1} }; + + /** To get data from OpenGL. */ + static protected IntBuffer intBuffer; + static protected FloatBuffer floatBuffer; + + // ........................................................ + + // Error strings: + + static final String OPENGL_THREAD_ERROR = + "Cannot run the OpenGL renderer outside the main thread, change your code" + + "\nso the drawing calls are all inside the main thread, " + + "\nor use the default renderer instead."; + static final String BLEND_DRIVER_ERROR = + "blendMode(%1$s) is not supported by this hardware (or driver)"; + static final String BLEND_RENDERER_ERROR = + "blendMode(%1$s) is not supported by this renderer"; + static final String ALREADY_BEGAN_CONTOUR_ERROR = + "Already called beginContour()"; + static final String NO_BEGIN_CONTOUR_ERROR = + "Need to call beginContour() first"; + static final String UNSUPPORTED_SMOOTH_LEVEL_ERROR = + "Smooth level %1$s is not available. Using %2$s instead"; + static final String UNSUPPORTED_SMOOTH_ERROR = + "Smooth is not supported by this hardware (or driver)"; + static final String TOO_MANY_SMOOTH_CALLS_ERROR = + "The smooth/noSmooth functions are being called too often.\n" + + "This results in screen flickering, so they will be disabled\n" + + "for the rest of the sketch's execution"; + static final String UNSUPPORTED_SHAPE_FORMAT_ERROR = + "Unsupported shape format"; + static final String MISSING_UV_TEXCOORDS_ERROR = + "No uv texture coordinates supplied with vertex() call"; + static final String INVALID_FILTER_SHADER_ERROR = + "Your shader cannot be used as a filter because is of type POINT or LINES"; + static final String INCONSISTENT_SHADER_TYPES = + "The vertex and fragment shaders have different types"; + static final String WRONG_SHADER_TYPE_ERROR = + "shader() called with a wrong shader"; + static final String SHADER_NEED_LIGHT_ATTRIBS = + "The provided shader needs light attributes (ambient, diffuse, etc.), but " + + "the current scene is unlit, so the default shader will be used instead"; + static final String MISSING_FRAGMENT_SHADER = + "The fragment shader is missing, cannot create shader object"; + static final String MISSING_VERTEX_SHADER = + "The vertex shader is missing, cannot create shader object"; + static final String UNKNOWN_SHADER_KIND_ERROR = + "Unknown shader kind"; + static final String NO_TEXLIGHT_SHADER_ERROR = + "Your shader needs to be of TEXLIGHT type " + + "to render this geometry properly, using default shader instead."; + static final String NO_LIGHT_SHADER_ERROR = + "Your shader needs to be of LIGHT type " + + "to render this geometry properly, using default shader instead."; + static final String NO_TEXTURE_SHADER_ERROR = + "Your shader needs to be of TEXTURE type " + + "to render this geometry properly, using default shader instead."; + static final String NO_COLOR_SHADER_ERROR = + "Your shader needs to be of COLOR type " + + "to render this geometry properly, using default shader instead."; + static final String TESSELLATION_ERROR = + "Tessellation Error: %1$s"; + static final String GL_THREAD_NOT_CURRENT = + "You are trying to draw outside OpenGL's animation thread.\n" + + "Place all drawing commands in the draw() function, or inside\n" + + "your own functions as long as they are called from draw(),\n" + + "but not in event handling functions such as keyPressed()\n" + + "or mousePressed()."; + + ////////////////////////////////////////////////////////////// + + // INIT/ALLOCATE/FINISH + + + public PGraphicsOpenGL() { + pgl = createPGL(this); + + if (intBuffer == null) { + intBuffer = PGL.allocateIntBuffer(2); + floatBuffer = PGL.allocateFloatBuffer(2); + } + + viewport = PGL.allocateIntBuffer(4); + + polyAttribs = newAttributeMap(); + inGeo = newInGeometry(this, polyAttribs, IMMEDIATE); + tessGeo = newTessGeometry(this, polyAttribs, IMMEDIATE); + texCache = newTexCache(this); + + projection = new PMatrix3D(); + camera = new PMatrix3D(); + cameraInv = new PMatrix3D(); + modelview = new PMatrix3D(); + modelviewInv = new PMatrix3D(); + projmodelview = new PMatrix3D(); + + lightType = new int[PGL.MAX_LIGHTS]; + lightPosition = new float[4 * PGL.MAX_LIGHTS]; + lightNormal = new float[3 * PGL.MAX_LIGHTS]; + lightAmbient = new float[3 * PGL.MAX_LIGHTS]; + lightDiffuse = new float[3 * PGL.MAX_LIGHTS]; + lightSpecular = new float[3 * PGL.MAX_LIGHTS]; + lightFalloffCoefficients = new float[3 * PGL.MAX_LIGHTS]; + lightSpotParameters = new float[2 * PGL.MAX_LIGHTS]; + currentLightSpecular = new float[3]; + + initialized = false; + } + + + @Override + public void setParent(PApplet parent) { + super.setParent(parent); + if (pgl != null) { + pgl.sketch = parent; + } + } + + + @Override + public void setPrimary(boolean primary) { + super.setPrimary(primary); + pgl.setPrimary(primary); + format = ARGB; + if (primary) { + fbStack = new FrameBuffer[FB_STACK_DEPTH]; + fontMap = new WeakHashMap<>(); + tessellator = new Tessellator(); + } else { + tessellator = getPrimaryPG().tessellator; + } + } + + + //public void setPath(String path) // PGraphics + + + //public void setAntiAlias(int samples) // PGraphics + + + @Override + public void setSize(int iwidth, int iheight) { + width = iwidth; + height = iheight; + updatePixelSize(); + + texture = null; + ptexture = null; + + // init perspective projection based on new dimensions + defCameraFOV = 60 * DEG_TO_RAD; // at least for now + defCameraX = width / 2.0f; + defCameraY = height / 2.0f; + defCameraZ = defCameraY / ((float) Math.tan(defCameraFOV / 2.0f)); + defCameraNear = defCameraZ / 10.0f; + defCameraFar = defCameraZ * 10.0f; + defCameraAspect = (float) width / (float) height; + + cameraFOV = defCameraFOV; + cameraX = defCameraX; + cameraY = defCameraY; + cameraZ = defCameraZ; + cameraNear = defCameraNear; + cameraFar = defCameraFar; + cameraAspect = defCameraAspect; + + sized = true; + } + + + @Override + public void dispose() { // PGraphics + if (asyncPixelReader != null) { + asyncPixelReader.dispose(); + asyncPixelReader = null; + } + + if (!primaryGraphics) { + deleteSurfaceTextures(); + FrameBuffer ofb = offscreenFramebuffer; + FrameBuffer mfb = multisampleFramebuffer; + if (ofb != null) { + ofb.dispose(); + } + if (mfb != null) { + mfb.dispose(); + } + } + + pgl.dispose(); + + super.dispose(); + } + + + protected void setFlushMode(int mode) { + flushMode = mode; + } + + + protected void updatePixelSize() { + float f = pgl.getPixelScale(); + pixelWidth = (int)(width * f); + pixelHeight = (int)(height * f); + if (primaryGraphics) { + parent.pixelWidth = pixelWidth; + parent.pixelHeight = pixelHeight; + } + } + + + ////////////////////////////////////////////////////////////// + + // PLATFORM-SPECIFIC CODE (Java, Android, etc.). Needs to be manually edited. + + + // Factory method + protected PGL createPGL(PGraphicsOpenGL pg) { + return new PJOGL(pg); +// return new PGLES(pg); + } + + +/* + @Override + // Android only + public void setFrameRate(float frameRate) { + pgl.setFrameRate(frameRate); + } + + + @Override + // Android only + public boolean canDraw() { + return pgl.canDraw(); + } + + + @Override + // Android only + public void requestDraw() { + if (primaryGraphics) { + if (initialized) { + if (sized) pgl.reinitSurface(); + if (parent.canDraw()) pgl.requestDraw(); + } else { + initPrimary(); + } + } + } +*/ + + + @Override + // Java only + public PSurface createSurface() { // ignore + return surface = new PSurfaceJOGL(this); + } + + + public boolean saveImpl(String filename) { +// return super.save(filename); // ASYNC save frame using PBOs not yet available on Android + + if (getHint(DISABLE_ASYNC_SAVEFRAME)) { + // Act as an opaque surface for the purposes of saving. + if (primaryGraphics) { + int prevFormat = format; + format = RGB; + boolean result = super.save(filename); + format = prevFormat; + return result; + } + + return super.save(filename); + } + + if (asyncImageSaver == null) { + asyncImageSaver = new AsyncImageSaver(); + } + + if (!asyncPixelReaderInitialized) { + // First call! Get this guy initialized + if (pgl.hasPBOs() && pgl.hasSynchronization()) { + asyncPixelReader = new AsyncPixelReader(); + } + asyncPixelReaderInitialized = true; + } + + if (asyncPixelReader != null && !loaded) { + boolean needEndDraw = false; + if (!drawing) { + beginDraw(); + needEndDraw = true; + } + flush(); + updatePixelSize(); + + // get the whole async package + asyncPixelReader.readAndSaveAsync(parent.sketchFile(filename)); + + if (needEndDraw) endDraw(); + } else { + // async transfer is not supported or + // pixels are already in memory, just do async save + if (!loaded) loadPixels(); + int format = primaryGraphics ? RGB : ARGB; + PImage target = asyncImageSaver.getAvailableTarget(pixelWidth, pixelHeight, + format); + if (target == null) return false; + int count = PApplet.min(pixels.length, target.pixels.length); + System.arraycopy(pixels, 0, target.pixels, 0, count); + asyncImageSaver.saveTargetAsync(this, target, parent.sketchFile(filename)); + } + + return true; + } + + + ////////////////////////////////////////////////////////////// + + // IMAGE METADATA FOR THIS RENDERER + + + @Override + public void setCache(PImage image, Object storage) { + if (image instanceof PGraphicsOpenGL) { + // Prevent strong reference to the key from the value by wrapping + // the Texture into WeakReference (proposed solution by WeakHashMap docs) + getPrimaryPG().cacheMap.put(image, new WeakReference<>(storage)); + return; + } + getPrimaryPG().cacheMap.put(image, storage); + } + + + @Override + @SuppressWarnings("rawtypes") + public Object getCache(PImage image) { + Object storage = getPrimaryPG().cacheMap.get(image); + if (storage != null && storage.getClass() == WeakReference.class) { + // Unwrap the value, use getClass() for fast check + return ((WeakReference) storage).get(); + } + return storage; + } + + + @Override + public void removeCache(PImage image) { + getPrimaryPG().cacheMap.remove(image); + } + + + ////////////////////////////////////////////////////////////// + + + protected void setFontTexture(PFont font, FontTexture fontTexture) { + getPrimaryPG().fontMap.put(font, fontTexture); + } + + + protected FontTexture getFontTexture(PFont font) { + return getPrimaryPG().fontMap.get(font); + } + + + protected void removeFontTexture(PFont font) { + getPrimaryPG().fontMap.remove(font); + } + + + ////////////////////////////////////////////////////////////// + + + protected static class GLResourceTexture extends Disposable { + int glName; + + private PGL pgl; + private int context; + + public GLResourceTexture(Texture tex) { + super(tex); + + + pgl = tex.pg.getPrimaryPGL(); + pgl.genTextures(1, intBuffer); + tex.glName = intBuffer.get(0); + + this.glName = tex.glName; + this.context = tex.context; + } + + @Override + public void disposeNative() { + if (pgl != null) { + if (glName != 0) { + intBuffer.put(0, glName); + pgl.deleteTextures(1, intBuffer); + glName = 0; + } + pgl = null; + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof GLResourceTexture)) { + return false; + } + GLResourceTexture other = (GLResourceTexture)obj; + return other.glName == glName && + other.context == context; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + glName; + result = 31 * result + context; + return result; + } + } + + + protected static class GLResourceVertexBuffer extends Disposable { + int glId; + + private PGL pgl; + private int context; + + public GLResourceVertexBuffer(VertexBuffer vbo) { + super(vbo); + + pgl = vbo.pgl.graphics.getPrimaryPGL(); + pgl.genBuffers(1, intBuffer); + vbo.glId = intBuffer.get(0); + + this.glId = vbo.glId; + this.context = vbo.context; + } + + @Override + public void disposeNative() { + if (pgl != null) { + if (glId != 0) { + intBuffer.put(0, glId); + pgl.deleteBuffers(1, intBuffer); + glId = 0; + } + pgl = null; + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof GLResourceVertexBuffer)) { + return false; + } + GLResourceVertexBuffer other = (GLResourceVertexBuffer)obj; + return other.glId == glId && + other.context == context; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + glId; + result = 31 * result + context; + return result; + } + } + + + protected static class GLResourceShader extends Disposable { + int glProgram; + int glVertex; + int glFragment; + + private PGL pgl; + private int context; + + public GLResourceShader(PShader sh) { + super(sh); + + this.pgl = sh.pgl.graphics.getPrimaryPGL(); + sh.glProgram = pgl.createProgram(); + sh.glVertex = pgl.createShader(PGL.VERTEX_SHADER); + sh.glFragment = pgl.createShader(PGL.FRAGMENT_SHADER); + + this.glProgram = sh.glProgram; + this.glVertex = sh.glVertex; + this.glFragment = sh.glFragment; + + this.context = sh.context; + } + + @Override + public void disposeNative() { + if (pgl != null) { + if (glFragment != 0) { + pgl.deleteShader(glFragment); + glFragment = 0; + } + if (glVertex != 0) { + pgl.deleteShader(glVertex); + glVertex = 0; + } + if (glProgram != 0) { + pgl.deleteProgram(glProgram); + glProgram = 0; + } + pgl = null; + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof GLResourceShader)) { + return false; + } + GLResourceShader other = (GLResourceShader)obj; + return other.glProgram == glProgram && + other.glVertex == glVertex && + other.glFragment == glFragment && + other.context == context; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + glProgram; + result = 31 * result + glVertex; + result = 31 * result + glFragment; + result = 31 * result + context; + return result; + } + } + + + protected static class GLResourceFrameBuffer extends Disposable { + int glFbo; + int glDepth; + int glStencil; + int glDepthStencil; + int glMultisample; + + private PGL pgl; + private int context; + + public GLResourceFrameBuffer(FrameBuffer fb) { + super(fb); + + pgl = fb.pg.getPrimaryPGL(); + if (!fb.screenFb) { + pgl.genFramebuffers(1, intBuffer); + fb.glFbo = intBuffer.get(0); + + if (fb.multisample) { + pgl.genRenderbuffers(1, intBuffer); + fb.glMultisample = intBuffer.get(0); + } + + if (fb.packedDepthStencil) { + pgl.genRenderbuffers(1, intBuffer); + fb.glDepthStencil = intBuffer.get(0); + } else { + if (0 < fb.depthBits) { + pgl.genRenderbuffers(1, intBuffer); + fb.glDepth = intBuffer.get(0); + } + if (0 < fb.stencilBits) { + pgl.genRenderbuffers(1, intBuffer); + fb.glStencil = intBuffer.get(0); + } + } + + this.glFbo = fb.glFbo; + this.glDepth = fb.glDepth; + this.glStencil = fb.glStencil; + this.glDepthStencil = fb.glDepthStencil; + this.glMultisample = fb.glMultisample; + } + + this.context = fb.context; + } + + @Override + public void disposeNative() { + if (pgl != null) { + if (glFbo != 0) { + intBuffer.put(0, glFbo); + pgl.deleteFramebuffers(1, intBuffer); + glFbo = 0; + } + if (glDepth != 0) { + intBuffer.put(0, glDepth); + pgl.deleteRenderbuffers(1, intBuffer); + glDepth = 0; + } + if (glStencil != 0) { + intBuffer.put(0, glStencil); + pgl.deleteRenderbuffers(1, intBuffer); + glStencil = 0; + } + if (glDepthStencil != 0) { + intBuffer.put(0, glDepthStencil); + pgl.deleteRenderbuffers(1, intBuffer); + glDepthStencil = 0; + } + if (glMultisample != 0) { + intBuffer.put(0, glMultisample); + pgl.deleteRenderbuffers(1, intBuffer); + glMultisample = 0; + } + pgl = null; + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof GLResourceFrameBuffer)) { + return false; + } + GLResourceFrameBuffer other = (GLResourceFrameBuffer)obj; + return other.glFbo == glFbo && + other.glDepth == glDepth && + other.glStencil == glStencil && + other.glDepthStencil == glDepthStencil && + other.glMultisample == glMultisample && + other.context == context; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + glFbo; + result = 31 * result + glDepth; + result = 31 * result + glStencil; + result = 31 * result + glDepthStencil; + result = 31 * result + glMultisample; + result = 31 * result + context; + return result; + } + } + + + ////////////////////////////////////////////////////////////// + + // FRAMEBUFFERS + + + protected void pushFramebuffer() { + PGraphicsOpenGL ppg = getPrimaryPG(); + if (ppg.fbStackDepth == FB_STACK_DEPTH) { + throw new RuntimeException("Too many pushFramebuffer calls"); + } + ppg.fbStack[ppg.fbStackDepth] = ppg.currentFramebuffer; + ppg.fbStackDepth++; + } + + + protected void setFramebuffer(FrameBuffer fbo) { + PGraphicsOpenGL ppg = getPrimaryPG(); + if (ppg.currentFramebuffer != fbo) { + ppg.currentFramebuffer = fbo; + if (ppg.currentFramebuffer != null) ppg.currentFramebuffer.bind(); + } + } + + + protected void popFramebuffer() { + PGraphicsOpenGL ppg = getPrimaryPG(); + if (ppg.fbStackDepth == 0) { + throw new RuntimeException("popFramebuffer call is unbalanced."); + } + ppg.fbStackDepth--; + FrameBuffer fbo = ppg.fbStack[ppg.fbStackDepth]; + if (ppg.currentFramebuffer != fbo) { + ppg.currentFramebuffer.finish(); + ppg.currentFramebuffer = fbo; + if (ppg.currentFramebuffer != null) ppg.currentFramebuffer.bind(); + } + } + + + protected FrameBuffer getCurrentFB() { + return getPrimaryPG().currentFramebuffer; + } + + + ////////////////////////////////////////////////////////////// + + // FRAME RENDERING + + + protected void createPolyBuffers() { + if (!polyBuffersCreated || polyBuffersContextIsOutdated()) { + polyBuffersContext = pgl.getCurrentContext(); + + bufPolyVertex = new VertexBuffer(this, PGL.ARRAY_BUFFER, 3, PGL.SIZEOF_FLOAT); + bufPolyColor = new VertexBuffer(this, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_INT); + bufPolyNormal = new VertexBuffer(this, PGL.ARRAY_BUFFER, 3, PGL.SIZEOF_FLOAT); + bufPolyTexcoord = new VertexBuffer(this, PGL.ARRAY_BUFFER, 2, PGL.SIZEOF_FLOAT); + bufPolyAmbient = new VertexBuffer(this, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_INT); + bufPolySpecular = new VertexBuffer(this, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_INT); + bufPolyEmissive = new VertexBuffer(this, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_INT); + bufPolyShininess = new VertexBuffer(this, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_FLOAT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + bufPolyIndex = new VertexBuffer(this, PGL.ELEMENT_ARRAY_BUFFER, 1, PGL.SIZEOF_INDEX, true); + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, 0); + + polyBuffersCreated = true; + } + + boolean created = false; + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + if (!attrib.bufferCreated() || polyBuffersContextIsOutdated()) { + attrib.createBuffer(pgl); + created = true; + } + } + if (created) pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void updatePolyBuffers(boolean lit, boolean tex, + boolean needNormals, boolean needTexCoords) { + createPolyBuffers(); + + int size = tessGeo.polyVertexCount; + int sizef = size * PGL.SIZEOF_FLOAT; + int sizei = size * PGL.SIZEOF_INT; + + tessGeo.updatePolyVerticesBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyVertex.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 4 * sizef, + tessGeo.polyVerticesBuffer, PGL.STATIC_DRAW); + + tessGeo.updatePolyColorsBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyColor.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizei, + tessGeo.polyColorsBuffer, PGL.STATIC_DRAW); + + if (lit) { + tessGeo.updatePolyAmbientBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyAmbient.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizei, + tessGeo.polyAmbientBuffer, PGL.STATIC_DRAW); + + tessGeo.updatePolySpecularBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolySpecular.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizei, + tessGeo.polySpecularBuffer, PGL.STATIC_DRAW); + + tessGeo.updatePolyEmissiveBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyEmissive.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizei, + tessGeo.polyEmissiveBuffer, PGL.STATIC_DRAW); + + tessGeo.updatePolyShininessBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyShininess.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizef, + tessGeo.polyShininessBuffer, PGL.STATIC_DRAW); + } + + if (lit || needNormals) { + tessGeo.updatePolyNormalsBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyNormal.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 3 * sizef, + tessGeo.polyNormalsBuffer, PGL.STATIC_DRAW); + } + + if (tex || needTexCoords) { + tessGeo.updatePolyTexCoordsBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyTexcoord.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 2 * sizef, + tessGeo.polyTexCoordsBuffer, PGL.STATIC_DRAW); + } + + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + tessGeo.updateAttribBuffer(name); + pgl.bindBuffer(PGL.ARRAY_BUFFER, attrib.buf.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, attrib.sizeInBytes(size), + tessGeo.polyAttribBuffers.get(name), PGL.STATIC_DRAW); + } + + tessGeo.updatePolyIndicesBuffer(); + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, bufPolyIndex.glId); + pgl.bufferData(PGL.ELEMENT_ARRAY_BUFFER, + tessGeo.polyIndexCount * PGL.SIZEOF_INDEX, tessGeo.polyIndicesBuffer, + PGL.STATIC_DRAW); + } + + + protected void unbindPolyBuffers() { + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, 0); + } + + + protected boolean polyBuffersContextIsOutdated() { + return !pgl.contextIsCurrent(polyBuffersContext); + } + + + protected void createLineBuffers() { + if (!lineBuffersCreated || lineBufferContextIsOutdated()) { + lineBuffersContext = pgl.getCurrentContext(); + + bufLineVertex = new VertexBuffer(this, PGL.ARRAY_BUFFER, 3, PGL.SIZEOF_FLOAT); + bufLineColor = new VertexBuffer(this, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_INT); + bufLineAttrib = new VertexBuffer(this, PGL.ARRAY_BUFFER, 4, PGL.SIZEOF_FLOAT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + bufLineIndex = new VertexBuffer(this, PGL.ELEMENT_ARRAY_BUFFER, 1, PGL.SIZEOF_INDEX, true); + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, 0); + + lineBuffersCreated = true; + } + } + + + protected void updateLineBuffers() { + createLineBuffers(); + + int size = tessGeo.lineVertexCount; + int sizef = size * PGL.SIZEOF_FLOAT; + int sizei = size * PGL.SIZEOF_INT; + + + + tessGeo.updateLineVerticesBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufLineVertex.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 4 * sizef, tessGeo.lineVerticesBuffer, + PGL.STATIC_DRAW); + + tessGeo.updateLineColorsBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufLineColor.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizei, + tessGeo.lineColorsBuffer, PGL.STATIC_DRAW); + + tessGeo.updateLineDirectionsBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufLineAttrib.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 4 * sizef, + tessGeo.lineDirectionsBuffer, PGL.STATIC_DRAW); + + tessGeo.updateLineIndicesBuffer(); + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, bufLineIndex.glId); + pgl.bufferData(PGL.ELEMENT_ARRAY_BUFFER, + tessGeo.lineIndexCount * PGL.SIZEOF_INDEX, + tessGeo.lineIndicesBuffer, PGL.STATIC_DRAW); + } + + + protected void unbindLineBuffers() { + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, 0); + } + + + protected boolean lineBufferContextIsOutdated() { + return !pgl.contextIsCurrent(lineBuffersContext); + } + + + protected void createPointBuffers() { + if (!pointBuffersCreated || pointBuffersContextIsOutdated()) { + pointBuffersContext = pgl.getCurrentContext(); + + bufPointVertex = new VertexBuffer(this, PGL.ARRAY_BUFFER, 3, PGL.SIZEOF_FLOAT); + bufPointColor = new VertexBuffer(this, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_INT); + bufPointAttrib = new VertexBuffer(this, PGL.ARRAY_BUFFER, 2, PGL.SIZEOF_FLOAT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + bufPointIndex = new VertexBuffer(this, PGL.ELEMENT_ARRAY_BUFFER, 1, PGL.SIZEOF_INDEX, true); + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, 0); + + pointBuffersCreated = true; + } + } + + + protected void updatePointBuffers() { + createPointBuffers(); + + int size = tessGeo.pointVertexCount; + int sizef = size * PGL.SIZEOF_FLOAT; + int sizei = size * PGL.SIZEOF_INT; + + tessGeo.updatePointVerticesBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPointVertex.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 4 * sizef, + tessGeo.pointVerticesBuffer, PGL.STATIC_DRAW); + + tessGeo.updatePointColorsBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPointColor.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizei, + tessGeo.pointColorsBuffer, PGL.STATIC_DRAW); + + tessGeo.updatePointOffsetsBuffer(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPointAttrib.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 2 * sizef, + tessGeo.pointOffsetsBuffer, PGL.STATIC_DRAW); + + tessGeo.updatePointIndicesBuffer(); + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, bufPointIndex.glId); + pgl.bufferData(PGL.ELEMENT_ARRAY_BUFFER, + tessGeo.pointIndexCount * PGL.SIZEOF_INDEX, + tessGeo.pointIndicesBuffer, PGL.STATIC_DRAW); + } + + + protected void unbindPointBuffers() { + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, 0); + } + + + protected boolean pointBuffersContextIsOutdated() { + return !pgl.contextIsCurrent(pointBuffersContext); + } + + + @Override + public void beginDraw() { + if (primaryGraphics) { + if (!initialized) { + initPrimary(); + } + setCurrentPG(this); + } else { + pgl.getGL(getPrimaryPGL()); + getPrimaryPG().setCurrentPG(this); + } + + if (!pgl.threadIsCurrent()) { + PGraphics.showWarning(GL_THREAD_NOT_CURRENT); + return; + } + + // This has to go after the surface initialization, otherwise offscreen + // surfaces will have a null gl object. + report("top beginDraw()"); + + if (!checkGLThread()) { + return; + } + + if (drawing) { + return; + } + + if (!primaryGraphics && getPrimaryPG().texCache.containsTexture(this)) { + // This offscreen surface is being used as a texture earlier in draw, + // so we should update the rendering up to this point since it will be + // modified. + getPrimaryPG().flush(); + } + + if (!glParamsRead) { + getGLParameters(); + } + + setViewport(); + if (primaryGraphics) { + beginOnscreenDraw(); + } else { + beginOffscreenDraw(); + } + checkSettings(); + + drawing = true; + + report("bot beginDraw()"); + } + + + @Override + public void endDraw() { + report("top endDraw()"); + + if (!drawing) { + return; + } + + // Flushing any remaining geometry. + flush(); + + if (primaryGraphics) { + endOnscreenDraw(); + } else { + endOffscreenDraw(); + } + + if (primaryGraphics) { + setCurrentPG(null); + } else { + getPrimaryPG().setCurrentPG(); + } + drawing = false; + + report("bot endDraw()"); + } + + + protected PGraphicsOpenGL getPrimaryPG() { + if (primaryGraphics) { + return this; + } else { + return (PGraphicsOpenGL)parent.g; + } + } + + protected void setCurrentPG(PGraphicsOpenGL pg) { + currentPG = pg; + } + + protected void setCurrentPG() { + currentPG = this; + } + + protected PGraphicsOpenGL getCurrentPG() { + return currentPG; + } + + protected PGL getPrimaryPGL() { + if (primaryGraphics) { + return pgl; + } else { + return ((PGraphicsOpenGL)parent.g).pgl; + } + } + + + @Override + public PGL beginPGL() { + flush(); + pgl.beginGL(); + return pgl; + } + + + @Override + public void endPGL() { + pgl.endGL(); + restoreGL(); + } + + + public void updateProjmodelview() { + projmodelview.set(projection); + projmodelview.apply(modelview); + } + + + protected void restartPGL() { + initialized = false; + } + + + protected void restoreGL() { + blendMode(blendMode); // this should be set by reapplySettings... + + if (hints[DISABLE_DEPTH_TEST]) { + pgl.disable(PGL.DEPTH_TEST); + } else { + pgl.enable(PGL.DEPTH_TEST); + } + pgl.depthFunc(PGL.LEQUAL); + + if (OPENGL_RENDERER.equals("VideoCore IV HW")) { + // Broadcom's VC IV driver is unhappy with either of these + // ignore for now + } else if (smooth < 1) { + pgl.disable(PGL.MULTISAMPLE); + } else if (1 <= smooth) { + pgl.enable(PGL.MULTISAMPLE); + pgl.disable(PGL.POLYGON_SMOOTH); + } + + pgl.viewport(viewport.get(0), viewport.get(1), + viewport.get(2), viewport.get(3)); + if (clip) { + pgl.enable(PGL.SCISSOR_TEST); + pgl.scissor(clipRect[0], clipRect[1], clipRect[2], clipRect[3]); + } else { + pgl.disable(PGL.SCISSOR_TEST); + } + + pgl.frontFace(PGL.CW); + pgl.disable(PGL.CULL_FACE); + + pgl.activeTexture(PGL.TEXTURE0); + + if (hints[DISABLE_DEPTH_MASK]) { + pgl.depthMask(false); + } else { + pgl.depthMask(true); + } + + FrameBuffer fb = getCurrentFB(); + if (fb != null) { + fb.bind(); + if (drawBufferSupported) pgl.drawBuffer(fb.getDefaultDrawBuffer()); + } + } + + protected void beginBindFramebuffer(int target, int framebuffer) { + // Actually, nothing to do here. + } + + protected void endBindFramebuffer(int target, int framebuffer) { + FrameBuffer fb = getCurrentFB(); + if (framebuffer == 0 && fb != null && fb.glFbo != 0) { + // The user is setting the framebuffer to 0 (screen buffer), but the + // renderer is drawing into an offscreen buffer. + fb.bind(); + } + } + + protected void beginReadPixels() { + beginPixelsOp(OP_READ); + } + + protected void endReadPixels() { + endPixelsOp(); + } + + protected void beginPixelsOp(int op) { + FrameBuffer pixfb = null; + FrameBuffer currfb = getCurrentFB(); + if (primaryGraphics) { + FrameBuffer rfb = readFramebuffer; + FrameBuffer dfb = drawFramebuffer; + if ((currfb == rfb) || (currfb == dfb)) { + // Not user-provided FB, need to check if the correct FB is current. + if (op == OP_READ) { + if (pgl.isFBOBacked() && pgl.isMultisampled()) { + // Making sure the back texture is up-to-date... + pgl.syncBackTexture(); + // ...because the read framebuffer uses it as the color buffer (the + // draw framebuffer is MSAA so it cannot be read from it). + pixfb = rfb; + } else { + pixfb = dfb; + } + } else if (op == OP_WRITE) { + // We can write to the draw framebuffer irrespective of whether is + // FBO-baked or multisampled. + pixfb = dfb; + } + } + } else { + FrameBuffer ofb = offscreenFramebuffer; + FrameBuffer mfb = multisampleFramebuffer; + if ((currfb == ofb) || (currfb == mfb)) { + // Not user-provided FB, need to check if the correct FB is current. + if (op == OP_READ) { + if (offscreenMultisample) { + // Making sure the offscreen FBO is up-to-date + int mask = PGL.COLOR_BUFFER_BIT; + if (hints[ENABLE_BUFFER_READING]) { + mask |= PGL.DEPTH_BUFFER_BIT | PGL.STENCIL_BUFFER_BIT; + } + if (ofb != null && mfb != null) { + mfb.copy(ofb, mask); + } + } + // We always read the screen pixels from the color FBO. + pixfb = ofb; + } else if (op == OP_WRITE) { + // We can write directly to the color FBO, or to the multisample FBO + // if multisampling is enabled. + pixfb = offscreenMultisample ? mfb : ofb; + } + } + } + + // Set the framebuffer where the pixel operation shall be carried out. + if (pixfb != null && pixfb != getCurrentFB()) { + pushFramebuffer(); + setFramebuffer(pixfb); + pixOpChangedFB = true; + } + + // We read from/write to the draw buffer. + if (op == OP_READ) { + if (readBufferSupported) pgl.readBuffer(getCurrentFB().getDefaultDrawBuffer()); + } else if (op == OP_WRITE) { + if (drawBufferSupported) pgl.drawBuffer(getCurrentFB().getDefaultDrawBuffer()); + } + + pixelsOp = op; + } + + + protected void endPixelsOp() { + // Restoring current framebuffer prior to the pixel operation + if (pixOpChangedFB) { + popFramebuffer(); + pixOpChangedFB = false; + } + + // Restoring default read/draw buffer configuration. + if (readBufferSupported) pgl.readBuffer(getCurrentFB().getDefaultReadBuffer()); + if (drawBufferSupported) pgl.drawBuffer(getCurrentFB().getDefaultDrawBuffer()); + + pixelsOp = OP_NONE; + } + + + protected void updateGLProjection() { + if (glProjection == null) { + glProjection = new float[16]; + } + + glProjection[0] = projection.m00; + glProjection[1] = projection.m10; + glProjection[2] = projection.m20; + glProjection[3] = projection.m30; + + glProjection[4] = projection.m01; + glProjection[5] = projection.m11; + glProjection[6] = projection.m21; + glProjection[7] = projection.m31; + + glProjection[8] = projection.m02; + glProjection[9] = projection.m12; + glProjection[10] = projection.m22; + glProjection[11] = projection.m32; + + glProjection[12] = projection.m03; + glProjection[13] = projection.m13; + glProjection[14] = projection.m23; + glProjection[15] = projection.m33; + } + + + protected void updateGLModelview() { + if (glModelview == null) { + glModelview = new float[16]; + } + + glModelview[0] = modelview.m00; + glModelview[1] = modelview.m10; + glModelview[2] = modelview.m20; + glModelview[3] = modelview.m30; + + glModelview[4] = modelview.m01; + glModelview[5] = modelview.m11; + glModelview[6] = modelview.m21; + glModelview[7] = modelview.m31; + + glModelview[8] = modelview.m02; + glModelview[9] = modelview.m12; + glModelview[10] = modelview.m22; + glModelview[11] = modelview.m32; + + glModelview[12] = modelview.m03; + glModelview[13] = modelview.m13; + glModelview[14] = modelview.m23; + glModelview[15] = modelview.m33; + } + + + protected void updateGLProjmodelview() { + if (glProjmodelview == null) { + glProjmodelview = new float[16]; + } + + glProjmodelview[0] = projmodelview.m00; + glProjmodelview[1] = projmodelview.m10; + glProjmodelview[2] = projmodelview.m20; + glProjmodelview[3] = projmodelview.m30; + + glProjmodelview[4] = projmodelview.m01; + glProjmodelview[5] = projmodelview.m11; + glProjmodelview[6] = projmodelview.m21; + glProjmodelview[7] = projmodelview.m31; + + glProjmodelview[8] = projmodelview.m02; + glProjmodelview[9] = projmodelview.m12; + glProjmodelview[10] = projmodelview.m22; + glProjmodelview[11] = projmodelview.m32; + + glProjmodelview[12] = projmodelview.m03; + glProjmodelview[13] = projmodelview.m13; + glProjmodelview[14] = projmodelview.m23; + glProjmodelview[15] = projmodelview.m33; + } + + + protected void updateGLNormal() { + if (glNormal == null) { + glNormal = new float[9]; + } + + // The normal matrix is the transpose of the inverse of the + // modelview (remember that gl matrices are column-major, + // meaning that elements 0, 1, 2 are the first column, + // 3, 4, 5 the second, etc.): + glNormal[0] = modelviewInv.m00; + glNormal[1] = modelviewInv.m01; + glNormal[2] = modelviewInv.m02; + + glNormal[3] = modelviewInv.m10; + glNormal[4] = modelviewInv.m11; + glNormal[5] = modelviewInv.m12; + + glNormal[6] = modelviewInv.m20; + glNormal[7] = modelviewInv.m21; + glNormal[8] = modelviewInv.m22; + } + + + ////////////////////////////////////////////////////////////// + + // SETTINGS + + // protected void checkSettings() + + + @Override + protected void defaultSettings() { + super.defaultSettings(); + + manipulatingCamera = false; + + // easiest for beginners + textureMode(IMAGE); + + // Default material properties + ambient(255); + specular(125); + emissive(0); + shininess(1); + + // To indicate that the user hasn't set ambient + setAmbient = false; + } + + + // reapplySettings + + ////////////////////////////////////////////////////////////// + + // HINTS + + + @Override + public void hint(int which) { + boolean oldValue = hints[PApplet.abs(which)]; + super.hint(which); + boolean newValue = hints[PApplet.abs(which)]; + + if (oldValue == newValue) { + return; + } + + if (which == DISABLE_DEPTH_TEST) { + flush(); + pgl.disable(PGL.DEPTH_TEST); + } else if (which == ENABLE_DEPTH_TEST) { + flush(); + pgl.enable(PGL.DEPTH_TEST); + } else if (which == DISABLE_DEPTH_MASK) { + flush(); + pgl.depthMask(false); + } else if (which == ENABLE_DEPTH_MASK) { + flush(); + pgl.depthMask(true); + } else if (which == ENABLE_OPTIMIZED_STROKE) { + flush(); + setFlushMode(FLUSH_WHEN_FULL); + } else if (which == DISABLE_OPTIMIZED_STROKE) { + if (is2D()) { + PGraphics.showWarning("Optimized strokes can only be disabled in 3D"); + } else { + flush(); + setFlushMode(FLUSH_CONTINUOUSLY); + } + } else if (which == DISABLE_STROKE_PERSPECTIVE) { + if (0 < tessGeo.lineVertexCount && 0 < tessGeo.lineIndexCount) { + // We flush the geometry using the previous line setting. + flush(); + } + } else if (which == ENABLE_STROKE_PERSPECTIVE) { + if (0 < tessGeo.lineVertexCount && 0 < tessGeo.lineIndexCount) { + // We flush the geometry using the previous line setting. + flush(); + } + } else if (which == ENABLE_DEPTH_SORT) { + if (is3D()) { + flush(); + if (sorter == null) sorter = new DepthSorter(this); + isDepthSortingEnabled = true; + } else { + PGraphics.showWarning("Depth sorting can only be enabled in 3D"); + } + } else if (which == DISABLE_DEPTH_SORT) { + if (is3D()) { + flush(); + isDepthSortingEnabled = false; + } + } else if (which == ENABLE_BUFFER_READING) { + restartPGL(); + } else if (which == DISABLE_BUFFER_READING) { + restartPGL(); + } + } + + + protected boolean getHint(int which) { + if (which > 0) { + return hints[which]; + } else { + return !hints[-which]; + } + } + + + ////////////////////////////////////////////////////////////// + + // CREATE SHAPE + + + @Override + protected PShape createShapeFamily(int type) { + PShape shape = new PShapeOpenGL(this, type); + if (is3D()) { + shape.set3D(true); + } + return shape; + } + + + @Override + protected PShape createShapePrimitive(int kind, float... p) { + PShape shape = new PShapeOpenGL(this, kind, p); + if (is3D()) { + shape.set3D(true); + } + return shape; + } + + + + ////////////////////////////////////////////////////////////// + + // VERTEX SHAPES + + + @Override + public void beginShape(int kind) { + shape = kind; + inGeo.clear(); + + curveVertexCount = 0; + breakShape = false; + defaultEdges = true; + + // The superclass method is called to avoid an early flush. + super.noTexture(); + + normalMode = NORMAL_MODE_AUTO; + } + + + @Override + public void endShape(int mode) { + tessellate(mode); + + if ((flushMode == FLUSH_CONTINUOUSLY) || + (flushMode == FLUSH_WHEN_FULL && tessGeo.isFull())) { + flush(); + } else { + // pixels array is not up-to-date anymore + loaded = false; + } + } + + + protected void endShape(int[] indices) { + if (shape != TRIANGLE && shape != TRIANGLES) { + throw new RuntimeException("Indices and edges can only be set for " + + "TRIANGLE shapes"); + } + + tessellate(indices); + + if (flushMode == FLUSH_CONTINUOUSLY || + (flushMode == FLUSH_WHEN_FULL && tessGeo.isFull())) { + flush(); + } else { + // pixels array is not up-to-date anymore + loaded = false; + } + } + + + @Override + public void textureWrap(int wrap) { + if (this.textureWrap != wrap) { + flush(); + } + this.textureWrap = wrap; + } + + + public void textureSampling(int sampling) { + this.textureSampling = sampling; + } + + + @Override + public void beginContour() { + if (openContour) { + PGraphics.showWarning(ALREADY_BEGAN_CONTOUR_ERROR); + return; + } + openContour = true; + breakShape = true; + } + + + @Override + public void endContour() { + if (!openContour) { + PGraphics.showWarning(NO_BEGIN_CONTOUR_ERROR); + return; + } + openContour = false; + } + + + @Override + public void vertex(float x, float y) { + vertexImpl(x, y, 0, 0, 0); + if (textureImage != null) PGraphics.showWarning(MISSING_UV_TEXCOORDS_ERROR); + } + + + @Override + public void vertex(float x, float y, float u, float v) { + vertexImpl(x, y, 0, u, v); + } + + + @Override + public void vertex(float x, float y, float z) { + vertexImpl(x, y, z, 0, 0); + if (textureImage != null) PGraphics.showWarning(MISSING_UV_TEXCOORDS_ERROR); + } + + + @Override + public void vertex(float x, float y, float z, float u, float v) { + vertexImpl(x, y, z, u, v); + } + + + @Override + public void attribPosition(String name, float x, float y, float z) { + VertexAttribute attrib = attribImpl(name, VertexAttribute.POSITION, + PGL.FLOAT, 3); + if (attrib != null) attrib.set(x, y, z); + } + + + @Override + public void attribNormal(String name, float nx, float ny, float nz) { + VertexAttribute attrib = attribImpl(name, VertexAttribute.NORMAL, + PGL.FLOAT, 3); + if (attrib != null) attrib.set(nx, ny, nz); + } + + + @Override + public void attribColor(String name, int color) { + VertexAttribute attrib = attribImpl(name, VertexAttribute.COLOR, PGL.INT, 1); + if (attrib != null) attrib.set(new int[] {color}); + } + + + @Override + public void attrib(String name, float... values) { + VertexAttribute attrib = attribImpl(name, VertexAttribute.OTHER, + PGL.FLOAT, values.length); + if (attrib != null) attrib.set(values); + } + + + @Override + public void attrib(String name, int... values) { + VertexAttribute attrib = attribImpl(name, VertexAttribute.OTHER, + PGL.INT, values.length); + if (attrib != null) attrib.set(values); + } + + + @Override + public void attrib(String name, boolean... values) { + VertexAttribute attrib = attribImpl(name, VertexAttribute.OTHER, + PGL.BOOL, values.length); + if (attrib != null) attrib.set(values); + } + + + protected VertexAttribute attribImpl(String name, int kind, int type, int size) { + if (4 < size) { + PGraphics.showWarning("Vertex attributes cannot have more than 4 values"); + return null; + } + VertexAttribute attrib = polyAttribs.get(name); + if (attrib == null) { + attrib = new VertexAttribute(this, name, kind, type, size); + polyAttribs.put(name, attrib); + inGeo.initAttrib(attrib); + tessGeo.initAttrib(attrib); + } + if (attrib.kind != kind) { + PGraphics.showWarning("The attribute kind cannot be changed after creation"); + return null; + } + if (attrib.type != type) { + PGraphics.showWarning("The attribute type cannot be changed after creation"); + return null; + } + if (attrib.size != size) { + PGraphics.showWarning("New value for vertex attribute has wrong number of values"); + return null; + } + return attrib; + } + + + protected void vertexImpl(float x, float y, float z, float u, float v) { + boolean textured = textureImage != null; + int fcolor = 0x00; + if (fill || textured) { + if (!textured) { + fcolor = fillColor; + } else { + if (tint) { + fcolor = tintColor; + } else { + fcolor = 0xffFFFFFF; + } + } + } + + int scolor = 0x00; + float sweight = 0; + if (stroke) { + scolor = strokeColor; + sweight = strokeWeight; + } + + if (textured && textureMode == IMAGE) { + u /= textureImage.pixelWidth; + v /= textureImage.pixelHeight; + } + + inGeo.addVertex(x, y, z, + fcolor, + normalX, normalY, normalZ, + u, v, + scolor, sweight, + ambientColor, specularColor, emissiveColor, shininess, + VERTEX, vertexBreak()); + } + + + protected boolean vertexBreak() { + if (breakShape) { + breakShape = false; + return true; + } + return false; + } + + + @Override + protected void clipImpl(float x1, float y1, float x2, float y2) { + flush(); + pgl.enable(PGL.SCISSOR_TEST); + + float h = y2 - y1; + clipRect[0] = (int)x1; + clipRect[1] = (int)(height - y1 - h); + clipRect[2] = (int)(x2 - x1); + clipRect[3] = (int)h; + pgl.scissor(clipRect[0], clipRect[1], clipRect[2], clipRect[3]); + + clip = true; + } + + + @Override + public void noClip() { + if (clip) { + flush(); + pgl.disable(PGL.SCISSOR_TEST); + clip = false; + } + } + + + ////////////////////////////////////////////////////////////// + + // RENDERING + + // protected void render() + + // protected void sort() + + + protected void tessellate(int mode) { + tessellator.setInGeometry(inGeo); + tessellator.setTessGeometry(tessGeo); + tessellator.setFill(fill || textureImage != null); + tessellator.setTexCache(texCache, textureImage); + tessellator.setStroke(stroke); + tessellator.setStrokeColor(strokeColor); + tessellator.setStrokeWeight(strokeWeight); + tessellator.setStrokeCap(strokeCap); + tessellator.setStrokeJoin(strokeJoin); + tessellator.setRenderer(this); + tessellator.setTransform(modelview); + tessellator.set3D(is3D()); + + if (shape == POINTS) { + tessellator.tessellatePoints(); + } else if (shape == LINES) { + tessellator.tessellateLines(); + } else if (shape == LINE_STRIP) { + tessellator.tessellateLineStrip(); + } else if (shape == LINE_LOOP) { + tessellator.tessellateLineLoop(); + } else if (shape == TRIANGLE || shape == TRIANGLES) { + if (stroke && defaultEdges) inGeo.addTrianglesEdges(); + if (normalMode == NORMAL_MODE_AUTO) inGeo.calcTrianglesNormals(); + tessellator.tessellateTriangles(); + } else if (shape == TRIANGLE_FAN) { + if (stroke && defaultEdges) inGeo.addTriangleFanEdges(); + if (normalMode == NORMAL_MODE_AUTO) inGeo.calcTriangleFanNormals(); + tessellator.tessellateTriangleFan(); + } else if (shape == TRIANGLE_STRIP) { + if (stroke && defaultEdges) inGeo.addTriangleStripEdges(); + if (normalMode == NORMAL_MODE_AUTO) inGeo.calcTriangleStripNormals(); + tessellator.tessellateTriangleStrip(); + } else if (shape == QUAD || shape == QUADS) { + if (stroke && defaultEdges) inGeo.addQuadsEdges(); + if (normalMode == NORMAL_MODE_AUTO) inGeo.calcQuadsNormals(); + tessellator.tessellateQuads(); + } else if (shape == QUAD_STRIP) { + if (stroke && defaultEdges) inGeo.addQuadStripEdges(); + if (normalMode == NORMAL_MODE_AUTO) inGeo.calcQuadStripNormals(); + tessellator.tessellateQuadStrip(); + } else if (shape == POLYGON) { + tessellator.tessellatePolygon(true, mode == CLOSE, + normalMode == NORMAL_MODE_AUTO); + } + } + + + protected void tessellate(int[] indices) { + tessellator.setInGeometry(inGeo); + tessellator.setTessGeometry(tessGeo); + tessellator.setFill(fill || textureImage != null); + tessellator.setStroke(stroke); + tessellator.setStrokeColor(strokeColor); + tessellator.setStrokeWeight(strokeWeight); + tessellator.setStrokeCap(strokeCap); + tessellator.setStrokeJoin(strokeJoin); + tessellator.setTexCache(texCache, textureImage); + tessellator.setTransform(modelview); + tessellator.set3D(is3D()); + + if (stroke && defaultEdges) inGeo.addTrianglesEdges(); + if (normalMode == NORMAL_MODE_AUTO) inGeo.calcTrianglesNormals(); + tessellator.tessellateTriangles(indices); + } + + + @Override + public void flush() { + boolean hasPolys = 0 < tessGeo.polyVertexCount && + 0 < tessGeo.polyIndexCount; + boolean hasLines = 0 < tessGeo.lineVertexCount && + 0 < tessGeo.lineIndexCount; + boolean hasPoints = 0 < tessGeo.pointVertexCount && + 0 < tessGeo.pointIndexCount; + + boolean hasPixels = modified && pixels != null; + + if (hasPixels) { + // If the user has been manipulating individual pixels, + // the changes need to be copied to the screen before + // drawing any new geometry. + flushPixels(); + } + + if (hasPoints || hasLines || hasPolys) { + PMatrix3D modelview0 = null; + PMatrix3D modelviewInv0 = null; + if (flushMode == FLUSH_WHEN_FULL) { + // The modelview transformation has been applied already to the + // tessellated vertices, so we set the OpenGL modelview matrix as + // the identity to avoid applying the model transformations twice. + // We save the modelview objects and temporarily use the identity + // static matrix to avoid calling pushMatrix(), resetMatrix(), + // popMatrix(). + modelview0 = modelview; + modelviewInv0 = modelviewInv; + modelview = modelviewInv = identity; + projmodelview.set(projection); + } + + if (hasPolys && !isDepthSortingEnabled) { + flushPolys(); + if (raw != null) { + rawPolys(); + } + } + + if (is3D()) { + if (hasLines) { + flushLines(); + if (raw != null) { + rawLines(); + } + } + + if (hasPoints) { + flushPoints(); + if (raw != null) { + rawPoints(); + } + } + } + + if (hasPolys && isDepthSortingEnabled) { + // We flush after lines so they are visible + // under transparent polygons + flushSortedPolys(); + if (raw != null) { + rawSortedPolys(); + } + } + + if (flushMode == FLUSH_WHEN_FULL) { + modelview = modelview0; + modelviewInv = modelviewInv0; + updateProjmodelview(); + } + + loaded = false; + } + + tessGeo.clear(); + texCache.clear(); + } + + + protected void flushPixels() { + drawPixels(mx1, my1, mx2 - mx1, my2 - my1); + modified = false; + } + + + protected void flushPolys() { + boolean customShader = polyShader != null; + boolean needNormals = customShader ? polyShader.accessNormals() : false; + boolean needTexCoords = customShader ? polyShader.accessTexCoords() : false; + + updatePolyBuffers(lights, texCache.hasTextures, needNormals, needTexCoords); + + for (int i = 0; i < texCache.size; i++) { + Texture tex = texCache.getTexture(i); + + // If the renderer is 2D, then lights should always be false, + // so no need to worry about that. + PShader shader = getPolyShader(lights, tex != null); + shader.bind(); + + int first = texCache.firstCache[i]; + int last = texCache.lastCache[i]; + IndexCache cache = tessGeo.polyIndexCache; + + for (int n = first; n <= last; n++) { + int ioffset = n == first ? texCache.firstIndex[i] : cache.indexOffset[n]; + int icount = n == last ? texCache.lastIndex[i] - ioffset + 1 : + cache.indexOffset[n] + cache.indexCount[n] - ioffset; + int voffset = cache.vertexOffset[n]; + + shader.setVertexAttribute(bufPolyVertex.glId, 4, PGL.FLOAT, 0, + 4 * voffset * PGL.SIZEOF_FLOAT); + shader.setColorAttribute(bufPolyColor.glId, 4, PGL.UNSIGNED_BYTE, 0, + 4 * voffset * PGL.SIZEOF_BYTE); + + if (lights) { + shader.setNormalAttribute(bufPolyNormal.glId, 3, PGL.FLOAT, 0, + 3 * voffset * PGL.SIZEOF_FLOAT); + shader.setAmbientAttribute(bufPolyAmbient.glId, 4, PGL.UNSIGNED_BYTE, 0, + 4 * voffset * PGL.SIZEOF_BYTE); + shader.setSpecularAttribute(bufPolySpecular.glId, 4, PGL.UNSIGNED_BYTE, 0, + 4 * voffset * PGL.SIZEOF_BYTE); + shader.setEmissiveAttribute(bufPolyEmissive.glId, 4, PGL.UNSIGNED_BYTE, 0, + 4 * voffset * PGL.SIZEOF_BYTE); + shader.setShininessAttribute(bufPolyShininess.glId, 1, PGL.FLOAT, 0, + voffset * PGL.SIZEOF_FLOAT); + } + + if (lights || needNormals) { + shader.setNormalAttribute(bufPolyNormal.glId, 3, PGL.FLOAT, 0, + 3 * voffset * PGL.SIZEOF_FLOAT); + } + + if (tex != null || needTexCoords) { + shader.setTexcoordAttribute(bufPolyTexcoord.glId, 2, PGL.FLOAT, 0, + 2 * voffset * PGL.SIZEOF_FLOAT); + shader.setTexture(tex); + } + + for (VertexAttribute attrib: polyAttribs.values()) { + if (!attrib.active(shader)) continue; + attrib.bind(pgl); + shader.setAttributeVBO(attrib.glLoc, attrib.buf.glId, + attrib.tessSize, attrib.type, + attrib.isColor(), 0, attrib.sizeInBytes(voffset)); + } + + shader.draw(bufPolyIndex.glId, icount, ioffset); + } + + for (VertexAttribute attrib: polyAttribs.values()) { + if (attrib.active(shader)) attrib.unbind(pgl); + } + shader.unbind(); + } + unbindPolyBuffers(); + } + + protected void flushSortedPolys() { + boolean customShader = polyShader != null; + boolean needNormals = customShader ? polyShader.accessNormals() : false; + boolean needTexCoords = customShader ? polyShader.accessTexCoords() : false; + + sorter.sort(tessGeo); + + int triangleCount = tessGeo.polyIndexCount / 3; + int[] texMap = sorter.texMap; + int[] voffsetMap = sorter.voffsetMap; + + int[] vertexOffset = tessGeo.polyIndexCache.vertexOffset; + + updatePolyBuffers(lights, texCache.hasTextures, needNormals, needTexCoords); + + int ti = 0; + + while (ti < triangleCount) { + + int startTi = ti; + int texId = texMap[ti]; + int voffsetId = voffsetMap[ti]; + + do { + ++ti; + } while (ti < triangleCount && + texId == texMap[ti] && + voffsetId == voffsetMap[ti]); + + int endTi = ti; + + Texture tex = texCache.getTexture(texId); + + int voffset = vertexOffset[voffsetId]; + + int ioffset = 3 * startTi; + int icount = 3 * (endTi - startTi); + + // If the renderer is 2D, then lights should always be false, + // so no need to worry about that. + PShader shader = getPolyShader(lights, tex != null); + shader.bind(); + + shader.setVertexAttribute(bufPolyVertex.glId, 4, PGL.FLOAT, 0, + 4 * voffset * PGL.SIZEOF_FLOAT); + shader.setColorAttribute(bufPolyColor.glId, 4, PGL.UNSIGNED_BYTE, 0, + 4 * voffset * PGL.SIZEOF_BYTE); + + if (lights) { + shader.setNormalAttribute(bufPolyNormal.glId, 3, PGL.FLOAT, 0, + 3 * voffset * PGL.SIZEOF_FLOAT); + shader.setAmbientAttribute(bufPolyAmbient.glId, 4, PGL.UNSIGNED_BYTE, 0, + 4 * voffset * PGL.SIZEOF_BYTE); + shader.setSpecularAttribute(bufPolySpecular.glId, 4, PGL.UNSIGNED_BYTE, 0, + 4 * voffset * PGL.SIZEOF_BYTE); + shader.setEmissiveAttribute(bufPolyEmissive.glId, 4, PGL.UNSIGNED_BYTE, 0, + 4 * voffset * PGL.SIZEOF_BYTE); + shader.setShininessAttribute(bufPolyShininess.glId, 1, PGL.FLOAT, 0, + voffset * PGL.SIZEOF_FLOAT); + } + + if (lights || needNormals) { + shader.setNormalAttribute(bufPolyNormal.glId, 3, PGL.FLOAT, 0, + 3 * voffset * PGL.SIZEOF_FLOAT); + } + + if (tex != null || needTexCoords) { + shader.setTexcoordAttribute(bufPolyTexcoord.glId, 2, PGL.FLOAT, 0, + 2 * voffset * PGL.SIZEOF_FLOAT); + shader.setTexture(tex); + } + + for (VertexAttribute attrib: polyAttribs.values()) { + if (!attrib.active(shader)) continue; + attrib.bind(pgl); + shader.setAttributeVBO(attrib.glLoc, attrib.buf.glId, + attrib.tessSize, attrib.type, + attrib.isColor(), 0, attrib.sizeInBytes(voffset)); + } + + shader.draw(bufPolyIndex.glId, icount, ioffset); + + for (VertexAttribute attrib: polyAttribs.values()) { + if (attrib.active(shader)) attrib.unbind(pgl); + } + shader.unbind(); + } + unbindPolyBuffers(); + } + + + void rawPolys() { + raw.colorMode(RGB); + raw.noStroke(); + raw.beginShape(TRIANGLES); + + float[] vertices = tessGeo.polyVertices; + int[] color = tessGeo.polyColors; + float[] uv = tessGeo.polyTexCoords; + short[] indices = tessGeo.polyIndices; + + for (int i = 0; i < texCache.size; i++) { + PImage textureImage = texCache.getTextureImage(i); + + int first = texCache.firstCache[i]; + int last = texCache.lastCache[i]; + IndexCache cache = tessGeo.polyIndexCache; + for (int n = first; n <= last; n++) { + int ioffset = n == first ? texCache.firstIndex[i] : + cache.indexOffset[n]; + int icount = n == last ? texCache.lastIndex[i] - ioffset + 1 : + cache.indexOffset[n] + cache.indexCount[n] - + ioffset; + int voffset = cache.vertexOffset[n]; + + for (int tr = ioffset / 3; tr < (ioffset + icount) / 3; tr++) { + int i0 = voffset + indices[3 * tr + 0]; + int i1 = voffset + indices[3 * tr + 1]; + int i2 = voffset + indices[3 * tr + 2]; + + float[] pt0 = {0, 0, 0, 0}; + float[] pt1 = {0, 0, 0, 0}; + float[] pt2 = {0, 0, 0, 0}; + int argb0 = PGL.nativeToJavaARGB(color[i0]); + int argb1 = PGL.nativeToJavaARGB(color[i1]); + int argb2 = PGL.nativeToJavaARGB(color[i2]); + + if (flushMode == FLUSH_CONTINUOUSLY) { + float[] src0 = {0, 0, 0, 0}; + float[] src1 = {0, 0, 0, 0}; + float[] src2 = {0, 0, 0, 0}; + PApplet.arrayCopy(vertices, 4 * i0, src0, 0, 4); + PApplet.arrayCopy(vertices, 4 * i1, src1, 0, 4); + PApplet.arrayCopy(vertices, 4 * i2, src2, 0, 4); + modelview.mult(src0, pt0); + modelview.mult(src1, pt1); + modelview.mult(src2, pt2); + } else { + PApplet.arrayCopy(vertices, 4 * i0, pt0, 0, 4); + PApplet.arrayCopy(vertices, 4 * i1, pt1, 0, 4); + PApplet.arrayCopy(vertices, 4 * i2, pt2, 0, 4); + } + + if (textureImage != null) { + raw.texture(textureImage); + if (raw.is3D()) { + raw.fill(argb0); + raw.vertex(pt0[X], pt0[Y], pt0[Z], uv[2 * i0 + 0], uv[2 * i0 + 1]); + raw.fill(argb1); + raw.vertex(pt1[X], pt1[Y], pt1[Z], uv[2 * i1 + 0], uv[2 * i1 + 1]); + raw.fill(argb2); + raw.vertex(pt2[X], pt2[Y], pt2[Z], uv[2 * i2 + 0], uv[2 * i2 + 1]); + } else if (raw.is2D()) { + float sx0 = screenXImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sy0 = screenYImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sx1 = screenXImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sy1 = screenYImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sx2 = screenXImpl(pt2[0], pt2[1], pt2[2], pt2[3]); + float sy2 = screenYImpl(pt2[0], pt2[1], pt2[2], pt2[3]); + raw.fill(argb0); + raw.vertex(sx0, sy0, uv[2 * i0 + 0], uv[2 * i0 + 1]); + raw.fill(argb1); + raw.vertex(sx1, sy1, uv[2 * i1 + 0], uv[2 * i1 + 1]); + raw.fill(argb1); + raw.vertex(sx2, sy2, uv[2 * i2 + 0], uv[2 * i2 + 1]); + } + } else { + if (raw.is3D()) { + raw.fill(argb0); + raw.vertex(pt0[X], pt0[Y], pt0[Z]); + raw.fill(argb1); + raw.vertex(pt1[X], pt1[Y], pt1[Z]); + raw.fill(argb2); + raw.vertex(pt2[X], pt2[Y], pt2[Z]); + } else if (raw.is2D()) { + float sx0 = screenXImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sy0 = screenYImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sx1 = screenXImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sy1 = screenYImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sx2 = screenXImpl(pt2[0], pt2[1], pt2[2], pt2[3]); + float sy2 = screenYImpl(pt2[0], pt2[1], pt2[2], pt2[3]); + raw.fill(argb0); + raw.vertex(sx0, sy0); + raw.fill(argb1); + raw.vertex(sx1, sy1); + raw.fill(argb2); + raw.vertex(sx2, sy2); + } + } + } + } + } + + + raw.endShape(); + } + + + void rawSortedPolys() { + raw.colorMode(RGB); + raw.noStroke(); + raw.beginShape(TRIANGLES); + + float[] vertices = tessGeo.polyVertices; + int[] color = tessGeo.polyColors; + float[] uv = tessGeo.polyTexCoords; + short[] indices = tessGeo.polyIndices; + + sorter.sort(tessGeo); + int[] triangleIndices = sorter.triangleIndices; + int[] texMap = sorter.texMap; + int[] voffsetMap = sorter.voffsetMap; + + int[] vertexOffset = tessGeo.polyIndexCache.vertexOffset; + + for (int i = 0; i < tessGeo.polyIndexCount/3; i++) { + int ti = triangleIndices[i]; + PImage tex = texCache.getTextureImage(texMap[ti]); + int voffset = vertexOffset[voffsetMap[ti]]; + + int i0 = voffset + indices[3*ti+0]; + int i1 = voffset + indices[3*ti+1]; + int i2 = voffset + indices[3*ti+2]; + + float[] pt0 = {0, 0, 0, 0}; + float[] pt1 = {0, 0, 0, 0}; + float[] pt2 = {0, 0, 0, 0}; + int argb0 = PGL.nativeToJavaARGB(color[i0]); + int argb1 = PGL.nativeToJavaARGB(color[i1]); + int argb2 = PGL.nativeToJavaARGB(color[i2]); + + if (flushMode == FLUSH_CONTINUOUSLY) { + float[] src0 = {0, 0, 0, 0}; + float[] src1 = {0, 0, 0, 0}; + float[] src2 = {0, 0, 0, 0}; + PApplet.arrayCopy(vertices, 4 * i0, src0, 0, 4); + PApplet.arrayCopy(vertices, 4 * i1, src1, 0, 4); + PApplet.arrayCopy(vertices, 4 * i2, src2, 0, 4); + modelview.mult(src0, pt0); + modelview.mult(src1, pt1); + modelview.mult(src2, pt2); + } else { + PApplet.arrayCopy(vertices, 4 * i0, pt0, 0, 4); + PApplet.arrayCopy(vertices, 4 * i1, pt1, 0, 4); + PApplet.arrayCopy(vertices, 4 * i2, pt2, 0, 4); + } + + if (tex != null) { + raw.texture(tex); + if (raw.is3D()) { + raw.fill(argb0); + raw.vertex(pt0[X], pt0[Y], pt0[Z], uv[2 * i0 + 0], uv[2 * i0 + 1]); + raw.fill(argb1); + raw.vertex(pt1[X], pt1[Y], pt1[Z], uv[2 * i1 + 0], uv[2 * i1 + 1]); + raw.fill(argb2); + raw.vertex(pt2[X], pt2[Y], pt2[Z], uv[2 * i2 + 0], uv[2 * i2 + 1]); + } else if (raw.is2D()) { + float sx0 = screenXImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sy0 = screenYImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sx1 = screenXImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sy1 = screenYImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sx2 = screenXImpl(pt2[0], pt2[1], pt2[2], pt2[3]); + float sy2 = screenYImpl(pt2[0], pt2[1], pt2[2], pt2[3]); + raw.fill(argb0); + raw.vertex(sx0, sy0, uv[2 * i0 + 0], uv[2 * i0 + 1]); + raw.fill(argb1); + raw.vertex(sx1, sy1, uv[2 * i1 + 0], uv[2 * i1 + 1]); + raw.fill(argb1); + raw.vertex(sx2, sy2, uv[2 * i2 + 0], uv[2 * i2 + 1]); + } + } else { + if (raw.is3D()) { + raw.fill(argb0); + raw.vertex(pt0[X], pt0[Y], pt0[Z]); + raw.fill(argb1); + raw.vertex(pt1[X], pt1[Y], pt1[Z]); + raw.fill(argb2); + raw.vertex(pt2[X], pt2[Y], pt2[Z]); + } else if (raw.is2D()) { + float sx0 = screenXImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sy0 = screenYImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sx1 = screenXImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sy1 = screenYImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sx2 = screenXImpl(pt2[0], pt2[1], pt2[2], pt2[3]); + float sy2 = screenYImpl(pt2[0], pt2[1], pt2[2], pt2[3]); + raw.fill(argb0); + raw.vertex(sx0, sy0); + raw.fill(argb1); + raw.vertex(sx1, sy1); + raw.fill(argb2); + raw.vertex(sx2, sy2); + } + } + } + + raw.endShape(); + } + + + protected void flushLines() { + updateLineBuffers(); + + PShader shader = getLineShader(); + shader.bind(); + + IndexCache cache = tessGeo.lineIndexCache; + for (int n = 0; n < cache.size; n++) { + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int voffset = cache.vertexOffset[n]; + + shader.setVertexAttribute(bufLineVertex.glId, 4, PGL.FLOAT, 0, + 4 * voffset * PGL.SIZEOF_FLOAT); + shader.setColorAttribute(bufLineColor.glId, 4, PGL.UNSIGNED_BYTE, 0, + 4 * voffset * PGL.SIZEOF_BYTE); + shader.setLineAttribute(bufLineAttrib.glId, 4, PGL.FLOAT, 0, + 4 * voffset * PGL.SIZEOF_FLOAT); + + shader.draw(bufLineIndex.glId, icount, ioffset); + } + + shader.unbind(); + unbindLineBuffers(); + } + + + void rawLines() { + raw.colorMode(RGB); + raw.noFill(); + raw.strokeCap(strokeCap); + raw.strokeJoin(strokeJoin); + raw.beginShape(LINES); + + float[] vertices = tessGeo.lineVertices; + int[] color = tessGeo.lineColors; + float[] attribs = tessGeo.lineDirections; + short[] indices = tessGeo.lineIndices; + + IndexCache cache = tessGeo.lineIndexCache; + for (int n = 0; n < cache.size; n++) { + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int voffset = cache.vertexOffset[n]; + + for (int ln = ioffset / 6; ln < (ioffset + icount) / 6; ln++) { + // Each line segment is defined by six indices since its + // formed by two triangles. We only need the first and last + // vertices. + // This bunch of vertices could also be the bevel triangles, + // with we detect this situation by looking at the line weight. + int i0 = voffset + indices[6 * ln + 0]; + int i1 = voffset + indices[6 * ln + 5]; + float sw0 = 2 * attribs[4 * i0 + 3]; + float sw1 = 2 * attribs[4 * i1 + 3]; + + if (zero(sw0)) continue; // Bevel triangles, skip. + + float[] pt0 = {0, 0, 0, 0}; + float[] pt1 = {0, 0, 0, 0}; + int argb0 = PGL.nativeToJavaARGB(color[i0]); + int argb1 = PGL.nativeToJavaARGB(color[i1]); + + if (flushMode == FLUSH_CONTINUOUSLY) { + float[] src0 = {0, 0, 0, 0}; + float[] src1 = {0, 0, 0, 0}; + PApplet.arrayCopy(vertices, 4 * i0, src0, 0, 4); + PApplet.arrayCopy(vertices, 4 * i1, src1, 0, 4); + modelview.mult(src0, pt0); + modelview.mult(src1, pt1); + } else { + PApplet.arrayCopy(vertices, 4 * i0, pt0, 0, 4); + PApplet.arrayCopy(vertices, 4 * i1, pt1, 0, 4); + } + + if (raw.is3D()) { + raw.strokeWeight(sw0); + raw.stroke(argb0); + raw.vertex(pt0[X], pt0[Y], pt0[Z]); + raw.strokeWeight(sw1); + raw.stroke(argb1); + raw.vertex(pt1[X], pt1[Y], pt1[Z]); + } else if (raw.is2D()) { + float sx0 = screenXImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sy0 = screenYImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sx1 = screenXImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sy1 = screenYImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + raw.strokeWeight(sw0); + raw.stroke(argb0); + raw.vertex(sx0, sy0); + raw.strokeWeight(sw1); + raw.stroke(argb1); + raw.vertex(sx1, sy1); + } + } + } + + raw.endShape(); + } + + + protected void flushPoints() { + updatePointBuffers(); + + PShader shader = getPointShader(); + shader.bind(); + + IndexCache cache = tessGeo.pointIndexCache; + for (int n = 0; n < cache.size; n++) { + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int voffset = cache.vertexOffset[n]; + + shader.setVertexAttribute(bufPointVertex.glId, 4, PGL.FLOAT, 0, + 4 * voffset * PGL.SIZEOF_FLOAT); + shader.setColorAttribute(bufPointColor.glId, 4, PGL.UNSIGNED_BYTE, 0, + 4 * voffset * PGL.SIZEOF_BYTE); + shader.setPointAttribute(bufPointAttrib.glId, 2, PGL.FLOAT, 0, + 2 * voffset * PGL.SIZEOF_FLOAT); + + shader.draw(bufPointIndex.glId, icount, ioffset); + } + + shader.unbind(); + unbindPointBuffers(); + } + + + void rawPoints() { + raw.colorMode(RGB); + raw.noFill(); + raw.strokeCap(strokeCap); + raw.beginShape(POINTS); + + float[] vertices = tessGeo.pointVertices; + int[] color = tessGeo.pointColors; + float[] attribs = tessGeo.pointOffsets; + short[] indices = tessGeo.pointIndices; + + IndexCache cache = tessGeo.pointIndexCache; + for (int n = 0; n < cache.size; n++) { + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int voffset = cache.vertexOffset[n]; + + int pt = ioffset; + while (pt < (ioffset + icount) / 3) { + float size = attribs[2 * pt + 2]; + float weight; + int perim; + if (0 < size) { // round point + weight = +size / 0.5f; + perim = PApplet.min(MAX_POINT_ACCURACY, PApplet.max(MIN_POINT_ACCURACY, + (int) (TWO_PI * weight / POINT_ACCURACY_FACTOR))) + 1; + } else { // Square point + weight = -size / 0.5f; + perim = 5; + } + + int i0 = voffset + indices[3 * pt]; + int argb0 = PGL.nativeToJavaARGB(color[i0]); + float[] pt0 = {0, 0, 0, 0}; + + if (flushMode == FLUSH_CONTINUOUSLY) { + float[] src0 = {0, 0, 0, 0}; + PApplet.arrayCopy(vertices, 4 * i0, src0, 0, 4); + modelview.mult(src0, pt0); + } else { + PApplet.arrayCopy(vertices, 4 * i0, pt0, 0, 4); + } + + if (raw.is3D()) { + raw.strokeWeight(weight); + raw.stroke(argb0); + raw.vertex(pt0[X], pt0[Y], pt0[Z]); + } else if (raw.is2D()) { + float sx0 = screenXImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sy0 = screenYImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + raw.strokeWeight(weight); + raw.stroke(argb0); + raw.vertex(sx0, sy0); + } + + pt += perim; + } + } + + raw.endShape(); + } + + + ////////////////////////////////////////////////////////////// + + // BEZIER CURVE VERTICES + + + @Override + public void bezierVertex(float x2, float y2, + float x3, float y3, + float x4, float y4) { + bezierVertexImpl(x2, y2, 0, + x3, y3, 0, + x4, y4, 0); + } + + + @Override + public void bezierVertex(float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + bezierVertexImpl(x2, y2, z2, + x3, y3, z3, + x4, y4, z4); + } + + + protected void bezierVertexImpl(float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + bezierVertexCheck(shape, inGeo.vertexCount); + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addBezierVertex(x2, y2, z2, + x3, y3, z3, + x4, y4, z4, vertexBreak()); + } + + + @Override + public void quadraticVertex(float cx, float cy, + float x3, float y3) { + quadraticVertexImpl(cx, cy, 0, + x3, y3, 0); + } + + + @Override + public void quadraticVertex(float cx, float cy, float cz, + float x3, float y3, float z3) { + quadraticVertexImpl(cx, cy, cz, + x3, y3, z3); + } + + + protected void quadraticVertexImpl(float cx, float cy, float cz, + float x3, float y3, float z3) { + bezierVertexCheck(shape, inGeo.vertexCount); + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addQuadraticVertex(cx, cy, cz, + x3, y3, z3, vertexBreak()); + } + + + ////////////////////////////////////////////////////////////// + + // CATMULL-ROM CURVE VERTICES + + + @Override + public void curveVertex(float x, float y) { + curveVertexImpl(x, y, 0); + } + + + @Override + public void curveVertex(float x, float y, float z) { + curveVertexImpl(x, y, z); + } + + + protected void curveVertexImpl(float x, float y, float z) { + curveVertexCheck(shape); + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addCurveVertex(x, y, z, vertexBreak()); + } + + + ////////////////////////////////////////////////////////////// + + // POINT, LINE, TRIANGLE, QUAD + + + @Override + public void point(float x, float y) { + pointImpl(x, y, 0); + } + + + @Override + public void point(float x, float y, float z) { + pointImpl(x, y, z); + } + + + protected void pointImpl(float x, float y, float z) { + beginShape(POINTS); + defaultEdges = false; + normalMode = NORMAL_MODE_SHAPE; + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addPoint(x, y, z, fill, stroke); + endShape(); + } + + + @Override + public void line(float x1, float y1, float x2, float y2) { + lineImpl(x1, y1, 0, x2, y2, 0); + } + + + @Override + public void line(float x1, float y1, float z1, + float x2, float y2, float z2) { + lineImpl(x1, y1, z1, x2, y2, z2); + } + + + protected void lineImpl(float x1, float y1, float z1, + float x2, float y2, float z2) { + beginShape(LINES); + defaultEdges = false; + normalMode = NORMAL_MODE_SHAPE; + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addLine(x1, y1, z1, + x2, y2, z2, + fill, stroke); + endShape(); + } + + + @Override + public void triangle(float x1, float y1, float x2, float y2, + float x3, float y3) { + beginShape(TRIANGLES); + defaultEdges = false; + normalMode = NORMAL_MODE_SHAPE; + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addTriangle(x1, y1, 0, + x2, y2, 0, + x3, y3, 0, + fill, stroke); + endShape(); + } + + + @Override + public void quad(float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4) { + beginShape(QUADS); + defaultEdges = false; + normalMode = NORMAL_MODE_SHAPE; + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addQuad(x1, y1, 0, + x2, y2, 0, + x3, y3, 0, + x4, y4, 0, + stroke); + endShape(); + } + + + @Override + protected void rectImpl(float x1, float y1, float x2, float y2, + float tl, float tr, float br, float bl) { + beginShape(POLYGON); + defaultEdges = false; + normalMode = NORMAL_MODE_SHAPE; + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addRect(x1, y1, x2, y2, tl, tr, br, bl, stroke); + endShape(CLOSE); + } + + + ////////////////////////////////////////////////////////////// + + // ELLIPSE + + + @Override + public void ellipseImpl(float a, float b, float c, float d) { + beginShape(TRIANGLE_FAN); + defaultEdges = false; + normalMode = NORMAL_MODE_SHAPE; + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addEllipse(a, b, c, d, fill, stroke); + endShape(); + } + + + @Override + protected void arcImpl(float x, float y, float w, float h, + float start, float stop, int mode) { + beginShape(TRIANGLE_FAN); + defaultEdges = false; + normalMode = NORMAL_MODE_SHAPE; + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addArc(x, y, w, h, start, stop, fill, stroke, mode); + endShape(); + } + + + ////////////////////////////////////////////////////////////// + + // BOX + + // public void box(float size) + + @Override + public void box(float w, float h, float d) { + beginShape(QUADS); + defaultEdges = false; + normalMode = NORMAL_MODE_VERTEX; + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.addBox(w, h, d, fill, stroke); + endShape(); + } + + ////////////////////////////////////////////////////////////// + + // SPHERE + + // public void sphereDetail(int res) + + // public void sphereDetail(int ures, int vres) + + @Override + public void sphere(float r) { + if ((sphereDetailU < 3) || (sphereDetailV < 2)) { + sphereDetail(30); + } + + beginShape(TRIANGLES); + defaultEdges = false; + normalMode = NORMAL_MODE_VERTEX; + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + int[] indices = inGeo.addSphere(r, sphereDetailU, sphereDetailV, + fill, stroke); + endShape(indices); + } + + ////////////////////////////////////////////////////////////// + + // BEZIER + + // public float bezierPoint(float a, float b, float c, float d, float t) + + // public float bezierTangent(float a, float b, float c, float d, float t) + + // public void bezierDetail(int detail) + + // public void bezier(float x1, float y1, + // float x2, float y2, + // float x3, float y3, + // float x4, float y4) + + // public void bezier(float x1, float y1, float z1, + // float x2, float y2, float z2, + // float x3, float y3, float z3, + // float x4, float y4, float z4) + + ////////////////////////////////////////////////////////////// + + // CATMULL-ROM CURVES + + // public float curvePoint(float a, float b, float c, float d, float t) + + // public float curveTangent(float a, float b, float c, float d, float t) + + // public void curveDetail(int detail) + + // public void curveTightness(float tightness) + + // public void curve(float x1, float y1, + // float x2, float y2, + // float x3, float y3, + // float x4, float y4) + + // public void curve(float x1, float y1, float z1, + // float x2, float y2, float z2, + // float x3, float y3, float z3, + // float x4, float y4, float z4) + + ////////////////////////////////////////////////////////////// + + // IMAGES + + // public void imageMode(int mode) + + // public void image(PImage image, float x, float y) + + // public void image(PImage image, float x, float y, float c, float d) + + // public void image(PImage image, + // float a, float b, float c, float d, + // int u1, int v1, int u2, int v2) + + // protected void imageImpl(PImage image, + // float x1, float y1, float x2, float y2, + // int u1, int v1, int u2, int v2) + + ////////////////////////////////////////////////////////////// + + // SMOOTH + +/* + @Override + public void smooth() { + if (quality < 2) { + smooth(2); + } else { + smooth(quality); + } + } + + + @Override + public void smooth(int level) { + if (smoothDisabled || PGL.MAX_SAMPLES == -1) return; + + smooth = true; + + if (maxSamples < level) { + if (0 < maxSamples) { + PGraphics.showWarning(UNSUPPORTED_SMOOTH_LEVEL_ERROR, level, maxSamples); + } else{ + PGraphics.showWarning(UNSUPPORTED_SMOOTH_ERROR); + } + level = maxSamples; + } + + if (quality != level) { + smoothCallCount++; + if (parent.frameCount - lastSmoothCall < 30 && 5 < smoothCallCount) { + smoothDisabled = true; + PGraphics.showWarning(TOO_MANY_SMOOTH_CALLS_ERROR); + } + lastSmoothCall = parent.frameCount; + + quality = level; + + if (quality <= 1) { + quality = 0; + textureSampling = Texture.POINT; + } else { + textureSampling = Texture.TRILINEAR; + } + + // This will trigger a surface restart next time + // requestDraw() is called. +// restartPGL(); + } + } + + + @Override + public void noSmooth() { + if (smoothDisabled) return; + + smooth = false; + textureSampling = Texture.POINT; + + if (1 < quality) { + smoothCallCount++; + if (parent.frameCount - lastSmoothCall < 30 && 5 < smoothCallCount) { + smoothDisabled = true; + PGraphics.showWarning(TOO_MANY_SMOOTH_CALLS_ERROR); + } + lastSmoothCall = parent.frameCount; + + quality = 0; + + // This will trigger a surface restart next time + // requestDraw() is called. + restartPGL(); + } + } + */ + + + ////////////////////////////////////////////////////////////// + + // SHAPE + + // public void shapeMode(int mode) + + + // TODO unapproved + @Override + protected void shape(PShape shape, float x, float y, float z) { + if (shape.isVisible()) { // don't do expensive matrix ops if invisible + flush(); + + pushMatrix(); + + if (shapeMode == CENTER) { + translate(x - shape.getWidth() / 2, y - shape.getHeight() / 2, + z - shape.getDepth() / 2); + + } else if ((shapeMode == CORNER) || (shapeMode == CORNERS)) { + translate(x, y, z); + } + shape.draw(this); + + popMatrix(); + } + } + + + // TODO unapproved + @Override + protected void shape(PShape shape, float x, float y, float z, + float c, float d, float e) { + if (shape.isVisible()) { // don't do expensive matrix ops if invisible + flush(); + + pushMatrix(); + + if (shapeMode == CENTER) { + // x, y and z are center, c, d and e refer to a diameter + translate(x - c / 2f, y - d / 2f, z - e / 2f); + scale(c / shape.getWidth(), + d / shape.getHeight(), + e / shape.getDepth()); + + } else if (shapeMode == CORNER) { + translate(x, y, z); + scale(c / shape.getWidth(), + d / shape.getHeight(), + e / shape.getDepth()); + + } else if (shapeMode == CORNERS) { + // c, d, e are x2/y2/z2, make them into width/height/depth + c -= x; + d -= y; + e -= z; + // then same as above + translate(x, y, z); + scale(c / shape.getWidth(), + d / shape.getHeight(), + e / shape.getDepth()); + } + shape.draw(this); + + popMatrix(); + } + } + + + ////////////////////////////////////////////////////////////// + + // SHAPE I/O + + + @Override + public PShape loadShape(String filename) { + String ext = PApplet.getExtension(filename); + if (PGraphics2D.isSupportedExtension(ext)) { + return PGraphics2D.loadShapeImpl(this, filename, ext); + } if (PGraphics3D.isSupportedExtension(ext)) { + return PGraphics3D.loadShapeImpl(this, filename, ext); + } else { + PGraphics.showWarning(UNSUPPORTED_SHAPE_FORMAT_ERROR); + return null; + } + } + + + ////////////////////////////////////////////////////////////// + + // TEXT SETTINGS + + // public void textAlign(int align) + + // public void textAlign(int alignX, int alignY) + + // public float textAscent() + + // public float textDescent() + + // public void textFont(PFont which) + + // public void textFont(PFont which, float size) + + // public void textLeading(float leading) + + // public void textMode(int mode) + + @Override + protected boolean textModeCheck(int mode) { + return mode == MODEL || (mode == SHAPE && PGL.SHAPE_TEXT_SUPPORTED); + } + + // public void textSize(float size) + + // public float textWidth(char c) + + // public float textWidth(String str) + + // protected float textWidthImpl(char buffer[], int start, int stop) + + + ////////////////////////////////////////////////////////////// + + // TEXT IMPL + + + @Override + public float textAscent() { + if (textFont == null) defaultFontOrDeath("textAscent"); + Object font = textFont.getNative(); + float ascent = 0; + if (font != null) ascent = pgl.getFontAscent(font); + if (ascent == 0) ascent = super.textAscent(); + return ascent; + } + + + @Override + public float textDescent() { + if (textFont == null) defaultFontOrDeath("textDescent"); + Object font = textFont.getNative(); + float descent = 0; + if (font != null) descent = pgl.getFontDescent(font); + if (descent == 0) descent = super.textDescent(); + return descent; + } + + + @Override + protected float textWidthImpl(char buffer[], int start, int stop) { + if (textFont == null) defaultFontOrDeath("textWidth"); + Object font = textFont.getNative(); + float twidth = 0; + if (font != null) twidth = pgl.getTextWidth(font, buffer, start, stop); + if (twidth == 0) twidth = super.textWidthImpl(buffer, start, stop); + return twidth; + } + + + @Override + protected void handleTextSize(float size) { + Object font = textFont.getNative(); + if (font != null) { + Object dfont = pgl.getDerivedFont(font, size); + textFont.setNative(dfont); + } + super.handleTextSize(size); + } + + + /** + * Implementation of actual drawing for a line of text. + */ + @Override + protected void textLineImpl(char buffer[], int start, int stop, + float x, float y) { + + if (textMode == SHAPE && textFont.getNative() == null) { + showWarning("textMode(SHAPE) not available for .vlw fonts, " + + "use an .otf or .ttf instead."); + textMode(MODEL); + } + if (textMode == MODEL) { + textTex = getFontTexture(textFont); + + if (textTex == null || textTex.contextIsOutdated()) { + textTex = new FontTexture(this, textFont, is3D()); + setFontTexture(textFont, textTex); + } + + textTex.begin(); + + // Saving style parameters modified by text rendering. + int savedTextureMode = textureMode; + boolean savedStroke = stroke; + float savedNormalX = normalX; + float savedNormalY = normalY; + float savedNormalZ = normalZ; + boolean savedTint = tint; + int savedTintColor = tintColor; + int savedBlendMode = blendMode; + + // Setting style used in text rendering. + textureMode = NORMAL; + stroke = false; + normalX = 0; + normalY = 0; + normalZ = 1; + tint = true; + tintColor = fillColor; + + blendMode(BLEND); + + super.textLineImpl(buffer, start, stop, x, y); + + // Restoring original style. + textureMode = savedTextureMode; + stroke = savedStroke; + normalX = savedNormalX; + normalY = savedNormalY; + normalZ = savedNormalZ; + tint = savedTint; + tintColor = savedTintColor; + + // Note that if the user is using a blending mode different from + // BLEND, and has a bunch of continuous text rendering, the performance + // won't be optimal because at the end of each text() call the geometry + // will be flushed when restoring the user's blend. + blendMode(savedBlendMode); + + textTex.end(); + } else if (textMode == SHAPE) { + super.textLineImpl(buffer, start, stop, x, y); + } + } + + + @Override + protected void textCharImpl(char ch, float x, float y) { + PFont.Glyph glyph = textFont.getGlyph(ch); + if (glyph != null) { + if (textMode == MODEL) { + FontTexture.TextureInfo tinfo = textTex.getTexInfo(glyph); + + if (tinfo == null) { + // Adding new glyph to the font texture. + tinfo = textTex.addToTexture(this, glyph); + } + + float high = glyph.height / (float) textFont.getSize(); + float bwidth = glyph.width / (float) textFont.getSize(); + float lextent = glyph.leftExtent / (float) textFont.getSize(); + float textent = glyph.topExtent / (float) textFont.getSize(); + + float x1 = x + lextent * textSize; + float y1 = y - textent * textSize; + float x2 = x1 + bwidth * textSize; + float y2 = y1 + high * textSize; + + textCharModelImpl(tinfo, x1, y1, x2, y2); + } else if (textMode == SHAPE) { + textCharShapeImpl(ch, x, y); + } + } + } + + + protected void textCharModelImpl(FontTexture.TextureInfo info, + float x0, float y0, + float x1, float y1) { + beginShape(QUADS); + texture(textTex.getTexture(info)); + vertex(x0, y0, info.u0, info.v0); + vertex(x1, y0, info.u1, info.v0); + vertex(x1, y1, info.u1, info.v1); + vertex(x0, y1, info.u0, info.v1); + endShape(); + } + + + /** + * Ported from the implementation of textCharShapeImpl() in 1.5.1 + * + * No attempt has been made to optimize this code + *

        + * TODO: Implement a FontShape class where each glyph is tessellated and + * stored inside a larger PShapeOpenGL object (which needs to be expanded as + * new glyphs are added and exceed the initial capacity in a similar way as + * the textures in FontTexture work). When a string of text is to be rendered + * in shape mode, then the correct sequences of vertex indices are computed + * (akin to the texcoords in the texture case) and used to draw only those + * parts of the PShape object that are required for the text. + *

        + * + * Some issues of the original implementation probably remain, so they are + * reproduced below: + *

        + * Also a problem where some fonts seem to be a bit slight, as if the + * control points aren't being mapped quite correctly. Probably doing + * something dumb that the control points don't map to P5's control + * points. Perhaps it's returning b-spline data from the TrueType font? + * Though it seems like that would make a lot of garbage rather than + * just a little flattening. + *

        + * There also seems to be a bug that is causing a line (but not a filled + * triangle) back to the origin on some letters (i.e. a capital L when + * tested with Akzidenz Grotesk Light). But this won't be visible + * with the stroke shut off, so tabling that bug for now. + */ + protected void textCharShapeImpl(char ch, float x, float y) { + // save the current stroke because it needs to be disabled + // while the text is being drawn + boolean strokeSaved = stroke; + stroke = false; + + PGL.FontOutline outline = pgl.createFontOutline(ch, textFont.getNative()); + + // six element array received from the Java2D path iterator + float textPoints[] = new float[6]; + float lastX = 0; + float lastY = 0; + + boolean open = false; + beginShape(); + while (!outline.isDone()) { + int type = outline.currentSegment(textPoints); + if (!open) { + beginContour(); + open = true; + } + if (type == PGL.SEG_MOVETO || type == PGL.SEG_LINETO) { // 1 point + vertex(x + textPoints[0], y + textPoints[1]); + lastX = textPoints[0]; + lastY = textPoints[1]; + } else if (type == PGL.SEG_QUADTO) { // 2 points + for (int i = 1; i < bezierDetail; i++) { + float t = (float)i / (float)bezierDetail; + vertex(x + bezierPoint(lastX, + lastX + (float) ((textPoints[0] - lastX) * 2/3.0), + textPoints[2] + (float) ((textPoints[0] - textPoints[2]) * 2/3.0), + textPoints[2], t), + y + bezierPoint(lastY, + lastY + (float) ((textPoints[1] - lastY) * 2/3.0), + textPoints[3] + (float) ((textPoints[1] - textPoints[3]) * 2/3.0), + textPoints[3], t)); + } + lastX = textPoints[2]; + lastY = textPoints[3]; + } else if (type == PGL.SEG_CUBICTO) { // 3 points + for (int i = 1; i < bezierDetail; i++) { + float t = (float)i / (float)bezierDetail; + vertex(x + bezierPoint(lastX, textPoints[0], + textPoints[2], textPoints[4], t), + y + bezierPoint(lastY, textPoints[1], + textPoints[3], textPoints[5], t)); + } + lastX = textPoints[4]; + lastY = textPoints[5]; + } else if (type == PGL.SEG_CLOSE) { + endContour(); + open = false; + } + outline.next(); + } + endShape(); + + // re-enable stroke if it was in use before + stroke = strokeSaved; + } + + + ////////////////////////////////////////////////////////////// + + // MATRIX STACK + + + @Override + public void pushMatrix() { + if (modelviewStackDepth == MATRIX_STACK_DEPTH) { + throw new RuntimeException(ERROR_PUSHMATRIX_OVERFLOW); + } + modelview.get(modelviewStack[modelviewStackDepth]); + modelviewInv.get(modelviewInvStack[modelviewStackDepth]); + camera.get(cameraStack[modelviewStackDepth]); + cameraInv.get(cameraInvStack[modelviewStackDepth]); + modelviewStackDepth++; + } + + + @Override + public void popMatrix() { + if (modelviewStackDepth == 0) { + throw new RuntimeException(ERROR_PUSHMATRIX_UNDERFLOW); + } + modelviewStackDepth--; + modelview.set(modelviewStack[modelviewStackDepth]); + modelviewInv.set(modelviewInvStack[modelviewStackDepth]); + camera.set(cameraStack[modelviewStackDepth]); + cameraInv.set(cameraInvStack[modelviewStackDepth]); + updateProjmodelview(); + } + + + ////////////////////////////////////////////////////////////// + + // MATRIX TRANSFORMATIONS + + + @Override + public void translate(float tx, float ty) { + translateImpl(tx, ty, 0); + } + + + @Override + public void translate(float tx, float ty, float tz) { + translateImpl(tx, ty, tz); + } + + + protected void translateImpl(float tx, float ty, float tz) { + modelview.translate(tx, ty, tz); + invTranslate(modelviewInv, tx, ty, tz); + projmodelview.translate(tx, ty, tz); + } + + + static protected void invTranslate(PMatrix3D matrix, + float tx, float ty, float tz) { + matrix.preApply(1, 0, 0, -tx, + 0, 1, 0, -ty, + 0, 0, 1, -tz, + 0, 0, 0, 1); + } + + + static protected float matrixScale(PMatrix matrix) { + // Volumetric scaling factor that is associated to the given + // transformation matrix, which is given by the absolute value of its + // determinant: + float factor = 1; + + if (matrix != null) { + if (matrix instanceof PMatrix2D) { + PMatrix2D tr = (PMatrix2D)matrix; + float areaScaleFactor = Math.abs(tr.m00 * tr.m11 - tr.m01 * tr.m10); + factor = (float) Math.sqrt(areaScaleFactor); + } else if (matrix instanceof PMatrix3D) { + PMatrix3D tr = (PMatrix3D)matrix; + float volumeScaleFactor = + Math.abs(tr.m00 * (tr.m11 * tr.m22 - tr.m12 * tr.m21) + + tr.m01 * (tr.m12 * tr.m20 - tr.m10 * tr.m22) + + tr.m02 * (tr.m10 * tr.m21 - tr.m11 * tr.m20)); + factor = (float) Math.pow(volumeScaleFactor, 1.0f / 3.0f); + } + } + return factor; + } + + + /** + * Two dimensional rotation. Same as rotateZ (this is identical to a 3D + * rotation along the z-axis) but included for clarity -- it'd be weird for + * people drawing 2D graphics to be using rotateZ. And they might kick our a-- + * for the confusion. + */ + @Override + public void rotate(float angle) { + rotateImpl(angle, 0, 0, 1); + } + + + @Override + public void rotateX(float angle) { + rotateImpl(angle, 1, 0, 0); + } + + + @Override + public void rotateY(float angle) { + rotateImpl(angle, 0, 1, 0); + } + + + @Override + public void rotateZ(float angle) { + rotateImpl(angle, 0, 0, 1); + } + + + /** + * Rotate around an arbitrary vector, similar to glRotate(), except that it + * takes radians (instead of degrees). + */ + @Override + public void rotate(float angle, float v0, float v1, float v2) { + rotateImpl(angle, v0, v1, v2); + } + + + protected void rotateImpl(float angle, float v0, float v1, float v2) { + float norm2 = v0 * v0 + v1 * v1 + v2 * v2; + if (zero(norm2)) { + // The vector is zero, cannot apply rotation. + return; + } + + if (diff(norm2, 1)) { + // The rotation vector is not normalized. + float norm = PApplet.sqrt(norm2); + v0 /= norm; + v1 /= norm; + v2 /= norm; + } + + modelview.rotate(angle, v0, v1, v2); + invRotate(modelviewInv, angle, v0, v1, v2); + updateProjmodelview(); // Possibly cheaper than doing projmodelview.rotate() + } + + + static private void invRotate(PMatrix3D matrix, float angle, + float v0, float v1, float v2) { + float c = PApplet.cos(-angle); + float s = PApplet.sin(-angle); + float t = 1.0f - c; + + matrix.preApply((t*v0*v0) + c, (t*v0*v1) - (s*v2), (t*v0*v2) + (s*v1), 0, + (t*v0*v1) + (s*v2), (t*v1*v1) + c, (t*v1*v2) - (s*v0), 0, + (t*v0*v2) - (s*v1), (t*v1*v2) + (s*v0), (t*v2*v2) + c, 0, + 0, 0, 0, 1); + } + + + /** + * Same as scale(s, s, s). + */ + @Override + public void scale(float s) { + scaleImpl(s, s, s); + } + + + /** + * Same as scale(sx, sy, 1). + */ + @Override + public void scale(float sx, float sy) { + scaleImpl(sx, sy, 1); + } + + + /** + * Scale in three dimensions. + */ + @Override + public void scale(float sx, float sy, float sz) { + scaleImpl(sx, sy, sz); + } + + /** + * Scale in three dimensions. + */ + protected void scaleImpl(float sx, float sy, float sz) { + modelview.scale(sx, sy, sz); + invScale(modelviewInv, sx, sy, sz); + projmodelview.scale(sx, sy, sz); + } + + + static protected void invScale(PMatrix3D matrix, float x, float y, float z) { + matrix.preApply(1/x, 0, 0, 0, 0, 1/y, 0, 0, 0, 0, 1/z, 0, 0, 0, 0, 1); + } + + + @Override + public void shearX(float angle) { + float t = (float) Math.tan(angle); + applyMatrixImpl(1, t, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + @Override + public void shearY(float angle) { + float t = (float) Math.tan(angle); + applyMatrixImpl(1, 0, 0, 0, + t, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + ////////////////////////////////////////////////////////////// + + // MATRIX MORE! + + + @Override + public void resetMatrix() { + modelview.reset(); + modelviewInv.reset(); + projmodelview.set(projection); + + // For consistency, since modelview = camera * [all other transformations] + // the camera matrix should be set to the identity as well: + camera.reset(); + cameraInv.reset(); + } + + + @Override + public void applyMatrix(PMatrix2D source) { + applyMatrixImpl(source.m00, source.m01, 0, source.m02, + source.m10, source.m11, 0, source.m12, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + @Override + public void applyMatrix(float n00, float n01, float n02, + float n10, float n11, float n12) { + applyMatrixImpl(n00, n01, 0, n02, + n10, n11, 0, n12, + 0, 0, 1, 0, + 0, 0, 0, 1); + } + + + @Override + public void applyMatrix(PMatrix3D source) { + applyMatrixImpl(source.m00, source.m01, source.m02, source.m03, + source.m10, source.m11, source.m12, source.m13, + source.m20, source.m21, source.m22, source.m23, + source.m30, source.m31, source.m32, source.m33); + } + + + /** + * Apply a 4x4 transformation matrix to the modelview stack. + */ + @Override + public void applyMatrix(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + applyMatrixImpl(n00, n01, n02, n03, + n10, n11, n12, n13, + n20, n21, n22, n23, + n30, n31, n32, n33); + } + + + protected void applyMatrixImpl(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + modelview.apply(n00, n01, n02, n03, + n10, n11, n12, n13, + n20, n21, n22, n23, + n30, n31, n32, n33); + modelviewInv.set(modelview); + modelviewInv.invert(); + + projmodelview.apply(n00, n01, n02, n03, + n10, n11, n12, n13, + n20, n21, n22, n23, + n30, n31, n32, n33); + } + + + protected void begin2D() { + } + + + protected void end2D() { + } + + + ////////////////////////////////////////////////////////////// + + // MATRIX GET/SET/PRINT + + + @Override + public PMatrix getMatrix() { + return modelview.get(); + } + + + // public PMatrix2D getMatrix(PMatrix2D target) + + + @Override + public PMatrix3D getMatrix(PMatrix3D target) { + if (target == null) { + target = new PMatrix3D(); + } + target.set(modelview); + return target; + } + + + // public void setMatrix(PMatrix source) + + + @Override + public void setMatrix(PMatrix2D source) { + resetMatrix(); + applyMatrix(source); + } + + + /** + * Set the current transformation to the contents of the specified source. + */ + @Override + public void setMatrix(PMatrix3D source) { + resetMatrix(); + applyMatrix(source); + } + + + /** + * Print the current model (or "transformation") matrix. + */ + @Override + public void printMatrix() { + modelview.print(); + } + + + ////////////////////////////////////////////////////////////// + + // PROJECTION + + + public void pushProjection() { + if (projectionStackDepth == MATRIX_STACK_DEPTH) { + throw new RuntimeException(ERROR_PUSHMATRIX_OVERFLOW); + } + projection.get(projectionStack[projectionStackDepth]); + projectionStackDepth++; + } + + + public void popProjection() { + flush(); // The geometry with the old projection matrix needs to be drawn now + + if (projectionStackDepth == 0) { + throw new RuntimeException(ERROR_PUSHMATRIX_UNDERFLOW); + } + projectionStackDepth--; + projection.set(projectionStack[projectionStackDepth]); + updateProjmodelview(); + } + + + public void resetProjection() { + flush(); + projection.reset(); + updateProjmodelview(); + } + + + public void applyProjection(PMatrix3D mat) { + flush(); + projection.apply(mat); + updateProjmodelview(); + } + + + public void applyProjection(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + flush(); + projection.apply(n00, n01, n02, n03, + n10, n11, n12, n13, + n20, n21, n22, n23, + n30, n31, n32, n33); + updateProjmodelview(); + } + + + public void setProjection(PMatrix3D mat) { + flush(); + projection.set(mat); + updateProjmodelview(); + } + + + // Returns true if the matrix is of the form: + // x, 0, 0, a, + // 0, y, 0, b, + // 0, 0, z, c, + // 0, 0, 0, 1 + protected boolean orthoProjection() { + return zero(projection.m01) && zero(projection.m02) && + zero(projection.m10) && zero(projection.m12) && + zero(projection.m20) && zero(projection.m21) && + zero(projection.m30) && zero(projection.m31) && + zero(projection.m32) && same(projection.m33, 1); + } + + + protected boolean nonOrthoProjection() { + return nonZero(projection.m01) || nonZero(projection.m02) || + nonZero(projection.m10) || nonZero(projection.m12) || + nonZero(projection.m20) || nonZero(projection.m21) || + nonZero(projection.m30) || nonZero(projection.m31) || + nonZero(projection.m32) || diff(projection.m33, 1); + } + + + ////////////////////////////////////////////////////////////// + + // Some float math utilities + + + protected static boolean same(float a, float b) { + return Math.abs(a - b) < PGL.FLOAT_EPS; + } + + + protected static boolean diff(float a, float b) { + return PGL.FLOAT_EPS <= Math.abs(a - b); + } + + + protected static boolean zero(float a) { + return Math.abs(a) < PGL.FLOAT_EPS; + } + + + protected static boolean nonZero(float a) { + return PGL.FLOAT_EPS <= Math.abs(a); + } + + + ////////////////////////////////////////////////////////////// + + // CAMERA + + /** + * Set matrix mode to the camera matrix (instead of the current transformation + * matrix). This means applyMatrix, resetMatrix, etc. will affect the camera. + *

        + * Note that the camera matrix is *not* the perspective matrix, it contains + * the values of the modelview matrix immediatly after the latter was + * initialized with ortho() or camera(), or the modelview matrix as result of + * the operations applied between beginCamera()/endCamera(). + *

        + * beginCamera() specifies that all coordinate transforms until endCamera() + * should be pre-applied in inverse to the camera transform matrix. Note that + * this is only challenging when a user specifies an arbitrary matrix with + * applyMatrix(). Then that matrix will need to be inverted, which may not be + * possible. But take heart, if a user is applying a non-invertible matrix to + * the camera transform, then he is clearly up to no good, and we can wash our + * hands of those bad intentions. + *

        + * begin/endCamera clauses do not automatically reset the camera transform + * matrix. That's because we set up a nice default camera transform in + * setup(), and we expect it to hold through draw(). So we don't reset the + * camera transform matrix at the top of draw(). That means that an + * innocuous-looking clause like + * + *

        +   * beginCamera();
        +   * translate(0, 0, 10);
        +   * endCamera();
        +   * 
        + * + * at the top of draw(), will result in a runaway camera that shoots + * infinitely out of the screen over time. In order to prevent this, it is + * necessary to call some function that does a hard reset of the camera + * transform matrix inside of begin/endCamera. Two options are + * + *
        +   * camera(); // sets up the nice default camera transform
        +   * resetMatrix(); // sets up the identity camera transform
        +   * 
        + * + * So to rotate a camera a constant amount, you might try + * + *
        +   * beginCamera();
        +   * camera();
        +   * rotateY(PI / 8);
        +   * endCamera();
        +   * 
        + */ + @Override + public void beginCamera() { + if (manipulatingCamera) { + throw new RuntimeException("beginCamera() cannot be called again " + + "before endCamera()"); + } else { + manipulatingCamera = true; + } + } + + + /** + * Record the current settings into the camera matrix, and set the matrix mode + * back to the current transformation matrix. + *

        + * Note that this will destroy any settings to scale(), translate(), or + * whatever, because the final camera matrix will be copied (not multiplied) + * into the modelview. + */ + @Override + public void endCamera() { + if (!manipulatingCamera) { + throw new RuntimeException("Cannot call endCamera() " + + "without first calling beginCamera()"); + } + + camera.set(modelview); + cameraInv.set(modelviewInv); + + // all done + manipulatingCamera = false; + } + + + /** + * Set camera to the default settings. + *

        + * Processing camera behavior: + *

        + * Camera behavior can be split into two separate components, camera + * transformation, and projection. The transformation corresponds to the + * physical location, orientation, and scale of the camera. In a physical + * camera metaphor, this is what can manipulated by handling the camera body + * (with the exception of scale, which doesn't really have a physcial analog). + * The projection corresponds to what can be changed by manipulating the lens. + *

        + * We maintain separate matrices to represent the camera transform and + * projection. An important distinction between the two is that the camera + * transform should be invertible, where the projection matrix should not, + * since it serves to map three dimensions to two. It is possible to bake the + * two matrices into a single one just by multiplying them together, but it + * isn't a good idea, since lighting, z-ordering, and z-buffering all demand a + * true camera z coordinate after modelview and camera transforms have been + * applied but before projection. If the camera transform and projection are + * combined there is no way to recover a good camera-space z-coordinate from a + * model coordinate. + *

        + * Fortunately, there are no functions that manipulate both camera + * transformation and projection. + *

        + * camera() sets the camera position, orientation, and center of the scene. It + * replaces the camera transform with a new one. + *

        + * The transformation functions are the same ones used to manipulate the + * modelview matrix (scale, translate, rotate, etc.). But they are bracketed + * with beginCamera(), endCamera() to indicate that they should apply (in + * inverse), to the camera transformation matrix. + */ + @Override + public void camera() { + camera(defCameraX, defCameraY, defCameraZ, defCameraX, defCameraY, + 0, 0, 1, 0); + } + + + /** + * More flexible method for dealing with camera(). + *

        + * The actual call is like gluLookat. Here's the real skinny on what does + * what: + * + *

        +   * camera(); or
        +   * camera(ex, ey, ez, cx, cy, cz, ux, uy, uz);
        +   * 
        + * + * do not need to be called from with beginCamera();/endCamera(); That's + * because they always apply to the camera transformation, and they always + * totally replace it. That means that any coordinate transforms done before + * camera(); in draw() will be wiped out. It also means that camera() always + * operates in untransformed world coordinates. Therefore it is always + * redundant to call resetMatrix(); before camera(); This isn't technically + * true of gluLookat, but it's pretty much how it's used. + *

        + * Now, beginCamera(); and endCamera(); are useful if you want to move the + * camera around using transforms like translate(), etc. They will wipe out + * any coordinate system transforms that occur before them in draw(), but they + * will not automatically wipe out the camera transform. This means that they + * should be at the top of draw(). It also means that the following: + * + *

        +   * beginCamera();
        +   * rotateY(PI / 8);
        +   * endCamera();
        +   * 
        + * + * will result in a camera that spins without stopping. If you want to just + * rotate a small constant amount, try this: + * + *
        +   * beginCamera();
        +   * camera(); // sets up the default view
        +   * rotateY(PI / 8);
        +   * endCamera();
        +   * 
        + * + * That will rotate a little off of the default view. Note that this is + * entirely equivalent to + * + *
        +   * camera(); // sets up the default view
        +   * beginCamera();
        +   * rotateY(PI / 8);
        +   * endCamera();
        +   * 
        + * + * because camera() doesn't care whether or not it's inside a begin/end + * clause. Basically it's safe to use camera() or camera(ex, ey, ez, cx, cy, + * cz, ux, uy, uz) as naked calls because they do all the matrix resetting + * automatically. + */ + @Override + public void camera(float eyeX, float eyeY, float eyeZ, + float centerX, float centerY, float centerZ, + float upX, float upY, float upZ) { + cameraX = eyeX; + cameraY = eyeY; + cameraZ = eyeZ; + + // Calculating Z vector + float z0 = eyeX - centerX; + float z1 = eyeY - centerY; + float z2 = eyeZ - centerZ; + eyeDist = PApplet.sqrt(z0 * z0 + z1 * z1 + z2 * z2); + if (nonZero(eyeDist)) { + z0 /= eyeDist; + z1 /= eyeDist; + z2 /= eyeDist; + } + + // Calculating Y vector + float y0 = upX; + float y1 = upY; + float y2 = upZ; + + // Computing X vector as Y cross Z + float x0 = y1 * z2 - y2 * z1; + float x1 = -y0 * z2 + y2 * z0; + float x2 = y0 * z1 - y1 * z0; + + // Recompute Y = Z cross X + y0 = z1 * x2 - z2 * x1; + y1 = -z0 * x2 + z2 * x0; + y2 = z0 * x1 - z1 * x0; + + // Cross product gives area of parallelogram, which is < 1.0 for + // non-perpendicular unit-length vectors; so normalize x, y here: + float xmag = PApplet.sqrt(x0 * x0 + x1 * x1 + x2 * x2); + if (nonZero(xmag)) { + x0 /= xmag; + x1 /= xmag; + x2 /= xmag; + } + + float ymag = PApplet.sqrt(y0 * y0 + y1 * y1 + y2 * y2); + if (nonZero(ymag)) { + y0 /= ymag; + y1 /= ymag; + y2 /= ymag; + } + + modelview.set(x0, x1, x2, 0, + y0, y1, y2, 0, + z0, z1, z2, 0, + 0, 0, 0, 1); + + float tx = -eyeX; + float ty = -eyeY; + float tz = -eyeZ; + modelview.translate(tx, ty, tz); + + modelviewInv.set(modelview); + modelviewInv.invert(); + + camera.set(modelview); + cameraInv.set(modelviewInv); + + updateProjmodelview(); + } + + + /** + * Print the current camera matrix. + */ + @Override + public void printCamera() { + camera.print(); + } + + + protected void defaultCamera() { + camera(); + } + + + ////////////////////////////////////////////////////////////// + + // PROJECTION + + + /** + * Calls ortho() with the proper parameters for Processing's standard + * orthographic projection. + */ + @Override + public void ortho() { + ortho(-width/2f, width/2f, -height/2f, height/2f, 0, eyeDist * 10); + } + + + /** + * Calls ortho() with the specified size of the viewing volume along + * the X and Z directions. + */ + @Override + public void ortho(float left, float right, + float bottom, float top) { + ortho(left, right, bottom, top, 0, eyeDist * 10); + } + + + /** + * Sets an orthographic projection. + * + */ + @Override + public void ortho(float left, float right, + float bottom, float top, + float near, float far) { + float w = right - left; + float h = top - bottom; + float d = far - near; + + // Flushing geometry with a different perspective configuration. + flush(); + + float x = +2.0f / w; + float y = +2.0f / h; + float z = -2.0f / d; + + float tx = -(right + left) / w; + float ty = -(top + bottom) / h; + float tz = -(far + near) / d; + + // The minus sign is needed to invert the Y axis. + projection.set(x, 0, 0, tx, + 0, -y, 0, ty, + 0, 0, z, tz, + 0, 0, 0, 1); + + updateProjmodelview(); + } + + + /** + * Calls perspective() with Processing's standard coordinate projection. + *

        + * Projection functions: + *

          + *
        • frustrum() + *
        • ortho() + *
        • perspective() + *
        + * Each of these three functions completely replaces the projection matrix + * with a new one. They can be called inside setup(), and their effects will + * be felt inside draw(). At the top of draw(), the projection matrix is not + * reset. Therefore the last projection function to be called always + * dominates. On resize, the default projection is always established, which + * has perspective. + *

        + * This behavior is pretty much familiar from OpenGL, except where functions + * replace matrices, rather than multiplying against the previous. + *

        + */ + @Override + public void perspective() { + perspective(defCameraFOV, defCameraAspect, defCameraNear, defCameraFar); + } + + + /** + * Similar to gluPerspective(). Implementation based on Mesa's glu.c + */ + @Override + public void perspective(float fov, float aspect, float zNear, float zFar) { + float ymax = zNear * (float) Math.tan(fov / 2); + float ymin = -ymax; + float xmin = ymin * aspect; + float xmax = ymax * aspect; + frustum(xmin, xmax, ymin, ymax, zNear, zFar); + } + + + /** + * Same as glFrustum(), except that it wipes out (rather than multiplies + * against) the current perspective matrix. + *

        + * Implementation based on the explanation in the OpenGL blue book. + */ + @Override + public void frustum(float left, float right, float bottom, float top, + float znear, float zfar) { + // Flushing geometry with a different perspective configuration. + flush(); + + cameraFOV = 2 * (float) Math.atan2(top, znear); + cameraAspect = left / bottom; + cameraNear = znear; + cameraFar = zfar; + + float n2 = 2 * znear; + float w = right - left; + float h = top - bottom; + float d = zfar - znear; + + projection.set(n2 / w, 0, (right + left) / w, 0, + 0, -n2 / h, (top + bottom) / h, 0, + 0, 0, -(zfar + znear) / d, -(n2 * zfar) / d, + 0, 0, -1, 0); + + updateProjmodelview(); + } + + + /** + * Print the current projection matrix. + */ + @Override + public void printProjection() { + projection.print(); + } + + + protected void defaultPerspective() { + perspective(); + } + + + ////////////////////////////////////////////////////////////// + + // SCREEN AND MODEL COORDS + + + @Override + public float screenX(float x, float y) { + return screenXImpl(x, y, 0); + } + + + @Override + public float screenY(float x, float y) { + return screenYImpl(x, y, 0); + } + + + @Override + public float screenX(float x, float y, float z) { + return screenXImpl(x, y, z); + } + + + @Override + public float screenY(float x, float y, float z) { + return screenYImpl(x, y, z); + } + + + @Override + public float screenZ(float x, float y, float z) { + return screenZImpl(x, y, z); + } + + + protected float screenXImpl(float x, float y, float z) { + float ax = + modelview.m00*x + modelview.m01*y + modelview.m02*z + modelview.m03; + float ay = + modelview.m10*x + modelview.m11*y + modelview.m12*z + modelview.m13; + float az = + modelview.m20*x + modelview.m21*y + modelview.m22*z + modelview.m23; + float aw = + modelview.m30*x + modelview.m31*y + modelview.m32*z + modelview.m33; + return screenXImpl(ax, ay, az, aw); + } + + + protected float screenXImpl(float x, float y, float z, float w) { + float ox = + projection.m00*x + projection.m01*y + projection.m02*z + projection.m03*w; + float ow = + projection.m30*x + projection.m31*y + projection.m32*z + projection.m33*w; + + if (nonZero(ow)) { + ox /= ow; + } + float sx = width * (1 + ox) / 2.0f; + return sx; + } + + + protected float screenYImpl(float x, float y, float z) { + float ax = + modelview.m00*x + modelview.m01*y + modelview.m02*z + modelview.m03; + float ay = + modelview.m10*x + modelview.m11*y + modelview.m12*z + modelview.m13; + float az = + modelview.m20*x + modelview.m21*y + modelview.m22*z + modelview.m23; + float aw = + modelview.m30*x + modelview.m31*y + modelview.m32*z + modelview.m33; + return screenYImpl(ax, ay, az, aw); + } + + + protected float screenYImpl(float x, float y, float z, float w) { + float oy = + projection.m10*x + projection.m11*y + projection.m12*z + projection.m13*w; + float ow = + projection.m30*x + projection.m31*y + projection.m32*z + projection.m33*w; + + if (nonZero(ow)) { + oy /= ow; + } + float sy = height * (1 + oy) / 2.0f; + // Turning value upside down because of Processing's inverted Y axis. + sy = height - sy; + return sy; + } + + + protected float screenZImpl(float x, float y, float z) { + float ax = + modelview.m00*x + modelview.m01*y + modelview.m02*z + modelview.m03; + float ay = + modelview.m10*x + modelview.m11*y + modelview.m12*z + modelview.m13; + float az = + modelview.m20*x + modelview.m21*y + modelview.m22*z + modelview.m23; + float aw = + modelview.m30*x + modelview.m31*y + modelview.m32*z + modelview.m33; + return screenZImpl(ax, ay, az, aw); + } + + + protected float screenZImpl(float x, float y, float z, float w) { + float oz = + projection.m20*x + projection.m21*y + projection.m22*z + projection.m23*w; + float ow = + projection.m30*x + projection.m31*y + projection.m32*z + projection.m33*w; + + if (nonZero(ow)) { + oz /= ow; + } + float sz = (oz + 1) / 2.0f; + return sz; + } + + + @Override + public float modelX(float x, float y, float z) { + float ax = + modelview.m00*x + modelview.m01*y + modelview.m02*z + modelview.m03; + float ay = + modelview.m10*x + modelview.m11*y + modelview.m12*z + modelview.m13; + float az = + modelview.m20*x + modelview.m21*y + modelview.m22*z + modelview.m23; + float aw = + modelview.m30*x + modelview.m31*y + modelview.m32*z + modelview.m33; + + float ox = + cameraInv.m00*ax + cameraInv.m01*ay + cameraInv.m02*az + cameraInv.m03*aw; + float ow = + cameraInv.m30*ax + cameraInv.m31*ay + cameraInv.m32*az + cameraInv.m33*aw; + + return nonZero(ow) ? ox / ow : ox; + } + + + @Override + public float modelY(float x, float y, float z) { + float ax = + modelview.m00*x + modelview.m01*y + modelview.m02*z + modelview.m03; + float ay = + modelview.m10*x + modelview.m11*y + modelview.m12*z + modelview.m13; + float az = + modelview.m20*x + modelview.m21*y + modelview.m22*z + modelview.m23; + float aw = + modelview.m30*x + modelview.m31*y + modelview.m32*z + modelview.m33; + + float oy = + cameraInv.m10*ax + cameraInv.m11*ay + cameraInv.m12*az + cameraInv.m13*aw; + float ow = + cameraInv.m30*ax + cameraInv.m31*ay + cameraInv.m32*az + cameraInv.m33*aw; + + return nonZero(ow) ? oy / ow : oy; + } + + + @Override + public float modelZ(float x, float y, float z) { + float ax = + modelview.m00*x + modelview.m01*y + modelview.m02*z + modelview.m03; + float ay = + modelview.m10*x + modelview.m11*y + modelview.m12*z + modelview.m13; + float az = + modelview.m20*x + modelview.m21*y + modelview.m22*z + modelview.m23; + float aw = + modelview.m30*x + modelview.m31*y + modelview.m32*z + modelview.m33; + + float oz = + cameraInv.m20*ax + cameraInv.m21*ay + cameraInv.m22*az + cameraInv.m23*aw; + float ow = + cameraInv.m30*ax + cameraInv.m31*ay + cameraInv.m32*az + cameraInv.m33*aw; + + return nonZero(ow) ? oz / ow : oz; + } + + ////////////////////////////////////////////////////////////// + + // STYLES + + @Override + public void popStyle() { + // popStyle() sets ambient to true (because it calls ambient() in style()) + // and so setting the setAmbient flag to true, even if the user didn't call + // ambient, so need to revert to false. + boolean savedSetAmbient = setAmbient; + super.popStyle(); + if (!savedSetAmbient) setAmbient = false; + } + + // public void pushStyle() + // public void popStyle() + // public void style(PStyle) + // public PStyle getStyle() + // public void getStyle(PStyle) + + ////////////////////////////////////////////////////////////// + + // COLOR MODE + + // public void colorMode(int mode) + // public void colorMode(int mode, float max) + // public void colorMode(int mode, float mx, float my, float mz); + // public void colorMode(int mode, float mx, float my, float mz, float ma); + + ////////////////////////////////////////////////////////////// + + // COLOR CALC + + // protected void colorCalc(int rgb) + // protected void colorCalc(int rgb, float alpha) + // protected void colorCalc(float gray) + // protected void colorCalc(float gray, float alpha) + // protected void colorCalc(float x, float y, float z) + // protected void colorCalc(float x, float y, float z, float a) + // protected void colorCalcARGB(int argb, float alpha) + + ////////////////////////////////////////////////////////////// + + // STROKE CAP/JOIN/WEIGHT + + + @Override + public void strokeWeight(float weight) { + this.strokeWeight = weight; + } + + + @Override + public void strokeJoin(int join) { + this.strokeJoin = join; + } + + + @Override + public void strokeCap(int cap) { + this.strokeCap = cap; + } + + + ////////////////////////////////////////////////////////////// + + // FILL COLOR + + + @Override + protected void fillFromCalc() { + super.fillFromCalc(); + + if (!setAmbient) { + // Setting the ambient color from the current fill + // is what the old P3D did and allows to have an + // default ambient color when the user doesn't specify + // it explicitly. + ambientFromCalc(); + // ambientFromCalc sets setAmbient to true, but it hasn't been + // set by the user so put back to false. + setAmbient = false; + } + } + + + ////////////////////////////////////////////////////////////// + + // LIGHTING + + /** + * Sets up an ambient and directional light using OpenGL. API taken from + * PGraphics3D. + * + *

        +   * The Lighting Skinny:
        +   * The way lighting works is complicated enough that it's worth
        +   * producing a document to describe it. Lighting calculations proceed
        +   * pretty much exactly as described in the OpenGL red book.
        +   * Light-affecting material properties:
        +   *   AMBIENT COLOR
        +   *   - multiplies by light's ambient component
        +   *   - for believability this should match diffuse color
        +   *   DIFFUSE COLOR
        +   *   - multiplies by light's diffuse component
        +   *   SPECULAR COLOR
        +   *   - multiplies by light's specular component
        +   *   - usually less colored than diffuse/ambient
        +   *   SHININESS
        +   *   - the concentration of specular effect
        +   *   - this should be set pretty high (20-50) to see really
        +   *     noticeable specularity
        +   *   EMISSIVE COLOR
        +   *   - constant additive color effect
        +   * Light types:
        +   *   AMBIENT
        +   *   - one color
        +   *   - no specular color
        +   *   - no direction
        +   *   - may have falloff (constant, linear, and quadratic)
        +   *   - may have position (which matters in non-constant falloff case)
        +   *   - multiplies by a material's ambient reflection
        +   *   DIRECTIONAL
        +   *   - has diffuse color
        +   *   - has specular color
        +   *   - has direction
        +   *   - no position
        +   *   - no falloff
        +   *   - multiplies by a material's diffuse and specular reflections
        +   *   POINT
        +   *   - has diffuse color
        +   *   - has specular color
        +   *   - has position
        +   *   - no direction
        +   *   - may have falloff (constant, linear, and quadratic)
        +   *   - multiplies by a material's diffuse and specular reflections
        +   *   SPOT
        +   *   - has diffuse color
        +   *   - has specular color
        +   *   - has position
        +   *   - has direction
        +   *   - has cone angle (set to half the total cone angle)
        +   *   - has concentration value
        +   *   - may have falloff (constant, linear, and quadratic)
        +   *   - multiplies by a material's diffuse and specular reflections
        +   * Normal modes:
        +   * All of the primitives (rect, box, sphere, etc.) have their normals
        +   * set nicely. During beginShape/endShape normals can be set by the user.
        +   *   AUTO-NORMAL
        +   *   - if no normal is set during the shape, we are in auto-normal mode
        +   *   - auto-normal calculates one normal per triangle (face-normal mode)
        +   *   SHAPE-NORMAL
        +   *   - if one normal is set during the shape, it will be used for
        +   *     all vertices
        +   *   VERTEX-NORMAL
        +   *   - if multiple normals are set, each normal applies to
        +   *     subsequent vertices
        +   *   - (except for the first one, which applies to previous
        +   *     and subsequent vertices)
        +   * Efficiency consequences:
        +   *   There is a major efficiency consequence of position-dependent
        +   *   lighting calculations per vertex. (See below for determining
        +   *   whether lighting is vertex position-dependent.) If there is no
        +   *   position dependency then the only factors that affect the lighting
        +   *   contribution per vertex are its colors and its normal.
        +   *   There is a major efficiency win if
        +   *   1) lighting is not position dependent
        +   *   2) we are in AUTO-NORMAL or SHAPE-NORMAL mode
        +   *   because then we can calculate one lighting contribution per shape
        +   *   (SHAPE-NORMAL) or per triangle (AUTO-NORMAL) and simply multiply it
        +   *   into the vertex colors. The converse is our worst-case performance when
        +   *   1) lighting is position dependent
        +   *   2) we are in AUTO-NORMAL mode
        +   *   because then we must calculate lighting per-face * per-vertex.
        +   *   Each vertex has a different lighting contribution per face in
        +   *   which it appears. Yuck.
        +   * Determining vertex position dependency:
        +   *   If any of the following factors are TRUE then lighting is
        +   *   vertex position dependent:
        +   *   1) Any lights uses non-constant falloff
        +   *   2) There are any point or spot lights
        +   *   3) There is a light with specular color AND there is a
        +   *      material with specular color
        +   * So worth noting is that default lighting (a no-falloff ambient
        +   * and a directional without specularity) is not position-dependent.
        +   * We should capitalize.
        +   * Simon Greenwold, April 2005
        +   * 
        + */ + @Override + public void lights() { + enableLighting(); + + // reset number of lights + lightCount = 0; + + // need to make sure colorMode is RGB 255 here + int colorModeSaved = colorMode; + colorMode = RGB; + + lightFalloff(1, 0, 0); + lightSpecular(0, 0, 0); + + ambientLight(colorModeX * 0.5f, colorModeY * 0.5f, colorModeZ * 0.5f); + directionalLight(colorModeX * 0.5f, colorModeY * 0.5f, colorModeZ * 0.5f, + 0, 0, -1); + + colorMode = colorModeSaved; + } + + + /** + * Disables lighting. + */ + @Override + public void noLights() { + disableLighting(); + lightCount = 0; + } + + + /** + * Add an ambient light based on the current color mode. + */ + @Override + public void ambientLight(float r, float g, float b) { + ambientLight(r, g, b, 0, 0, 0); + } + + + /** + * Add an ambient light based on the current color mode. This version includes + * an (x, y, z) position for situations where the falloff distance is used. + */ + @Override + public void ambientLight(float r, float g, float b, + float x, float y, float z) { + enableLighting(); + if (lightCount == PGL.MAX_LIGHTS) { + throw new RuntimeException("can only create " + PGL.MAX_LIGHTS + + " lights"); + } + + lightType[lightCount] = AMBIENT; + + lightPosition(lightCount, x, y, z, false); + lightNormal(lightCount, 0, 0, 0); + + lightAmbient(lightCount, r, g, b); + noLightDiffuse(lightCount); + noLightSpecular(lightCount); + noLightSpot(lightCount); + lightFalloff(lightCount, currentLightFalloffConstant, + currentLightFalloffLinear, + currentLightFalloffQuadratic); + + lightCount++; + } + + + @Override + public void directionalLight(float r, float g, float b, + float dx, float dy, float dz) { + enableLighting(); + if (lightCount == PGL.MAX_LIGHTS) { + throw new RuntimeException("can only create " + PGL.MAX_LIGHTS + + " lights"); + } + + lightType[lightCount] = DIRECTIONAL; + + lightPosition(lightCount, 0, 0, 0, true); + lightNormal(lightCount, dx, dy, dz); + + noLightAmbient(lightCount); + lightDiffuse(lightCount, r, g, b); + lightSpecular(lightCount, currentLightSpecular[0], + currentLightSpecular[1], + currentLightSpecular[2]); + noLightSpot(lightCount); + noLightFalloff(lightCount); + + lightCount++; + } + + + @Override + public void pointLight(float r, float g, float b, + float x, float y, float z) { + enableLighting(); + if (lightCount == PGL.MAX_LIGHTS) { + throw new RuntimeException("can only create " + PGL.MAX_LIGHTS + + " lights"); + } + + lightType[lightCount] = POINT; + + lightPosition(lightCount, x, y, z, false); + lightNormal(lightCount, 0, 0, 0); + + noLightAmbient(lightCount); + lightDiffuse(lightCount, r, g, b); + lightSpecular(lightCount, currentLightSpecular[0], + currentLightSpecular[1], + currentLightSpecular[2]); + noLightSpot(lightCount); + lightFalloff(lightCount, currentLightFalloffConstant, + currentLightFalloffLinear, + currentLightFalloffQuadratic); + + lightCount++; + } + + + @Override + public void spotLight(float r, float g, float b, + float x, float y, float z, + float dx, float dy, float dz, + float angle, float concentration) { + enableLighting(); + if (lightCount == PGL.MAX_LIGHTS) { + throw new RuntimeException("can only create " + PGL.MAX_LIGHTS + + " lights"); + } + + lightType[lightCount] = SPOT; + + lightPosition(lightCount, x, y, z, false); + lightNormal(lightCount, dx, dy, dz); + + noLightAmbient(lightCount); + lightDiffuse(lightCount, r, g, b); + lightSpecular(lightCount, currentLightSpecular[0], + currentLightSpecular[1], + currentLightSpecular[2]); + lightSpot(lightCount, angle, concentration); + lightFalloff(lightCount, currentLightFalloffConstant, + currentLightFalloffLinear, + currentLightFalloffQuadratic); + + lightCount++; + } + + + /** + * Set the light falloff rates for the last light that was created. Default is + * lightFalloff(1, 0, 0). + */ + @Override + public void lightFalloff(float constant, float linear, float quadratic) { + currentLightFalloffConstant = constant; + currentLightFalloffLinear = linear; + currentLightFalloffQuadratic = quadratic; + } + + + /** + * Set the specular color of the last light created. + */ + @Override + public void lightSpecular(float x, float y, float z) { + colorCalc(x, y, z); + currentLightSpecular[0] = calcR; + currentLightSpecular[1] = calcG; + currentLightSpecular[2] = calcB; + } + + + protected void enableLighting() { + flush(); + lights = true; + } + + + protected void disableLighting() { + flush(); + lights = false; + } + + + protected void lightPosition(int num, float x, float y, float z, + boolean dir) { + lightPosition[4 * num + 0] = + x*modelview.m00 + y*modelview.m01 + z*modelview.m02 + modelview.m03; + lightPosition[4 * num + 1] = + x*modelview.m10 + y*modelview.m11 + z*modelview.m12 + modelview.m13; + lightPosition[4 * num + 2] = + x*modelview.m20 + y*modelview.m21 + z*modelview.m22 + modelview.m23; + + // Used to indicate if the light is directional or not. + lightPosition[4 * num + 3] = dir ? 0 : 1; + } + + + protected void lightNormal(int num, float dx, float dy, float dz) { + // Applying normal matrix to the light direction vector, which is the + // transpose of the inverse of the modelview. + float nx = + dx*modelviewInv.m00 + dy*modelviewInv.m10 + dz*modelviewInv.m20; + float ny = + dx*modelviewInv.m01 + dy*modelviewInv.m11 + dz*modelviewInv.m21; + float nz = + dx*modelviewInv.m02 + dy*modelviewInv.m12 + dz*modelviewInv.m22; + + float d = PApplet.dist(0, 0, 0, nx, ny, nz); + if (0 < d) { + float invn = 1.0f / d; + lightNormal[3 * num + 0] = invn * nx; + lightNormal[3 * num + 1] = invn * ny; + lightNormal[3 * num + 2] = invn * nz; + } else { + lightNormal[3 * num + 0] = 0; + lightNormal[3 * num + 1] = 0; + lightNormal[3 * num + 2] = 0; + } + } + + + protected void lightAmbient(int num, float r, float g, float b) { + colorCalc(r, g, b); + lightAmbient[3 * num + 0] = calcR; + lightAmbient[3 * num + 1] = calcG; + lightAmbient[3 * num + 2] = calcB; + } + + + protected void noLightAmbient(int num) { + lightAmbient[3 * num + 0] = 0; + lightAmbient[3 * num + 1] = 0; + lightAmbient[3 * num + 2] = 0; + } + + + protected void lightDiffuse(int num, float r, float g, float b) { + colorCalc(r, g, b); + lightDiffuse[3 * num + 0] = calcR; + lightDiffuse[3 * num + 1] = calcG; + lightDiffuse[3 * num + 2] = calcB; + } + + + protected void noLightDiffuse(int num) { + lightDiffuse[3 * num + 0] = 0; + lightDiffuse[3 * num + 1] = 0; + lightDiffuse[3 * num + 2] = 0; + } + + + protected void lightSpecular(int num, float r, float g, float b) { + lightSpecular[3 * num + 0] = r; + lightSpecular[3 * num + 1] = g; + lightSpecular[3 * num + 2] = b; + } + + + protected void noLightSpecular(int num) { + lightSpecular[3 * num + 0] = 0; + lightSpecular[3 * num + 1] = 0; + lightSpecular[3 * num + 2] = 0; + } + + + protected void lightFalloff(int num, float c0, float c1, float c2) { + lightFalloffCoefficients[3 * num + 0] = c0; + lightFalloffCoefficients[3 * num + 1] = c1; + lightFalloffCoefficients[3 * num + 2] = c2; + } + + + protected void noLightFalloff(int num) { + lightFalloffCoefficients[3 * num + 0] = 1; + lightFalloffCoefficients[3 * num + 1] = 0; + lightFalloffCoefficients[3 * num + 2] = 0; + } + + + protected void lightSpot(int num, float angle, float exponent) { + lightSpotParameters[2 * num + 0] = Math.max(0, PApplet.cos(angle)); + lightSpotParameters[2 * num + 1] = exponent; + } + + + protected void noLightSpot(int num) { + lightSpotParameters[2 * num + 0] = 0; + lightSpotParameters[2 * num + 1] = 0; + } + + + ////////////////////////////////////////////////////////////// + + // BACKGROUND + + + @Override + protected void backgroundImpl(PImage image) { + backgroundImpl(); + set(0, 0, image); + // Setting the background as opaque. If this an offscreen surface, the + // alpha channel will be set to 1 in endOffscreenDraw(), even if + // blending operations during draw create translucent areas in the + // color buffer. + backgroundA = 1; + loaded = false; + } + + + @Override + protected void backgroundImpl() { + flush(); + pgl.clearBackground(backgroundR, backgroundG, backgroundB, backgroundA, + !hints[DISABLE_DEPTH_MASK], true); + loaded = false; + } + + + ////////////////////////////////////////////////////////////// + + // COLOR MODE + + // colorMode() is inherited from PGraphics. + + + ////////////////////////////////////////////////////////////// + + // COLOR METHODS + + // public final int color(int gray) + // public final int color(int gray, int alpha) + // public final int color(int rgb, float alpha) + // public final int color(int x, int y, int z) + + // public final float alpha(int what) + // public final float red(int what) + // public final float green(int what) + // public final float blue(int what) + // public final float hue(int what) + // public final float saturation(int what) + // public final float brightness(int what) + + // public int lerpColor(int c1, int c2, float amt) + // static public int lerpColor(int c1, int c2, float amt, int mode) + + ////////////////////////////////////////////////////////////// + + // BEGINRAW/ENDRAW + + // beginRaw, endRaw() both inherited. + + ////////////////////////////////////////////////////////////// + + // WARNINGS and EXCEPTIONS + + // showWarning() and showException() available from PGraphics. + + /** + * Report on anything from glError(). + * Don't use this inside glBegin/glEnd otherwise it'll + * throw an GL_INVALID_OPERATION error. + */ + protected void report(String where) { + if (!hints[DISABLE_OPENGL_ERRORS]) { + int err = pgl.getError(); + if (err != 0) { + String errString = pgl.errorString(err); + String msg = "OpenGL error " + err + " at " + where + ": " + errString; + PGraphics.showWarning(msg); + } + } + } + + + + ////////////////////////////////////////////////////////////// + + // RENDERER SUPPORT QUERIES + + // public boolean displayable() + + + @Override + public boolean isGL() { + return true; + } + + + ////////////////////////////////////////////////////////////// + + // LOAD/UPDATE PIXELS + + + // Initializes the pixels array, copying the current contents of the + // color buffer into it. + @Override + public void loadPixels() { + if (primaryGraphics && sized) { + // Something wrong going on with threading, sized can never be true if + // all the steps in a resize happen inside the Animation thread. + return; + } + + boolean needEndDraw = false; + if (!drawing) { + beginDraw(); + needEndDraw = true; + } + + if (!loaded) { + // Draws any remaining geometry in case the user is still not + // setting/getting new pixels. + flush(); + } + + allocatePixels(); + + if (!loaded) { + readPixels(); + } + + // Pixels are now up-to-date, set the flag. + loaded = true; + + + if (needEndDraw) { + endDraw(); + } + } + + + protected void allocatePixels() { + updatePixelSize(); + if ((pixels == null) || (pixels.length != pixelWidth * pixelHeight)) { + pixels = new int[pixelWidth * pixelHeight]; + pixelBuffer = PGL.allocateIntBuffer(pixels); + loaded = false; + } + } + + + protected void readPixels() { + updatePixelSize(); + beginPixelsOp(OP_READ); + try { + // The readPixelsImpl() call in inside a try/catch block because it appears + // that (only sometimes) JOGL will run beginDraw/endDraw on the EDT + // thread instead of the Animation thread right after a resize. Because + // of this the width and height might have a different size than the + // one of the pixels arrays. + pgl.readPixelsImpl(0, 0, pixelWidth, pixelHeight, PGL.RGBA, PGL.UNSIGNED_BYTE, + pixelBuffer); + } catch (IndexOutOfBoundsException e) { + // Silently catch the exception. + } + endPixelsOp(); + try { + // Idem... + PGL.getIntArray(pixelBuffer, pixels); + PGL.nativeToJavaARGB(pixels, pixelWidth, pixelHeight); + } catch (ArrayIndexOutOfBoundsException e) { + } + } + + + protected void drawPixels(int x, int y, int w, int h) { + int len = w * h; + if (nativePixels == null || nativePixels.length < len) { + nativePixels = new int[len]; + nativePixelBuffer = PGL.allocateIntBuffer(nativePixels); + } + + try { + if (0 < x || 0 < y || w < pixelWidth || h < pixelHeight) { + // The pixels to be copied to the texture need to be consecutive, and + // they are not in the pixels array, so putting each row one after + // another in nativePixels. + int offset0 = y * pixelWidth + x; + int offset1 = 0; + + for (int yc = y; yc < y + h; yc++) { + System.arraycopy(pixels, offset0, nativePixels, offset1, w); + offset0 += pixelWidth; + offset1 += w; + } + } else { + PApplet.arrayCopy(pixels, 0, nativePixels, 0, len); + } + PGL.javaToNativeARGB(nativePixels, w, h); + } catch (ArrayIndexOutOfBoundsException e) { + } + PGL.putIntArray(nativePixelBuffer, nativePixels); + // Copying pixel buffer to screen texture... + if (primaryGraphics && !pgl.isFBOBacked()) { + // First making sure that the screen texture is valid. Only in the case + // of non-FBO-backed primary surface we might need to create the texture. + loadTextureImpl(POINT, false); + } + + boolean needToDrawTex = primaryGraphics && (!pgl.isFBOBacked() || + (pgl.isFBOBacked() && pgl.isMultisampled())) || + offscreenMultisample; + if (texture == null) return; + if (needToDrawTex) { + // The texture to screen needs to be drawn only if we are on the primary + // surface w/out FBO-layer, or with FBO-layer and multisampling. Or, we + // are doing multisampled offscreen. Why? Because in the case of + // non-multisampled FBO, texture is actually the color buffer used by the + // color FBO, so with the copy operation we should be done updating the + // (off)screen buffer. + // First, copy the pixels to the texture. We don't need to invert the + // pixel copy because the texture will be drawn inverted. + int tw = PApplet.min(texture.glWidth - x, w); + int th = PApplet.min(texture.glHeight - y, h); + pgl.copyToTexture(texture.glTarget, texture.glFormat, texture.glName, + x, y, tw, th, nativePixelBuffer); + beginPixelsOp(OP_WRITE); + drawTexture(x, y, w, h); + endPixelsOp(); + } else { + // We only need to copy the pixels to the back texture where we are + // currently drawing to. Because the texture is inverted along Y, we + // need to reflect that in the vertical arguments. + pgl.copyToTexture(texture.glTarget, texture.glFormat, texture.glName, + x, pixelHeight - (y + h), w, h, nativePixelBuffer); + } + } + + + ////////////////////////////////////////////////////////////// + + // GET/SET PIXELS + + + @Override + public int get(int x, int y) { + loadPixels(); + return super.get(x, y); + } + + + @Override + protected void getImpl(int sourceX, int sourceY, + int sourceWidth, int sourceHeight, + PImage target, int targetX, int targetY) { + loadPixels(); + super.getImpl(sourceX, sourceY, sourceWidth, sourceHeight, + target, targetX, targetY); + } + + + @Override + public void set(int x, int y, int argb) { + loadPixels(); + super.set(x, y, argb); + } + + + @Override + protected void setImpl(PImage sourceImage, + int sourceX, int sourceY, + int sourceWidth, int sourceHeight, + int targetX, int targetY) { + updatePixelSize(); + + // Copies the pixels + loadPixels(); + int sourceOffset = sourceY * sourceImage.pixelWidth + sourceX; + int targetOffset = targetY * pixelWidth + targetX; + for (int y = sourceY; y < sourceY + sourceHeight; y++) { + System.arraycopy(sourceImage.pixels, sourceOffset, pixels, targetOffset, sourceWidth); + sourceOffset += sourceImage.pixelWidth; + targetOffset += pixelWidth; + } + + // Draws the texture, copy() is very efficient because it simply renders + // the texture cache of sourceImage using OpenGL. + copy(sourceImage, + sourceX, sourceY, sourceWidth, sourceHeight, + targetX, targetY, sourceWidth, sourceHeight); + } + + + ////////////////////////////////////////////////////////////// + + // SAVE + + + @Override + public boolean save(String filename) { + return saveImpl(filename); + } + + + @Override + protected void processImageBeforeAsyncSave(PImage image) { + if (image.format == AsyncPixelReader.OPENGL_NATIVE) { + PGL.nativeToJavaARGB(image.pixels, image.pixelWidth, image.pixelHeight); + image.format = ARGB; + } else if (image.format == AsyncPixelReader.OPENGL_NATIVE_OPAQUE) { + PGL.nativeToJavaRGB(image.pixels, image.pixelWidth, image.pixelHeight); + image.format = RGB; + } + } + + + protected static void completeFinishedPixelTransfers() { + ongoingPixelTransfersIterable.addAll(ongoingPixelTransfers); + for (AsyncPixelReader pixelReader : ongoingPixelTransfersIterable) { + // if the getter was not called this frame, + // tell it to check for completed transfers now + if (!pixelReader.calledThisFrame) { + pixelReader.completeFinishedTransfers(); + } + pixelReader.calledThisFrame = false; + } + ongoingPixelTransfersIterable.clear(); + } + + protected static void completeAllPixelTransfers() { + ongoingPixelTransfersIterable.addAll(ongoingPixelTransfers); + for (AsyncPixelReader pixelReader : ongoingPixelTransfersIterable) { + pixelReader.completeAllTransfers(); + } + ongoingPixelTransfersIterable.clear(); + } + + + @Override + protected void awaitAsyncSaveCompletion(String filename) { + if (asyncPixelReader != null) { + ongoingPixelTransfersIterable.addAll(ongoingPixelTransfers); + File file = parent.sketchFile(filename); + for (AsyncPixelReader pixelReader : ongoingPixelTransfersIterable) { + pixelReader.awaitTransferCompletion(file); + } + ongoingPixelTransfersIterable.clear(); + } + super.awaitAsyncSaveCompletion(filename); + } + + + protected class AsyncPixelReader { + + // PImage formats used internally to offload + // color format conversion to save threads + static final int OPENGL_NATIVE = -1; + static final int OPENGL_NATIVE_OPAQUE = -2; + + static final int BUFFER_COUNT = 3; + + int[] pbos; + long[] fences; + File[] files; + int[] widths; + int[] heights; + + int head; + int tail; + int size; + + boolean supportsAsyncTransfers; + + boolean calledThisFrame; + + + /// PGRAPHICS API ////////////////////////////////////////////////////////// + + public AsyncPixelReader() { + supportsAsyncTransfers = pgl.hasPBOs() && pgl.hasSynchronization(); + if (supportsAsyncTransfers) { + pbos = new int[BUFFER_COUNT]; + fences = new long[BUFFER_COUNT]; + files = new File[BUFFER_COUNT]; + widths = new int[BUFFER_COUNT]; + heights = new int[BUFFER_COUNT]; + + IntBuffer intBuffer = PGL.allocateIntBuffer(BUFFER_COUNT); + intBuffer.rewind(); + pgl.genBuffers(BUFFER_COUNT, intBuffer); + for (int i = 0; i < BUFFER_COUNT; i++) { + pbos[i] = intBuffer.get(i); + } + } + } + + + public void dispose() { + if (fences != null) { + while (size > 0) { + pgl.deleteSync(fences[tail]); + size--; + tail = (tail + 1) % BUFFER_COUNT; + } + fences = null; + } + if (pbos != null) { + for (int i = 0; i < BUFFER_COUNT; i++) { + IntBuffer intBuffer = PGL.allocateIntBuffer(pbos); + pgl.deleteBuffers(BUFFER_COUNT, intBuffer); + } + pbos = null; + } + files = null; + widths = null; + heights = null; + size = 0; + head = 0; + tail = 0; + calledThisFrame = false; + ongoingPixelTransfers.remove(this); + } + + + public void readAndSaveAsync(final File file) { + if (size > 0) { + boolean shouldRead = (size == BUFFER_COUNT); + if (!shouldRead) shouldRead = isLastTransferComplete(); + if (shouldRead) endTransfer(); + } else { + ongoingPixelTransfers.add(this); + } + beginTransfer(file); + calledThisFrame = true; + } + + + public void completeFinishedTransfers() { + if (size <= 0 || !asyncImageSaver.hasAvailableTarget()) return; + + boolean needEndDraw = false; + if (!drawing) { + beginDraw(); + needEndDraw = true; + } + + while (asyncImageSaver.hasAvailableTarget() && + isLastTransferComplete()) { + endTransfer(); + } + + // make sure to always unregister if there are no ongoing transfers + // so that PGraphics can be GC'd if needed + if (size <= 0) ongoingPixelTransfers.remove(this); + + if (needEndDraw) endDraw(); + } + + + protected void completeAllTransfers() { + if (size <= 0) return; + completeTransfers(size); + } + + + protected void completeTransfers(int count) { + if (size <= 0) return; + if (count <= 0) return; + + boolean needEndDraw = false; + if (!drawing) { + beginDraw(); + needEndDraw = true; + } + + while (size > 0 && count > 0) { + endTransfer(); + count--; + } + + // make sure to always unregister if there are no ongoing transfers + // so that PGraphics can be GC'd if needed + if (size <= 0) { + ongoingPixelTransfers.remove(this); + } + + if (needEndDraw) endDraw(); + } + + + protected void awaitTransferCompletion(File file) { + if (size <= 0) return; + + int i = tail; // tail -> head, wraps around (we have circular queue) + int j = 0; // 0 -> size, simple counter + int lastIndex = 0; + do { + if (file.equals(files[i])) { + lastIndex = j; // no 'break' here, we need last index for this filename + } + i = (i + 1) % BUFFER_COUNT; + j++; + } while (i != head); + + if (lastIndex <= 0) return; + + // Saving this file is in progress, block until transfers complete + completeTransfers(lastIndex + 1); + } + + + /// TRANSFERS ////////////////////////////////////////////////////////////// + + public boolean isLastTransferComplete() { + if (size <= 0) return false; + int status = pgl.clientWaitSync(fences[tail], 0, 0); + return (status == PGL.ALREADY_SIGNALED) || + (status == PGL.CONDITION_SATISFIED); + } + + + public void beginTransfer(File file) { + // check the size of the buffer + if (widths[head] != pixelWidth || heights[head] != pixelHeight) { + if (widths[head] * heights[head] != pixelWidth * pixelHeight) { + pgl.bindBuffer(PGL.PIXEL_PACK_BUFFER, pbos[head]); + pgl.bufferData(PGL.PIXEL_PACK_BUFFER, + Integer.SIZE/8 * pixelWidth * pixelHeight, + null, PGL.STREAM_READ); + } + widths[head] = pixelWidth; + heights[head] = pixelHeight; + pgl.bindBuffer(PGL.PIXEL_PACK_BUFFER, 0); + } + + pgl.bindBuffer(PGL.PIXEL_PACK_BUFFER, pbos[head]); + pgl.readPixels(0, 0, pixelWidth, pixelHeight, PGL.RGBA, PGL.UNSIGNED_BYTE, 0); + pgl.bindBuffer(PGL.PIXEL_PACK_BUFFER, 0); + + fences[head] = pgl.fenceSync(PGL.SYNC_GPU_COMMANDS_COMPLETE, 0); + files[head] = file; + + head = (head + 1) % BUFFER_COUNT; + size++; + } + + + public void endTransfer() { + pgl.deleteSync(fences[tail]); + pgl.bindBuffer(PGL.PIXEL_PACK_BUFFER, pbos[tail]); + ByteBuffer readBuffer = pgl.mapBuffer(PGL.PIXEL_PACK_BUFFER, + PGL.READ_ONLY); + if (readBuffer != null) { + int format = primaryGraphics ? OPENGL_NATIVE_OPAQUE : OPENGL_NATIVE; + PImage target = asyncImageSaver.getAvailableTarget(widths[tail], + heights[tail], + format); + if (target == null) return; + readBuffer.rewind(); + readBuffer.asIntBuffer().get(target.pixels); + pgl.unmapBuffer(PGL.PIXEL_PACK_BUFFER); + asyncImageSaver.saveTargetAsync(PGraphicsOpenGL.this, target, + files[tail]); + } + + pgl.bindBuffer(PGL.PIXEL_PACK_BUFFER, 0); + + size--; + tail = (tail + 1) % BUFFER_COUNT; + } + + } + + + ////////////////////////////////////////////////////////////// + + // LOAD/UPDATE TEXTURE + + + // Loads the current contents of the renderer's drawing surface into the + // its texture. + public void loadTexture() { + boolean needEndDraw = false; + if (!drawing) { + beginDraw(); + needEndDraw = true; + } + + flush(); // To make sure the color buffer is updated. + + if (primaryGraphics) { + updatePixelSize(); + + if (pgl.isFBOBacked()) { + // In the case of MSAA, this is needed so the back buffer is in sync + // with the rendering. + pgl.syncBackTexture(); + } else { + loadTextureImpl(Texture.POINT, false); + + // Here we go the slow route: we first copy the contents of the color + // buffer into a pixels array (but we keep it in native format) and + // then copy this array into the texture. + if (nativePixels == null || nativePixels.length < pixelWidth * pixelHeight) { + nativePixels = new int[pixelWidth * pixelHeight]; + nativePixelBuffer = PGL.allocateIntBuffer(nativePixels); + } + + beginPixelsOp(OP_READ); + try { + // See comments in readPixels() for the reason for this try/catch. + pgl.readPixelsImpl(0, 0, pixelWidth, pixelHeight, PGL.RGBA, PGL.UNSIGNED_BYTE, + nativePixelBuffer); + } catch (IndexOutOfBoundsException e) { + } + endPixelsOp(); + + if (texture != null) { + texture.setNative(nativePixelBuffer, 0, 0, pixelWidth, pixelHeight); + } + } + } else if (offscreenMultisample) { + // We need to copy the contents of the multisampled buffer to the color + // buffer, so the later is up-to-date with the last drawing. + FrameBuffer ofb = offscreenFramebuffer; + FrameBuffer mfb = multisampleFramebuffer; + if (ofb != null && mfb != null) { + mfb.copyColor(ofb); + } + } + + if (needEndDraw) { + endDraw(); + } + } + + + // Just marks the whole texture as updated + public void updateTexture() { + if (texture != null) { + texture.updateTexels(); + } + } + + + // Marks the specified rectanglular subregion in the texture as + // updated. + public void updateTexture(int x, int y, int w, int h) { + if (texture != null) { + texture.updateTexels(x, y, w, h); + } + } + + + // Draws wherever it is in the screen texture right now to the display. + public void updateDisplay() { + flush(); + beginPixelsOp(OP_WRITE); + drawTexture(); + endPixelsOp(); + } + + + protected void loadTextureImpl(int sampling, boolean mipmap) { + updatePixelSize(); + if (pixelWidth == 0 || pixelHeight == 0) return; + if (texture == null || texture.contextIsOutdated()) { + Texture.Parameters params = new Texture.Parameters(ARGB, + sampling, mipmap); + texture = new Texture(this, pixelWidth, pixelHeight, params); + texture.invertedY(true); + texture.colorBuffer(true); + setCache(this, texture); + } + } + + + protected void createPTexture() { + updatePixelSize(); + if (texture != null) { + ptexture = new Texture(this, pixelWidth, pixelHeight, texture.getParameters()); + ptexture.invertedY(true); + ptexture.colorBuffer(true); + } + } + + + protected void swapOffscreenTextures() { + FrameBuffer ofb = offscreenFramebuffer; + if (texture != null && ptexture != null && ofb != null) { + int temp = texture.glName; + texture.glName = ptexture.glName; + ptexture.glName = temp; + ofb.setColorBuffer(texture); + } + } + + + protected void drawTexture() { + if (texture != null) { + // No blend so the texure replaces wherever is on the screen, + // irrespective of the alpha + pgl.disable(PGL.BLEND); + pgl.drawTexture(texture.glTarget, texture.glName, + texture.glWidth, texture.glHeight, + 0, 0, width, height); + pgl.enable(PGL.BLEND); + } + } + + + protected void drawTexture(int x, int y, int w, int h) { + if (texture != null) { + // Processing Y axis is inverted with respect to OpenGL, so we need to + // invert the y coordinates of the screen rectangle. + pgl.disable(PGL.BLEND); + pgl.drawTexture(texture.glTarget, texture.glName, + texture.glWidth, texture.glHeight, + 0, 0, pixelWidth, pixelHeight, 1, + x, y, x + w, y + h, + x, pixelHeight - (y + h), x + w, pixelHeight - y); + pgl.enable(PGL.BLEND); + } + } + + + protected void drawPTexture() { + if (ptexture != null) { + // No blend so the texure replaces wherever is on the screen, + // irrespective of the alpha + pgl.disable(PGL.BLEND); + pgl.drawTexture(ptexture.glTarget, ptexture.glName, + ptexture.glWidth, ptexture.glHeight, + 0, 0, width, height); + pgl.enable(PGL.BLEND); + } + } + + + ////////////////////////////////////////////////////////////// + + // MASK + + +// @Override +// public void mask(int alpha[]) { +// PImage temp = get(); +// temp.mask(alpha); +// set(0, 0, temp); +// } + + + @Override + public void mask(PImage alpha) { + updatePixelSize(); + if (alpha.pixelWidth != pixelWidth || alpha.pixelHeight != pixelHeight) { + throw new RuntimeException("The PImage used with mask() must be " + + "the same size as the applet."); + } + + PGraphicsOpenGL ppg = getPrimaryPG(); + if (ppg.maskShader == null) { + ppg.maskShader = new PShader(parent, defTextureShaderVertURL, + maskShaderFragURL); + } + ppg.maskShader.set("mask", alpha); + filter(ppg.maskShader); + } + + + + ////////////////////////////////////////////////////////////// + + // FILTER + + + /** + * This is really inefficient and not a good idea in OpenGL. Use get() and + * set() with a smaller image area, or call the filter on an image instead, + * and then draw that. + */ + @Override + public void filter(int kind) { + PImage temp = get(); + temp.filter(kind); + set(0, 0, temp); + } + + + /** + * This is really inefficient and not a good idea in OpenGL. Use get() and + * set() with a smaller image area, or call the filter on an image instead, + * and then draw that. + */ + @Override + public void filter(int kind, float param) { + PImage temp = get(); + temp.filter(kind, param); + set(0, 0, temp); + } + + + @Override + public void filter(PShader shader) { + if (!shader.isPolyShader()) { + PGraphics.showWarning(INVALID_FILTER_SHADER_ERROR); + return; + } + + boolean needEndDraw = false; + if (primaryGraphics) { + pgl.enableFBOLayer(); + } else if (!drawing) { + beginDraw(); + needEndDraw = true; + } + loadTexture(); + + if (filterTexture == null || filterTexture.contextIsOutdated()) { + filterTexture = new Texture(this, texture.width, texture.height, + texture.getParameters()); + filterTexture.invertedY(true); + filterImage = wrapTexture(filterTexture); + } + filterTexture.set(texture); + + // Disable writing to the depth buffer, so that after applying the filter we + // can still use the depth information to keep adding geometry to the scene. + pgl.depthMask(false); + // Also disabling depth testing so the texture is drawn on top of everything + // that has been drawn before. + pgl.disable(PGL.DEPTH_TEST); + + // Drawing a textured quad in 2D, covering the entire screen, + // with the filter shader applied to it: + begin2D(); + + // Changing light configuration and shader after begin2D() + // because it calls flush(). + boolean prevLights = lights; + lights = false; + int prevTextureMode = textureMode; + textureMode = NORMAL; + boolean prevStroke = stroke; + stroke = false; + int prevBlendMode = blendMode; + blendMode(REPLACE); + PShader prevShader = polyShader; + polyShader = shader; + + beginShape(QUADS); + texture(filterImage); + vertex(0, 0, 0, 0); + vertex(width, 0, 1, 0); + vertex(width, height, 1, 1); + vertex(0, height, 0, 1); + endShape(); + end2D(); + + // Restoring previous configuration. + polyShader = prevShader; + stroke = prevStroke; + lights = prevLights; + textureMode = prevTextureMode; + blendMode(prevBlendMode); + + if (!hints[DISABLE_DEPTH_TEST]) { + pgl.enable(PGL.DEPTH_TEST); + } + if (!hints[DISABLE_DEPTH_MASK]) { + pgl.depthMask(true); + } + + if (needEndDraw) { + endDraw(); + } + } + + + ////////////////////////////////////////////////////////////// + + // COPY + + + @Override + public void copy(int sx, int sy, int sw, int sh, + int dx, int dy, int dw, int dh) { + if (primaryGraphics) pgl.enableFBOLayer(); + loadTexture(); + if (filterTexture == null || filterTexture.contextIsOutdated()) { + filterTexture = new Texture(this, texture.width, texture.height, texture.getParameters()); + filterTexture.invertedY(true); + filterImage = wrapTexture(filterTexture); + } + filterTexture.put(texture, sx, height - (sy + sh), sw, height - sy); + copy(filterImage, sx, sy, sw, sh, dx, dy, dw, dh); + } + + + @Override + public void copy(PImage src, + int sx, int sy, int sw, int sh, + int dx, int dy, int dw, int dh) { + boolean needEndDraw = false; + if (!drawing) { + beginDraw(); + needEndDraw = true; + } + + flush(); // make sure that the screen contents are up to date. + + Texture tex = getTexture(src); + boolean invX = tex.invertedX(); + boolean invY = tex.invertedY(); + int scrX0, scrX1; + int scrY0, scrY1; + if (invX) { + scrX0 = dx + dw; + scrX1 = dx; + } else { + scrX0 = dx; + scrX1 = dx + dw; + } + + int texX0 = sx; + int texX1 = sx + sw; + int texY0, texY1; + if (invY) { + scrY0 = height - (dy + dh); + scrY1 = height - dy; + texY0 = tex.height - (sy + sh); + texY1 = tex.height - sy; + } else { + // Because drawTexture uses bottom-to-top orientation of Y axis. + scrY0 = height - dy; + scrY1 = height - (dy + dh); + texY0 = sy; + texY1 = sy + sh; + } + + pgl.drawTexture(tex.glTarget, tex.glName, tex.glWidth, tex.glHeight, + 0, 0, width, height, + texX0, texY0, texX1, texY1, + scrX0, scrY0, scrX1, scrY1); + + + if (needEndDraw) { + endDraw(); + } + } + + + ////////////////////////////////////////////////////////////// + + // BLEND + + + /** + * Allows to set custom blend modes for the entire scene, using openGL. + * Reference article about blending modes: + * http://www.pegtop.net/delphi/articles/blendmodes/ + * DIFFERENCE, HARD_LIGHT, SOFT_LIGHT, OVERLAY, DODGE, BURN modes cannot be + * implemented in fixed-function pipeline because they require + * conditional blending and non-linear blending equations. + */ + @Override + protected void blendModeImpl() { + if (blendMode != lastBlendMode) { + // Flush any geometry that uses a different blending mode. + flush(); + } + + pgl.enable(PGL.BLEND); + + if (blendMode == REPLACE) { + if (blendEqSupported) { + pgl.blendEquation(PGL.FUNC_ADD); + } + pgl.blendFunc(PGL.ONE, PGL.ZERO); + + } else if (blendMode == BLEND) { + if (blendEqSupported) { + pgl.blendEquationSeparate(PGL.FUNC_ADD, + PGL.FUNC_ADD); + } + pgl.blendFuncSeparate(PGL.SRC_ALPHA, PGL.ONE_MINUS_SRC_ALPHA, + PGL.ONE, PGL.ONE); + + } else if (blendMode == ADD) { + if (blendEqSupported) { + pgl.blendEquationSeparate(PGL.FUNC_ADD, + PGL.FUNC_ADD); + } + pgl.blendFuncSeparate(PGL.SRC_ALPHA, PGL.ONE, + PGL.ONE, PGL.ONE); + + } else if (blendMode == SUBTRACT) { + if (blendEqSupported) { + pgl.blendEquationSeparate(PGL.FUNC_REVERSE_SUBTRACT, + PGL.FUNC_ADD); + pgl.blendFuncSeparate(PGL.SRC_ALPHA, PGL.ONE, + PGL.ONE, PGL.ONE); + } else { + PGraphics.showWarning(BLEND_DRIVER_ERROR, "SUBTRACT"); + } + + } else if (blendMode == LIGHTEST) { + if (blendEqSupported) { + pgl.blendEquationSeparate(PGL.FUNC_MAX, + PGL.FUNC_ADD); + pgl.blendFuncSeparate(PGL.ONE, PGL.ONE, + PGL.ONE, PGL.ONE); + } else { + PGraphics.showWarning(BLEND_DRIVER_ERROR, "LIGHTEST"); + } + + } else if (blendMode == DARKEST) { + if (blendEqSupported) { + pgl.blendEquationSeparate(PGL.FUNC_MIN, + PGL.FUNC_ADD); + pgl.blendFuncSeparate(PGL.ONE, PGL.ONE, + PGL.ONE, PGL.ONE); + } else { + PGraphics.showWarning(BLEND_DRIVER_ERROR, "DARKEST"); + } + + } else if (blendMode == EXCLUSION) { + if (blendEqSupported) { + pgl.blendEquationSeparate(PGL.FUNC_ADD, + PGL.FUNC_ADD); + } + pgl.blendFuncSeparate(PGL.ONE_MINUS_DST_COLOR, PGL.ONE_MINUS_SRC_COLOR, + PGL.ONE, PGL.ONE); + + } else if (blendMode == MULTIPLY) { + if (blendEqSupported) { + pgl.blendEquationSeparate(PGL.FUNC_ADD, + PGL.FUNC_ADD); + } + pgl.blendFuncSeparate(PGL.ZERO, PGL.SRC_COLOR, + PGL.ONE, PGL.ONE); + + } else if (blendMode == SCREEN) { + if (blendEqSupported) { + pgl.blendEquationSeparate(PGL.FUNC_ADD, + PGL.FUNC_ADD); + } + pgl.blendFuncSeparate(PGL.ONE_MINUS_DST_COLOR, PGL.ONE, + PGL.ONE, PGL.ONE); + + } else if (blendMode == DIFFERENCE) { + PGraphics.showWarning(BLEND_RENDERER_ERROR, "DIFFERENCE"); + + } else if (blendMode == OVERLAY) { + PGraphics.showWarning(BLEND_RENDERER_ERROR, "OVERLAY"); + + } else if (blendMode == HARD_LIGHT) { + PGraphics.showWarning(BLEND_RENDERER_ERROR, "HARD_LIGHT"); + + } else if (blendMode == SOFT_LIGHT) { + PGraphics.showWarning(BLEND_RENDERER_ERROR, "SOFT_LIGHT"); + + } else if (blendMode == DODGE) { + PGraphics.showWarning(BLEND_RENDERER_ERROR, "DODGE"); + + } else if (blendMode == BURN) { + PGraphics.showWarning(BLEND_RENDERER_ERROR, "BURN"); + } + lastBlendMode = blendMode; + } + + + ////////////////////////////////////////////////////////////// + + // SAVE + + // public void save(String filename) // PImage calls loadPixels() + + + ////////////////////////////////////////////////////////////// + + // TEXTURE UTILS + + + /** + * Not an approved function, this will change or be removed in the future. + * This utility method returns the texture associated to the renderer's. + * drawing surface, making sure is updated to reflect the current contents + * off the screen (or offscreen drawing surface). + */ + public Texture getTexture() { + return getTexture(true); + } + + + /** + * Not an approved function either, don't use it. + */ + public Texture getTexture(boolean load) { + if (load) loadTexture(); + return texture; + } + + + /** + * Not an approved function, this will change or be removed in the future. + * This utility method returns the texture associated to the image. + * creating and/or updating it if needed. + * + * @param img the image to have a texture metadata associated to it + */ + public Texture getTexture(PImage img) { + Texture tex = (Texture)initCache(img); + if (tex == null) return null; + + if (img.isModified()) { + if (img.pixelWidth != tex.width || img.pixelHeight != tex.height) { + tex.init(img.pixelWidth, img.pixelHeight); + } + updateTexture(img, tex); + } + + if (tex.hasBuffers()) { + tex.bufferUpdate(); + } + + checkTexture(tex); + + return tex; + } + + + /** + * Not an approved function, test its use in libraries to grab the FB objects + * for offscreen PGraphics. + */ + public FrameBuffer getFrameBuffer() { + return getFrameBuffer(false); + } + + + public FrameBuffer getFrameBuffer(boolean multi) { + if (multi) { + return multisampleFramebuffer; + } else { + return offscreenFramebuffer; + } + } + + + protected Object initCache(PImage img) { + if (!checkGLThread()) { + return null; + } + + Texture tex = (Texture)getCache(img); + if (tex == null || tex.contextIsOutdated()) { + tex = addTexture(img); + if (tex != null) { + img.loadPixels(); + tex.set(img.pixels, img.format); + img.setModified(); + } + } + return tex; + } + + + protected void bindFrontTexture() { + if (primaryGraphics) { + pgl.bindFrontTexture(); + } else { + if (ptexture == null) { + createPTexture(); + } + ptexture.bind(); + } + } + + + protected void unbindFrontTexture() { + if (primaryGraphics) { + pgl.unbindFrontTexture(); + } else { + ptexture.unbind(); + } + } + + + /** + * This utility method creates a texture for the provided image, and adds it + * to the metadata cache of the image. + * @param img the image to have a texture metadata associated to it + */ + protected Texture addTexture(PImage img) { + Texture.Parameters params = + new Texture.Parameters(ARGB, textureSampling, + getHint(ENABLE_TEXTURE_MIPMAPS), textureWrap); + return addTexture(img, params); + } + + + protected Texture addTexture(PImage img, Texture.Parameters params) { + if (img.width == 0 || img.height == 0) { + // Cannot add textures of size 0 + return null; + } + if (img.parent == null) { + img.parent = parent; + } + Texture tex = new Texture(this, img.pixelWidth, img.pixelHeight, params); + setCache(img, tex); + return tex; + } + + + protected void checkTexture(Texture tex) { + if (!tex.colorBuffer() && + (tex.usingMipmaps == hints[DISABLE_TEXTURE_MIPMAPS] || + tex.currentSampling() != textureSampling)) { + if (hints[DISABLE_TEXTURE_MIPMAPS]) { + tex.usingMipmaps(false, textureSampling); + } else { + tex.usingMipmaps(true, textureSampling); + } + } + + if ((tex.usingRepeat && textureWrap == CLAMP) || + (!tex.usingRepeat && textureWrap == REPEAT)) { + if (textureWrap == CLAMP) { + tex.usingRepeat(false); + } else { + tex.usingRepeat(true); + } + } + } + + + protected PImage wrapTexture(Texture tex) { + // We don't use the PImage(int width, int height, int mode) constructor to + // avoid initializing the pixels array. + PImage img = new PImage(); + img.parent = parent; + img.width = img.pixelWidth = tex.width; + img.height = img.pixelHeight = tex.height; + img.format = ARGB; + setCache(img, tex); + return img; + } + + + protected void updateTexture(PImage img, Texture tex) { + if (tex != null) { + if (img.isModified()) { + int x = img.getModifiedX1(); + int y = img.getModifiedY1(); + int w = img.getModifiedX2() - x; + int h = img.getModifiedY2() - y; + tex.set(img.pixels, x, y, w, h, img.format); + } + } + img.setModified(false); + } + + + protected void deleteSurfaceTextures() { + if (texture != null) { + texture.dispose(); + } + + if (ptexture != null) { + ptexture.dispose(); + } + + if (filterTexture != null) { + filterTexture.dispose(); + } + + + } + + + protected boolean checkGLThread() { + if (pgl.threadIsCurrent()) { + return true; + } else { + PGraphics.showWarning(OPENGL_THREAD_ERROR); + return false; + } + } + + + ////////////////////////////////////////////////////////////// + + // RESIZE + + + @Override + public void resize(int wide, int high) { + PGraphics.showMethodWarning("resize"); + } + + + ////////////////////////////////////////////////////////////// + + // INITIALIZATION ROUTINES + + + protected void initPrimary() { + pgl.initSurface(smooth); + if (texture != null) { + removeCache(this); + texture = null; + ptexture = null; + } + initialized = true; + } + + + protected void beginOnscreenDraw() { + updatePixelSize(); + + pgl.beginRender(); + + if (drawFramebuffer == null) { + drawFramebuffer = new FrameBuffer(this, pixelWidth, pixelHeight, true); + } + drawFramebuffer.setFBO(pgl.getDrawFramebuffer()); + if (readFramebuffer == null) { + readFramebuffer = new FrameBuffer(this, pixelWidth, pixelHeight, true); + } + readFramebuffer.setFBO(pgl.getReadFramebuffer()); + if (currentFramebuffer == null) { + setFramebuffer(drawFramebuffer); + } + + if (pgl.isFBOBacked()) { + texture = pgl.wrapBackTexture(texture); + ptexture = pgl.wrapFrontTexture(ptexture); + } + } + + + protected void endOnscreenDraw() { + pgl.endRender(parent.sketchWindowColor()); + } + + + protected void initOffscreen() { + // Getting the context and capabilities from the main renderer. + loadTextureImpl(textureSampling, false); + + FrameBuffer ofb = offscreenFramebuffer; + FrameBuffer mfb = multisampleFramebuffer; + + // In case of re-initialization (for example, when the smooth level + // is changed), we make sure that all the OpenGL resources associated + // to the surface are released by calling delete(). + if (ofb != null) { + ofb.dispose(); + ofb = null; + } + if (mfb != null) { + mfb.dispose(); + mfb = null; + } + + boolean packed = depthBits == 24 && stencilBits == 8 && + packedDepthStencilSupported; + if (PGraphicsOpenGL.fboMultisampleSupported && 1 < PGL.smoothToSamples(smooth)) { + mfb = new FrameBuffer(this, texture.glWidth, texture.glHeight, PGL.smoothToSamples(smooth), 0, + depthBits, stencilBits, packed, false); + mfb.clear(); + multisampleFramebuffer = mfb; + offscreenMultisample = true; + + // The offscreen framebuffer where the multisampled image is finally drawn + // to. If depth reading is disabled it doesn't need depth and stencil buffers + // since they are part of the multisampled framebuffer. + if (hints[ENABLE_BUFFER_READING]) { + ofb = new FrameBuffer(this, texture.glWidth, texture.glHeight, 1, 1, + depthBits, stencilBits, packed, false); + } else { + ofb = new FrameBuffer(this, texture.glWidth, texture.glHeight, 1, 1, + 0, 0, false, false); + } + } else { + smooth = 0; + ofb = new FrameBuffer(this, texture.glWidth, texture.glHeight, 1, 1, + depthBits, stencilBits, packed, false); + offscreenMultisample = false; + } + ofb.setColorBuffer(texture); + ofb.clear(); + offscreenFramebuffer = ofb; + + initialized = true; + } + + + protected void beginOffscreenDraw() { + if (!initialized) { + initOffscreen(); + } else { + FrameBuffer ofb = offscreenFramebuffer; + FrameBuffer mfb = multisampleFramebuffer; + boolean outdated = ofb != null && ofb.contextIsOutdated(); + boolean outdatedMulti = mfb != null && mfb.contextIsOutdated(); + if (outdated || outdatedMulti) { + restartPGL(); + initOffscreen(); + } else { + // The back texture of the past frame becomes the front, + // and the front texture becomes the new back texture where the + // new frame is drawn to. + swapOffscreenTextures(); + } + } + + pushFramebuffer(); + if (offscreenMultisample) { + FrameBuffer mfb = multisampleFramebuffer; + if (mfb != null) { + setFramebuffer(mfb); + } + } else { + FrameBuffer ofb = offscreenFramebuffer; + if (ofb != null) { + setFramebuffer(ofb); + } + } + + // Render previous back texture (now is the front) as background + drawPTexture(); + + // Restoring the clipping configuration of the offscreen surface. + if (clip) { + pgl.enable(PGL.SCISSOR_TEST); + pgl.scissor(clipRect[0], clipRect[1], clipRect[2], clipRect[3]); + } else { + pgl.disable(PGL.SCISSOR_TEST); + } + } + + + protected void endOffscreenDraw() { + if (offscreenMultisample) { + FrameBuffer ofb = offscreenFramebuffer; + FrameBuffer mfb = multisampleFramebuffer; + if (ofb != null && mfb != null) { + mfb.copyColor(ofb); + } + } + + popFramebuffer(); + + if (backgroundA == 1) { + // Set alpha channel to opaque in order to match behavior of JAVA2D, not + // on the multisampled FBO because it leads to wrong background color + // on some Macbooks with AMD graphics. + pgl.colorMask(false, false, false, true); + pgl.clearColor(0, 0, 0, backgroundA); + pgl.clear(PGL.COLOR_BUFFER_BIT); + pgl.colorMask(true, true, true, true); + } + + if (texture != null) { + texture.updateTexels(); // Mark all texels in screen texture as modified. + } + + getPrimaryPG().restoreGL(); + } + + + protected void setViewport() { + viewport.put(0, 0); viewport.put(1, 0); + viewport.put(2, width); viewport.put(3, height); + pgl.viewport(viewport.get(0), viewport.get(1), + viewport.get(2), viewport.get(3)); + } + + + @Override + protected void checkSettings() { + super.checkSettings(); + setGLSettings(); + } + + + protected void setGLSettings() { + inGeo.clear(); + tessGeo.clear(); + texCache.clear(); + + // Each frame starts with textures disabled. + super.noTexture(); + + // Making sure that OpenGL is using the last blend mode set by the user. + blendModeImpl(); + + // this is necessary for 3D drawing + if (hints[DISABLE_DEPTH_TEST]) { + pgl.disable(PGL.DEPTH_TEST); + } else { + pgl.enable(PGL.DEPTH_TEST); + } + // use <= since that's what processing.core does + pgl.depthFunc(PGL.LEQUAL); + + if (hints[DISABLE_OPTIMIZED_STROKE]) { + flushMode = FLUSH_CONTINUOUSLY; + } else { + flushMode = FLUSH_WHEN_FULL; + } + + if (primaryGraphics) { +// pgl.getIntegerv(PGL.SAMPLES, intBuffer); +// int temp = intBuffer.get(0); +// if (smooth != temp && 1 < temp && 1 < smooth) { + // TODO check why the samples is higher that initialized smooth level. +// quality = temp; +// } + } + if (OPENGL_RENDERER.equals("VideoCore IV HW")) { + // Broadcom's VC IV driver is unhappy with either of these + // ignore for now + } else if (smooth < 1) { + pgl.disable(PGL.MULTISAMPLE); + } else if (1 <= smooth) { + pgl.enable(PGL.MULTISAMPLE); + } + // work around runtime exceptions in Broadcom's VC IV driver + if (false == OPENGL_RENDERER.equals("VideoCore IV HW")) { + pgl.disable(PGL.POLYGON_SMOOTH); + } + + if (sized) { +// reapplySettings(); + + // To avoid having garbage in the screen after a resize, + // in the case background is not called in draw(). + if (primaryGraphics) { + background(backgroundColor); + } else { + // offscreen surfaces are transparent by default. + background(0x00 << 24 | (backgroundColor & 0xFFFFFF)); + } + + // Sets the default projection and camera (initializes modelview). + // If the user has setup up their own projection, they'll need + // to fix it after resize anyway. This helps the people who haven't + // set up their own projection. + defaultPerspective(); + defaultCamera(); + + // clear the flag + sized = false; + } else { + // Eliminating any user's transformations by going back to the + // original camera setup. + modelview.set(camera); + modelviewInv.set(cameraInv); + updateProjmodelview(); + } + + if (is3D()) { + noLights(); + lightFalloff(1, 0, 0); + lightSpecular(0, 0, 0); + } + + // Vertices should be specified by user in CW order (left-handed) + // That is CCW order (right-handed). Vertex shader inverts + // Y-axis and outputs vertices in CW order (right-handed). + // Culling occurs after the vertex shader, so FRONT FACE + // has to be set to CW (right-handed) for OpenGL to correctly + // recognize FRONT and BACK faces. + pgl.frontFace(PGL.CW); + pgl.disable(PGL.CULL_FACE); + + // Processing uses only one texture unit. + pgl.activeTexture(PGL.TEXTURE0); + + // The current normal vector is set to be parallel to the Z axis. + normalX = normalY = 0; + normalZ = 1; + + pgl.clearDepthStencil(); + + if (hints[DISABLE_DEPTH_MASK]) { + pgl.depthMask(false); + } else { + pgl.depthMask(true); + } + + pixelsOp = OP_NONE; + + modified = false; + loaded = false; + } + + + protected void getGLParameters() { + OPENGL_VENDOR = pgl.getString(PGL.VENDOR); + OPENGL_RENDERER = pgl.getString(PGL.RENDERER); + OPENGL_VERSION = pgl.getString(PGL.VERSION); + OPENGL_EXTENSIONS = pgl.getString(PGL.EXTENSIONS); + GLSL_VERSION = pgl.getString(PGL.SHADING_LANGUAGE_VERSION); + + npotTexSupported = pgl.hasNpotTexSupport(); + autoMipmapGenSupported = pgl.hasAutoMipmapGenSupport(); + fboMultisampleSupported = pgl.hasFboMultisampleSupport(); + packedDepthStencilSupported = pgl.hasPackedDepthStencilSupport(); + anisoSamplingSupported = pgl.hasAnisoSamplingSupport(); + readBufferSupported = pgl.hasReadBuffer(); + drawBufferSupported = pgl.hasDrawBuffer(); + + try { + pgl.blendEquation(PGL.FUNC_ADD); + blendEqSupported = true; + } catch (Exception e) { + blendEqSupported = false; + } + + depthBits = pgl.getDepthBits(); + stencilBits = pgl.getStencilBits(); + + pgl.getIntegerv(PGL.MAX_TEXTURE_SIZE, intBuffer); + maxTextureSize = intBuffer.get(0); + + // work around runtime exceptions in Broadcom's VC IV driver + if (false == OPENGL_RENDERER.equals("VideoCore IV HW")) { + pgl.getIntegerv(PGL.MAX_SAMPLES, intBuffer); + maxSamples = intBuffer.get(0); + } + + if (anisoSamplingSupported) { + pgl.getFloatv(PGL.MAX_TEXTURE_MAX_ANISOTROPY, floatBuffer); + maxAnisoAmount = floatBuffer.get(0); + } + + // overwrite the default shaders with vendor specific versions + // if needed + if (OPENGL_RENDERER.equals("VideoCore IV HW") || // Broadcom's binary driver for Raspberry Pi + OPENGL_RENDERER.contains("VC4")) { // Mesa driver for same hardware + defLightShaderVertURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/LightVert-vc4.glsl"); + defTexlightShaderVertURL = + PGraphicsOpenGL.class.getResource("/processing/opengl/shaders/TexLightVert-vc4.glsl"); + } + + glParamsRead = true; + } + + + ////////////////////////////////////////////////////////////// + + // SHADER HANDLING + + + @Override + public PShader loadShader(String fragFilename) { + if (fragFilename == null || fragFilename.equals("")) { + PGraphics.showWarning(MISSING_FRAGMENT_SHADER); + return null; + } + + int type = PShader.getShaderType(parent.loadStrings(fragFilename), + PShader.POLY); + PShader shader = new PShader(parent); + shader.setType(type); + shader.setFragmentShader(fragFilename); + if (type == PShader.POINT) { + String[] vertSource = pgl.loadVertexShader(defPointShaderVertURL); + shader.setVertexShader(vertSource); + } else if (type == PShader.LINE) { + String[] vertSource = pgl.loadVertexShader(defLineShaderVertURL); + shader.setVertexShader(vertSource); + } else if (type == PShader.TEXLIGHT) { + String[] vertSource = pgl.loadVertexShader(defTexlightShaderVertURL); + shader.setVertexShader(vertSource); + } else if (type == PShader.LIGHT) { + String[] vertSource = pgl.loadVertexShader(defLightShaderVertURL); + shader.setVertexShader(vertSource); + } else if (type == PShader.TEXTURE) { + String[] vertSource = pgl.loadVertexShader(defTextureShaderVertURL); + shader.setVertexShader(vertSource); + } else if (type == PShader.COLOR) { + String[] vertSource = pgl.loadVertexShader(defColorShaderVertURL); + shader.setVertexShader(vertSource); + } else { + String[] vertSource = pgl.loadVertexShader(defTextureShaderVertURL); + shader.setVertexShader(vertSource); + } + return shader; + } + + + @Override + public PShader loadShader(String fragFilename, String vertFilename) { + PShader shader = null; + if (fragFilename == null || fragFilename.equals("")) { + PGraphics.showWarning(MISSING_FRAGMENT_SHADER); + } else if (vertFilename == null || vertFilename.equals("")) { + PGraphics.showWarning(MISSING_VERTEX_SHADER); + } else { + shader = new PShader(parent, vertFilename, fragFilename); + } + return shader; + } + + + @Override + public void shader(PShader shader) { + flush(); // Flushing geometry drawn with a different shader. + + if (shader != null) shader.init(); + if (shader.isPolyShader()) polyShader = shader; + else if (shader.isLineShader()) lineShader = shader; + else if (shader.isPointShader()) pointShader = shader; + else PGraphics.showWarning(UNKNOWN_SHADER_KIND_ERROR); + } + + + @Override + public void shader(PShader shader, int kind) { + flush(); // Flushing geometry drawn with a different shader. + + if (shader != null) shader.init(); + if (kind == TRIANGLES) polyShader = shader; + else if (kind == LINES) lineShader = shader; + else if (kind == POINTS) pointShader = shader; + else PGraphics.showWarning(UNKNOWN_SHADER_KIND_ERROR); + } + + + @Override + public void resetShader() { + resetShader(TRIANGLES); + } + + + @Override + public void resetShader(int kind) { + flush(); // Flushing geometry drawn with a different shader. + + if (kind == TRIANGLES || kind == QUADS || kind == POLYGON) { + polyShader = null; + } else if (kind == LINES) { + lineShader = null; + } else if (kind == POINTS) { + pointShader = null; + } else { + PGraphics.showWarning(UNKNOWN_SHADER_KIND_ERROR); + } + } + + + protected PShader getPolyShader(boolean lit, boolean tex) { + PShader shader; + PGraphicsOpenGL ppg = getPrimaryPG(); + boolean useDefault = polyShader == null; + if (polyShader != null) { + polyShader.setRenderer(this); + polyShader.loadAttributes(); + polyShader.loadUniforms(); + } + if (lit) { + if (tex) { + if (useDefault || !polyShader.checkPolyType(PShader.TEXLIGHT)) { + if (ppg.defTexlightShader == null) { + String[] vertSource = pgl.loadVertexShader(defTexlightShaderVertURL); + String[] fragSource = pgl.loadFragmentShader(defTexlightShaderFragURL); + ppg.defTexlightShader = new PShader(parent, vertSource, fragSource); + } + shader = ppg.defTexlightShader; + } else { + shader = polyShader; + } + } else { + if (useDefault || !polyShader.checkPolyType(PShader.LIGHT)) { + if (ppg.defLightShader == null) { + String[] vertSource = pgl.loadVertexShader(defLightShaderVertURL); + String[] fragSource = pgl.loadFragmentShader(defLightShaderFragURL); + ppg.defLightShader = new PShader(parent, vertSource, fragSource); + } + shader = ppg.defLightShader; + } else { + shader = polyShader; + } + } + } else { + if (polyShader != null && polyShader.accessLightAttribs()) { + PGraphics.showWarning(SHADER_NEED_LIGHT_ATTRIBS); + useDefault = true; + } + + if (tex) { + if (useDefault || !polyShader.checkPolyType(PShader.TEXTURE)) { + if (ppg.defTextureShader == null) { + String[] vertSource = pgl.loadVertexShader(defTextureShaderVertURL); + String[] fragSource = pgl.loadFragmentShader(defTextureShaderFragURL); + ppg.defTextureShader = new PShader(parent, vertSource, fragSource); + } + shader = ppg.defTextureShader; + } else { + shader = polyShader; + } + } else { + if (useDefault || !polyShader.checkPolyType(PShader.COLOR)) { + if (ppg.defColorShader == null) { + String[] vertSource = pgl.loadVertexShader(defColorShaderVertURL); + String[] fragSource = pgl.loadFragmentShader(defColorShaderFragURL); + ppg.defColorShader = new PShader(parent, vertSource, fragSource); + } + shader = ppg.defColorShader; + } else { + shader = polyShader; + } + } + } + if (shader != polyShader) { + shader.setRenderer(this); + shader.loadAttributes(); + shader.loadUniforms(); + } + return shader; + } + + + protected PShader getLineShader() { + PShader shader; + PGraphicsOpenGL ppg = getPrimaryPG(); + if (lineShader == null) { + if (ppg.defLineShader == null) { + String[] vertSource = pgl.loadVertexShader(defLineShaderVertURL); + String[] fragSource = pgl.loadFragmentShader(defLineShaderFragURL); + ppg.defLineShader = new PShader(parent, vertSource, fragSource); + } + shader = ppg.defLineShader; + } else { + shader = lineShader; + } + shader.setRenderer(this); + shader.loadAttributes(); + shader.loadUniforms(); + return shader; + } + + + protected PShader getPointShader() { + PShader shader; + PGraphicsOpenGL ppg = getPrimaryPG(); + if (pointShader == null) { + if (ppg.defPointShader == null) { + String[] vertSource = pgl.loadVertexShader(defPointShaderVertURL); + String[] fragSource = pgl.loadFragmentShader(defPointShaderFragURL); + ppg.defPointShader = new PShader(parent, vertSource, fragSource); + } + shader = ppg.defPointShader; + } else { + shader = pointShader; + } + shader.setRenderer(this); + shader.loadAttributes(); + shader.loadUniforms(); + return shader; + } + + + ////////////////////////////////////////////////////////////// + + // Utils + + static protected int expandArraySize(int currSize, int newMinSize) { + int newSize = currSize; + while (newSize < newMinSize) { + newSize <<= 1; + } + return newSize; + } + + ////////////////////////////////////////////////////////////// + + // Generic vertex attributes. + + + static protected AttributeMap newAttributeMap() { + return new AttributeMap(); + } + + + static protected class AttributeMap extends HashMap { + public ArrayList names = new ArrayList<>(); + public int numComp = 0; // number of components for a single vertex + + @Override + public VertexAttribute put(String key, VertexAttribute value) { + VertexAttribute prev = super.put(key, value); + names.add(key); + if (value.kind == VertexAttribute.COLOR) numComp += 4; + else numComp += value.size; + return prev; + } + + public VertexAttribute get(int i) { + return super.get(names.get(i)); + } + } + + + static protected class VertexAttribute { + static final int POSITION = 0; + static final int NORMAL = 1; + static final int COLOR = 2; + static final int OTHER = 3; + + PGraphicsOpenGL pg; + String name; + int kind; // POSITION, NORMAL, COLOR, OTHER + int type; // GL_INT, GL_FLOAT, GL_BOOL + int size; // number of elements (1, 2, 3, or 4) + int tessSize; + int elementSize; + VertexBuffer buf; + int glLoc; + + float[] fvalues; + int[] ivalues; + byte[] bvalues; + + // For use in PShape + boolean modified; + int firstModified; + int lastModified; + boolean active; + + VertexAttribute(PGraphicsOpenGL pg, String name, int kind, int type, int size) { + this.pg = pg; + this.name = name; + this.kind = kind; + this.type = type; + this.size = size; + + if (kind == POSITION) { + tessSize = 4; // for w + } else { + tessSize = size; + } + + if (type == PGL.FLOAT) { + elementSize = PGL.SIZEOF_FLOAT; + fvalues = new float[size]; + } else if (type == PGL.INT) { + elementSize = PGL.SIZEOF_INT; + ivalues = new int[size]; + } else if (type == PGL.BOOL) { + elementSize = PGL.SIZEOF_INT; + bvalues = new byte[size]; + } + + buf = null; + glLoc = -1; + + modified = false; + firstModified = PConstants.MAX_INT; + lastModified = PConstants.MIN_INT; + + active = true; + } + + public boolean diff(VertexAttribute attr) { + return !name.equals(attr.name) || + kind != attr.kind || + type != attr.type || + size != attr.size || + tessSize != attr.tessSize || + elementSize != attr.elementSize; + } + + boolean isPosition() { + return kind == POSITION; + } + + boolean isNormal() { + return kind == NORMAL; + } + + boolean isColor() { + return kind == COLOR; + } + + boolean isOther() { + return kind == OTHER; + } + + boolean isFloat() { + return type == PGL.FLOAT; + } + + boolean isInt() { + return type == PGL.INT; + } + + boolean isBool() { + return type == PGL.BOOL; + } + + boolean bufferCreated() { + return buf != null && 0 < buf.glId; + } + + void createBuffer(PGL pgl) { + buf = new VertexBuffer(pg, PGL.ARRAY_BUFFER, size, elementSize, false); + } + + void deleteBuffer(PGL pgl) { + if (buf.glId != 0) { + intBuffer.put(0, buf.glId); + if (pgl.threadIsCurrent()) pgl.deleteBuffers(1, intBuffer); + } + } + + void bind(PGL pgl) { + pgl.enableVertexAttribArray(glLoc); + } + + void unbind(PGL pgl) { + pgl.disableVertexAttribArray(glLoc); + } + + boolean active(PShader shader) { + if (active) { + if (glLoc == -1) { + glLoc = shader.getAttributeLoc(name); + if (glLoc == -1) active = false; + } + } + return active; + } + + int sizeInBytes(int length) { + return length * tessSize * elementSize; + } + + void set(float x, float y, float z) { + fvalues[0] = x; + fvalues[1] = y; + fvalues[2] = z; + } + + void set(int c) { + ivalues[0] = c; + } + + void set(float[] values) { + PApplet.arrayCopy(values, 0, fvalues, 0, size); + } + + void set(int[] values) { + PApplet.arrayCopy(values, 0, ivalues, 0, size); + } + + void set(boolean[] values) { + for (int i = 0; i < values.length; i++) { + bvalues[i] = (byte)(values[i] ? 1 : 0); + } + } + + void add(float[] dstValues, int dstIdx) { + PApplet.arrayCopy(fvalues, 0, dstValues, dstIdx, size); + } + + void add(int[] dstValues, int dstIdx) { + PApplet.arrayCopy(ivalues, 0, dstValues, dstIdx, size); + } + + void add(byte[] dstValues, int dstIdx) { + PApplet.arrayCopy(bvalues, 0, dstValues, dstIdx, size); + } + } + + + ////////////////////////////////////////////////////////////// + + // Input (raw) and Tessellated geometry, tessellator. + + + static protected InGeometry newInGeometry(PGraphicsOpenGL pg, AttributeMap attr, + int mode) { + return new InGeometry(pg, attr, mode); + } + + + static protected TessGeometry newTessGeometry(PGraphicsOpenGL pg, + AttributeMap attr, int mode) { + return new TessGeometry(pg, attr, mode); + } + + + static protected TexCache newTexCache(PGraphicsOpenGL pg) { + return new TexCache(pg); + } + + + // Holds an array of textures and the range of vertex + // indices each texture applies to. + static protected class TexCache { + PGraphicsOpenGL pg; + int size; + PImage[] textures; + int[] firstIndex; + int[] lastIndex; + int[] firstCache; + int[] lastCache; + boolean hasTextures; + + TexCache(PGraphicsOpenGL pg) { + this.pg = pg; + allocate(); + } + + void allocate() { + textures = new PImage[PGL.DEFAULT_IN_TEXTURES]; + firstIndex = new int[PGL.DEFAULT_IN_TEXTURES]; + lastIndex = new int[PGL.DEFAULT_IN_TEXTURES]; + firstCache = new int[PGL.DEFAULT_IN_TEXTURES]; + lastCache = new int[PGL.DEFAULT_IN_TEXTURES]; + size = 0; + hasTextures = false; + } + + void clear() { + java.util.Arrays.fill(textures, 0, size, null); + size = 0; + hasTextures = false; + } + + boolean containsTexture(PImage img) { + for (int i = 0; i < size; i++) { + if (textures[i] == img) return true; + } + return false; + } + + PImage getTextureImage(int i) { + return textures[i]; + } + + Texture getTexture(int i) { + PImage img = textures[i]; + Texture tex = null; + + if (img != null) { + tex = pg.getTexture(img); + } + + return tex; + } + + void addTexture(PImage img, int firsti, int firstb, int lasti, int lastb) { + arrayCheck(); + + textures[size] = img; + firstIndex[size] = firsti; + lastIndex[size] = lasti; + firstCache[size] = firstb; + lastCache[size] = lastb; + + // At least one non-null texture since last reset. + hasTextures |= img != null; + + size++; + } + + void setLastIndex(int lasti, int lastb) { + lastIndex[size - 1] = lasti; + lastCache[size - 1] = lastb; + } + + void arrayCheck() { + if (size == textures.length) { + int newSize = size << 1; + + expandTextures(newSize); + expandFirstIndex(newSize); + expandLastIndex(newSize); + expandFirstCache(newSize); + expandLastCache(newSize); + } + } + + void expandTextures(int n) { + PImage[] temp = new PImage[n]; + PApplet.arrayCopy(textures, 0, temp, 0, size); + textures = temp; + } + + void expandFirstIndex(int n) { + int[] temp = new int[n]; + PApplet.arrayCopy(firstIndex, 0, temp, 0, size); + firstIndex = temp; + } + + void expandLastIndex(int n) { + int[] temp = new int[n]; + PApplet.arrayCopy(lastIndex, 0, temp, 0, size); + lastIndex = temp; + } + + void expandFirstCache(int n) { + int[] temp = new int[n]; + PApplet.arrayCopy(firstCache, 0, temp, 0, size); + firstCache = temp; + } + + void expandLastCache(int n) { + int[] temp = new int[n]; + PApplet.arrayCopy(lastCache, 0, temp, 0, size); + lastCache = temp; + } + } + + + // Stores the offsets and counts of indices and vertices + // to render a piece of geometry that doesn't fit in a single + // glDrawElements() call. + static protected class IndexCache { + int size; + int[] indexCount; + int[] indexOffset; + int[] vertexCount; + int[] vertexOffset; + int[] counter; + + IndexCache() { + allocate(); + } + + void allocate() { + size = 0; + indexCount = new int[2]; + indexOffset = new int[2]; + vertexCount = new int[2]; + vertexOffset = new int[2]; + counter = null; + } + + void clear() { + size = 0; + } + + int addNew() { + arrayCheck(); + init(size); + size++; + return size - 1; + } + + int addNew(int index) { + arrayCheck(); + indexCount[size] = indexCount[index]; + indexOffset[size] = indexOffset[index]; + vertexCount[size] = vertexCount[index]; + vertexOffset[size] = vertexOffset[index]; + size++; + return size - 1; + } + + int getLast() { + if (size == 0) { + arrayCheck(); + init(0); + size = 1; + } + return size - 1; + } + + void setCounter(int[] counter) { + this.counter = counter; + } + + void incCounts(int index, int icount, int vcount) { + indexCount[index] += icount; + vertexCount[index] += vcount; + if (counter != null) { + counter[0] += icount; + counter[1] += vcount; + } + } + + void init(int n) { + if (0 < n) { + indexOffset[n] = indexOffset[n - 1] + indexCount[n - 1]; + vertexOffset[n] = vertexOffset[n - 1] + vertexCount[n - 1]; + } else { + indexOffset[n] = 0; + vertexOffset[n] = 0; + } + indexCount[n] = 0; + vertexCount[n] = 0; + } + + void arrayCheck() { + if (size == indexCount.length) { + int newSize = size << 1; + + expandIndexCount(newSize); + expandIndexOffset(newSize); + expandVertexCount(newSize); + expandVertexOffset(newSize); + } + } + + void expandIndexCount(int n) { + int[] temp = new int[n]; + PApplet.arrayCopy(indexCount, 0, temp, 0, size); + indexCount = temp; + } + + void expandIndexOffset(int n) { + int[] temp = new int[n]; + PApplet.arrayCopy(indexOffset, 0, temp, 0, size); + indexOffset = temp; + } + + void expandVertexCount(int n) { + int[] temp = new int[n]; + PApplet.arrayCopy(vertexCount, 0, temp, 0, size); + vertexCount = temp; + } + + void expandVertexOffset(int n) { + int[] temp = new int[n]; + PApplet.arrayCopy(vertexOffset, 0, temp, 0, size); + vertexOffset = temp; + } + } + + + // Holds the input vertices: xyz coordinates, fill/tint color, + // normal, texture coordinates and stroke color and weight. + static protected class InGeometry { + PGraphicsOpenGL pg; + int renderMode; + AttributeMap attribs; + + int vertexCount; + int codeCount; + int edgeCount; + + float[] vertices; + int[] colors; + float[] normals; + float[] texcoords; + int[] strokeColors; + float[] strokeWeights; + + // vertex codes + int[] codes; + + // Stroke edges + int[][] edges; + + // Material properties + int[] ambient; + int[] specular; + int[] emissive; + float[] shininess; + + // Generic attributes + HashMap fattribs; + HashMap iattribs; + HashMap battribs; + + // Internally used by the addVertex() methods. + int fillColor; + int strokeColor; + float strokeWeight; + int ambientColor; + int specularColor; + int emissiveColor; + float shininessFactor; + float normalX, normalY, normalZ; + + InGeometry(PGraphicsOpenGL pg, AttributeMap attr, int mode) { + this.pg = pg; + this.attribs = attr; + renderMode = mode; + allocate(); + } + + // ----------------------------------------------------------------- + // + // Allocate/dispose + + void clear() { + vertexCount = 0; + codeCount = 0; + edgeCount = 0; + } + + void clearEdges() { + edgeCount = 0; + } + + void allocate() { + vertices = new float[3 * PGL.DEFAULT_IN_VERTICES]; + colors = new int[PGL.DEFAULT_IN_VERTICES]; + normals = new float[3 * PGL.DEFAULT_IN_VERTICES]; + texcoords = new float[2 * PGL.DEFAULT_IN_VERTICES]; + strokeColors = new int[PGL.DEFAULT_IN_VERTICES]; + strokeWeights = new float[PGL.DEFAULT_IN_VERTICES]; + ambient = new int[PGL.DEFAULT_IN_VERTICES]; + specular = new int[PGL.DEFAULT_IN_VERTICES]; + emissive = new int[PGL.DEFAULT_IN_VERTICES]; + shininess = new float[PGL.DEFAULT_IN_VERTICES]; + edges = new int[PGL.DEFAULT_IN_EDGES][3]; + + fattribs = new HashMap<>(); + iattribs = new HashMap<>(); + battribs = new HashMap<>(); + + clear(); + } + + void initAttrib(VertexAttribute attrib) { + if (attrib.type == PGL.FLOAT) { + float[] temp = new float[attrib.size * PGL.DEFAULT_IN_VERTICES]; + fattribs.put(attrib.name, temp); + } else if (attrib.type == PGL.INT) { + int[] temp = new int[attrib.size * PGL.DEFAULT_IN_VERTICES]; + iattribs.put(attrib.name, temp); + } else if (attrib.type == PGL.BOOL) { + byte[] temp = new byte[attrib.size * PGL.DEFAULT_IN_VERTICES]; + battribs.put(attrib.name, temp); + } + } + + void vertexCheck() { + if (vertexCount == vertices.length / 3) { + int newSize = vertexCount << 1; + + expandVertices(newSize); + expandColors(newSize); + expandNormals(newSize); + expandTexCoords(newSize); + expandStrokeColors(newSize); + expandStrokeWeights(newSize); + expandAmbient(newSize); + expandSpecular(newSize); + expandEmissive(newSize); + expandShininess(newSize); + expandAttribs(newSize); + } + } + + void codeCheck() { + if (codeCount == codes.length) { + int newLen = codeCount << 1; + + expandCodes(newLen); + } + } + + void edgeCheck() { + if (edgeCount == edges.length) { + int newLen = edgeCount << 1; + + expandEdges(newLen); + } + } + + // ----------------------------------------------------------------- + // + // Query + + float getVertexX(int idx) { + return vertices[3 * idx + 0]; + } + + float getVertexY(int idx) { + return vertices[3 * idx + 1]; + } + + float getVertexZ(int idx) { + return vertices[3 * idx + 2]; + } + + float getLastVertexX() { + return vertices[3 * (vertexCount - 1) + 0]; + } + + float getLastVertexY() { + return vertices[3 * (vertexCount - 1) + 1]; + } + + float getLastVertexZ() { + return vertices[3 * (vertexCount - 1) + 2]; + } + + int getNumEdgeClosures() { + int count = 0; + for (int i = 0; i < edgeCount; i++) { + if (edges[i][2] == EDGE_CLOSE) count++; + } + return count; + } + + int getNumEdgeVertices(boolean bevel) { + int segVert = edgeCount; + int bevVert = 0; + if (bevel) { + for (int i = 0; i < edgeCount; i++) { + int[] edge = edges[i]; + if (edge[2] == EDGE_MIDDLE || edge[2] == EDGE_START) bevVert += 3; + if (edge[2] == EDGE_CLOSE) { + bevVert += 5; + segVert--; + } + } + } else { + segVert -= getNumEdgeClosures(); + } + return 4 * segVert + bevVert; + } + + int getNumEdgeIndices(boolean bevel) { + int segInd = edgeCount; + int bevInd = 0; + if (bevel) { + for (int i = 0; i < edgeCount; i++) { + int[] edge = edges[i]; + if (edge[2] == EDGE_MIDDLE || edge[2] == EDGE_START) bevInd++; + if (edge[2] == EDGE_CLOSE) { + bevInd++; + segInd--; + } + } + } else { + segInd -= getNumEdgeClosures(); + } + return 6 * (segInd + bevInd); + } + + void getVertexMin(PVector v) { + int index; + for (int i = 0; i < vertexCount; i++) { + index = 4 * i; + v.x = PApplet.min(v.x, vertices[index++]); + v.y = PApplet.min(v.y, vertices[index++]); + v.z = PApplet.min(v.z, vertices[index ]); + } + } + + void getVertexMax(PVector v) { + int index; + for (int i = 0; i < vertexCount; i++) { + index = 4 * i; + v.x = PApplet.max(v.x, vertices[index++]); + v.y = PApplet.max(v.y, vertices[index++]); + v.z = PApplet.max(v.z, vertices[index ]); + } + } + + int getVertexSum(PVector v) { + int index; + for (int i = 0; i < vertexCount; i++) { + index = 4 * i; + v.x += vertices[index++]; + v.y += vertices[index++]; + v.z += vertices[index ]; + } + return vertexCount; + } + + double[] getAttribVector(int idx) { + double[] vector = new double[attribs.numComp]; + int vidx = 0; + for (int i = 0; i < attribs.size(); i++) { + VertexAttribute attrib = attribs.get(i); + String name = attrib.name; + int aidx = attrib.size * idx; + if (attrib.isColor()) { + int[] iarray = iattribs.get(name); + int col = iarray[aidx]; + vector[vidx++] = (col >> 24) & 0xFF; + vector[vidx++] = (col >> 16) & 0xFF; + vector[vidx++] = (col >> 8) & 0xFF; + vector[vidx++] = (col >> 0) & 0xFF; + } else { + if (attrib.isFloat()) { + float[] farray = fattribs.get(name); + for (int n = 0; n < attrib.size; n++) { + vector[vidx++] = farray[aidx++]; + } + } else if (attrib.isInt()) { + int[] iarray = iattribs.get(name); + for (int n = 0; n < attrib.size; n++) { + vector[vidx++] = iarray[aidx++]; + } + } else if (attrib.isBool()) { + byte[] barray = battribs.get(name); + for (int n = 0; n < attrib.size; n++) { + vector[vidx++] = barray[aidx++]; + } + } + } + } + return vector; + } + + // ----------------------------------------------------------------- + // + // Expand arrays + + void expandVertices(int n) { + float temp[] = new float[3 * n]; + PApplet.arrayCopy(vertices, 0, temp, 0, 3 * vertexCount); + vertices = temp; + } + + void expandColors(int n) { + int temp[] = new int[n]; + PApplet.arrayCopy(colors, 0, temp, 0, vertexCount); + colors = temp; + } + + void expandNormals(int n) { + float temp[] = new float[3 * n]; + PApplet.arrayCopy(normals, 0, temp, 0, 3 * vertexCount); + normals = temp; + } + + void expandTexCoords(int n) { + float temp[] = new float[2 * n]; + PApplet.arrayCopy(texcoords, 0, temp, 0, 2 * vertexCount); + texcoords = temp; + } + + void expandStrokeColors(int n) { + int temp[] = new int[n]; + PApplet.arrayCopy(strokeColors, 0, temp, 0, vertexCount); + strokeColors = temp; + } + + void expandStrokeWeights(int n) { + float temp[] = new float[n]; + PApplet.arrayCopy(strokeWeights, 0, temp, 0, vertexCount); + strokeWeights = temp; + } + + void expandAmbient(int n) { + int temp[] = new int[n]; + PApplet.arrayCopy(ambient, 0, temp, 0, vertexCount); + ambient = temp; + } + + void expandSpecular(int n) { + int temp[] = new int[n]; + PApplet.arrayCopy(specular, 0, temp, 0, vertexCount); + specular = temp; + } + + void expandEmissive(int n) { + int temp[] = new int[n]; + PApplet.arrayCopy(emissive, 0, temp, 0, vertexCount); + emissive = temp; + } + + void expandShininess(int n) { + float temp[] = new float[n]; + PApplet.arrayCopy(shininess, 0, temp, 0, vertexCount); + shininess = temp; + } + + void expandAttribs(int n) { + for (String name: attribs.keySet()) { + VertexAttribute attrib = attribs.get(name); + if (attrib.type == PGL.FLOAT) { + expandFloatAttrib(attrib, n); + } else if (attrib.type == PGL.INT) { + expandIntAttrib(attrib, n); + } else if (attrib.type == PGL.BOOL) { + expandBoolAttrib(attrib, n); + } + } + } + + void expandFloatAttrib(VertexAttribute attrib, int n) { + float[] values = fattribs.get(attrib.name); + float temp[] = new float[attrib.size * n]; + PApplet.arrayCopy(values, 0, temp, 0, attrib.size * vertexCount); + fattribs.put(attrib.name, temp); + } + + void expandIntAttrib(VertexAttribute attrib, int n) { + int[] values = iattribs.get(attrib.name); + int temp[] = new int[attrib.size * n]; + PApplet.arrayCopy(values, 0, temp, 0, attrib.size * vertexCount); + iattribs.put(attrib.name, temp); + } + + void expandBoolAttrib(VertexAttribute attrib, int n) { + byte[] values = battribs.get(attrib.name); + byte temp[] = new byte[attrib.size * n]; + PApplet.arrayCopy(values, 0, temp, 0, attrib.size * vertexCount); + battribs.put(attrib.name, temp); + } + + void expandCodes(int n) { + int temp[] = new int[n]; + PApplet.arrayCopy(codes, 0, temp, 0, codeCount); + codes = temp; + } + + void expandEdges(int n) { + int temp[][] = new int[n][3]; + PApplet.arrayCopy(edges, 0, temp, 0, edgeCount); + edges = temp; + } + + // ----------------------------------------------------------------- + // + // Trim arrays + + void trim() { + if (0 < vertexCount && vertexCount < vertices.length / 3) { + trimVertices(); + trimColors(); + trimNormals(); + trimTexCoords(); + trimStrokeColors(); + trimStrokeWeights(); + trimAmbient(); + trimSpecular(); + trimEmissive(); + trimShininess(); + trimAttribs(); + } + + if (0 < codeCount && codeCount < codes.length) { + trimCodes(); + } + + if (0 < edgeCount && edgeCount < edges.length) { + trimEdges(); + } + } + + void trimVertices() { + float temp[] = new float[3 * vertexCount]; + PApplet.arrayCopy(vertices, 0, temp, 0, 3 * vertexCount); + vertices = temp; + } + + void trimColors() { + int temp[] = new int[vertexCount]; + PApplet.arrayCopy(colors, 0, temp, 0, vertexCount); + colors = temp; + } + + void trimNormals() { + float temp[] = new float[3 * vertexCount]; + PApplet.arrayCopy(normals, 0, temp, 0, 3 * vertexCount); + normals = temp; + } + + void trimTexCoords() { + float temp[] = new float[2 * vertexCount]; + PApplet.arrayCopy(texcoords, 0, temp, 0, 2 * vertexCount); + texcoords = temp; + } + + void trimStrokeColors() { + int temp[] = new int[vertexCount]; + PApplet.arrayCopy(strokeColors, 0, temp, 0, vertexCount); + strokeColors = temp; + } + + void trimStrokeWeights() { + float temp[] = new float[vertexCount]; + PApplet.arrayCopy(strokeWeights, 0, temp, 0, vertexCount); + strokeWeights = temp; + } + + void trimAmbient() { + int temp[] = new int[vertexCount]; + PApplet.arrayCopy(ambient, 0, temp, 0, vertexCount); + ambient = temp; + } + + void trimSpecular() { + int temp[] = new int[vertexCount]; + PApplet.arrayCopy(specular, 0, temp, 0, vertexCount); + specular = temp; + } + + void trimEmissive() { + int temp[] = new int[vertexCount]; + PApplet.arrayCopy(emissive, 0, temp, 0, vertexCount); + emissive = temp; + } + + void trimShininess() { + float temp[] = new float[vertexCount]; + PApplet.arrayCopy(shininess, 0, temp, 0, vertexCount); + shininess = temp; + } + + void trimCodes() { + int temp[] = new int[codeCount]; + PApplet.arrayCopy(codes, 0, temp, 0, codeCount); + codes = temp; + } + + void trimEdges() { + int temp[][] = new int[edgeCount][3]; + PApplet.arrayCopy(edges, 0, temp, 0, edgeCount); + edges = temp; + } + + void trimAttribs() { + for (String name: attribs.keySet()) { + VertexAttribute attrib = attribs.get(name); + if (attrib.type == PGL.FLOAT) { + trimFloatAttrib(attrib); + } else if (attrib.type == PGL.INT) { + trimIntAttrib(attrib); + } else if (attrib.type == PGL.BOOL) { + trimBoolAttrib(attrib); + } + } + } + + void trimFloatAttrib(VertexAttribute attrib) { + float[] values = fattribs.get(attrib.name); + float temp[] = new float[attrib.size * vertexCount]; + PApplet.arrayCopy(values, 0, temp, 0, attrib.size * vertexCount); + fattribs.put(attrib.name, temp); + } + + void trimIntAttrib(VertexAttribute attrib) { + int[] values = iattribs.get(attrib.name); + int temp[] = new int[attrib.size * vertexCount]; + PApplet.arrayCopy(values, 0, temp, 0, attrib.size * vertexCount); + iattribs.put(attrib.name, temp); + } + + void trimBoolAttrib(VertexAttribute attrib) { + byte[] values = battribs.get(attrib.name); + byte temp[] = new byte[attrib.size * vertexCount]; + PApplet.arrayCopy(values, 0, temp, 0, attrib.size * vertexCount); + battribs.put(attrib.name, temp); + } + + // ----------------------------------------------------------------- + // + // Vertices + + int addVertex(float x, float y, boolean brk) { + return addVertex(x, y, 0, + fillColor, + normalX, normalY, normalZ, + 0, 0, + strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininessFactor, + VERTEX, brk); + } + + int addVertex(float x, float y, + int code, boolean brk) { + return addVertex(x, y, 0, + fillColor, + normalX, normalY, normalZ, + 0, 0, + strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininessFactor, + code, brk); + } + + int addVertex(float x, float y, + float u, float v, + boolean brk) { + return addVertex(x, y, 0, + fillColor, + normalX, normalY, normalZ, + u, v, + strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininessFactor, + VERTEX, brk); + } + + int addVertex(float x, float y, + float u, float v, + int code, boolean brk) { + return addVertex(x, y, 0, + fillColor, + normalX, normalY, normalZ, + u, v, + strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininessFactor, + code, brk); + } + + int addVertex(float x, float y, float z, boolean brk) { + return addVertex(x, y, z, + fillColor, + normalX, normalY, normalZ, + 0, 0, + strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininessFactor, + VERTEX, brk); + } + + int addVertex(float x, float y, float z, int code, boolean brk) { + return addVertex(x, y, z, + fillColor, + normalX, normalY, normalZ, + 0, 0, + strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininessFactor, + code, brk); + } + + int addVertex(float x, float y, float z, + float u, float v, + boolean brk) { + return addVertex(x, y, z, + fillColor, + normalX, normalY, normalZ, + u, v, + strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininessFactor, + VERTEX, brk); + } + + int addVertex(float x, float y, float z, + float u, float v, + int code, boolean brk) { + return addVertex(x, y, z, + fillColor, + normalX, normalY, normalZ, + u, v, + strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininessFactor, + code, brk); + } + + int addVertex(float x, float y, float z, + int fcolor, + float nx, float ny, float nz, + float u, float v, + int scolor, float sweight, + int am, int sp, int em, float shine, + int code, boolean brk) { + vertexCheck(); + int index; + + index = 3 * vertexCount; + vertices[index++] = x; + vertices[index++] = y; + vertices[index ] = z; + + colors[vertexCount] = PGL.javaToNativeARGB(fcolor); + + index = 3 * vertexCount; + normals[index++] = nx; + normals[index++] = ny; + normals[index ] = nz; + + index = 2 * vertexCount; + texcoords[index++] = u; + texcoords[index ] = v; + + strokeColors[vertexCount] = PGL.javaToNativeARGB(scolor); + strokeWeights[vertexCount] = sweight; + + ambient[vertexCount] = PGL.javaToNativeARGB(am); + specular[vertexCount] = PGL.javaToNativeARGB(sp); + emissive[vertexCount] = PGL.javaToNativeARGB(em); + shininess[vertexCount] = shine; + + for (String name: attribs.keySet()) { + VertexAttribute attrib = attribs.get(name); + index = attrib.size * vertexCount; + if (attrib.type == PGL.FLOAT) { + float[] values = fattribs.get(name); + attrib.add(values, index); + } else if (attrib.type == PGL.INT) { + int[] values = iattribs.get(name); + attrib.add(values, index); + } else if (attrib.type == PGL.BOOL) { + byte[] values = battribs.get(name); + attrib.add(values, index); + } + } + + if (brk || (code == VERTEX && codes != null) || + code == BEZIER_VERTEX || + code == QUADRATIC_VERTEX || + code == CURVE_VERTEX) { + if (codes == null) { + codes = new int[PApplet.max(PGL.DEFAULT_IN_VERTICES, vertexCount)]; + Arrays.fill(codes, 0, vertexCount, VERTEX); + codeCount = vertexCount; + } + + if (brk) { + codeCheck(); + codes[codeCount] = BREAK; + codeCount++; + } + + if (code != -1) { + codeCheck(); + codes[codeCount] = code; + codeCount++; + } + } + + vertexCount++; + + return vertexCount - 1; + } + + public void addBezierVertex(float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4, + boolean brk) { + addVertex(x2, y2, z2, BEZIER_VERTEX, brk); + addVertex(x3, y3, z3, -1, false); + addVertex(x4, y4, z4, -1, false); + } + + public void addQuadraticVertex(float cx, float cy, float cz, + float x3, float y3, float z3, + boolean brk) { + addVertex(cx, cy, cz, QUADRATIC_VERTEX, brk); + addVertex(x3, y3, z3, -1, false); + } + + public void addCurveVertex(float x, float y, float z, boolean brk) { + addVertex(x, y, z, CURVE_VERTEX, brk); + } + + // Returns the vertex data in the PGraphics double array format. + float[][] getVertexData() { + float[][] data = new float[vertexCount][VERTEX_FIELD_COUNT]; + for (int i = 0; i < vertexCount; i++) { + float[] vert = data[i]; + + vert[X] = vertices[3 * i + 0]; + vert[Y] = vertices[3 * i + 1]; + vert[Z] = vertices[3 * i + 2]; + + vert[R] = ((colors[i] >> 16) & 0xFF) / 255.0f; + vert[G] = ((colors[i] >> 8) & 0xFF) / 255.0f; + vert[B] = ((colors[i] >> 0) & 0xFF) / 255.0f; + vert[A] = ((colors[i] >> 24) & 0xFF) / 255.0f; + + vert[U] = texcoords[2 * i + 0]; + vert[V] = texcoords[2 * i + 1]; + + vert[NX] = normals[3 * i + 0]; + vert[NY] = normals[3 * i + 1]; + vert[NZ] = normals[3 * i + 2]; + + vert[SR] = ((strokeColors[i] >> 16) & 0xFF) / 255.0f; + vert[SG] = ((strokeColors[i] >> 8) & 0xFF) / 255.0f; + vert[SB] = ((strokeColors[i] >> 0) & 0xFF) / 255.0f; + vert[SA] = ((strokeColors[i] >> 24) & 0xFF) / 255.0f; + + vert[SW] = strokeWeights[i]; + } + + return data; + } + + boolean hasBezierVertex() { + for (int i = 0; i < codeCount; i++) { + if (codes[i] == BEZIER_VERTEX) return true; + } + return false; + } + + boolean hasQuadraticVertex() { + for (int i = 0; i < codeCount; i++) { + if (codes[i] == QUADRATIC_VERTEX) return true; + } + return false; + } + + boolean hasCurveVertex() { + for (int i = 0; i < codeCount; i++) { + if (codes[i] == CURVE_VERTEX) return true; + } + return false; + } + + // ----------------------------------------------------------------- + // + // Edges + + int addEdge(int i, int j, boolean start, boolean end) { + edgeCheck(); + + int[] edge = edges[edgeCount]; + edge[0] = i; + edge[1] = j; + + // Possible values for state: + // 0 = middle edge (not start, not end) + // 1 = start edge (start, not end) + // 2 = end edge (not start, end) + // 3 = isolated edge (start, end) + edge[2] = (start ? 1 : 0) + 2 * (end ? 1 : 0); + + edgeCount++; + + return edgeCount - 1; + } + + int closeEdge(int i, int j) { + edgeCheck(); + + int[] edge = edges[edgeCount]; + edge[0] = i; + edge[1] = j; + edge[2] = EDGE_CLOSE; + + edgeCount++; + + return edgeCount - 1; + } + + void addTrianglesEdges() { + for (int i = 0; i < vertexCount / 3; i++) { + int i0 = 3 * i + 0; + int i1 = 3 * i + 1; + int i2 = 3 * i + 2; + + addEdge(i0, i1, true, false); + addEdge(i1, i2, false, false); + addEdge(i2, i0, false, false); + closeEdge(i2, i0); + } + } + + void addTriangleFanEdges() { + for (int i = 1; i < vertexCount - 1; i++) { + int i0 = 0; + int i1 = i; + int i2 = i + 1; + + addEdge(i0, i1, true, false); + addEdge(i1, i2, false, false); + addEdge(i2, i0, false, false); + closeEdge(i2, i0); + } + } + + void addTriangleStripEdges() { + for (int i = 1; i < vertexCount - 1; i++) { + int i0 = i; + int i1, i2; + if (i % 2 == 0) { + i1 = i - 1; + i2 = i + 1; + } else { + i1 = i + 1; + i2 = i - 1; + } + + addEdge(i0, i1, true, false); + addEdge(i1, i2, false, false); + addEdge(i2, i0, false, false); + closeEdge(i2, i0); + } + } + + void addQuadsEdges() { + for (int i = 0; i < vertexCount / 4; i++) { + int i0 = 4 * i + 0; + int i1 = 4 * i + 1; + int i2 = 4 * i + 2; + int i3 = 4 * i + 3; + + addEdge(i0, i1, true, false); + addEdge(i1, i2, false, false); + addEdge(i2, i3, false, false); + addEdge(i3, i0, false, false); + closeEdge(i3, i0); + } + } + + void addQuadStripEdges() { + for (int qd = 1; qd < vertexCount / 2; qd++) { + int i0 = 2 * (qd - 1); + int i1 = 2 * (qd - 1) + 1; + int i2 = 2 * qd + 1; + int i3 = 2 * qd; + + addEdge(i0, i1, true, false); + addEdge(i1, i2, false, false); + addEdge(i2, i3, false, false); + addEdge(i3, i0, false, false); + closeEdge(i3, i0); + } + } + + // ----------------------------------------------------------------- + // + // Normal calculation + + // Expects vertices in CW (left-handed) order. + void calcTriangleNormal(int i0, int i1, int i2) { + int index; + + index = 3 * i0; + float x0 = vertices[index++]; + float y0 = vertices[index++]; + float z0 = vertices[index ]; + + index = 3 * i1; + float x1 = vertices[index++]; + float y1 = vertices[index++]; + float z1 = vertices[index ]; + + index = 3 * i2; + float x2 = vertices[index++]; + float y2 = vertices[index++]; + float z2 = vertices[index ]; + + float v12x = x2 - x1; + float v12y = y2 - y1; + float v12z = z2 - z1; + + float v10x = x0 - x1; + float v10y = y0 - y1; + float v10z = z0 - z1; + + // The automatic normal calculation in Processing assumes + // that vertices as given in CCW order (right-handed) so: + // n = v12 x v10 + // so that the normal extends from the front face. + float nx = v12y * v10z - v10y * v12z; + float ny = v12z * v10x - v10z * v12x; + float nz = v12x * v10y - v10x * v12y; + float d = PApplet.sqrt(nx * nx + ny * ny + nz * nz); + nx /= d; + ny /= d; + nz /= d; + + index = 3 * i0; + normals[index++] = nx; + normals[index++] = ny; + normals[index ] = nz; + + index = 3 * i1; + normals[index++] = nx; + normals[index++] = ny; + normals[index ] = nz; + + index = 3 * i2; + normals[index++] = nx; + normals[index++] = ny; + normals[index ] = nz; + } + + void calcTrianglesNormals() { + for (int i = 0; i < vertexCount / 3; i++) { + int i0 = 3 * i + 0; + int i1 = 3 * i + 1; + int i2 = 3 * i + 2; + + calcTriangleNormal(i0, i1, i2); + } + } + + void calcTriangleFanNormals() { + for (int i = 1; i < vertexCount - 1; i++) { + int i0 = 0; + int i1 = i; + int i2 = i + 1; + + calcTriangleNormal(i0, i1, i2); + } + } + + void calcTriangleStripNormals() { + for (int i = 1; i < vertexCount - 1; i++) { + int i1 = i; + int i0, i2; + // Vertices are specified by user as: + // 1-3 ... + // |\|\ ... + // 0-2-4 ... + if (i % 2 == 1) { + // The odd triangles (1, 3, 5...) should be CW (left-handed) + i0 = i - 1; + i2 = i + 1; + } else { + // The even triangles (2, 4, 6...) should be CCW (left-handed) + i0 = i + 1; + i2 = i - 1; + } + calcTriangleNormal(i0, i1, i2); + } + } + + void calcQuadsNormals() { + for (int i = 0; i < vertexCount / 4; i++) { + int i0 = 4 * i + 0; + int i1 = 4 * i + 1; + int i2 = 4 * i + 2; + int i3 = 4 * i + 3; + + calcTriangleNormal(i0, i1, i2); + calcTriangleNormal(i2, i3, i0); + } + } + + void calcQuadStripNormals() { + for (int qd = 1; qd < vertexCount / 2; qd++) { + int i0 = 2 * (qd - 1); + int i1 = 2 * (qd - 1) + 1; + int i2 = 2 * qd; + int i3 = 2 * qd + 1; + + // Vertices are specified by user as: + // 1-3-5 ... + // |\|\| ... + // 0-2-4 ... + // thus (0, 1, 2) and (2, 1, 3) are triangles + // in CW order (left-handed). + calcTriangleNormal(i0, i1, i2); + calcTriangleNormal(i2, i1, i3); + } + } + + // ----------------------------------------------------------------- + // + // Primitives + + void setMaterial(int fillColor, int strokeColor, float strokeWeight, + int ambientColor, int specularColor, int emissiveColor, + float shininessFactor) { + this.fillColor = fillColor; + this.strokeColor = strokeColor; + this.strokeWeight = strokeWeight; + this.ambientColor = ambientColor; + this.specularColor = specularColor; + this.emissiveColor = emissiveColor; + this.shininessFactor = shininessFactor; + } + + void setNormal(float normalX, float normalY, float normalZ) { + this.normalX = normalX; + this.normalY = normalY; + this.normalZ = normalZ; + } + + void addPoint(float x, float y, float z, boolean fill, boolean stroke) { + addVertex(x, y, z, VERTEX, true); + } + + void addLine(float x1, float y1, float z1, + float x2, float y2, float z2, + boolean fill, boolean stroke) { + int idx1 = addVertex(x1, y1, z1, VERTEX, true); + int idx2 = addVertex(x2, y2, z2, VERTEX, false); + if (stroke) addEdge(idx1, idx2, true, true); + } + + void addTriangle(float x1, float y1, float z1, + float x2, float y2, float z2, + float x3, float y3, float z3, + boolean fill, boolean stroke) { + int idx1 = addVertex(x1, y1, z1, VERTEX, true); + int idx2 = addVertex(x2, y2, z2, VERTEX, false); + int idx3 = addVertex(x3, y3, z3, VERTEX, false); + if (stroke) { + addEdge(idx1, idx2, true, false); + addEdge(idx2, idx3, false, false); + addEdge(idx3, idx1, false, false); + closeEdge(idx3, idx1); + } + } + + void addQuad(float x1, float y1, float z1, + float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4, + boolean stroke) { + int idx1 = addVertex(x1, y1, z1, 0, 0, VERTEX, true); + int idx2 = addVertex(x2, y2, z2, 1, 0, VERTEX, false); + int idx3 = addVertex(x3, y3, z3, 1, 1, VERTEX, false); + int idx4 = addVertex(x4, y4, z4, 0, 1, VERTEX, false); + if (stroke) { + addEdge(idx1, idx2, true, false); + addEdge(idx2, idx3, false, false); + addEdge(idx3, idx4, false, false); + addEdge(idx4, idx1, false, false); + closeEdge(idx4, idx1); + } + } + + void addRect(float a, float b, float c, float d, + boolean stroke) { + addQuad(a, b, 0, + c, b, 0, + c, d, 0, + a, d, 0, + stroke); + } + + void addRect(float a, float b, float c, float d, + float tl, float tr, float br, float bl, + boolean stroke) { + if (nonZero(tr)) { + addVertex(c-tr, b, VERTEX, true); + addQuadraticVertex(c, b, 0, c, b+tr, 0, false); + } else { + addVertex(c, b, VERTEX, true); + } + if (nonZero(br)) { + addVertex(c, d-br, VERTEX, false); + addQuadraticVertex(c, d, 0, c-br, d, 0, false); + } else { + addVertex(c, d, VERTEX, false); + } + if (nonZero(bl)) { + addVertex(a+bl, d, VERTEX, false); + addQuadraticVertex(a, d, 0, a, d-bl, 0, false); + } else { + addVertex(a, d, VERTEX, false); + } + if (nonZero(tl)) { + addVertex(a, b+tl, VERTEX, false); + addQuadraticVertex(a, b, 0, a+tl, b, 0, false); + } else { + addVertex(a, b, VERTEX, false); + } + } + + void addEllipse(float x, float y, float w, float h, + boolean fill, boolean stroke) { + float radiusH = w / 2; + float radiusV = h / 2; + + float centerX = x + radiusH; + float centerY = y + radiusV; + + // should call screenX/Y using current renderer. + float sx1 = pg.screenX(x, y); + float sy1 = pg.screenY(x, y); + float sx2 = pg.screenX(x + w, y + h); + float sy2 = pg.screenY(x + w, y + h); + + int accuracy = + PApplet.min(MAX_POINT_ACCURACY, PApplet.max(MIN_POINT_ACCURACY, + (int) (TWO_PI * PApplet.dist(sx1, sy1, sx2, sy2) / + POINT_ACCURACY_FACTOR))); + float inc = (float) SINCOS_LENGTH / accuracy; + + if (fill) { + addVertex(centerX, centerY, VERTEX, true); + } + int idx0, pidx, idx; + idx0 = pidx = idx = 0; + float val = 0; + for (int i = 0; i < accuracy; i++) { + idx = addVertex(centerX + cosLUT[(int) val] * radiusH, + centerY + sinLUT[(int) val] * radiusV, + VERTEX, i == 0 && !fill); + val = (val + inc) % SINCOS_LENGTH; + + if (0 < i) { + if (stroke) addEdge(pidx, idx, i == 1, false); + } else { + idx0 = idx; + } + + pidx = idx; + } + // Back to the beginning + addVertex(centerX + cosLUT[0] * radiusH, + centerY + sinLUT[0] * radiusV, + VERTEX, false); + if (stroke) { + addEdge(idx, idx0, false, false); + closeEdge(idx, idx0); + } + } + + // arcMode can be 0, OPEN, CHORD, or PIE + void addArc(float x, float y, float w, float h, + float start, float stop, + boolean fill, boolean stroke, int arcMode) { + float hr = w / 2f; + float vr = h / 2f; + + float centerX = x + hr; + float centerY = y + vr; + + int startLUT = (int) (0.5f + (start / TWO_PI) * SINCOS_LENGTH); + int stopLUT = (int) (0.5f + (stop / TWO_PI) * SINCOS_LENGTH); + + // get length before wrapping indexes so (startLUT <= stopLUT); + int length = PApplet.constrain(stopLUT - startLUT, 0, SINCOS_LENGTH); + + boolean fullCircle = length == SINCOS_LENGTH; + + if (fullCircle && arcMode == CHORD) { + // get rid of overlapping vertices, + // solves problem with closing edge in P3D + length -= 1; + stopLUT -= 1; + } + + { // wrap indexes so they are safe to use in LUT + startLUT %= SINCOS_LENGTH; + if (startLUT < 0) startLUT += SINCOS_LENGTH; + + stopLUT %= SINCOS_LENGTH; + if (stopLUT < 0) stopLUT += SINCOS_LENGTH; + } + + int idx0; + if (arcMode == CHORD || arcMode == OPEN) { + // move center to the middle of flat side + // to properly display arcs smaller than PI + float relX = (cosLUT[startLUT] + cosLUT[stopLUT]) * 0.5f * hr; + float relY = (sinLUT[startLUT] + sinLUT[stopLUT]) * 0.5f * vr; + idx0 = addVertex(centerX + relX, centerY + relY, VERTEX, true); + } else { + idx0 = addVertex(centerX, centerY, VERTEX, true); + } + + int inc; + { // initializes inc the same way ellipse does + float sx1 = pg.screenX(x, y); + float sy1 = pg.screenY(x, y); + float sx2 = pg.screenX(x + w, y + h); + float sy2 = pg.screenY(x + w, y + h); + + int accuracy = + PApplet.min(MAX_POINT_ACCURACY, PApplet.max(MIN_POINT_ACCURACY, + (int) (TWO_PI * PApplet.dist(sx1, sy1, sx2, sy2) / + POINT_ACCURACY_FACTOR))); + inc = PApplet.max(1, SINCOS_LENGTH / accuracy); + } + + int idx = idx0; + int pidx; + + int i = -inc; + int ii; + + // i: (0 -> length) inclusive + // ii: (startLUT -> stopLUT) inclusive, going CW (left-handed), + // wrapping around end of LUT + do { + i += inc; + i = PApplet.min(i, length); // clamp so last vertex won't go over + + ii = startLUT + i; // ii from 0 to (2 * SINCOS_LENGTH - 1) + if (ii >= SINCOS_LENGTH) ii -= SINCOS_LENGTH; + + pidx = idx; + idx = addVertex(centerX + cosLUT[ii] * hr, + centerY + sinLUT[ii] * vr, + VERTEX, i == 0 && !fill); + + if (stroke) { + if (arcMode == CHORD || arcMode == PIE) { + addEdge(pidx, idx, i == 0, false); + } else if (0 < i) { + // when drawing full circle, the edge is closed later + addEdge(pidx, idx, i == PApplet.min(inc, length), + i == length && !fullCircle); + } + } + } while (i < length); + + // keeping last vertex as idx and second last vertex as pidx + + if (stroke) { + if (arcMode == CHORD || arcMode == PIE) { + addEdge(idx, idx0, false, false); + closeEdge(idx, idx0); + } else if (fullCircle) { + closeEdge(pidx, idx); + } + } + } + + void addBox(float w, float h, float d, + boolean fill, boolean stroke) { + + // Correct normals if some dimensions are negative so they always + // extend from front face. We could just take absolute value + // of dimensions, but that would affect texturing. + boolean invertNormX = (h > 0) != (d > 0); + boolean invertNormY = (w > 0) != (d > 0); + boolean invertNormZ = (w > 0) != (h > 0); + + int normX = invertNormX ? -1 : 1; + int normY = invertNormY ? -1 : 1; + int normZ = invertNormZ ? -1 : 1; + + float x1 = -w/2f; float x2 = w/2f; + float y1 = -h/2f; float y2 = h/2f; + float z1 = -d/2f; float z2 = d/2f; + + int idx1 = 0, idx2 = 0, idx3 = 0, idx4 = 0; + if (fill || stroke) { + // back face + setNormal(0, 0, -normZ); + idx1 = addVertex(x1, y1, z1, 0, 0, VERTEX, true); + idx2 = addVertex(x1, y2, z1, 0, 1, VERTEX, false); + idx3 = addVertex(x2, y2, z1, 1, 1, VERTEX, false); + idx4 = addVertex(x2, y1, z1, 1, 0, VERTEX, false); + if (stroke) { + addEdge(idx1, idx2, true, false); + addEdge(idx2, idx3, false, false); + addEdge(idx3, idx4, false, false); + addEdge(idx4, idx1, false, false); + closeEdge(idx4, idx1); + } + + // front face + setNormal(0, 0, normZ); + idx1 = addVertex(x1, y2, z2, 1, 1, VERTEX, false); + idx2 = addVertex(x1, y1, z2, 1, 0, VERTEX, false); + idx3 = addVertex(x2, y1, z2, 0, 0, VERTEX, false); + idx4 = addVertex(x2, y2, z2, 0, 1, VERTEX, false); + if (stroke) { + addEdge(idx1, idx2, true, false); + addEdge(idx2, idx3, false, false); + addEdge(idx3, idx4, false, false); + addEdge(idx4, idx1, false, false); + closeEdge(idx4, idx1); + } + + // right face + setNormal(normX, 0, 0); + idx1 = addVertex(x2, y1, z1, 0, 0, VERTEX, false); + idx2 = addVertex(x2, y2, z1, 0, 1, VERTEX, false); + idx3 = addVertex(x2, y2, z2, 1, 1, VERTEX, false); + idx4 = addVertex(x2, y1, z2, 1, 0, VERTEX, false); + if (stroke) { + addEdge(idx1, idx2, true, false); + addEdge(idx2, idx3, false, false); + addEdge(idx3, idx4, false, false); + addEdge(idx4, idx1, false, false); + closeEdge(idx4, idx1); + } + + // left face + setNormal(-normX, 0, 0); + idx1 = addVertex(x1, y2, z1, 1, 1, VERTEX, false); + idx2 = addVertex(x1, y1, z1, 1, 0, VERTEX, false); + idx3 = addVertex(x1, y1, z2, 0, 0, VERTEX, false); + idx4 = addVertex(x1, y2, z2, 0, 1, VERTEX, false); + if (stroke) { + addEdge(idx1, idx2, true, false); + addEdge(idx2, idx3, false, false); + addEdge(idx3, idx4, false, false); + addEdge(idx4, idx1, false, false); + closeEdge(idx4, idx1); + } + + // top face + setNormal(0, -normY, 0); + idx1 = addVertex(x2, y1, z1, 1, 1, VERTEX, false); + idx2 = addVertex(x2, y1, z2, 1, 0, VERTEX, false); + idx3 = addVertex(x1, y1, z2, 0, 0, VERTEX, false); + idx4 = addVertex(x1, y1, z1, 0, 1, VERTEX, false); + if (stroke) { + addEdge(idx1, idx2, true, false); + addEdge(idx2, idx3, false, false); + addEdge(idx3, idx4, false, false); + addEdge(idx4, idx1, false, false); + closeEdge(idx4, idx1); + } + + // bottom face + setNormal(0, normY, 0); + idx1 = addVertex(x1, y2, z1, 0, 0, VERTEX, false); + idx2 = addVertex(x1, y2, z2, 0, 1, VERTEX, false); + idx3 = addVertex(x2, y2, z2, 1, 1, VERTEX, false); + idx4 = addVertex(x2, y2, z1, 1, 0, VERTEX, false); + if (stroke) { + addEdge(idx1, idx2, true, false); + addEdge(idx2, idx3, false, false); + addEdge(idx3, idx4, false, false); + addEdge(idx4, idx1, false, false); + closeEdge(idx4, idx1); + } + } + } + + // Adds the vertices that define an sphere, without duplicating + // any vertex or edge. + int[] addSphere(float r, int detailU, int detailV, + boolean fill, boolean stroke) { + int nind = 3 * detailU + (6 * detailU + 3) * (detailV - 2) + 3 * detailU; + int[] indices = new int[nind]; + + int vertCount = 0; + int indCount = 0; + int vert0, vert1; + + float u, v; + float du = 1.0f / (detailU); + float dv = 1.0f / (detailV); + + // Southern cap ------------------------------------------------------- + + // Adding multiple copies of the south pole vertex, each one with a + // different u coordinate, so the texture mapping is correct when + // making the first strip of triangles. + u = 1; v = 1; + for (int i = 0; i < detailU; i++) { + setNormal(0, 1, 0); + addVertex(0, r, 0, u , v, VERTEX, true); + u -= du; + } + vertCount = detailU; + vert0 = vertCount; + u = 1; v -= dv; + for (int i = 0; i < detailU; i++) { + setNormal(pg.sphereX[i], pg.sphereY[i], pg.sphereZ[i]); + addVertex(r*pg.sphereX[i], r*pg.sphereY[i], r*pg.sphereZ[i], u , v, + VERTEX, false); + u -= du; + } + vertCount += detailU; + vert1 = vertCount; + setNormal(pg.sphereX[0], pg.sphereY[0], pg.sphereZ[0]); + addVertex(r*pg.sphereX[0], r*pg.sphereY[0], r*pg.sphereZ[0], u, v, + VERTEX, false); + vertCount++; + + for (int i = 0; i < detailU; i++) { + int i1 = vert0 + i; + int i0 = vert0 + i - detailU; + + indices[3 * i + 0] = i1; + indices[3 * i + 1] = i0; + indices[3 * i + 2] = i1 + 1; + + addEdge(i0, i1, true, true); + addEdge(i1, i1 + 1, true, true); + } + indCount += 3 * detailU; + + // Middle rings ------------------------------------------------------- + + int offset = 0; + for (int j = 2; j < detailV; j++) { + offset += detailU; + vert0 = vertCount; + u = 1; v -= dv; + for (int i = 0; i < detailU; i++) { + int ioff = offset + i; + setNormal(pg.sphereX[ioff], pg.sphereY[ioff], pg.sphereZ[ioff]); + addVertex(r*pg.sphereX[ioff], r*pg.sphereY[ioff], r*pg.sphereZ[ioff], + u , v, VERTEX, false); + u -= du; + } + vertCount += detailU; + vert1 = vertCount; + setNormal(pg.sphereX[offset], pg.sphereY[offset], pg.sphereZ[offset]); + addVertex(r*pg.sphereX[offset], r*pg.sphereY[offset], r*pg.sphereZ[offset], + u, v, VERTEX, false); + vertCount++; + + for (int i = 0; i < detailU; i++) { + int i1 = vert0 + i; + int i0 = vert0 + i - detailU - 1; + + indices[indCount + 6 * i + 0] = i1; + indices[indCount + 6 * i + 1] = i0; + indices[indCount + 6 * i + 2] = i0 + 1; + + indices[indCount + 6 * i + 3] = i1; + indices[indCount + 6 * i + 4] = i0 + 1; + indices[indCount + 6 * i + 5] = i1 + 1; + + addEdge(i0, i1, true, true); + addEdge(i1, i1 + 1, true, true); + addEdge(i0 + 1, i1, true, true); + } + indCount += 6 * detailU; + indices[indCount + 0] = vert1; + indices[indCount + 1] = vert1 - detailU; + indices[indCount + 2] = vert1 - 1; + indCount += 3; + } + + // Northern cap ------------------------------------------------------- + + // Adding multiple copies of the north pole vertex, each one with a + // different u coordinate, so the texture mapping is correct when + // making the last strip of triangles. + u = 1; v = 0; + for (int i = 0; i < detailU; i++) { + setNormal(0, -1, 0); + addVertex(0, -r, 0, u , v, VERTEX, false); + u -= du; + } + vertCount += detailU; + + for (int i = 0; i < detailU; i++) { + int i0 = vert0 + i; + int i1 = vert0 + i + detailU + 1; + + indices[indCount + 3 * i + 0] = i1; + indices[indCount + 3 * i + 1] = i0; + indices[indCount + 3 * i + 2] = i0 + 1; + + addEdge(i0, i1, true, true); + } + indCount += 3 * detailU; + + return indices; + } + } + + + // Holds tessellated data for polygon, line and point geometry. + static protected class TessGeometry { + int renderMode; + PGraphicsOpenGL pg; + AttributeMap polyAttribs; + + // Tessellated polygon data + int polyVertexCount; + int firstPolyVertex; + int lastPolyVertex; + FloatBuffer polyVerticesBuffer; + IntBuffer polyColorsBuffer; + FloatBuffer polyNormalsBuffer; + FloatBuffer polyTexCoordsBuffer; + + // Polygon material properties (polyColors is used + // as the diffuse color when lighting is enabled) + IntBuffer polyAmbientBuffer; + IntBuffer polySpecularBuffer; + IntBuffer polyEmissiveBuffer; + FloatBuffer polyShininessBuffer; + + // Generic attributes + HashMap polyAttribBuffers = new HashMap<>(); + + int polyIndexCount; + int firstPolyIndex; + int lastPolyIndex; + ShortBuffer polyIndicesBuffer; + IndexCache polyIndexCache = new IndexCache(); + + // Tessellated line data + int lineVertexCount; + int firstLineVertex; + int lastLineVertex; + FloatBuffer lineVerticesBuffer; + IntBuffer lineColorsBuffer; + FloatBuffer lineDirectionsBuffer; + + int lineIndexCount; + int firstLineIndex; + int lastLineIndex; + ShortBuffer lineIndicesBuffer; + IndexCache lineIndexCache = new IndexCache(); + + // Tessellated point data + int pointVertexCount; + int firstPointVertex; + int lastPointVertex; + FloatBuffer pointVerticesBuffer; + IntBuffer pointColorsBuffer; + FloatBuffer pointOffsetsBuffer; + + int pointIndexCount; + int firstPointIndex; + int lastPointIndex; + ShortBuffer pointIndicesBuffer; + IndexCache pointIndexCache = new IndexCache(); + + // Backing arrays + float[] polyVertices; + int[] polyColors; + float[] polyNormals; + float[] polyTexCoords; + int[] polyAmbient; + int[] polySpecular; + int[] polyEmissive; + float[] polyShininess; + short[] polyIndices; + float[] lineVertices; + int[] lineColors; + float[] lineDirections; + short[] lineIndices; + float[] pointVertices; + int[] pointColors; + float[] pointOffsets; + short[] pointIndices; + + HashMap fpolyAttribs = new HashMap<>(); + HashMap ipolyAttribs = new HashMap<>(); + HashMap bpolyAttribs = new HashMap<>(); + + TessGeometry(PGraphicsOpenGL pg, AttributeMap attr, int mode) { + this.pg = pg; + this.polyAttribs = attr; + renderMode = mode; + allocate(); + } + + // ----------------------------------------------------------------- + // + // Allocate/dispose + + void allocate() { + polyVertices = new float[4 * PGL.DEFAULT_TESS_VERTICES]; + polyColors = new int[PGL.DEFAULT_TESS_VERTICES]; + polyNormals = new float[3 * PGL.DEFAULT_TESS_VERTICES]; + polyTexCoords = new float[2 * PGL.DEFAULT_TESS_VERTICES]; + polyAmbient = new int[PGL.DEFAULT_TESS_VERTICES]; + polySpecular = new int[PGL.DEFAULT_TESS_VERTICES]; + polyEmissive = new int[PGL.DEFAULT_TESS_VERTICES]; + polyShininess = new float[PGL.DEFAULT_TESS_VERTICES]; + polyIndices = new short[PGL.DEFAULT_TESS_VERTICES]; + + lineVertices = new float[4 * PGL.DEFAULT_TESS_VERTICES]; + lineColors = new int[PGL.DEFAULT_TESS_VERTICES]; + lineDirections = new float[4 * PGL.DEFAULT_TESS_VERTICES]; + lineIndices = new short[PGL.DEFAULT_TESS_VERTICES]; + + pointVertices = new float[4 * PGL.DEFAULT_TESS_VERTICES]; + pointColors = new int[PGL.DEFAULT_TESS_VERTICES]; + pointOffsets = new float[2 * PGL.DEFAULT_TESS_VERTICES]; + pointIndices = new short[PGL.DEFAULT_TESS_VERTICES]; + + polyVerticesBuffer = PGL.allocateFloatBuffer(polyVertices); + polyColorsBuffer = PGL.allocateIntBuffer(polyColors); + polyNormalsBuffer = PGL.allocateFloatBuffer(polyNormals); + polyTexCoordsBuffer = PGL.allocateFloatBuffer(polyTexCoords); + polyAmbientBuffer = PGL.allocateIntBuffer(polyAmbient); + polySpecularBuffer = PGL.allocateIntBuffer(polySpecular); + polyEmissiveBuffer = PGL.allocateIntBuffer(polyEmissive); + polyShininessBuffer = PGL.allocateFloatBuffer(polyShininess); + polyIndicesBuffer = PGL.allocateShortBuffer(polyIndices); + + lineVerticesBuffer = PGL.allocateFloatBuffer(lineVertices); + lineColorsBuffer = PGL.allocateIntBuffer(lineColors); + lineDirectionsBuffer = PGL.allocateFloatBuffer(lineDirections); + lineIndicesBuffer = PGL.allocateShortBuffer(lineIndices); + + pointVerticesBuffer = PGL.allocateFloatBuffer(pointVertices); + pointColorsBuffer = PGL.allocateIntBuffer(pointColors); + pointOffsetsBuffer = PGL.allocateFloatBuffer(pointOffsets); + pointIndicesBuffer = PGL.allocateShortBuffer(pointIndices); + + clear(); + } + + void initAttrib(VertexAttribute attrib) { + if (attrib.type == PGL.FLOAT && !fpolyAttribs.containsKey(attrib.name)) { + float[] temp = new float[attrib.tessSize * PGL.DEFAULT_TESS_VERTICES]; + fpolyAttribs.put(attrib.name, temp); + polyAttribBuffers.put(attrib.name, PGL.allocateFloatBuffer(temp)); + } else if (attrib.type == PGL.INT && !ipolyAttribs.containsKey(attrib.name)) { + int[] temp = new int[attrib.tessSize * PGL.DEFAULT_TESS_VERTICES]; + ipolyAttribs.put(attrib.name, temp); + polyAttribBuffers.put(attrib.name, PGL.allocateIntBuffer(temp)); + } else if (attrib.type == PGL.BOOL && !bpolyAttribs.containsKey(attrib.name)) { + byte[] temp = new byte[attrib.tessSize * PGL.DEFAULT_TESS_VERTICES]; + bpolyAttribs.put(attrib.name, temp); + polyAttribBuffers.put(attrib.name, PGL.allocateByteBuffer(temp)); + } + } + + void clear() { + firstPolyVertex = lastPolyVertex = polyVertexCount = 0; + firstPolyIndex = lastPolyIndex = polyIndexCount = 0; + + firstLineVertex = lastLineVertex = lineVertexCount = 0; + firstLineIndex = lastLineIndex = lineIndexCount = 0; + + firstPointVertex = lastPointVertex = pointVertexCount = 0; + firstPointIndex = lastPointIndex = pointIndexCount = 0; + + polyIndexCache.clear(); + lineIndexCache.clear(); + pointIndexCache.clear(); + } + + void polyVertexCheck() { + if (polyVertexCount == polyVertices.length / 4) { + int newSize = polyVertexCount << 1; + + expandPolyVertices(newSize); + expandPolyColors(newSize); + expandPolyNormals(newSize); + expandPolyTexCoords(newSize); + expandPolyAmbient(newSize); + expandPolySpecular(newSize); + expandPolyEmissive(newSize); + expandPolyShininess(newSize); + expandAttributes(newSize); + } + + firstPolyVertex = polyVertexCount; + polyVertexCount++; + lastPolyVertex = polyVertexCount - 1; + } + + void polyVertexCheck(int count) { + int oldSize = polyVertices.length / 4; + if (polyVertexCount + count > oldSize) { + int newSize = expandArraySize(oldSize, polyVertexCount + count); + + expandPolyVertices(newSize); + expandPolyColors(newSize); + expandPolyNormals(newSize); + expandPolyTexCoords(newSize); + expandPolyAmbient(newSize); + expandPolySpecular(newSize); + expandPolyEmissive(newSize); + expandPolyShininess(newSize); + expandAttributes(newSize); + } + + firstPolyVertex = polyVertexCount; + polyVertexCount += count; + lastPolyVertex = polyVertexCount - 1; + } + + void polyIndexCheck(int count) { + int oldSize = polyIndices.length; + if (polyIndexCount + count > oldSize) { + int newSize = expandArraySize(oldSize, polyIndexCount + count); + + expandPolyIndices(newSize); + } + + firstPolyIndex = polyIndexCount; + polyIndexCount += count; + lastPolyIndex = polyIndexCount - 1; + } + + void polyIndexCheck() { + if (polyIndexCount == polyIndices.length) { + int newSize = polyIndexCount << 1; + + expandPolyIndices(newSize); + } + + firstPolyIndex = polyIndexCount; + polyIndexCount++; + lastPolyIndex = polyIndexCount - 1; + } + + void lineVertexCheck(int count) { + int oldSize = lineVertices.length / 4; + if (lineVertexCount + count > oldSize) { + int newSize = expandArraySize(oldSize, lineVertexCount + count); + + expandLineVertices(newSize); + expandLineColors(newSize); + expandLineDirections(newSize); + } + + firstLineVertex = lineVertexCount; + lineVertexCount += count; + lastLineVertex = lineVertexCount - 1; + } + + void lineIndexCheck(int count) { + int oldSize = lineIndices.length; + if (lineIndexCount + count > oldSize) { + int newSize = expandArraySize(oldSize, lineIndexCount + count); + + expandLineIndices(newSize); + } + + firstLineIndex = lineIndexCount; + lineIndexCount += count; + lastLineIndex = lineIndexCount - 1; + } + + void pointVertexCheck(int count) { + int oldSize = pointVertices.length / 4; + if (pointVertexCount + count > oldSize) { + int newSize = expandArraySize(oldSize, pointVertexCount + count); + + expandPointVertices(newSize); + expandPointColors(newSize); + expandPointOffsets(newSize); + } + + firstPointVertex = pointVertexCount; + pointVertexCount += count; + lastPointVertex = pointVertexCount - 1; + } + + void pointIndexCheck(int count) { + int oldSize = pointIndices.length; + if (pointIndexCount + count > oldSize) { + int newSize = expandArraySize(oldSize, pointIndexCount + count); + + expandPointIndices(newSize); + } + + firstPointIndex = pointIndexCount; + pointIndexCount += count; + lastPointIndex = pointIndexCount - 1; + } + + // ----------------------------------------------------------------- + // + // Query + + boolean isFull() { + return PGL.FLUSH_VERTEX_COUNT <= polyVertexCount || + PGL.FLUSH_VERTEX_COUNT <= lineVertexCount || + PGL.FLUSH_VERTEX_COUNT <= pointVertexCount; + } + + void getPolyVertexMin(PVector v, int first, int last) { + for (int i = first; i <= last; i++) { + int index = 4 * i; + v.x = PApplet.min(v.x, polyVertices[index++]); + v.y = PApplet.min(v.y, polyVertices[index++]); + v.z = PApplet.min(v.z, polyVertices[index ]); + } + } + + void getLineVertexMin(PVector v, int first, int last) { + for (int i = first; i <= last; i++) { + int index = 4 * i; + v.x = PApplet.min(v.x, lineVertices[index++]); + v.y = PApplet.min(v.y, lineVertices[index++]); + v.z = PApplet.min(v.z, lineVertices[index ]); + } + } + + void getPointVertexMin(PVector v, int first, int last) { + for (int i = first; i <= last; i++) { + int index = 4 * i; + v.x = PApplet.min(v.x, pointVertices[index++]); + v.y = PApplet.min(v.y, pointVertices[index++]); + v.z = PApplet.min(v.z, pointVertices[index ]); + } + } + + void getPolyVertexMax(PVector v, int first, int last) { + for (int i = first; i <= last; i++) { + int index = 4 * i; + v.x = PApplet.max(v.x, polyVertices[index++]); + v.y = PApplet.max(v.y, polyVertices[index++]); + v.z = PApplet.max(v.z, polyVertices[index ]); + } + } + + void getLineVertexMax(PVector v, int first, int last) { + for (int i = first; i <= last; i++) { + int index = 4 * i; + v.x = PApplet.max(v.x, lineVertices[index++]); + v.y = PApplet.max(v.y, lineVertices[index++]); + v.z = PApplet.max(v.z, lineVertices[index ]); + } + } + + void getPointVertexMax(PVector v, int first, int last) { + for (int i = first; i <= last; i++) { + int index = 4 * i; + v.x = PApplet.max(v.x, pointVertices[index++]); + v.y = PApplet.max(v.y, pointVertices[index++]); + v.z = PApplet.max(v.z, pointVertices[index ]); + } + } + + int getPolyVertexSum(PVector v, int first, int last) { + for (int i = first; i <= last; i++) { + int index = 4 * i; + v.x += polyVertices[index++]; + v.y += polyVertices[index++]; + v.z += polyVertices[index ]; + } + return last - first + 1; + } + + int getLineVertexSum(PVector v, int first, int last) { + for (int i = first; i <= last; i++) { + int index = 4 * i; + v.x += lineVertices[index++]; + v.y += lineVertices[index++]; + v.z += lineVertices[index ]; + } + return last - first + 1; + } + + int getPointVertexSum(PVector v, int first, int last) { + for (int i = first; i <= last; i++) { + int index = 4 * i; + v.x += pointVertices[index++]; + v.y += pointVertices[index++]; + v.z += pointVertices[index ]; + } + return last - first + 1; + } + + // ----------------------------------------------------------------- + // + // Methods to prepare buffers for relative read/write operations + + protected void updatePolyVerticesBuffer() { + updatePolyVerticesBuffer(0, polyVertexCount); + } + + protected void updatePolyVerticesBuffer(int offset, int size) { + PGL.updateFloatBuffer(polyVerticesBuffer, polyVertices, + 4 * offset, 4 * size); + } + + protected void updatePolyColorsBuffer() { + updatePolyColorsBuffer(0, polyVertexCount); + } + + protected void updatePolyColorsBuffer(int offset, int size) { + PGL.updateIntBuffer(polyColorsBuffer, polyColors, offset, size); + } + + protected void updatePolyNormalsBuffer() { + updatePolyNormalsBuffer(0, polyVertexCount); + } + + protected void updatePolyNormalsBuffer(int offset, int size) { + PGL.updateFloatBuffer(polyNormalsBuffer, polyNormals, + 3 * offset, 3 * size); + } + + protected void updatePolyTexCoordsBuffer() { + updatePolyTexCoordsBuffer(0, polyVertexCount); + } + + protected void updatePolyTexCoordsBuffer(int offset, int size) { + PGL.updateFloatBuffer(polyTexCoordsBuffer, polyTexCoords, + 2 * offset, 2 * size); + } + + protected void updatePolyAmbientBuffer() { + updatePolyAmbientBuffer(0, polyVertexCount); + } + + protected void updatePolyAmbientBuffer(int offset, int size) { + PGL.updateIntBuffer(polyAmbientBuffer, polyAmbient, offset, size); + } + + protected void updatePolySpecularBuffer() { + updatePolySpecularBuffer(0, polyVertexCount); + } + + protected void updatePolySpecularBuffer(int offset, int size) { + PGL.updateIntBuffer(polySpecularBuffer, polySpecular, offset, size); + } + + protected void updatePolyEmissiveBuffer() { + updatePolyEmissiveBuffer(0, polyVertexCount); + } + + protected void updatePolyEmissiveBuffer(int offset, int size) { + PGL.updateIntBuffer(polyEmissiveBuffer, polyEmissive, offset, size); + } + + protected void updatePolyShininessBuffer() { + updatePolyShininessBuffer(0, polyVertexCount); + } + + protected void updatePolyShininessBuffer(int offset, int size) { + PGL.updateFloatBuffer(polyShininessBuffer, polyShininess, offset, size); + } + + protected void updateAttribBuffer(String name) { + updateAttribBuffer(name, 0, polyVertexCount); + } + + protected void updateAttribBuffer(String name, int offset, int size) { + VertexAttribute attrib = polyAttribs.get(name); + if (attrib.type == PGL.FLOAT) { + FloatBuffer buffer = (FloatBuffer)polyAttribBuffers.get(name); + float[] array = fpolyAttribs.get(name); + PGL.updateFloatBuffer(buffer, array, + attrib.tessSize * offset, attrib.tessSize * size); + } else if (attrib.type == PGL.INT) { + IntBuffer buffer = (IntBuffer)polyAttribBuffers.get(name); + int[] array = ipolyAttribs.get(name); + PGL.updateIntBuffer(buffer, array, + attrib.tessSize * offset, attrib.tessSize * size); + } else if (attrib.type == PGL.BOOL) { + ByteBuffer buffer = (ByteBuffer)polyAttribBuffers.get(name); + byte[] array = bpolyAttribs.get(name); + PGL.updateByteBuffer(buffer, array, + attrib.tessSize * offset, attrib.tessSize * size); + } + } + + protected void updatePolyIndicesBuffer() { + updatePolyIndicesBuffer(0, polyIndexCount); + } + + protected void updatePolyIndicesBuffer(int offset, int size) { + PGL.updateShortBuffer(polyIndicesBuffer, polyIndices, offset, size); + } + + protected void updateLineVerticesBuffer() { + updateLineVerticesBuffer(0, lineVertexCount); + } + + protected void updateLineVerticesBuffer(int offset, int size) { + PGL.updateFloatBuffer(lineVerticesBuffer, lineVertices, + 4 * offset, 4 * size); + } + + protected void updateLineColorsBuffer() { + updateLineColorsBuffer(0, lineVertexCount); + } + + protected void updateLineColorsBuffer(int offset, int size) { + PGL.updateIntBuffer(lineColorsBuffer, lineColors, offset, size); + } + + protected void updateLineDirectionsBuffer() { + updateLineDirectionsBuffer(0, lineVertexCount); + } + + protected void updateLineDirectionsBuffer(int offset, int size) { + PGL.updateFloatBuffer(lineDirectionsBuffer, lineDirections, + 4 * offset, 4 * size); + } + + protected void updateLineIndicesBuffer() { + updateLineIndicesBuffer(0, lineIndexCount); + } + + protected void updateLineIndicesBuffer(int offset, int size) { + PGL.updateShortBuffer(lineIndicesBuffer, lineIndices, offset, size); + } + + protected void updatePointVerticesBuffer() { + updatePointVerticesBuffer(0, pointVertexCount); + } + + protected void updatePointVerticesBuffer(int offset, int size) { + PGL.updateFloatBuffer(pointVerticesBuffer, pointVertices, + 4 * offset, 4 * size); + } + + protected void updatePointColorsBuffer() { + updatePointColorsBuffer(0, pointVertexCount); + } + + protected void updatePointColorsBuffer(int offset, int size) { + PGL.updateIntBuffer(pointColorsBuffer, pointColors, offset, size); + } + + protected void updatePointOffsetsBuffer() { + updatePointOffsetsBuffer(0, pointVertexCount); + } + + protected void updatePointOffsetsBuffer(int offset, int size) { + PGL.updateFloatBuffer(pointOffsetsBuffer, pointOffsets, + 2 * offset, 2 * size); + } + + protected void updatePointIndicesBuffer() { + updatePointIndicesBuffer(0, pointIndexCount); + } + + protected void updatePointIndicesBuffer(int offset, int size) { + PGL.updateShortBuffer(pointIndicesBuffer, pointIndices, offset, size); + } + + // ----------------------------------------------------------------- + // + // Expand arrays + + void expandPolyVertices(int n) { + float temp[] = new float[4 * n]; + PApplet.arrayCopy(polyVertices, 0, temp, 0, 4 * polyVertexCount); + polyVertices = temp; + polyVerticesBuffer = PGL.allocateFloatBuffer(polyVertices); + } + + void expandPolyColors(int n) { + int temp[] = new int[n]; + PApplet.arrayCopy(polyColors, 0, temp, 0, polyVertexCount); + polyColors = temp; + polyColorsBuffer = PGL.allocateIntBuffer(polyColors); + } + + void expandPolyNormals(int n) { + float temp[] = new float[3 * n]; + PApplet.arrayCopy(polyNormals, 0, temp, 0, 3 * polyVertexCount); + polyNormals = temp; + polyNormalsBuffer = PGL.allocateFloatBuffer(polyNormals); + } + + void expandPolyTexCoords(int n) { + float temp[] = new float[2 * n]; + PApplet.arrayCopy(polyTexCoords, 0, temp, 0, 2 * polyVertexCount); + polyTexCoords = temp; + polyTexCoordsBuffer = PGL.allocateFloatBuffer(polyTexCoords); + } + + void expandPolyAmbient(int n) { + int temp[] = new int[n]; + PApplet.arrayCopy(polyAmbient, 0, temp, 0, polyVertexCount); + polyAmbient = temp; + polyAmbientBuffer = PGL.allocateIntBuffer(polyAmbient); + } + + void expandPolySpecular(int n) { + int temp[] = new int[n]; + PApplet.arrayCopy(polySpecular, 0, temp, 0, polyVertexCount); + polySpecular = temp; + polySpecularBuffer = PGL.allocateIntBuffer(polySpecular); + } + + void expandPolyEmissive(int n) { + int temp[] = new int[n]; + PApplet.arrayCopy(polyEmissive, 0, temp, 0, polyVertexCount); + polyEmissive = temp; + polyEmissiveBuffer = PGL.allocateIntBuffer(polyEmissive); + } + + void expandPolyShininess(int n) { + float temp[] = new float[n]; + PApplet.arrayCopy(polyShininess, 0, temp, 0, polyVertexCount); + polyShininess = temp; + polyShininessBuffer = PGL.allocateFloatBuffer(polyShininess); + } + + void expandAttributes(int n) { + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + if (attrib.type == PGL.FLOAT) { + expandFloatAttribute(attrib, n); + } else if (attrib.type == PGL.INT) { + expandIntAttribute(attrib, n); + } else if (attrib.type == PGL.BOOL) { + expandBoolAttribute(attrib, n); + } + } + } + + void expandFloatAttribute(VertexAttribute attrib, int n) { + float[] array = fpolyAttribs.get(attrib.name); + float temp[] = new float[attrib.tessSize * n]; + PApplet.arrayCopy(array, 0, temp, 0, attrib.tessSize * polyVertexCount); + fpolyAttribs.put(attrib.name, temp); + polyAttribBuffers.put(attrib.name, PGL.allocateFloatBuffer(temp)); + } + + void expandIntAttribute(VertexAttribute attrib, int n) { + int[] array = ipolyAttribs.get(attrib.name); + int temp[] = new int[attrib.tessSize * n]; + PApplet.arrayCopy(array, 0, temp, 0, attrib.tessSize * polyVertexCount); + ipolyAttribs.put(attrib.name, temp); + polyAttribBuffers.put(attrib.name, PGL.allocateIntBuffer(temp)); + } + + void expandBoolAttribute(VertexAttribute attrib, int n) { + byte[] array = bpolyAttribs.get(attrib.name); + byte temp[] = new byte[attrib.tessSize * n]; + PApplet.arrayCopy(array, 0, temp, 0, attrib.tessSize * polyVertexCount); + bpolyAttribs.put(attrib.name, temp); + polyAttribBuffers.put(attrib.name, PGL.allocateByteBuffer(temp)); + } + + void expandPolyIndices(int n) { + short temp[] = new short[n]; + PApplet.arrayCopy(polyIndices, 0, temp, 0, polyIndexCount); + polyIndices = temp; + polyIndicesBuffer = PGL.allocateShortBuffer(polyIndices); + } + + void expandLineVertices(int n) { + float temp[] = new float[4 * n]; + PApplet.arrayCopy(lineVertices, 0, temp, 0, 4 * lineVertexCount); + lineVertices = temp; + lineVerticesBuffer = PGL.allocateFloatBuffer(lineVertices); + } + + void expandLineColors(int n) { + int temp[] = new int[n]; + PApplet.arrayCopy(lineColors, 0, temp, 0, lineVertexCount); + lineColors = temp; + lineColorsBuffer = PGL.allocateIntBuffer(lineColors); + } + + void expandLineDirections(int n) { + float temp[] = new float[4 * n]; + PApplet.arrayCopy(lineDirections, 0, temp, 0, 4 * lineVertexCount); + lineDirections = temp; + lineDirectionsBuffer = PGL.allocateFloatBuffer(lineDirections); + } + + void expandLineIndices(int n) { + short temp[] = new short[n]; + PApplet.arrayCopy(lineIndices, 0, temp, 0, lineIndexCount); + lineIndices = temp; + lineIndicesBuffer = PGL.allocateShortBuffer(lineIndices); + } + + void expandPointVertices(int n) { + float temp[] = new float[4 * n]; + PApplet.arrayCopy(pointVertices, 0, temp, 0, 4 * pointVertexCount); + pointVertices = temp; + pointVerticesBuffer = PGL.allocateFloatBuffer(pointVertices); + } + + void expandPointColors(int n) { + int temp[] = new int[n]; + PApplet.arrayCopy(pointColors, 0, temp, 0, pointVertexCount); + pointColors = temp; + pointColorsBuffer = PGL.allocateIntBuffer(pointColors); + } + + void expandPointOffsets(int n) { + float temp[] = new float[2 * n]; + PApplet.arrayCopy(pointOffsets, 0, temp, 0, 2 * pointVertexCount); + pointOffsets = temp; + pointOffsetsBuffer = PGL.allocateFloatBuffer(pointOffsets); + } + + void expandPointIndices(int n) { + short temp[] = new short[n]; + PApplet.arrayCopy(pointIndices, 0, temp, 0, pointIndexCount); + pointIndices = temp; + pointIndicesBuffer = PGL.allocateShortBuffer(pointIndices); + } + + // ----------------------------------------------------------------- + // + // Trim arrays + + void trim() { + if (0 < polyVertexCount && polyVertexCount < polyVertices.length / 4) { + trimPolyVertices(); + trimPolyColors(); + trimPolyNormals(); + trimPolyTexCoords(); + trimPolyAmbient(); + trimPolySpecular(); + trimPolyEmissive(); + trimPolyShininess(); + trimPolyAttributes(); + } + + if (0 < polyIndexCount && polyIndexCount < polyIndices.length) { + trimPolyIndices(); + } + + if (0 < lineVertexCount && lineVertexCount < lineVertices.length / 4) { + trimLineVertices(); + trimLineColors(); + trimLineDirections(); + } + + if (0 < lineIndexCount && lineIndexCount < lineIndices.length) { + trimLineIndices(); + } + + if (0 < pointVertexCount && pointVertexCount < pointVertices.length / 4) { + trimPointVertices(); + trimPointColors(); + trimPointOffsets(); + } + + if (0 < pointIndexCount && pointIndexCount < pointIndices.length) { + trimPointIndices(); + } + } + + void trimPolyVertices() { + float temp[] = new float[4 * polyVertexCount]; + PApplet.arrayCopy(polyVertices, 0, temp, 0, 4 * polyVertexCount); + polyVertices = temp; + polyVerticesBuffer = PGL.allocateFloatBuffer(polyVertices); + } + + void trimPolyColors() { + int temp[] = new int[polyVertexCount]; + PApplet.arrayCopy(polyColors, 0, temp, 0, polyVertexCount); + polyColors = temp; + polyColorsBuffer = PGL.allocateIntBuffer(polyColors); + } + + void trimPolyNormals() { + float temp[] = new float[3 * polyVertexCount]; + PApplet.arrayCopy(polyNormals, 0, temp, 0, 3 * polyVertexCount); + polyNormals = temp; + polyNormalsBuffer = PGL.allocateFloatBuffer(polyNormals); + } + + void trimPolyTexCoords() { + float temp[] = new float[2 * polyVertexCount]; + PApplet.arrayCopy(polyTexCoords, 0, temp, 0, 2 * polyVertexCount); + polyTexCoords = temp; + polyTexCoordsBuffer = PGL.allocateFloatBuffer(polyTexCoords); + } + + void trimPolyAmbient() { + int temp[] = new int[polyVertexCount]; + PApplet.arrayCopy(polyAmbient, 0, temp, 0, polyVertexCount); + polyAmbient = temp; + polyAmbientBuffer = PGL.allocateIntBuffer(polyAmbient); + } + + void trimPolySpecular() { + int temp[] = new int[polyVertexCount]; + PApplet.arrayCopy(polySpecular, 0, temp, 0, polyVertexCount); + polySpecular = temp; + polySpecularBuffer = PGL.allocateIntBuffer(polySpecular); + } + + void trimPolyEmissive() { + int temp[] = new int[polyVertexCount]; + PApplet.arrayCopy(polyEmissive, 0, temp, 0, polyVertexCount); + polyEmissive = temp; + polyEmissiveBuffer = PGL.allocateIntBuffer(polyEmissive); + } + + void trimPolyShininess() { + float temp[] = new float[polyVertexCount]; + PApplet.arrayCopy(polyShininess, 0, temp, 0, polyVertexCount); + polyShininess = temp; + polyShininessBuffer = PGL.allocateFloatBuffer(polyShininess); + } + + void trimPolyAttributes() { + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + if (attrib.type == PGL.FLOAT) { + trimFloatAttribute(attrib); + } else if (attrib.type == PGL.INT) { + trimIntAttribute(attrib); + } else if (attrib.type == PGL.BOOL) { + trimBoolAttribute(attrib); + } + } + } + + void trimFloatAttribute(VertexAttribute attrib) { + float[] array = fpolyAttribs.get(attrib.name); + float temp[] = new float[attrib.tessSize * polyVertexCount]; + PApplet.arrayCopy(array, 0, temp, 0, attrib.tessSize * polyVertexCount); + fpolyAttribs.put(attrib.name, temp); + polyAttribBuffers.put(attrib.name, PGL.allocateFloatBuffer(temp)); + } + + void trimIntAttribute(VertexAttribute attrib) { + int[] array = ipolyAttribs.get(attrib.name); + int temp[] = new int[attrib.tessSize * polyVertexCount]; + PApplet.arrayCopy(array, 0, temp, 0, attrib.tessSize * polyVertexCount); + ipolyAttribs.put(attrib.name, temp); + polyAttribBuffers.put(attrib.name, PGL.allocateIntBuffer(temp)); + } + + void trimBoolAttribute(VertexAttribute attrib) { + byte[] array = bpolyAttribs.get(attrib.name); + byte temp[] = new byte[attrib.tessSize * polyVertexCount]; + PApplet.arrayCopy(array, 0, temp, 0, attrib.tessSize * polyVertexCount); + bpolyAttribs.put(attrib.name, temp); + polyAttribBuffers.put(attrib.name, PGL.allocateByteBuffer(temp)); + } + + void trimPolyIndices() { + short temp[] = new short[polyIndexCount]; + PApplet.arrayCopy(polyIndices, 0, temp, 0, polyIndexCount); + polyIndices = temp; + polyIndicesBuffer = PGL.allocateShortBuffer(polyIndices); + } + + void trimLineVertices() { + float temp[] = new float[4 * lineVertexCount]; + PApplet.arrayCopy(lineVertices, 0, temp, 0, 4 * lineVertexCount); + lineVertices = temp; + lineVerticesBuffer = PGL.allocateFloatBuffer(lineVertices); + } + + void trimLineColors() { + int temp[] = new int[lineVertexCount]; + PApplet.arrayCopy(lineColors, 0, temp, 0, lineVertexCount); + lineColors = temp; + lineColorsBuffer = PGL.allocateIntBuffer(lineColors); + } + + void trimLineDirections() { + float temp[] = new float[4 * lineVertexCount]; + PApplet.arrayCopy(lineDirections, 0, temp, 0, 4 * lineVertexCount); + lineDirections = temp; + lineDirectionsBuffer = PGL.allocateFloatBuffer(lineDirections); + } + + void trimLineIndices() { + short temp[] = new short[lineIndexCount]; + PApplet.arrayCopy(lineIndices, 0, temp, 0, lineIndexCount); + lineIndices = temp; + lineIndicesBuffer = PGL.allocateShortBuffer(lineIndices); + } + + void trimPointVertices() { + float temp[] = new float[4 * pointVertexCount]; + PApplet.arrayCopy(pointVertices, 0, temp, 0, 4 * pointVertexCount); + pointVertices = temp; + pointVerticesBuffer = PGL.allocateFloatBuffer(pointVertices); + } + + void trimPointColors() { + int temp[] = new int[pointVertexCount]; + PApplet.arrayCopy(pointColors, 0, temp, 0, pointVertexCount); + pointColors = temp; + pointColorsBuffer = PGL.allocateIntBuffer(pointColors); + } + + void trimPointOffsets() { + float temp[] = new float[2 * pointVertexCount]; + PApplet.arrayCopy(pointOffsets, 0, temp, 0, 2 * pointVertexCount); + pointOffsets = temp; + pointOffsetsBuffer = PGL.allocateFloatBuffer(pointOffsets); + } + + void trimPointIndices() { + short temp[] = new short[pointIndexCount]; + PApplet.arrayCopy(pointIndices, 0, temp, 0, pointIndexCount); + pointIndices = temp; + pointIndicesBuffer = PGL.allocateShortBuffer(pointIndices); + } + + // ----------------------------------------------------------------- + // + // Aggregation methods + + void incPolyIndices(int first, int last, int inc) { + for (int i = first; i <= last; i++) { + polyIndices[i] += inc; + } + } + + void incLineIndices(int first, int last, int inc) { + for (int i = first; i <= last; i++) { + lineIndices[i] += inc; + } + } + + void incPointIndices(int first, int last, int inc) { + for (int i = first; i <= last; i++) { + pointIndices[i] += inc; + } + } + + // ----------------------------------------------------------------- + // + // Normal calculation + + // Expects vertices in CW (left-handed) order. + void calcPolyNormal(int i0, int i1, int i2) { + int index; + + index = 4 * i0; + float x0 = polyVertices[index++]; + float y0 = polyVertices[index++]; + float z0 = polyVertices[index ]; + + index = 4 * i1; + float x1 = polyVertices[index++]; + float y1 = polyVertices[index++]; + float z1 = polyVertices[index ]; + + index = 4 * i2; + float x2 = polyVertices[index++]; + float y2 = polyVertices[index++]; + float z2 = polyVertices[index ]; + + float v12x = x2 - x1; + float v12y = y2 - y1; + float v12z = z2 - z1; + + float v10x = x0 - x1; + float v10y = y0 - y1; + float v10z = z0 - z1; + + float nx = v12y * v10z - v10y * v12z; + float ny = v12z * v10x - v10z * v12x; + float nz = v12x * v10y - v10x * v12y; + float d = PApplet.sqrt(nx * nx + ny * ny + nz * nz); + nx /= d; + ny /= d; + nz /= d; + + index = 3 * i0; + polyNormals[index++] = nx; + polyNormals[index++] = ny; + polyNormals[index ] = nz; + + index = 3 * i1; + polyNormals[index++] = nx; + polyNormals[index++] = ny; + polyNormals[index ] = nz; + + index = 3 * i2; + polyNormals[index++] = nx; + polyNormals[index++] = ny; + polyNormals[index ] = nz; + } + + // ----------------------------------------------------------------- + // + // Add point geometry + + // Sets point vertex with index tessIdx using the data from input vertex + // inIdx. + void setPointVertex(int tessIdx, InGeometry in, int inIdx) { + int index; + + index = 3 * inIdx; + float x = in.vertices[index++]; + float y = in.vertices[index++]; + float z = in.vertices[index ]; + + if (renderMode == IMMEDIATE && pg.flushMode == FLUSH_WHEN_FULL) { + PMatrix3D mm = pg.modelview; + + index = 4 * tessIdx; + pointVertices[index++] = x*mm.m00 + y*mm.m01 + z*mm.m02 + mm.m03; + pointVertices[index++] = x*mm.m10 + y*mm.m11 + z*mm.m12 + mm.m13; + pointVertices[index++] = x*mm.m20 + y*mm.m21 + z*mm.m22 + mm.m23; + pointVertices[index ] = x*mm.m30 + y*mm.m31 + z*mm.m32 + mm.m33; + } else { + index = 4 * tessIdx; + pointVertices[index++] = x; + pointVertices[index++] = y; + pointVertices[index++] = z; + pointVertices[index ] = 1; + } + + pointColors[tessIdx] = in.strokeColors[inIdx]; + } + + // ----------------------------------------------------------------- + // + // Add line geometry + + void setLineVertex(int tessIdx, float[] vertices, int inIdx0, int rgba) { + int index; + + index = 3 * inIdx0; + float x0 = vertices[index++]; + float y0 = vertices[index++]; + float z0 = vertices[index ]; + + if (renderMode == IMMEDIATE && pg.flushMode == FLUSH_WHEN_FULL) { + PMatrix3D mm = pg.modelview; + + index = 4 * tessIdx; + lineVertices[index++] = x0*mm.m00 + y0*mm.m01 + z0*mm.m02 + mm.m03; + lineVertices[index++] = x0*mm.m10 + y0*mm.m11 + z0*mm.m12 + mm.m13; + lineVertices[index++] = x0*mm.m20 + y0*mm.m21 + z0*mm.m22 + mm.m23; + lineVertices[index ] = x0*mm.m30 + y0*mm.m31 + z0*mm.m32 + mm.m33; + } else { + index = 4 * tessIdx; + lineVertices[index++] = x0; + lineVertices[index++] = y0; + lineVertices[index++] = z0; + lineVertices[index ] = 1; + } + + lineColors[tessIdx] = rgba; + index = 4 * tessIdx; + lineDirections[index++] = 0; + lineDirections[index++] = 0; + lineDirections[index++] = 0; + lineDirections[index ] = 0; + } + + // Sets line vertex with index tessIdx using the data from input vertices + // inIdx0 and inIdx1. + void setLineVertex(int tessIdx, float[] vertices, int inIdx0, int inIdx1, + int rgba, float weight) { + int index; + + index = 3 * inIdx0; + float x0 = vertices[index++]; + float y0 = vertices[index++]; + float z0 = vertices[index ]; + + index = 3 * inIdx1; + float x1 = vertices[index++]; + float y1 = vertices[index++]; + float z1 = vertices[index ]; + + float dx = x1 - x0; + float dy = y1 - y0; + float dz = z1 - z0; + + if (renderMode == IMMEDIATE && pg.flushMode == FLUSH_WHEN_FULL) { + PMatrix3D mm = pg.modelview; + + index = 4 * tessIdx; + lineVertices[index++] = x0*mm.m00 + y0*mm.m01 + z0*mm.m02 + mm.m03; + lineVertices[index++] = x0*mm.m10 + y0*mm.m11 + z0*mm.m12 + mm.m13; + lineVertices[index++] = x0*mm.m20 + y0*mm.m21 + z0*mm.m22 + mm.m23; + lineVertices[index ] = x0*mm.m30 + y0*mm.m31 + z0*mm.m32 + mm.m33; + + index = 4 * tessIdx; + lineDirections[index++] = dx*mm.m00 + dy*mm.m01 + dz*mm.m02; + lineDirections[index++] = dx*mm.m10 + dy*mm.m11 + dz*mm.m12; + lineDirections[index ] = dx*mm.m20 + dy*mm.m21 + dz*mm.m22; + } else { + index = 4 * tessIdx; + lineVertices[index++] = x0; + lineVertices[index++] = y0; + lineVertices[index++] = z0; + lineVertices[index ] = 1; + + index = 4 * tessIdx; + lineDirections[index++] = dx; + lineDirections[index++] = dy; + lineDirections[index ] = dz; + } + + lineColors[tessIdx] = rgba; + lineDirections[4 * tessIdx + 3] = weight; + } + + // ----------------------------------------------------------------- + // + // Add poly geometry + + void addPolyVertex(double[] d, boolean clampXY) { + int fcolor = + (int)d[ 3] << 24 | (int)d[ 4] << 16 | (int)d[ 5] << 8 | (int)d[ 6]; + int acolor = + (int)d[12] << 24 | (int)d[13] << 16 | (int)d[14] << 8 | (int)d[15]; + int scolor = + (int)d[16] << 24 | (int)d[17] << 16 | (int)d[18] << 8 | (int)d[19]; + int ecolor = + (int)d[20] << 24 | (int)d[21] << 16 | (int)d[22] << 8 | (int)d[23]; + + addPolyVertex((float)d[ 0], (float)d[ 1], (float)d[ 2], + fcolor, + (float)d[ 7], (float)d[ 8], (float)d[ 9], + (float)d[10], (float)d[11], + acolor, scolor, ecolor, + (float)d[24], + clampXY); + + if (25 < d.length) { + // Add the values of the custom attributes... + PMatrix3D mm = pg.modelview; + PMatrix3D nm = pg.modelviewInv; + int tessIdx = polyVertexCount - 1; + int index; + int pos = 25; + for (int i = 0; i < polyAttribs.size(); i++) { + VertexAttribute attrib = polyAttribs.get(i); + String name = attrib.name; + index = attrib.tessSize * tessIdx; + if (attrib.isColor()) { + // Reconstruct color from ARGB components + int color = + (int)d[pos + 0] << 24 | (int)d[pos + 1] << 16 | (int)d[pos + 2] << 8 | (int)d[pos + 3]; + int[] tessValues = ipolyAttribs.get(name); + tessValues[index] = color; + pos += 4; + } else if (attrib.isPosition()) { + float[] farray = fpolyAttribs.get(name); + float x = (float)d[pos++]; + float y = (float)d[pos++]; + float z = (float)d[pos++]; + if (renderMode == IMMEDIATE && pg.flushMode == FLUSH_WHEN_FULL) { + if (clampXY) { + // ceil emulates the behavior of JAVA2D + farray[index++] = + PApplet.ceil(x*mm.m00 + y*mm.m01 + z*mm.m02 + mm.m03); + farray[index++] = + PApplet.ceil(x*mm.m10 + y*mm.m11 + z*mm.m12 + mm.m13); + } else { + farray[index++] = x*mm.m00 + y*mm.m01 + z*mm.m02 + mm.m03; + farray[index++] = x*mm.m10 + y*mm.m11 + z*mm.m12 + mm.m13; + } + farray[index++] = x*mm.m20 + y*mm.m21 + z*mm.m22 + mm.m23; + farray[index ] = x*mm.m30 + y*mm.m31 + z*mm.m32 + mm.m33; + } else { + farray[index++] = x; + farray[index++] = y; + farray[index++] = z; + farray[index ] = 1; + } + } else if (attrib.isNormal()) { + float[] farray = fpolyAttribs.get(name); + float x = (float)d[pos + 0]; + float y = (float)d[pos + 1]; + float z = (float)d[pos + 2]; + if (renderMode == IMMEDIATE && pg.flushMode == FLUSH_WHEN_FULL) { + farray[index++] = x*nm.m00 + y*nm.m10 + z*nm.m20; + farray[index++] = x*nm.m01 + y*nm.m11 + z*nm.m21; + farray[index ] = x*nm.m02 + y*nm.m12 + z*nm.m22; + } else { + farray[index++] = x; + farray[index++] = y; + farray[index ] = z; + } + pos += 3; + } else { + if (attrib.isFloat()) { + float[] farray = fpolyAttribs.get(name); + for (int n = 0; n < attrib.size; n++) { + farray[index++] = (float)d[pos++]; + } + } else if (attrib.isInt()) { + int[] iarray = ipolyAttribs.get(name); + for (int n = 0; n < attrib.size; n++) { + iarray[index++] = (int)d[pos++]; + } + } else if (attrib.isBool()) { + byte[] barray = bpolyAttribs.get(name); + for (int n = 0; n < attrib.size; n++) { + barray[index++] = (byte)d[pos++]; + } + } + pos += attrib.size; + } + } + } + } + + void addPolyVertex(float x, float y, float z, + int rgba, + float nx, float ny, float nz, + float u, float v, + int am, int sp, int em, float shine, + boolean clampXY) { + polyVertexCheck(); + int tessIdx = polyVertexCount - 1; + setPolyVertex(tessIdx, x, y, z, + rgba, + nx, ny, nz, + u, v, + am, sp, em, shine, clampXY); + } + + void setPolyVertex(int tessIdx, float x, float y, float z, int rgba, + boolean clampXY) { + setPolyVertex(tessIdx, x, y, z, + rgba, + 0, 0, 1, + 0, 0, + 0, 0, 0, 0, clampXY); + } + + void setPolyVertex(int tessIdx, float x, float y, float z, + int rgba, + float nx, float ny, float nz, + float u, float v, + int am, int sp, int em, float shine, + boolean clampXY) { + int index; + + if (renderMode == IMMEDIATE && pg.flushMode == FLUSH_WHEN_FULL) { + PMatrix3D mm = pg.modelview; + PMatrix3D nm = pg.modelviewInv; + + index = 4 * tessIdx; + if (clampXY) { + // ceil emulates the behavior of JAVA2D + polyVertices[index++] = + PApplet.ceil(x*mm.m00 + y*mm.m01 + z*mm.m02 + mm.m03); + polyVertices[index++] = + PApplet.ceil(x*mm.m10 + y*mm.m11 + z*mm.m12 + mm.m13); + } else { + polyVertices[index++] = x*mm.m00 + y*mm.m01 + z*mm.m02 + mm.m03; + polyVertices[index++] = x*mm.m10 + y*mm.m11 + z*mm.m12 + mm.m13; + } + polyVertices[index++] = x*mm.m20 + y*mm.m21 + z*mm.m22 + mm.m23; + polyVertices[index ] = x*mm.m30 + y*mm.m31 + z*mm.m32 + mm.m33; + + index = 3 * tessIdx; + polyNormals[index++] = nx*nm.m00 + ny*nm.m10 + nz*nm.m20; + polyNormals[index++] = nx*nm.m01 + ny*nm.m11 + nz*nm.m21; + polyNormals[index ] = nx*nm.m02 + ny*nm.m12 + nz*nm.m22; + } else { + index = 4 * tessIdx; + polyVertices[index++] = x; + polyVertices[index++] = y; + polyVertices[index++] = z; + polyVertices[index ] = 1; + + index = 3 * tessIdx; + polyNormals[index++] = nx; + polyNormals[index++] = ny; + polyNormals[index ] = nz; + } + + polyColors[tessIdx] = rgba; + + index = 2 * tessIdx; + polyTexCoords[index++] = u; + polyTexCoords[index ] = v; + + polyAmbient[tessIdx] = am; + polySpecular[tessIdx] = sp; + polyEmissive[tessIdx] = em; + polyShininess[tessIdx] = shine; + } + + void addPolyVertices(InGeometry in, boolean clampXY) { + addPolyVertices(in, 0, in.vertexCount - 1, clampXY); + } + + void addPolyVertex(InGeometry in, int i, boolean clampXY) { + addPolyVertices(in, i, i, clampXY); + } + + void addPolyVertices(InGeometry in, int i0, int i1, boolean clampXY) { + int index = 0; + int nvert = i1 - i0 + 1; + + polyVertexCheck(nvert); + + if (renderMode == IMMEDIATE && pg.flushMode == FLUSH_WHEN_FULL) { + modelviewCoords(in, i0, index, nvert, clampXY); + } else { + if (nvert <= PGL.MIN_ARRAYCOPY_SIZE) { + copyFewCoords(in, i0, index, nvert); + } else { + copyManyCoords(in, i0, index, nvert); + } + } + + if (nvert <= PGL.MIN_ARRAYCOPY_SIZE) { + copyFewAttribs(in, i0, index, nvert); + } else { + copyManyAttribs(in, i0, index, nvert); + } + } + + // Apply modelview transformation on the vertices + private void modelviewCoords(InGeometry in, int i0, int index, int nvert, boolean clampXY) { + PMatrix3D mm = pg.modelview; + PMatrix3D nm = pg.modelviewInv; + + for (int i = 0; i < nvert; i++) { + int inIdx = i0 + i; + int tessIdx = firstPolyVertex + i; + + index = 3 * inIdx; + float x = in.vertices[index++]; + float y = in.vertices[index++]; + float z = in.vertices[index ]; + + index = 3 * inIdx; + float nx = in.normals[index++]; + float ny = in.normals[index++]; + float nz = in.normals[index ]; + + index = 4 * tessIdx; + if (clampXY) { + // ceil emulates the behavior of JAVA2D + polyVertices[index++] = + PApplet.ceil(x*mm.m00 + y*mm.m01 + z*mm.m02 + mm.m03); + polyVertices[index++] = + PApplet.ceil(x*mm.m10 + y*mm.m11 + z*mm.m12 + mm.m13); + } else { + polyVertices[index++] = x*mm.m00 + y*mm.m01 + z*mm.m02 + mm.m03; + polyVertices[index++] = x*mm.m10 + y*mm.m11 + z*mm.m12 + mm.m13; + } + polyVertices[index++] = x*mm.m20 + y*mm.m21 + z*mm.m22 + mm.m23; + polyVertices[index ] = x*mm.m30 + y*mm.m31 + z*mm.m32 + mm.m33; + + index = 3 * tessIdx; + polyNormals[index++] = nx*nm.m00 + ny*nm.m10 + nz*nm.m20; + polyNormals[index++] = nx*nm.m01 + ny*nm.m11 + nz*nm.m21; + polyNormals[index ] = nx*nm.m02 + ny*nm.m12 + nz*nm.m22; + + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + if (attrib.isColor() || attrib.isOther()) continue; + + float[] inValues = in.fattribs.get(name); + index = 3 * inIdx; + x = inValues[index++]; + y = inValues[index++]; + z = inValues[index ]; + + float[] tessValues = fpolyAttribs.get(name); + if (attrib.isPosition()) { + index = 4 * tessIdx; + if (clampXY) { + // ceil emulates the behavior of JAVA2D + tessValues[index++] = + PApplet.ceil(x*mm.m00 + y*mm.m01 + z*mm.m02 + mm.m03); + tessValues[index++] = + PApplet.ceil(x*mm.m10 + y*mm.m11 + z*mm.m12 + mm.m13); + } else { + tessValues[index++] = x*mm.m00 + y*mm.m01 + z*mm.m02 + mm.m03; + tessValues[index++] = x*mm.m10 + y*mm.m11 + z*mm.m12 + mm.m13; + } + tessValues[index++] = x*mm.m20 + y*mm.m21 + z*mm.m22 + mm.m23; + tessValues[index ] = x*mm.m30 + y*mm.m31 + z*mm.m32 + mm.m33; + } else { + index = 3 * tessIdx; + tessValues[index++] = x*nm.m00 + y*nm.m10 + z*nm.m20; + tessValues[index++] = x*nm.m01 + y*nm.m11 + z*nm.m21; + tessValues[index ] = x*nm.m02 + y*nm.m12 + z*nm.m22; + } + } + } + } + + // Just copy vertices one by one. + private void copyFewCoords(InGeometry in, int i0, int index, int nvert) { + // Copying elements one by one instead of using arrayCopy is more + // efficient for few vertices... + for (int i = 0; i < nvert; i++) { + int inIdx = i0 + i; + int tessIdx = firstPolyVertex + i; + + index = 3 * inIdx; + float x = in.vertices[index++]; + float y = in.vertices[index++]; + float z = in.vertices[index ]; + + index = 3 * inIdx; + float nx = in.normals[index++]; + float ny = in.normals[index++]; + float nz = in.normals[index ]; + + index = 4 * tessIdx; + polyVertices[index++] = x; + polyVertices[index++] = y; + polyVertices[index++] = z; + polyVertices[index ] = 1; + + index = 3 * tessIdx; + polyNormals[index++] = nx; + polyNormals[index++] = ny; + polyNormals[index ] = nz; + + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + if (attrib.isColor() || attrib.isOther()) continue; + + float[] inValues = in.fattribs.get(name); + index = 3 * inIdx; + x = inValues[index++]; + y = inValues[index++]; + z = inValues[index ]; + + float[] tessValues = fpolyAttribs.get(name); + if (attrib.isPosition()) { + index = 4 * tessIdx; + tessValues[index++] = x; + tessValues[index++] = y; + tessValues[index++] = z; + tessValues[index ] = 1; + } else { + index = 3 * tessIdx; + tessValues[index++] = x; + tessValues[index++] = y; + tessValues[index ] = z; + } + } + } + } + + // Copy many vertices using arrayCopy + private void copyManyCoords(InGeometry in, int i0, int index, int nvert) { + for (int i = 0; i < nvert; i++) { + // Position data needs to be copied in batches of three, because the + // input vertices don't have a w coordinate. + int inIdx = i0 + i; + int tessIdx = firstPolyVertex + i; + PApplet.arrayCopy(in.vertices, 3 * inIdx, + polyVertices, 4 * tessIdx, 3); + polyVertices[4 * tessIdx + 3] = 1; + + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + if (!attrib.isPosition()) continue; + float[] inValues = in.fattribs.get(name); + float[] tessValues = fpolyAttribs.get(name); + PApplet.arrayCopy(inValues, 3 * inIdx, + tessValues, 4 * tessIdx, 3); + tessValues[4 * tessIdx + 3] = 1; + } + } + PApplet.arrayCopy(in.normals, 3 * i0, + polyNormals, 3 * firstPolyVertex, 3 * nvert); + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + if (!attrib.isNormal()) continue; + float[] inValues = in.fattribs.get(name); + float[] tessValues = fpolyAttribs.get(name); + PApplet.arrayCopy(inValues, 3 * i0, + tessValues, 3 * firstPolyVertex, 3 * nvert); + } + } + + // Just copy attributes one by one. + private void copyFewAttribs(InGeometry in, int i0, int index, int nvert) { + for (int i = 0; i < nvert; i++) { + int inIdx = i0 + i; + int tessIdx = firstPolyVertex + i; + + index = 2 * inIdx; + float u = in.texcoords[index++]; + float v = in.texcoords[index ]; + + polyColors[tessIdx] = in.colors[inIdx]; + + index = 2 * tessIdx; + polyTexCoords[index++] = u; + polyTexCoords[index ] = v; + + polyAmbient[tessIdx] = in.ambient[inIdx]; + polySpecular[tessIdx] = in.specular[inIdx]; + polyEmissive[tessIdx] = in.emissive[inIdx]; + polyShininess[tessIdx] = in.shininess[inIdx]; + + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + if (attrib.isPosition() || attrib.isNormal()) continue; + int index0 = attrib.size * inIdx; + int index1 = attrib.size * tessIdx; + if (attrib.isFloat()) { + float[] inValues = in.fattribs.get(name); + float[] tessValues = fpolyAttribs.get(name); + for (int n = 0; n < attrib.size; n++) { + tessValues[index1++] = inValues[index0++]; + } + } else if (attrib.isInt()) { + int[] inValues = in.iattribs.get(name); + int[] tessValues = ipolyAttribs.get(name); + for (int n = 0; n < attrib.size; n++) { + tessValues[index1++] = inValues[index0++]; + } + } else if (attrib.isBool()) { + byte[] inValues = in.battribs.get(name); + byte[] tessValues = bpolyAttribs.get(name); + for (int n = 0; n < attrib.size; n++) { + tessValues[index1++] = inValues[index0++]; + } + } + } + } + } + + // Copy many attributes using arrayCopy() + private void copyManyAttribs(InGeometry in, int i0, int index, int nvert) { + PApplet.arrayCopy(in.colors, i0, + polyColors, firstPolyVertex, nvert); + PApplet.arrayCopy(in.texcoords, 2 * i0, + polyTexCoords, 2 * firstPolyVertex, 2 * nvert); + PApplet.arrayCopy(in.ambient, i0, + polyAmbient, firstPolyVertex, nvert); + PApplet.arrayCopy(in.specular, i0, + polySpecular, firstPolyVertex, nvert); + PApplet.arrayCopy(in.emissive, i0, + polyEmissive, firstPolyVertex, nvert); + PApplet.arrayCopy(in.shininess, i0, + polyShininess, firstPolyVertex, nvert); + + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + if (attrib.isPosition() || attrib.isNormal()) continue; + Object inValues = null; + Object tessValues = null; + if (attrib.isFloat()) { + inValues = in.fattribs.get(name); + tessValues = fpolyAttribs.get(name); + } else if (attrib.isInt()) { + inValues = in.iattribs.get(name); + tessValues = ipolyAttribs.get(name); + } else if (attrib.isBool()) { + inValues = in.battribs.get(name); + tessValues = bpolyAttribs.get(name); + } + PApplet.arrayCopy(inValues, attrib.size * i0, + tessValues, attrib.tessSize * firstPolyVertex, + attrib.size * nvert); + } + } + + // ----------------------------------------------------------------- + // + // Matrix transformations + + void applyMatrixOnPolyGeometry(PMatrix tr, int first, int last) { + if (tr instanceof PMatrix2D) { + applyMatrixOnPolyGeometry((PMatrix2D) tr, first, last); + } else if (tr instanceof PMatrix3D) { + applyMatrixOnPolyGeometry((PMatrix3D) tr, first, last); + } + } + + void applyMatrixOnLineGeometry(PMatrix tr, int first, int last) { + if (tr instanceof PMatrix2D) { + applyMatrixOnLineGeometry((PMatrix2D) tr, first, last); + } else if (tr instanceof PMatrix3D) { + applyMatrixOnLineGeometry((PMatrix3D) tr, first, last); + } + } + + void applyMatrixOnPointGeometry(PMatrix tr, int first, int last) { + if (tr instanceof PMatrix2D) { + applyMatrixOnPointGeometry((PMatrix2D) tr, first, last); + } else if (tr instanceof PMatrix3D) { + applyMatrixOnPointGeometry((PMatrix3D) tr, first, last); + } + } + + void applyMatrixOnPolyGeometry(PMatrix2D tr, int first, int last) { + if (first < last) { + int index; + + for (int i = first; i <= last; i++) { + index = 4 * i; + float x = polyVertices[index++]; + float y = polyVertices[index ]; + + index = 3 * i; + float nx = polyNormals[index++]; + float ny = polyNormals[index ]; + + index = 4 * i; + polyVertices[index++] = x*tr.m00 + y*tr.m01 + tr.m02; + polyVertices[index ] = x*tr.m10 + y*tr.m11 + tr.m12; + + index = 3 * i; + polyNormals[index++] = nx*tr.m00 + ny*tr.m01; + polyNormals[index ] = nx*tr.m10 + ny*tr.m11; + + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + if (attrib.isColor() || attrib.isOther()) continue; + float[] values = fpolyAttribs.get(name); + if (attrib.isPosition()) { + index = 4 * i; + x = values[index++]; + y = values[index ]; + index = 4 * i; + values[index++] = x*tr.m00 + y*tr.m01 + tr.m02; + values[index ] = x*tr.m10 + y*tr.m11 + tr.m12; + } else { + index = 3 * i; + nx = values[index++]; + ny = values[index ]; + index = 3 * i; + values[index++] = nx*tr.m00 + ny*tr.m01; + values[index ] = nx*tr.m10 + ny*tr.m11; + } + } + } + } + } + + void applyMatrixOnLineGeometry(PMatrix2D tr, int first, int last) { + if (first < last) { + int index; + + float scaleFactor = matrixScale(tr); + for (int i = first; i <= last; i++) { + index = 4 * i; + float x = lineVertices[index++]; + float y = lineVertices[index ]; + + index = 4 * i; + float dx = lineDirections[index++]; + float dy = lineDirections[index ]; + + index = 4 * i; + lineVertices[index++] = x*tr.m00 + y*tr.m01 + tr.m02; + lineVertices[index ] = x*tr.m10 + y*tr.m11 + tr.m12; + + index = 4 * i; + lineDirections[index++] = dx*tr.m00 + dy*tr.m01; + lineDirections[index ] = dx*tr.m10 + dy*tr.m11; + lineDirections[index + 2] *= scaleFactor; + } + } + } + + void applyMatrixOnPointGeometry(PMatrix2D tr, int first, int last) { + if (first < last) { + int index; + + float matrixScale = matrixScale(tr); + for (int i = first; i <= last; i++) { + index = 4 * i; + float x = pointVertices[index++]; + float y = pointVertices[index ]; + + index = 4 * i; + pointVertices[index++] = x*tr.m00 + y*tr.m01 + tr.m02; + pointVertices[index ] = x*tr.m10 + y*tr.m11 + tr.m12; + + index = 2 * i; + pointOffsets[index++] *= matrixScale; + pointOffsets[index] *= matrixScale; + } + } + } + + void applyMatrixOnPolyGeometry(PMatrix3D tr, int first, int last) { + if (first < last) { + int index; + + for (int i = first; i <= last; i++) { + index = 4 * i; + float x = polyVertices[index++]; + float y = polyVertices[index++]; + float z = polyVertices[index++]; + float w = polyVertices[index ]; + + index = 3 * i; + float nx = polyNormals[index++]; + float ny = polyNormals[index++]; + float nz = polyNormals[index ]; + + index = 4 * i; + polyVertices[index++] = x*tr.m00 + y*tr.m01 + z*tr.m02 + w*tr.m03; + polyVertices[index++] = x*tr.m10 + y*tr.m11 + z*tr.m12 + w*tr.m13; + polyVertices[index++] = x*tr.m20 + y*tr.m21 + z*tr.m22 + w*tr.m23; + polyVertices[index ] = x*tr.m30 + y*tr.m31 + z*tr.m32 + w*tr.m33; + + index = 3 * i; + polyNormals[index++] = nx*tr.m00 + ny*tr.m01 + nz*tr.m02; + polyNormals[index++] = nx*tr.m10 + ny*tr.m11 + nz*tr.m12; + polyNormals[index ] = nx*tr.m20 + ny*tr.m21 + nz*tr.m22; + + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + if (attrib.isColor() || attrib.isOther()) continue; + float[] values = fpolyAttribs.get(name); + if (attrib.isPosition()) { + index = 4 * i; + x = values[index++]; + y = values[index++]; + z = values[index++]; + w = values[index ]; + index = 4 * i; + values[index++] = x*tr.m00 + y*tr.m01 + z*tr.m02 + w*tr.m03; + values[index++] = x*tr.m10 + y*tr.m11 + z*tr.m12 + w*tr.m13; + values[index++] = x*tr.m20 + y*tr.m21 + z*tr.m22 + w*tr.m23; + values[index ] = x*tr.m30 + y*tr.m31 + z*tr.m32 + w*tr.m33; + } else { + index = 3 * i; + nx = values[index++]; + ny = values[index++]; + nz = values[index ]; + index = 3 * i; + values[index++] = nx*tr.m00 + ny*tr.m01 + nz*tr.m02; + values[index++] = nx*tr.m10 + ny*tr.m11 + nz*tr.m12; + values[index ] = nx*tr.m20 + ny*tr.m21 + nz*tr.m22; + } + } + } + } + } + + void applyMatrixOnLineGeometry(PMatrix3D tr, int first, int last) { + if (first < last) { + int index; + + float scaleFactor = matrixScale(tr); + for (int i = first; i <= last; i++) { + index = 4 * i; + float x = lineVertices[index++]; + float y = lineVertices[index++]; + float z = lineVertices[index++]; + float w = lineVertices[index ]; + + index = 4 * i; + float dx = lineDirections[index++]; + float dy = lineDirections[index++]; + float dz = lineDirections[index ]; + + index = 4 * i; + lineVertices[index++] = x*tr.m00 + y*tr.m01 + z*tr.m02 + w*tr.m03; + lineVertices[index++] = x*tr.m10 + y*tr.m11 + z*tr.m12 + w*tr.m13; + lineVertices[index++] = x*tr.m20 + y*tr.m21 + z*tr.m22 + w*tr.m23; + lineVertices[index ] = x*tr.m30 + y*tr.m31 + z*tr.m32 + w*tr.m33; + + index = 4 * i; + lineDirections[index++] = dx*tr.m00 + dy*tr.m01 + dz*tr.m02; + lineDirections[index++] = dx*tr.m10 + dy*tr.m11 + dz*tr.m12; + lineDirections[index++] = dx*tr.m20 + dy*tr.m21 + dz*tr.m22; + lineDirections[index] *= scaleFactor; + } + } + } + + void applyMatrixOnPointGeometry(PMatrix3D tr, int first, int last) { + if (first < last) { + int index; + + float matrixScale = matrixScale(tr); + for (int i = first; i <= last; i++) { + index = 4 * i; + float x = pointVertices[index++]; + float y = pointVertices[index++]; + float z = pointVertices[index++]; + float w = pointVertices[index ]; + + index = 4 * i; + pointVertices[index++] = x*tr.m00 + y*tr.m01 + z*tr.m02 + w*tr.m03; + pointVertices[index++] = x*tr.m10 + y*tr.m11 + z*tr.m12 + w*tr.m13; + pointVertices[index++] = x*tr.m20 + y*tr.m21 + z*tr.m22 + w*tr.m23; + pointVertices[index ] = x*tr.m30 + y*tr.m31 + z*tr.m32 + w*tr.m33; + + index = 2 * i; + pointOffsets[index++] *= matrixScale; + pointOffsets[index] *= matrixScale; + } + } + } + } + + // Generates tessellated geometry given a batch of input vertices. + static protected class Tessellator { + InGeometry in; + TessGeometry tess; + TexCache texCache; + PImage prevTexImage; + PImage newTexImage; + int firstTexIndex; + int firstTexCache; + + PGL.Tessellator gluTess; + TessellatorCallback callback; + + boolean fill; + boolean stroke; + int strokeColor; + float strokeWeight; + int strokeJoin; + int strokeCap; + boolean accurate2DStrokes; + + PMatrix transform; + float transformScale; + boolean is2D, is3D; + protected PGraphicsOpenGL pg; + + int[] rawIndices; + int rawSize; + int[] dupIndices; + int dupCount; + + int firstPolyIndexCache; + int lastPolyIndexCache; + int firstLineIndexCache; + int lastLineIndexCache; + int firstPointIndexCache; + int lastPointIndexCache; + + // Accessor arrays to get the geometry data needed to tessellate the + // strokes, it can point to either the input geometry, or the internal + // path vertices generated in the polygon discretization. + float[] strokeVertices; + int[] strokeColors; + float[] strokeWeights; + + // Path vertex data that results from discretizing a polygon (i.e.: turning + // bezier, quadratic, and curve vertices into "regular" vertices). + int pathVertexCount; + float[] pathVertices; + int[] pathColors; + float[] pathWeights; + int beginPath; + + public Tessellator() { + rawIndices = new int[512]; + accurate2DStrokes = true; + transform = null; + is2D = false; + is3D = true; + } + + void initGluTess() { + if (gluTess == null) { + callback = new TessellatorCallback(tess.polyAttribs); + gluTess = pg.pgl.createTessellator(callback); + } + } + + void setInGeometry(InGeometry in) { + this.in = in; + + firstPolyIndexCache = -1; + lastPolyIndexCache = -1; + firstLineIndexCache = -1; + lastLineIndexCache = -1; + firstPointIndexCache = -1; + lastPointIndexCache = -1; + } + + void setTessGeometry(TessGeometry tess) { + this.tess = tess; + } + + void setFill(boolean fill) { + this.fill = fill; + } + + void setTexCache(TexCache texCache, PImage newTexImage) { + this.texCache = texCache; + this.newTexImage = newTexImage; + } + + void setStroke(boolean stroke) { + this.stroke = stroke; + } + + void setStrokeColor(int color) { + this.strokeColor = PGL.javaToNativeARGB(color); + } + + void setStrokeWeight(float weight) { + this.strokeWeight = weight; + } + + void setStrokeCap(int strokeCap) { + this.strokeCap = strokeCap; + } + + void setStrokeJoin(int strokeJoin) { + this.strokeJoin = strokeJoin; + } + + void setAccurate2DStrokes(boolean accurate) { + this.accurate2DStrokes = accurate; + } + + protected void setRenderer(PGraphicsOpenGL pg) { + this.pg = pg; + } + + void set3D(boolean value) { + if (value) { + this.is2D = false; + this.is3D = true; + } else { + this.is2D = true; + this.is3D = false; + } + } + + void setTransform(PMatrix transform) { + this.transform = transform; + transformScale = -1; + } + + void resetCurveVertexCount() { + pg.curveVertexCount = 0; + } + + // ----------------------------------------------------------------- + // + // Point tessellation + + void tessellatePoints() { + if (strokeCap == ROUND) { + tessellateRoundPoints(); + } else { + tessellateSquarePoints(); + } + } + + void tessellateRoundPoints() { + int nInVert = in.vertexCount; + if (stroke && 1 <= nInVert) { + // Each point generates a separate triangle fan. + // The number of triangles of each fan depends on the + // stroke weight of the point. + int nPtVert = + PApplet.min(MAX_POINT_ACCURACY, PApplet.max(MIN_POINT_ACCURACY, + (int) (TWO_PI * strokeWeight / + POINT_ACCURACY_FACTOR))) + 1; + if (PGL.MAX_VERTEX_INDEX1 <= nPtVert) { + throw new RuntimeException("Error in point tessellation."); + } + updateTex(); + int nvertTot = nPtVert * nInVert; + int nindTot = 3 * (nPtVert - 1) * nInVert; + if (is3D) { + tessellateRoundPoints3D(nvertTot, nindTot, nPtVert); + } else if (is2D) { + beginNoTex(); + tessellateRoundPoints2D(nvertTot, nindTot, nPtVert); + endNoTex(); + } + } + } + + void tessellateRoundPoints3D(int nvertTot, int nindTot, int nPtVert) { + int perim = nPtVert - 1; + tess.pointVertexCheck(nvertTot); + tess.pointIndexCheck(nindTot); + int vertIdx = tess.firstPointVertex; + int attribIdx = tess.firstPointVertex; + int indIdx = tess.firstPointIndex; + IndexCache cache = tess.pointIndexCache; + int index = in.renderMode == RETAINED ? cache.addNew() : cache.getLast(); + firstPointIndexCache = index; + for (int i = 0; i < in.vertexCount; i++) { + // Creating the triangle fan for each input vertex. + + int count = cache.vertexCount[index]; + if (PGL.MAX_VERTEX_INDEX1 <= count + nPtVert) { + // We need to start a new index block for this point. + index = cache.addNew(); + count = 0; + } + + // All the tessellated vertices are identical to the center point + for (int k = 0; k < nPtVert; k++) { + tess.setPointVertex(vertIdx, in, i); + vertIdx++; + } + + // The attributes for each tessellated vertex are the displacement along + // the circle perimeter. The point shader will read these attributes and + // displace the vertices in screen coordinates so the circles are always + // camera facing (bilboards) + tess.pointOffsets[2 * attribIdx + 0] = 0; + tess.pointOffsets[2 * attribIdx + 1] = 0; + attribIdx++; + float val = 0; + float inc = (float) SINCOS_LENGTH / perim; + for (int k = 0; k < perim; k++) { + tess.pointOffsets[2 * attribIdx + 0] = + 0.5f * cosLUT[(int) val] * transformScale() * strokeWeight; + tess.pointOffsets[2 * attribIdx + 1] = + 0.5f * sinLUT[(int) val] * transformScale() * strokeWeight; + val = (val + inc) % SINCOS_LENGTH; + attribIdx++; + } + + // Adding vert0 to take into account the triangles of all + // the preceding points. + for (int k = 1; k < nPtVert - 1; k++) { + tess.pointIndices[indIdx++] = (short) (count + 0); + tess.pointIndices[indIdx++] = (short) (count + k); + tess.pointIndices[indIdx++] = (short) (count + k + 1); + } + // Final triangle between the last and first point: + tess.pointIndices[indIdx++] = (short) (count + 0); + tess.pointIndices[indIdx++] = (short) (count + 1); + tess.pointIndices[indIdx++] = (short) (count + nPtVert - 1); + + cache.incCounts(index, 3 * (nPtVert - 1), nPtVert); + } + lastPointIndexCache = index; + } + + void tessellateRoundPoints2D(int nvertTot, int nindTot, int nPtVert) { + int perim = nPtVert - 1; + tess.polyVertexCheck(nvertTot); + tess.polyIndexCheck(nindTot); + int vertIdx = tess.firstPolyVertex; + int indIdx = tess.firstPolyIndex; + IndexCache cache = tess.polyIndexCache; + int index = in.renderMode == RETAINED ? cache.addNew() : cache.getLast(); + firstPointIndexCache = index; + if (firstPolyIndexCache == -1) firstPolyIndexCache = index; // If the geometry has no fill, needs the first poly index. + for (int i = 0; i < in.vertexCount; i++) { + int count = cache.vertexCount[index]; + if (PGL.MAX_VERTEX_INDEX1 <= count + nPtVert) { + // We need to start a new index block for this point. + index = cache.addNew(); + count = 0; + } + + float x0 = in.vertices[3 * i + 0]; + float y0 = in.vertices[3 * i + 1]; + int rgba = in.strokeColors[i]; + + float val = 0; + float inc = (float) SINCOS_LENGTH / perim; + tess.setPolyVertex(vertIdx, x0, y0, 0, rgba, false); + vertIdx++; + for (int k = 0; k < perim; k++) { + tess.setPolyVertex(vertIdx, + x0 + 0.5f * cosLUT[(int) val] * strokeWeight, + y0 + 0.5f * sinLUT[(int) val] * strokeWeight, + 0, rgba, false); + vertIdx++; + val = (val + inc) % SINCOS_LENGTH; + } + + // Adding vert0 to take into account the triangles of all + // the preceding points. + for (int k = 1; k < nPtVert - 1; k++) { + tess.polyIndices[indIdx++] = (short) (count + 0); + tess.polyIndices[indIdx++] = (short) (count + k); + tess.polyIndices[indIdx++] = (short) (count + k + 1); + } + // Final triangle between the last and first point: + tess.polyIndices[indIdx++] = (short) (count + 0); + tess.polyIndices[indIdx++] = (short) (count + 1); + tess.polyIndices[indIdx++] = (short) (count + nPtVert - 1); + + cache.incCounts(index, 3 * (nPtVert - 1), nPtVert); + } + lastPointIndexCache = lastPolyIndexCache = index; + } + + void tessellateSquarePoints() { + int nInVert = in.vertexCount; + if (stroke && 1 <= nInVert) { + updateTex(); + int quadCount = nInVert; // Each point generates a separate quad. + // Each quad is formed by 5 vertices, the center one + // is the input vertex, and the other 4 define the + // corners (so, a triangle fan again). + int nvertTot = 5 * quadCount; + // So the quad is formed by 4 triangles, each requires + // 3 indices. + int nindTot = 12 * quadCount; + if (is3D) { + tessellateSquarePoints3D(nvertTot, nindTot); + } else if (is2D) { + beginNoTex(); + tessellateSquarePoints2D(nvertTot, nindTot); + endNoTex(); + } + } + } + + void tessellateSquarePoints3D(int nvertTot, int nindTot) { + tess.pointVertexCheck(nvertTot); + tess.pointIndexCheck(nindTot); + int vertIdx = tess.firstPointVertex; + int attribIdx = tess.firstPointVertex; + int indIdx = tess.firstPointIndex; + IndexCache cache = tess.pointIndexCache; + int index = in.renderMode == RETAINED ? cache.addNew() : cache.getLast(); + firstPointIndexCache = index; + for (int i = 0; i < in.vertexCount; i++) { + int nvert = 5; + int count = cache.vertexCount[index]; + if (PGL.MAX_VERTEX_INDEX1 <= count + nvert) { + // We need to start a new index block for this point. + index = cache.addNew(); + count = 0; + } + + for (int k = 0; k < nvert; k++) { + tess.setPointVertex(vertIdx, in, i); + vertIdx++; + } + + // The attributes for each tessellated vertex are the displacement along + // the quad corners. The point shader will read these attributes and + // displace the vertices in screen coordinates so the quads are always + // camera facing (bilboards) + tess.pointOffsets[2 * attribIdx + 0] = 0; + tess.pointOffsets[2 * attribIdx + 1] = 0; + attribIdx++; + for (int k = 0; k < 4; k++) { + tess.pointOffsets[2 * attribIdx + 0] = + 0.5f * QUAD_POINT_SIGNS[k][0] * transformScale() * strokeWeight; + tess.pointOffsets[2 * attribIdx + 1] = + 0.5f * QUAD_POINT_SIGNS[k][1] * transformScale() * strokeWeight; + attribIdx++; + } + + // Adding firstVert to take into account the triangles of all + // the preceding points. + for (int k = 1; k < nvert - 1; k++) { + tess.pointIndices[indIdx++] = (short) (count + 0); + tess.pointIndices[indIdx++] = (short) (count + k); + tess.pointIndices[indIdx++] = (short) (count + k + 1); + } + // Final triangle between the last and first point: + tess.pointIndices[indIdx++] = (short) (count + 0); + tess.pointIndices[indIdx++] = (short) (count + 1); + tess.pointIndices[indIdx++] = (short) (count + nvert - 1); + + cache.incCounts(index, 12, 5); + } + lastPointIndexCache = index; + } + + void tessellateSquarePoints2D(int nvertTot, int nindTot) { + tess.polyVertexCheck(nvertTot); + tess.polyIndexCheck(nindTot); + boolean clamp = clampSquarePoints2D(); + int vertIdx = tess.firstPolyVertex; + int indIdx = tess.firstPolyIndex; + IndexCache cache = tess.polyIndexCache; + int index = in.renderMode == RETAINED ? cache.addNew() : cache.getLast(); + firstPointIndexCache = index; + if (firstPolyIndexCache == -1) firstPolyIndexCache = index; // If the geometry has no fill, needs the first poly index. + for (int i = 0; i < in.vertexCount; i++) { + int nvert = 5; + int count = cache.vertexCount[index]; + if (PGL.MAX_VERTEX_INDEX1 <= count + nvert) { + // We need to start a new index block for this point. + index = cache.addNew(); + count = 0; + } + + float x0 = in.vertices[3 * i + 0]; + float y0 = in.vertices[3 * i + 1]; + int rgba = in.strokeColors[i]; + + tess.setPolyVertex(vertIdx, x0, y0, 0, rgba, clamp); + vertIdx++; + for (int k = 0; k < nvert - 1; k++) { + tess.setPolyVertex(vertIdx, + x0 + 0.5f * QUAD_POINT_SIGNS[k][0] * strokeWeight, + y0 + 0.5f * QUAD_POINT_SIGNS[k][1] * strokeWeight, + 0, rgba, clamp); + vertIdx++; + } + + for (int k = 1; k < nvert - 1; k++) { + tess.polyIndices[indIdx++] = (short) (count + 0); + tess.polyIndices[indIdx++] = (short) (count + k); + tess.polyIndices[indIdx++] = (short) (count + k + 1); + } + // Final triangle between the last and first point: + tess.polyIndices[indIdx++] = (short) (count + 0); + tess.polyIndices[indIdx++] = (short) (count + 1); + tess.polyIndices[indIdx++] = (short) (count + nvert - 1); + + cache.incCounts(index, 12, 5); + } + lastPointIndexCache = lastPolyIndexCache = index; + } + + boolean clamp2D() { + return is2D && tess.renderMode == IMMEDIATE && + zero(pg.modelview.m01) && zero(pg.modelview.m10); + } + + boolean clampSquarePoints2D() { + return clamp2D(); + } + + // ----------------------------------------------------------------- + // + // Line tessellation + + void tessellateLines() { + int nInVert = in.vertexCount; + if (stroke && 2 <= nInVert) { + strokeVertices = in.vertices; + strokeColors = in.strokeColors; + strokeWeights = in.strokeWeights; + updateTex(); + int lineCount = nInVert / 2; // Each individual line is formed by two consecutive input vertices. + if (is3D) { + tessellateLines3D(lineCount); + } else if (is2D) { + beginNoTex(); // Line geometry in 2D are stored in the poly array next to the fill triangles, but w/out textures. + tessellateLines2D(lineCount); + endNoTex(); + } + } + } + + void tessellateLines3D(int lineCount) { + // Lines are made up of 4 vertices defining the quad. + int nvert = lineCount * 4; + // Each stroke line has 4 vertices, defining 2 triangles, which + // require 3 indices to specify their connectivities. + int nind = lineCount * 2 * 3; + + int vcount0 = tess.lineVertexCount; + int icount0 = tess.lineIndexCount; + tess.lineVertexCheck(nvert); + tess.lineIndexCheck(nind); + int index = in.renderMode == RETAINED ? tess.lineIndexCache.addNew() : + tess.lineIndexCache.getLast(); + firstLineIndexCache = index; + int[] tmp = {0, 0}; + tess.lineIndexCache.setCounter(tmp); + for (int ln = 0; ln < lineCount; ln++) { + int i0 = 2 * ln + 0; + int i1 = 2 * ln + 1; + index = addLineSegment3D(i0, i1, i0 - 2, i1 - 1, index, null, false); + } + // Adjust counts of line vertices and indices to exact values + tess.lineIndexCache.setCounter(null); + tess.lineIndexCount = icount0 + tmp[0]; + tess.lineVertexCount = vcount0 + tmp[1]; + lastLineIndexCache = index; + } + + void tessellateLines2D(int lineCount) { + int nvert = lineCount * 4; + int nind = lineCount * 2 * 3; + + if (noCapsJoins(nvert)) { + tess.polyVertexCheck(nvert); + tess.polyIndexCheck(nind); + int index = in.renderMode == RETAINED ? tess.polyIndexCache.addNew() : + tess.polyIndexCache.getLast(); + firstLineIndexCache = index; + if (firstPolyIndexCache == -1) firstPolyIndexCache = index; // If the geometry has no fill, needs the first poly index. + boolean clamp = clampLines2D(lineCount); + for (int ln = 0; ln < lineCount; ln++) { + int i0 = 2 * ln + 0; + int i1 = 2 * ln + 1; + index = addLineSegment2D(i0, i1, index, false, clamp); + } + lastLineIndexCache = lastPolyIndexCache = index; + } else { // full stroking algorithm + LinePath path = new LinePath(LinePath.WIND_NON_ZERO); + for (int ln = 0; ln < lineCount; ln++) { + int i0 = 2 * ln + 0; + int i1 = 2 * ln + 1; + path.moveTo(in.vertices[3 * i0 + 0], in.vertices[3 * i0 + 1], + in.strokeColors[i0]); + path.lineTo(in.vertices[3 * i1 + 0], in.vertices[3 * i1 + 1], + in.strokeColors[i1]); + } + tessellateLinePath(path); + } + } + + boolean clampLines2D(int lineCount) { + boolean res = clamp2D(); + if (res) { + for (int ln = 0; ln < lineCount; ln++) { + int i0 = 2 * ln + 0; + int i1 = 2 * ln + 1; + res = segmentIsAxisAligned(i0, i1); + if (!res) break; + } + } + return res; + } + + void tessellateLineStrip() { + int nInVert = in.vertexCount; + if (stroke && 2 <= nInVert) { + strokeVertices = in.vertices; + strokeColors = in.strokeColors; + strokeWeights = in.strokeWeights; + updateTex(); + int lineCount = nInVert - 1; + if (is3D) { + tessellateLineStrip3D(lineCount); + } else if (is2D) { + beginNoTex(); + tessellateLineStrip2D(lineCount); + endNoTex(); + } + } + } + + void tessellateLineStrip3D(int lineCount) { + int nBevelTr = noCapsJoins() ? 0 : (lineCount - 1); + int nvert = lineCount * 4 + nBevelTr * 3; + int nind = lineCount * 2 * 3 + nBevelTr * 2 * 3; + + int vcount0 = tess.lineVertexCount; + int icount0 = tess.lineIndexCount; + tess.lineVertexCheck(nvert); + tess.lineIndexCheck(nind); + int index = in.renderMode == RETAINED ? tess.lineIndexCache.addNew() : + tess.lineIndexCache.getLast(); + firstLineIndexCache = index; + int i0 = 0; + short[] lastInd = {-1, -1}; + int[] tmp = {0, 0}; + tess.lineIndexCache.setCounter(tmp); + for (int ln = 0; ln < lineCount; ln++) { + int i1 = ln + 1; + if (0 < nBevelTr) { + index = addLineSegment3D(i0, i1, i1 - 2, i1 - 1, index, lastInd, false); + } else { + index = addLineSegment3D(i0, i1, i1 - 2, i1 - 1, index, null, false); + } + i0 = i1; + } + // Adjust counts of line vertices and indices to exact values + tess.lineIndexCache.setCounter(null); + tess.lineIndexCount = icount0 + tmp[0]; + tess.lineVertexCount = vcount0 + tmp[1]; + lastLineIndexCache = index; + } + + void tessellateLineStrip2D(int lineCount) { + int nvert = lineCount * 4; + int nind = lineCount * 2 * 3; + + if (noCapsJoins(nvert)) { + tess.polyVertexCheck(nvert); + tess.polyIndexCheck(nind); + int index = in.renderMode == RETAINED ? tess.polyIndexCache.addNew() : + tess.polyIndexCache.getLast(); + firstLineIndexCache = index; + if (firstPolyIndexCache == -1) firstPolyIndexCache = index; // If the geometry has no fill, needs the first poly index. + int i0 = 0; + boolean clamp = clampLineStrip2D(lineCount); + for (int ln = 0; ln < lineCount; ln++) { + int i1 = ln + 1; + index = addLineSegment2D(i0, i1, index, false, clamp); + i0 = i1; + } + lastLineIndexCache = lastPolyIndexCache = index; + } else { // full stroking algorithm + LinePath path = new LinePath(LinePath.WIND_NON_ZERO); + path.moveTo(in.vertices[0], in.vertices[1], in.strokeColors[0]); + for (int ln = 0; ln < lineCount; ln++) { + int i1 = ln + 1; + path.lineTo(in.vertices[3 * i1 + 0], in.vertices[3 * i1 + 1], + in.strokeColors[i1]); + } + tessellateLinePath(path); + } + } + + boolean clampLineStrip2D(int lineCount) { + boolean res = clamp2D(); + if (res) { + for (int ln = 0; ln < lineCount; ln++) { + res = segmentIsAxisAligned(0, ln + 1); + if (!res) break; + } + } + return res; + } + + void tessellateLineLoop() { + int nInVert = in.vertexCount; + if (stroke && 2 <= nInVert) { + strokeVertices = in.vertices; + strokeColors = in.strokeColors; + strokeWeights = in.strokeWeights; + updateTex(); + int lineCount = nInVert; + if (is3D) { + tessellateLineLoop3D(lineCount); + } else if (is2D) { + beginNoTex(); + tessellateLineLoop2D(lineCount); + endNoTex(); + } + } + } + + void tessellateLineLoop3D(int lineCount) { + int nBevelTr = noCapsJoins() ? 0 : lineCount; + int nvert = lineCount * 4 + nBevelTr * 3; + int nind = lineCount * 2 * 3 + nBevelTr * 2 * 3; + + int vcount0 = tess.lineVertexCount; + int icount0 = tess.lineIndexCount; + tess.lineVertexCheck(nvert); + tess.lineIndexCheck(nind); + int index = in.renderMode == RETAINED ? tess.lineIndexCache.addNew() : + tess.lineIndexCache.getLast(); + firstLineIndexCache = index; + int i0 = 0; + int i1 = -1; + short[] lastInd = {-1, -1}; + int[] tmp = {0, 0}; + tess.lineIndexCache.setCounter(tmp); + for (int ln = 0; ln < lineCount - 1; ln++) { + i1 = ln + 1; + if (0 < nBevelTr) { + index = addLineSegment3D(i0, i1, i1 - 2, i1 - 1, index, lastInd, false); + } else { + index = addLineSegment3D(i0, i1, i1 - 2, i1 - 1, index, null, false); + } + i0 = i1; + } + index = addLineSegment3D(in.vertexCount - 1, 0, i1 - 2, i1 - 1, index, lastInd, false); + if (0 < nBevelTr) { + index = addBevel3D(0, 1, in.vertexCount - 1, 0, index, lastInd, false); + } + // Adjust counts of line vertices and indices to exact values + tess.lineIndexCache.setCounter(null); + tess.lineIndexCount = icount0 + tmp[0]; + tess.lineVertexCount = vcount0 + tmp[1]; + lastLineIndexCache = index; + } + + void tessellateLineLoop2D(int lineCount) { + int nvert = lineCount * 4; + int nind = lineCount * 2 * 3; + + if (noCapsJoins(nvert)) { + tess.polyVertexCheck(nvert); + tess.polyIndexCheck(nind); + int index = in.renderMode == RETAINED ? tess.polyIndexCache.addNew() : + tess.polyIndexCache.getLast(); + firstLineIndexCache = index; + if (firstPolyIndexCache == -1) firstPolyIndexCache = index; // If the geometry has no fill, needs the first poly index. + int i0 = 0; + boolean clamp = clampLineLoop2D(lineCount); + for (int ln = 0; ln < lineCount - 1; ln++) { + int i1 = ln + 1; + index = addLineSegment2D(i0, i1, index, false, clamp); + i0 = i1; + } + index = addLineSegment2D(0, in.vertexCount - 1, index, false, clamp); + lastLineIndexCache = lastPolyIndexCache = index; + } else { // full stroking algorithm + LinePath path = new LinePath(LinePath.WIND_NON_ZERO); + path.moveTo(in.vertices[0], in.vertices[1], in.strokeColors[0]); + for (int ln = 0; ln < lineCount - 1; ln++) { + int i1 = ln + 1; + path.lineTo(in.vertices[3 * i1 + 0], in.vertices[3 * i1 + 1], + in.strokeColors[i1]); + } + path.closePath(); + tessellateLinePath(path); + } + } + + boolean clampLineLoop2D(int lineCount) { + boolean res = clamp2D(); + if (res) { + for (int ln = 0; ln < lineCount; ln++) { + res = segmentIsAxisAligned(0, ln + 1); + if (!res) break; + } + } + return res; + } + + void tessellateEdges() { + if (stroke) { + if (in.edgeCount == 0) return; + strokeVertices = in.vertices; + strokeColors = in.strokeColors; + strokeWeights = in.strokeWeights; + if (is3D) { + tessellateEdges3D(); + } else if (is2D) { + beginNoTex(); + tessellateEdges2D(); + endNoTex(); + } + } + } + + void tessellateEdges3D() { + boolean bevel = !noCapsJoins(); + int nInVert = in.getNumEdgeVertices(bevel); + int nInInd = in.getNumEdgeIndices(bevel); + + int vcount0 = tess.lineVertexCount; + int icount0 = tess.lineIndexCount; + tess.lineVertexCheck(nInVert); + tess.lineIndexCheck(nInInd); + int index = in.renderMode == RETAINED ? tess.lineIndexCache.addNew() : + tess.lineIndexCache.getLast(); + firstLineIndexCache = index; + int fi0 = 0; + int fi1 = 0; + short[] lastInd = {-1, -1}; + int pi0 = -1; + int pi1 = -1; + + int[] tmp = {0, 0}; + tess.lineIndexCache.setCounter(tmp); + for (int i = 0; i <= in.edgeCount - 1; i++) { + int[] edge = in.edges[i]; + int i0 = edge[0]; + int i1 = edge[1]; + if (bevel) { + if (edge[2] == EDGE_CLOSE) { + index = addBevel3D(fi0, fi1, pi0, pi1, index, lastInd, false); + } else { + index = addLineSegment3D(i0, i1, pi0, pi1, index, lastInd, false); + } + } else if (edge[2] != EDGE_CLOSE) { + index = addLineSegment3D(i0, i1, pi0, pi1, index, null, false); + } + if (edge[2] == EDGE_START) { + fi0 = i0; + fi1 = i1; + } + + if (edge[2] == EDGE_STOP || edge[2] == EDGE_SINGLE || edge[2] == EDGE_CLOSE) { + // No join with next line segment. + lastInd[0] = lastInd[1] = -1; + pi1 = pi0 = -1; + } else { + pi0 = i0; + pi1 = i1; + } + } + // Adjust counts of line vertices and indices to exact values + tess.lineIndexCache.setCounter(null); + tess.lineIndexCount = icount0 + tmp[0]; + tess.lineVertexCount = vcount0 + tmp[1]; + + lastLineIndexCache = index; + } + + void tessellateEdges2D() { + int nInVert = in.getNumEdgeVertices(false); + if (noCapsJoins(nInVert)) { + int nInInd = in.getNumEdgeIndices(false); + + tess.polyVertexCheck(nInVert); + tess.polyIndexCheck(nInInd); + int index = in.renderMode == RETAINED ? tess.polyIndexCache.addNew() : + tess.polyIndexCache.getLast(); + firstLineIndexCache = index; + if (firstPolyIndexCache == -1) firstPolyIndexCache = index; // If the geometry has no fill, needs the first poly index. + boolean clamp = clampEdges2D(); + for (int i = 0; i <= in.edgeCount - 1; i++) { + int[] edge = in.edges[i]; + if (edge[2] == EDGE_CLOSE) continue; // ignoring edge closures when not doing caps or joins. + int i0 = edge[0]; + int i1 = edge[1]; + index = addLineSegment2D(i0, i1, index, false, clamp); + } + lastLineIndexCache = lastPolyIndexCache = index; + } else { // full stroking algorithm + LinePath path = new LinePath(LinePath.WIND_NON_ZERO); + for (int i = 0; i <= in.edgeCount - 1; i++) { + int[] edge = in.edges[i]; + int i0 = edge[0]; + int i1 = edge[1]; + switch (edge[2]) { + case EDGE_MIDDLE: + path.lineTo(strokeVertices[3 * i1 + 0], strokeVertices[3 * i1 + 1], + strokeColors[i1]); + break; + case EDGE_START: + path.moveTo(strokeVertices[3 * i0 + 0], strokeVertices[3 * i0 + 1], + strokeColors[i0]); + path.lineTo(strokeVertices[3 * i1 + 0], strokeVertices[3 * i1 + 1], + strokeColors[i1]); + break; + case EDGE_STOP: + path.lineTo(strokeVertices[3 * i1 + 0], strokeVertices[3 * i1 + 1], + strokeColors[i1]); + path.moveTo(strokeVertices[3 * i1 + 0], strokeVertices[3 * i1 + 1], + strokeColors[i1]); + break; + case EDGE_SINGLE: + path.moveTo(strokeVertices[3 * i0 + 0], strokeVertices[3 * i0 + 1], + strokeColors[i0]); + path.lineTo(strokeVertices[3 * i1 + 0], strokeVertices[3 * i1 + 1], + strokeColors[i1]); + path.moveTo(strokeVertices[3 * i1 + 0], strokeVertices[3 * i1 + 1], + strokeColors[i1]); + break; + case EDGE_CLOSE: + path.closePath(); + break; + } + } + tessellateLinePath(path); + } + } + + boolean clampEdges2D() { + boolean res = clamp2D(); + if (res) { + for (int i = 0; i <= in.edgeCount - 1; i++) { + int[] edge = in.edges[i]; + if (edge[2] == EDGE_CLOSE) continue; + int i0 = edge[0]; + int i1 = edge[1]; + res = segmentIsAxisAligned(strokeVertices, i0, i1); + if (!res) break; + } + } + return res; + } + + // Adding the data that defines a quad starting at vertex i0 and + // ending at i1. + int addLineSegment3D(int i0, int i1, int pi0, int pi1, int index, short[] lastInd, + boolean constStroke) { + IndexCache cache = tess.lineIndexCache; + int count = cache.vertexCount[index]; + boolean addBevel = lastInd != null && -1 < lastInd[0] && -1 < lastInd[1]; + boolean newCache = false; + if (PGL.MAX_VERTEX_INDEX1 <= count + 4 + (addBevel ? 1 : 0)) { + // We need to start a new index block for this line. + index = cache.addNew(); + count = 0; + newCache = true; + } + int iidx = cache.indexOffset[index] + cache.indexCount[index]; + int vidx = cache.vertexOffset[index] + cache.vertexCount[index]; + int color, color0; + float weight; + + color0 = color = constStroke ? strokeColor : strokeColors[i0]; + weight = constStroke ? strokeWeight : strokeWeights[i0]; + weight *= transformScale(); + + tess.setLineVertex(vidx++, strokeVertices, i0, i1, color, +weight/2); + tess.lineIndices[iidx++] = (short) (count + 0); + + tess.setLineVertex(vidx++, strokeVertices, i0, i1, color, -weight/2); + tess.lineIndices[iidx++] = (short) (count + 1); + + color = constStroke ? strokeColor : strokeColors[i1]; + weight = constStroke ? strokeWeight : strokeWeights[i1]; + weight *= transformScale(); + + tess.setLineVertex(vidx++, strokeVertices, i1, i0, color, -weight/2); + tess.lineIndices[iidx++] = (short) (count + 2); + + // Starting a new triangle re-using prev vertices. + tess.lineIndices[iidx++] = (short) (count + 2); + tess.lineIndices[iidx++] = (short) (count + 1); + + tess.setLineVertex(vidx++, strokeVertices, i1, i0, color, +weight/2); + tess.lineIndices[iidx++] = (short) (count + 3); + + cache.incCounts(index, 6, 4); + + if (lastInd != null) { + if (-1 < lastInd[0] && -1 < lastInd[1]) { + // Adding bevel triangles + if (newCache) { + if (-1 < pi0 && -1 < pi1) { + // Vertices used in the previous cache need to be copied to the + // newly created one + color = constStroke ? strokeColor : strokeColors[pi0]; + weight = constStroke ? strokeWeight : strokeWeights[pi0]; + weight *= transformScale(); + + tess.setLineVertex(vidx++, strokeVertices, pi1, color); + tess.setLineVertex(vidx++, strokeVertices, pi1, pi0, color, -weight/2); // count+2 vert from previous block + tess.setLineVertex(vidx, strokeVertices, pi1, pi0, color, +weight/2); // count+3 vert from previous block + + tess.lineIndices[iidx++] = (short) (count + 4); + tess.lineIndices[iidx++] = (short) (count + 5); + tess.lineIndices[iidx++] = (short) (count + 0); + + tess.lineIndices[iidx++] = (short) (count + 4); + tess.lineIndices[iidx++] = (short) (count + 6); + tess.lineIndices[iidx ] = (short) (count + 1); + + cache.incCounts(index, 6, 3); + } + } else { + tess.setLineVertex(vidx, strokeVertices, i0, color0); + + tess.lineIndices[iidx++] = (short) (count + 4); + tess.lineIndices[iidx++] = lastInd[0]; + tess.lineIndices[iidx++] = (short) (count + 0); + + tess.lineIndices[iidx++] = (short) (count + 4); + tess.lineIndices[iidx++] = lastInd[1]; + tess.lineIndices[iidx ] = (short) (count + 1); + + cache.incCounts(index, 6, 1); + } + } + + // The last two vertices of the segment will be used in the next + // bevel triangle + lastInd[0] = (short) (count + 2); + lastInd[1] = (short) (count + 3); + } + return index; + } + + int addBevel3D(int fi0, int fi1, int pi0 ,int pi1, int index, short[] lastInd, + boolean constStroke) { + IndexCache cache = tess.lineIndexCache; + int count = cache.vertexCount[index]; + boolean newCache = false; + if (PGL.MAX_VERTEX_INDEX1 <= count + 3) { + // We need to start a new index block for this line. + index = cache.addNew(); + count = 0; + newCache = true; + } + + int iidx = cache.indexOffset[index] + cache.indexCount[index]; + int vidx = cache.vertexOffset[index] + cache.vertexCount[index]; + int color = constStroke ? strokeColor : strokeColors[fi0]; + float weight = constStroke ? strokeWeight : strokeWeights[fi0]; + weight *= transformScale(); + + tess.setLineVertex(vidx++, strokeVertices, fi0, color); + tess.setLineVertex(vidx++, strokeVertices, fi0, fi1, color, +weight/2); + tess.setLineVertex(vidx++, strokeVertices, fi0, fi1, color, -weight/2); + + int extra = 0; + if (newCache && -1 < pi0 && -1 < pi1) { + // Vertices used in the previous cache need to be copied to the + // newly created one + color = constStroke ? strokeColor : strokeColors[pi1]; + weight = constStroke ? strokeWeight : strokeWeights[pi1]; + weight *= transformScale(); + + tess.setLineVertex(vidx++, strokeVertices, pi1, pi0, color, -weight/2); + tess.setLineVertex(vidx , strokeVertices, pi1, pi0, color, +weight/2); + + lastInd[0] = (short) (count + 3); + lastInd[1] = (short) (count + 4); + extra = 2; + } + + tess.lineIndices[iidx++] = (short) (count + 0); + tess.lineIndices[iidx++] = lastInd[0]; + tess.lineIndices[iidx++] = (short) (count + 1); + + tess.lineIndices[iidx++] = (short) (count + 0); + tess.lineIndices[iidx++] = (short) (count + 2); + tess.lineIndices[iidx ] = lastInd[1]; + + cache.incCounts(index, 6, 3 + extra); + + return index; + } + + // Adding the data that defines a quad starting at vertex i0 and + // ending at i1, in the case of pure 2D renderers (line geometry + // is added to the poly arrays). + int addLineSegment2D(int i0, int i1, int index, + boolean constStroke, boolean clamp) { + IndexCache cache = tess.polyIndexCache; + int count = cache.vertexCount[index]; + if (PGL.MAX_VERTEX_INDEX1 <= count + 4) { + // We need to start a new index block for this line. + index = cache.addNew(); + count = 0; + } + int iidx = cache.indexOffset[index] + cache.indexCount[index]; + int vidx = cache.vertexOffset[index] + cache.vertexCount[index]; + + int color = constStroke ? strokeColor : strokeColors[i0]; + float weight = constStroke ? strokeWeight : strokeWeights[i0]; + if (subPixelStroke(weight)) clamp = false; + + float x0 = strokeVertices[3 * i0 + 0]; + float y0 = strokeVertices[3 * i0 + 1]; + + float x1 = strokeVertices[3 * i1 + 0]; + float y1 = strokeVertices[3 * i1 + 1]; + + // Calculating direction and normal of the line. + float dirx = x1 - x0; + float diry = y1 - y0; + float llen = PApplet.sqrt(dirx * dirx + diry * diry); + float normx = 0, normy = 0; + float dirdx = 0, dirdy = 0; + if (nonZero(llen)) { + normx = -diry / llen; + normy = +dirx / llen; + + // Displacement along the direction of the line to force rounding to next + // integer and so making sure that no pixels are missing, some relevant + // links: + // http://stackoverflow.com/questions/10040961/opengl-pixel-perfect-2d-drawing + // http://msdn.microsoft.com/en-us/library/dd374282(VS.85) + dirdx = (dirx / llen) * PApplet.min(0.75f, weight/2); + dirdy = (diry / llen) * PApplet.min(0.75f, weight/2); + } + + float normdx = normx * weight/2; + float normdy = normy * weight/2; + + tess.setPolyVertex(vidx++, x0 + normdx - dirdx, y0 + normdy - dirdy, + 0, color, clamp); + tess.polyIndices[iidx++] = (short) (count + 0); + + tess.setPolyVertex(vidx++, x0 - normdx - dirdx, y0 - normdy - dirdy, + 0, color, clamp); + tess.polyIndices[iidx++] = (short) (count + 1); + + if (clamp) { + // Check for degeneracy due to coordinate clamping + float xac = tess.polyVertices[4 * (vidx - 2) + 0]; + float yac = tess.polyVertices[4 * (vidx - 2) + 1]; + float xbc = tess.polyVertices[4 * (vidx - 1) + 0]; + float ybc = tess.polyVertices[4 * (vidx - 1) + 1]; + if (same(xac, xbc) && same(yac, ybc)) { + unclampLine2D(vidx - 2, x0 + normdx - dirdx, y0 + normdy - dirdy); + unclampLine2D(vidx - 1, x0 - normdx - dirdx, y0 - normdy - dirdy); + } + } + + if (!constStroke) { + color = strokeColors[i1]; + weight = strokeWeights[i1]; + normdx = normx * weight/2; + normdy = normy * weight/2; + if (subPixelStroke(weight)) clamp = false; + } + + tess.setPolyVertex(vidx++, x1 - normdx + dirdx, y1 - normdy + dirdy, + 0, color, clamp); + tess.polyIndices[iidx++] = (short) (count + 2); + + // Starting a new triangle re-using prev vertices. + tess.polyIndices[iidx++] = (short) (count + 2); + tess.polyIndices[iidx++] = (short) (count + 0); + + tess.setPolyVertex(vidx++, x1 + normdx + dirdx, y1 + normdy + dirdy, + 0, color, clamp); + tess.polyIndices[iidx++] = (short) (count + 3); + + if (clamp) { + // Check for degeneracy due to coordinate clamping + float xac = tess.polyVertices[4 * (vidx - 2) + 0]; + float yac = tess.polyVertices[4 * (vidx - 2) + 1]; + float xbc = tess.polyVertices[4 * (vidx - 1) + 0]; + float ybc = tess.polyVertices[4 * (vidx - 1) + 1]; + if (same(xac, xbc) && same(yac, ybc)) { + unclampLine2D(vidx - 2, x1 - normdx + dirdx, y1 - normdy + dirdy); + unclampLine2D(vidx - 1, x1 + normdx + dirdx, y1 + normdy + dirdy); + } + } + + cache.incCounts(index, 6, 4); + return index; + } + + void unclampLine2D(int tessIdx, float x, float y) { + PMatrix3D mm = pg.modelview; + int index = 4 * tessIdx; + tess.polyVertices[index++] = x*mm.m00 + y*mm.m01 + mm.m03; + tess.polyVertices[index++] = x*mm.m10 + y*mm.m11 + mm.m13; + } + + boolean noCapsJoins(int nInVert) { + if (!accurate2DStrokes) { + return true; + } else if (PGL.MAX_CAPS_JOINS_LENGTH <= nInVert) { + // The line path is too long, so it could make the GLU tess + // to run out of memory, so full caps and joins are disabled. + return true; + } else { + return noCapsJoins(); + } + } + + boolean subPixelStroke(float weight) { + float sw = transformScale() * weight; + return PApplet.abs(sw - (int)sw) > 0; + } + + boolean noCapsJoins() { + // The stroke weight is scaled so it corresponds to the current + // "zoom level" being applied on the geometry due to scaling: + return tess.renderMode == IMMEDIATE && + transformScale() * strokeWeight < PGL.MIN_CAPS_JOINS_WEIGHT; + } + + float transformScale() { + if (-1 < transformScale) return transformScale; + return transformScale = matrixScale(transform); + } + + boolean segmentIsAxisAligned(int i0, int i1) { + return zero(in.vertices[3 * i0 + 0] - in.vertices[3 * i1 + 0]) || + zero(in.vertices[3 * i0 + 1] - in.vertices[3 * i1 + 1]); + } + + boolean segmentIsAxisAligned(float[] vertices, int i0, int i1) { + return zero(vertices[3 * i0 + 0] - vertices[3 * i1 + 0]) || + zero(vertices[3 * i0 + 1] - vertices[3 * i1 + 1]); + } + + // ----------------------------------------------------------------- + // + // Polygon primitives tessellation + + void tessellateTriangles() { + beginTex(); + int nTri = in.vertexCount / 3; + if (fill && 1 <= nTri) { + int nInInd = 3 * nTri; + setRawSize(nInInd); + int idx = 0; + boolean clamp = clampTriangles(); + for (int i = 0; i < 3 * nTri; i++) { + rawIndices[idx++] = i; + } + splitRawIndices(clamp); + } + endTex(); + tessellateEdges(); + } + + boolean clampTriangles() { + boolean res = clamp2D(); + if (res) { + int nTri = in.vertexCount / 3; + for (int i = 0; i < nTri; i++) { + int i0 = 3 * i + 0; + int i1 = 3 * i + 1; + int i2 = 3 * i + 2; + int count = 0; + if (segmentIsAxisAligned(i0, i1)) count++; + if (segmentIsAxisAligned(i0, i2)) count++; + if (segmentIsAxisAligned(i1, i2)) count++; + res = 1 < count; + if (!res) break; + } + } + return res; + } + + void tessellateTriangles(int[] indices) { + beginTex(); + int nInVert = in.vertexCount; + if (fill && 3 <= nInVert) { + int nInInd = indices.length; + setRawSize(nInInd); + PApplet.arrayCopy(indices, rawIndices, nInInd); + boolean clamp = clampTriangles(indices); + splitRawIndices(clamp); + } + endTex(); + tessellateEdges(); + } + + boolean clampTriangles(int[] indices) { + boolean res = clamp2D(); + if (res) { + int nTri = indices.length; + for (int i = 0; i < nTri; i++) { + int i0 = indices[3 * i + 0]; + int i1 = indices[3 * i + 1]; + int i2 = indices[3 * i + 2]; + int count = 0; + if (segmentIsAxisAligned(i0, i1)) count++; + if (segmentIsAxisAligned(i0, i2)) count++; + if (segmentIsAxisAligned(i1, i2)) count++; + res = 1 < count; + if (!res) break; + } + } + return res; + } + + void tessellateTriangleFan() { + beginTex(); + int nInVert = in.vertexCount; + if (fill && 3 <= nInVert) { + int nInInd = 3 * (nInVert - 2); + setRawSize(nInInd); + int idx = 0; + boolean clamp = clampTriangleFan(); + for (int i = 1; i < in.vertexCount - 1; i++) { + rawIndices[idx++] = 0; + rawIndices[idx++] = i; + rawIndices[idx++] = i + 1; + } + splitRawIndices(clamp); + } + endTex(); + tessellateEdges(); + } + + boolean clampTriangleFan() { + boolean res = clamp2D(); + if (res) { + for (int i = 1; i < in.vertexCount - 1; i++) { + int i0 = 0; + int i1 = i; + int i2 = i + 1; + int count = 0; + if (segmentIsAxisAligned(i0, i1)) count++; + if (segmentIsAxisAligned(i0, i2)) count++; + if (segmentIsAxisAligned(i1, i2)) count++; + res = 1 < count; + if (!res) break; + } + } + return res; + } + + void tessellateTriangleStrip() { + beginTex(); + int nInVert = in.vertexCount; + if (fill && 3 <= nInVert) { + int nInInd = 3 * (nInVert - 2); + setRawSize(nInInd); + int idx = 0; + boolean clamp = clampTriangleStrip(); + for (int i = 1; i < in.vertexCount - 1; i++) { + rawIndices[idx++] = i; + if (i % 2 == 0) { + rawIndices[idx++] = i - 1; + rawIndices[idx++] = i + 1; + } else { + rawIndices[idx++] = i + 1; + rawIndices[idx++] = i - 1; + } + } + splitRawIndices(clamp); + } + endTex(); + tessellateEdges(); + } + + boolean clampTriangleStrip() { + boolean res = clamp2D(); + if (res) { + for (int i = 1; i < in.vertexCount - 1; i++) { + int i0 = i; + int i1, i2; + if (i % 2 == 0) { + i1 = i - 1; + i2 = i + 1; + } else { + i1 = i + 1; + i2 = i - 1; + } + int count = 0; + if (segmentIsAxisAligned(i0, i1)) count++; + if (segmentIsAxisAligned(i0, i2)) count++; + if (segmentIsAxisAligned(i1, i2)) count++; + res = 1 < count; + if (!res) break; + } + } + return res; + } + + void tessellateQuads() { + beginTex(); + int quadCount = in.vertexCount / 4; + if (fill && 1 <= quadCount) { + int nInInd = 6 * quadCount; + setRawSize(nInInd); + int idx = 0; + boolean clamp = clampQuads(quadCount); + for (int qd = 0; qd < quadCount; qd++) { + int i0 = 4 * qd + 0; + int i1 = 4 * qd + 1; + int i2 = 4 * qd + 2; + int i3 = 4 * qd + 3; + + rawIndices[idx++] = i0; + rawIndices[idx++] = i1; + rawIndices[idx++] = i2; + + rawIndices[idx++] = i2; + rawIndices[idx++] = i3; + rawIndices[idx++] = i0; + } + splitRawIndices(clamp); + } + endTex(); + tessellateEdges(); + } + + boolean clampQuads(int quadCount) { + boolean res = clamp2D(); + if (res) { + for (int qd = 0; qd < quadCount; qd++) { + int i0 = 4 * qd + 0; + int i1 = 4 * qd + 1; + int i2 = 4 * qd + 2; + int i3 = 4 * qd + 3; + res = segmentIsAxisAligned(i0, i1) && + segmentIsAxisAligned(i1, i2) && + segmentIsAxisAligned(i2, i3); + if (!res) break; + } + } + return res; + } + + void tessellateQuadStrip() { + beginTex(); + int quadCount = in.vertexCount / 2 - 1; + if (fill && 1 <= quadCount) { + int nInInd = 6 * quadCount; + setRawSize(nInInd); + int idx = 0; + boolean clamp = clampQuadStrip(quadCount); + for (int qd = 1; qd < quadCount + 1; qd++) { + int i0 = 2 * (qd - 1); + int i1 = 2 * (qd - 1) + 1; + int i2 = 2 * qd + 1; + int i3 = 2 * qd; + + rawIndices[idx++] = i0; + rawIndices[idx++] = i1; + rawIndices[idx++] = i3; + + rawIndices[idx++] = i1; + rawIndices[idx++] = i2; + rawIndices[idx++] = i3; + } + splitRawIndices(clamp); + } + endTex(); + tessellateEdges(); + } + + boolean clampQuadStrip(int quadCount) { + boolean res = clamp2D(); + if (res) { + for (int qd = 1; qd < quadCount + 1; qd++) { + int i0 = 2 * (qd - 1); + int i1 = 2 * (qd - 1) + 1; + int i2 = 2 * qd + 1; + int i3 = 2 * qd; + res = segmentIsAxisAligned(i0, i1) && + segmentIsAxisAligned(i1, i2) && + segmentIsAxisAligned(i2, i3); + if (!res) break; + } + } + return res; + } + + // Uses the raw indices to split the geometry into contiguous + // index groups when the vertex indices become too large. The basic + // idea of this algorithm is to scan through the array of raw indices + // in groups of three vertices at the time (since we are always dealing + // with triangles) and create a new offset in the index cache once the + // index values go above the MAX_VERTEX_INDEX constant. The tricky part + // is that triangles in the new group might refer to vertices in a + // previous group. Since the index groups are by definition disjoint, + // these vertices need to be duplicated at the end of the corresponding + // region in the vertex array. + // + // Also to keep in mind, the ordering of the indices affects performance + // take a look at some of this references: + // http://gameangst.com/?p=9 + // http://home.comcast.net/~tom_forsyth/papers/fast_vert_cache_opt.html + // http://www.ludicon.com/castano/blog/2009/02/optimal-grid-rendering/ + void splitRawIndices(boolean clamp) { + tess.polyIndexCheck(rawSize); + int offset = tess.firstPolyIndex; + + // Current index and vertex ranges + int inInd0 = 0, inInd1 = 0; + int inMaxVert0 = 0, inMaxVert1 = 0; + + int inMaxVertRef = inMaxVert0; // Reference vertex where last break split occurred + int inMaxVertRel = -1; // Position of vertices from last range relative to + // split position. + dupCount = 0; + + IndexCache cache = tess.polyIndexCache; + // In retained mode, each shape has with its own cache item, since + // they should always be available to be rendered individually, even + // if contained in a larger hierarchy. + int index = in.renderMode == RETAINED ? cache.addNew() : cache.getLast(); + firstPolyIndexCache = index; + + int trCount = rawSize / 3; + for (int tr = 0; tr < trCount; tr++) { + if (index == -1) index = cache.addNew(); + + int i0 = rawIndices[3 * tr + 0]; + int i1 = rawIndices[3 * tr + 1]; + int i2 = rawIndices[3 * tr + 2]; + + // Vertex indices relative to the last copy position. + int ii0 = i0 - inMaxVertRef; + int ii1 = i1 - inMaxVertRef; + int ii2 = i2 - inMaxVertRef; + + // Vertex indices relative to the current group. + int count = cache.vertexCount[index]; + int ri0, ri1, ri2; + if (ii0 < 0) { + addDupIndex(ii0); + ri0 = ii0; + } else ri0 = count + ii0; + if (ii1 < 0) { + addDupIndex(ii1); + ri1 = ii1; + } else ri1 = count + ii1; + if (ii2 < 0) { + addDupIndex(ii2); + ri2 = ii2; + } else ri2 = count + ii2; + + tess.polyIndices[offset + 3 * tr + 0] = (short) ri0; + tess.polyIndices[offset + 3 * tr + 1] = (short) ri1; + tess.polyIndices[offset + 3 * tr + 2] = (short) ri2; + + inInd1 = 3 * tr + 2; + inMaxVert1 = PApplet.max(inMaxVert1, PApplet.max(i0, i1, i2)); + inMaxVert0 = PApplet.min(inMaxVert0, PApplet.min(i0, i1, i2)); + + inMaxVertRel = PApplet.max(inMaxVertRel, PApplet.max(ri0, ri1, ri2)); + + if ((PGL.MAX_VERTEX_INDEX1 - 3 <= inMaxVertRel + dupCount && + inMaxVertRel + dupCount < PGL.MAX_VERTEX_INDEX1) || + (tr == trCount - 1)) { + // The vertex indices of the current group are about to + // surpass the MAX_VERTEX_INDEX limit, or we are at the last triangle + // so we need to wrap-up things anyways. + + int nondupCount = 0; + if (0 < dupCount) { + // Adjusting the negative indices so they correspond to vertices + // added at the end of the block. + for (int i = inInd0; i <= inInd1; i++) { + int ri = tess.polyIndices[offset + i]; + if (ri < 0) { + tess.polyIndices[offset + i] = + (short) (inMaxVertRel + 1 + dupIndexPos(ri)); + } + } + + if (inMaxVertRef <= inMaxVert1) { + // Copy non-duplicated vertices from current region first + tess.addPolyVertices(in, inMaxVertRef, inMaxVert1, clamp); + nondupCount = inMaxVert1 - inMaxVertRef + 1; + } + + // Copy duplicated vertices from previous regions last + for (int i = 0; i < dupCount; i++) { + tess.addPolyVertex(in, dupIndices[i] + inMaxVertRef, clamp); + } + } else { + // Copy non-duplicated vertices from current region first + tess.addPolyVertices(in, inMaxVert0, inMaxVert1, clamp); + nondupCount = inMaxVert1 - inMaxVert0 + 1; + } + + // Increment counts: + cache.incCounts(index, inInd1 - inInd0 + 1, nondupCount + dupCount); + lastPolyIndexCache = index; + + // Prepare all variables to start next cache: + index = -1; + inMaxVertRel = -1; + inMaxVertRef = inMaxVert1 + 1; + inMaxVert0 = inMaxVertRef; + inInd0 = inInd1 + 1; + if (dupIndices != null) Arrays.fill(dupIndices, 0, dupCount, 0); + dupCount = 0; + } + } + } + + void addDupIndex(int idx) { + if (dupIndices == null) { + dupIndices = new int[16]; + } + if (dupIndices.length == dupCount) { + int n = dupCount << 1; + + int temp[] = new int[n]; + PApplet.arrayCopy(dupIndices, 0, temp, 0, dupCount); + dupIndices = temp; + } + + if (idx < dupIndices[0]) { + // Add at the beginning + for (int i = dupCount; i > 0; i--) dupIndices[i] = dupIndices[i - 1]; + dupIndices[0] = idx; + dupCount++; + } else if (dupIndices[dupCount - 1] < idx) { + // Add at the end + dupIndices[dupCount] = idx; + dupCount++; + } else { + for (int i = 0; i < dupCount - 1; i++) { + if (dupIndices[i] == idx) break; + if (dupIndices[i] < idx && idx < dupIndices[i + 1]) { + // Insert between i and i + 1: + for (int j = dupCount; j > i + 1; j--) { + dupIndices[j] = dupIndices[j - 1]; + } + dupIndices[i + 1] = idx; + dupCount++; + break; + } + } + } + } + + int dupIndexPos(int idx) { + for (int i = 0; i < dupCount; i++) { + if (dupIndices[i] == idx) return i; + } + return 0; + } + + void setRawSize(int size) { + int size0 = rawIndices.length; + if (size0 < size) { + int size1 = expandArraySize(size0, size); + expandRawIndices(size1); + } + rawSize = size; + } + + void expandRawIndices(int n) { + int temp[] = new int[n]; + PApplet.arrayCopy(rawIndices, 0, temp, 0, rawSize); + rawIndices = temp; + } + + void beginTex() { + setFirstTexIndex(tess.polyIndexCount, tess.polyIndexCache.size - 1); + } + + void endTex() { + setLastTexIndex(tess.lastPolyIndex, tess.polyIndexCache.size - 1); + } + + void beginNoTex() { + newTexImage = null; + setFirstTexIndex(tess.polyIndexCount, tess.polyIndexCache.size - 1); + } + + void endNoTex() { + setLastTexIndex(tess.lastPolyIndex, tess.polyIndexCache.size - 1); + } + + void updateTex() { + beginTex(); + endTex(); + } + + void setFirstTexIndex(int firstIndex, int firstCache) { + if (texCache != null) { + firstTexIndex = firstIndex; + firstTexCache = PApplet.max(0, firstCache); + } + } + + void setLastTexIndex(int lastIndex, int lastCache) { + if (texCache != null) { + if (prevTexImage != newTexImage || texCache.size == 0) { + texCache.addTexture(newTexImage, firstTexIndex, firstTexCache, + lastIndex, lastCache); + } else { + texCache.setLastIndex(lastIndex, lastCache); + } + prevTexImage = newTexImage; + } + } + + // ----------------------------------------------------------------- + // + // Polygon tessellation, includes edge calculation and tessellation. + + void tessellatePolygon(boolean solid, boolean closed, boolean calcNormals) { + beginTex(); + + int nInVert = in.vertexCount; + + if (3 <= nInVert) { + firstPolyIndexCache = -1; + + initGluTess(); + boolean clamp = clampPolygon(); + callback.init(in.renderMode == RETAINED, false, calcNormals, clamp); + + if (fill) { + gluTess.beginPolygon(); + if (solid) { + // Using NONZERO winding rule for solid polygons. + gluTess.setWindingRule(PGL.TESS_WINDING_NONZERO); + } else { + // Using ODD winding rule to generate polygon with holes. + gluTess.setWindingRule(PGL.TESS_WINDING_ODD); + } + gluTess.beginContour(); + } + + if (stroke) { + beginPolygonStroke(); + beginStrokePath(); + } + + int i = 0; + int c = 0; + while (i < in.vertexCount) { + int code = VERTEX; + boolean brk = false; + if (in.codes != null && c < in.codeCount) { + code = in.codes[c++]; + if (code == BREAK && c < in.codeCount) { + brk = true; + code = in.codes[c++]; + } + } + + if (brk) { + if (stroke) { + endStrokePath(closed); + beginStrokePath(); + } + if (fill) { + gluTess.endContour(); + gluTess.beginContour(); + } + } + + if (code == BEZIER_VERTEX) { + addBezierVertex(i); + i += 3; + } else if (code == QUADRATIC_VERTEX) { + addQuadraticVertex(i); + i += 2; + } else if (code == CURVE_VERTEX) { + addCurveVertex(i); + i++; + } else { + addVertex(i); + i++; + } + } + if (stroke) { + endStrokePath(closed); + endPolygonStroke(); + } + if (fill) { + gluTess.endContour(); + gluTess.endPolygon(); + } + } + endTex(); + + if (stroke) tessellateStrokePath(); + } + + void addBezierVertex(int i) { + pg.curveVertexCount = 0; + pg.bezierInitCheck(); + pg.bezierVertexCheck(POLYGON, i); + + PMatrix3D draw = pg.bezierDrawMatrix; + + int i1 = i - 1; + float x1 = in.vertices[3*i1 + 0]; + float y1 = in.vertices[3*i1 + 1]; + float z1 = in.vertices[3*i1 + 2]; + + int strokeColor = 0; + float strokeWeight = 0; + if (stroke) { + strokeColor = in.strokeColors[i]; + strokeWeight = in.strokeWeights[i]; + } + + double[] vertexT = fill ? collectVertexAttributes(i) : null; + + float x2 = in.vertices[3*i + 0]; + float y2 = in.vertices[3*i + 1]; + float z2 = in.vertices[3*i + 2]; + float x3 = in.vertices[3*(i+1) + 0]; + float y3 = in.vertices[3*(i+1) + 1]; + float z3 = in.vertices[3*(i+1) + 2]; + float x4 = in.vertices[3*(i+2) + 0]; + float y4 = in.vertices[3*(i+2) + 1]; + float z4 = in.vertices[3*(i+2) + 2]; + + float xplot1 = draw.m10*x1 + draw.m11*x2 + draw.m12*x3 + draw.m13*x4; + float xplot2 = draw.m20*x1 + draw.m21*x2 + draw.m22*x3 + draw.m23*x4; + float xplot3 = draw.m30*x1 + draw.m31*x2 + draw.m32*x3 + draw.m33*x4; + + float yplot1 = draw.m10*y1 + draw.m11*y2 + draw.m12*y3 + draw.m13*y4; + float yplot2 = draw.m20*y1 + draw.m21*y2 + draw.m22*y3 + draw.m23*y4; + float yplot3 = draw.m30*y1 + draw.m31*y2 + draw.m32*y3 + draw.m33*y4; + + float zplot1 = draw.m10*z1 + draw.m11*z2 + draw.m12*z3 + draw.m13*z4; + float zplot2 = draw.m20*z1 + draw.m21*z2 + draw.m22*z3 + draw.m23*z4; + float zplot3 = draw.m30*z1 + draw.m31*z2 + draw.m32*z3 + draw.m33*z4; + + for (int j = 0; j < pg.bezierDetail; j++) { + x1 += xplot1; xplot1 += xplot2; xplot2 += xplot3; + y1 += yplot1; yplot1 += yplot2; yplot2 += yplot3; + z1 += zplot1; zplot1 += zplot2; zplot2 += zplot3; + if (fill) { + double[] vertex = Arrays.copyOf(vertexT, vertexT.length); + vertex[0] = x1; + vertex[1] = y1; + vertex[2] = z1; + gluTess.addVertex(vertex); + } + if (stroke) addStrokeVertex(x1, y1, z1, strokeColor, strokeWeight); + } + } + + void addQuadraticVertex(int i) { + pg.curveVertexCount = 0; + pg.bezierInitCheck(); + pg.bezierVertexCheck(POLYGON, i); + + PMatrix3D draw = pg.bezierDrawMatrix; + + int i1 = i - 1; + float x1 = in.vertices[3*i1 + 0]; + float y1 = in.vertices[3*i1 + 1]; + float z1 = in.vertices[3*i1 + 2]; + + int strokeColor = 0; + float strokeWeight = 0; + if (stroke) { + strokeColor = in.strokeColors[i]; + strokeWeight = in.strokeWeights[i]; + } + + double[] vertexT = fill ? collectVertexAttributes(i) : null; + + float cx = in.vertices[3*i + 0]; + float cy = in.vertices[3*i + 1]; + float cz = in.vertices[3*i + 2]; + float x = in.vertices[3*(i+1) + 0]; + float y = in.vertices[3*(i+1) + 1]; + float z = in.vertices[3*(i+1) + 2]; + + float x2 = x1 + ((cx-x1)*2/3.0f); + float y2 = y1 + ((cy-y1)*2/3.0f); + float z2 = z1 + ((cz-z1)*2/3.0f); + float x3 = x + ((cx-x)*2/3.0f); + float y3 = y + ((cy-y)*2/3.0f); + float z3 = z + ((cz-z)*2/3.0f); + float x4 = x; + float y4 = y; + float z4 = z; + + float xplot1 = draw.m10*x1 + draw.m11*x2 + draw.m12*x3 + draw.m13*x4; + float xplot2 = draw.m20*x1 + draw.m21*x2 + draw.m22*x3 + draw.m23*x4; + float xplot3 = draw.m30*x1 + draw.m31*x2 + draw.m32*x3 + draw.m33*x4; + + float yplot1 = draw.m10*y1 + draw.m11*y2 + draw.m12*y3 + draw.m13*y4; + float yplot2 = draw.m20*y1 + draw.m21*y2 + draw.m22*y3 + draw.m23*y4; + float yplot3 = draw.m30*y1 + draw.m31*y2 + draw.m32*y3 + draw.m33*y4; + + float zplot1 = draw.m10*z1 + draw.m11*z2 + draw.m12*z3 + draw.m13*z4; + float zplot2 = draw.m20*z1 + draw.m21*z2 + draw.m22*z3 + draw.m23*z4; + float zplot3 = draw.m30*z1 + draw.m31*z2 + draw.m32*z3 + draw.m33*z4; + + for (int j = 0; j < pg.bezierDetail; j++) { + x1 += xplot1; xplot1 += xplot2; xplot2 += xplot3; + y1 += yplot1; yplot1 += yplot2; yplot2 += yplot3; + z1 += zplot1; zplot1 += zplot2; zplot2 += zplot3; + if (fill) { + double[] vertex = Arrays.copyOf(vertexT, vertexT.length); + vertex[0] = x1; + vertex[1] = y1; + vertex[2] = z1; + gluTess.addVertex(vertex); + } + if (stroke) addStrokeVertex(x1, y1, z1, strokeColor, strokeWeight); + } + } + + void addCurveVertex(int i) { + pg.curveVertexCheck(POLYGON); + + float[] vertex = pg.curveVertices[pg.curveVertexCount]; + vertex[X] = in.vertices[3*i + 0]; + vertex[Y] = in.vertices[3*i + 1]; + vertex[Z] = in.vertices[3*i + 2]; + pg.curveVertexCount++; + + // draw a segment if there are enough points + if (pg.curveVertexCount == 3) { + float[] v = pg.curveVertices[pg.curveVertexCount - 2]; + addCurveInitialVertex(i, v[X], v[Y], v[Z]); + } + if (pg.curveVertexCount > 3) { + float[] v1 = pg.curveVertices[pg.curveVertexCount - 4]; + float[] v2 = pg.curveVertices[pg.curveVertexCount - 3]; + float[] v3 = pg.curveVertices[pg.curveVertexCount - 2]; + float[] v4 = pg.curveVertices[pg.curveVertexCount - 1]; + addCurveVertexSegment(i, v1[X], v1[Y], v1[Z], + v2[X], v2[Y], v2[Z], + v3[X], v3[Y], v3[Z], + v4[X], v4[Y], v4[Z]); + } + } + + void addCurveInitialVertex(int i, float x, float y, float z) { + if (fill) { + double[] vertex0 = collectVertexAttributes(i); + vertex0[0] = x; + vertex0[1] = y; + vertex0[2] = z; + gluTess.addVertex(vertex0); + } + if (stroke) { + addStrokeVertex(x, y, z, in.strokeColors[i], strokeWeight); + } + } + + void addCurveVertexSegment(int i, float x1, float y1, float z1, + float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + int strokeColor = 0; + float strokeWeight = 0; + if (stroke) { + strokeColor = in.strokeColors[i]; + strokeWeight = in.strokeWeights[i]; + } + + double[] vertexT = fill ? collectVertexAttributes(i) : null; + + float x = x2; + float y = y2; + float z = z2; + + PMatrix3D draw = pg.curveDrawMatrix; + + float xplot1 = draw.m10*x1 + draw.m11*x2 + draw.m12*x3 + draw.m13*x4; + float xplot2 = draw.m20*x1 + draw.m21*x2 + draw.m22*x3 + draw.m23*x4; + float xplot3 = draw.m30*x1 + draw.m31*x2 + draw.m32*x3 + draw.m33*x4; + + float yplot1 = draw.m10*y1 + draw.m11*y2 + draw.m12*y3 + draw.m13*y4; + float yplot2 = draw.m20*y1 + draw.m21*y2 + draw.m22*y3 + draw.m23*y4; + float yplot3 = draw.m30*y1 + draw.m31*y2 + draw.m32*y3 + draw.m33*y4; + + float zplot1 = draw.m10*z1 + draw.m11*z2 + draw.m12*z3 + draw.m13*z4; + float zplot2 = draw.m20*z1 + draw.m21*z2 + draw.m22*z3 + draw.m23*z4; + float zplot3 = draw.m30*z1 + draw.m31*z2 + draw.m32*z3 + draw.m33*z4; + + for (int j = 0; j < pg.curveDetail; j++) { + x += xplot1; xplot1 += xplot2; xplot2 += xplot3; + y += yplot1; yplot1 += yplot2; yplot2 += yplot3; + z += zplot1; zplot1 += zplot2; zplot2 += zplot3; + if (fill) { + double[] vertex1 = Arrays.copyOf(vertexT, vertexT.length); + vertex1[0] = x; + vertex1[1] = y; + vertex1[2] = z; + gluTess.addVertex(vertex1); + } + if (stroke) addStrokeVertex(x, y, z, strokeColor, strokeWeight); + } + } + + void addVertex(int i) { + pg.curveVertexCount = 0; + + float x = in.vertices[3*i + 0]; + float y = in.vertices[3*i + 1]; + float z = in.vertices[3*i + 2]; + + if (fill) { + double[] vertex = collectVertexAttributes(i); + vertex[0] = x; + vertex[1] = y; + vertex[2] = z; + gluTess.addVertex(vertex); + } + if (stroke) { + addStrokeVertex(x, y, z, in.strokeColors[i], in.strokeWeights[i]); + } + } + + double[] collectVertexAttributes(int i) { + final int COORD_COUNT = 3; + final int ATTRIB_COUNT = 22; + + double[] avect = in.getAttribVector(i); + + double[] r = new double[COORD_COUNT + ATTRIB_COUNT + avect.length]; + + int j = COORD_COUNT; + + int fcol = in.colors[i]; + r[j++] = (fcol >> 24) & 0xFF; // fa + r[j++] = (fcol >> 16) & 0xFF; // fr + r[j++] = (fcol >> 8) & 0xFF; // fg + r[j++] = (fcol >> 0) & 0xFF; // fb + + r[j++] = in.normals[3*i + 0]; // nx + r[j++] = in.normals[3*i + 1]; // ny + r[j++] = in.normals[3*i + 2]; // nz + + r[j++] = in.texcoords[2*i + 0]; // u + r[j++] = in.texcoords[2*i + 1]; // v + + int acol = in.ambient[i]; + r[j++] = (acol >> 24) & 0xFF; // aa + r[j++] = (acol >> 16) & 0xFF; // ar + r[j++] = (acol >> 8) & 0xFF; // ag + r[j++] = (acol >> 0) & 0xFF; // ab + + int scol = in.specular[i]; + r[j++] = (scol >> 24) & 0xFF; // sa + r[j++] = (scol >> 16) & 0xFF; // sr + r[j++] = (scol >> 8) & 0xFF; // sg + r[j++] = (scol >> 0) & 0xFF; // sb + + int ecol = in.emissive[i]; + r[j++] = (ecol >> 24) & 0xFF; // ea + r[j++] = (ecol >> 16) & 0xFF; // er + r[j++] = (ecol >> 8) & 0xFF; // eg + r[j++] = (ecol >> 0) & 0xFF; // eb + + r[j++] = in.shininess[i]; // sh + + System.arraycopy(avect, 0, r, j, avect.length); + + return r; + } + + void beginPolygonStroke() { + pathVertexCount = 0; + if (pathVertices == null) { + pathVertices = new float[3 * PGL.DEFAULT_IN_VERTICES]; + pathColors = new int[PGL.DEFAULT_IN_VERTICES]; + pathWeights = new float[PGL.DEFAULT_IN_VERTICES]; + } + } + + void endPolygonStroke() { + // Nothing to do here. + } + + void beginStrokePath() { + beginPath = pathVertexCount; + } + + void endStrokePath(boolean closed) { + int idx = pathVertexCount; + if (beginPath + 1 < idx) { + boolean begin = beginPath == idx - 2; + boolean end = begin || !closed; + in.addEdge(idx - 2, idx - 1, begin, end); + if (!end) { + in.addEdge(idx - 1, beginPath, false, false); + in.closeEdge(idx - 1, beginPath); + } + } + } + + void addStrokeVertex(float x, float y, float z, int c, float w) { + int idx = pathVertexCount; + if (beginPath + 1 < idx) { + in.addEdge(idx - 2, idx - 1, beginPath == idx - 2, false); + } + + if (pathVertexCount == pathVertices.length / 3) { + int newSize = pathVertexCount << 1; + + float vtemp[] = new float[3 * newSize]; + PApplet.arrayCopy(pathVertices, 0, vtemp, 0, 3 * pathVertexCount); + pathVertices = vtemp; + + int ctemp[] = new int[newSize]; + PApplet.arrayCopy(pathColors, 0, ctemp, 0, pathVertexCount); + pathColors = ctemp; + + float wtemp[] = new float[newSize]; + PApplet.arrayCopy(pathWeights, 0, wtemp, 0, pathVertexCount); + pathWeights = wtemp; + } + + pathVertices[3 * idx + 0] = x; + pathVertices[3 * idx + 1] = y; + pathVertices[3 * idx + 2] = z; + pathColors[idx] = c; + pathWeights[idx] = w; + + pathVertexCount++; + } + + void tessellateStrokePath() { + if (in.edgeCount == 0) return; + strokeVertices = pathVertices; + strokeColors = pathColors; + strokeWeights = pathWeights; + if (is3D) { + tessellateEdges3D(); + } else if (is2D) { + beginNoTex(); + tessellateEdges2D(); + endNoTex(); + } + } + + boolean clampPolygon() { + return false; + } + + // Tessellates the path given as parameter. This will work only in 2D. + // Based on the opengl stroke hack described here: + // http://wiki.processing.org/w/Stroke_attributes_in_OpenGL + public void tessellateLinePath(LinePath path) { + initGluTess(); + boolean clamp = clampLinePath(); + callback.init(in.renderMode == RETAINED, true, false, clamp); + + int cap = strokeCap == ROUND ? LinePath.CAP_ROUND : + strokeCap == PROJECT ? LinePath.CAP_SQUARE : + LinePath.CAP_BUTT; + int join = strokeJoin == ROUND ? LinePath.JOIN_ROUND : + strokeJoin == BEVEL ? LinePath.JOIN_BEVEL : + LinePath.JOIN_MITER; + + // Make the outline of the stroke from the path + LinePath strokedPath = LinePath.createStrokedPath(path, strokeWeight, + cap, join); + + gluTess.beginPolygon(); + + double[] vertex; + float[] coords = new float[6]; + + LinePath.PathIterator iter = strokedPath.getPathIterator(); + int rule = iter.getWindingRule(); + switch(rule) { + case LinePath.WIND_EVEN_ODD: + gluTess.setWindingRule(PGL.TESS_WINDING_ODD); + break; + case LinePath.WIND_NON_ZERO: + gluTess.setWindingRule(PGL.TESS_WINDING_NONZERO); + break; + } + + while (!iter.isDone()) { + switch (iter.currentSegment(coords)) { + + case LinePath.SEG_MOVETO: + gluTess.beginContour(); + + // $FALL-THROUGH$ + case LinePath.SEG_LINETO: + // Vertex data includes coordinates, colors, normals, texture + // coordinates, and material properties. + vertex = new double[] { coords[0], coords[1], 0, + coords[2], coords[3], coords[4], coords[5], + 0, 0, 1, + 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + + gluTess.addVertex(vertex); + + break; + case LinePath.SEG_CLOSE: + gluTess.endContour(); + break; + } + iter.next(); + } + gluTess.endPolygon(); + } + + boolean clampLinePath() { + return clamp2D() && + strokeCap == PROJECT && strokeJoin == BEVEL && + !subPixelStroke(strokeWeight); + } + + ///////////////////////////////////////// + + // Interesting notes about using the GLU tessellator to render thick + // polylines: + // http://stackoverflow.com/questions/687173/how-do-i-render-thick-2d-lines-as-polygons + // + // "...Since I disliked the tesselator API I lifted the tesselation code + // from the free SGI OpenGL reference implementation, rewrote the entire + // front-end and added memory pools to get the number of allocations down. + // It took two days to do this, but it was well worth it (like factor five + // performance improvement)..." + // + // This C implementation of GLU could be useful: + // http://code.google.com/p/glues/ + // to eventually come up with an optimized GLU tessellator in native code. + protected class TessellatorCallback implements PGL.TessellatorCallback { + AttributeMap attribs; + boolean calcNormals; + boolean strokeTess; + boolean clampXY; + IndexCache cache; + int cacheIndex; + int vertFirst; + int vertCount; + int vertOffset; + int primitive; + + public TessellatorCallback(AttributeMap attribs) { + this.attribs = attribs; + } + + public void init(boolean addCache, boolean strokeTess, boolean calcNorm, + boolean clampXY) { + this.strokeTess = strokeTess; + this.calcNormals = calcNorm; + this.clampXY = clampXY; + + cache = tess.polyIndexCache; + if (addCache) { + cache.addNew(); + } + } + + public void begin(int type) { + cacheIndex = cache.getLast(); + if (firstPolyIndexCache == -1) { + firstPolyIndexCache = cacheIndex; + } + if (strokeTess && firstLineIndexCache == -1) { + firstLineIndexCache = cacheIndex; + } + + vertFirst = cache.vertexCount[cacheIndex]; + vertOffset = cache.vertexOffset[cacheIndex]; + vertCount = 0; + + if (type == PGL.TRIANGLE_FAN) primitive = TRIANGLE_FAN; + else if (type == PGL.TRIANGLE_STRIP) primitive = TRIANGLE_STRIP; + else if (type == PGL.TRIANGLES) primitive = TRIANGLES; + } + + public void end() { + if (PGL.MAX_VERTEX_INDEX1 <= vertFirst + vertCount) { + // We need a new index block for the new batch of + // vertices resulting from this primitive. tessVert can + // be safely assumed here to be less or equal than + // MAX_VERTEX_INDEX1 because the condition was checked + // every time a new vertex was emitted (see vertex() below). + //tessBlock = tess.addFillIndexBlock(tessBlock); + cacheIndex = cache.addNew(); + vertFirst = cache.vertexCount[cacheIndex]; + vertOffset = cache.vertexOffset[cacheIndex]; + } + + int indCount = 0; + switch (primitive) { + case TRIANGLE_FAN: + indCount = 3 * (vertCount - 2); + for (int i = 1; i < vertCount - 1; i++) { + addIndex(0); + addIndex(i); + addIndex(i + 1); + if (calcNormals) calcTriNormal(0, i, i + 1); + } + break; + case TRIANGLE_STRIP: + indCount = 3 * (vertCount - 2); + for (int i = 1; i < vertCount - 1; i++) { + if (i % 2 == 0) { + addIndex(i + 1); + addIndex(i); + addIndex(i - 1); + if (calcNormals) calcTriNormal(i + 1, i, i - 1); + } else { + addIndex(i - 1); + addIndex(i); + addIndex(i + 1); + if (calcNormals) calcTriNormal(i - 1, i, i + 1); + } + } + break; + case TRIANGLES: + indCount = vertCount; + for (int i = 0; i < vertCount; i++) { + addIndex(i); + } + if (calcNormals) { + for (int tr = 0; tr < vertCount / 3; tr++) { + int i0 = 3 * tr + 0; + int i1 = 3 * tr + 1; + int i2 = 3 * tr + 2; + calcTriNormal(i0, i1, i2); + } + } + break; + } + + cache.incCounts(cacheIndex, indCount, vertCount); + lastPolyIndexCache = cacheIndex; + if (strokeTess) { + lastLineIndexCache = cacheIndex; + } + } + + protected void addIndex(int tessIdx) { + tess.polyIndexCheck(); + tess.polyIndices[tess.polyIndexCount - 1] = + (short) (vertFirst + tessIdx); + } + + protected void calcTriNormal(int tessIdx0, int tessIdx1, int tessIdx2) { + tess.calcPolyNormal(vertFirst + vertOffset + tessIdx0, + vertFirst + vertOffset + tessIdx1, + vertFirst + vertOffset + tessIdx2); + } + + public void vertex(Object data) { + if (data instanceof double[]) { + double[] d = (double[]) data; + int l = d.length; + if (l < 25) { + throw new RuntimeException("TessCallback vertex() data is " + + "too small"); + } + + if (vertCount < PGL.MAX_VERTEX_INDEX1) { + tess.addPolyVertex(d, clampXY); + vertCount++; + } else { + throw new RuntimeException("The tessellator is generating too " + + "many vertices, reduce complexity of " + + "shape."); + } + + } else { + throw new RuntimeException("TessCallback vertex() data not " + + "understood"); + } + } + + public void error(int errnum) { + String estring = pg.pgl.tessError(errnum); + PGraphics.showWarning(TESSELLATION_ERROR, estring); + } + + /** + * Implementation of the GLU_TESS_COMBINE callback. + * @param coords is the 3-vector of the new vertex + * @param data is the vertex data to be combined, up to four elements. + * This is useful when mixing colors together or any other + * user data that was passed in to gluTessVertex. + * @param weight is an array of weights, one for each element of "data" + * that should be linearly combined for new values. + * @param outData is the set of new values of "data" after being + * put back together based on the weights. it's passed back as a + * single element Object[] array because that's the closest + * that Java gets to a pointer. + */ + public void combine(double[] coords, Object[] data, + float[] weight, Object[] outData) { + int n = ((double[])data[0]).length; + double[] vertex = new double[n]; + vertex[0] = coords[0]; + vertex[1] = coords[1]; + vertex[2] = coords[2]; + + // Calculating the rest of the vertex parameters (color, + // normal, texcoords) as the linear combination of the + // combined vertices. + for (int i = 3; i < n; i++) { + vertex[i] = 0; + for (int j = 0; j < 4; j++) { + double[] vertData = (double[])data[j]; + if (vertData != null) { + vertex[i] += weight[j] * vertData[i]; + } + } + } + + // Normalizing normal vectors, since the weighted + // combination of normal vectors is not necessarily + // normal. + normalize(vertex, 7); + if (25 < n) { + // We have custom attributes, look for normal attributes + int pos = 25; + for (int i = 0; i < attribs.size(); i++) { + VertexAttribute attrib = attribs.get(i); + if (attrib.isNormal()) { + normalize(vertex, pos); + pos += 3; + } else { + pos += attrib.size; + } + } + } + + outData[0] = vertex; + } + + private void normalize(double[] v, int i) { + double sum = v[i ] * v[i ] + + v[i + 1] * v[i + 1] + + v[i + 2] * v[i + 2]; + double len = Math.sqrt(sum); + if (0 < len) { + v[i ] /= len; + v[i + 1] /= len; + v[i + 2] /= len; + } + } + } + } + + + static protected class DepthSorter { + + static final int X = 0; + static final int Y = 1; + static final int Z = 2; + static final int W = 3; + + static final int X0 = 0; + static final int Y0 = 1; + static final int Z0 = 2; + static final int X1 = 3; + static final int Y1 = 4; + static final int Z1 = 5; + static final int X2 = 6; + static final int Y2 = 7; + static final int Z2 = 8; + + int[] triangleIndices = new int[0]; + int[] texMap = new int[0]; + int[] voffsetMap = new int[0]; + + float[] minXBuffer = new float[0]; + float[] minYBuffer = new float[0]; + float[] minZBuffer = new float[0]; + float[] maxXBuffer = new float[0]; + float[] maxYBuffer = new float[0]; + float[] maxZBuffer = new float[0]; + + float[] screenVertices = new float[0]; + + float[] triA = new float[9]; + float[] triB = new float[9]; + + BitSet marked = new BitSet(); + BitSet swapped = new BitSet(); + + PGraphicsOpenGL pg; + + DepthSorter (PGraphicsOpenGL pg) { + this.pg = pg; + } + + void checkIndexBuffers(int newTriangleCount) { + if (triangleIndices.length < newTriangleCount) { + int newSize = (newTriangleCount / 4 + 1) * 5; + triangleIndices = new int[newSize]; + texMap = new int[newSize]; + voffsetMap = new int[newSize]; + minXBuffer = new float[newSize]; + minYBuffer = new float[newSize]; + minZBuffer = new float[newSize]; + maxXBuffer = new float[newSize]; + maxYBuffer = new float[newSize]; + maxZBuffer = new float[newSize]; + } + } + + void checkVertexBuffer(int newVertexCount) { + int coordCount = 3*newVertexCount; + if (screenVertices.length < coordCount) { + int newSize = (coordCount / 4 + 1) * 5; + screenVertices = new float[newSize]; + } + } + + // Sorting -------------------------------------------- + + void sort(TessGeometry tessGeo) { + + int triangleCount = tessGeo.polyIndexCount / 3; + checkIndexBuffers(triangleCount); + int[] triangleIndices = this.triangleIndices; + int[] texMap = this.texMap; + int[] voffsetMap = this.voffsetMap; + + { // Initialize triangle indices + for (int i = 0; i < triangleCount; i++) { + triangleIndices[i] = i; + } + } + + { // Map caches to triangles + TexCache texCache = pg.texCache; + IndexCache indexCache = tessGeo.polyIndexCache; + for (int i = 0; i < texCache.size; i++) { + int first = texCache.firstCache[i]; + int last = texCache.lastCache[i]; + for (int n = first; n <= last; n++) { + int ioffset = n == first + ? texCache.firstIndex[i] + : indexCache.indexOffset[n]; + int icount = n == last + ? texCache.lastIndex[i] - ioffset + 1 + : indexCache.indexOffset[n] + indexCache.indexCount[n] - ioffset; + + for (int tr = ioffset / 3; tr < (ioffset + icount) / 3; tr++) { + texMap[tr] = i; + voffsetMap[tr] = n; + } + } + } + } + + { // Map vertices to screen + int polyVertexCount = tessGeo.polyVertexCount; + checkVertexBuffer(polyVertexCount); + float[] screenVertices = this.screenVertices; + + float[] polyVertices = tessGeo.polyVertices; + + PMatrix3D projection = pg.projection; + + for (int i = 0; i < polyVertexCount; i++) { + float x = polyVertices[4*i+X]; + float y = polyVertices[4*i+Y]; + float z = polyVertices[4*i+Z]; + float w = polyVertices[4*i+W]; + + float ox = projection.m00 * x + projection.m01 * y + + projection.m02 * z + projection.m03 * w; + float oy = projection.m10 * x + projection.m11 * y + + projection.m12 * z + projection.m13 * w; + float oz = projection.m20 * x + projection.m21 * y + + projection.m22 * z + projection.m23 * w; + float ow = projection.m30 * x + projection.m31 * y + + projection.m32 * z + projection.m33 * w; + if (nonZero(ow)) { + ox /= ow; + oy /= ow; + oz /= ow; + } + screenVertices[3*i+X] = ox; + screenVertices[3*i+Y] = oy; + screenVertices[3*i+Z] = -oz; + } + } + float[] screenVertices = this.screenVertices; + + int[] vertexOffset = tessGeo.polyIndexCache.vertexOffset; + short[] polyIndices = tessGeo.polyIndices; + + float[] triA = this.triA; + float[] triB = this.triB; + + for (int i = 0; i < triangleCount; i++) { + fetchTriCoords(triA, i, vertexOffset, voffsetMap, screenVertices, polyIndices); + minXBuffer[i] = PApplet.min(triA[X0], triA[X1], triA[X2]); + maxXBuffer[i] = PApplet.max(triA[X0], triA[X1], triA[X2]); + minYBuffer[i] = PApplet.min(triA[Y0], triA[Y1], triA[Y2]); + maxYBuffer[i] = PApplet.max(triA[Y0], triA[Y1], triA[Y2]); + minZBuffer[i] = PApplet.min(triA[Z0], triA[Z1], triA[Z2]); + maxZBuffer[i] = PApplet.max(triA[Z0], triA[Z1], triA[Z2]); + } + + sortByMinZ(0, triangleCount - 1, triangleIndices, minZBuffer); + + int activeTid = 0; + + BitSet marked = this.marked; + BitSet swapped = this.swapped; + + marked.clear(); + + while (activeTid < triangleCount) { + int testTid = activeTid + 1; + boolean draw = false; + + swapped.clear(); + + int ati = triangleIndices[activeTid]; + float minXA = minXBuffer[ati]; + float maxXA = maxXBuffer[ati]; + float minYA = minYBuffer[ati]; + float maxYA = maxYBuffer[ati]; + float maxZA = maxZBuffer[ati]; + + fetchTriCoords(triA, ati, vertexOffset, voffsetMap, screenVertices, polyIndices); + + while (!draw && testTid < triangleCount) { + int tti = triangleIndices[testTid]; + + // TEST 1 // Z overlap + if (maxZA <= minZBuffer[tti] && !marked.get(tti)) { + draw = true; // pass, not overlapping in Z, draw it + + // TEST 2 // XY overlap using square window + } else if (maxXA <= minXBuffer[tti] || maxYA <= minYBuffer[tti] || + minXA >= maxXBuffer[tti] || minYA >= maxYBuffer[tti]) { + testTid++; // pass, not overlapping in XY + + // TEST 3 // test on which side ACTIVE is relative to TEST + } else { + fetchTriCoords(triB, tti, vertexOffset, voffsetMap, + screenVertices, polyIndices); + if (side(triB, triA, -1) > 0) { + testTid++; // pass, ACTIVE is in halfspace behind current TEST + + // TEST 4 // test on which side TEST is relative to ACTIVE + } else if (side(triA, triB, 1) > 0) { + testTid++; // pass, current TEST is in halfspace in front of ACTIVE + + // FAIL, wrong depth order, swap + } else { + if (!swapped.get(tti)) { + swapped.set(ati); + marked.set(tti); + rotateRight(triangleIndices, activeTid, testTid); + + ati = tti; + System.arraycopy(triB, 0, triA, 0, 9); + minXA = minXBuffer[ati]; + maxXA = maxXBuffer[ati]; + minYA = minYBuffer[ati]; + maxYA = maxYBuffer[ati]; + maxZA = maxZBuffer[ati]; + + testTid = activeTid + 1; + } else { + // oops, we already tested this one, either in one plane or + // interlocked in loop with others, just ignore it for now :( + testTid++; + } + } + } + } + activeTid++; + } + + { // Reorder the buffers + for (int id = 0; id < triangleCount; id++) { + int mappedId = triangleIndices[id]; + if (id != mappedId) { + + // put the first index aside + short i0 = polyIndices[3*id+0]; + short i1 = polyIndices[3*id+1]; + short i2 = polyIndices[3*id+2]; + int texId = texMap[id]; + int voffsetId = voffsetMap[id]; + + // process the whole permutation cycle + int currId = id; + int nextId = mappedId; + do { + triangleIndices[currId] = currId; + polyIndices[3*currId+0] = polyIndices[3*nextId+0]; + polyIndices[3*currId+1] = polyIndices[3*nextId+1]; + polyIndices[3*currId+2] = polyIndices[3*nextId+2]; + texMap[currId] = texMap[nextId]; + voffsetMap[currId] = voffsetMap[nextId]; + + currId = nextId; + nextId = triangleIndices[nextId]; + } while (nextId != id); + + // place the first index at the end + triangleIndices[currId] = currId; + polyIndices[3*currId+0] = i0; + polyIndices[3*currId+1] = i1; + polyIndices[3*currId+2] = i2; + texMap[currId] = texId; + voffsetMap[currId] = voffsetId; + } + } + } + + } + + static void fetchTriCoords(float[] tri, int ti, int[] vertexOffset, + int[] voffsetMap, float[] screenVertices, short[] polyIndices) { + int voffset = vertexOffset[voffsetMap[ti]]; + int i0 = 3 * (voffset + polyIndices[3*ti+0]); + int i1 = 3 * (voffset + polyIndices[3*ti+1]); + int i2 = 3 * (voffset + polyIndices[3*ti+2]); + tri[X0] = screenVertices[i0+X]; + tri[Y0] = screenVertices[i0+Y]; + tri[Z0] = screenVertices[i0+Z]; + tri[X1] = screenVertices[i1+X]; + tri[Y1] = screenVertices[i1+Y]; + tri[Z1] = screenVertices[i1+Z]; + tri[X2] = screenVertices[i2+X]; + tri[Y2] = screenVertices[i2+Y]; + tri[Z2] = screenVertices[i2+Z]; + } + + static void sortByMinZ(int leftTid, int rightTid, int[] triangleIndices, + float[] minZBuffer) { + + // swap pivot to the front + swap(triangleIndices, leftTid, ((leftTid + rightTid) / 2)); + + int k = leftTid; + float leftMinZ = minZBuffer[triangleIndices[leftTid]]; + + // sort by min z + for (int tid = leftTid+1; tid <= rightTid; tid++) { + float minZ = minZBuffer[triangleIndices[tid]]; + if (minZ < leftMinZ) { + swap(triangleIndices, ++k, tid); + } + } + + // swap pivot back to the middle + swap(triangleIndices, leftTid, k); + + if (leftTid < k - 1) sortByMinZ(leftTid, k - 1, triangleIndices, + minZBuffer); + if (k + 1 < rightTid) sortByMinZ(k + 1, rightTid, triangleIndices, + minZBuffer); + } + + // Math ----------------------------------------------- + + static int side(float[] tri1, float[] tri2, float tz) { + float Dx, Dy, Dz, Dw; + { // Get the equation of the plane + float + ABx = tri1[X1] - tri1[X0], ACx = tri1[X2] - tri1[X0], + ABy = tri1[Y1] - tri1[Y0], ACy = tri1[Y2] - tri1[Y0], + ABz = tri1[Z1] - tri1[Z0], ACz = tri1[Z2] - tri1[Z0]; + + Dx = ABy*ACz - ABz*ACy; Dy = ABz*ACx - ABx*ACz; Dz = ABx*ACy - ABy*ACx; + + // Normalize normal vector + float rMag = 1.0f/(float) Math.sqrt(Dx * Dx + Dy * Dy + Dz * Dz); + Dx *= rMag; Dy *= rMag; Dz *= rMag; + + Dw = -dot(Dx, Dy, Dz, tri1[X0], tri1[Y0], tri1[Z0]); + } + + float distTest = dot(Dx, Dy, Dz, + tri1[X0], tri1[Y0], tri1[Z0] + 100*tz) + Dw; + + float distA = dot(Dx, Dy, Dz, tri2[X0], tri2[Y0], tri2[Z0]) + Dw; + float distB = dot(Dx, Dy, Dz, tri2[X1], tri2[Y1], tri2[Z1]) + Dw; + float distC = dot(Dx, Dy, Dz, tri2[X2], tri2[Y2], tri2[Z2]) + Dw; + + // Ignore relatively close vertices to get stable results + // when some parts of polygons are close to each other + float absA = PApplet.abs(distA); + float absB = PApplet.abs(distB); + float absC = PApplet.abs(distC); + float eps = PApplet.max(absA, absB, absC) * 0.1f; + + float sideA = ((absA < eps) ? 0.0f : distA) * distTest; + float sideB = ((absB < eps) ? 0.0f : distB) * distTest; + float sideC = ((absC < eps) ? 0.0f : distC) * distTest; + + boolean sameSide = sideA >= 0 && sideB >= 0 && sideC >= 0; + boolean notSameSide = sideA <= 0 && sideB <= 0 && sideC <= 0; + + return sameSide ? 1 : notSameSide ? -1 : 0; + } + + static float dot(float a1, float a2, float a3, + float b1, float b2, float b3) { + return a1 * b1 + a2 * b2 + a3 * b3; + } + + + // Array utils --------------------------------------- + + static void swap(int[] array, int i1, int i2) { + int temp = array[i1]; + array[i1] = array[i2]; + array[i2] = temp; + } + + static void rotateRight(int[] array, int i1, int i2) { + if (i1 == i2) return; + int temp = array[i2]; + System.arraycopy(array, i1, array, i1 + 1, i2 - i1); + array[i1] = temp; + } + + } + +} diff --git a/src/main/java/processing/opengl/PJOGL.java b/src/main/java/processing/opengl/PJOGL.java new file mode 100644 index 0000000..58f7618 --- /dev/null +++ b/src/main/java/processing/opengl/PJOGL.java @@ -0,0 +1,1966 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.opengl; + +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Shape; +import java.awt.Toolkit; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.geom.PathIterator; +import java.io.IOException; +import java.net.URL; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +import com.jogamp.common.util.VersionNumber; +import com.jogamp.opengl.GL; +import com.jogamp.opengl.GL2; +import com.jogamp.opengl.GL2ES2; +import com.jogamp.opengl.GL2ES3; +import com.jogamp.opengl.GL2GL3; +import com.jogamp.opengl.GL3ES3; +import com.jogamp.opengl.GLAutoDrawable; +import com.jogamp.opengl.GLCapabilities; +import com.jogamp.opengl.GLCapabilitiesImmutable; +import com.jogamp.opengl.GLContext; +import com.jogamp.opengl.GLDrawable; +import com.jogamp.opengl.fixedfunc.GLMatrixFunc; +import com.jogamp.opengl.glu.GLU; +import com.jogamp.opengl.glu.GLUtessellator; +import com.jogamp.opengl.glu.GLUtessellatorCallbackAdapter; + +import processing.core.PApplet; +import processing.core.PGraphics; +import processing.core.PMatrix3D; +import processing.core.PSurface; + + +public class PJOGL extends PGL { + // OpenGL profile to use (2, 3 or 4) + public static int profile = 2; + + // User-provided icons to override defaults + protected static String[] icons = null; + + // The two windowing toolkits available to use in JOGL: + public static final int AWT = 0; // http://jogamp.org/wiki/index.php/Using_JOGL_in_AWT_SWT_and_Swing + public static final int NEWT = 1; // http://jogamp.org/jogl/doc/NEWT-Overview.html + + // ........................................................ + + // Public members to access the underlying GL objects and context + + /** Basic GL functionality, common to all profiles */ + public GL gl; + + /** GLU interface **/ + public GLU glu; + + /** The rendering context (holds rendering state info) */ + public GLContext context; + + // ........................................................ + + // Additional parameters + + /** Time that the Processing's animation thread will wait for JOGL's rendering + * thread to be done with a single frame. + */ + protected static int DRAW_TIMEOUT_MILLIS = 500; + + // ........................................................ + + // Protected JOGL-specific objects needed to access the GL profiles + + /** The capabilities of the OpenGL rendering surface */ + protected GLCapabilitiesImmutable capabilities; + + /** The rendering surface */ + protected GLDrawable drawable; + + /** GLES2 functionality (shaders, etc) */ + protected GL2ES2 gl2; + + /** GL3 interface */ + protected GL2GL3 gl3; + + /** GL2 desktop functionality (blit framebuffer, map buffer range, + * multisampled renderbuffers) */ + protected GL2 gl2x; + + /** GL3ES3 interface */ + protected GL3ES3 gl3es3; + + /** Stores exceptions that ocurred during drawing */ + protected Exception drawException; + + // ........................................................ + + // Utility arrays to copy projection/modelview matrices to GL + + protected float[] projMatrix; + protected float[] mvMatrix; + + // ........................................................ + + // Static initialization for some parameters that need to be different for + // JOGL + + static { + MIN_DIRECT_BUFFER_SIZE = 2; + INDEX_TYPE = GL.GL_UNSIGNED_SHORT; + } + + + /////////////////////////////////////////////////////////////// + + // Initialization, finalization + + + public PJOGL(PGraphicsOpenGL pg) { + super(pg); + glu = new GLU(); + } + + + @Override + public Object getNative() { + return sketch.getSurface().getNative(); + } + + + @Override + protected void setFrameRate(float fps) {} + + + @Override + protected void initSurface(int antialias) {} + + + @Override + protected void reinitSurface() {} + + + @Override + protected void registerListeners() {} + + + static public void setIcon(String... icons) { + PJOGL.icons = new String[icons.length]; + PApplet.arrayCopy(icons, PJOGL.icons); + } + + + /////////////////////////////////////////////////////////////// + + // Public methods to get/set renderer's properties + + + public void setCaps(GLCapabilities caps) { + reqNumSamples = caps.getNumSamples(); + capabilities = caps; + } + + + public GLCapabilitiesImmutable getCaps() { + return capabilities; + } + + + public void setFps(float fps) { + if (!setFps || targetFps != fps) { + if (60 < fps) { + // Disables v-sync + gl.setSwapInterval(0); + } else if (30 < fps) { + gl.setSwapInterval(1); + } else { + gl.setSwapInterval(2); + } + targetFps = currentFps = fps; + setFps = true; + } + } + + + @Override + protected int getDepthBits() { + return capabilities.getDepthBits(); + } + + + @Override + protected int getStencilBits() { + return capabilities.getStencilBits(); + } + + + @Override + protected float getPixelScale() { + PSurface surf = sketch.getSurface(); + if (surf == null) { + return graphics.pixelDensity; + } else if (surf instanceof PSurfaceJOGL) { + return ((PSurfaceJOGL)surf).getPixelScale(); + } else { + throw new RuntimeException("Renderer cannot find a JOGL surface"); + } + } + + + @Override + protected void getGL(PGL pgl) { + PJOGL pjogl = (PJOGL)pgl; + + this.drawable = pjogl.drawable; + this.context = pjogl.context; + this.glContext = pjogl.glContext; + setThread(pjogl.glThread); + + this.gl = pjogl.gl; + this.gl2 = pjogl.gl2; + this.gl2x = pjogl.gl2x; + this.gl3 = pjogl.gl3; + this.gl3es3 = pjogl.gl3es3; + } + + + public void getGL(GLAutoDrawable glDrawable) { + context = glDrawable.getContext(); + glContext = context.hashCode(); + setThread(Thread.currentThread()); + + gl = context.getGL(); + gl2 = gl.getGL2ES2(); + try { + gl2x = gl.getGL2(); + } catch (com.jogamp.opengl.GLException e) { + gl2x = null; + } + try { + gl3 = gl.getGL2GL3(); + } catch (com.jogamp.opengl.GLException e) { + gl3 = null; + } + try { + gl3es3 = gl.getGL3ES3(); + } catch (com.jogamp.opengl.GLException e) { + gl3es3 = null; + } + } + + + @Override + protected boolean canDraw() { return true; } + + + @Override + protected void requestFocus() {} + + + @Override + protected void requestDraw() {} + + + @Override + protected void swapBuffers() { + PSurfaceJOGL surf = (PSurfaceJOGL)sketch.getSurface(); + surf.window.swapBuffers(); + } + + + @Override + protected void initFBOLayer() { + if (0 < sketch.frameCount) { + if (isES()) initFBOLayerES(); + else initFBOLayerGL(); + } + } + + + private void initFBOLayerES() { + IntBuffer buf = allocateDirectIntBuffer(fboWidth * fboHeight); + + if (hasReadBuffer()) readBuffer(BACK); + readPixelsImpl(0, 0, fboWidth, fboHeight, RGBA, UNSIGNED_BYTE, buf); + bindTexture(TEXTURE_2D, glColorTex.get(frontTex)); + texSubImage2D(TEXTURE_2D, 0, 0, 0, fboWidth, fboHeight, RGBA, UNSIGNED_BYTE, buf); + + bindTexture(TEXTURE_2D, glColorTex.get(backTex)); + texSubImage2D(TEXTURE_2D, 0, 0, 0, fboWidth, fboHeight, RGBA, UNSIGNED_BYTE, buf); + + bindTexture(TEXTURE_2D, 0); + bindFramebufferImpl(FRAMEBUFFER, 0); + } + + + private void initFBOLayerGL() { + // Copy the contents of the front and back screen buffers to the textures + // of the FBO, so they are properly initialized. Note that the front buffer + // of the default framebuffer (the screen) contains the previous frame: + // https://www.opengl.org/wiki/Default_Framebuffer + // so it is copied to the front texture of the FBO layer: + if (pclearColor || 0 < pgeomCount || !sketch.isLooping()) { + if (hasReadBuffer()) readBuffer(FRONT); + } else { + // ...except when the previous frame has not been cleared and nothing was + // rendered while looping. In this case the back buffer, which holds the + // initial state of the previous frame, still contains the most up-to-date + // screen state. + readBuffer(BACK); + } + bindFramebufferImpl(DRAW_FRAMEBUFFER, glColorFbo.get(0)); + framebufferTexture2D(FRAMEBUFFER, COLOR_ATTACHMENT0, + TEXTURE_2D, glColorTex.get(frontTex), 0); + if (hasDrawBuffer()) drawBuffer(COLOR_ATTACHMENT0); + blitFramebuffer(0, 0, fboWidth, fboHeight, + 0, 0, fboWidth, fboHeight, + COLOR_BUFFER_BIT, NEAREST); + + readBuffer(BACK); + bindFramebufferImpl(DRAW_FRAMEBUFFER, glColorFbo.get(0)); + framebufferTexture2D(FRAMEBUFFER, COLOR_ATTACHMENT0, + TEXTURE_2D, glColorTex.get(backTex), 0); + drawBuffer(COLOR_ATTACHMENT0); + blitFramebuffer(0, 0, fboWidth, fboHeight, + 0, 0, fboWidth, fboHeight, + COLOR_BUFFER_BIT, NEAREST); + + bindFramebufferImpl(FRAMEBUFFER, 0); + } + + + @Override + protected void beginGL() { + PMatrix3D proj = graphics.projection; + PMatrix3D mdl = graphics.modelview; + if (gl2x != null) { + if (projMatrix == null) { + projMatrix = new float[16]; + } + gl2x.glMatrixMode(GLMatrixFunc.GL_PROJECTION); + projMatrix[ 0] = proj.m00; + projMatrix[ 1] = proj.m10; + projMatrix[ 2] = proj.m20; + projMatrix[ 3] = proj.m30; + projMatrix[ 4] = proj.m01; + projMatrix[ 5] = proj.m11; + projMatrix[ 6] = proj.m21; + projMatrix[ 7] = proj.m31; + projMatrix[ 8] = proj.m02; + projMatrix[ 9] = proj.m12; + projMatrix[10] = proj.m22; + projMatrix[11] = proj.m32; + projMatrix[12] = proj.m03; + projMatrix[13] = proj.m13; + projMatrix[14] = proj.m23; + projMatrix[15] = proj.m33; + gl2x.glLoadMatrixf(projMatrix, 0); + + if (mvMatrix == null) { + mvMatrix = new float[16]; + } + gl2x.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); + mvMatrix[ 0] = mdl.m00; + mvMatrix[ 1] = mdl.m10; + mvMatrix[ 2] = mdl.m20; + mvMatrix[ 3] = mdl.m30; + mvMatrix[ 4] = mdl.m01; + mvMatrix[ 5] = mdl.m11; + mvMatrix[ 6] = mdl.m21; + mvMatrix[ 7] = mdl.m31; + mvMatrix[ 8] = mdl.m02; + mvMatrix[ 9] = mdl.m12; + mvMatrix[10] = mdl.m22; + mvMatrix[11] = mdl.m32; + mvMatrix[12] = mdl.m03; + mvMatrix[13] = mdl.m13; + mvMatrix[14] = mdl.m23; + mvMatrix[15] = mdl.m33; + gl2x.glLoadMatrixf(mvMatrix, 0); + } + } + + + @Override + protected boolean hasFBOs() { + if (context.hasBasicFBOSupport()) return true; + else return super.hasFBOs(); + } + + + @Override + protected boolean hasShaders() { + if (context.hasGLSL()) return true; + else return super.hasShaders(); + } + + + public void init(GLAutoDrawable glDrawable) { + capabilities = glDrawable.getChosenGLCapabilities(); + if (!hasFBOs()) { + throw new RuntimeException(MISSING_FBO_ERROR); + } + if (!hasShaders()) { + throw new RuntimeException(MISSING_GLSL_ERROR); + } + } + + + /////////////////////////////////////////////////////////// + + // Utility functions + + + @Override + protected void enableTexturing(int target) { + if (target == TEXTURE_2D) { + texturingTargets[0] = true; + } else if (target == TEXTURE_RECTANGLE) { + texturingTargets[1] = true; + } + } + + + @Override + protected void disableTexturing(int target) { + if (target == TEXTURE_2D) { + texturingTargets[0] = false; + } else if (target == TEXTURE_RECTANGLE) { + texturingTargets[1] = false; + } + } + + + /** + * Convenience method to get a legit FontMetrics object. Where possible, + * override this any renderer subclass so that you're not using what's + * returned by getDefaultToolkit() to get your metrics. + */ + @SuppressWarnings("deprecation") + private FontMetrics getFontMetrics(Font font) { // ignore + return Toolkit.getDefaultToolkit().getFontMetrics(font); + } + + + /** + * Convenience method to jump through some Java2D hoops and get an FRC. + */ + private FontRenderContext getFontRenderContext(Font font) { // ignore + return getFontMetrics(font).getFontRenderContext(); + } + + + @Override + protected int getFontAscent(Object font) { + return getFontMetrics((Font) font).getAscent(); + } + + + @Override + protected int getFontDescent(Object font) { + return getFontMetrics((Font) font).getDescent(); + } + + + @Override + protected int getTextWidth(Object font, char[] buffer, int start, int stop) { + // maybe should use one of the newer/fancier functions for this? + int length = stop - start; + FontMetrics metrics = getFontMetrics((Font) font); + return metrics.charsWidth(buffer, start, length); + } + + + @Override + protected Object getDerivedFont(Object font, float size) { + return ((Font) font).deriveFont(size); + } + + @Override + protected int getGLSLVersion() { + VersionNumber vn = context.getGLSLVersionNumber(); + return vn.getMajor() * 100 + vn.getMinor(); + } + + + @Override + protected String[] loadVertexShader(String filename) { + return loadVertexShader(filename, getGLSLVersion()); + } + + + @Override + protected String[] loadFragmentShader(String filename) { + return loadFragmentShader(filename, getGLSLVersion()); + } + + + @Override + protected String[] loadVertexShader(URL url) { + return loadVertexShader(url, getGLSLVersion()); + } + + + @Override + protected String[] loadFragmentShader(URL url) { + return loadFragmentShader(url, getGLSLVersion()); + } + + + @Override + protected String[] loadFragmentShader(String filename, int version) { + String[] fragSrc0 = sketch.loadStrings(filename); + return preprocessFragmentSource(fragSrc0, version); + } + + + @Override + protected String[] loadVertexShader(String filename, int version) { + String[] vertSrc0 = sketch.loadStrings(filename); + return preprocessVertexSource(vertSrc0, version); + } + + + @Override + protected String[] loadFragmentShader(URL url, int version) { + try { + String[] fragSrc0 = PApplet.loadStrings(url.openStream()); + return preprocessFragmentSource(fragSrc0, version); + } catch (IOException e) { + PGraphics.showException("Cannot load fragment shader " + url.getFile()); + } + return null; + } + + + @Override + protected String[] loadVertexShader(URL url, int version) { + try { + String[] vertSrc0 = PApplet.loadStrings(url.openStream()); + return preprocessVertexSource(vertSrc0, version); + } catch (IOException e) { + PGraphics.showException("Cannot load vertex shader " + url.getFile()); + } + return null; + } + + + /////////////////////////////////////////////////////////// + + // Tessellator + + + @Override + protected Tessellator createTessellator(TessellatorCallback callback) { + return new Tessellator(callback); + } + + + protected static class Tessellator implements PGL.Tessellator { + protected GLUtessellator tess; + protected TessellatorCallback callback; + protected GLUCallback gluCallback; + + public Tessellator(TessellatorCallback callback) { + this.callback = callback; + tess = GLU.gluNewTess(); + gluCallback = new GLUCallback(); + + GLU.gluTessCallback(tess, GLU.GLU_TESS_BEGIN, gluCallback); + GLU.gluTessCallback(tess, GLU.GLU_TESS_END, gluCallback); + GLU.gluTessCallback(tess, GLU.GLU_TESS_VERTEX, gluCallback); + GLU.gluTessCallback(tess, GLU.GLU_TESS_COMBINE, gluCallback); + GLU.gluTessCallback(tess, GLU.GLU_TESS_ERROR, gluCallback); + } + + @Override + public void setCallback(int flag) { + GLU.gluTessCallback(tess, flag, gluCallback); + } + + @Override + public void setWindingRule(int rule) { + setProperty(GLU.GLU_TESS_WINDING_RULE, rule); + } + + public void setProperty(int property, int value) { + GLU.gluTessProperty(tess, property, value); + } + + @Override + public void beginPolygon() { + beginPolygon(null); + } + + @Override + public void beginPolygon(Object data) { + GLU.gluTessBeginPolygon(tess, data); + } + + @Override + public void endPolygon() { + GLU.gluTessEndPolygon(tess); + } + + @Override + public void beginContour() { + GLU.gluTessBeginContour(tess); + } + + @Override + public void endContour() { + GLU.gluTessEndContour(tess); + } + + @Override + public void addVertex(double[] v) { + addVertex(v, 0, v); + } + + @Override + public void addVertex(double[] v, int n, Object data) { + GLU.gluTessVertex(tess, v, n, data); + } + + protected class GLUCallback extends GLUtessellatorCallbackAdapter { + @Override + public void begin(int type) { + callback.begin(type); + } + + @Override + public void end() { + callback.end(); + } + + @Override + public void vertex(Object data) { + callback.vertex(data); + } + + @Override + public void combine(double[] coords, Object[] data, + float[] weight, Object[] outData) { + callback.combine(coords, data, weight, outData); + } + + @Override + public void error(int errnum) { + callback.error(errnum); + } + } + } + + + @Override + protected String tessError(int err) { + return glu.gluErrorString(err); + } + + + /////////////////////////////////////////////////////////// + + // Font outline + + + static { + SHAPE_TEXT_SUPPORTED = true; + SEG_MOVETO = PathIterator.SEG_MOVETO; + SEG_LINETO = PathIterator.SEG_LINETO; + SEG_QUADTO = PathIterator.SEG_QUADTO; + SEG_CUBICTO = PathIterator.SEG_CUBICTO; + SEG_CLOSE = PathIterator.SEG_CLOSE; + } + + + @Override + protected FontOutline createFontOutline(char ch, Object font) { + return new FontOutline(ch, (Font) font); + } + + + protected class FontOutline implements PGL.FontOutline { + PathIterator iter; + + public FontOutline(char ch, Font font) { + char textArray[] = new char[] { ch }; + FontRenderContext frc = getFontRenderContext(font); + GlyphVector gv = font.createGlyphVector(frc, textArray); + Shape shp = gv.getOutline(); + iter = shp.getPathIterator(null); + } + + public boolean isDone() { + return iter.isDone(); + } + + public int currentSegment(float coords[]) { + return iter.currentSegment(coords); + } + + public void next() { + iter.next(); + } + } + + + /////////////////////////////////////////////////////////// + + // Constants + + static { + FALSE = GL.GL_FALSE; + TRUE = GL.GL_TRUE; + + INT = GL2ES2.GL_INT; + BYTE = GL.GL_BYTE; + SHORT = GL.GL_SHORT; + FLOAT = GL.GL_FLOAT; + BOOL = GL2ES2.GL_BOOL; + UNSIGNED_INT = GL.GL_UNSIGNED_INT; + UNSIGNED_BYTE = GL.GL_UNSIGNED_BYTE; + UNSIGNED_SHORT = GL.GL_UNSIGNED_SHORT; + + RGB = GL.GL_RGB; + RGBA = GL.GL_RGBA; + ALPHA = GL.GL_ALPHA; + LUMINANCE = GL.GL_LUMINANCE; + LUMINANCE_ALPHA = GL.GL_LUMINANCE_ALPHA; + + UNSIGNED_SHORT_5_6_5 = GL.GL_UNSIGNED_SHORT_5_6_5; + UNSIGNED_SHORT_4_4_4_4 = GL.GL_UNSIGNED_SHORT_4_4_4_4; + UNSIGNED_SHORT_5_5_5_1 = GL.GL_UNSIGNED_SHORT_5_5_5_1; + + RGBA4 = GL.GL_RGBA4; + RGB5_A1 = GL.GL_RGB5_A1; + RGB565 = GL.GL_RGB565; + RGB8 = GL.GL_RGB8; + RGBA8 = GL.GL_RGBA8; + ALPHA8 = GL.GL_ALPHA8; + + READ_ONLY = GL2ES3.GL_READ_ONLY; + WRITE_ONLY = GL.GL_WRITE_ONLY; + READ_WRITE = GL2ES3.GL_READ_WRITE; + + TESS_WINDING_NONZERO = GLU.GLU_TESS_WINDING_NONZERO; + TESS_WINDING_ODD = GLU.GLU_TESS_WINDING_ODD; + TESS_EDGE_FLAG = GLU.GLU_TESS_EDGE_FLAG; + + GENERATE_MIPMAP_HINT = GL.GL_GENERATE_MIPMAP_HINT; + FASTEST = GL.GL_FASTEST; + NICEST = GL.GL_NICEST; + DONT_CARE = GL.GL_DONT_CARE; + + VENDOR = GL.GL_VENDOR; + RENDERER = GL.GL_RENDERER; + VERSION = GL.GL_VERSION; + EXTENSIONS = GL.GL_EXTENSIONS; + SHADING_LANGUAGE_VERSION = GL2ES2.GL_SHADING_LANGUAGE_VERSION; + + MAX_SAMPLES = GL.GL_MAX_SAMPLES; + SAMPLES = GL.GL_SAMPLES; + + ALIASED_LINE_WIDTH_RANGE = GL.GL_ALIASED_LINE_WIDTH_RANGE; + ALIASED_POINT_SIZE_RANGE = GL.GL_ALIASED_POINT_SIZE_RANGE; + + DEPTH_BITS = GL.GL_DEPTH_BITS; + STENCIL_BITS = GL.GL_STENCIL_BITS; + + CCW = GL.GL_CCW; + CW = GL.GL_CW; + + VIEWPORT = GL.GL_VIEWPORT; + + ARRAY_BUFFER = GL.GL_ARRAY_BUFFER; + ELEMENT_ARRAY_BUFFER = GL.GL_ELEMENT_ARRAY_BUFFER; + PIXEL_PACK_BUFFER = GL2ES3.GL_PIXEL_PACK_BUFFER; + + MAX_VERTEX_ATTRIBS = GL2ES2.GL_MAX_VERTEX_ATTRIBS; + + STATIC_DRAW = GL.GL_STATIC_DRAW; + DYNAMIC_DRAW = GL.GL_DYNAMIC_DRAW; + STREAM_DRAW = GL2ES2.GL_STREAM_DRAW; + STREAM_READ = GL2ES3.GL_STREAM_READ; + + BUFFER_SIZE = GL.GL_BUFFER_SIZE; + BUFFER_USAGE = GL.GL_BUFFER_USAGE; + + POINTS = GL.GL_POINTS; + LINE_STRIP = GL.GL_LINE_STRIP; + LINE_LOOP = GL.GL_LINE_LOOP; + LINES = GL.GL_LINES; + TRIANGLE_FAN = GL.GL_TRIANGLE_FAN; + TRIANGLE_STRIP = GL.GL_TRIANGLE_STRIP; + TRIANGLES = GL.GL_TRIANGLES; + + CULL_FACE = GL.GL_CULL_FACE; + FRONT = GL.GL_FRONT; + BACK = GL.GL_BACK; + FRONT_AND_BACK = GL.GL_FRONT_AND_BACK; + + POLYGON_OFFSET_FILL = GL.GL_POLYGON_OFFSET_FILL; + + UNPACK_ALIGNMENT = GL.GL_UNPACK_ALIGNMENT; + PACK_ALIGNMENT = GL.GL_PACK_ALIGNMENT; + + TEXTURE_2D = GL.GL_TEXTURE_2D; + TEXTURE_RECTANGLE = GL2GL3.GL_TEXTURE_RECTANGLE; + + TEXTURE_BINDING_2D = GL.GL_TEXTURE_BINDING_2D; + TEXTURE_BINDING_RECTANGLE = GL2GL3.GL_TEXTURE_BINDING_RECTANGLE; + + MAX_TEXTURE_SIZE = GL.GL_MAX_TEXTURE_SIZE; + TEXTURE_MAX_ANISOTROPY = GL.GL_TEXTURE_MAX_ANISOTROPY_EXT; + MAX_TEXTURE_MAX_ANISOTROPY = GL.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT; + + MAX_VERTEX_TEXTURE_IMAGE_UNITS = GL2ES2.GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS; + MAX_TEXTURE_IMAGE_UNITS = GL2ES2.GL_MAX_TEXTURE_IMAGE_UNITS; + MAX_COMBINED_TEXTURE_IMAGE_UNITS = GL2ES2.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS; + + NUM_COMPRESSED_TEXTURE_FORMATS = GL.GL_NUM_COMPRESSED_TEXTURE_FORMATS; + COMPRESSED_TEXTURE_FORMATS = GL.GL_COMPRESSED_TEXTURE_FORMATS; + + NEAREST = GL.GL_NEAREST; + LINEAR = GL.GL_LINEAR; + LINEAR_MIPMAP_NEAREST = GL.GL_LINEAR_MIPMAP_NEAREST; + LINEAR_MIPMAP_LINEAR = GL.GL_LINEAR_MIPMAP_LINEAR; + + CLAMP_TO_EDGE = GL.GL_CLAMP_TO_EDGE; + REPEAT = GL.GL_REPEAT; + + TEXTURE0 = GL.GL_TEXTURE0; + TEXTURE1 = GL.GL_TEXTURE1; + TEXTURE2 = GL.GL_TEXTURE2; + TEXTURE3 = GL.GL_TEXTURE3; + TEXTURE_MIN_FILTER = GL.GL_TEXTURE_MIN_FILTER; + TEXTURE_MAG_FILTER = GL.GL_TEXTURE_MAG_FILTER; + TEXTURE_WRAP_S = GL.GL_TEXTURE_WRAP_S; + TEXTURE_WRAP_T = GL.GL_TEXTURE_WRAP_T; + TEXTURE_WRAP_R = GL2ES2.GL_TEXTURE_WRAP_R; + + TEXTURE_CUBE_MAP = GL.GL_TEXTURE_CUBE_MAP; + TEXTURE_CUBE_MAP_POSITIVE_X = GL.GL_TEXTURE_CUBE_MAP_POSITIVE_X; + TEXTURE_CUBE_MAP_POSITIVE_Y = GL.GL_TEXTURE_CUBE_MAP_POSITIVE_Y; + TEXTURE_CUBE_MAP_POSITIVE_Z = GL.GL_TEXTURE_CUBE_MAP_POSITIVE_Z; + TEXTURE_CUBE_MAP_NEGATIVE_X = GL.GL_TEXTURE_CUBE_MAP_NEGATIVE_X; + TEXTURE_CUBE_MAP_NEGATIVE_Y = GL.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y; + TEXTURE_CUBE_MAP_NEGATIVE_Z = GL.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z; + + VERTEX_SHADER = GL2ES2.GL_VERTEX_SHADER; + FRAGMENT_SHADER = GL2ES2.GL_FRAGMENT_SHADER; + INFO_LOG_LENGTH = GL2ES2.GL_INFO_LOG_LENGTH; + SHADER_SOURCE_LENGTH = GL2ES2.GL_SHADER_SOURCE_LENGTH; + COMPILE_STATUS = GL2ES2.GL_COMPILE_STATUS; + LINK_STATUS = GL2ES2.GL_LINK_STATUS; + VALIDATE_STATUS = GL2ES2.GL_VALIDATE_STATUS; + SHADER_TYPE = GL2ES2.GL_SHADER_TYPE; + DELETE_STATUS = GL2ES2.GL_DELETE_STATUS; + + FLOAT_VEC2 = GL2ES2.GL_FLOAT_VEC2; + FLOAT_VEC3 = GL2ES2.GL_FLOAT_VEC3; + FLOAT_VEC4 = GL2ES2.GL_FLOAT_VEC4; + FLOAT_MAT2 = GL2ES2.GL_FLOAT_MAT2; + FLOAT_MAT3 = GL2ES2.GL_FLOAT_MAT3; + FLOAT_MAT4 = GL2ES2.GL_FLOAT_MAT4; + INT_VEC2 = GL2ES2.GL_INT_VEC2; + INT_VEC3 = GL2ES2.GL_INT_VEC3; + INT_VEC4 = GL2ES2.GL_INT_VEC4; + BOOL_VEC2 = GL2ES2.GL_BOOL_VEC2; + BOOL_VEC3 = GL2ES2.GL_BOOL_VEC3; + BOOL_VEC4 = GL2ES2.GL_BOOL_VEC4; + SAMPLER_2D = GL2ES2.GL_SAMPLER_2D; + SAMPLER_CUBE = GL2ES2.GL_SAMPLER_CUBE; + + LOW_FLOAT = GL2ES2.GL_LOW_FLOAT; + MEDIUM_FLOAT = GL2ES2.GL_MEDIUM_FLOAT; + HIGH_FLOAT = GL2ES2.GL_HIGH_FLOAT; + LOW_INT = GL2ES2.GL_LOW_INT; + MEDIUM_INT = GL2ES2.GL_MEDIUM_INT; + HIGH_INT = GL2ES2.GL_HIGH_INT; + + CURRENT_VERTEX_ATTRIB = GL2ES2.GL_CURRENT_VERTEX_ATTRIB; + + VERTEX_ATTRIB_ARRAY_BUFFER_BINDING = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING; + VERTEX_ATTRIB_ARRAY_ENABLED = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_ENABLED; + VERTEX_ATTRIB_ARRAY_SIZE = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_SIZE; + VERTEX_ATTRIB_ARRAY_STRIDE = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_STRIDE; + VERTEX_ATTRIB_ARRAY_TYPE = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_TYPE; + VERTEX_ATTRIB_ARRAY_NORMALIZED = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_NORMALIZED; + VERTEX_ATTRIB_ARRAY_POINTER = GL2ES2.GL_VERTEX_ATTRIB_ARRAY_POINTER; + + BLEND = GL.GL_BLEND; + ONE = GL.GL_ONE; + ZERO = GL.GL_ZERO; + SRC_ALPHA = GL.GL_SRC_ALPHA; + DST_ALPHA = GL.GL_DST_ALPHA; + ONE_MINUS_SRC_ALPHA = GL.GL_ONE_MINUS_SRC_ALPHA; + ONE_MINUS_DST_COLOR = GL.GL_ONE_MINUS_DST_COLOR; + ONE_MINUS_SRC_COLOR = GL.GL_ONE_MINUS_SRC_COLOR; + DST_COLOR = GL.GL_DST_COLOR; + SRC_COLOR = GL.GL_SRC_COLOR; + + SAMPLE_ALPHA_TO_COVERAGE = GL.GL_SAMPLE_ALPHA_TO_COVERAGE; + SAMPLE_COVERAGE = GL.GL_SAMPLE_COVERAGE; + + KEEP = GL.GL_KEEP; + REPLACE = GL.GL_REPLACE; + INCR = GL.GL_INCR; + DECR = GL.GL_DECR; + INVERT = GL.GL_INVERT; + INCR_WRAP = GL.GL_INCR_WRAP; + DECR_WRAP = GL.GL_DECR_WRAP; + NEVER = GL.GL_NEVER; + ALWAYS = GL.GL_ALWAYS; + + EQUAL = GL.GL_EQUAL; + LESS = GL.GL_LESS; + LEQUAL = GL.GL_LEQUAL; + GREATER = GL.GL_GREATER; + GEQUAL = GL.GL_GEQUAL; + NOTEQUAL = GL.GL_NOTEQUAL; + + FUNC_ADD = GL.GL_FUNC_ADD; + FUNC_MIN = GL2ES3.GL_MIN; + FUNC_MAX = GL2ES3.GL_MAX; + FUNC_REVERSE_SUBTRACT = GL.GL_FUNC_REVERSE_SUBTRACT; + FUNC_SUBTRACT = GL.GL_FUNC_SUBTRACT; + + DITHER = GL.GL_DITHER; + + CONSTANT_COLOR = GL2ES2.GL_CONSTANT_COLOR; + CONSTANT_ALPHA = GL2ES2.GL_CONSTANT_ALPHA; + ONE_MINUS_CONSTANT_COLOR = GL2ES2.GL_ONE_MINUS_CONSTANT_COLOR; + ONE_MINUS_CONSTANT_ALPHA = GL2ES2.GL_ONE_MINUS_CONSTANT_ALPHA; + SRC_ALPHA_SATURATE = GL.GL_SRC_ALPHA_SATURATE; + + SCISSOR_TEST = GL.GL_SCISSOR_TEST; + STENCIL_TEST = GL.GL_STENCIL_TEST; + DEPTH_TEST = GL.GL_DEPTH_TEST; + DEPTH_WRITEMASK = GL.GL_DEPTH_WRITEMASK; + + COLOR_BUFFER_BIT = GL.GL_COLOR_BUFFER_BIT; + DEPTH_BUFFER_BIT = GL.GL_DEPTH_BUFFER_BIT; + STENCIL_BUFFER_BIT = GL.GL_STENCIL_BUFFER_BIT; + + FRAMEBUFFER = GL.GL_FRAMEBUFFER; + COLOR_ATTACHMENT0 = GL.GL_COLOR_ATTACHMENT0; + COLOR_ATTACHMENT1 = GL2ES2.GL_COLOR_ATTACHMENT1; + COLOR_ATTACHMENT2 = GL2ES2.GL_COLOR_ATTACHMENT2; + COLOR_ATTACHMENT3 = GL2ES2.GL_COLOR_ATTACHMENT3; + RENDERBUFFER = GL.GL_RENDERBUFFER; + DEPTH_ATTACHMENT = GL.GL_DEPTH_ATTACHMENT; + STENCIL_ATTACHMENT = GL.GL_STENCIL_ATTACHMENT; + READ_FRAMEBUFFER = GL.GL_READ_FRAMEBUFFER; + DRAW_FRAMEBUFFER = GL.GL_DRAW_FRAMEBUFFER; + + RGBA8 = GL.GL_RGBA8; + DEPTH24_STENCIL8 = GL.GL_DEPTH24_STENCIL8; + + DEPTH_COMPONENT = GL2ES2.GL_DEPTH_COMPONENT; + DEPTH_COMPONENT16 = GL.GL_DEPTH_COMPONENT16; + DEPTH_COMPONENT24 = GL.GL_DEPTH_COMPONENT24; + DEPTH_COMPONENT32 = GL.GL_DEPTH_COMPONENT32; + + STENCIL_INDEX = GL2ES2.GL_STENCIL_INDEX; + STENCIL_INDEX1 = GL.GL_STENCIL_INDEX1; + STENCIL_INDEX4 = GL.GL_STENCIL_INDEX4; + STENCIL_INDEX8 = GL.GL_STENCIL_INDEX8; + + DEPTH_STENCIL = GL.GL_DEPTH_STENCIL; + + FRAMEBUFFER_COMPLETE = GL.GL_FRAMEBUFFER_COMPLETE; + FRAMEBUFFER_UNDEFINED = GL2ES3.GL_FRAMEBUFFER_UNDEFINED; + FRAMEBUFFER_INCOMPLETE_ATTACHMENT = GL.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT; + FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT = GL.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT; + FRAMEBUFFER_INCOMPLETE_DIMENSIONS = GL.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS; + FRAMEBUFFER_INCOMPLETE_FORMATS = GL.GL_FRAMEBUFFER_INCOMPLETE_FORMATS; + FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER = GL2GL3.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER; + FRAMEBUFFER_INCOMPLETE_READ_BUFFER = GL2GL3.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER; + FRAMEBUFFER_UNSUPPORTED = GL.GL_FRAMEBUFFER_UNSUPPORTED; + FRAMEBUFFER_INCOMPLETE_MULTISAMPLE = GL.GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE; + FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS = GL3ES3.GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS; + + FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE = GL.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE; + FRAMEBUFFER_ATTACHMENT_OBJECT_NAME = GL.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME; + FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL = GL.GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL; + FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE = GL.GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE; + + RENDERBUFFER_WIDTH = GL.GL_RENDERBUFFER_WIDTH; + RENDERBUFFER_HEIGHT = GL.GL_RENDERBUFFER_HEIGHT; + RENDERBUFFER_RED_SIZE = GL.GL_RENDERBUFFER_RED_SIZE; + RENDERBUFFER_GREEN_SIZE = GL.GL_RENDERBUFFER_GREEN_SIZE; + RENDERBUFFER_BLUE_SIZE = GL.GL_RENDERBUFFER_BLUE_SIZE; + RENDERBUFFER_ALPHA_SIZE = GL.GL_RENDERBUFFER_ALPHA_SIZE; + RENDERBUFFER_DEPTH_SIZE = GL.GL_RENDERBUFFER_DEPTH_SIZE; + RENDERBUFFER_STENCIL_SIZE = GL.GL_RENDERBUFFER_STENCIL_SIZE; + RENDERBUFFER_INTERNAL_FORMAT = GL.GL_RENDERBUFFER_INTERNAL_FORMAT; + + MULTISAMPLE = GL.GL_MULTISAMPLE; + LINE_SMOOTH = GL.GL_LINE_SMOOTH; + POLYGON_SMOOTH = GL2GL3.GL_POLYGON_SMOOTH; + + SYNC_GPU_COMMANDS_COMPLETE = GL3ES3.GL_SYNC_GPU_COMMANDS_COMPLETE; + ALREADY_SIGNALED = GL3ES3.GL_ALREADY_SIGNALED; + CONDITION_SATISFIED = GL3ES3.GL_CONDITION_SATISFIED; + } + + /////////////////////////////////////////////////////////// + + // Special Functions + + @Override + public void flush() { + gl.glFlush(); + } + + @Override + public void finish() { + gl.glFinish(); + } + + @Override + public void hint(int target, int hint) { + gl.glHint(target, hint); + } + + /////////////////////////////////////////////////////////// + + // State and State Requests + + @Override + public void enable(int value) { + if (-1 < value) { + gl.glEnable(value); + } + } + + @Override + public void disable(int value) { + if (-1 < value) { + gl.glDisable(value); + } + } + + @Override + public void getBooleanv(int value, IntBuffer data) { + if (-1 < value) { + if (byteBuffer.capacity() < data.capacity()) { + byteBuffer = allocateDirectByteBuffer(data.capacity()); + } + gl.glGetBooleanv(value, byteBuffer); + for (int i = 0; i < data.capacity(); i++) { + data.put(i, byteBuffer.get(i)); + } + } else { + fillIntBuffer(data, 0, data.capacity() - 1, 0); + } + } + + @Override + public void getIntegerv(int value, IntBuffer data) { + if (-1 < value) { + gl.glGetIntegerv(value, data); + } else { + fillIntBuffer(data, 0, data.capacity() - 1, 0); + } + } + + @Override + public void getFloatv(int value, FloatBuffer data) { + if (-1 < value) { + gl.glGetFloatv(value, data); + } else { + fillFloatBuffer(data, 0, data.capacity() - 1, 0); + } + } + + @Override + public boolean isEnabled(int value) { + return gl.glIsEnabled(value); + } + + @Override + public String getString(int name) { + return gl.glGetString(name); + } + + /////////////////////////////////////////////////////////// + + // Error Handling + + @Override + public int getError() { + return gl.glGetError(); + } + + @Override + public String errorString(int err) { + return glu.gluErrorString(err); + } + + ////////////////////////////////////////////////////////////////////////////// + + // Buffer Objects + + @Override + public void genBuffers(int n, IntBuffer buffers) { + gl.glGenBuffers(n, buffers); + } + + @Override + public void deleteBuffers(int n, IntBuffer buffers) { + gl.glDeleteBuffers(n, buffers); + } + + @Override + public void bindBuffer(int target, int buffer) { + gl.glBindBuffer(target, buffer); + } + + @Override + public void bufferData(int target, int size, Buffer data, int usage) { + gl.glBufferData(target, size, data, usage); + } + + @Override + public void bufferSubData(int target, int offset, int size, Buffer data) { + gl.glBufferSubData(target, offset, size, data); + } + + @Override + public void isBuffer(int buffer) { + gl.glIsBuffer(buffer); + } + + @Override + public void getBufferParameteriv(int target, int value, IntBuffer data) { + gl.glGetBufferParameteriv(target, value, data); + } + + @Override + public ByteBuffer mapBuffer(int target, int access) { + return gl2.glMapBuffer(target, access); + } + + @Override + public ByteBuffer mapBufferRange(int target, int offset, int length, int access) { + if (gl2x != null) { + return gl2x.glMapBufferRange(target, offset, length, access); + } else if (gl3 != null) { + return gl3.glMapBufferRange(target, offset, length, access); + } else { + throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glMapBufferRange()")); + } + } + + @Override + public void unmapBuffer(int target) { + gl2.glUnmapBuffer(target); + } + + ////////////////////////////////////////////////////////////////////////////// + + // Synchronization + + @Override + public long fenceSync(int condition, int flags) { + if (gl3es3 != null) { + return gl3es3.glFenceSync(condition, flags); + } else { + throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "fenceSync()")); + } + } + + @Override + public void deleteSync(long sync) { + if (gl3es3 != null) { + gl3es3.glDeleteSync(sync); + } else { + throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "deleteSync()")); + } + } + + @Override + public int clientWaitSync(long sync, int flags, long timeout) { + if (gl3es3 != null) { + return gl3es3.glClientWaitSync(sync, flags, timeout); + } else { + throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "clientWaitSync()")); + } + } + + ////////////////////////////////////////////////////////////////////////////// + + // Viewport and Clipping + + @Override + public void depthRangef(float n, float f) { + gl.glDepthRangef(n, f); + } + + @Override + public void viewport(int x, int y, int w, int h) { + float scale = getPixelScale(); + viewportImpl((int)scale * x, (int)(scale * y), (int)(scale * w), (int)(scale * h)); + } + + @Override + protected void viewportImpl(int x, int y, int w, int h) { + gl.glViewport(x, y, w, h); + } + + ////////////////////////////////////////////////////////////////////////////// + + // Reading Pixels + + @Override + protected void readPixelsImpl(int x, int y, int width, int height, int format, int type, Buffer buffer) { + gl.glReadPixels(x, y, width, height, format, type, buffer); + } + + @Override + protected void readPixelsImpl(int x, int y, int width, int height, int format, int type, long offset) { + gl.glReadPixels(x, y, width, height, format, type, 0); + } + + ////////////////////////////////////////////////////////////////////////////// + + // Vertices + + @Override + public void vertexAttrib1f(int index, float value) { + gl2.glVertexAttrib1f(index, value); + } + + @Override + public void vertexAttrib2f(int index, float value0, float value1) { + gl2.glVertexAttrib2f(index, value0, value1); + } + + @Override + public void vertexAttrib3f(int index, float value0, float value1, float value2) { + gl2.glVertexAttrib3f(index, value0, value1, value2); + } + + @Override + public void vertexAttrib4f(int index, float value0, float value1, float value2, float value3) { + gl2.glVertexAttrib4f(index, value0, value1, value2, value3); + } + + @Override + public void vertexAttrib1fv(int index, FloatBuffer values) { + gl2.glVertexAttrib1fv(index, values); + } + + @Override + public void vertexAttrib2fv(int index, FloatBuffer values) { + gl2.glVertexAttrib2fv(index, values); + } + + @Override + public void vertexAttrib3fv(int index, FloatBuffer values) { + gl2.glVertexAttrib3fv(index, values); + } + + @Override + public void vertexAttrib4fv(int index, FloatBuffer values) { + gl2.glVertexAttrib4fv(index, values); + } + + @Override + public void vertexAttribPointer(int index, int size, int type, boolean normalized, int stride, int offset) { + gl2.glVertexAttribPointer(index, size, type, normalized, stride, offset); + } + + @Override + public void enableVertexAttribArray(int index) { + gl2.glEnableVertexAttribArray(index); + } + + @Override + public void disableVertexAttribArray(int index) { + gl2.glDisableVertexAttribArray(index); + } + + @Override + public void drawArraysImpl(int mode, int first, int count) { + gl.glDrawArrays(mode, first, count); + } + + @Override + public void drawElementsImpl(int mode, int count, int type, int offset) { + gl.glDrawElements(mode, count, type, offset); + } + + ////////////////////////////////////////////////////////////////////////////// + + // Rasterization + + @Override + public void lineWidth(float width) { + gl.glLineWidth(width); + } + + @Override + public void frontFace(int dir) { + gl.glFrontFace(dir); + } + + @Override + public void cullFace(int mode) { + gl.glCullFace(mode); + } + + @Override + public void polygonOffset(float factor, float units) { + gl.glPolygonOffset(factor, units); + } + + ////////////////////////////////////////////////////////////////////////////// + + // Pixel Rectangles + + @Override + public void pixelStorei(int pname, int param) { + gl.glPixelStorei(pname, param); + } + + /////////////////////////////////////////////////////////// + + // Texturing + + @Override + public void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, Buffer data) { + gl.glTexImage2D(target, level, internalFormat, width, height, border, format, type, data); + } + + @Override + public void copyTexImage2D(int target, int level, int internalFormat, int x, int y, int width, int height, int border) { + gl.glCopyTexImage2D(target, level, internalFormat, x, y, width, height, border); + } + + @Override + public void texSubImage2D(int target, int level, int xOffset, int yOffset, int width, int height, int format, int type, Buffer data) { + gl.glTexSubImage2D(target, level, xOffset, yOffset, width, height, format, type, data); + } + + @Override + public void copyTexSubImage2D(int target, int level, int xOffset, int yOffset, int x, int y, int width, int height) { + gl.glCopyTexSubImage2D(target, level, x, y, xOffset, yOffset, width, height); + } + + @Override + public void compressedTexImage2D(int target, int level, int internalFormat, int width, int height, int border, int imageSize, Buffer data) { + gl.glCompressedTexImage2D(target, level, internalFormat, width, height, border, imageSize, data); + } + + @Override + public void compressedTexSubImage2D(int target, int level, int xOffset, int yOffset, int width, int height, int format, int imageSize, Buffer data) { + gl.glCompressedTexSubImage2D(target, level, xOffset, yOffset, width, height, format, imageSize, data); + } + + @Override + public void texParameteri(int target, int pname, int param) { + gl.glTexParameteri(target, pname, param); + } + + @Override + public void texParameterf(int target, int pname, float param) { + gl.glTexParameterf(target, pname, param); + } + + @Override + public void texParameteriv(int target, int pname, IntBuffer params) { + gl.glTexParameteriv(target, pname, params); + } + + @Override + public void texParameterfv(int target, int pname, FloatBuffer params) { + gl.glTexParameterfv(target, pname, params); + } + + @Override + public void generateMipmap(int target) { + gl.glGenerateMipmap(target); + } + + @Override + public void genTextures(int n, IntBuffer textures) { + gl.glGenTextures(n, textures); + } + + @Override + public void deleteTextures(int n, IntBuffer textures) { + gl.glDeleteTextures(n, textures); + } + + @Override + public void getTexParameteriv(int target, int pname, IntBuffer params) { + gl.glGetTexParameteriv(target, pname, params); + } + + @Override + public void getTexParameterfv(int target, int pname, FloatBuffer params) { + gl.glGetTexParameterfv(target, pname, params); + } + + @Override + public boolean isTexture(int texture) { + return gl.glIsTexture(texture); + } + + @Override + protected void activeTextureImpl(int texture) { + gl.glActiveTexture(texture); + } + + @Override + protected void bindTextureImpl(int target, int texture) { + gl.glBindTexture(target, texture); + } + + /////////////////////////////////////////////////////////// + + // Shaders and Programs + + @Override + public int createShader(int type) { + return gl2.glCreateShader(type); + } + + @Override + public void shaderSource(int shader, String source) { + gl2.glShaderSource(shader, 1, new String[] { source }, (int[]) null, 0); + } + + @Override + public void compileShader(int shader) { + gl2.glCompileShader(shader); + } + + @Override + public void releaseShaderCompiler() { + gl2.glReleaseShaderCompiler(); + } + + @Override + public void deleteShader(int shader) { + gl2.glDeleteShader(shader); + } + + @Override + public void shaderBinary(int count, IntBuffer shaders, int binaryFormat, Buffer binary, int length) { + gl2.glShaderBinary(count, shaders, binaryFormat, binary, length); + } + + @Override + public int createProgram() { + return gl2.glCreateProgram(); + } + + @Override + public void attachShader(int program, int shader) { + gl2.glAttachShader(program, shader); + } + + @Override + public void detachShader(int program, int shader) { + gl2.glDetachShader(program, shader); + } + + @Override + public void linkProgram(int program) { + gl2.glLinkProgram(program); + } + + @Override + public void useProgram(int program) { + gl2.glUseProgram(program); + } + + @Override + public void deleteProgram(int program) { + gl2.glDeleteProgram(program); + } + + @Override + public String getActiveAttrib(int program, int index, IntBuffer size, IntBuffer type) { + int[] tmp = {0, 0, 0}; + byte[] namebuf = new byte[1024]; + gl2.glGetActiveAttrib(program, index, 1024, tmp, 0, tmp, 1, tmp, 2, namebuf, 0); + size.put(tmp[1]); + type.put(tmp[2]); + String name = new String(namebuf, 0, tmp[0]); + return name; + } + + @Override + public int getAttribLocation(int program, String name) { + return gl2.glGetAttribLocation(program, name); + } + + @Override + public void bindAttribLocation(int program, int index, String name) { + gl2.glBindAttribLocation(program, index, name); + } + + @Override + public int getUniformLocation(int program, String name) { + return gl2.glGetUniformLocation(program, name); + } + + @Override + public String getActiveUniform(int program, int index, IntBuffer size, IntBuffer type) { + int[] tmp= {0, 0, 0}; + byte[] namebuf = new byte[1024]; + gl2.glGetActiveUniform(program, index, 1024, tmp, 0, tmp, 1, tmp, 2, namebuf, 0); + size.put(tmp[1]); + type.put(tmp[2]); + String name = new String(namebuf, 0, tmp[0]); + return name; + } + + @Override + public void uniform1i(int location, int value) { + gl2.glUniform1i(location, value); + } + + @Override + public void uniform2i(int location, int value0, int value1) { + gl2.glUniform2i(location, value0, value1); + } + + @Override + public void uniform3i(int location, int value0, int value1, int value2) { + gl2.glUniform3i(location, value0, value1, value2); + } + + @Override + public void uniform4i(int location, int value0, int value1, int value2, int value3) { + gl2.glUniform4i(location, value0, value1, value2, value3); + } + + @Override + public void uniform1f(int location, float value) { + gl2.glUniform1f(location, value); + } + + @Override + public void uniform2f(int location, float value0, float value1) { + gl2.glUniform2f(location, value0, value1); + } + + @Override + public void uniform3f(int location, float value0, float value1, float value2) { + gl2.glUniform3f(location, value0, value1, value2); + } + + @Override + public void uniform4f(int location, float value0, float value1, float value2, float value3) { + gl2.glUniform4f(location, value0, value1, value2, value3); + } + + @Override + public void uniform1iv(int location, int count, IntBuffer v) { + gl2.glUniform1iv(location, count, v); + } + + @Override + public void uniform2iv(int location, int count, IntBuffer v) { + gl2.glUniform2iv(location, count, v); + } + + @Override + public void uniform3iv(int location, int count, IntBuffer v) { + gl2.glUniform3iv(location, count, v); + } + + @Override + public void uniform4iv(int location, int count, IntBuffer v) { + gl2.glUniform4iv(location, count, v); + } + + @Override + public void uniform1fv(int location, int count, FloatBuffer v) { + gl2.glUniform1fv(location, count, v); + } + + @Override + public void uniform2fv(int location, int count, FloatBuffer v) { + gl2.glUniform2fv(location, count, v); + } + + @Override + public void uniform3fv(int location, int count, FloatBuffer v) { + gl2.glUniform3fv(location, count, v); + } + + @Override + public void uniform4fv(int location, int count, FloatBuffer v) { + gl2.glUniform4fv(location, count, v); + } + + @Override + public void uniformMatrix2fv(int location, int count, boolean transpose, FloatBuffer mat) { + gl2.glUniformMatrix2fv(location, count, transpose, mat); + } + + @Override + public void uniformMatrix3fv(int location, int count, boolean transpose, FloatBuffer mat) { + gl2.glUniformMatrix3fv(location, count, transpose, mat); + } + + @Override + public void uniformMatrix4fv(int location, int count, boolean transpose, FloatBuffer mat) { + gl2.glUniformMatrix4fv(location, count, transpose, mat); + } + + @Override + public void validateProgram(int program) { + gl2.glValidateProgram(program); + } + + @Override + public boolean isShader(int shader) { + return gl2.glIsShader(shader); + } + + @Override + public void getShaderiv(int shader, int pname, IntBuffer params) { + gl2.glGetShaderiv(shader, pname, params); + } + + @Override + public void getAttachedShaders(int program, int maxCount, IntBuffer count, IntBuffer shaders) { + gl2.glGetAttachedShaders(program, maxCount, count, shaders); + } + + @Override + public String getShaderInfoLog(int shader) { + int[] val = { 0 }; + gl2.glGetShaderiv(shader, GL2ES2.GL_INFO_LOG_LENGTH, val, 0); + int length = val[0]; + + byte[] log = new byte[length]; + gl2.glGetShaderInfoLog(shader, length, val, 0, log, 0); + return new String(log); + } + + @Override + public String getShaderSource(int shader) { + int[] len = {0}; + byte[] buf = new byte[1024]; + gl2.glGetShaderSource(shader, 1024, len, 0, buf, 0); + return new String(buf, 0, len[0]); + } + + @Override + public void getShaderPrecisionFormat(int shaderType, int precisionType, IntBuffer range, IntBuffer precision) { + gl2.glGetShaderPrecisionFormat(shaderType, precisionType, range, precision); + } + + @Override + public void getVertexAttribfv(int index, int pname, FloatBuffer params) { + gl2.glGetVertexAttribfv(index, pname, params); + } + + @Override + public void getVertexAttribiv(int index, int pname, IntBuffer params) { + gl2.glGetVertexAttribiv(index, pname, params); + } + + @Override + public void getVertexAttribPointerv(int index, int pname, ByteBuffer data) { + throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glGetVertexAttribPointerv()")); + } + + @Override + public void getUniformfv(int program, int location, FloatBuffer params) { + gl2.glGetUniformfv(program, location, params); + } + + @Override + public void getUniformiv(int program, int location, IntBuffer params) { + gl2.glGetUniformiv(program, location, params); + } + + @Override + public boolean isProgram(int program) { + return gl2.glIsProgram(program); + } + + @Override + public void getProgramiv(int program, int pname, IntBuffer params) { + gl2.glGetProgramiv(program, pname, params); + } + + @Override + public String getProgramInfoLog(int program) { + int[] val = { 0 }; + gl2.glGetProgramiv(program, GL2ES2.GL_INFO_LOG_LENGTH, val, 0); + int length = val[0]; + + if (0 < length) { + byte[] log = new byte[length]; + gl2.glGetProgramInfoLog(program, length, val, 0, log, 0); + return new String(log); + } else { + return "Unknown error"; + } + } + + /////////////////////////////////////////////////////////// + + // Per-Fragment Operations + + @Override + public void scissor(int x, int y, int w, int h) { + float scale = getPixelScale(); + gl.glScissor((int)scale * x, (int)(scale * y), (int)(scale * w), (int)(scale * h)); +// gl.glScissor(x, y, w, h); + } + + @Override + public void sampleCoverage(float value, boolean invert) { + gl2.glSampleCoverage(value, invert); + } + + @Override + public void stencilFunc(int func, int ref, int mask) { + gl2.glStencilFunc(func, ref, mask); + } + + @Override + public void stencilFuncSeparate(int face, int func, int ref, int mask) { + gl2.glStencilFuncSeparate(face, func, ref, mask); + } + + @Override + public void stencilOp(int sfail, int dpfail, int dppass) { + gl2.glStencilOp(sfail, dpfail, dppass); + } + + @Override + public void stencilOpSeparate(int face, int sfail, int dpfail, int dppass) { + gl2.glStencilOpSeparate(face, sfail, dpfail, dppass); + } + + @Override + public void depthFunc(int func) { + gl.glDepthFunc(func); + } + + @Override + public void blendEquation(int mode) { + gl.glBlendEquation(mode); + } + + @Override + public void blendEquationSeparate(int modeRGB, int modeAlpha) { + gl.glBlendEquationSeparate(modeRGB, modeAlpha); + } + + @Override + public void blendFunc(int src, int dst) { + gl.glBlendFunc(src, dst); + } + + @Override + public void blendFuncSeparate(int srcRGB, int dstRGB, int srcAlpha, int dstAlpha) { + gl.glBlendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha); + } + + @Override + public void blendColor(float red, float green, float blue, float alpha) { + gl2.glBlendColor(red, green, blue, alpha); + } + + /////////////////////////////////////////////////////////// + + // Whole Framebuffer Operations + + @Override + public void colorMask(boolean r, boolean g, boolean b, boolean a) { + gl.glColorMask(r, g, b, a); + } + + @Override + public void depthMask(boolean mask) { + gl.glDepthMask(mask); + } + + @Override + public void stencilMask(int mask) { + gl.glStencilMask(mask); + } + + @Override + public void stencilMaskSeparate(int face, int mask) { + gl2.glStencilMaskSeparate(face, mask); + } + + @Override + public void clearColor(float r, float g, float b, float a) { + gl.glClearColor(r, g, b, a); + } + + @Override + public void clearDepth(float d) { + gl.glClearDepth(d); + } + + @Override + public void clearStencil(int s) { + gl.glClearStencil(s); + } + + @Override + public void clear(int buf) { + gl.glClear(buf); + } + + /////////////////////////////////////////////////////////// + + // Framebuffers Objects + + @Override + protected void bindFramebufferImpl(int target, int framebuffer) { + gl.glBindFramebuffer(target, framebuffer); + } + + @Override + public void deleteFramebuffers(int n, IntBuffer framebuffers) { + gl.glDeleteFramebuffers(n, framebuffers); + } + + @Override + public void genFramebuffers(int n, IntBuffer framebuffers) { + gl.glGenFramebuffers(n, framebuffers); + } + + @Override + public void bindRenderbuffer(int target, int renderbuffer) { + gl.glBindRenderbuffer(target, renderbuffer); + } + + @Override + public void deleteRenderbuffers(int n, IntBuffer renderbuffers) { + gl.glDeleteRenderbuffers(n, renderbuffers); + } + + @Override + public void genRenderbuffers(int n, IntBuffer renderbuffers) { + gl.glGenRenderbuffers(n, renderbuffers); + } + + @Override + public void renderbufferStorage(int target, int internalFormat, int width, int height) { + gl.glRenderbufferStorage(target, internalFormat, width, height); + } + + @Override + public void framebufferRenderbuffer(int target, int attachment, int rendbuferfTarget, int renderbuffer) { + gl.glFramebufferRenderbuffer(target, attachment, rendbuferfTarget, renderbuffer); + } + + @Override + public void framebufferTexture2D(int target, int attachment, int texTarget, int texture, int level) { + gl.glFramebufferTexture2D(target, attachment, texTarget, texture, level); + } + + @Override + public int checkFramebufferStatus(int target) { + return gl.glCheckFramebufferStatus(target); + } + + @Override + public boolean isFramebuffer(int framebuffer) { + return gl2.glIsFramebuffer(framebuffer); + } + + @Override + public void getFramebufferAttachmentParameteriv(int target, int attachment, int pname, IntBuffer params) { + gl2.glGetFramebufferAttachmentParameteriv(target, attachment, pname, params); + } + + @Override + public boolean isRenderbuffer(int renderbuffer) { + return gl2.glIsRenderbuffer(renderbuffer); + } + + @Override + public void getRenderbufferParameteriv(int target, int pname, IntBuffer params) { + gl2.glGetRenderbufferParameteriv(target, pname, params); + } + + @Override + public void blitFramebuffer(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) { + if (gl2x != null) { + gl2x.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + } else if (gl3 != null) { + gl3.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + } else { + throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glBlitFramebuffer()")); + } + } + + @Override + public void renderbufferStorageMultisample(int target, int samples, int format, int width, int height) { + if (gl2x != null) { + gl2x.glRenderbufferStorageMultisample(target, samples, format, width, height); + } else if (gl3 != null) { + gl3.glRenderbufferStorageMultisample(target, samples, format, width, height); + } else { + throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glRenderbufferStorageMultisample()")); + } + } + + @Override + public void readBuffer(int buf) { + if (gl2x != null) { + gl2x.glReadBuffer(buf); + } else if (gl3 != null) { + gl3.glReadBuffer(buf); + } else { + throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glReadBuffer()")); + } + } + + @Override + public void drawBuffer(int buf) { + if (gl2x != null) { + gl2x.glDrawBuffer(buf); + } else if (gl3 != null) { + gl3.glDrawBuffer(buf); + } else { + throw new RuntimeException(String.format(MISSING_GLFUNC_ERROR, "glDrawBuffer()")); + } + } +} diff --git a/src/main/java/processing/opengl/PShader.java b/src/main/java/processing/opengl/PShader.java new file mode 100644 index 0000000..d5f8f1d --- /dev/null +++ b/src/main/java/processing/opengl/PShader.java @@ -0,0 +1,1478 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.opengl; + +import processing.core.*; +import processing.opengl.PGraphicsOpenGL.GLResourceShader; + +import java.net.URL; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.HashMap; + +/** + * This class encapsulates a GLSL shader program, including a vertex + * and a fragment shader. Based on the GLSLShader class from GLGraphics, which + * in turn was originally based in the code by JohnG: + * http://processing.org/discourse/beta/num_1159494801.html + * + * @webref rendering:shaders + */ +public class PShader implements PConstants { + static protected final int POINT = 0; + static protected final int LINE = 1; + static protected final int POLY = 2; + static protected final int COLOR = 3; + static protected final int LIGHT = 4; + static protected final int TEXTURE = 5; + static protected final int TEXLIGHT = 6; + + static protected String pointShaderAttrRegexp = + "attribute *vec2 *offset"; + static protected String pointShaderInRegexp = + "in *vec2 *offset;"; + static protected String lineShaderAttrRegexp = + "attribute *vec4 *direction"; + static protected String lineShaderInRegexp = + "in *vec4 *direction"; + static protected String pointShaderDefRegexp = + "#define *PROCESSING_POINT_SHADER"; + static protected String lineShaderDefRegexp = + "#define *PROCESSING_LINE_SHADER"; + static protected String colorShaderDefRegexp = + "#define *PROCESSING_COLOR_SHADER"; + static protected String lightShaderDefRegexp = + "#define *PROCESSING_LIGHT_SHADER"; + static protected String texShaderDefRegexp = + "#define *PROCESSING_TEXTURE_SHADER"; + static protected String texlightShaderDefRegexp = + "#define *PROCESSING_TEXLIGHT_SHADER"; + static protected String polyShaderDefRegexp = + "#define *PROCESSING_POLYGON_SHADER"; + static protected String triShaderAttrRegexp = + "#define *PROCESSING_TRIANGLES_SHADER"; + static protected String quadShaderAttrRegexp = + "#define *PROCESSING_QUADS_SHADER"; + + protected PApplet parent; + // The main renderer associated to the parent PApplet. + //protected PGraphicsOpenGL pgMain; + // We need a reference to the renderer since a shader might + // be called by different renderers within a single application + // (the one corresponding to the main surface, or other offscreen + // renderers). + protected PGraphicsOpenGL primaryPG; + protected PGraphicsOpenGL currentPG; + protected PGL pgl; + protected int context; // The context that created this shader. + + // The shader type: POINT, LINE, POLY, etc. + protected int type; + + public int glProgram; + public int glVertex; + public int glFragment; + private GLResourceShader glres; + + protected URL vertexURL; + protected URL fragmentURL; + + protected String vertexFilename; + protected String fragmentFilename; + + protected String[] vertexShaderSource; + protected String[] fragmentShaderSource; + + protected boolean bound; + + protected HashMap uniformValues = null; + + protected HashMap textures; + protected HashMap texUnits; + + // Direct buffers to pass shader data to GL + protected IntBuffer intBuffer; + protected FloatBuffer floatBuffer; + + protected boolean loadedAttributes = false; + protected boolean loadedUniforms = false; + + // Uniforms common to all shader types + protected int transformMatLoc; + protected int modelviewMatLoc; + protected int projectionMatLoc; + protected int ppixelsLoc; + protected int ppixelsUnit; + protected int viewportLoc; + protected int resolutionLoc; + + // Uniforms only for lines and points + protected int perspectiveLoc; + protected int scaleLoc; + + // Lighting uniforms + protected int lightCountLoc; + protected int lightPositionLoc; + protected int lightNormalLoc; + protected int lightAmbientLoc; + protected int lightDiffuseLoc; + protected int lightSpecularLoc; + protected int lightFalloffLoc; + protected int lightSpotLoc; + + // Texturing uniforms + protected Texture texture; + protected int texUnit; + protected int textureLoc; + protected int texMatrixLoc; + protected int texOffsetLoc; + protected float[] tcmat; + + // Vertex attributes + protected int vertexLoc; + protected int colorLoc; + protected int normalLoc; + protected int texCoordLoc; + protected int normalMatLoc; + protected int directionLoc; + protected int offsetLoc; + protected int ambientLoc; + protected int specularLoc; + protected int emissiveLoc; + protected int shininessLoc; + + public PShader() { + parent = null; + pgl = null; + context = -1; + + this.vertexURL = null; + this.fragmentURL = null; + this.vertexFilename = null; + this.fragmentFilename = null; + + glProgram = 0; + glVertex = 0; + glFragment = 0; + + intBuffer = PGL.allocateIntBuffer(1); + floatBuffer = PGL.allocateFloatBuffer(1); + + bound = false; + + type = -1; + } + + + public PShader(PApplet parent) { + this(); + this.parent = parent; + primaryPG = (PGraphicsOpenGL)parent.g; + pgl = primaryPG.pgl; + context = pgl.createEmptyContext(); + } + + + /** + * Creates a shader program using the specified vertex and fragment + * shaders. + * + * @param parent the parent program + * @param vertFilename name of the vertex shader + * @param fragFilename name of the fragment shader + */ + public PShader(PApplet parent, String vertFilename, String fragFilename) { + this.parent = parent; + primaryPG = (PGraphicsOpenGL)parent.g; + pgl = primaryPG.pgl; + + this.vertexURL = null; + this.fragmentURL = null; + this.vertexFilename = vertFilename; + this.fragmentFilename = fragFilename; + fragmentShaderSource = pgl.loadFragmentShader(fragFilename); + vertexShaderSource = pgl.loadVertexShader(vertFilename); + + glProgram = 0; + glVertex = 0; + glFragment = 0; + + intBuffer = PGL.allocateIntBuffer(1); + floatBuffer = PGL.allocateFloatBuffer(1); + + int vertType = getShaderType(vertexShaderSource, -1); + int fragType = getShaderType(fragmentShaderSource, -1); + if (vertType == -1 && fragType == -1) { + type = PShader.POLY; + } else if (vertType == -1) { + type = fragType; + } else if (fragType == -1) { + type = vertType; + } else if (fragType == vertType) { + type = vertType; + } else { + PGraphics.showWarning(PGraphicsOpenGL.INCONSISTENT_SHADER_TYPES); + } + } + + + /** + * @param vertURL network location of the vertex shader + * @param fragURL network location of the fragment shader + */ + public PShader(PApplet parent, URL vertURL, URL fragURL) { + this.parent = parent; + primaryPG = (PGraphicsOpenGL)parent.g; + pgl = primaryPG.pgl; + + this.vertexURL = vertURL; + this.fragmentURL = fragURL; + this.vertexFilename = null; + this.fragmentFilename = null; + fragmentShaderSource = pgl.loadFragmentShader(fragURL); + vertexShaderSource = pgl.loadVertexShader(vertURL); + + glProgram = 0; + glVertex = 0; + glFragment = 0; + + intBuffer = PGL.allocateIntBuffer(1); + floatBuffer = PGL.allocateFloatBuffer(1); + + int vertType = getShaderType(vertexShaderSource, -1); + int fragType = getShaderType(fragmentShaderSource, -1); + if (vertType == -1 && fragType == -1) { + type = PShader.POLY; + } else if (vertType == -1) { + type = fragType; + } else if (fragType == -1) { + type = vertType; + } else if (fragType == vertType) { + type = vertType; + } else { + PGraphics.showWarning(PGraphicsOpenGL.INCONSISTENT_SHADER_TYPES); + } + } + + public PShader(PApplet parent, String[] vertSource, String[] fragSource) { + this.parent = parent; + primaryPG = (PGraphicsOpenGL)parent.g; + pgl = primaryPG.pgl; + + this.vertexURL = null; + this.fragmentURL = null; + this.vertexFilename = null; + this.fragmentFilename = null; + vertexShaderSource = vertSource; + fragmentShaderSource = fragSource; + + glProgram = 0; + glVertex = 0; + glFragment = 0; + + intBuffer = PGL.allocateIntBuffer(1); + floatBuffer = PGL.allocateFloatBuffer(1); + + int vertType = getShaderType(vertexShaderSource, -1); + int fragType = getShaderType(fragmentShaderSource, -1); + if (vertType == -1 && fragType == -1) { + type = PShader.POLY; + } else if (vertType == -1) { + type = fragType; + } else if (fragType == -1) { + type = vertType; + } else if (fragType == vertType) { + type = vertType; + } else { + PGraphics.showWarning(PGraphicsOpenGL.INCONSISTENT_SHADER_TYPES); + } + } + + + public void setVertexShader(String vertFilename) { + this.vertexFilename = vertFilename; + vertexShaderSource = pgl.loadVertexShader(vertFilename); + } + + + public void setVertexShader(URL vertURL) { + this.vertexURL = vertURL; + vertexShaderSource = pgl.loadVertexShader(vertURL); + } + + + public void setVertexShader(String[] vertSource) { + vertexShaderSource = vertSource; + } + + + public void setFragmentShader(String fragFilename) { + this.fragmentFilename = fragFilename; + fragmentShaderSource = pgl.loadFragmentShader(fragFilename); + } + + + public void setFragmentShader(URL fragURL) { + this.fragmentURL = fragURL; + fragmentShaderSource = pgl.loadFragmentShader(fragURL); + } + + public void setFragmentShader(String[] fragSource) { + fragmentShaderSource = fragSource; + } + + + /** + * Initializes (if needed) and binds the shader program. + */ + public void bind() { + init(); + if (!bound) { + pgl.useProgram(glProgram); + bound = true; + consumeUniforms(); + bindTextures(); + } + + if (hasType()) bindTyped(); + } + + + /** + * Unbinds the shader program. + */ + public void unbind() { + if (hasType()) unbindTyped(); + + if (bound) { + unbindTextures(); + pgl.useProgram(0); + bound = false; + } + } + + + /** + * Returns true if the shader is bound, false otherwise. + */ + public boolean bound() { + return bound; + } + + /** + * @webref rendering:shaders + * @brief Sets a variable within the shader + * @param name the name of the uniform variable to modify + * @param x first component of the variable to modify + */ + public void set(String name, int x) { + setUniformImpl(name, UniformValue.INT1, new int[] { x }); + } + + /** + * @param y second component of the variable to modify. The variable has to be declared with an array/vector type in the shader (i.e.: int[2], vec2) + */ + public void set(String name, int x, int y) { + setUniformImpl(name, UniformValue.INT2, new int[] { x, y }); + } + + /** + * @param z third component of the variable to modify. The variable has to be declared with an array/vector type in the shader (i.e.: int[3], vec3) + */ + public void set(String name, int x, int y, int z) { + setUniformImpl(name, UniformValue.INT3, new int[] { x, y, z }); + } + + /** + * @param w fourth component of the variable to modify. The variable has to be declared with an array/vector type in the shader (i.e.: int[4], vec4) + */ + public void set(String name, int x, int y, int z, int w) { + setUniformImpl(name, UniformValue.INT4, new int[] { x, y, z, w }); + } + + + public void set(String name, float x) { + setUniformImpl(name, UniformValue.FLOAT1, new float[] { x }); + } + + + public void set(String name, float x, float y) { + setUniformImpl(name, UniformValue.FLOAT2, new float[] { x, y }); + } + + + public void set(String name, float x, float y, float z) { + setUniformImpl(name, UniformValue.FLOAT3, new float[] { x, y, z }); + } + + + public void set(String name, float x, float y, float z, float w) { + setUniformImpl(name, UniformValue.FLOAT4, new float[] { x, y, z, w }); + } + + /** + * @param vec modifies all the components of an array/vector uniform variable. PVector can only be used if the type of the variable is vec3. + */ + public void set(String name, PVector vec) { + setUniformImpl(name, UniformValue.FLOAT3, + new float[] { vec.x, vec.y, vec.z }); + } + + + public void set(String name, boolean x) { + setUniformImpl(name, UniformValue.INT1, new int[] { (x)?1:0 }); + } + + + public void set(String name, boolean x, boolean y) { + setUniformImpl(name, UniformValue.INT2, + new int[] { (x)?1:0, (y)?1:0 }); + } + + + public void set(String name, boolean x, boolean y, boolean z) { + setUniformImpl(name, UniformValue.INT3, + new int[] { (x)?1:0, (y)?1:0, (z)?1:0 }); + } + + + public void set(String name, boolean x, boolean y, boolean z, boolean w) { + setUniformImpl(name, UniformValue.INT4, + new int[] { (x)?1:0, (y)?1:0, (z)?1:0, (w)?1:0 }); + } + + + public void set(String name, int[] vec) { + set(name, vec, 1); + } + + + /** + * @param ncoords number of coordinates per element, max 4 + */ + public void set(String name, int[] vec, int ncoords) { + if (ncoords == 1) { + setUniformImpl(name, UniformValue.INT1VEC, vec); + } else if (ncoords == 2) { + setUniformImpl(name, UniformValue.INT2VEC, vec); + } else if (ncoords == 3) { + setUniformImpl(name, UniformValue.INT3VEC, vec); + } else if (ncoords == 4) { + setUniformImpl(name, UniformValue.INT4VEC, vec); + } else if (4 < ncoords) { + PGraphics.showWarning("Only up to 4 coordinates per element are " + + "supported."); + } else { + PGraphics.showWarning("Wrong number of coordinates: it is negative!"); + } + } + + + public void set(String name, float[] vec) { + set(name, vec, 1); + } + + + public void set(String name, float[] vec, int ncoords) { + if (ncoords == 1) { + setUniformImpl(name, UniformValue.FLOAT1VEC, vec); + } else if (ncoords == 2) { + setUniformImpl(name, UniformValue.FLOAT2VEC, vec); + } else if (ncoords == 3) { + setUniformImpl(name, UniformValue.FLOAT3VEC, vec); + } else if (ncoords == 4) { + setUniformImpl(name, UniformValue.FLOAT4VEC, vec); + } else if (4 < ncoords) { + PGraphics.showWarning("Only up to 4 coordinates per element are " + + "supported."); + } else { + PGraphics.showWarning("Wrong number of coordinates: it is negative!"); + } + } + + + public void set(String name, boolean[] vec) { + set(name, vec, 1); + } + + + public void set(String name, boolean[] boolvec, int ncoords) { + int[] vec = new int[boolvec.length]; + for (int i = 0; i < boolvec.length; i++) { + vec[i] = (boolvec[i])?1:0; + } + set(name, vec, ncoords); + } + + + /** + * @param mat matrix of values + */ + public void set(String name, PMatrix2D mat) { + float[] matv = { mat.m00, mat.m01, + mat.m10, mat.m11 }; + setUniformImpl(name, UniformValue.MAT2, matv); + } + + + public void set(String name, PMatrix3D mat) { + set(name, mat, false); + } + + /** + * @param use3x3 enforces the matrix is 3 x 3 + */ + public void set(String name, PMatrix3D mat, boolean use3x3) { + if (use3x3) { + float[] matv = { mat.m00, mat.m01, mat.m02, + mat.m10, mat.m11, mat.m12, + mat.m20, mat.m21, mat.m22 }; + setUniformImpl(name, UniformValue.MAT3, matv); + } else { + float[] matv = { mat.m00, mat.m01, mat.m02, mat.m03, + mat.m10, mat.m11, mat.m12, mat.m13, + mat.m20, mat.m21, mat.m22, mat.m23, + mat.m30, mat.m31, mat.m32, mat.m33 }; + setUniformImpl(name, UniformValue.MAT4, matv); + } + } + + /** + * @param tex sets the sampler uniform variable to read from this image texture + */ + public void set(String name, PImage tex) { + setUniformImpl(name, UniformValue.SAMPLER2D, tex); + } + + + /** + * Extra initialization method that can be used by subclasses, called after + * compiling and attaching the vertex and fragment shaders, and before + * linking the shader program. + * + */ + protected void setup() { + } + + + protected void draw(int idxId, int count, int offset) { + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, idxId); + pgl.drawElements(PGL.TRIANGLES, count, PGL.INDEX_TYPE, + offset * PGL.SIZEOF_INDEX); + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, 0); + } + + + /** + * Returns the ID location of the attribute parameter given its name. + * + * @param name String + * @return int + */ + protected int getAttributeLoc(String name) { + init(); + return pgl.getAttribLocation(glProgram, name); + } + + + /** + * Returns the ID location of the uniform parameter given its name. + * + * @param name String + * @return int + */ + protected int getUniformLoc(String name) { + init(); + return pgl.getUniformLocation(glProgram, name); + } + + + protected void setAttributeVBO(int loc, int vboId, int size, int type, + boolean normalized, int stride, int offset) { + if (-1 < loc) { + pgl.bindBuffer(PGL.ARRAY_BUFFER, vboId); + pgl.vertexAttribPointer(loc, size, type, normalized, stride, offset); + } + } + + + protected void setUniformValue(int loc, int x) { + if (-1 < loc) { + pgl.uniform1i(loc, x); + } + } + + + protected void setUniformValue(int loc, int x, int y) { + if (-1 < loc) { + pgl.uniform2i(loc, x, y); + } + } + + + protected void setUniformValue(int loc, int x, int y, int z) { + if (-1 < loc) { + pgl.uniform3i(loc, x, y, z); + } + } + + + protected void setUniformValue(int loc, int x, int y, int z, int w) { + if (-1 < loc) { + pgl.uniform4i(loc, x, y, z, w); + } + } + + + protected void setUniformValue(int loc, float x) { + if (-1 < loc) { + pgl.uniform1f(loc, x); + } + } + + protected void setUniformValue(int loc, float x, float y) { + if (-1 < loc) { + pgl.uniform2f(loc, x, y); + } + } + + + protected void setUniformValue(int loc, float x, float y, float z) { + if (-1 < loc) { + pgl.uniform3f(loc, x, y, z); + } + } + + + protected void setUniformValue(int loc, float x, float y, float z, float w) { + if (-1 < loc) { + pgl.uniform4f(loc, x, y, z, w); + } + } + + + protected void setUniformVector(int loc, int[] vec, int ncoords, + int length) { + if (-1 < loc) { + updateIntBuffer(vec); + if (ncoords == 1) { + pgl.uniform1iv(loc, length, intBuffer); + } else if (ncoords == 2) { + pgl.uniform2iv(loc, length, intBuffer); + } else if (ncoords == 3) { + pgl.uniform3iv(loc, length, intBuffer); + } else if (ncoords == 4) { + pgl.uniform3iv(loc, length, intBuffer); + } + } + } + + + protected void setUniformVector(int loc, float[] vec, int ncoords, + int length) { + if (-1 < loc) { + updateFloatBuffer(vec); + if (ncoords == 1) { + pgl.uniform1fv(loc, length, floatBuffer); + } else if (ncoords == 2) { + pgl.uniform2fv(loc, length, floatBuffer); + } else if (ncoords == 3) { + pgl.uniform3fv(loc, length, floatBuffer); + } else if (ncoords == 4) { + pgl.uniform4fv(loc, length, floatBuffer); + } + } + } + + + protected void setUniformMatrix(int loc, float[] mat) { + if (-1 < loc) { + updateFloatBuffer(mat); + if (mat.length == 4) { + pgl.uniformMatrix2fv(loc, 1, false, floatBuffer); + } else if (mat.length == 9) { + pgl.uniformMatrix3fv(loc, 1, false, floatBuffer); + } else if (mat.length == 16) { + pgl.uniformMatrix4fv(loc, 1, false, floatBuffer); + } + } + } + + + protected void setUniformTex(int loc, Texture tex) { + if (texUnits != null) { + Integer unit = texUnits.get(loc); + if (unit != null) { + pgl.activeTexture(PGL.TEXTURE0 + unit); + tex.bind(); + } else { + throw new RuntimeException("Cannot find unit for texture " + tex); + } + } + } + + + protected void setUniformImpl(String name, int type, Object value) { + if (uniformValues == null) { + uniformValues = new HashMap(); + } + uniformValues.put(name, new UniformValue(type, value)); + } + + + protected void consumeUniforms() { + if (uniformValues != null && 0 < uniformValues.size()) { + int unit = 0; + for (String name: uniformValues.keySet()) { + int loc = getUniformLoc(name); + if (loc == -1) { + PGraphics.showWarning("The shader doesn't have a uniform called \"" + + name + "\" OR the uniform was removed during " + + "compilation because it was unused."); + continue; + } + UniformValue val = uniformValues.get(name); + if (val.type == UniformValue.INT1) { + int[] v = ((int[])val.value); + pgl.uniform1i(loc, v[0]); + } else if (val.type == UniformValue.INT2) { + int[] v = ((int[])val.value); + pgl.uniform2i(loc, v[0], v[1]); + } else if (val.type == UniformValue.INT3) { + int[] v = ((int[])val.value); + pgl.uniform3i(loc, v[0], v[1], v[2]); + } else if (val.type == UniformValue.INT4) { + int[] v = ((int[])val.value); + pgl.uniform4i(loc, v[0], v[1], v[2], v[3]); + } else if (val.type == UniformValue.FLOAT1) { + float[] v = ((float[])val.value); + pgl.uniform1f(loc, v[0]); + } else if (val.type == UniformValue.FLOAT2) { + float[] v = ((float[])val.value); + pgl.uniform2f(loc, v[0], v[1]); + } else if (val.type == UniformValue.FLOAT3) { + float[] v = ((float[])val.value); + pgl.uniform3f(loc, v[0], v[1], v[2]); + } else if (val.type == UniformValue.FLOAT4) { + float[] v = ((float[])val.value); + pgl.uniform4f(loc, v[0], v[1], v[2], v[3]); + } else if (val.type == UniformValue.INT1VEC) { + int[] v = ((int[])val.value); + updateIntBuffer(v); + pgl.uniform1iv(loc, v.length, intBuffer); + } else if (val.type == UniformValue.INT2VEC) { + int[] v = ((int[])val.value); + updateIntBuffer(v); + pgl.uniform2iv(loc, v.length / 2, intBuffer); + } else if (val.type == UniformValue.INT3VEC) { + int[] v = ((int[])val.value); + updateIntBuffer(v); + pgl.uniform3iv(loc, v.length / 3, intBuffer); + } else if (val.type == UniformValue.INT4VEC) { + int[] v = ((int[])val.value); + updateIntBuffer(v); + pgl.uniform4iv(loc, v.length / 4, intBuffer); + } else if (val.type == UniformValue.FLOAT1VEC) { + float[] v = ((float[])val.value); + updateFloatBuffer(v); + pgl.uniform1fv(loc, v.length, floatBuffer); + } else if (val.type == UniformValue.FLOAT2VEC) { + float[] v = ((float[])val.value); + updateFloatBuffer(v); + pgl.uniform2fv(loc, v.length / 2, floatBuffer); + } else if (val.type == UniformValue.FLOAT3VEC) { + float[] v = ((float[])val.value); + updateFloatBuffer(v); + pgl.uniform3fv(loc, v.length / 3, floatBuffer); + } else if (val.type == UniformValue.FLOAT4VEC) { + float[] v = ((float[])val.value); + updateFloatBuffer(v); + pgl.uniform4fv(loc, v.length / 4, floatBuffer); + } else if (val.type == UniformValue.MAT2) { + float[] v = ((float[])val.value); + updateFloatBuffer(v); + pgl.uniformMatrix2fv(loc, 1, false, floatBuffer); + } else if (val.type == UniformValue.MAT3) { + float[] v = ((float[])val.value); + updateFloatBuffer(v); + pgl.uniformMatrix3fv(loc, 1, false, floatBuffer); + } else if (val.type == UniformValue.MAT4) { + float[] v = ((float[])val.value); + updateFloatBuffer(v); + pgl.uniformMatrix4fv(loc, 1, false, floatBuffer); + } else if (val.type == UniformValue.SAMPLER2D) { + PImage img = (PImage)val.value; + Texture tex = currentPG.getTexture(img); + + if (textures == null) textures = new HashMap(); + textures.put(loc, tex); + + if (texUnits == null) texUnits = new HashMap(); + if (texUnits.containsKey(loc)) { + unit = texUnits.get(loc); + pgl.uniform1i(loc, unit); + } else { + texUnits.put(loc, unit); + pgl.uniform1i(loc, unit); + } + unit++; + } + } + uniformValues.clear(); + } + } + + + protected void updateIntBuffer(int[] vec) { + intBuffer = PGL.updateIntBuffer(intBuffer, vec, false); + } + + + protected void updateFloatBuffer(float[] vec) { + floatBuffer = PGL.updateFloatBuffer(floatBuffer, vec, false); + } + + + protected void bindTextures() { + if (textures != null && texUnits != null) { + for (int loc: textures.keySet()) { + Texture tex = textures.get(loc); + Integer unit = texUnits.get(loc); + if (unit != null) { + pgl.activeTexture(PGL.TEXTURE0 + unit); + tex.bind(); + } else { + throw new RuntimeException("Cannot find unit for texture " + tex); + } + } + } + } + + + protected void unbindTextures() { + if (textures != null && texUnits != null) { + for (int loc: textures.keySet()) { + Texture tex = textures.get(loc); + Integer unit = texUnits.get(loc); + if (unit != null) { + pgl.activeTexture(PGL.TEXTURE0 + unit); + tex.unbind(); + } else { + throw new RuntimeException("Cannot find unit for texture " + tex); + } + } + pgl.activeTexture(PGL.TEXTURE0); + } + } + + + public void init() { + if (glProgram == 0 || contextIsOutdated()) { + create(); + if (compile()) { + pgl.attachShader(glProgram, glVertex); + pgl.attachShader(glProgram, glFragment); + + setup(); + + pgl.linkProgram(glProgram); + + validate(); + } + } + } + + + protected void create() { + context = pgl.getCurrentContext(); + glres = new GLResourceShader(this); + } + + + protected boolean compile() { + boolean vertRes = true; + if (hasVertexShader()) { + vertRes = compileVertexShader(); + } else { + PGraphics.showException("Doesn't have a vertex shader"); + } + + boolean fragRes = true; + if (hasFragmentShader()) { + fragRes = compileFragmentShader(); + } else { + PGraphics.showException("Doesn't have a fragment shader"); + } + + return vertRes && fragRes; + } + + + protected void validate() { + pgl.getProgramiv(glProgram, PGL.LINK_STATUS, intBuffer); + boolean linked = intBuffer.get(0) == 0 ? false : true; + if (!linked) { + PGraphics.showException("Cannot link shader program:\n" + + pgl.getProgramInfoLog(glProgram)); + } + + pgl.validateProgram(glProgram); + pgl.getProgramiv(glProgram, PGL.VALIDATE_STATUS, intBuffer); + boolean validated = intBuffer.get(0) == 0 ? false : true; + if (!validated) { + PGraphics.showException("Cannot validate shader program:\n" + + pgl.getProgramInfoLog(glProgram)); + } + } + + + protected boolean contextIsOutdated() { + boolean outdated = !pgl.contextIsCurrent(context); + if (outdated) { + dispose(); + } + return outdated; + } + + + + protected boolean hasVertexShader() { + return vertexShaderSource != null && 0 < vertexShaderSource.length; + } + + + protected boolean hasFragmentShader() { + return fragmentShaderSource != null && 0 < fragmentShaderSource.length; + } + + + /** + * @param shaderSource a string containing the shader's code + */ + protected boolean compileVertexShader() { + pgl.shaderSource(glVertex, PApplet.join(vertexShaderSource, "\n")); + pgl.compileShader(glVertex); + + pgl.getShaderiv(glVertex, PGL.COMPILE_STATUS, intBuffer); + boolean compiled = intBuffer.get(0) == 0 ? false : true; + if (!compiled) { + PGraphics.showException("Cannot compile vertex shader:\n" + + pgl.getShaderInfoLog(glVertex)); + return false; + } else { + return true; + } + } + + + /** + * @param shaderSource a string containing the shader's code + */ + protected boolean compileFragmentShader() { + pgl.shaderSource(glFragment, PApplet.join(fragmentShaderSource, "\n")); + pgl.compileShader(glFragment); + + pgl.getShaderiv(glFragment, PGL.COMPILE_STATUS, intBuffer); + boolean compiled = intBuffer.get(0) == 0 ? false : true; + if (!compiled) { + PGraphics.showException("Cannot compile fragment shader:\n" + + pgl.getShaderInfoLog(glFragment)); + return false; + } else { + return true; + } + } + + + protected void dispose() { + if (glres != null) { + glres.dispose(); + glVertex = 0; + glFragment = 0; + glProgram = 0; + glres = null; + } + } + + + static protected int getShaderType(String[] source, int defaultType) { + for (int i = 0; i < source.length; i++) { + String line = source[i].trim(); + + if (PApplet.match(line, colorShaderDefRegexp) != null) + return PShader.COLOR; + else if (PApplet.match(line, lightShaderDefRegexp) != null) + return PShader.LIGHT; + else if (PApplet.match(line, texShaderDefRegexp) != null) + return PShader.TEXTURE; + else if (PApplet.match(line, texlightShaderDefRegexp) != null) + return PShader.TEXLIGHT; + else if (PApplet.match(line, polyShaderDefRegexp) != null) + return PShader.POLY; + else if (PApplet.match(line, triShaderAttrRegexp) != null) + return PShader.POLY; + else if (PApplet.match(line, quadShaderAttrRegexp) != null) + return PShader.POLY; + else if (PApplet.match(line, pointShaderDefRegexp) != null) + return PShader.POINT; + else if (PApplet.match(line, lineShaderDefRegexp) != null) + return PShader.LINE; + else if (PApplet.match(line, pointShaderAttrRegexp) != null) + return PShader.POINT; + else if (PApplet.match(line, pointShaderInRegexp) != null) + return PShader.POINT; + else if (PApplet.match(line, lineShaderAttrRegexp) != null) + return PShader.LINE; + else if (PApplet.match(line, lineShaderInRegexp) != null) + return PShader.LINE; + } + return defaultType; + } + + + // *************************************************************************** + // + // Processing specific + + + protected int getType() { + return type; + } + + + protected void setType(int type) { + this.type = type; + } + + + protected boolean hasType() { + return POINT <= type && type <= TEXLIGHT; + } + + + protected boolean isPointShader() { + return type == POINT; + } + + + protected boolean isLineShader() { + return type == LINE; + } + + + protected boolean isPolyShader() { + return POLY <= type && type <= TEXLIGHT; + } + + + protected boolean checkPolyType(int type) { + if (getType() == PShader.POLY) return true; + + if (getType() != type) { + if (type == TEXLIGHT) { + PGraphics.showWarning(PGraphicsOpenGL.NO_TEXLIGHT_SHADER_ERROR); + } else if (type == LIGHT) { + PGraphics.showWarning(PGraphicsOpenGL.NO_LIGHT_SHADER_ERROR); + } else if (type == TEXTURE) { + PGraphics.showWarning(PGraphicsOpenGL.NO_TEXTURE_SHADER_ERROR); + } else if (type == COLOR) { + PGraphics.showWarning(PGraphicsOpenGL.NO_COLOR_SHADER_ERROR); + } + return false; + } + + return true; + } + + + protected int getLastTexUnit() { + return texUnits == null ? -1 : texUnits.size() - 1; + } + + + protected void setRenderer(PGraphicsOpenGL pg) { + this.currentPG = pg; + } + + + protected void loadAttributes() { + if (loadedAttributes) return; + + vertexLoc = getAttributeLoc("vertex"); + if (vertexLoc == -1) vertexLoc = getAttributeLoc("position"); + + colorLoc = getAttributeLoc("color"); + texCoordLoc = getAttributeLoc("texCoord"); + normalLoc = getAttributeLoc("normal"); + + ambientLoc = getAttributeLoc("ambient"); + specularLoc = getAttributeLoc("specular"); + emissiveLoc = getAttributeLoc("emissive"); + shininessLoc = getAttributeLoc("shininess"); + + directionLoc = getAttributeLoc("direction"); + + offsetLoc = getAttributeLoc("offset"); + + directionLoc = getAttributeLoc("direction"); + offsetLoc = getAttributeLoc("offset"); + + loadedAttributes = true; + } + + + protected void loadUniforms() { + if (loadedUniforms) return; + transformMatLoc = getUniformLoc("transform"); + if (transformMatLoc == -1) + transformMatLoc = getUniformLoc("transformMatrix"); + + modelviewMatLoc = getUniformLoc("modelview"); + if (modelviewMatLoc == -1) + modelviewMatLoc = getUniformLoc("modelviewMatrix"); + + projectionMatLoc = getUniformLoc("projection"); + if (projectionMatLoc == -1) + projectionMatLoc = getUniformLoc("projectionMatrix"); + + viewportLoc = getUniformLoc("viewport"); + resolutionLoc = getUniformLoc("resolution"); + ppixelsLoc = getUniformLoc("ppixels"); + + normalMatLoc = getUniformLoc("normalMatrix"); + + lightCountLoc = getUniformLoc("lightCount"); + lightPositionLoc = getUniformLoc("lightPosition"); + lightNormalLoc = getUniformLoc("lightNormal"); + lightAmbientLoc = getUniformLoc("lightAmbient"); + lightDiffuseLoc = getUniformLoc("lightDiffuse"); + lightSpecularLoc = getUniformLoc("lightSpecular"); + lightFalloffLoc = getUniformLoc("lightFalloff"); + lightSpotLoc = getUniformLoc("lightSpot"); + + textureLoc = getUniformLoc("texture"); + if (textureLoc == -1) { + textureLoc = getUniformLoc("texMap"); + } + + texMatrixLoc = getUniformLoc("texMatrix"); + texOffsetLoc = getUniformLoc("texOffset"); + + perspectiveLoc = getUniformLoc("perspective"); + scaleLoc = getUniformLoc("scale"); + loadedUniforms = true; + } + + + protected void setCommonUniforms() { + if (-1 < transformMatLoc) { + currentPG.updateGLProjmodelview(); + setUniformMatrix(transformMatLoc, currentPG.glProjmodelview); + } + + if (-1 < modelviewMatLoc) { + currentPG.updateGLModelview(); + setUniformMatrix(modelviewMatLoc, currentPG.glModelview); + } + + if (-1 < projectionMatLoc) { + currentPG.updateGLProjection(); + setUniformMatrix(projectionMatLoc, currentPG.glProjection); + } + + if (-1 < viewportLoc) { + float x = currentPG.viewport.get(0); + float y = currentPG.viewport.get(1); + float w = currentPG.viewport.get(2); + float h = currentPG.viewport.get(3); + setUniformValue(viewportLoc, x, y, w, h); + } + + if (-1 < resolutionLoc) { + float w = currentPG.viewport.get(2); + float h = currentPG.viewport.get(3); + setUniformValue(resolutionLoc, w, h); + } + + if (-1 < ppixelsLoc) { + ppixelsUnit = getLastTexUnit() + 1; + setUniformValue(ppixelsLoc, ppixelsUnit); + pgl.activeTexture(PGL.TEXTURE0 + ppixelsUnit); + currentPG.bindFrontTexture(); + } else { + ppixelsUnit = -1; + } + } + + + protected void bindTyped() { + if (currentPG == null) { + setRenderer(primaryPG.getCurrentPG()); + loadAttributes(); + loadUniforms(); + } + setCommonUniforms(); + + if (-1 < vertexLoc) pgl.enableVertexAttribArray(vertexLoc); + if (-1 < colorLoc) pgl.enableVertexAttribArray(colorLoc); + if (-1 < texCoordLoc) pgl.enableVertexAttribArray(texCoordLoc); + if (-1 < normalLoc) pgl.enableVertexAttribArray(normalLoc); + + if (-1 < normalMatLoc) { + currentPG.updateGLNormal(); + setUniformMatrix(normalMatLoc, currentPG.glNormal); + } + + if (-1 < ambientLoc) pgl.enableVertexAttribArray(ambientLoc); + if (-1 < specularLoc) pgl.enableVertexAttribArray(specularLoc); + if (-1 < emissiveLoc) pgl.enableVertexAttribArray(emissiveLoc); + if (-1 < shininessLoc) pgl.enableVertexAttribArray(shininessLoc); + + int count = currentPG.lightCount; + setUniformValue(lightCountLoc, count); + if (0 < count) { + setUniformVector(lightPositionLoc, currentPG.lightPosition, 4, count); + setUniformVector(lightNormalLoc, currentPG.lightNormal, 3, count); + setUniformVector(lightAmbientLoc, currentPG.lightAmbient, 3, count); + setUniformVector(lightDiffuseLoc, currentPG.lightDiffuse, 3, count); + setUniformVector(lightSpecularLoc, currentPG.lightSpecular, 3, count); + setUniformVector(lightFalloffLoc, currentPG.lightFalloffCoefficients, + 3, count); + setUniformVector(lightSpotLoc, currentPG.lightSpotParameters, 2, count); + } + + if (-1 < directionLoc) pgl.enableVertexAttribArray(directionLoc); + + if (-1 < offsetLoc) pgl.enableVertexAttribArray(offsetLoc); + + if (-1 < perspectiveLoc) { + if (currentPG.getHint(ENABLE_STROKE_PERSPECTIVE) && + currentPG.nonOrthoProjection()) { + setUniformValue(perspectiveLoc, 1); + } else { + setUniformValue(perspectiveLoc, 0); + } + } + + if (-1 < scaleLoc) { + if (currentPG.getHint(DISABLE_OPTIMIZED_STROKE)) { + setUniformValue(scaleLoc, 1.0f, 1.0f, 1.0f); + } else { + float f = PGL.STROKE_DISPLACEMENT; + if (currentPG.orthoProjection()) { + setUniformValue(scaleLoc, 1, 1, f); + } else { + setUniformValue(scaleLoc, f, f, f); + } + } + } + } + + protected void unbindTyped() { + if (-1 < offsetLoc) pgl.disableVertexAttribArray(offsetLoc); + + if (-1 < directionLoc) pgl.disableVertexAttribArray(directionLoc); + + if (-1 < textureLoc && texture != null) { + pgl.activeTexture(PGL.TEXTURE0 + texUnit); + texture.unbind(); + pgl.activeTexture(PGL.TEXTURE0); + texture = null; + } + + if (-1 < ambientLoc) pgl.disableVertexAttribArray(ambientLoc); + if (-1 < specularLoc) pgl.disableVertexAttribArray(specularLoc); + if (-1 < emissiveLoc) pgl.disableVertexAttribArray(emissiveLoc); + if (-1 < shininessLoc) pgl.disableVertexAttribArray(shininessLoc); + + if (-1 < vertexLoc) pgl.disableVertexAttribArray(vertexLoc); + if (-1 < colorLoc) pgl.disableVertexAttribArray(colorLoc); + if (-1 < texCoordLoc) pgl.disableVertexAttribArray(texCoordLoc); + if (-1 < normalLoc) pgl.disableVertexAttribArray(normalLoc); + + if (-1 < ppixelsLoc) { + pgl.enableFBOLayer(); + pgl.activeTexture(PGL.TEXTURE0 + ppixelsUnit); + currentPG.unbindFrontTexture(); + pgl.activeTexture(PGL.TEXTURE0); + } + + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + protected void setTexture(Texture tex) { + texture = tex; + + float scaleu = 1; + float scalev = 1; + float dispu = 0; + float dispv = 0; + + if (tex != null) { + if (tex.invertedX()) { + scaleu = -1; + dispu = 1; + } + + if (tex.invertedY()) { + scalev = -1; + dispv = 1; + } + + scaleu *= tex.maxTexcoordU(); + dispu *= tex.maxTexcoordU(); + scalev *= tex.maxTexcoordV(); + dispv *= tex.maxTexcoordV(); + + setUniformValue(texOffsetLoc, 1.0f / tex.width, 1.0f / tex.height); + + if (-1 < textureLoc) { + texUnit = -1 < ppixelsUnit ? ppixelsUnit + 1 : getLastTexUnit() + 1; + setUniformValue(textureLoc, texUnit); + pgl.activeTexture(PGL.TEXTURE0 + texUnit); + tex.bind(); + } + } + + if (-1 < texMatrixLoc) { + if (tcmat == null) { + tcmat = new float[16]; + } + tcmat[0] = scaleu; tcmat[4] = 0; tcmat[ 8] = 0; tcmat[12] = dispu; + tcmat[1] = 0; tcmat[5] = scalev; tcmat[ 9] = 0; tcmat[13] = dispv; + tcmat[2] = 0; tcmat[6] = 0; tcmat[10] = 0; tcmat[14] = 0; + tcmat[3] = 0; tcmat[7] = 0; tcmat[11] = 0; tcmat[15] = 0; + setUniformMatrix(texMatrixLoc, tcmat); + } + } + + + protected boolean supportsTexturing() { + return -1 < textureLoc; + } + + protected boolean supportLighting() { + return -1 < lightCountLoc || -1 < lightPositionLoc || -1 < lightNormalLoc; + } + + protected boolean accessTexCoords() { + return -1 < texCoordLoc; + } + + protected boolean accessNormals() { + return -1 < normalLoc; + } + + protected boolean accessLightAttribs() { + return -1 < ambientLoc || -1 < specularLoc || -1 < emissiveLoc || + -1 < shininessLoc; + } + + protected void setVertexAttribute(int vboId, int size, int type, + int stride, int offset) { + setAttributeVBO(vertexLoc, vboId, size, type, false, stride, offset); + } + + protected void setColorAttribute(int vboId, int size, int type, + int stride, int offset) { + setAttributeVBO(colorLoc, vboId, size, type, true, stride, offset); + } + + protected void setNormalAttribute(int vboId, int size, int type, + int stride, int offset) { + setAttributeVBO(normalLoc, vboId, size, type, false, stride, offset); + } + + protected void setTexcoordAttribute(int vboId, int size, int type, + int stride, int offset) { + setAttributeVBO(texCoordLoc, vboId, size, type, false, stride, offset); + } + + protected void setAmbientAttribute(int vboId, int size, int type, + int stride, int offset) { + setAttributeVBO(ambientLoc, vboId, size, type, true, stride, offset); + } + + protected void setSpecularAttribute(int vboId, int size, int type, + int stride, int offset) { + setAttributeVBO(specularLoc, vboId, size, type, true, stride, offset); + } + + protected void setEmissiveAttribute(int vboId, int size, int type, + int stride, int offset) { + setAttributeVBO(emissiveLoc, vboId, size, type, true, stride, offset); + } + + protected void setShininessAttribute(int vboId, int size, int type, + int stride, int offset) { + setAttributeVBO(shininessLoc, vboId, size, type, false, stride, offset); + } + + protected void setLineAttribute(int vboId, int size, int type, + int stride, int offset) { + setAttributeVBO(directionLoc, vboId, size, type, false, stride, offset); + } + + protected void setPointAttribute(int vboId, int size, int type, + int stride, int offset) { + setAttributeVBO(offsetLoc, vboId, size, type, false, stride, offset); + } + + + // *************************************************************************** + // + // Class to store a user-specified value for a uniform parameter + // in the shader + protected static class UniformValue { + static final int INT1 = 0; + static final int INT2 = 1; + static final int INT3 = 2; + static final int INT4 = 3; + static final int FLOAT1 = 4; + static final int FLOAT2 = 5; + static final int FLOAT3 = 6; + static final int FLOAT4 = 7; + static final int INT1VEC = 8; + static final int INT2VEC = 9; + static final int INT3VEC = 10; + static final int INT4VEC = 11; + static final int FLOAT1VEC = 12; + static final int FLOAT2VEC = 13; + static final int FLOAT3VEC = 14; + static final int FLOAT4VEC = 15; + static final int MAT2 = 16; + static final int MAT3 = 17; + static final int MAT4 = 18; + static final int SAMPLER2D = 19; + + int type; + Object value; + + UniformValue(int type, Object value) { + this.type = type; + this.value = value; + } + } +} diff --git a/src/main/java/processing/opengl/PShapeOpenGL.java b/src/main/java/processing/opengl/PShapeOpenGL.java new file mode 100644 index 0000000..69701ac --- /dev/null +++ b/src/main/java/processing/opengl/PShapeOpenGL.java @@ -0,0 +1,5234 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.opengl; + +import processing.core.PApplet; +import processing.core.PConstants; +import processing.core.PGraphics; +import processing.core.PImage; +import processing.core.PMatrix; +import processing.core.PMatrix2D; +import processing.core.PMatrix3D; +import processing.core.PShape; +import processing.core.PVector; +import processing.opengl.PGraphicsOpenGL.AttributeMap; +import processing.opengl.PGraphicsOpenGL.IndexCache; +import processing.opengl.PGraphicsOpenGL.InGeometry; +import processing.opengl.PGraphicsOpenGL.TessGeometry; +import processing.opengl.PGraphicsOpenGL.Tessellator; +import processing.opengl.PGraphicsOpenGL.VertexAttribute; + +import java.nio.Buffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Stack; + +/** + * This class holds a 3D model composed of vertices, normals, colors + * (per vertex) and texture coordinates (also per vertex). All this data is + * stored in Vertex Buffer Objects (VBO) in GPU memory for very fast access. + * OBJ loading implemented using code from Saito's OBJLoader library: + * http://code.google.com/p/saitoobjloader/ + * and OBJReader from Ahmet Kizilay + * http://www.openprocessing.org/visuals/?visualID=191 + * By Andres Colubri + * + * + * Other formats to consider: + * AMF: http://en.wikipedia.org/wiki/Additive_Manufacturing_File_Format + * STL: http://en.wikipedia.org/wiki/STL_(file_format) + * OFF: http://people.sc.fsu.edu/~jburkardt/data/off/off.html(file_format) + * DXF: http://en.wikipedia.org/wiki/AutoCAD_DXF + */ +public class PShapeOpenGL extends PShape { + // Testing these constants, not use as they might go away... + static public final int POSITION = 0; + static public final int NORMAL = 1; + static public final int TEXCOORD = 2; + static public final int DIRECTION = 3; + static public final int OFFSET = 4; + + static protected final int TRANSLATE = 0; + static protected final int ROTATE = 1; + static protected final int SCALE = 2; + static protected final int MATRIX = 3; + + protected PGraphicsOpenGL pg; + protected PGL pgl; + protected int context; // The context that created this shape. + + protected PShapeOpenGL root; + + // ........................................................ + + // Input, tessellated geometry + + protected InGeometry inGeo; + protected TessGeometry tessGeo; + protected Tessellator tessellator; + + protected AttributeMap polyAttribs; + + // ........................................................ + + // Texturing + + protected HashSet textures; + protected boolean strokedTexture; + protected boolean untexChild; + + // ........................................................ + + // OpenGL buffers + + protected VertexBuffer bufPolyVertex; + protected VertexBuffer bufPolyColor; + protected VertexBuffer bufPolyNormal; + protected VertexBuffer bufPolyTexcoord; + protected VertexBuffer bufPolyAmbient; + protected VertexBuffer bufPolySpecular; + protected VertexBuffer bufPolyEmissive; + protected VertexBuffer bufPolyShininess; + protected VertexBuffer bufPolyIndex; + + protected VertexBuffer bufLineVertex; + protected VertexBuffer bufLineColor; + protected VertexBuffer bufLineAttrib; + protected VertexBuffer bufLineIndex; + + protected VertexBuffer bufPointVertex; + protected VertexBuffer bufPointColor; + protected VertexBuffer bufPointAttrib; + protected VertexBuffer bufPointIndex; + + // Testing this field, not use as it might go away... + public int glUsage = PGL.STATIC_DRAW; + + // ........................................................ + + // Offsets for geometry aggregation and update. + + protected int polyVertCopyOffset; + protected int polyIndCopyOffset; + protected int lineVertCopyOffset; + protected int lineIndCopyOffset; + protected int pointVertCopyOffset; + protected int pointIndCopyOffset; + + protected int polyIndexOffset; + protected int polyVertexOffset; + protected int polyVertexAbs; + protected int polyVertexRel; + + protected int lineIndexOffset; + protected int lineVertexOffset; + protected int lineVertexAbs; + protected int lineVertexRel; + + protected int pointIndexOffset; + protected int pointVertexOffset; + protected int pointVertexAbs; + protected int pointVertexRel; + + protected int firstPolyIndexCache; + protected int lastPolyIndexCache; + protected int firstLineIndexCache; + protected int lastLineIndexCache; + protected int firstPointIndexCache; + protected int lastPointIndexCache; + + protected int firstPolyVertex; + protected int lastPolyVertex; + protected int firstLineVertex; + protected int lastLineVertex; + protected int firstPointVertex; + protected int lastPointVertex; + + // ........................................................ + + // Geometric transformations. + + protected PMatrix transform; + protected Stack transformStack; + + // ........................................................ + + // State/rendering flags + + protected boolean tessellated; + protected boolean needBufferInit = false; + + // Flag to indicate if the shape can have holes or not. + protected boolean solid = true; + + protected boolean breakShape = false; + protected boolean shapeCreated = false; + + // These variables indicate if the shape contains + // polygon, line and/or point geometry. In the case of + // 3D shapes, poly geometry is coincident with the fill + // triangles, as the lines and points are stored separately. + // However, for 2D shapes the poly geometry contains all of + // the three since the same rendering shader applies to + // fill, line and point geometry. + protected boolean hasPolys; + protected boolean hasLines; + protected boolean hasPoints; + + // ........................................................ + + // Bezier and Catmull-Rom curves + + protected int bezierDetail; + protected int curveDetail; + protected float curveTightness; + + protected int savedBezierDetail; + protected int savedCurveDetail; + protected float savedCurveTightness; + + // ........................................................ + + // Normals + + protected float normalX, normalY, normalZ; + + // normal calculated per triangle + static protected final int NORMAL_MODE_AUTO = 0; + // one normal manually specified per shape + static protected final int NORMAL_MODE_SHAPE = 1; + // normals specified for each shape vertex + static protected final int NORMAL_MODE_VERTEX = 2; + + // Current mode for normals, one of AUTO, SHAPE, or VERTEX + protected int normalMode; + + // ........................................................ + + // Modification variables (used only by the root shape) + + protected boolean modified; + + protected boolean modifiedPolyVertices; + protected boolean modifiedPolyColors; + protected boolean modifiedPolyNormals; + protected boolean modifiedPolyTexCoords; + protected boolean modifiedPolyAmbient; + protected boolean modifiedPolySpecular; + protected boolean modifiedPolyEmissive; + protected boolean modifiedPolyShininess; + + protected boolean modifiedLineVertices; + protected boolean modifiedLineColors; + protected boolean modifiedLineAttributes; + + protected boolean modifiedPointVertices; + protected boolean modifiedPointColors; + protected boolean modifiedPointAttributes; + + protected int firstModifiedPolyVertex; + protected int lastModifiedPolyVertex; + protected int firstModifiedPolyColor; + protected int lastModifiedPolyColor; + protected int firstModifiedPolyNormal; + protected int lastModifiedPolyNormal; + protected int firstModifiedPolyTexcoord; + protected int lastModifiedPolyTexcoord; + protected int firstModifiedPolyAmbient; + protected int lastModifiedPolyAmbient; + protected int firstModifiedPolySpecular; + protected int lastModifiedPolySpecular; + protected int firstModifiedPolyEmissive; + protected int lastModifiedPolyEmissive; + protected int firstModifiedPolyShininess; + protected int lastModifiedPolyShininess; + + protected int firstModifiedLineVertex; + protected int lastModifiedLineVertex; + protected int firstModifiedLineColor; + protected int lastModifiedLineColor; + protected int firstModifiedLineAttribute; + protected int lastModifiedLineAttribute; + + protected int firstModifiedPointVertex; + protected int lastModifiedPointVertex; + protected int firstModifiedPointColor; + protected int lastModifiedPointColor; + protected int firstModifiedPointAttribute; + protected int lastModifiedPointAttribute; + + // ........................................................ + + // Saved style variables to style can be re-enabled after disableStyle, + // although it won't work if properties are defined on a per-vertex basis. + + protected boolean savedStroke; + protected int savedStrokeColor; + protected float savedStrokeWeight; + protected int savedStrokeCap; + protected int savedStrokeJoin; + + protected boolean savedFill; + protected int savedFillColor; + + protected boolean savedTint; + protected int savedTintColor; + + protected int savedAmbientColor; + protected int savedSpecularColor; + protected int savedEmissiveColor; + protected float savedShininess; + + protected int savedTextureMode; + + + PShapeOpenGL() { + } + + + public PShapeOpenGL(PGraphicsOpenGL pg, int family) { + this.pg = pg; + this.family = family; + + pgl = pg.pgl; + context = pgl.createEmptyContext(); + + bufPolyVertex = null; + bufPolyColor = null; + bufPolyNormal = null; + bufPolyTexcoord = null; + bufPolyAmbient = null; + bufPolySpecular = null; + bufPolyEmissive = null; + bufPolyShininess = null; + bufPolyIndex = null; + + bufLineVertex = null; + bufLineColor = null; + bufLineAttrib = null; + bufLineIndex = null; + + bufPointVertex = null; + bufPointColor = null; + bufPointAttrib = null; + bufPointIndex = null; + + this.tessellator = pg.tessellator; + this.root = this; + this.parent = null; + this.tessellated = false; + + if (family == GEOMETRY || family == PRIMITIVE || family == PATH) { + polyAttribs = PGraphicsOpenGL.newAttributeMap(); + inGeo = PGraphicsOpenGL.newInGeometry(pg, polyAttribs, PGraphicsOpenGL.RETAINED); + } + + // Style parameters are retrieved from the current values in the renderer. + textureMode = pg.textureMode; + + colorMode(pg.colorMode, + pg.colorModeX, pg.colorModeY, pg.colorModeZ, pg.colorModeA); + + // Initial values for fill, stroke and tint colors are also imported from + // the renderer. This is particular relevant for primitive shapes, since is + // not possible to set their color separately when creating them, and their + // input vertices are actually generated at rendering time, by which the + // color configuration of the renderer might have changed. + fill = pg.fill; + fillColor = pg.fillColor; + + stroke = pg.stroke; + strokeColor = pg.strokeColor; + strokeWeight = pg.strokeWeight; + strokeCap = pg.strokeCap; + strokeJoin = pg.strokeJoin; + + tint = pg.tint; + tintColor = pg.tintColor; + + setAmbient = pg.setAmbient; + ambientColor = pg.ambientColor; + specularColor = pg.specularColor; + emissiveColor = pg.emissiveColor; + shininess = pg.shininess; + + sphereDetailU = pg.sphereDetailU; + sphereDetailV = pg.sphereDetailV; + + bezierDetail = pg.bezierDetail; + curveDetail = pg.curveDetail; + curveTightness = pg.curveTightness; + + rectMode = pg.rectMode; + ellipseMode = pg.ellipseMode; + + normalX = normalY = 0; + normalZ = 1; + + normalMode = NORMAL_MODE_AUTO; + + // To make sure that the first vertex is marked as a break. + // Same behavior as in the immediate mode. + breakShape = false; + + if (family == GROUP) { + // GROUP shapes are always marked as ended. + shapeCreated = true; + } + + // OpenGL supports per-vertex coloring (unlike Java2D) + perVertexStyles = true; + } + + + /** Create a shape from the PRIMITIVE family, using this kind and these params */ + public PShapeOpenGL(PGraphicsOpenGL pg, int kind, float... p) { + this(pg, PRIMITIVE); + setKind(kind); + setParams(p); + } + + + @Override + public void addChild(PShape who) { + if (who instanceof PShapeOpenGL) { + if (family == GROUP) { + PShapeOpenGL c3d = (PShapeOpenGL)who; + + super.addChild(c3d); + c3d.updateRoot(root); + markForTessellation(); + + if (c3d.family == GROUP) { + if (c3d.textures != null) { + for (PImage tex: c3d.textures) { + addTexture(tex); + } + } else { + untexChild(true); + } + if (c3d.strokedTexture) { + strokedTexture(true); + } + } else { + if (c3d.image != null) { + addTexture(c3d.image); + if (c3d.stroke) { + strokedTexture(true); + } + } else { + untexChild(true); + } + } + + } else { + PGraphics.showWarning("Cannot add child shape to non-group shape."); + } + } else { + PGraphics.showWarning("Shape must be OpenGL to be added to the group."); + } + } + + + @Override + public void addChild(PShape who, int idx) { + if (who instanceof PShapeOpenGL) { + if (family == GROUP) { + PShapeOpenGL c3d = (PShapeOpenGL)who; + + super.addChild(c3d, idx); + c3d.updateRoot(root); + markForTessellation(); + + if (c3d.family == GROUP) { + if (c3d.textures != null) { + for (PImage tex: c3d.textures) { + addTexture(tex); + } + } else { + untexChild(true); + } + if (c3d.strokedTexture) { + strokedTexture(true); + } + } else { + if (c3d.image != null) { + addTexture(c3d.image); + if (c3d.stroke) { + strokedTexture(true); + } + } else { + untexChild(true); + } + } + + } else { + PGraphics.showWarning("Cannot add child shape to non-group shape."); + } + } else { + PGraphics.showWarning("Shape must be OpenGL to be added to the group."); + } + } + + + @Override + public void removeChild(int idx) { + super.removeChild(idx); + strokedTexture(false); + untexChild(false); + markForTessellation(); + } + + + protected void updateRoot(PShape root) { + this.root = (PShapeOpenGL) root; + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL)children[i]; + child.updateRoot(root); + } + } + } + + + /////////////////////////////////////////////////////////// + + // + // Shape creation (temporary hack) + + + public static PShapeOpenGL createShape(PGraphicsOpenGL pg, PShape src) { + PShapeOpenGL dest = null; + if (src.getFamily() == GROUP) { + //dest = PGraphics3D.createShapeImpl(pg, GROUP); + dest = (PShapeOpenGL) pg.createShapeFamily(GROUP); + copyGroup(pg, src, dest); + } else if (src.getFamily() == PRIMITIVE) { + //dest = PGraphics3D.createShapeImpl(pg, src.getKind(), src.getParams()); + dest = (PShapeOpenGL) pg.createShapePrimitive(src.getKind(), src.getParams()); + PShape.copyPrimitive(src, dest); + } else if (src.getFamily() == GEOMETRY) { + //dest = PGraphics3D.createShapeImpl(pg, PShape.GEOMETRY); + dest = (PShapeOpenGL) pg.createShapeFamily(PShape.GEOMETRY); + PShape.copyGeometry(src, dest); + } else if (src.getFamily() == PATH) { + dest = (PShapeOpenGL) pg.createShapeFamily(PShape.PATH); + //dest = PGraphics3D.createShapeImpl(pg, PATH); + PShape.copyPath(src, dest); + } + dest.setName(src.getName()); + dest.width = src.width; + dest.height = src.height; + dest.depth = src.depth; + return dest; + } + + + /* + static public PShapeOpenGL createShape2D(PGraphicsOpenGL pg, PShape src) { + PShapeOpenGL dest = null; + if (src.getFamily() == GROUP) { + //dest = PGraphics2D.createShapeImpl(pg, GROUP); + dest = (PShapeOpenGL) pg.createShapeFamily(GROUP); + copyGroup2D(pg, src, dest); + } else if (src.getFamily() == PRIMITIVE) { + //dest = PGraphics2D.createShapeImpl(pg, src.getKind(), src.getParams()); + dest = (PShapeOpenGL) pg.createShapePrimitive(src.getKind(), src.getParams()); + PShape.copyPrimitive(src, dest); + } else if (src.getFamily() == GEOMETRY) { + //dest = PGraphics2D.createShapeImpl(pg, PShape.GEOMETRY); + dest = (PShapeOpenGL) pg.createShapeFamily(PShape.GEOMETRY); + PShape.copyGeometry(src, dest); + } else if (src.getFamily() == PATH) { + //dest = PGraphics2D.createShapeImpl(pg, PATH); + dest = (PShapeOpenGL) pg.createShapeFamily(PShape.PATH); + PShape.copyPath(src, dest); + } + dest.setName(src.getName()); + dest.width = src.width; + dest.height = src.height; + return dest; + } +*/ + + static public void copyGroup(PGraphicsOpenGL pg, PShape src, PShape dest) { + copyMatrix(src, dest); + copyStyles(src, dest); + copyImage(src, dest); + + for (int i = 0; i < src.getChildCount(); i++) { + PShape c = createShape(pg, src.getChild(i)); + dest.addChild(c); + } + } + + + /* + static public void copyGroup2D(PGraphicsOpenGL pg, PShape src, PShape dest) { + copyMatrix(src, dest); + copyStyles(src, dest); + copyImage(src, dest); + + for (int i = 0; i < src.getChildCount(); i++) { + PShape c = createShape2D(pg, src.getChild(i)); + dest.addChild(c); + } + } +*/ + + /////////////////////////////////////////////////////////// + + // + + // Query methods + + + @Override + public float getWidth() { + PVector min = new PVector(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, + Float.POSITIVE_INFINITY); + PVector max = new PVector(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, + Float.NEGATIVE_INFINITY); + if (shapeCreated) { + getVertexMin(min); + getVertexMax(max); + } + width = max.x - min.x; + return width; + } + + + @Override + public float getHeight() { + PVector min = new PVector(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, + Float.POSITIVE_INFINITY); + PVector max = new PVector(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, + Float.NEGATIVE_INFINITY); + if (shapeCreated) { + getVertexMin(min); + getVertexMax(max); + } + height = max.y - min.y; + return height; + } + + + @Override + public float getDepth() { + PVector min = new PVector(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, + Float.POSITIVE_INFINITY); + PVector max = new PVector(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, + Float.NEGATIVE_INFINITY); + if (shapeCreated) { + getVertexMin(min); + getVertexMax(max); + } + depth = max.z - min.z; + return depth; + } + + + protected void getVertexMin(PVector min) { + updateTessellation(); + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.getVertexMin(min); + } + } else { + if (hasPolys) { + tessGeo.getPolyVertexMin(min, firstPolyVertex, lastPolyVertex); + } + if (is3D()) { + if (hasLines) { + tessGeo.getLineVertexMin(min, firstLineVertex, lastLineVertex); + } + if (hasPoints) { + tessGeo.getPointVertexMin(min, firstPointVertex, lastPointVertex); + } + } + } + } + + + protected void getVertexMax(PVector max) { + updateTessellation(); + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.getVertexMax(max); + } + } else { + if (hasPolys) { + tessGeo.getPolyVertexMax(max, firstPolyVertex, lastPolyVertex); + } + if (is3D()) { + if (hasLines) { + tessGeo.getLineVertexMax(max, firstLineVertex, lastLineVertex); + } + if (hasPoints) { + tessGeo.getPointVertexMax(max, firstPointVertex, lastPointVertex); + } + } + } + } + + + protected int getVertexSum(PVector sum, int count) { + updateTessellation(); + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + count += child.getVertexSum(sum, count); + } + } else { + if (hasPolys) { + count += tessGeo.getPolyVertexSum(sum, firstPolyVertex, lastPolyVertex); + } + if (is3D()) { + if (hasLines) { + count += tessGeo.getLineVertexSum(sum, firstLineVertex, + lastLineVertex); + } + if (hasPoints) { + count += tessGeo.getPointVertexSum(sum, firstPointVertex, + lastPointVertex); + } + } + } + return count; + } + + + /////////////////////////////////////////////////////////// + + // + + // Drawing methods + + + @Override + public void setTextureMode(int mode) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setTextureMode()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setTextureMode(mode); + } + } else { + setTextureModeImpl(mode); + } + } + + + protected void setTextureModeImpl(int mode) { + if (textureMode == mode) return; + textureMode = mode; + if (image != null) { + float uFactor = image.width; + float vFactor = image.height; + if (textureMode == NORMAL) { + uFactor = 1.0f / uFactor; + vFactor = 1.0f / vFactor; + } + scaleTextureUV(uFactor, vFactor); + } + } + + + @Override + public void setTexture(PImage tex) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setTexture()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setTexture(tex); + } + } else { + setTextureImpl(tex); + } + } + + + protected void setTextureImpl(PImage tex) { + PImage image0 = image; + image = tex; + + if (textureMode == IMAGE && image0 != image) { + // Need to rescale the texture coordinates + float uFactor = 1; + float vFactor = 1; + if (image != null) { + uFactor /= image.width; + vFactor /= image.height; + } + if (image0 != null) { + uFactor *= image0.width; + vFactor *= image0.height; + } + scaleTextureUV(uFactor, vFactor); + } + + if (image0 != tex && parent != null) { + ((PShapeOpenGL)parent).removeTexture(image0, this); + } + if (parent != null) { + ((PShapeOpenGL)parent).addTexture(image); + if (is2D() && stroke) { + ((PShapeOpenGL)parent).strokedTexture(true); + } + } + } + + + protected void scaleTextureUV(float uFactor, float vFactor) { + if (PGraphicsOpenGL.same(uFactor, 1) && + PGraphicsOpenGL.same(vFactor, 1)) return; + + for (int i = 0; i < inGeo.vertexCount; i++) { + float u = inGeo.texcoords[2 * i + 0]; + float v = inGeo.texcoords[2 * i + 1]; + inGeo.texcoords[2 * i + 0] = PApplet.min(1, u * uFactor); + inGeo.texcoords[2 * i + 1] = PApplet.min(1, v * uFactor); + } + + if (shapeCreated && tessellated && hasPolys) { + int last1 = 0; + if (is3D()) { + last1 = lastPolyVertex + 1; + } else if (is2D()) { + last1 = lastPolyVertex + 1; + if (-1 < firstLineVertex) last1 = firstLineVertex; + if (-1 < firstPointVertex) last1 = firstPointVertex; + } + for (int i = firstLineVertex; i < last1; i++) { + float u = tessGeo.polyTexCoords[2 * i + 0]; + float v = tessGeo.polyTexCoords[2 * i + 1]; + tessGeo.polyTexCoords[2 * i + 0] = PApplet.min(1, u * uFactor); + tessGeo.polyTexCoords[2 * i + 1] = PApplet.min(1, v * uFactor); + } + root.setModifiedPolyTexCoords(firstPolyVertex, last1 - 1); + } + } + + + protected void addTexture(PImage tex) { + if (textures == null) { + textures = new HashSet(); + } + textures.add(tex); + if (parent != null) { + ((PShapeOpenGL)parent).addTexture(tex); + } + } + + + protected void removeTexture(PImage tex, PShapeOpenGL caller) { + if (textures == null || !textures.contains(tex)) return; // Nothing to remove. + + // First check that none of the child shapes have texture tex... + boolean childHasTex = false; + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + if (child == caller) continue; + if (child.hasTexture(tex)) { + childHasTex = true; + break; + } + } + + if (!childHasTex) { + // ...if not, it is safe to remove from this shape. + textures.remove(tex); + if (textures.size() == 0) { + textures = null; + } + } + + // Since this shape and all its child shapes don't contain + // tex anymore, we now can remove it from the parent. + if (parent != null) { + ((PShapeOpenGL)parent).removeTexture(tex, this); + } + } + + + protected void strokedTexture(boolean newValue) { + strokedTexture(newValue, null); + } + + + protected void strokedTexture(boolean newValue, PShapeOpenGL caller) { + if (strokedTexture == newValue) return; // Nothing to change. + + if (newValue) { + strokedTexture = true; + } else { + // Check that none of the child shapes have a stroked texture... + strokedTexture = false; + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + if (child == caller) continue; + if (child.hasStrokedTexture()) { + strokedTexture = true; + break; + } + } + } + + // Now we can update the parent shape. + if (parent != null) { + ((PShapeOpenGL)parent).strokedTexture(newValue, this); + } + } + + + protected void untexChild(boolean newValue) { + untexChild(newValue, null); + } + + + protected void untexChild(boolean newValue, PShapeOpenGL caller) { + if (untexChild == newValue) return; // Nothing to change. + + if (newValue) { + untexChild = true; + } else { + // Check if any of the child shapes is not textured... + untexChild = false; + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + if (child == caller) continue; + if (!child.hasTexture()) { + untexChild = true; + break; + } + } + } + + // Now we can update the parent shape. + if (parent != null) { + ((PShapeOpenGL)parent).untexChild(newValue, this); + } + } + + + protected boolean hasTexture() { + if (family == GROUP) { + return textures != null && 0 < textures.size(); + } else { + return image != null; + } + } + + + protected boolean hasTexture(PImage tex) { + if (family == GROUP) { + return textures != null && textures.contains(tex); + } else { + return image == tex; + } + } + + + protected boolean hasStrokedTexture() { + if (family == GROUP) { + return strokedTexture; + } else { + return image != null && stroke; + } + } + + + @Override + public void solid(boolean solid) { + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.solid(solid); + } + } else { + this.solid = solid; + } + } + + + @Override + protected void beginContourImpl() { + breakShape = true; + } + + + @Override + protected void endContourImpl() { + } + + + @Override + public void vertex(float x, float y) { + vertexImpl(x, y, 0, 0, 0); + if (image != null) + PGraphics.showWarning(PGraphicsOpenGL.MISSING_UV_TEXCOORDS_ERROR); + } + + + @Override + public void vertex(float x, float y, float u, float v) { + vertexImpl(x, y, 0, u, v); + } + + + @Override + public void vertex(float x, float y, float z) { + vertexImpl(x, y, z, 0, 0); + if (image != null) + PGraphics.showWarning(PGraphicsOpenGL.MISSING_UV_TEXCOORDS_ERROR); + } + + + @Override + public void vertex(float x, float y, float z, float u, float v) { + vertexImpl(x, y, z, u, v); + } + + + protected void vertexImpl(float x, float y, float z, float u, float v) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "vertex()"); + return; + } + + if (family == GROUP) { + PGraphics.showWarning("Cannot add vertices to GROUP shape"); + return; + } + + boolean textured = image != null; + int fcolor = 0x00; + if (fill || textured) { + if (!textured) { + fcolor = fillColor; + } else { + if (tint) { + fcolor = tintColor; + } else { + fcolor = 0xffFFFFFF; + } + } + } + + if (textureMode == IMAGE && image != null) { + u /= image.width; + v /= image.height; + } + + int scolor = 0x00; + float sweight = 0; + if (stroke) { + scolor = strokeColor; + sweight = strokeWeight; + } + + inGeo.addVertex(x, y, z, + fcolor, + normalX, normalY, normalZ, + u, v, + scolor, sweight, + ambientColor, specularColor, emissiveColor, shininess, + VERTEX, vertexBreak()); + + markForTessellation(); + } + + + protected boolean vertexBreak() { + if (breakShape) { + breakShape = false; + return true; + } + return false; + } + + + @Override + public void normal(float nx, float ny, float nz) { + if (!openShape) { + PGraphics.showWarning(OUTSIDE_BEGIN_END_ERROR, "normal()"); + return; + } + + if (family == GROUP) { + PGraphics.showWarning("Cannot set normal in GROUP shape"); + return; + } + + normalX = nx; + normalY = ny; + normalZ = nz; + + // if drawing a shape and the normal hasn't been set yet, + // then we need to set the normals for each vertex so far + if (normalMode == NORMAL_MODE_AUTO) { + // One normal per begin/end shape + normalMode = NORMAL_MODE_SHAPE; + } else if (normalMode == NORMAL_MODE_SHAPE) { + // a separate normal for each vertex + normalMode = NORMAL_MODE_VERTEX; + } + } + + + @Override + public void attribPosition(String name, float x, float y, float z) { + VertexAttribute attrib = attribImpl(name, VertexAttribute.POSITION, + PGL.FLOAT, 3); + if (attrib != null) attrib.set(x, y, z); + } + + + @Override + public void attribNormal(String name, float nx, float ny, float nz) { + VertexAttribute attrib = attribImpl(name, VertexAttribute.NORMAL, + PGL.FLOAT, 3); + if (attrib != null) attrib.set(nx, ny, nz); + } + + + @Override + public void attribColor(String name, int color) { + VertexAttribute attrib = attribImpl(name, VertexAttribute.COLOR, PGL.INT, 1); + if (attrib != null) attrib.set(new int[] {color}); + } + + + @Override + public void attrib(String name, float... values) { + VertexAttribute attrib = attribImpl(name, VertexAttribute.OTHER, PGL.FLOAT, + values.length); + if (attrib != null) attrib.set(values); + } + + + @Override + public void attrib(String name, int... values) { + VertexAttribute attrib = attribImpl(name, VertexAttribute.OTHER, PGL.INT, + values.length); + if (attrib != null) attrib.set(values); + } + + + @Override + public void attrib(String name, boolean... values) { + VertexAttribute attrib = attribImpl(name, VertexAttribute.OTHER, PGL.BOOL, + values.length); + if (attrib != null) attrib.set(values); + } + + + protected VertexAttribute attribImpl(String name, int kind, int type, int size) { + if (4 < size) { + PGraphics.showWarning("Vertex attributes cannot have more than 4 values"); + return null; + } + VertexAttribute attrib = polyAttribs.get(name); + if (attrib == null) { + attrib = new VertexAttribute(pg, name, kind, type, size); + polyAttribs.put(name, attrib); + inGeo.initAttrib(attrib); + } + if (attrib.kind != kind) { + PGraphics.showWarning("The attribute kind cannot be changed after creation"); + return null; + } + if (attrib.type != type) { + PGraphics.showWarning("The attribute type cannot be changed after creation"); + return null; + } + if (attrib.size != size) { + PGraphics.showWarning("New value for vertex attribute has wrong number of values"); + return null; + } + return attrib; + } + + + @Override + public void endShape(int mode) { + super.endShape(mode); + + // Input arrays are trimmed since they are expanded by doubling their old + // size, which might lead to arrays larger than the vertex counts. + inGeo.trim(); + + close = mode == CLOSE; + markForTessellation(); + shapeCreated = true; + } + + + @Override + public void setParams(float[] source) { + if (family != PRIMITIVE) { + PGraphics.showWarning("Parameters can only be set to PRIMITIVE shapes"); + return; + } + + super.setParams(source); + markForTessellation(); + shapeCreated = true; + } + + + @Override + public void setPath(int vcount, float[][] verts, int ccount, int[] codes) { + if (family != PATH) { + PGraphics.showWarning("Vertex coordinates and codes can only be set to " + + "PATH shapes"); + return; + } + + super.setPath(vcount, verts, ccount, codes); + markForTessellation(); + shapeCreated = true; + } + + + /////////////////////////////////////////////////////////// + + // + + // Geometric transformations + + + @Override + public void translate(float tx, float ty) { + if (is3D) { + transform(TRANSLATE, tx, ty, 0); + } else { + transform(TRANSLATE, tx, ty); + } + } + + + @Override + public void translate(float tx, float ty, float tz) { + transform(TRANSLATE, tx, ty, tz); + } + + + @Override + public void rotate(float angle) { + transform(ROTATE, angle); + } + + + @Override + public void rotateX(float angle) { + rotate(angle, 1, 0, 0); + } + + + @Override + public void rotateY(float angle) { + rotate(angle, 0, 1, 0); + } + + + @Override + public void rotateZ(float angle) { + transform(ROTATE, angle); + } + + + @Override + public void rotate(float angle, float v0, float v1, float v2) { + transform(ROTATE, angle, v0, v1, v2); + } + + + @Override + public void scale(float s) { + if (is3D) { + transform(SCALE, s, s, s); + } else { + transform(SCALE, s, s); + } + } + + + @Override + public void scale(float x, float y) { + if (is3D) { + transform(SCALE, x, y, 1); + } else { + transform(SCALE, x, y); + } + } + + + @Override + public void scale(float x, float y, float z) { + transform(SCALE, x, y, z); + } + + + @Override + public void applyMatrix(PMatrix2D source) { + transform(MATRIX, source.m00, source.m01, source.m02, + source.m10, source.m11, source.m12); + } + + + @Override + public void applyMatrix(float n00, float n01, float n02, + float n10, float n11, float n12) { + transform(MATRIX, n00, n01, n02, + n10, n11, n12); + } + + + @Override + public void applyMatrix(float n00, float n01, float n02, float n03, + float n10, float n11, float n12, float n13, + float n20, float n21, float n22, float n23, + float n30, float n31, float n32, float n33) { + transform(MATRIX, n00, n01, n02, n03, + n10, n11, n12, n13, + n20, n21, n22, n23, + n30, n31, n32, n33); + } + + + @Override + public void resetMatrix() { + if (shapeCreated && matrix != null && transformStack != null) { + if (family == GROUP) { + updateTessellation(); + } + if (tessellated) { + PMatrix mat = popTransform(); + while (mat != null) { + boolean res = mat.invert(); + if (res) { + applyMatrixImpl(mat); + } else { + PGraphics.showWarning("Transformation applied on the shape cannot be inverted"); + } + mat = popTransform(); + } + } + matrix.reset(); + transformStack.clear(); + } + } + + + protected void transform(int type, float... args) { + int dimensions = is3D ? 3 : 2; + checkMatrix(dimensions); + if (transform == null) { + if (dimensions == 2) { + transform = new PMatrix2D(); + } else { + transform = new PMatrix3D(); + } + } else { + transform.reset(); + } + + int ncoords = args.length; + if (type == ROTATE) { + ncoords = args.length == 1 ? 2 : 3; + } else if (type == MATRIX) { + ncoords = args.length == 6 ? 2 : 3; + } + + switch (type) { + case TRANSLATE: + if (ncoords == 3) { + transform.translate(args[0], args[1], args[2]); + } else { + transform.translate(args[0], args[1]); + } + break; + case ROTATE: + if (ncoords == 3) { + transform.rotate(args[0], args[1], args[2], args[3]); + } else { + transform.rotate(args[0]); + } + break; + case SCALE: + if (ncoords == 3) { + transform.scale(args[0], args[1], args[2]); + } else { + transform.scale(args[0], args[1]); + } + break; + case MATRIX: + if (ncoords == 3) { + transform.set(args[ 0], args[ 1], args[ 2], args[ 3], + args[ 4], args[ 5], args[ 6], args[ 7], + args[ 8], args[ 9], args[10], args[11], + args[12], args[13], args[14], args[15]); + } else { + transform.set(args[0], args[1], args[2], + args[3], args[4], args[5]); + } + break; + } + matrix.preApply(transform); + pushTransform(); + if (tessellated) applyMatrixImpl(transform); + } + + + protected void pushTransform() { + if (transformStack == null) transformStack = new Stack(); + PMatrix mat; + if (transform instanceof PMatrix2D) { + mat = new PMatrix2D(); + } else { + mat = new PMatrix3D(); + } + mat.set(transform); + transformStack.push(mat); + } + + + protected PMatrix popTransform() { + if (transformStack == null || transformStack.size() == 0) return null; + return transformStack.pop(); + } + + protected void applyMatrixImpl(PMatrix matrix) { + if (hasPolys) { + tessGeo.applyMatrixOnPolyGeometry(matrix, + firstPolyVertex, lastPolyVertex); + root.setModifiedPolyVertices(firstPolyVertex, lastPolyVertex); + root.setModifiedPolyNormals(firstPolyVertex, lastPolyVertex); + for (VertexAttribute attrib: polyAttribs.values()) { + if (attrib.isPosition() || attrib.isNormal()) { + root.setModifiedPolyAttrib(attrib, firstPolyVertex, lastPolyVertex); + } + } + } + + if (is3D()) { + if (hasLines) { + tessGeo.applyMatrixOnLineGeometry(matrix, + firstLineVertex, lastLineVertex); + root.setModifiedLineVertices(firstLineVertex, lastLineVertex); + root.setModifiedLineAttributes(firstLineVertex, lastLineVertex); + } + + if (hasPoints) { + tessGeo.applyMatrixOnPointGeometry(matrix, + firstPointVertex, lastPointVertex); + root.setModifiedPointVertices(firstPointVertex, lastPointVertex); + root.setModifiedPointAttributes(firstPointVertex, lastPointVertex); + } + } + } + + + /////////////////////////////////////////////////////////// + + // + + // Bezier curves + + + @Override + public void bezierDetail(int detail) { + bezierDetail = detail; + if (0 < inGeo.codeCount) { + markForTessellation(); + } + //pg.bezierDetail(detail); // setting the detail in the renderer, WTF?? + } + + + @Override + public void bezierVertex(float x2, float y2, + float x3, float y3, + float x4, float y4) { + bezierVertexImpl(x2, y2, 0, + x3, y3, 0, + x4, y4, 0); + } + + + @Override + public void bezierVertex(float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + bezierVertexImpl(x2, y2, z2, + x3, y3, z3, + x4, y4, z4); + } + + + protected void bezierVertexImpl(float x2, float y2, float z2, + float x3, float y3, float z3, + float x4, float y4, float z4) { + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addBezierVertex(x2, y2, z2, + x3, y3, z3, + x4, y4, z4, vertexBreak()); + } + + + @Override + public void quadraticVertex(float cx, float cy, + float x3, float y3) { + quadraticVertexImpl(cx, cy, 0, + x3, y3, 0); + } + + + @Override + public void quadraticVertex(float cx, float cy, float cz, + float x3, float y3, float z3) { + quadraticVertexImpl(cx, cy, cz, + x3, y3, z3); + } + + + protected void quadraticVertexImpl(float cx, float cy, float cz, + float x3, float y3, float z3) { + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addQuadraticVertex(cx, cy, cz, + x3, y3, z3, vertexBreak()); + } + + + /////////////////////////////////////////////////////////// + + // + + // Catmull-Rom curves + + + @Override + public void curveDetail(int detail) { + curveDetail = detail; +// pg.curveDetail(detail); + if (0 < inGeo.codeCount) { + markForTessellation(); + } + } + + + @Override + public void curveTightness(float tightness) { + curveTightness = tightness; +// pg.curveTightness(tightness); + if (0 < inGeo.codeCount) { + markForTessellation(); + } + } + + + @Override + public void curveVertex(float x, float y) { + curveVertexImpl(x, y, 0); + } + + + @Override + public void curveVertex(float x, float y, float z) { + curveVertexImpl(x, y, z); + } + + + protected void curveVertexImpl(float x, float y, float z) { + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addCurveVertex(x, y, z, vertexBreak()); + } + + + /////////////////////////////////////////////////////////// + + // + + // Setters/getters of individual vertices + + + @Override + public int getVertexCount() { + if (family == GROUP) return 0; // Group shapes don't have vertices + else { + if (family == PRIMITIVE || family == PATH) { + // the input geometry of primitive and path shapes is built during + // tessellation + updateTessellation(); + } + return inGeo.vertexCount; + } + } + + + @Override + public PVector getVertex(int index, PVector vec) { + if (vec == null) { + vec = new PVector(); + } + vec.x = inGeo.vertices[3 * index + 0]; + vec.y = inGeo.vertices[3 * index + 1]; + vec.z = inGeo.vertices[3 * index + 2]; + return vec; + } + + + @Override + public float getVertexX(int index) { + return inGeo.vertices[3 * index + 0]; + } + + + @Override + public float getVertexY(int index) { + return inGeo.vertices[3 * index + 1]; + } + + + @Override + public float getVertexZ(int index) { + return inGeo.vertices[3 * index + 2]; + } + + + @Override + public void setVertex(int index, float x, float y) { + setVertex(index, x, y, 0); + } + + + @Override + public void setVertex(int index, float x, float y, float z) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setVertex()"); + return; + } + + // TODO: in certain cases (kind = TRIANGLE, etc) the correspondence between + // input and tessellated vertices is 1-1, so in those cases re-tessellation + // wouldn't be necessary. But in order to reasonable take care of that + // situation, we would need a complete rethinking of the rendering architecture + // in Processing :-) + if (family == PATH) { + if (vertexCodes != null && vertexCodeCount > 0 && + vertexCodes[index] != VERTEX) { + PGraphics.showWarning(NOT_A_SIMPLE_VERTEX, "setVertex()"); + return; + } + vertices[index][X] = x; + vertices[index][Y] = y; + if (is3D && vertices[index].length > 2) { + // P3D allows to modify 2D shapes, ignoring the Z coordinate. + vertices[index][Z] = z; + } + } else { + inGeo.vertices[3 * index + 0] = x; + inGeo.vertices[3 * index + 1] = y; + inGeo.vertices[3 * index + 2] = z; + } + markForTessellation(); + } + + + @Override + public void setVertex(int index, PVector vec) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setVertex()"); + return; + } + + if (family == PATH) { + if (vertexCodes != null && vertexCodeCount > 0 && + vertexCodes[index] != VERTEX) { + PGraphics.showWarning(NOT_A_SIMPLE_VERTEX, "setVertex()"); + return; + } + vertices[index][X] = vec.x; + vertices[index][Y] = vec.y; + if (is3D && vertices[index].length > 2) { + vertices[index][Z] = vec.z; + } + } else { + inGeo.vertices[3 * index + 0] = vec.x; + inGeo.vertices[3 * index + 1] = vec.y; + inGeo.vertices[3 * index + 2] = vec.z; + } + markForTessellation(); + } + + + @Override + public PVector getNormal(int index, PVector vec) { + if (vec == null) { + vec = new PVector(); + } + vec.x = inGeo.normals[3 * index + 0]; + vec.y = inGeo.normals[3 * index + 1]; + vec.z = inGeo.normals[3 * index + 2]; + return vec; + } + + + @Override + public float getNormalX(int index) { + return inGeo.normals[3 * index + 0]; + } + + + @Override + public float getNormalY(int index) { + return inGeo.normals[3 * index + 1]; + } + + + @Override + public float getNormalZ(int index) { + return inGeo.normals[3 * index + 2]; + } + + + @Override + public void setNormal(int index, float nx, float ny, float nz) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setNormal()"); + return; + } + + inGeo.normals[3 * index + 0] = nx; + inGeo.normals[3 * index + 1] = ny; + inGeo.normals[3 * index + 2] = nz; + markForTessellation(); + } + + + @Override + public void setAttrib(String name, int index, float... values) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setNormal()"); + return; + } + + VertexAttribute attrib = polyAttribs.get(name); + float[] array = inGeo.fattribs.get(name); + for (int i = 0; i < values.length; i++) { + array[attrib.size * index + 0] = values[i]; + } + markForTessellation(); + } + + + @Override + public void setAttrib(String name, int index, int... values) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setNormal()"); + return; + } + + VertexAttribute attrib = polyAttribs.get(name); + int[] array = inGeo.iattribs.get(name); + for (int i = 0; i < values.length; i++) { + array[attrib.size * index + 0] = values[i]; + } + markForTessellation(); + } + + + @Override + public void setAttrib(String name, int index, boolean... values) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setNormal()"); + return; + } + + VertexAttribute attrib = polyAttribs.get(name); + byte[] array = inGeo.battribs.get(name); + for (int i = 0; i < values.length; i++) { + array[attrib.size * index + 0] = (byte)(values[i]?1:0); + } + markForTessellation(); + } + + + @Override + public float getTextureU(int index) { + return inGeo.texcoords[2 * index + 0]; + } + + + @Override + public float getTextureV(int index) { + return inGeo.texcoords[2 * index + 1]; + } + + + @Override + public void setTextureUV(int index, float u, float v) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setTextureUV()"); + return; + } + + if (textureMode == IMAGE && image != null) { + u /= image.width; + v /= image.height; + } + inGeo.texcoords[2 * index + 0] = u; + inGeo.texcoords[2 * index + 1] = v; + + markForTessellation(); + } + + + @Override + public int getFill(int index) { + if (family != GROUP && image == null) { + return PGL.nativeToJavaARGB(inGeo.colors[index]); + } else { + return 0; + } + } + + + @Override + public void setFill(boolean fill) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setFill()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setFill(fill); + } + } else if (this.fill != fill) { + markForTessellation(); + } + this.fill = fill; + } + + + @Override + public void setFill(int fill) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setFill()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setFill(fill); + } + } else { + setFillImpl(fill); + } + } + + + protected void setFillImpl(int fill) { + if (fillColor == fill) return; + fillColor = fill; + + if (image == null) { + Arrays.fill(inGeo.colors, 0, inGeo.vertexCount, + PGL.javaToNativeARGB(fillColor)); + if (shapeCreated && tessellated && hasPolys) { + if (is3D()) { + Arrays.fill(tessGeo.polyColors, firstPolyVertex, lastPolyVertex + 1, + PGL.javaToNativeARGB(fillColor)); + root.setModifiedPolyColors(firstPolyVertex, lastPolyVertex); + } else if (is2D()) { + int last1 = lastPolyVertex + 1; + if (-1 < firstLineVertex) last1 = firstLineVertex; + if (-1 < firstPointVertex) last1 = firstPointVertex; + Arrays.fill(tessGeo.polyColors, firstPolyVertex, last1, + PGL.javaToNativeARGB(fillColor)); + root.setModifiedPolyColors(firstPolyVertex, last1 - 1); + } + } + } + + if (!setAmbient) { + // Setting the ambient color from the current fill + // is what the old P3D did and allows to have an + // default ambient color when the user doesn't specify + // it explicitly. + setAmbientImpl(fill); + setAmbient = false; + } + } + + + @Override + public void setFill(int index, int fill) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setFill()"); + return; + } + + if (image == null) { + inGeo.colors[index] = PGL.javaToNativeARGB(fill); + markForTessellation(); + } + } + + + @Override + public int getTint(int index) { + if (family != GROUP && image != null) { + return PGL.nativeToJavaARGB(inGeo.colors[index]); + } else { + return 0; + } + } + + + @Override + public void setTint(boolean tint) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setTint()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setTint(fill); + } + } else if (this.tint && !tint) { + setTintImpl(0xFFFFFFFF); + } + this.tint = tint; + } + + + @Override + public void setTint(int tint) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setTint()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setTint(tint); + } + } else { + setTintImpl(tint); + } + } + + + protected void setTintImpl(int tint) { + if (tintColor == tint) return; + tintColor = tint; + + if (image != null) { + Arrays.fill(inGeo.colors, 0, inGeo.vertexCount, + PGL.javaToNativeARGB(tintColor)); + if (shapeCreated && tessellated && hasPolys) { + if (is3D()) { + Arrays.fill(tessGeo.polyColors, firstPolyVertex, lastPolyVertex + 1, + PGL.javaToNativeARGB(tintColor)); + root.setModifiedPolyColors(firstPolyVertex, lastPolyVertex); + } else if (is2D()) { + int last1 = lastPolyVertex + 1; + if (-1 < firstLineVertex) last1 = firstLineVertex; + if (-1 < firstPointVertex) last1 = firstPointVertex; + Arrays.fill(tessGeo.polyColors, firstPolyVertex, last1, + PGL.javaToNativeARGB(tintColor)); + root.setModifiedPolyColors(firstPolyVertex, last1 - 1); + } + } + } + } + + + @Override + public void setTint(int index, int tint) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setTint()"); + return; + } + + if (image != null) { + inGeo.colors[index] = PGL.javaToNativeARGB(tint); + markForTessellation(); + } + } + + + @Override + public int getStroke(int index) { + if (family != GROUP) { + return PGL.nativeToJavaARGB(inGeo.strokeColors[index]); + } else { + return 0; + } + } + + + @Override + public void setStroke(boolean stroke) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStroke()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setStroke(stroke); + } + this.stroke = stroke; + } else { + setStrokeImpl(stroke); + } + } + + + protected void setStrokeImpl(boolean stroke) { + if (this.stroke != stroke) { + if (stroke) { + // Before there was no stroke, now there is stroke, so current stroke + // color should be copied to the input geometry, and geometry should + // be marked as modified in case it needs to be re-tessellated. + int color = strokeColor; + strokeColor += 1; // Forces a color change + setStrokeImpl(color); + } + + markForTessellation(); + if (is2D() && parent != null) { + ((PShapeOpenGL)parent).strokedTexture(stroke && image != null); + } + + this.stroke = stroke; + } + } + + + @Override + public void setStroke(int stroke) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStroke()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setStroke(stroke); + } + } else { + setStrokeImpl(stroke); + } + } + + + protected void setStrokeImpl(int stroke) { + if (strokeColor == stroke) return; + strokeColor = stroke; + + Arrays.fill(inGeo.strokeColors, 0, inGeo.vertexCount, + PGL.javaToNativeARGB(strokeColor)); + if (shapeCreated && tessellated && (hasLines || hasPoints)) { + if (hasLines) { + if (is3D()) { + Arrays.fill(tessGeo.lineColors, firstLineVertex, lastLineVertex + 1, + PGL.javaToNativeARGB(strokeColor)); + root.setModifiedLineColors(firstLineVertex, lastLineVertex); + } else if (is2D()) { + Arrays.fill(tessGeo.polyColors, firstLineVertex, lastLineVertex + 1, + PGL.javaToNativeARGB(strokeColor)); + root.setModifiedPolyColors(firstLineVertex, lastLineVertex); + } + } + if (hasPoints) { + if (is3D()) { + Arrays.fill(tessGeo.pointColors, firstPointVertex, lastPointVertex + 1, + PGL.javaToNativeARGB(strokeColor)); + root.setModifiedPointColors(firstPointVertex, lastPointVertex); + } else if (is2D()) { + Arrays.fill(tessGeo.polyColors, firstPointVertex, lastPointVertex + 1, + PGL.javaToNativeARGB(strokeColor)); + root.setModifiedPolyColors(firstPointVertex, lastPointVertex); + } + } + } + } + + + @Override + public void setStroke(int index, int stroke) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStroke()"); + return; + } + + inGeo.strokeColors[index] = PGL.javaToNativeARGB(stroke); + markForTessellation(); + } + + + @Override + public float getStrokeWeight(int index) { + if (family != GROUP) { + return inGeo.strokeWeights[index]; + } else { + return 0; + } + } + + + @Override + public void setStrokeWeight(float weight) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStrokeWeight()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setStrokeWeight(weight); + } + } else { + setStrokeWeightImpl(weight); + } + } + + + protected void setStrokeWeightImpl(float weight) { + if (PGraphicsOpenGL.same(strokeWeight, weight)) return; + float oldWeight = strokeWeight; + strokeWeight = weight; + + Arrays.fill(inGeo.strokeWeights, 0, inGeo.vertexCount, strokeWeight); + if (shapeCreated && tessellated && (hasLines || hasPoints)) { + float resizeFactor = weight / oldWeight; + if (hasLines) { + if (is3D()) { + for (int i = firstLineVertex; i <= lastLineVertex; i++) { + tessGeo.lineDirections[4 * i + 3] *= resizeFactor; + } + root.setModifiedLineAttributes(firstLineVertex, lastLineVertex); + } else if (is2D()) { + // Changing the stroke weight on a 2D shape needs a + // re-tessellation in order to replace the old line + // geometry. + markForTessellation(); + } + } + if (hasPoints) { + if (is3D()) { + for (int i = firstPointVertex; i <= lastPointVertex; i++) { + tessGeo.pointOffsets[2 * i + 0] *= resizeFactor; + tessGeo.pointOffsets[2 * i + 1] *= resizeFactor; + } + root.setModifiedPointAttributes(firstPointVertex, lastPointVertex); + } else if (is2D()) { + // Changing the stroke weight on a 2D shape needs a + // re-tessellation in order to replace the old point + // geometry. + markForTessellation(); + } + } + } + } + + + @Override + public void setStrokeWeight(int index, float weight) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStrokeWeight()"); + return; + } + + inGeo.strokeWeights[index] = weight; + markForTessellation(); + } + + + @Override + public void setStrokeJoin(int join) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStrokeJoin()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setStrokeJoin(join); + } + } else { + if (is2D() && strokeJoin != join) { + // Changing the stroke join on a 2D shape needs a + // re-tessellation in order to replace the old join + // geometry. + markForTessellation(); + } + strokeJoin = join; + } + } + + + @Override + public void setStrokeCap(int cap) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setStrokeCap()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setStrokeCap(cap); + } + } else { + if (is2D() && strokeCap != cap) { + // Changing the stroke cap on a 2D shape needs a + // re-tessellation in order to replace the old cap + // geometry. + markForTessellation(); + } + strokeCap = cap; + } + } + + + @Override + public int getAmbient(int index) { + if (family != GROUP) { + return PGL.nativeToJavaARGB(inGeo.ambient[index]); + } else { + return 0; + } + } + + + @Override + public void setAmbient(int ambient) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setAmbient()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setAmbient(ambient); + } + } else { + setAmbientImpl(ambient); + } + } + + + protected void setAmbientImpl(int ambient) { + if (ambientColor == ambient) return; + ambientColor = ambient; + + Arrays.fill(inGeo.ambient, 0, inGeo.vertexCount, + PGL.javaToNativeARGB(ambientColor)); + if (shapeCreated && tessellated && hasPolys) { + if (is3D()) { + Arrays.fill(tessGeo.polyAmbient, firstPolyVertex, lastPolyVertex + 1, + PGL.javaToNativeARGB(ambientColor)); + root.setModifiedPolyAmbient(firstPolyVertex, lastPolyVertex); + } else if (is2D()) { + int last1 = lastPolyVertex + 1; + if (-1 < firstLineVertex) last1 = firstLineVertex; + if (-1 < firstPointVertex) last1 = firstPointVertex; + Arrays.fill(tessGeo.polyAmbient, firstPolyVertex, last1, + PGL.javaToNativeARGB(ambientColor)); + root.setModifiedPolyColors(firstPolyVertex, last1 - 1); + } + } + setAmbient = true; + } + + + @Override + public void setAmbient(int index, int ambient) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setAmbient()"); + return; + } + + inGeo.ambient[index] = PGL.javaToNativeARGB(ambient); + markForTessellation(); + setAmbient = true; + } + + + @Override + public int getSpecular(int index) { + if (family == GROUP) { + return PGL.nativeToJavaARGB(inGeo.specular[index]); + } else { + return 0; + } + } + + + @Override + public void setSpecular(int specular) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setSpecular()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setSpecular(specular); + } + } else { + setSpecularImpl(specular); + } + } + + + protected void setSpecularImpl(int specular) { + if (specularColor == specular) return; + specularColor = specular; + + Arrays.fill(inGeo.specular, 0, inGeo.vertexCount, + PGL.javaToNativeARGB(specularColor)); + if (shapeCreated && tessellated && hasPolys) { + if (is3D()) { + Arrays.fill(tessGeo.polySpecular, firstPolyVertex, lastPolyVertex + 1, + PGL.javaToNativeARGB(specularColor)); + root.setModifiedPolySpecular(firstPolyVertex, lastPolyVertex); + } else if (is2D()) { + int last1 = lastPolyVertex + 1; + if (-1 < firstLineVertex) last1 = firstLineVertex; + if (-1 < firstPointVertex) last1 = firstPointVertex; + Arrays.fill(tessGeo.polySpecular, firstPolyVertex, last1, + PGL.javaToNativeARGB(specularColor)); + root.setModifiedPolyColors(firstPolyVertex, last1 - 1); + } + } + } + + + @Override + public void setSpecular(int index, int specular) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setSpecular()"); + return; + } + + inGeo.specular[index] = PGL.javaToNativeARGB(specular); + markForTessellation(); + } + + + @Override + public int getEmissive(int index) { + if (family == GROUP) { + return PGL.nativeToJavaARGB(inGeo.emissive[index]); + } else { + return 0; + } + } + + + @Override + public void setEmissive(int emissive) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setEmissive()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setEmissive(emissive); + } + } else { + setEmissiveImpl(emissive); + } + } + + + protected void setEmissiveImpl(int emissive) { + if (emissiveColor == emissive) return; + emissiveColor = emissive; + + Arrays.fill(inGeo.emissive, 0, inGeo.vertexCount, + PGL.javaToNativeARGB(emissiveColor)); + if (shapeCreated && tessellated && 0 < tessGeo.polyVertexCount) { + if (is3D()) { + Arrays.fill(tessGeo.polyEmissive, firstPolyVertex, lastPolyVertex + 1, + PGL.javaToNativeARGB(emissiveColor)); + root.setModifiedPolyEmissive(firstPolyVertex, lastPolyVertex); + } else if (is2D()) { + int last1 = lastPolyVertex + 1; + if (-1 < firstLineVertex) last1 = firstLineVertex; + if (-1 < firstPointVertex) last1 = firstPointVertex; + Arrays.fill(tessGeo.polyEmissive, firstPolyVertex, last1, + PGL.javaToNativeARGB(emissiveColor)); + root.setModifiedPolyColors(firstPolyVertex, last1 - 1); + } + } + } + + + @Override + public void setEmissive(int index, int emissive) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setEmissive()"); + return; + } + + inGeo.emissive[index] = PGL.javaToNativeARGB(emissive); + markForTessellation(); + } + + + @Override + public float getShininess(int index) { + if (family == GROUP) { + return inGeo.shininess[index]; + } else { + return 0; + } + } + + + @Override + public void setShininess(float shininess) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setShininess()"); + return; + } + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.setShininess(shininess); + } + } else { + setShininessImpl(shininess); + } + } + + + protected void setShininessImpl(float shininess) { + if (PGraphicsOpenGL.same(this.shininess, shininess)) return; + this.shininess = shininess; + + Arrays.fill(inGeo.shininess, 0, inGeo.vertexCount, shininess); + if (shapeCreated && tessellated && hasPolys) { + if (is3D()) { + Arrays.fill(tessGeo.polyShininess, firstPolyVertex, lastPolyVertex + 1, + shininess); + root.setModifiedPolyShininess(firstPolyVertex, lastPolyVertex); + } else if (is2D()) { + int last1 = lastPolyVertex + 1; + if (-1 < firstLineVertex) last1 = firstLineVertex; + if (-1 < firstPointVertex) last1 = firstPointVertex; + Arrays.fill(tessGeo.polyShininess, firstPolyVertex, last1, shininess); + root.setModifiedPolyColors(firstPolyVertex, last1 - 1); + } + } + } + + + @Override + public void setShininess(int index, float shine) { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "setShininess()"); + return; + } + + inGeo.shininess[index] = shine; + markForTessellation(); + } + + /////////////////////////////////////////////////////////// + + // + + // Vertex codes + + + @Override + public int[] getVertexCodes() { + if (family == GROUP) return null; + else { + if (family == PRIMITIVE || family == PATH) { + // the input geometry of primitive and path shapes is built during + // tessellation + updateTessellation(); + } + if (inGeo.codes == null) return null; + return inGeo.codes; + } + } + + + @Override + public int getVertexCodeCount() { + if (family == GROUP) return 0; + else { + if (family == PRIMITIVE || family == PATH) { + // the input geometry of primitive and path shapes is built during + // tessellation + updateTessellation(); + } + return inGeo.codeCount; + } + } + + + /** + * One of VERTEX, BEZIER_VERTEX, CURVE_VERTEX, or BREAK. + */ + @Override + public int getVertexCode(int index) { + return inGeo.codes[index]; + } + + + /////////////////////////////////////////////////////////// + + // + + // Tessellated geometry getter. + + + @Override + public PShape getTessellation() { + updateTessellation(); + + float[] vertices = tessGeo.polyVertices; + float[] normals = tessGeo.polyNormals; + int[] color = tessGeo.polyColors; + float[] uv = tessGeo.polyTexCoords; + short[] indices = tessGeo.polyIndices; + + PShape tess; +// if (is3D()) { +// //tess = PGraphics3D.createShapeImpl(pg, PShape.GEOMETRY); +// tess = pg.createShapeFamily(PShape.GEOMETRY); +// } else if (is2D()) { +// //tess = PGraphics2D.createShapeImpl(pg, PShape.GEOMETRY); +// tess = pg.createShapeFamily(PShape.GEOMETRY); +// } else { +// PGraphics.showWarning("This shape is not either 2D or 3D!"); +// return null; +// } + tess = pg.createShapeFamily(PShape.GEOMETRY); + tess.set3D(is3D); // if this is a 3D shape, make the new shape 3D as well + tess.beginShape(TRIANGLES); + tess.noStroke(); + + IndexCache cache = tessGeo.polyIndexCache; + for (int n = firstPolyIndexCache; n <= lastPolyIndexCache; n++) { + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int voffset = cache.vertexOffset[n]; + + for (int tr = ioffset / 3; tr < (ioffset + icount) / 3; tr++) { + int i0 = voffset + indices[3 * tr + 0]; + int i1 = voffset + indices[3 * tr + 1]; + int i2 = voffset + indices[3 * tr + 2]; + + if (is3D()) { + float x0 = vertices[4 * i0 + 0]; + float y0 = vertices[4 * i0 + 1]; + float z0 = vertices[4 * i0 + 2]; + float x1 = vertices[4 * i1 + 0]; + float y1 = vertices[4 * i1 + 1]; + float z1 = vertices[4 * i1 + 2]; + float x2 = vertices[4 * i2 + 0]; + float y2 = vertices[4 * i2 + 1]; + float z2 = vertices[4 * i2 + 2]; + + float nx0 = normals[3 * i0 + 0]; + float ny0 = normals[3 * i0 + 1]; + float nz0 = normals[3 * i0 + 2]; + float nx1 = normals[3 * i1 + 0]; + float ny1 = normals[3 * i1 + 1]; + float nz1 = normals[3 * i1 + 2]; + float nx2 = normals[3 * i2 + 0]; + float ny2 = normals[3 * i2 + 1]; + float nz2 = normals[3 * i2 + 2]; + + int argb0 = PGL.nativeToJavaARGB(color[i0]); + int argb1 = PGL.nativeToJavaARGB(color[i1]); + int argb2 = PGL.nativeToJavaARGB(color[i2]); + + tess.fill(argb0); + tess.normal(nx0, ny0, nz0); + tess.vertex(x0, y0, z0, uv[2 * i0 + 0], uv[2 * i0 + 1]); + + tess.fill(argb1); + tess.normal(nx1, ny1, nz1); + tess.vertex(x1, y1, z1, uv[2 * i1 + 0], uv[2 * i1 + 1]); + + tess.fill(argb2); + tess.normal(nx2, ny2, nz2); + tess.vertex(x2, y2, z2, uv[2 * i2 + 0], uv[2 * i2 + 1]); + } else if (is2D()) { + float x0 = vertices[4 * i0 + 0], y0 = vertices[4 * i0 + 1]; + float x1 = vertices[4 * i1 + 0], y1 = vertices[4 * i1 + 1]; + float x2 = vertices[4 * i2 + 0], y2 = vertices[4 * i2 + 1]; + + int argb0 = PGL.nativeToJavaARGB(color[i0]); + int argb1 = PGL.nativeToJavaARGB(color[i1]); + int argb2 = PGL.nativeToJavaARGB(color[i2]); + + tess.fill(argb0); + tess.vertex(x0, y0, uv[2 * i0 + 0], uv[2 * i0 + 1]); + + tess.fill(argb1); + tess.vertex(x1, y1, uv[2 * i1 + 0], uv[2 * i1 + 1]); + + tess.fill(argb2); + tess.vertex(x2, y2, uv[2 * i2 + 0], uv[2 * i2 + 1]); + } + } + } + tess.endShape(); + + return tess; + } + + // Testing this method, not use as it might go away... + public float[] getTessellation(int kind, int data) { + updateTessellation(); + + if (kind == TRIANGLES) { + if (data == POSITION) { + if (is3D()) { + root.setModifiedPolyVertices(firstPolyVertex, lastPolyVertex); + } else if (is2D()) { + int last1 = lastPolyVertex + 1; + if (-1 < firstLineVertex) last1 = firstLineVertex; + if (-1 < firstPointVertex) last1 = firstPointVertex; + root.setModifiedPolyVertices(firstPolyVertex, last1 - 1); + } + return tessGeo.polyVertices; + } else if (data == NORMAL) { + if (is3D()) { + root.setModifiedPolyNormals(firstPolyVertex, lastPolyVertex); + } else if (is2D()) { + int last1 = lastPolyVertex + 1; + if (-1 < firstLineVertex) last1 = firstLineVertex; + if (-1 < firstPointVertex) last1 = firstPointVertex; + root.setModifiedPolyNormals(firstPolyVertex, last1 - 1); + } + return tessGeo.polyNormals; + } else if (data == TEXCOORD) { + if (is3D()) { + root.setModifiedPolyTexCoords(firstPolyVertex, lastPolyVertex); + } else if (is2D()) { + int last1 = lastPolyVertex + 1; + if (-1 < firstLineVertex) last1 = firstLineVertex; + if (-1 < firstPointVertex) last1 = firstPointVertex; + root.setModifiedPolyTexCoords(firstPolyVertex, last1 - 1); + } + return tessGeo.polyTexCoords; + } + } else if (kind == LINES) { + if (data == POSITION) { + if (is3D()) { + root.setModifiedLineVertices(firstLineVertex, lastLineVertex); + } else if (is2D()) { + root.setModifiedPolyVertices(firstLineVertex, lastLineVertex); + } + return tessGeo.lineVertices; + } else if (data == DIRECTION) { + if (is2D()) { + root.setModifiedLineAttributes(firstLineVertex, lastLineVertex); + } + return tessGeo.lineDirections; + } + } else if (kind == POINTS) { + if (data == POSITION) { + if (is3D()) { + root.setModifiedPointVertices(firstPointVertex, lastPointVertex); + } else if (is2D()) { + root.setModifiedPolyVertices(firstPointVertex, lastPointVertex); + } + return tessGeo.pointVertices; + } else if (data == OFFSET) { + if (is2D()) { + root.setModifiedPointAttributes(firstPointVertex, lastPointVertex); + } + return tessGeo.pointOffsets; + } + } + return null; + } + + /////////////////////////////////////////////////////////// + + // + + // Geometry utils + + // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html + @Override + public boolean contains(float x, float y) { + if (family == PATH) { + boolean c = false; + for (int i = 0, j = inGeo.vertexCount-1; i < inGeo.vertexCount; j = i++) { + if (((inGeo.vertices[3 * i + 1] > y) != (inGeo.vertices[3 * j + 1] > y)) && + (x < + (inGeo.vertices[3 * j]-inGeo.vertices[3 * i]) * + (y-inGeo.vertices[3 * i + 1]) / + (inGeo.vertices[3 * j + 1]-inGeo.vertices[3 * i + 1]) + + inGeo.vertices[3 * i])) { + c = !c; + } + } + return c; + } else { + throw new IllegalArgumentException("The contains() method is only implemented for paths."); + } + } + + + /////////////////////////////////////////////////////////// + + // + + // Tessellation + + + protected void updateTessellation() { + if (!root.tessellated) { + root.tessellate(); + root.aggregate(); + root.initModified(); + root.needBufferInit = true; + } + } + + + protected void markForTessellation() { + root.tessellated = false; + tessellated = false; + } + + + protected void initModified() { + modified = false; + + modifiedPolyVertices = false; + modifiedPolyColors = false; + modifiedPolyNormals = false; + modifiedPolyTexCoords = false; + modifiedPolyAmbient = false; + modifiedPolySpecular = false; + modifiedPolyEmissive = false; + modifiedPolyShininess = false; + + modifiedLineVertices = false; + modifiedLineColors = false; + modifiedLineAttributes = false; + + modifiedPointVertices = false; + modifiedPointColors = false; + modifiedPointAttributes = false; + + firstModifiedPolyVertex = PConstants.MAX_INT; + lastModifiedPolyVertex = PConstants.MIN_INT; + firstModifiedPolyColor = PConstants.MAX_INT; + lastModifiedPolyColor = PConstants.MIN_INT; + firstModifiedPolyNormal = PConstants.MAX_INT; + lastModifiedPolyNormal = PConstants.MIN_INT; + firstModifiedPolyTexcoord = PConstants.MAX_INT; + lastModifiedPolyTexcoord = PConstants.MIN_INT; + firstModifiedPolyAmbient = PConstants.MAX_INT; + lastModifiedPolyAmbient = PConstants.MIN_INT; + firstModifiedPolySpecular = PConstants.MAX_INT; + lastModifiedPolySpecular = PConstants.MIN_INT; + firstModifiedPolyEmissive = PConstants.MAX_INT; + lastModifiedPolyEmissive = PConstants.MIN_INT; + firstModifiedPolyShininess = PConstants.MAX_INT; + lastModifiedPolyShininess = PConstants.MIN_INT; + + firstModifiedLineVertex = PConstants.MAX_INT; + lastModifiedLineVertex = PConstants.MIN_INT; + firstModifiedLineColor = PConstants.MAX_INT; + lastModifiedLineColor = PConstants.MIN_INT; + firstModifiedLineAttribute = PConstants.MAX_INT; + lastModifiedLineAttribute = PConstants.MIN_INT; + + firstModifiedPointVertex = PConstants.MAX_INT; + lastModifiedPointVertex = PConstants.MIN_INT; + firstModifiedPointColor = PConstants.MAX_INT; + lastModifiedPointColor = PConstants.MIN_INT; + firstModifiedPointAttribute = PConstants.MAX_INT; + lastModifiedPointAttribute = PConstants.MIN_INT; + } + + + protected void tessellate() { + if (root == this && parent == null) { // Root shape + if (polyAttribs == null) { + polyAttribs = PGraphicsOpenGL.newAttributeMap(); + collectPolyAttribs(); + } + + if (tessGeo == null) { + tessGeo = PGraphicsOpenGL.newTessGeometry(pg, polyAttribs, PGraphicsOpenGL.RETAINED); + } + tessGeo.clear(); + for (int i = 0; i < polyAttribs.size(); i++) { + VertexAttribute attrib = polyAttribs.get(i); + tessGeo.initAttrib(attrib); + } + + tessellateImpl(); + + // Tessellated arrays are trimmed since they are expanded + // by doubling their old size, which might lead to arrays + // larger than the vertex counts. + tessGeo.trim(); + } + } + + + protected void collectPolyAttribs() { + AttributeMap rootAttribs = root.polyAttribs; + + if (family == GROUP) { + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.collectPolyAttribs(); + } + } else { + for (int i = 0; i < polyAttribs.size(); i++) { + VertexAttribute attrib = polyAttribs.get(i); + tessGeo.initAttrib(attrib); + if (rootAttribs.containsKey(attrib.name)) { + VertexAttribute rattrib = rootAttribs.get(attrib.name); + if (rattrib.diff(attrib)) { + throw new RuntimeException("Children shapes cannot have different attributes with same name"); + } + } else { + rootAttribs.put(attrib.name, attrib); + } + } + } + } + + protected void tessellateImpl() { + tessGeo = root.tessGeo; + + firstPolyIndexCache = -1; + lastPolyIndexCache = -1; + firstLineIndexCache = -1; + lastLineIndexCache = -1; + firstPointIndexCache = -1; + lastPointIndexCache = -1; + + if (family == GROUP) { + if (polyAttribs == null) { + polyAttribs = PGraphicsOpenGL.newAttributeMap(); + collectPolyAttribs(); + } + + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.tessellateImpl(); + } + } else { + if (shapeCreated) { + // If the geometry was tessellated previously, then + // the edges information will still be stored in the + // input object, so it needs to be removed to avoid + // duplication. + inGeo.clearEdges(); + + tessellator.setInGeometry(inGeo); + tessellator.setTessGeometry(tessGeo); + tessellator.setFill(fill || image != null); + tessellator.setTexCache(null, null); + tessellator.setStroke(stroke); + tessellator.setStrokeColor(strokeColor); + tessellator.setStrokeWeight(strokeWeight); + tessellator.setStrokeCap(strokeCap); + tessellator.setStrokeJoin(strokeJoin); + tessellator.setRenderer(pg); + tessellator.setTransform(matrix); + tessellator.set3D(is3D()); + + if (family == GEOMETRY) { + if (kind == POINTS) { + tessellator.tessellatePoints(); + } else if (kind == LINES) { + tessellator.tessellateLines(); + } else if (kind == LINE_STRIP) { + tessellator.tessellateLineStrip(); + } else if (kind == LINE_LOOP) { + tessellator.tessellateLineLoop(); + } else if (kind == TRIANGLE || kind == TRIANGLES) { + if (stroke) inGeo.addTrianglesEdges(); + if (normalMode == NORMAL_MODE_AUTO) inGeo.calcTrianglesNormals(); + tessellator.tessellateTriangles(); + } else if (kind == TRIANGLE_FAN) { + if (stroke) inGeo.addTriangleFanEdges(); + if (normalMode == NORMAL_MODE_AUTO) inGeo.calcTriangleFanNormals(); + tessellator.tessellateTriangleFan(); + } else if (kind == TRIANGLE_STRIP) { + if (stroke) inGeo.addTriangleStripEdges(); + if (normalMode == NORMAL_MODE_AUTO) inGeo.calcTriangleStripNormals(); + tessellator.tessellateTriangleStrip(); + } else if (kind == QUAD || kind == QUADS) { + if (stroke) inGeo.addQuadsEdges(); + if (normalMode == NORMAL_MODE_AUTO) inGeo.calcQuadsNormals(); + tessellator.tessellateQuads(); + } else if (kind == QUAD_STRIP) { + if (stroke) inGeo.addQuadStripEdges(); + if (normalMode == NORMAL_MODE_AUTO) inGeo.calcQuadStripNormals(); + tessellator.tessellateQuadStrip(); + } else if (kind == POLYGON) { + boolean bez = inGeo.hasBezierVertex(); + boolean quad = inGeo.hasQuadraticVertex(); + boolean curv = inGeo.hasCurveVertex(); + if (bez || quad) saveBezierVertexSettings(); + if (curv) { + saveCurveVertexSettings(); + tessellator.resetCurveVertexCount(); + } + tessellator.tessellatePolygon(solid, close, + normalMode == NORMAL_MODE_AUTO); + if (bez ||quad) restoreBezierVertexSettings(); + if (curv) restoreCurveVertexSettings(); + } + } else if (family == PRIMITIVE) { + // The input geometry needs to be cleared because the geometry + // generation methods in InGeometry add the vertices of the + // new primitive to what is already stored. + inGeo.clear(); + + if (kind == POINT) { + tessellatePoint(); + } else if (kind == LINE) { + tessellateLine(); + } else if (kind == TRIANGLE) { + tessellateTriangle(); + } else if (kind == QUAD) { + tessellateQuad(); + } else if (kind == RECT) { + tessellateRect(); + } else if (kind == ELLIPSE) { + tessellateEllipse(); + } else if (kind == ARC) { + tessellateArc(); + } else if (kind == BOX) { + tessellateBox(); + } else if (kind == SPHERE) { + tessellateSphere(); + } + } else if (family == PATH) { + inGeo.clear(); + tessellatePath(); + } + + if (image != null && parent != null) { + ((PShapeOpenGL)parent).addTexture(image); + } + + firstPolyIndexCache = tessellator.firstPolyIndexCache; + lastPolyIndexCache = tessellator.lastPolyIndexCache; + firstLineIndexCache = tessellator.firstLineIndexCache; + lastLineIndexCache = tessellator.lastLineIndexCache; + firstPointIndexCache = tessellator.firstPointIndexCache; + lastPointIndexCache = tessellator.lastPointIndexCache; + } + } + + firstPolyVertex = lastPolyVertex = -1; + firstLineVertex = lastLineVertex = -1; + firstPointVertex = lastPointVertex = -1; + + tessellated = true; + } + + + protected void tessellatePoint() { + float x = 0, y = 0, z = 0; + if (params.length == 2) { + x = params[0]; + y = params[1]; + z = 0; + } else if (params.length == 3) { + x = params[0]; + y = params[1]; + z = params[2]; + } + + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addPoint(x, y, z, fill, stroke); + tessellator.tessellatePoints(); + } + + + protected void tessellateLine() { + float x1 = 0, y1 = 0, z1 = 0; + float x2 = 0, y2 = 0, z2 = 0; + if (params.length == 4) { + x1 = params[0]; + y1 = params[1]; + x2 = params[2]; + y2 = params[3]; + } else if (params.length == 6) { + x1 = params[0]; + y1 = params[1]; + z1 = params[2]; + x2 = params[3]; + y2 = params[4]; + z2 = params[5]; + } + + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addLine(x1, y1, z1, + x2, y2, z2, + fill, stroke); + tessellator.tessellateLines(); + } + + + protected void tessellateTriangle() { + float x1 = 0, y1 = 0; + float x2 = 0, y2 = 0; + float x3 = 0, y3 = 0; + if (params.length == 6) { + x1 = params[0]; + y1 = params[1]; + x2 = params[2]; + y2 = params[3]; + x3 = params[4]; + y3 = params[5]; + } + + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addTriangle(x1, y1, 0, + x2, y2, 0, + x3, y3, 0, + fill, stroke); + tessellator.tessellateTriangles(); + } + + + protected void tessellateQuad() { + float x1 = 0, y1 = 0; + float x2 = 0, y2 = 0; + float x3 = 0, y3 = 0; + float x4 = 0, y4 = 0; + if (params.length == 8) { + x1 = params[0]; + y1 = params[1]; + x2 = params[2]; + y2 = params[3]; + x3 = params[4]; + y3 = params[5]; + x4 = params[6]; + y4 = params[7]; + } + + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addQuad(x1, y1, 0, + x2, y2, 0, + x3, y3, 0, + x4, y4, 0, + stroke); + tessellator.tessellateQuads(); + } + + + protected void tessellateRect() { + float a = 0, b = 0, c = 0, d = 0; + float tl = 0, tr = 0, br = 0, bl = 0; + boolean rounded = false; + int mode = rectMode; + + if (params.length == 4 || params.length == 5) { + a = params[0]; + b = params[1]; + c = params[2]; + d = params[3]; + rounded = false; + if (params.length == 5) { + tl = params[4]; + tr = params[4]; + br = params[4]; + bl = params[4]; + rounded = true; + } + } else if (params.length == 8) { + a = params[0]; + b = params[1]; + c = params[2]; + d = params[3]; + tl = params[4]; + tr = params[5]; + br = params[6]; + bl = params[7]; + rounded = true; + } + + float hradius, vradius; + switch (mode) { + case CORNERS: + break; + case CORNER: + c += a; d += b; + break; + case RADIUS: + hradius = c; + vradius = d; + c = a + hradius; + d = b + vradius; + a -= hradius; + b -= vradius; + break; + case CENTER: + hradius = c / 2.0f; + vradius = d / 2.0f; + c = a + hradius; + d = b + vradius; + a -= hradius; + b -= vradius; + } + + if (a > c) { + float temp = a; a = c; c = temp; + } + + if (b > d) { + float temp = b; b = d; d = temp; + } + + float maxRounding = PApplet.min((c - a) / 2, (d - b) / 2); + if (tl > maxRounding) tl = maxRounding; + if (tr > maxRounding) tr = maxRounding; + if (br > maxRounding) br = maxRounding; + if (bl > maxRounding) bl = maxRounding; + + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + if (rounded) { + saveBezierVertexSettings(); + inGeo.addRect(a, b, c, d, tl, tr, br, bl, stroke); + tessellator.tessellatePolygon(true, true, true); + restoreBezierVertexSettings(); + } else { + inGeo.addRect(a, b, c, d, stroke); + tessellator.tessellateQuads(); + } + } + + + protected void tessellateEllipse() { + float a = 0, b = 0, c = 0, d = 0; + int mode = ellipseMode; + + if (4 <= params.length) { + a = params[0]; + b = params[1]; + c = params[2]; + d = params[3]; + } + + float x = a; + float y = b; + float w = c; + float h = d; + + if (mode == CORNERS) { + w = c - a; + h = d - b; + + } else if (mode == RADIUS) { + x = a - c; + y = b - d; + w = c * 2; + h = d * 2; + + } else if (mode == DIAMETER) { + x = a - c/2f; + y = b - d/2f; + } + + if (w < 0) { // undo negative width + x += w; + w = -w; + } + + if (h < 0) { // undo negative height + y += h; + h = -h; + } + + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addEllipse(x, y, w, h, fill, stroke); + tessellator.tessellateTriangleFan(); + } + + + protected void tessellateArc() { + float a = 0, b = 0, c = 0, d = 0; + float start = 0, stop = 0; + int mode = ellipseMode; + int arcMode = 0; + + if (6 <= params.length) { + a = params[0]; + b = params[1]; + c = params[2]; + d = params[3]; + start = params[4]; + stop = params[5]; + if (params.length == 7) { + arcMode = (int)(params[6]); + } + } + + float x = a; + float y = b; + float w = c; + float h = d; + + if (mode == CORNERS) { + w = c - a; + h = d - b; + + } else if (mode == RADIUS) { + x = a - c; + y = b - d; + w = c * 2; + h = d * 2; + + } else if (mode == CENTER) { + x = a - c/2f; + y = b - d/2f; + } + + // make sure the loop will exit before starting while + if (!Float.isInfinite(start) && !Float.isInfinite(stop)) { + // ignore equal and degenerate cases + if (stop > start) { + // make sure that we're starting at a useful point + while (start < 0) { + start += TWO_PI; + stop += TWO_PI; + } + + if (stop - start > TWO_PI) { + // don't change start, it is visible in PIE mode + stop = start + TWO_PI; + } + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.setNormal(normalX, normalY, normalZ); + inGeo.addArc(x, y, w, h, start, stop, fill, stroke, arcMode); + tessellator.tessellateTriangleFan(); + } + } + } + + + protected void tessellateBox() { + float w = 0, h = 0, d = 0; + if (params.length == 1) { + w = h = d = params[0]; + } else if (params.length == 3) { + w = params[0]; + h = params[1]; + d = params[2]; + } + + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + inGeo.addBox(w, h, d, fill, stroke); + tessellator.tessellateQuads(); + } + + + protected void tessellateSphere() { + float r = 0; + int nu = sphereDetailU; + int nv = sphereDetailV; + if (1 <= params.length) { + r = params[0]; + if (params.length == 2) { + nu = nv = (int)params[1]; + } else if (params.length == 3) { + nu = (int)params[1]; + nv = (int)params[2]; + } + } + + if (nu < 3 || nv < 2) { + nu = nv = 30; + } + int savedDetailU = pg.sphereDetailU; + int savedDetailV = pg.sphereDetailV; + if (pg.sphereDetailU != nu || pg.sphereDetailV != nv) { + pg.sphereDetail(nu, nv); + } + + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + int[] indices = inGeo.addSphere(r, nu, nv, fill, stroke); + tessellator.tessellateTriangles(indices); + + if ((0 < savedDetailU && savedDetailU != nu) || + (0 < savedDetailV && savedDetailV != nv)) { + pg.sphereDetail(savedDetailU, savedDetailV); + } + } + + + protected void tessellatePath() { + if (vertices == null) return; + + inGeo.setMaterial(fillColor, strokeColor, strokeWeight, + ambientColor, specularColor, emissiveColor, shininess); + + if (vertexCodeCount == 0) { // each point is a simple vertex + if (vertices[0].length == 2) { // tessellating 2D vertices + for (int i = 0; i < vertexCount; i++) { + inGeo.addVertex(vertices[i][X], vertices[i][Y], VERTEX, false); + } + } else { // drawing 3D vertices + for (int i = 0; i < vertexCount; i++) { + inGeo.addVertex(vertices[i][X], vertices[i][Y], vertices[i][Z], + VERTEX, false); + } + } + } else { // coded set of vertices + int idx = 0; + boolean brk = true; + + if (vertices[0].length == 2) { // tessellating a 2D path + + for (int j = 0; j < vertexCodeCount; j++) { + switch (vertexCodes[j]) { + + case VERTEX: + inGeo.addVertex(vertices[idx][X], vertices[idx][Y], VERTEX, brk); + brk = false; + idx++; + break; + + case QUADRATIC_VERTEX: + inGeo.addQuadraticVertex(vertices[idx+0][X], vertices[idx+0][Y], 0, + vertices[idx+1][X], vertices[idx+1][Y], 0, + brk); + brk = false; + idx += 2; + break; + + case BEZIER_VERTEX: + inGeo.addBezierVertex(vertices[idx+0][X], vertices[idx+0][Y], 0, + vertices[idx+1][X], vertices[idx+1][Y], 0, + vertices[idx+2][X], vertices[idx+2][Y], 0, + brk); + brk = false; + idx += 3; + break; + + case CURVE_VERTEX: + inGeo.addCurveVertex(vertices[idx][X], vertices[idx][Y], 0, brk); + brk = false; + idx++; + break; + + case BREAK: + brk = true; + } + } + } else { // tessellating a 3D path + for (int j = 0; j < vertexCodeCount; j++) { + switch (vertexCodes[j]) { + + case VERTEX: + inGeo.addVertex(vertices[idx][X], vertices[idx][Y], + vertices[idx][Z], brk); + brk = false; + idx++; + break; + + case QUADRATIC_VERTEX: + inGeo.addQuadraticVertex(vertices[idx+0][X], + vertices[idx+0][Y], + vertices[idx+0][Z], + vertices[idx+1][X], + vertices[idx+1][Y], + vertices[idx+0][Z], + brk); + brk = false; + idx += 2; + break; + + case BEZIER_VERTEX: + inGeo.addBezierVertex(vertices[idx+0][X], + vertices[idx+0][Y], + vertices[idx+0][Z], + vertices[idx+1][X], + vertices[idx+1][Y], + vertices[idx+1][Z], + vertices[idx+2][X], + vertices[idx+2][Y], + vertices[idx+2][Z], + brk); + brk = false; + idx += 3; + break; + + case CURVE_VERTEX: + inGeo.addCurveVertex(vertices[idx][X], + vertices[idx][Y], + vertices[idx][Z], + brk); + brk = false; + idx++; + break; + + case BREAK: + brk = true; + } + } + } + } + + boolean bez = inGeo.hasBezierVertex(); + boolean quad = inGeo.hasQuadraticVertex(); + boolean curv = inGeo.hasCurveVertex(); + if (bez || quad) saveBezierVertexSettings(); + if (curv) { + saveCurveVertexSettings(); + tessellator.resetCurveVertexCount(); + } + tessellator.tessellatePolygon(true, close, true); + if (bez || quad) restoreBezierVertexSettings(); + if (curv) restoreCurveVertexSettings(); + } + + protected void saveBezierVertexSettings() { + savedBezierDetail = pg.bezierDetail; + if (pg.bezierDetail != bezierDetail) { + pg.bezierDetail(bezierDetail); + } + } + + protected void restoreBezierVertexSettings() { + if (savedBezierDetail != bezierDetail) { + pg.bezierDetail(savedBezierDetail); + } + } + + protected void saveCurveVertexSettings() { + savedCurveDetail = pg.curveDetail; + savedCurveTightness = pg.curveTightness; + if (pg.curveDetail != curveDetail) { + pg.curveDetail(curveDetail); + } + if (pg.curveTightness != curveTightness) { + pg.curveTightness(curveTightness); + } + } + + protected void restoreCurveVertexSettings() { + if (savedCurveDetail != curveDetail) { + pg.curveDetail(savedCurveDetail); + } + if (savedCurveTightness != curveTightness) { + pg.curveTightness(savedCurveTightness); + } + } + + /////////////////////////////////////////////////////////// + + // + + // Aggregation + + + protected void aggregate() { + if (root == this && parent == null) { + // Initializing auxiliary variables in root node + // needed for aggregation. + polyIndexOffset = 0; + polyVertexOffset = 0; + polyVertexAbs = 0; + polyVertexRel = 0; + + lineIndexOffset = 0; + lineVertexOffset = 0; + lineVertexAbs = 0; + lineVertexRel = 0; + + pointIndexOffset = 0; + pointVertexOffset = 0; + pointVertexAbs = 0; + pointVertexRel = 0; + + // Recursive aggregation. + aggregateImpl(); + } + } + + + // This method is very important, as it is responsible of generating the + // correct vertex and index offsets for each level of the shape hierarchy. + // This is the core of the recursive algorithm that calculates the indices + // for the vertices accumulated in a single VBO. + // Basically, the algorithm traverses all the shapes in the hierarchy and + // updates the index cache for each child shape holding geometry (those being + // the leaf nodes in the hierarchy tree), and creates index caches for the + // group shapes so that the draw() method can be called from any shape in the + // hierarchy and the correct piece of geometry will be rendered. + // + // For example, in the following hierarchy: + // + // ROOT GROUP + // | + // /-----------------0-----------------\ + // | | + // CHILD GROUP 0 CHILD GROUP 1 + // | | + // | /---------------0-----------------\ + // | | | | + // GEO SHAPE 0 GEO SHAPE 0 GEO SHAPE 1 GEO SHAPE 2 + // 4 vertices 5 vertices 6 vertices 3 vertices + // + // calling draw() from the root group should result in all the + // vertices (4 + 5 + 6 + 3 = 18) being rendered, while calling + // draw() from either child groups 0 or 1 should result in the first + // 4 vertices or the last 14 vertices being rendered, respectively. + protected void aggregateImpl() { + if (family == GROUP) { + // Recursively aggregating the child shapes. + hasPolys = false; + hasLines = false; + hasPoints = false; + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + child.aggregateImpl(); + hasPolys |= child.hasPolys; + hasLines |= child.hasLines; + hasPoints |= child.hasPoints; + } + } else { // LEAF SHAPE (family either GEOMETRY, PATH or PRIMITIVE) + hasPolys = -1 < firstPolyIndexCache && -1 < lastPolyIndexCache; + hasLines = -1 < firstLineIndexCache && -1 < lastLineIndexCache; + hasPoints = -1 < firstPointIndexCache && -1 < lastPointIndexCache; + } + + if (hasPolys) { + updatePolyIndexCache(); + } + if (is3D()) { + if (hasLines) updateLineIndexCache(); + if (hasPoints) updatePointIndexCache(); + } + + if (matrix != null) { + // Some geometric transformations were applied on + // this shape before tessellation, so they are applied now. + if (hasPolys) { + tessGeo.applyMatrixOnPolyGeometry(matrix, + firstPolyVertex, lastPolyVertex); + } + if (is3D()) { + if (hasLines) { + tessGeo.applyMatrixOnLineGeometry(matrix, + firstLineVertex, lastLineVertex); + } + if (hasPoints) { + tessGeo.applyMatrixOnPointGeometry(matrix, + firstPointVertex, lastPointVertex); + } + } + } + } + + + // Updates the index cache for the range that corresponds to this shape. + protected void updatePolyIndexCache() { + IndexCache cache = tessGeo.polyIndexCache; + if (family == GROUP) { + // Updates the index cache to include the elements corresponding to + // a group shape, using the cache entries of the child shapes. The + // index cache has a pyramidal structure where the base is formed + // by the entries corresponding to the leaf (geometry) shapes, and + // each subsequent level is determined by the higher-level group shapes + // The index pyramid is flattened into arrays in order to use simple + // data structures, so each shape needs to store the positions in the + // cache that corresponds to itself. + + // The index ranges of the child shapes that share the vertex offset + // are unified into a single range in the parent level. + + firstPolyIndexCache = lastPolyIndexCache = -1; + int gindex = -1; + + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + + int first = child.firstPolyIndexCache; + int count = -1 < first ? child.lastPolyIndexCache - first + 1 : -1; + for (int n = first; n < first + count; n++) { + if (gindex == -1) { + gindex = cache.addNew(n); + firstPolyIndexCache = gindex; + } else { + if (cache.vertexOffset[gindex] == cache.vertexOffset[n]) { + // When the vertex offsets are the same, this means that the + // current index range in the group shape can be extended to + // include the index range in the current child shape. + // This is a result of how the indices are updated for the + // leaf shapes. + cache.incCounts(gindex, + cache.indexCount[n], cache.vertexCount[n]); + } else { + gindex = cache.addNew(n); + } + } + } + + // Updating the first and last poly vertices for this group shape. + if (-1 < child.firstPolyVertex) { + if (firstPolyVertex == -1) { + firstPolyVertex = Integer.MAX_VALUE; + } + firstPolyVertex = PApplet.min(firstPolyVertex, child.firstPolyVertex); + } + if (-1 < child.lastPolyVertex) { + lastPolyVertex = PApplet.max(lastPolyVertex, child.lastPolyVertex); + } + } + lastPolyIndexCache = gindex; + } else { + // The index cache is updated in order to reflect the fact that all + // the vertices will be stored in a single VBO in the root shape. + // This update works as follows (the methodology is the same for + // poly, line and point): the VertexAbs variable in the root shape + // stores the index of the last vertex up to this shape (plus one) + // without taking into consideration the MAX_VERTEX_INDEX limit, so + // it effectively runs over the entire range. + // VertexRel, on the other hand, is reset every time the limit is + // exceeded, therefore creating the start of a new index group in the + // root shape. When this happens, the indices in the child shape need + // to be restarted as well to reflect the new index offset. + + firstPolyVertex = lastPolyVertex = + cache.vertexOffset[firstPolyIndexCache]; + for (int n = firstPolyIndexCache; n <= lastPolyIndexCache; n++) { + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int vcount = cache.vertexCount[n]; + + if (PGL.MAX_VERTEX_INDEX1 <= root.polyVertexRel + vcount || // Too many vertices already signal the start of a new cache... + (is2D() && startStrokedTex(n))) { // ... or, in 2D, the beginning of line or points. + root.polyVertexRel = 0; + root.polyVertexOffset = root.polyVertexAbs; + cache.indexOffset[n] = root.polyIndexOffset; + } else { + tessGeo.incPolyIndices(ioffset, ioffset + icount - 1, + root.polyVertexRel); + } + cache.vertexOffset[n] = root.polyVertexOffset; + if (is2D()) { + setFirstStrokeVertex(n, lastPolyVertex); + } + + root.polyIndexOffset += icount; + root.polyVertexAbs += vcount; + root.polyVertexRel += vcount; + lastPolyVertex += vcount; + } + lastPolyVertex--; + if (is2D()) { + setLastStrokeVertex(lastPolyVertex); + } + } + } + + + protected boolean startStrokedTex(int n) { + return image != null && (n == firstLineIndexCache || + n == firstPointIndexCache); + } + + + protected void setFirstStrokeVertex(int n, int vert) { + if (n == firstLineIndexCache && firstLineVertex == -1) { + firstLineVertex = lastLineVertex = vert; + } + if (n == firstPointIndexCache && firstPointVertex == -1) { + firstPointVertex = lastPointVertex = vert; + } + } + + protected void setLastStrokeVertex(int vert) { + if (-1 < lastLineVertex) { + lastLineVertex = vert; + } + if (-1 < lastPointVertex) { + lastPointVertex += vert; + } + } + + protected void updateLineIndexCache() { + IndexCache cache = tessGeo.lineIndexCache; + if (family == GROUP) { + firstLineIndexCache = lastLineIndexCache = -1; + int gindex = -1; + + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + + int first = child.firstLineIndexCache; + int count = -1 < first ? child.lastLineIndexCache - first + 1 : -1; + for (int n = first; n < first + count; n++) { + if (gindex == -1) { + gindex = cache.addNew(n); + firstLineIndexCache = gindex; + } else { + if (cache.vertexOffset[gindex] == cache.vertexOffset[n]) { + cache.incCounts(gindex, cache.indexCount[n], + cache.vertexCount[n]); + } else { + gindex = cache.addNew(n); + } + } + } + + // Updating the first and last line vertices for this group shape. + if (-1 < child.firstLineVertex) { + if (firstLineVertex == -1) firstLineVertex = Integer.MAX_VALUE; + firstLineVertex = PApplet.min(firstLineVertex, child.firstLineVertex); + } + if (-1 < child.lastLineVertex) { + lastLineVertex = PApplet.max(lastLineVertex, child.lastLineVertex); + } + } + lastLineIndexCache = gindex; + } else { + firstLineVertex = lastLineVertex = + cache.vertexOffset[firstLineIndexCache]; + for (int n = firstLineIndexCache; n <= lastLineIndexCache; n++) { + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int vcount = cache.vertexCount[n]; + + if (PGL.MAX_VERTEX_INDEX1 <= root.lineVertexRel + vcount) { + root.lineVertexRel = 0; + root.lineVertexOffset = root.lineVertexAbs; + cache.indexOffset[n] = root.lineIndexOffset; + } else { + tessGeo.incLineIndices(ioffset, ioffset + icount - 1, + root.lineVertexRel); + } + cache.vertexOffset[n] = root.lineVertexOffset; + + root.lineIndexOffset += icount; + root.lineVertexAbs += vcount; + root.lineVertexRel += vcount; + lastLineVertex += vcount; + } + lastLineVertex--; + } + } + + + protected void updatePointIndexCache() { + IndexCache cache = tessGeo.pointIndexCache; + if (family == GROUP) { + firstPointIndexCache = lastPointIndexCache = -1; + int gindex = -1; + + for (int i = 0; i < childCount; i++) { + PShapeOpenGL child = (PShapeOpenGL) children[i]; + + int first = child.firstPointIndexCache; + int count = -1 < first ? child.lastPointIndexCache - first + 1 : -1; + for (int n = first; n < first + count; n++) { + if (gindex == -1) { + gindex = cache.addNew(n); + firstPointIndexCache = gindex; + } else { + if (cache.vertexOffset[gindex] == cache.vertexOffset[n]) { + // When the vertex offsets are the same, this means that the + // current index range in the group shape can be extended to + // include either the index range in the current child shape. + // This is a result of how the indices are updated for the + // leaf shapes in aggregateImpl(). + cache.incCounts(gindex, cache.indexCount[n], + cache.vertexCount[n]); + } else { + gindex = cache.addNew(n); + } + } + } + + // Updating the first and last point vertices for this group shape. + if (-1 < child.firstPointVertex) { + if (firstPointVertex == -1) firstPointVertex = Integer.MAX_VALUE; + firstPointVertex = PApplet.min(firstPointVertex, + child.firstPointVertex); + } + if (-1 < child.lastPointVertex) { + lastPointVertex = PApplet.max(lastPointVertex, child.lastPointVertex); + } + } + lastPointIndexCache = gindex; + } else { + firstPointVertex = lastPointVertex = + cache.vertexOffset[firstPointIndexCache]; + for (int n = firstPointIndexCache; n <= lastPointIndexCache; n++) { + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int vcount = cache.vertexCount[n]; + + if (PGL.MAX_VERTEX_INDEX1 <= root.pointVertexRel + vcount) { + root.pointVertexRel = 0; + root.pointVertexOffset = root.pointVertexAbs; + cache.indexOffset[n] = root.pointIndexOffset; + } else { + tessGeo.incPointIndices(ioffset, ioffset + icount - 1, + root.pointVertexRel); + } + cache.vertexOffset[n] = root.pointVertexOffset; + + root.pointIndexOffset += icount; + root.pointVertexAbs += vcount; + root.pointVertexRel += vcount; + lastPointVertex += vcount; + } + lastPointVertex--; + } + } + + + /////////////////////////////////////////////////////////// + + // + + // Buffer initialization + + + protected void initBuffers() { + boolean outdated = contextIsOutdated(); + context = pgl.getCurrentContext(); + + if (hasPolys && (needBufferInit || outdated)) { + initPolyBuffers(); + } + + if (hasLines && (needBufferInit || outdated)) { + initLineBuffers(); + } + + if (hasPoints && (needBufferInit || outdated)) { + initPointBuffers(); + } + + needBufferInit = false; + } + + + protected void initPolyBuffers() { + int size = tessGeo.polyVertexCount; + int sizef = size * PGL.SIZEOF_FLOAT; + int sizei = size * PGL.SIZEOF_INT; + + tessGeo.updatePolyVerticesBuffer(); + if (bufPolyVertex == null) + bufPolyVertex = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 4, PGL.SIZEOF_FLOAT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyVertex.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 4 * sizef, + tessGeo.polyVerticesBuffer, glUsage); + + tessGeo.updatePolyColorsBuffer(); + if (bufPolyColor == null) + bufPolyColor = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_INT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyColor.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizei, + tessGeo.polyColorsBuffer, glUsage); + + tessGeo.updatePolyNormalsBuffer(); + if (bufPolyNormal == null) + bufPolyNormal = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 3, PGL.SIZEOF_FLOAT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyNormal.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 3 * sizef, + tessGeo.polyNormalsBuffer, glUsage); + + tessGeo.updatePolyTexCoordsBuffer(); + if (bufPolyTexcoord == null) + bufPolyTexcoord = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 2, PGL.SIZEOF_FLOAT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyTexcoord.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 2 * sizef, + tessGeo.polyTexCoordsBuffer, glUsage); + + tessGeo.updatePolyAmbientBuffer(); + if (bufPolyAmbient == null) + bufPolyAmbient = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_INT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyAmbient.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizei, + tessGeo.polyAmbientBuffer, glUsage); + + tessGeo.updatePolySpecularBuffer(); + if (bufPolySpecular == null) + bufPolySpecular = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_INT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolySpecular.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizei, + tessGeo.polySpecularBuffer, glUsage); + + tessGeo.updatePolyEmissiveBuffer(); + if (bufPolyEmissive == null) + bufPolyEmissive = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_INT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyEmissive.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizei, + tessGeo.polyEmissiveBuffer, glUsage); + + tessGeo.updatePolyShininessBuffer(); + if (bufPolyShininess == null) + bufPolyShininess = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_FLOAT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyShininess.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizef, + tessGeo.polyShininessBuffer, glUsage); + + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + tessGeo.updateAttribBuffer(attrib.name); + if (!attrib.bufferCreated()) attrib.createBuffer(pgl); + pgl.bindBuffer(PGL.ARRAY_BUFFER, attrib.buf.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, attrib.sizeInBytes(size), + tessGeo.polyAttribBuffers.get(name), glUsage); + } + + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + + tessGeo.updatePolyIndicesBuffer(); + if (bufPolyIndex == null) + bufPolyIndex = new VertexBuffer(pg, PGL.ELEMENT_ARRAY_BUFFER, 1, PGL.SIZEOF_INDEX, true); + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, bufPolyIndex.glId); + pgl.bufferData(PGL.ELEMENT_ARRAY_BUFFER, + tessGeo.polyIndexCount * PGL.SIZEOF_INDEX, + tessGeo.polyIndicesBuffer, glUsage); + + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, 0); + } + + + protected void initLineBuffers() { + int size = tessGeo.lineVertexCount; + int sizef = size * PGL.SIZEOF_FLOAT; + int sizei = size * PGL.SIZEOF_INT; + + tessGeo.updateLineVerticesBuffer(); + if (bufLineVertex == null) + bufLineVertex = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 4, PGL.SIZEOF_FLOAT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufLineVertex.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 4 * sizef, + tessGeo.lineVerticesBuffer, glUsage); + + tessGeo.updateLineColorsBuffer(); + if (bufLineColor == null) + bufLineColor = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_INT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufLineColor.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizei, + tessGeo.lineColorsBuffer, glUsage); + + tessGeo.updateLineDirectionsBuffer(); + if (bufLineAttrib == null) + bufLineAttrib = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 4, PGL.SIZEOF_FLOAT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufLineAttrib.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 4 * sizef, + tessGeo.lineDirectionsBuffer, glUsage); + + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + + tessGeo.updateLineIndicesBuffer(); + if (bufLineIndex == null) + bufLineIndex = new VertexBuffer(pg, PGL.ELEMENT_ARRAY_BUFFER, 1, PGL.SIZEOF_INDEX, true); + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, bufLineIndex.glId); + pgl.bufferData(PGL.ELEMENT_ARRAY_BUFFER, + tessGeo.lineIndexCount * PGL.SIZEOF_INDEX, + tessGeo.lineIndicesBuffer, glUsage); + + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, 0); + } + + + protected void initPointBuffers() { + int size = tessGeo.pointVertexCount; + int sizef = size * PGL.SIZEOF_FLOAT; + int sizei = size * PGL.SIZEOF_INT; + + tessGeo.updatePointVerticesBuffer(); + if (bufPointVertex == null) + bufPointVertex = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 4, PGL.SIZEOF_FLOAT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPointVertex.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 4 * sizef, + tessGeo.pointVerticesBuffer, glUsage); + + tessGeo.updatePointColorsBuffer(); + if (bufPointColor == null) + bufPointColor = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 1, PGL.SIZEOF_INT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPointColor.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, sizei, + tessGeo.pointColorsBuffer, glUsage); + + tessGeo.updatePointOffsetsBuffer(); + if (bufPointAttrib == null) + bufPointAttrib = new VertexBuffer(pg, PGL.ARRAY_BUFFER, 2, PGL.SIZEOF_FLOAT); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPointAttrib.glId); + pgl.bufferData(PGL.ARRAY_BUFFER, 2 * sizef, + tessGeo.pointOffsetsBuffer, glUsage); + + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + + tessGeo.updatePointIndicesBuffer(); + if (bufPointIndex == null) + bufPointIndex = new VertexBuffer(pg, PGL.ELEMENT_ARRAY_BUFFER, 1, PGL.SIZEOF_INDEX, true); + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, bufPointIndex.glId); + pgl.bufferData(PGL.ELEMENT_ARRAY_BUFFER, + tessGeo.pointIndexCount * PGL.SIZEOF_INDEX, + tessGeo.pointIndicesBuffer, glUsage); + + pgl.bindBuffer(PGL.ELEMENT_ARRAY_BUFFER, 0); + } + + + protected boolean contextIsOutdated() { + boolean outdated = !pgl.contextIsCurrent(context); + if (outdated) { + bufPolyVertex.dispose(); + bufPolyColor.dispose(); + bufPolyNormal.dispose(); + bufPolyTexcoord.dispose(); + bufPolyAmbient.dispose(); + bufPolySpecular.dispose(); + bufPolyEmissive.dispose(); + bufPolyShininess.dispose(); + for (VertexAttribute attrib: polyAttribs.values()) { + attrib.buf.dispose(); + } + bufPolyIndex.dispose(); + + bufLineVertex.dispose(); + bufLineColor.dispose(); + bufLineAttrib.dispose(); + bufLineIndex.dispose(); + + bufPointVertex.dispose(); + bufPointColor.dispose(); + bufPointAttrib.dispose(); + bufPointIndex.dispose(); + } + return outdated; + } + + + /////////////////////////////////////////////////////////// + + // + + // Geometry update + + + protected void updateGeometry() { + root.initBuffers(); + if (root.modified) { + root.updateGeometryImpl(); + } + } + + + protected void updateGeometryImpl() { + if (modifiedPolyVertices) { + int offset = firstModifiedPolyVertex; + int size = lastModifiedPolyVertex - offset + 1; + copyPolyVertices(offset, size); + modifiedPolyVertices = false; + firstModifiedPolyVertex = PConstants.MAX_INT; + lastModifiedPolyVertex = PConstants.MIN_INT; + } + if (modifiedPolyColors) { + int offset = firstModifiedPolyColor; + int size = lastModifiedPolyColor - offset + 1; + copyPolyColors(offset, size); + modifiedPolyColors = false; + firstModifiedPolyColor = PConstants.MAX_INT; + lastModifiedPolyColor = PConstants.MIN_INT; + } + if (modifiedPolyNormals) { + int offset = firstModifiedPolyNormal; + int size = lastModifiedPolyNormal - offset + 1; + copyPolyNormals(offset, size); + modifiedPolyNormals = false; + firstModifiedPolyNormal = PConstants.MAX_INT; + lastModifiedPolyNormal = PConstants.MIN_INT; + } + if (modifiedPolyTexCoords) { + int offset = firstModifiedPolyTexcoord; + int size = lastModifiedPolyTexcoord - offset + 1; + copyPolyTexCoords(offset, size); + modifiedPolyTexCoords = false; + firstModifiedPolyTexcoord = PConstants.MAX_INT; + lastModifiedPolyTexcoord = PConstants.MIN_INT; + } + if (modifiedPolyAmbient) { + int offset = firstModifiedPolyAmbient; + int size = lastModifiedPolyAmbient - offset + 1; + copyPolyAmbient(offset, size); + modifiedPolyAmbient = false; + firstModifiedPolyAmbient = PConstants.MAX_INT; + lastModifiedPolyAmbient = PConstants.MIN_INT; + } + if (modifiedPolySpecular) { + int offset = firstModifiedPolySpecular; + int size = lastModifiedPolySpecular - offset + 1; + copyPolySpecular(offset, size); + modifiedPolySpecular = false; + firstModifiedPolySpecular = PConstants.MAX_INT; + lastModifiedPolySpecular = PConstants.MIN_INT; + } + if (modifiedPolyEmissive) { + int offset = firstModifiedPolyEmissive; + int size = lastModifiedPolyEmissive - offset + 1; + copyPolyEmissive(offset, size); + modifiedPolyEmissive = false; + firstModifiedPolyEmissive = PConstants.MAX_INT; + lastModifiedPolyEmissive = PConstants.MIN_INT; + } + if (modifiedPolyShininess) { + int offset = firstModifiedPolyShininess; + int size = lastModifiedPolyShininess - offset + 1; + copyPolyShininess(offset, size); + modifiedPolyShininess = false; + firstModifiedPolyShininess = PConstants.MAX_INT; + lastModifiedPolyShininess = PConstants.MIN_INT; + } + for (String name: polyAttribs.keySet()) { + VertexAttribute attrib = polyAttribs.get(name); + if (attrib.modified) { + int offset = firstModifiedPolyVertex; + int size = lastModifiedPolyVertex - offset + 1; + copyPolyAttrib(attrib, offset, size); + attrib.modified = false; + attrib.firstModified = PConstants.MAX_INT; + attrib.lastModified = PConstants.MIN_INT; + } + } + + if (modifiedLineVertices) { + int offset = firstModifiedLineVertex; + int size = lastModifiedLineVertex - offset + 1; + copyLineVertices(offset, size); + modifiedLineVertices = false; + firstModifiedLineVertex = PConstants.MAX_INT; + lastModifiedLineVertex = PConstants.MIN_INT; + } + if (modifiedLineColors) { + int offset = firstModifiedLineColor; + int size = lastModifiedLineColor - offset + 1; + copyLineColors(offset, size); + modifiedLineColors = false; + firstModifiedLineColor = PConstants.MAX_INT; + lastModifiedLineColor = PConstants.MIN_INT; + } + if (modifiedLineAttributes) { + int offset = firstModifiedLineAttribute; + int size = lastModifiedLineAttribute - offset + 1; + copyLineAttributes(offset, size); + modifiedLineAttributes = false; + firstModifiedLineAttribute = PConstants.MAX_INT; + lastModifiedLineAttribute = PConstants.MIN_INT; + } + + if (modifiedPointVertices) { + int offset = firstModifiedPointVertex; + int size = lastModifiedPointVertex - offset + 1; + copyPointVertices(offset, size); + modifiedPointVertices = false; + firstModifiedPointVertex = PConstants.MAX_INT; + lastModifiedPointVertex = PConstants.MIN_INT; + } + if (modifiedPointColors) { + int offset = firstModifiedPointColor; + int size = lastModifiedPointColor - offset + 1; + copyPointColors(offset, size); + modifiedPointColors = false; + firstModifiedPointColor = PConstants.MAX_INT; + lastModifiedPointColor = PConstants.MIN_INT; + } + if (modifiedPointAttributes) { + int offset = firstModifiedPointAttribute; + int size = lastModifiedPointAttribute - offset + 1; + copyPointAttributes(offset, size); + modifiedPointAttributes = false; + firstModifiedPointAttribute = PConstants.MAX_INT; + lastModifiedPointAttribute = PConstants.MIN_INT; + } + + modified = false; + } + + + protected void copyPolyVertices(int offset, int size) { + tessGeo.updatePolyVerticesBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyVertex.glId); + tessGeo.polyVerticesBuffer.position(4 * offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, 4 * offset * PGL.SIZEOF_FLOAT, + 4 * size * PGL.SIZEOF_FLOAT, tessGeo.polyVerticesBuffer); + tessGeo.polyVerticesBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyPolyColors(int offset, int size) { + tessGeo.updatePolyColorsBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyColor.glId); + tessGeo.polyColorsBuffer.position(offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, offset * PGL.SIZEOF_INT, + size * PGL.SIZEOF_INT, tessGeo.polyColorsBuffer); + tessGeo.polyColorsBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyPolyNormals(int offset, int size) { + tessGeo.updatePolyNormalsBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyNormal.glId); + tessGeo.polyNormalsBuffer.position(3 * offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, 3 * offset * PGL.SIZEOF_FLOAT, + 3 * size * PGL.SIZEOF_FLOAT, tessGeo.polyNormalsBuffer); + tessGeo.polyNormalsBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyPolyTexCoords(int offset, int size) { + tessGeo.updatePolyTexCoordsBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyTexcoord.glId); + tessGeo.polyTexCoordsBuffer.position(2 * offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, 2 * offset * PGL.SIZEOF_FLOAT, + 2 * size * PGL.SIZEOF_FLOAT, tessGeo.polyTexCoordsBuffer); + tessGeo.polyTexCoordsBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyPolyAmbient(int offset, int size) { + tessGeo.updatePolyAmbientBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyAmbient.glId); + tessGeo.polyAmbientBuffer.position(offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, offset * PGL.SIZEOF_INT, + size * PGL.SIZEOF_INT, tessGeo.polyAmbientBuffer); + tessGeo.polyAmbientBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyPolySpecular(int offset, int size) { + tessGeo.updatePolySpecularBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolySpecular.glId); + tessGeo.polySpecularBuffer.position(offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, offset * PGL.SIZEOF_INT, + size * PGL.SIZEOF_INT, tessGeo.polySpecularBuffer); + tessGeo.polySpecularBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyPolyEmissive(int offset, int size) { + tessGeo.updatePolyEmissiveBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyEmissive.glId); + tessGeo.polyEmissiveBuffer.position(offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, offset * PGL.SIZEOF_INT, + size * PGL.SIZEOF_INT, tessGeo.polyEmissiveBuffer); + tessGeo.polyEmissiveBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyPolyShininess(int offset, int size) { + tessGeo.updatePolyShininessBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPolyShininess.glId); + tessGeo.polyShininessBuffer.position(offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, offset * PGL.SIZEOF_FLOAT, + size * PGL.SIZEOF_FLOAT, tessGeo.polyShininessBuffer); + tessGeo.polyShininessBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyPolyAttrib(VertexAttribute attrib, int offset, int size) { + tessGeo.updateAttribBuffer(attrib.name, offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, attrib.buf.glId); + Buffer buf = tessGeo.polyAttribBuffers.get(attrib.name); + buf.position(attrib.size * offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, attrib.sizeInBytes(offset), + attrib.sizeInBytes(size), buf); + buf.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyLineVertices(int offset, int size) { + tessGeo.updateLineVerticesBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufLineVertex.glId); + tessGeo.lineVerticesBuffer.position(4 * offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, 4 * offset * PGL.SIZEOF_FLOAT, + 4 * size * PGL.SIZEOF_FLOAT, tessGeo.lineVerticesBuffer); + tessGeo.lineVerticesBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyLineColors(int offset, int size) { + tessGeo.updateLineColorsBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufLineColor.glId); + tessGeo.lineColorsBuffer.position(offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, offset * PGL.SIZEOF_INT, + size * PGL.SIZEOF_INT, tessGeo.lineColorsBuffer); + tessGeo.lineColorsBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyLineAttributes(int offset, int size) { + tessGeo.updateLineDirectionsBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufLineAttrib.glId); + tessGeo.lineDirectionsBuffer.position(4 * offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, 4 * offset * PGL.SIZEOF_FLOAT, + 4 * size * PGL.SIZEOF_FLOAT, tessGeo.lineDirectionsBuffer); + tessGeo.lineDirectionsBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyPointVertices(int offset, int size) { + tessGeo.updatePointVerticesBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPointVertex.glId); + tessGeo.pointVerticesBuffer.position(4 * offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, 4 * offset * PGL.SIZEOF_FLOAT, + 4 * size * PGL.SIZEOF_FLOAT, tessGeo.pointVerticesBuffer); + tessGeo.pointVerticesBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyPointColors(int offset, int size) { + tessGeo.updatePointColorsBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPointColor.glId); + tessGeo.pointColorsBuffer.position(offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, offset * PGL.SIZEOF_INT, + size * PGL.SIZEOF_INT,tessGeo.pointColorsBuffer); + tessGeo.pointColorsBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void copyPointAttributes(int offset, int size) { + tessGeo.updatePointOffsetsBuffer(offset, size); + pgl.bindBuffer(PGL.ARRAY_BUFFER, bufPointAttrib.glId); + tessGeo.pointOffsetsBuffer.position(2 * offset); + pgl.bufferSubData(PGL.ARRAY_BUFFER, 2 * offset * PGL.SIZEOF_FLOAT, + 2 * size * PGL.SIZEOF_FLOAT, tessGeo.pointOffsetsBuffer); + tessGeo.pointOffsetsBuffer.rewind(); + pgl.bindBuffer(PGL.ARRAY_BUFFER, 0); + } + + + protected void setModifiedPolyVertices(int first, int last) { + if (first < firstModifiedPolyVertex) firstModifiedPolyVertex = first; + if (last > lastModifiedPolyVertex) lastModifiedPolyVertex = last; + modifiedPolyVertices = true; + modified = true; + } + + + protected void setModifiedPolyColors(int first, int last) { + if (first < firstModifiedPolyColor) firstModifiedPolyColor = first; + if (last > lastModifiedPolyColor) lastModifiedPolyColor = last; + modifiedPolyColors = true; + modified = true; + } + + + protected void setModifiedPolyNormals(int first, int last) { + if (first < firstModifiedPolyNormal) firstModifiedPolyNormal = first; + if (last > lastModifiedPolyNormal) lastModifiedPolyNormal = last; + modifiedPolyNormals = true; + modified = true; + } + + + protected void setModifiedPolyTexCoords(int first, int last) { + if (first < firstModifiedPolyTexcoord) firstModifiedPolyTexcoord = first; + if (last > lastModifiedPolyTexcoord) lastModifiedPolyTexcoord = last; + modifiedPolyTexCoords = true; + modified = true; + } + + + protected void setModifiedPolyAmbient(int first, int last) { + if (first < firstModifiedPolyAmbient) firstModifiedPolyAmbient = first; + if (last > lastModifiedPolyAmbient) lastModifiedPolyAmbient = last; + modifiedPolyAmbient = true; + modified = true; + } + + + protected void setModifiedPolySpecular(int first, int last) { + if (first < firstModifiedPolySpecular) firstModifiedPolySpecular = first; + if (last > lastModifiedPolySpecular) lastModifiedPolySpecular = last; + modifiedPolySpecular = true; + modified = true; + } + + + protected void setModifiedPolyEmissive(int first, int last) { + if (first < firstModifiedPolyEmissive) firstModifiedPolyEmissive = first; + if (last > lastModifiedPolyEmissive) lastModifiedPolyEmissive = last; + modifiedPolyEmissive = true; + modified = true; + } + + + protected void setModifiedPolyShininess(int first, int last) { + if (first < firstModifiedPolyShininess) firstModifiedPolyShininess = first; + if (last > lastModifiedPolyShininess) lastModifiedPolyShininess = last; + modifiedPolyShininess = true; + modified = true; + } + + + protected void setModifiedPolyAttrib(VertexAttribute attrib, int first, int last) { + if (first < attrib.firstModified) attrib.firstModified = first; + if (last > attrib.lastModified) attrib.lastModified = last; + attrib.modified = true; + modified = true; + } + + + protected void setModifiedLineVertices(int first, int last) { + if (first < firstModifiedLineVertex) firstModifiedLineVertex = first; + if (last > lastModifiedLineVertex) lastModifiedLineVertex = last; + modifiedLineVertices = true; + modified = true; + } + + + protected void setModifiedLineColors(int first, int last) { + if (first < firstModifiedLineColor) firstModifiedLineColor = first; + if (last > lastModifiedLineColor) lastModifiedLineColor = last; + modifiedLineColors = true; + modified = true; + } + + + protected void setModifiedLineAttributes(int first, int last) { + if (first < firstModifiedLineAttribute) firstModifiedLineAttribute = first; + if (last > lastModifiedLineAttribute) lastModifiedLineAttribute = last; + modifiedLineAttributes = true; + modified = true; + } + + + protected void setModifiedPointVertices(int first, int last) { + if (first < firstModifiedPointVertex) firstModifiedPointVertex = first; + if (last > lastModifiedPointVertex) lastModifiedPointVertex = last; + modifiedPointVertices = true; + modified = true; + } + + + protected void setModifiedPointColors(int first, int last) { + if (first < firstModifiedPointColor) firstModifiedPointColor = first; + if (last > lastModifiedPointColor) lastModifiedPointColor = last; + modifiedPointColors = true; + modified = true; + } + + + protected void setModifiedPointAttributes(int first, int last) { + if (first < firstModifiedPointAttribute) firstModifiedPointAttribute = first; + if (last > lastModifiedPointAttribute) lastModifiedPointAttribute = last; + modifiedPointAttributes = true; + modified = true; + } + + + /////////////////////////////////////////////////////////// + + // + + // Style handling + + + @Override + public void disableStyle() { + if (openShape) { + PGraphics.showWarning(INSIDE_BEGIN_END_ERROR, "disableStyle()"); + return; + } + + // Saving the current values to use if the style is re-enabled later + savedStroke = stroke; + savedStrokeColor = strokeColor; + savedStrokeWeight = strokeWeight; + savedStrokeCap = strokeCap; + savedStrokeJoin = strokeJoin; + savedFill = fill; + savedFillColor = fillColor; + savedTint = tint; + savedTintColor = tintColor; + savedAmbientColor = ambientColor; + savedSpecularColor = specularColor; + savedEmissiveColor = emissiveColor; + savedShininess = shininess; + savedTextureMode = textureMode; + + super.disableStyle(); + } + + + @Override + public void enableStyle() { + if (savedStroke) { + setStroke(true); + setStroke(savedStrokeColor); + setStrokeWeight(savedStrokeWeight); + setStrokeCap(savedStrokeCap); + setStrokeJoin(savedStrokeJoin); + } else { + setStroke(false); + } + + if (savedFill) { + setFill(true); + setFill(savedFillColor); + } else { + setFill(false); + } + + if (savedTint) { + setTint(true); + setTint(savedTintColor); + } + + setAmbient(savedAmbientColor); + setSpecular(savedSpecularColor); + setEmissive(savedEmissiveColor); + setShininess(savedShininess); + + if (image != null) { + setTextureMode(savedTextureMode); + } + + super.enableStyle(); + } + + + @Override + protected void styles(PGraphics g) { + if (g instanceof PGraphicsOpenGL) { + if (g.stroke) { + setStroke(true); + setStroke(g.strokeColor); + setStrokeWeight(g.strokeWeight); + setStrokeCap(g.strokeCap); + setStrokeJoin(g.strokeJoin); + } else { + setStroke(false); + } + + if (g.fill) { + setFill(true); + setFill(g.fillColor); + } else { + setFill(false); + } + + if (g.tint) { + setTint(true); + setTint(g.tintColor); + } + + setAmbient(g.ambientColor); + setSpecular(g.specularColor); + setEmissive(g.emissiveColor); + setShininess(g.shininess); + + if (image != null) { + setTextureMode(g.textureMode); + } + } else { + super.styles(g); + } + } + + + /////////////////////////////////////////////////////////// + + // + + // Rendering methods + + + /* + public void draw() { + draw(pg); + } + */ + + + @Override + public void draw(PGraphics g) { + if (g instanceof PGraphicsOpenGL) { + PGraphicsOpenGL gl = (PGraphicsOpenGL)g; + if (visible) { + pre(gl); + + updateTessellation(); + updateGeometry(); + + if (family == GROUP) { + if (fragmentedGroup(gl)) { + for (int i = 0; i < childCount; i++) { + ((PShapeOpenGL) children[i]).draw(gl); + } + } else { + PImage tex = null; + if (textures != null && textures.size() == 1) { + tex = (PImage)textures.toArray()[0]; + } + render(gl, tex); + } + } else { + render(gl, image); + } + post(gl); + } + } else { + if (family == GEOMETRY) { + inGeoToVertices(); + } + pre(g); + drawImpl(g); + post(g); + } + } + + + private void inGeoToVertices() { + vertexCount = 0; + vertexCodeCount = 0; + if (inGeo.codeCount == 0) { + for (int i = 0; i < inGeo.vertexCount; i++) { + int index = 3 * i; + float x = inGeo.vertices[index++]; + float y = inGeo.vertices[index ]; + super.vertex(x, y); + } + } else { + int v; + float x, y; + float cx, cy; + float x2, y2, x3, y3, x4, y4; + int idx = 0; + boolean insideContour = false; + + for (int j = 0; j < inGeo.codeCount; j++) { + switch (inGeo.codes[j]) { + + case VERTEX: + v = 3 * idx; + x = inGeo.vertices[v++]; + y = inGeo.vertices[v ]; + super.vertex(x, y); + + idx++; + break; + + case QUADRATIC_VERTEX: + v = 3 * idx; + cx = inGeo.vertices[v++]; + cy = inGeo.vertices[v]; + + v = 3 * (idx + 1); + x3 = inGeo.vertices[v++]; + y3 = inGeo.vertices[v]; + + super.quadraticVertex(cx, cy, x3, y3); + + idx += 2; + break; + + case BEZIER_VERTEX: + v = 3 * idx; + x2 = inGeo.vertices[v++]; + y2 = inGeo.vertices[v ]; + + v = 3 * (idx + 1); + x3 = inGeo.vertices[v++]; + y3 = inGeo.vertices[v ]; + + v = 3 * (idx + 2); + x4 = inGeo.vertices[v++]; + y4 = inGeo.vertices[v ]; + + super.bezierVertex(x2, y2, x3, y3, x4, y4); + + idx += 3; + break; + + case CURVE_VERTEX: + v = 3 * idx; + x = inGeo.vertices[v++]; + y = inGeo.vertices[v ]; + + super.curveVertex(x, y); + + idx++; + break; + + case BREAK: + if (insideContour) { + super.endContourImpl(); + } + super.beginContourImpl(); + insideContour = true; + } + } + if (insideContour) { + super.endContourImpl(); + } + } + } + + + // Returns true if some child shapes below this one either + // use different texture maps (or only one texture is used by some while + // others are untextured), or have stroked textures, + // so they cannot rendered in a single call. + // Or accurate 2D mode is enabled, which forces each + // shape to be rendered separately. + protected boolean fragmentedGroup(PGraphicsOpenGL g) { + return g.getHint(DISABLE_OPTIMIZED_STROKE) || + (textures != null && (1 < textures.size() || untexChild)) || + strokedTexture; + } + + + @Override + protected void pre(PGraphics g) { + if (g instanceof PGraphicsOpenGL) { + if (!style) { + styles(g); + } + } else { + super.pre(g); + } + } + + + @Override + protected void post(PGraphics g) { + if (g instanceof PGraphicsOpenGL) { + } else { + super.post(g); + } + } + + + @Override + protected void drawGeometry(PGraphics g) { + vertexCount = inGeo.vertexCount; + vertices = inGeo.getVertexData(); + + super.drawGeometry(g); + + vertexCount = 0; + vertices = null; + } + + + // Render the geometry stored in the root shape as VBOs, for the vertices + // corresponding to this shape. Sometimes we can have root == this. + protected void render(PGraphicsOpenGL g, PImage texture) { + if (root == null) { + // Some error. Root should never be null. At least it should be 'this'. + throw new RuntimeException("Error rendering PShapeOpenGL, root shape is " + + "null"); + } + + if (hasPolys) { + renderPolys(g, texture); + if (g.haveRaw()) { + rawPolys(g, texture); + } + } + + if (is3D()) { + // In 3D mode, the lines and points need to be rendered separately + // as they require their own shaders. + if (hasLines) { + renderLines(g); + if (g.haveRaw()) { + rawLines(g); + } + } + + if (hasPoints) { + renderPoints(g); + if (g.haveRaw()) { + rawPoints(g); + } + } + } + } + + + protected void renderPolys(PGraphicsOpenGL g, PImage textureImage) { + boolean customShader = g.polyShader != null; + boolean needNormals = customShader ? g.polyShader.accessNormals() : false; + boolean needTexCoords = customShader ? g.polyShader.accessTexCoords() : false; + + Texture tex = textureImage != null ? g.getTexture(textureImage) : null; + + boolean renderingFill = false, renderingStroke = false; + PShader shader = null; + IndexCache cache = tessGeo.polyIndexCache; + for (int n = firstPolyIndexCache; n <= lastPolyIndexCache; n++) { + if (is3D() || (tex != null && (firstLineIndexCache == -1 || + n < firstLineIndexCache) && + (firstPointIndexCache == -1 || + n < firstPointIndexCache))) { + // Rendering fill triangles, which can be lit and textured. + if (!renderingFill) { + shader = g.getPolyShader(g.lights, tex != null); + shader.bind(); + renderingFill = true; + } + } else { + // Rendering line or point triangles, which are never lit nor textured. + if (!renderingStroke) { + if (tex != null) { + tex.unbind(); + tex = null; + } + + if (shader != null && shader.bound()) { + shader.unbind(); + } + + // If the renderer is 2D, then g.lights should always be false, + // so no need to worry about that. + shader = g.getPolyShader(g.lights, false); + shader.bind(); + + renderingFill = false; + renderingStroke = true; + } + } + + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int voffset = cache.vertexOffset[n]; + + shader.setVertexAttribute(root.bufPolyVertex.glId, 4, PGL.FLOAT, + 0, 4 * voffset * PGL.SIZEOF_FLOAT); + shader.setColorAttribute(root.bufPolyColor.glId, 4, PGL.UNSIGNED_BYTE, + 0, 4 * voffset * PGL.SIZEOF_BYTE); + + if (g.lights) { + shader.setNormalAttribute(root.bufPolyNormal.glId, 3, PGL.FLOAT, + 0, 3 * voffset * PGL.SIZEOF_FLOAT); + shader.setAmbientAttribute(root.bufPolyAmbient.glId, 4, PGL.UNSIGNED_BYTE, + 0, 4 * voffset * PGL.SIZEOF_BYTE); + shader.setSpecularAttribute(root.bufPolySpecular.glId, 4, PGL.UNSIGNED_BYTE, + 0, 4 * voffset * PGL.SIZEOF_BYTE); + shader.setEmissiveAttribute(root.bufPolyEmissive.glId, 4, PGL.UNSIGNED_BYTE, + 0, 4 * voffset * PGL.SIZEOF_BYTE); + shader.setShininessAttribute(root.bufPolyShininess.glId, 1, PGL.FLOAT, + 0, voffset * PGL.SIZEOF_FLOAT); + } + if (g.lights || needNormals) { + shader.setNormalAttribute(root.bufPolyNormal.glId, 3, PGL.FLOAT, + 0, 3 * voffset * PGL.SIZEOF_FLOAT); + } + + if (tex != null || needTexCoords) { + shader.setTexcoordAttribute(root.bufPolyTexcoord.glId, 2, PGL.FLOAT, + 0, 2 * voffset * PGL.SIZEOF_FLOAT); + shader.setTexture(tex); + } + + for (VertexAttribute attrib: polyAttribs.values()) { + if (!attrib.active(shader)) continue; + attrib.bind(pgl); + shader.setAttributeVBO(attrib.glLoc, attrib.buf.glId, + attrib.tessSize, attrib.type, + attrib.isColor(), 0, attrib.sizeInBytes(voffset)); + } + + shader.draw(root.bufPolyIndex.glId, icount, ioffset); + } + + for (VertexAttribute attrib: polyAttribs.values()) { + if (attrib.active(shader)) attrib.unbind(pgl); + } + if (shader != null && shader.bound()) { + shader.unbind(); + } + } + + + protected void rawPolys(PGraphicsOpenGL g, PImage textureImage) { + PGraphics raw = g.getRaw(); + + raw.colorMode(RGB); + raw.noStroke(); + raw.beginShape(TRIANGLES); + + float[] vertices = tessGeo.polyVertices; + int[] color = tessGeo.polyColors; + float[] uv = tessGeo.polyTexCoords; + short[] indices = tessGeo.polyIndices; + + IndexCache cache = tessGeo.polyIndexCache; + for (int n = firstPolyIndexCache; n <= lastPolyIndexCache; n++) { + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int voffset = cache.vertexOffset[n]; + + for (int tr = ioffset / 3; tr < (ioffset + icount) / 3; tr++) { + int i0 = voffset + indices[3 * tr + 0]; + int i1 = voffset + indices[3 * tr + 1]; + int i2 = voffset + indices[3 * tr + 2]; + + float[] src0 = {0, 0, 0, 0}; + float[] src1 = {0, 0, 0, 0}; + float[] src2 = {0, 0, 0, 0}; + float[] pt0 = {0, 0, 0, 0}; + float[] pt1 = {0, 0, 0, 0}; + float[] pt2 = {0, 0, 0, 0}; + int argb0 = PGL.nativeToJavaARGB(color[i0]); + int argb1 = PGL.nativeToJavaARGB(color[i1]); + int argb2 = PGL.nativeToJavaARGB(color[i2]); + + PApplet.arrayCopy(vertices, 4 * i0, src0, 0, 4); + PApplet.arrayCopy(vertices, 4 * i1, src1, 0, 4); + PApplet.arrayCopy(vertices, 4 * i2, src2, 0, 4); + // Applying any transformation is currently stored in the + // modelview matrix of the renderer. + g.modelview.mult(src0, pt0); + g.modelview.mult(src1, pt1); + g.modelview.mult(src2, pt2); + + if (textureImage != null) { + raw.texture(textureImage); + if (raw.is3D()) { + raw.fill(argb0); + raw.vertex(pt0[X], pt0[Y], pt0[Z], uv[2 * i0 + 0], uv[2 * i0 + 1]); + raw.fill(argb1); + raw.vertex(pt1[X], pt1[Y], pt1[Z], uv[2 * i1 + 0], uv[2 * i1 + 1]); + raw.fill(argb2); + raw.vertex(pt2[X], pt2[Y], pt2[Z], uv[2 * i2 + 0], uv[2 * i2 + 1]); + } else if (raw.is2D()) { + float sx0 = g.screenXImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sy0 = g.screenYImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sx1 = g.screenXImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sy1 = g.screenYImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sx2 = g.screenXImpl(pt2[0], pt2[1], pt2[2], pt2[3]); + float sy2 = g.screenYImpl(pt2[0], pt2[1], pt2[2], pt2[3]); + raw.fill(argb0); + raw.vertex(sx0, sy0, uv[2 * i0 + 0], uv[2 * i0 + 1]); + raw.fill(argb1); + raw.vertex(sx1, sy1, uv[2 * i1 + 0], uv[2 * i1 + 1]); + raw.fill(argb1); + raw.vertex(sx2, sy2, uv[2 * i2 + 0], uv[2 * i2 + 1]); + } + } else { + if (raw.is3D()) { + raw.fill(argb0); + raw.vertex(pt0[X], pt0[Y], pt0[Z]); + raw.fill(argb1); + raw.vertex(pt1[X], pt1[Y], pt1[Z]); + raw.fill(argb2); + raw.vertex(pt2[X], pt2[Y], pt2[Z]); + } else if (raw.is2D()) { + float sx0 = g.screenXImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sy0 = g.screenYImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sx1 = g.screenXImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sy1 = g.screenYImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sx2 = g.screenXImpl(pt2[0], pt2[1], pt2[2], pt2[3]); + float sy2 = g.screenYImpl(pt2[0], pt2[1], pt2[2], pt2[3]); + raw.fill(argb0); + raw.vertex(sx0, sy0); + raw.fill(argb1); + raw.vertex(sx1, sy1); + raw.fill(argb2); + raw.vertex(sx2, sy2); + } + } + } + } + + raw.endShape(); + } + + + protected void renderLines(PGraphicsOpenGL g) { + PShader shader = g.getLineShader(); + shader.bind(); + + IndexCache cache = tessGeo.lineIndexCache; + for (int n = firstLineIndexCache; n <= lastLineIndexCache; n++) { + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int voffset = cache.vertexOffset[n]; + + shader.setVertexAttribute(root.bufLineVertex.glId, 4, PGL.FLOAT, + 0, 4 * voffset * PGL.SIZEOF_FLOAT); + shader.setColorAttribute(root.bufLineColor.glId, 4, PGL.UNSIGNED_BYTE, + 0, 4 * voffset * PGL.SIZEOF_BYTE); + shader.setLineAttribute(root.bufLineAttrib.glId, 4, PGL.FLOAT, + 0, 4 * voffset * PGL.SIZEOF_FLOAT); + + shader.draw(root.bufLineIndex.glId, icount, ioffset); + } + + shader.unbind(); + } + + + protected void rawLines(PGraphicsOpenGL g) { + PGraphics raw = g.getRaw(); + + raw.colorMode(RGB); + raw.noFill(); + raw.strokeCap(strokeCap); + raw.strokeJoin(strokeJoin); + raw.beginShape(LINES); + + float[] vertices = tessGeo.lineVertices; + int[] color = tessGeo.lineColors; + float[] attribs = tessGeo.lineDirections; + short[] indices = tessGeo.lineIndices; + + IndexCache cache = tessGeo.lineIndexCache; + for (int n = firstLineIndexCache; n <= lastLineIndexCache; n++) { + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int voffset = cache.vertexOffset[n]; + + for (int ln = ioffset / 6; ln < (ioffset + icount) / 6; ln++) { + // Each line segment is defined by six indices since its + // formed by two triangles. We only need the first and last + // vertices. + // This bunch of vertices could also be the bevel triangles, + // with we detect this situation by looking at the line weight. + int i0 = voffset + indices[6 * ln + 0]; + int i1 = voffset + indices[6 * ln + 5]; + float sw0 = 2 * attribs[4 * i0 + 3]; + float sw1 = 2 * attribs[4 * i1 + 3]; + + if (PGraphicsOpenGL.zero(sw0)) continue; // Bevel triangles, skip. + + float[] src0 = {0, 0, 0, 0}; + float[] src1 = {0, 0, 0, 0}; + float[] pt0 = {0, 0, 0, 0}; + float[] pt1 = {0, 0, 0, 0}; + int argb0 = PGL.nativeToJavaARGB(color[i0]); + int argb1 = PGL.nativeToJavaARGB(color[i1]); + + PApplet.arrayCopy(vertices, 4 * i0, src0, 0, 4); + PApplet.arrayCopy(vertices, 4 * i1, src1, 0, 4); + // Applying any transformation is currently stored in the + // modelview matrix of the renderer. + g.modelview.mult(src0, pt0); + g.modelview.mult(src1, pt1); + + if (raw.is3D()) { + raw.strokeWeight(sw0); + raw.stroke(argb0); + raw.vertex(pt0[X], pt0[Y], pt0[Z]); + raw.strokeWeight(sw1); + raw.stroke(argb1); + raw.vertex(pt1[X], pt1[Y], pt1[Z]); + } else if (raw.is2D()) { + float sx0 = g.screenXImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sy0 = g.screenYImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sx1 = g.screenXImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + float sy1 = g.screenYImpl(pt1[0], pt1[1], pt1[2], pt1[3]); + raw.strokeWeight(sw0); + raw.stroke(argb0); + raw.vertex(sx0, sy0); + raw.strokeWeight(sw1); + raw.stroke(argb1); + raw.vertex(sx1, sy1); + } + } + } + + raw.endShape(); + } + + + protected void renderPoints(PGraphicsOpenGL g) { + PShader shader = g.getPointShader(); + shader.bind(); + + IndexCache cache = tessGeo.pointIndexCache; + for (int n = firstPointIndexCache; n <= lastPointIndexCache; n++) { + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int voffset = cache.vertexOffset[n]; + + shader.setVertexAttribute(root.bufPointVertex.glId, 4, PGL.FLOAT, + 0, 4 * voffset * PGL.SIZEOF_FLOAT); + shader.setColorAttribute(root.bufPointColor.glId, 4, PGL.UNSIGNED_BYTE, + 0, 4 * voffset * PGL.SIZEOF_BYTE); + shader.setPointAttribute(root.bufPointAttrib.glId, 2, PGL.FLOAT, + 0, 2 * voffset * PGL.SIZEOF_FLOAT); + + shader.draw(root.bufPointIndex.glId, icount, ioffset); + } + + shader.unbind(); + } + + + protected void rawPoints(PGraphicsOpenGL g) { + PGraphics raw = g.getRaw(); + + raw.colorMode(RGB); + raw.noFill(); + raw.strokeCap(strokeCap); + raw.beginShape(POINTS); + + float[] vertices = tessGeo.pointVertices; + int[] color = tessGeo.pointColors; + float[] attribs = tessGeo.pointOffsets; + short[] indices = tessGeo.pointIndices; + + IndexCache cache = tessGeo.pointIndexCache; + for (int n = 0; n < cache.size; n++) { + int ioffset = cache.indexOffset[n]; + int icount = cache.indexCount[n]; + int voffset = cache.vertexOffset[n]; + + int pt = ioffset; + while (pt < (ioffset + icount) / 3) { + float size = attribs[2 * pt + 2]; + float weight; + int perim; + if (0 < size) { // round point + weight = +size / 0.5f; + perim = PApplet.min(PGraphicsOpenGL.MAX_POINT_ACCURACY, + PApplet.max(PGraphicsOpenGL.MIN_POINT_ACCURACY, + (int) (TWO_PI * weight / + PGraphicsOpenGL.POINT_ACCURACY_FACTOR))) + 1; + } else { // Square point + weight = -size / 0.5f; + perim = 5; + } + + int i0 = voffset + indices[3 * pt]; + int argb0 = PGL.nativeToJavaARGB(color[i0]); + float[] pt0 = {0, 0, 0, 0}; + + float[] src0 = {0, 0, 0, 0}; + PApplet.arrayCopy(vertices, 4 * i0, src0, 0, 4); + g.modelview.mult(src0, pt0); + + if (raw.is3D()) { + raw.strokeWeight(weight); + raw.stroke(argb0); + raw.vertex(pt0[X], pt0[Y], pt0[Z]); + } else if (raw.is2D()) { + float sx0 = g.screenXImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + float sy0 = g.screenYImpl(pt0[0], pt0[1], pt0[2], pt0[3]); + raw.strokeWeight(weight); + raw.stroke(argb0); + raw.vertex(sx0, sy0); + } + + pt += perim; + } + } + + raw.endShape(); + } +} diff --git a/src/main/java/processing/opengl/PSurfaceJOGL.java b/src/main/java/processing/opengl/PSurfaceJOGL.java new file mode 100644 index 0000000..17d2d91 --- /dev/null +++ b/src/main/java/processing/opengl/PSurfaceJOGL.java @@ -0,0 +1,1315 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.opengl; + +import java.awt.Component; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +import javax.swing.ImageIcon; + +import com.jogamp.common.util.IOUtil; +import com.jogamp.common.util.IOUtil.ClassResources; +import com.jogamp.nativewindow.NativeSurface; +import com.jogamp.nativewindow.ScalableSurface; +import com.jogamp.nativewindow.util.Dimension; +import com.jogamp.nativewindow.util.PixelFormat; +import com.jogamp.nativewindow.util.PixelRectangle; +import com.jogamp.opengl.GLAnimatorControl; +import com.jogamp.opengl.GLAutoDrawable; +import com.jogamp.opengl.GLCapabilities; +import com.jogamp.opengl.GLEventListener; +import com.jogamp.opengl.GLException; +import com.jogamp.opengl.GLProfile; +import com.jogamp.nativewindow.MutableGraphicsConfiguration; +import com.jogamp.nativewindow.WindowClosingProtocol; +import com.jogamp.newt.Display; +import com.jogamp.newt.Display.PointerIcon; +import com.jogamp.newt.NewtFactory; +import com.jogamp.newt.Screen; +import com.jogamp.newt.awt.NewtCanvasAWT; +import com.jogamp.newt.event.InputEvent; +import com.jogamp.newt.opengl.GLWindow; +import com.jogamp.opengl.util.FPSAnimator; + + +import processing.core.PApplet; +import processing.core.PConstants; +import processing.core.PGraphics; +import processing.core.PImage; +import processing.core.PSurface; +import processing.event.KeyEvent; +import processing.event.MouseEvent; + + +public class PSurfaceJOGL implements PSurface { + /** Selected GL profile */ + public static GLProfile profile; + + public PJOGL pgl; + + protected GLWindow window; + protected FPSAnimator animator; + protected Rectangle screenRect; + + private Thread drawExceptionHandler; + + protected PApplet sketch; + protected PGraphics graphics; + + protected int sketchWidth0; + protected int sketchHeight0; + protected int sketchWidth; + protected int sketchHeight; + + protected Display display; + protected Screen screen; + protected Rectangle displayRect; + protected Throwable drawException; + private final Object drawExceptionMutex = new Object(); + + protected NewtCanvasAWT canvas; + + protected int windowScaleFactor; + + protected float[] currentPixelScale = {0, 0}; + + protected boolean external = false; + + public PSurfaceJOGL(PGraphics graphics) { + this.graphics = graphics; + this.pgl = (PJOGL) ((PGraphicsOpenGL)graphics).pgl; + } + + + public void initOffscreen(PApplet sketch) { + this.sketch = sketch; + + sketchWidth = sketch.sketchWidth(); + sketchHeight = sketch.sketchHeight(); + + if (window != null) { + canvas = new NewtCanvasAWT(window); + canvas.setBounds(0, 0, window.getWidth(), window.getHeight()); + canvas.setFocusable(true); + } + } + + + public void initFrame(PApplet sketch) { + this.sketch = sketch; + initIcons(); + initDisplay(); + initGL(); + initWindow(); + initListeners(); + initAnimator(); + } + + + public Object getNative() { + return window; + } + + + protected void initDisplay() { + display = NewtFactory.createDisplay(null); + display.addReference(); + screen = NewtFactory.createScreen(display, 0); + screen.addReference(); + + GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice[] awtDevices = environment.getScreenDevices(); + + GraphicsDevice awtDisplayDevice = null; + int displayNum = sketch.sketchDisplay(); + if (displayNum > 0) { // if -1, use the default device + if (displayNum <= awtDevices.length) { + awtDisplayDevice = awtDevices[displayNum-1]; + } else { + System.err.format("Display %d does not exist, " + + "using the default display instead.%n", displayNum); + for (int i = 0; i < awtDevices.length; i++) { + System.err.format("Display %d is %s%n", i+1, awtDevices[i]); + } + } + } else if (0 < awtDevices.length) { + awtDisplayDevice = awtDevices[0]; + } + + if (awtDisplayDevice == null) { + awtDisplayDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); + } + + displayRect = awtDisplayDevice.getDefaultConfiguration().getBounds(); + } + + + protected void initGL() { +// System.out.println("*******************************"); + if (profile == null) { + if (PJOGL.profile == 1) { + try { + profile = GLProfile.getGL2ES1(); + } catch (GLException ex) { + profile = GLProfile.getMaxFixedFunc(true); + } + } else if (PJOGL.profile == 2) { + try { + profile = GLProfile.getGL2ES2(); + + // workaround for https://jogamp.org/bugzilla/show_bug.cgi?id=1347 + if (!profile.isHardwareRasterizer()) { + GLProfile hardware = GLProfile.getMaxProgrammable(true); + if (hardware.isGL2ES2()) { + profile = hardware; + } + } + + } catch (GLException ex) { + profile = GLProfile.getMaxProgrammable(true); + } + } else if (PJOGL.profile == 3) { + try { + profile = GLProfile.getGL2GL3(); + } catch (GLException ex) { + profile = GLProfile.getMaxProgrammable(true); + } + if (!profile.isGL3()) { + PGraphics.showWarning("Requested profile GL3 but is not available, got: " + profile); + } + } else if (PJOGL.profile == 4) { + try { + profile = GLProfile.getGL4ES3(); + } catch (GLException ex) { + profile = GLProfile.getMaxProgrammable(true); + } + if (!profile.isGL4()) { + PGraphics.showWarning("Requested profile GL4 but is not available, got: " + profile); + } + } else throw new RuntimeException(PGL.UNSUPPORTED_GLPROF_ERROR); + } + + // Setting up the desired capabilities; + GLCapabilities caps = new GLCapabilities(profile); + caps.setAlphaBits(PGL.REQUESTED_ALPHA_BITS); + caps.setDepthBits(PGL.REQUESTED_DEPTH_BITS); + caps.setStencilBits(PGL.REQUESTED_STENCIL_BITS); + +// caps.setPBuffer(false); +// caps.setFBO(false); + +// pgl.reqNumSamples = PGL.smoothToSamples(graphics.smooth); + caps.setSampleBuffers(true); + caps.setNumSamples(PGL.smoothToSamples(graphics.smooth)); + caps.setBackgroundOpaque(true); + caps.setOnscreen(true); + pgl.setCaps(caps); + } + + + protected void initWindow() { + window = GLWindow.create(screen, pgl.getCaps()); + + // Make sure that we pass the window close through to exit(), otherwise + // we're likely to have OpenGL try to shut down halfway through rendering + // a frame. Particularly problematic for complex/slow apps. + // https://github.com/processing/processing/issues/4690 + window.setDefaultCloseOperation(WindowClosingProtocol.WindowClosingMode.DO_NOTHING_ON_CLOSE); + +// if (displayDevice == null) { +// +// +// } else { +// window = GLWindow.create(displayDevice.getScreen(), pgl.getCaps()); +// } + + windowScaleFactor = PApplet.platform == PConstants.MACOSX ? + 1 : sketch.pixelDensity; + + boolean spanDisplays = sketch.sketchDisplay() == PConstants.SPAN; + screenRect = spanDisplays ? + new Rectangle(screen.getX(), screen.getY(), screen.getWidth(), screen.getHeight()) : + new Rectangle((int) displayRect.getX(), (int) displayRect.getY(), + (int) displayRect.getWidth(), + (int) displayRect.getHeight()); + + // Set the displayWidth/Height variables inside PApplet, so that they're + // usable and can even be returned by the sketchWidth()/Height() methods. + sketch.displayWidth = screenRect.width; + sketch.displayHeight = screenRect.height; + + sketchWidth0 = sketch.sketchWidth(); + sketchHeight0 = sketch.sketchHeight(); + + /* + // Trying to fix + // https://github.com/processing/processing/issues/3401 + if (sketch.displayWidth < sketch.width || + sketch.displayHeight < sketch.height) { + int w = sketch.width; + int h = sketch.height; + if (sketch.displayWidth < w) { + w = sketch.displayWidth; + } + if (sketch.displayHeight < h) { + h = sketch.displayHeight; + } +// sketch.setSize(w, h - 22 - 22); +// graphics.setSize(w, h - 22 - 22); + System.err.println("setting width/height to " + w + " " + h); + } + */ + + sketchWidth = sketch.sketchWidth(); + sketchHeight = sketch.sketchHeight(); +// System.out.println("init: " + sketchWidth + " " + sketchHeight); + + boolean fullScreen = sketch.sketchFullScreen(); + // Removing the section below because sometimes people want to do the + // full screen size in a window, and it also breaks insideSettings(). + // With 3.x, fullScreen() is so easy, that it's just better that way. + // https://github.com/processing/processing/issues/3545 + /* + // Sketch has already requested to be the same as the screen's + // width and height, so let's roll with full screen mode. + if (screenRect.width == sketchWidth && + screenRect.height == sketchHeight) { + fullScreen = true; + sketch.fullScreen(); + } + */ + + if (fullScreen || spanDisplays) { + sketchWidth = screenRect.width / windowScaleFactor; + sketchHeight = screenRect.height / windowScaleFactor; + } + + sketch.setSize(sketchWidth, sketchHeight); + + float[] reqSurfacePixelScale; + if (graphics.is2X() && PApplet.platform == PConstants.MACOSX) { + // Retina + reqSurfacePixelScale = new float[] { ScalableSurface.AUTOMAX_PIXELSCALE, + ScalableSurface.AUTOMAX_PIXELSCALE }; + } else { + // Non-retina + reqSurfacePixelScale = new float[] { ScalableSurface.IDENTITY_PIXELSCALE, + ScalableSurface.IDENTITY_PIXELSCALE }; + } + window.setSurfaceScale(reqSurfacePixelScale); + window.setSize(sketchWidth * windowScaleFactor, sketchHeight * windowScaleFactor); + window.setResizable(false); + setSize(sketchWidth, sketchHeight); + if (fullScreen) { + PApplet.hideMenuBar(); + if (spanDisplays) { + window.setFullscreen(screen.getMonitorDevices()); + } else { + window.setUndecorated(true); + window.setTopLevelPosition((int) displayRect.getX(), (int) displayRect.getY()); + window.setTopLevelSize((int) displayRect.getWidth(), (int) displayRect.getHeight()); + } + } + } + + + protected void initListeners() { + NEWTMouseListener mouseListener = new NEWTMouseListener(); + window.addMouseListener(mouseListener); + NEWTKeyListener keyListener = new NEWTKeyListener(); + window.addKeyListener(keyListener); + NEWTWindowListener winListener = new NEWTWindowListener(); + window.addWindowListener(winListener); + + DrawListener drawlistener = new DrawListener(); + window.addGLEventListener(drawlistener); + } + + + protected void initAnimator() { + if (PApplet.platform == PConstants.WINDOWS) { + // Force Windows to keep timer resolution high by + // sleeping for time which is not a multiple of 10 ms. + // See section "Clocks and Timers on Windows": + // https://blogs.oracle.com/dholmes/entry/inside_the_hotspot_vm_clocks + Thread highResTimerThread = new Thread(() -> { + try { + Thread.sleep(Long.MAX_VALUE); + } catch (InterruptedException ignore) { } + }, "HighResTimerThread"); + highResTimerThread.setDaemon(true); + highResTimerThread.start(); + } + + animator = new FPSAnimator(window, 60); + drawException = null; + animator.setUncaughtExceptionHandler(new GLAnimatorControl.UncaughtExceptionHandler() { + @Override + public void uncaughtException(final GLAnimatorControl animator, + final GLAutoDrawable drawable, + final Throwable cause) { + synchronized (drawExceptionMutex) { + drawException = cause; + drawExceptionMutex.notify(); + } + } + }); + + drawExceptionHandler = new Thread(new Runnable() { + public void run() { + synchronized (drawExceptionMutex) { + try { + while (drawException == null) { + drawExceptionMutex.wait(); + } + // System.err.println("Caught exception: " + drawException.getMessage()); + if (drawException != null) { + Throwable cause = drawException.getCause(); + if (cause instanceof ThreadDeath) { + // System.out.println("caught ThreadDeath"); + // throw (ThreadDeath)cause; + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof UnsatisfiedLinkError) { + throw new UnsatisfiedLinkError(cause.getMessage()); + } else if (cause == null) { + throw new RuntimeException(drawException.getMessage()); + } else { + throw new RuntimeException(cause); + } + } + } catch (InterruptedException e) { + return; + } + } + } + }); + drawExceptionHandler.start(); + } + + + @Override + public void setTitle(final String title) { + display.getEDTUtil().invoke(false, new Runnable() { + @Override + public void run() { + window.setTitle(title); + } + }); + } + + + @Override + public void setVisible(final boolean visible) { + display.getEDTUtil().invoke(false, new Runnable() { + @Override + public void run() { + window.setVisible(visible); + } + }); + } + + + @Override + public void setResizable(final boolean resizable) { + display.getEDTUtil().invoke(false, new Runnable() { + @Override + public void run() { + window.setResizable(resizable); + } + }); + } + + + public void setIcon(PImage icon) { + PGraphics.showWarning("Window icons for OpenGL sketches can only be set in settings()\n" + + "using PJOGL.setIcon(filename)."); + } + + + @Override + public void setAlwaysOnTop(final boolean always) { + display.getEDTUtil().invoke(false, new Runnable() { + @Override + public void run() { + window.setAlwaysOnTop(always); + } + }); + } + + + protected void initIcons() { + IOUtil.ClassResources res = null; + if (PJOGL.icons == null || PJOGL.icons.length == 0) { + // Default Processing icons + final int[] sizes = { 16, 32, 48, 64, 128, 256, 512 }; + String[] iconImages = new String[sizes.length]; + for (int i = 0; i < sizes.length; i++) { + iconImages[i] = "/icon/icon-" + sizes[i] + ".png"; + } + res = new ClassResources(iconImages, + PApplet.class.getClassLoader(), + PApplet.class); + } else { + // Loading custom icons from user-provided files. + String[] iconImages = new String[PJOGL.icons.length]; + for (int i = 0; i < PJOGL.icons.length; i++) { + iconImages[i] = resourceFilename(PJOGL.icons[i]); + } + + res = new ClassResources(iconImages, + sketch.getClass().getClassLoader(), + sketch.getClass()); + } + NewtFactory.setWindowIcons(res); + } + + + @SuppressWarnings("resource") + private String resourceFilename(String filename) { + // The code below comes from PApplet.createInputRaw() with a few adaptations + InputStream stream = null; + try { + // First see if it's in a data folder. This may fail by throwing + // a SecurityException. If so, this whole block will be skipped. + File file = new File(sketch.dataPath(filename)); + if (!file.exists()) { + // next see if it's just in the sketch folder + file = sketch.sketchFile(filename); + } + + if (file.exists() && !file.isDirectory()) { + try { + // handle case sensitivity check + String filePath = file.getCanonicalPath(); + String filenameActual = new File(filePath).getName(); + // make sure there isn't a subfolder prepended to the name + String filenameShort = new File(filename).getName(); + // if the actual filename is the same, but capitalized + // differently, warn the user. + //if (filenameActual.equalsIgnoreCase(filenameShort) && + //!filenameActual.equals(filenameShort)) { + if (!filenameActual.equals(filenameShort)) { + throw new RuntimeException("This file is named " + + filenameActual + " not " + + filename + ". Rename the file " + + "or change your code."); + } + } catch (IOException e) { } + } + + stream = new FileInputStream(file); + if (stream != null) { + stream.close(); + return file.getCanonicalPath(); + } + + // have to break these out because a general Exception might + // catch the RuntimeException being thrown above + } catch (IOException ioe) { + } catch (SecurityException se) { } + + ClassLoader cl = sketch.getClass().getClassLoader(); + + try { + // by default, data files are exported to the root path of the jar. + // (not the data folder) so check there first. + stream = cl.getResourceAsStream("data/" + filename); + if (stream != null) { + String cn = stream.getClass().getName(); + // this is an irritation of sun's java plug-in, which will return + // a non-null stream for an object that doesn't exist. like all good + // things, this is probably introduced in java 1.5. awesome! + // http://dev.processing.org/bugs/show_bug.cgi?id=359 + if (!cn.equals("sun.plugin.cache.EmptyInputStream")) { + stream.close(); + return "data/" + filename; + } + } + + // When used with an online script, also need to check without the + // data folder, in case it's not in a subfolder called 'data'. + // http://dev.processing.org/bugs/show_bug.cgi?id=389 + stream = cl.getResourceAsStream(filename); + if (stream != null) { + String cn = stream.getClass().getName(); + if (!cn.equals("sun.plugin.cache.EmptyInputStream")) { + stream.close(); + return filename; + } + } + } catch (IOException e) { } + + try { + // attempt to load from a local file, used when running as + // an application, or as a signed applet + try { // first try to catch any security exceptions + try { + String path = sketch.dataPath(filename); + stream = new FileInputStream(path); + if (stream != null) { + stream.close(); + return path; + } + } catch (IOException e2) { } + + try { + String path = sketch.sketchPath(filename); + stream = new FileInputStream(path); + if (stream != null) { + stream.close(); + return path; + } + } catch (Exception e) { } // ignored + + try { + stream = new FileInputStream(filename); + if (stream != null) { + stream.close(); + return filename; + } + } catch (IOException e1) { } + + } catch (SecurityException se) { } // online, whups + + } catch (Exception e) { + //die(e.getMessage(), e); + e.printStackTrace(); + } + + return ""; + } + + + @Override + public void placeWindow(int[] location, int[] editorLocation) { + + if (sketch.sketchFullScreen()) { + return; + } + + int x = window.getX() - window.getInsets().getLeftWidth(); + int y = window.getY() - window.getInsets().getTopHeight(); + int w = window.getWidth() + window.getInsets().getTotalWidth(); + int h = window.getHeight() + window.getInsets().getTotalHeight(); + + if (location != null) { +// System.err.println("place window at " + location[0] + ", " + location[1]); + window.setTopLevelPosition(location[0], location[1]); + + } else if (editorLocation != null) { +// System.err.println("place window at editor location " + editorLocation[0] + ", " + editorLocation[1]); + int locationX = editorLocation[0] - 20; + int locationY = editorLocation[1]; + + if (locationX - w > 10) { + // if it fits to the left of the window + window.setTopLevelPosition(locationX - w, locationY); + + } else { // doesn't fit + /* + // if it fits inside the editor window, + // offset slightly from upper lefthand corner + // so that it's plunked inside the text area + locationX = editorLocation[0] + 66; + locationY = editorLocation[1] + 66; + + if ((locationX + w > sketch.displayWidth - 33) || + (locationY + h > sketch.displayHeight - 33)) { + // otherwise center on screen + */ + locationX = (sketch.displayWidth - w) / 2; + locationY = (sketch.displayHeight - h) / 2; + /* + } + */ + window.setTopLevelPosition(locationX, locationY); + } + } else { // just center on screen + // Can't use frame.setLocationRelativeTo(null) because it sends the + // frame to the main display, which undermines the --display setting. + window.setTopLevelPosition(screenRect.x + (screenRect.width - sketchWidth) / 2, + screenRect.y + (screenRect.height - sketchHeight) / 2); + } + + Point frameLoc = new Point(x, y); + if (frameLoc.y < 0) { + // Windows actually allows you to place frames where they can't be + // closed. Awesome. http://dev.processing.org/bugs/show_bug.cgi?id=1508 + window.setTopLevelPosition(frameLoc.x, 30); + } + } + + + public void placePresent(int stopColor) { + float scale = getPixelScale(); + pgl.initPresentMode(0.5f * (screenRect.width/scale - sketchWidth), + 0.5f * (screenRect.height/scale - sketchHeight), stopColor); + PApplet.hideMenuBar(); + + window.setUndecorated(true); + window.setTopLevelPosition((int) displayRect.getX(), (int) displayRect.getY()); + window.setTopLevelSize((int) displayRect.getWidth(), (int) displayRect.getHeight()); + } + + + public void setupExternalMessages() { + external = true; + } + + + public void startThread() { + if (animator != null) { + animator.start(); + } + } + + + public void pauseThread() { + if (animator != null) { + animator.pause(); + } + } + + + public void resumeThread() { + if (animator != null) { + animator.resume(); + } + } + + + public boolean stopThread() { + if (drawExceptionHandler != null) { + drawExceptionHandler.interrupt(); + drawExceptionHandler = null; + } + if (animator != null) { + return animator.stop(); + } else { + return false; + } + } + + + public boolean isStopped() { + if (animator != null) { + return !animator.isAnimating(); + } else { + return true; + } + } + + + public void setLocation(final int x, final int y) { + display.getEDTUtil().invoke(false, new Runnable() { + @Override + public void run() { + window.setTopLevelPosition(x, y); + } + }); + } + + + public void setSize(int wide, int high) { + if (pgl.presentMode()) return; + + // When the surface is set to resizable via surface.setResizable(true), + // a crash may occur if the user sets the window to size zero. + // https://github.com/processing/processing/issues/5052 + if (high <= 0) { + high = 1; + } + if (wide <= 0) { + wide = 1; + } + + boolean changed = sketch.width != wide || sketch.height != high; + + sketchWidth = wide; + sketchHeight = high; + + sketch.setSize(wide, high); + graphics.setSize(wide, high); + + if (changed) { + window.setSize(wide * windowScaleFactor, high * windowScaleFactor); + } + } + + + public float getPixelScale() { + if (graphics.pixelDensity == 1) { + return 1; + } + + if (PApplet.platform == PConstants.MACOSX) { + return getCurrentPixelScale(); + } + + return 2; + } + + private float getCurrentPixelScale() { + // Even if the graphics are retina, the user might have moved the window + // into a non-retina monitor, so we need to check + window.getCurrentSurfaceScale(currentPixelScale); + return currentPixelScale[0]; + } + + + public Component getComponent() { + return canvas; + } + + + public void setSmooth(int level) { + pgl.reqNumSamples = level; + GLCapabilities caps = new GLCapabilities(profile); + caps.setAlphaBits(PGL.REQUESTED_ALPHA_BITS); + caps.setDepthBits(PGL.REQUESTED_DEPTH_BITS); + caps.setStencilBits(PGL.REQUESTED_STENCIL_BITS); + caps.setSampleBuffers(true); + caps.setNumSamples(pgl.reqNumSamples); + caps.setBackgroundOpaque(true); + caps.setOnscreen(true); + NativeSurface target = window.getNativeSurface(); + MutableGraphicsConfiguration config = (MutableGraphicsConfiguration) target.getGraphicsConfiguration(); + config.setChosenCapabilities(caps); + } + + + public void setFrameRate(float fps) { + if (fps < 1) { + PGraphics.showWarning( + "The OpenGL renderer cannot have a frame rate lower than 1.\n" + + "Your sketch will run at 1 frame per second."); + fps = 1; + } else if (fps > 1000) { + PGraphics.showWarning( + "The OpenGL renderer cannot have a frame rate higher than 1000.\n" + + "Your sketch will run at 1000 frames per second."); + fps = 1000; + } + if (animator != null) { + animator.stop(); + animator.setFPS((int)fps); + pgl.setFps(fps); + animator.start(); + } + } + + + public void requestFocus() { + display.getEDTUtil().invoke(false, new Runnable() { + @Override + public void run() { + window.requestFocus(); + } + }); + } + + + class DrawListener implements GLEventListener { + public void display(GLAutoDrawable drawable) { + if (display.getEDTUtil().isCurrentThreadEDT()) { + // For some reason, the first two frames of the animator are run on the + // EDT, skipping rendering Processing's frame in that case. + return; + } + + if (sketch.frameCount == 0) { + if (sketchWidth < sketchWidth0 || sketchHeight < sketchHeight0) { + PGraphics.showWarning("The sketch has been automatically resized to fit the screen resolution"); + } +// System.out.println("display: " + window.getWidth() + " "+ window.getHeight() + " - " + sketchWidth + " " + sketchHeight); + requestFocus(); + } + + if (!sketch.finished) { + pgl.getGL(drawable); + int pframeCount = sketch.frameCount; + sketch.handleDraw(); + if (pframeCount == sketch.frameCount || sketch.finished) { + // This hack allows the FBO layer to be swapped normally even if + // the sketch is no looping or finished because it does not call draw(), + // otherwise background artifacts may occur (depending on the hardware/drivers). + pgl.beginRender(); + pgl.endRender(sketch.sketchWindowColor()); + } + PGraphicsOpenGL.completeFinishedPixelTransfers(); + } + + if (sketch.exitCalled()) { + PGraphicsOpenGL.completeAllPixelTransfers(); + + sketch.dispose(); // calls stopThread(), which stops the animator. + sketch.exitActual(); + } + } + public void dispose(GLAutoDrawable drawable) { +// sketch.dispose(); + } + public void init(GLAutoDrawable drawable) { + pgl.getGL(drawable); + pgl.init(drawable); + sketch.start(); + + int c = graphics.backgroundColor; + pgl.clearColor(((c >> 16) & 0xff) / 255f, + ((c >> 8) & 0xff) / 255f, + ((c >> 0) & 0xff) / 255f, + ((c >> 24) & 0xff) / 255f); + pgl.clear(PGL.COLOR_BUFFER_BIT); + } + + public void reshape(GLAutoDrawable drawable, int x, int y, int w, int h) { + pgl.resetFBOLayer(); + pgl.getGL(drawable); + float scale = PApplet.platform == PConstants.MACOSX ? + getCurrentPixelScale() : getPixelScale(); + setSize((int) (w / scale), (int) (h / scale)); + } + } + + + protected class NEWTWindowListener implements com.jogamp.newt.event.WindowListener { + public NEWTWindowListener() { + super(); + } + @Override + public void windowGainedFocus(com.jogamp.newt.event.WindowEvent arg0) { + sketch.focused = true; + sketch.focusGained(); + } + + @Override + public void windowLostFocus(com.jogamp.newt.event.WindowEvent arg0) { + sketch.focused = false; + sketch.focusLost(); + } + + @Override + public void windowDestroyNotify(com.jogamp.newt.event.WindowEvent arg0) { + sketch.exit(); + } + + @Override + public void windowDestroyed(com.jogamp.newt.event.WindowEvent arg0) { + sketch.exit(); + } + + @Override + public void windowMoved(com.jogamp.newt.event.WindowEvent arg0) { + if (external) { + sketch.frameMoved(window.getX(), window.getY()); + } + } + + @Override + public void windowRepaint(com.jogamp.newt.event.WindowUpdateEvent arg0) { + } + + @Override + public void windowResized(com.jogamp.newt.event.WindowEvent arg0) { + } + } + + + // NEWT mouse listener + protected class NEWTMouseListener extends com.jogamp.newt.event.MouseAdapter { + public NEWTMouseListener() { + super(); + } + @Override + public void mousePressed(com.jogamp.newt.event.MouseEvent e) { + nativeMouseEvent(e, MouseEvent.PRESS); + } + @Override + public void mouseReleased(com.jogamp.newt.event.MouseEvent e) { + nativeMouseEvent(e, MouseEvent.RELEASE); + } + @Override + public void mouseClicked(com.jogamp.newt.event.MouseEvent e) { + nativeMouseEvent(e, MouseEvent.CLICK); + } + @Override + public void mouseDragged(com.jogamp.newt.event.MouseEvent e) { + nativeMouseEvent(e, MouseEvent.DRAG); + } + @Override + public void mouseMoved(com.jogamp.newt.event.MouseEvent e) { + nativeMouseEvent(e, MouseEvent.MOVE); + } + @Override + public void mouseWheelMoved(com.jogamp.newt.event.MouseEvent e) { + nativeMouseEvent(e, MouseEvent.WHEEL); + } + @Override + public void mouseEntered(com.jogamp.newt.event.MouseEvent e) { +// System.out.println("enter"); + nativeMouseEvent(e, MouseEvent.ENTER); + } + @Override + public void mouseExited(com.jogamp.newt.event.MouseEvent e) { +// System.out.println("exit"); + nativeMouseEvent(e, MouseEvent.EXIT); + } + } + + + // NEWT key listener + protected class NEWTKeyListener extends com.jogamp.newt.event.KeyAdapter { + public NEWTKeyListener() { + super(); + } + @Override + public void keyPressed(com.jogamp.newt.event.KeyEvent e) { + nativeKeyEvent(e, KeyEvent.PRESS); + } + @Override + public void keyReleased(com.jogamp.newt.event.KeyEvent e) { + nativeKeyEvent(e, KeyEvent.RELEASE); + } + public void keyTyped(com.jogamp.newt.event.KeyEvent e) { + nativeKeyEvent(e, KeyEvent.TYPE); + } + } + + + protected void nativeMouseEvent(com.jogamp.newt.event.MouseEvent nativeEvent, + int peAction) { + int modifiers = nativeEvent.getModifiers(); + int peModifiers = modifiers & + (InputEvent.SHIFT_MASK | + InputEvent.CTRL_MASK | + InputEvent.META_MASK | + InputEvent.ALT_MASK); + + int peButton = 0; + switch (nativeEvent.getButton()) { + case com.jogamp.newt.event.MouseEvent.BUTTON1: + peButton = PConstants.LEFT; + break; + case com.jogamp.newt.event.MouseEvent.BUTTON2: + peButton = PConstants.CENTER; + break; + case com.jogamp.newt.event.MouseEvent.BUTTON3: + peButton = PConstants.RIGHT; + break; + } + + if (PApplet.platform == PConstants.MACOSX) { + //if (nativeEvent.isPopupTrigger()) { + if ((modifiers & InputEvent.CTRL_MASK) != 0) { + peButton = PConstants.RIGHT; + } + } + + int peCount = 0; + if (peAction == MouseEvent.WHEEL) { + // Invert wheel rotation count so it matches JAVA2D's + // https://github.com/processing/processing/issues/3840 + peCount = -(nativeEvent.isShiftDown() ? (int)nativeEvent.getRotation()[0]: + (int)nativeEvent.getRotation()[1]); + } else { + peCount = nativeEvent.getClickCount(); + } + + int scale; + if (PApplet.platform == PConstants.MACOSX) { + scale = (int) getCurrentPixelScale(); + } else { + scale = (int) getPixelScale(); + } + int sx = nativeEvent.getX() / scale; + int sy = nativeEvent.getY() / scale; + int mx = sx; + int my = sy; + + if (pgl.presentMode()) { + mx -= (int)pgl.presentX; + my -= (int)pgl.presentY; + if (peAction == KeyEvent.RELEASE && + pgl.insideStopButton(sx, sy - screenRect.height / windowScaleFactor)) { + sketch.exit(); + } + if (mx < 0 || sketchWidth < mx || my < 0 || sketchHeight < my) { + return; + } + } + + MouseEvent me = new MouseEvent(nativeEvent, nativeEvent.getWhen(), + peAction, peModifiers, + mx, my, + peButton, + peCount); + + sketch.postEvent(me); + } + + + protected void nativeKeyEvent(com.jogamp.newt.event.KeyEvent nativeEvent, + int peAction) { + int peModifiers = nativeEvent.getModifiers() & + (InputEvent.SHIFT_MASK | + InputEvent.CTRL_MASK | + InputEvent.META_MASK | + InputEvent.ALT_MASK); + + short code = nativeEvent.getKeyCode(); + char keyChar; + int keyCode; + if (isPCodedKey(code)) { + keyCode = mapToPConst(code); + keyChar = PConstants.CODED; + } else if (isHackyKey(code)) { + // we can return only one char for ENTER, let it be \n everywhere + keyCode = code == com.jogamp.newt.event.KeyEvent.VK_ENTER ? + PConstants.ENTER : code; + keyChar = hackToChar(code, nativeEvent.getKeyChar()); + } else { + keyCode = code; + keyChar = nativeEvent.getKeyChar(); + } + + // From http://jogamp.org/deployment/v2.1.0/javadoc/jogl/javadoc/com/jogamp/newt/event/KeyEvent.html + // public final short getKeySymbol() + // Returns the virtual key symbol reflecting the current keyboard layout. + // public final short getKeyCode() + // Returns the virtual key code using a fixed mapping to the US keyboard layout. + // In contrast to key symbol, key code uses a fixed US keyboard layout and therefore is keyboard layout independent. + // E.g. virtual key code VK_Y denotes the same physical key regardless whether keyboard layout QWERTY or QWERTZ is active. The key symbol of the former is VK_Y, where the latter produces VK_Y. + KeyEvent ke = new KeyEvent(nativeEvent, nativeEvent.getWhen(), + peAction, peModifiers, + keyChar, + keyCode, + nativeEvent.isAutoRepeat()); + + sketch.postEvent(ke); + + if (!isPCodedKey(code) && !isHackyKey(code)) { + if (peAction == KeyEvent.PRESS) { + // Create key typed event + // TODO: combine dead keys with the following key + KeyEvent tke = new KeyEvent(nativeEvent, nativeEvent.getWhen(), + KeyEvent.TYPE, peModifiers, + keyChar, + 0, + nativeEvent.isAutoRepeat()); + + sketch.postEvent(tke); + } + } + } + + + private static boolean isPCodedKey(short code) { + return code == com.jogamp.newt.event.KeyEvent.VK_UP || + code == com.jogamp.newt.event.KeyEvent.VK_DOWN || + code == com.jogamp.newt.event.KeyEvent.VK_LEFT || + code == com.jogamp.newt.event.KeyEvent.VK_RIGHT || + code == com.jogamp.newt.event.KeyEvent.VK_ALT || + code == com.jogamp.newt.event.KeyEvent.VK_CONTROL || + code == com.jogamp.newt.event.KeyEvent.VK_SHIFT || + code == com.jogamp.newt.event.KeyEvent.VK_WINDOWS; + } + + + // Why do we need this mapping? + // Relevant discussion and links here: + // http://forum.jogamp.org/Newt-wrong-keycode-for-key-td4033690.html#a4033697 + // (I don't think this is a complete solution). + private static int mapToPConst(short code) { + switch (code) { + case com.jogamp.newt.event.KeyEvent.VK_UP: + return PConstants.UP; + case com.jogamp.newt.event.KeyEvent.VK_DOWN: + return PConstants.DOWN; + case com.jogamp.newt.event.KeyEvent.VK_LEFT: + return PConstants.LEFT; + case com.jogamp.newt.event.KeyEvent.VK_RIGHT: + return PConstants.RIGHT; + case com.jogamp.newt.event.KeyEvent.VK_ALT: + return PConstants.ALT; + case com.jogamp.newt.event.KeyEvent.VK_CONTROL: + return PConstants.CONTROL; + case com.jogamp.newt.event.KeyEvent.VK_SHIFT: + return PConstants.SHIFT; + case com.jogamp.newt.event.KeyEvent.VK_WINDOWS: + return java.awt.event.KeyEvent.VK_META; + default: + return code; + } + } + + + private static boolean isHackyKey(short code) { + switch (code) { + case com.jogamp.newt.event.KeyEvent.VK_BACK_SPACE: + case com.jogamp.newt.event.KeyEvent.VK_TAB: + case com.jogamp.newt.event.KeyEvent.VK_ENTER: + case com.jogamp.newt.event.KeyEvent.VK_ESCAPE: + case com.jogamp.newt.event.KeyEvent.VK_DELETE: + return true; + } + return false; + } + + + private static char hackToChar(short code, char def) { + switch (code) { + case com.jogamp.newt.event.KeyEvent.VK_BACK_SPACE: + return PConstants.BACKSPACE; + case com.jogamp.newt.event.KeyEvent.VK_TAB: + return PConstants.TAB; + case com.jogamp.newt.event.KeyEvent.VK_ENTER: + return PConstants.ENTER; + case com.jogamp.newt.event.KeyEvent.VK_ESCAPE: + return PConstants.ESC; + case com.jogamp.newt.event.KeyEvent.VK_DELETE: + return PConstants.DELETE; + } + return def; + } + + + // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + + + class CursorInfo { + PImage image; + int x, y; + + CursorInfo(PImage image, int x, int y) { + this.image = image; + this.x = x; + this.y = y; + } + + void set() { + setCursor(image, x, y); + } + } + + static Map cursors = new HashMap<>(); + static Map cursorNames = new HashMap<>(); + static { + cursorNames.put(PConstants.ARROW, "arrow"); + cursorNames.put(PConstants.CROSS, "cross"); + cursorNames.put(PConstants.WAIT, "wait"); + cursorNames.put(PConstants.MOVE, "move"); + cursorNames.put(PConstants.HAND, "hand"); + cursorNames.put(PConstants.TEXT, "text"); + } + + + public void setCursor(int kind) { + if (!cursorNames.containsKey(kind)) { + PGraphics.showWarning("Unknown cursor type: " + kind); + return; + } + CursorInfo cursor = cursors.get(kind); + if (cursor == null) { + String name = cursorNames.get(kind); + if (name != null) { + ImageIcon icon = + new ImageIcon(getClass().getResource("cursors/" + name + ".png")); + PImage img = new PImage(icon.getImage()); + // Most cursors just use the center as the hotspot... + int x = img.width / 2; + int y = img.height / 2; + // ...others are more specific + if (kind == PConstants.ARROW) { + x = 10; y = 7; + } else if (kind == PConstants.HAND) { + x = 12; y = 8; + } else if (kind == PConstants.TEXT) { + x = 16; y = 22; + } + cursor = new CursorInfo(img, x, y); + cursors.put(kind, cursor); + } + } + if (cursor != null) { + cursor.set(); + } else { + PGraphics.showWarning("Cannot load cursor type: " + kind); + } + } + + + public void setCursor(PImage image, int hotspotX, int hotspotY) { + Display disp = window.getScreen().getDisplay(); + BufferedImage bimg = (BufferedImage)image.getNative(); + DataBufferInt dbuf = (DataBufferInt)bimg.getData().getDataBuffer(); + int[] ipix = dbuf.getData(); + ByteBuffer pixels = ByteBuffer.allocate(ipix.length * 4); + pixels.asIntBuffer().put(ipix); + PixelFormat format = PixelFormat.ARGB8888; + final Dimension size = new Dimension(bimg.getWidth(), bimg.getHeight()); + PixelRectangle pixelrect = new PixelRectangle.GenericPixelRect(format, size, 0, false, pixels); + final PointerIcon pi = disp.createPointerIcon(pixelrect, hotspotX, hotspotY); + display.getEDTUtil().invoke(false, new Runnable() { + @Override + public void run() { + window.setPointerVisible(true); + window.setPointerIcon(pi); + } + }); + } + + + public void showCursor() { + display.getEDTUtil().invoke(false, new Runnable() { + @Override + public void run() { + window.setPointerVisible(true); + } + }); + } + + + public void hideCursor() { + display.getEDTUtil().invoke(false, new Runnable() { + @Override + public void run() { + window.setPointerVisible(false); + } + }); + } +} diff --git a/src/main/java/processing/opengl/Texture.java b/src/main/java/processing/opengl/Texture.java new file mode 100644 index 0000000..4cc51ba --- /dev/null +++ b/src/main/java/processing/opengl/Texture.java @@ -0,0 +1,1670 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.opengl; + +import processing.core.PApplet; +import processing.core.PConstants; +import processing.core.PGraphics; +import processing.opengl.PGraphicsOpenGL.GLResourceTexture; + +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.NoSuchElementException; + +/** + * This class wraps an OpenGL texture. + * By Andres Colubri + * + */ +public class Texture implements PConstants { + /** + * Texture with normalized UV. + */ + protected static final int TEX2D = 0; + /** + * Texture with un-normalized UV. + */ + protected static final int TEXRECT = 1; + + /** Point sampling: both magnification and minification filtering are set + * to nearest */ + protected static final int POINT = 2; + /** Linear sampling: magnification filtering is nearest, minification set + * to linear */ + protected static final int LINEAR = 3; + /** Bilinear sampling: both magnification filtering is set to linear and + * minification either to linear-mipmap-nearest (linear interpolation is used + * within a mipmap, but not between different mipmaps). */ + protected static final int BILINEAR = 4; + /** Trilinear sampling: magnification filtering set to linear, minification to + * linear-mipmap-linear, which offers the best mipmap quality since linear + * interpolation to compute the value in each of two maps and then + * interpolates linearly between these two values. */ + protected static final int TRILINEAR = 5; + + + // This constant controls how many times pixelBuffer and rgbaPixels can be + // accessed before they are not released anymore. The idea is that if they + // have been used only a few times, it doesn't make sense to keep them around. + protected static final int MAX_UPDATES = 10; + + // The minimum amount of free JVM's memory (in MB) before pixelBuffer and + // rgbaPixels are released every time after they are used. + protected static final int MIN_MEMORY = 5; + + public int width, height; + + public int glName; + public int glTarget; + public int glFormat; + public int glMinFilter; + public int glMagFilter; + public int glWrapS; + public int glWrapT; + public int glWidth; + public int glHeight; + private GLResourceTexture glres; + + protected PGraphicsOpenGL pg; + protected PGL pgl; // The interface between Processing and OpenGL. + protected int context; // The context that created this texture. + protected boolean colorBuffer; // true if it is the color attachment of + // FrameBuffer object. + + protected boolean usingMipmaps; + protected boolean usingRepeat; + protected float maxTexcoordU; + protected float maxTexcoordV; + protected boolean bound; + + protected boolean invertedX; + protected boolean invertedY; + + protected int[] rgbaPixels = null; + protected IntBuffer pixelBuffer = null; + + protected int[] edgePixels = null; + protected IntBuffer edgeBuffer = null; + + protected FrameBuffer tempFbo = null; + protected int pixBufUpdateCount = 0; + protected int rgbaPixUpdateCount = 0; + + /** Modified portion of the texture */ + protected boolean modified; + protected int mx1, my1, mx2, my2; + + protected Object bufferSource; + protected LinkedList bufferCache = null; + protected LinkedList usedBuffers = null; + protected Method disposeBufferMethod; + public static final int MAX_BUFFER_CACHE_SIZE = 3; + + //////////////////////////////////////////////////////////// + + // Constructors. + + + public Texture(PGraphicsOpenGL pg) { + this.pg = pg; + pgl = pg.pgl; + context = pgl.createEmptyContext(); + + colorBuffer = false; + + glName = 0; + } + + + /** + * Creates an instance of PTexture with size width x height. The texture is + * initialized (empty) to that size. + * @param width int + * @param height int + */ + public Texture(PGraphicsOpenGL pg, int width, int height) { + this(pg, width, height, new Parameters()); + } + + + /** + * Creates an instance of PTexture with size width x height and with the + * specified parameters. The texture is initialized (empty) to that size. + * @param width int + * @param height int + * @param params Parameters + */ + public Texture(PGraphicsOpenGL pg, int width, int height, Object params) { + this.pg = pg; + pgl = pg.pgl; + context = pgl.createEmptyContext(); + + colorBuffer = false; + + glName = 0; + + init(width, height, (Parameters)params); + } + + + //////////////////////////////////////////////////////////// + + // Init, resize methods + + + /** + * Sets the size of the image and texture to width x height. If the texture is + * already initialized, it first destroys the current OpenGL texture object + * and then creates a new one with the specified size. + * @param width int + * @param height int + */ + public void init(int width, int height) { + Parameters params; + if (0 < glName) { + // Re-initializing a pre-existing texture. + // We use the current parameters as default: + params = getParameters(); + } else { + // Just built-in default parameters otherwise: + params = new Parameters(); + } + init(width, height, params); + } + + + /** + * Sets the size of the image and texture to width x height, and the + * parameters of the texture to params. If the texture is already initialized, + * it first destroys the current OpenGL texture object and then creates a new + * one with the specified size. + * @param width int + * @param height int + * @param params GLTextureParameters + */ + public void init(int width, int height, Parameters params) { + setParameters(params); + setSize(width, height); + allocate(); + } + + + /** + * Initializes the texture using GL parameters + */ + public void init(int width, int height, + int glName, int glTarget, int glFormat, + int glWidth, int glHeight, + int glMinFilter, int glMagFilter, + int glWrapS, int glWrapT) { + this.width = width; + this.height = height; + + this.glName = glName; + this.glTarget = glTarget; + this.glFormat = glFormat; + this.glWidth = glWidth; + this.glHeight = glHeight; + this.glMinFilter = glMinFilter; + this.glMagFilter = glMagFilter; + this.glWrapS = glWrapS; + this.glWrapT = glWrapT; + + maxTexcoordU = (float)width / glWidth; + maxTexcoordV = (float)height / glHeight; + + usingMipmaps = glMinFilter == PGL.LINEAR_MIPMAP_NEAREST || + glMinFilter == PGL.LINEAR_MIPMAP_LINEAR; + + usingRepeat = glWrapS == PGL.REPEAT || glWrapT == PGL.REPEAT; + } + + + public void resize(int wide, int high) { + // Disposing current resources. + dispose(); + + // Creating new texture with the appropriate size. + Texture tex = new Texture(pg, wide, high, getParameters()); + + // Copying the contents of this texture into tex. + tex.set(this); + + // Now, overwriting "this" with tex. + copyObject(tex); + + // Nullifying some utility objects so they are recreated with the + // appropriate size when needed. + tempFbo = null; + } + + + /** + * Returns true if the texture has been initialized. + * @return boolean + */ + public boolean available() { + return 0 < glName; + } + + + //////////////////////////////////////////////////////////// + + // Set methods + + + public void set(Texture tex) { + copyTexture(tex, 0, 0, tex.width, tex.height, true); + } + + + public void set(Texture tex, int x, int y, int w, int h) { + copyTexture(tex, x, y, w, h, true); + } + + + public void set(int texTarget, int texName, int texWidth, int texHeight, + int w, int h) { + copyTexture(texTarget, texName, texWidth, texHeight, 0, 0, w, h, true); + } + + + public void set(int texTarget, int texName, int texWidth, int texHeight, + int target, int tex, int x, int y, int w, int h) { + copyTexture(texTarget, texName, texWidth, texHeight, x, y, w, h, true); + } + + + public void set(int[] pixels) { + set(pixels, 0, 0, width, height, ARGB); + } + + + public void set(int[] pixels, int format) { + set(pixels, 0, 0, width, height, format); + } + + + public void set(int[] pixels, int x, int y, int w, int h) { + set(pixels, x, y, w, h, ARGB); + } + + + public void set(int[] pixels, int x, int y, int w, int h, int format) { + if (pixels == null) { + PGraphics.showWarning("The pixels array is null."); + return; + } + if (pixels.length < w * h) { + PGraphics.showWarning("The pixel array has a length of " + + pixels.length + ", but it should be at least " + + w * h); + return; + } + + if (pixels.length == 0 || w == 0 || h == 0) { + return; + } + + boolean enabledTex = false; + if (!pgl.texturingIsEnabled(glTarget)) { + pgl.enableTexturing(glTarget); + enabledTex = true; + } + pgl.bindTexture(glTarget, glName); + + loadPixels(w * h); + convertToRGBA(pixels, format, w, h); + if (invertedX) flipArrayOnX(rgbaPixels, 1); + if (invertedY) flipArrayOnY(rgbaPixels, 1); + updatePixelBuffer(rgbaPixels); + pgl.texSubImage2D(glTarget, 0, x, y, w, h, PGL.RGBA, PGL.UNSIGNED_BYTE, + pixelBuffer); + fillEdges(x, y, w, h); + + if (usingMipmaps) { + if (PGraphicsOpenGL.autoMipmapGenSupported) { + pgl.generateMipmap(glTarget); + } else { + manualMipmap(); + } + } + + pgl.bindTexture(glTarget, 0); + if (enabledTex) { + pgl.disableTexturing(glTarget); + } + + releasePixelBuffer(); + releaseRGBAPixels(); + + updateTexels(x, y, w, h); + } + + + //////////////////////////////////////////////////////////// + + // Native set methods + + + public void setNative(int[] pixels) { + setNative(pixels, 0, 0, width, height); + } + + + public void setNative(int[] pixels, int x, int y, int w, int h) { + updatePixelBuffer(pixels); + setNative(pixelBuffer, x, y, w, h); + releasePixelBuffer(); + } + + + public void setNative(IntBuffer pixBuf, int x, int y, int w, int h) { + if (pixBuf == null) { + pixBuf = null; + PGraphics.showWarning("The pixel buffer is null."); + return; + } + if (pixBuf.capacity() < w * h) { + PGraphics.showWarning("The pixel bufer has a length of " + + pixBuf.capacity() + ", but it should be at least " + + w * h); + return; + } + + if (pixBuf.capacity() == 0) { + // Nothing to do (means that w == h == 0) but not an erroneous situation + return; + } + + boolean enabledTex = false; + if (!pgl.texturingIsEnabled(glTarget)) { + pgl.enableTexturing(glTarget); + enabledTex = true; + } + pgl.bindTexture(glTarget, glName); + + pgl.texSubImage2D(glTarget, 0, x, y, w, h, PGL.RGBA, PGL.UNSIGNED_BYTE, + pixBuf); + fillEdges(x, y, w, h); + + if (usingMipmaps) { + if (PGraphicsOpenGL.autoMipmapGenSupported) { + pgl.generateMipmap(glTarget); + } else { + manualMipmap(); + } + } + pgl.bindTexture(glTarget, 0); + if (enabledTex) { + pgl.disableTexturing(glTarget); + } + + updateTexels(x, y, w, h); + } + + + //////////////////////////////////////////////////////////// + + // Get methods + + + /** + * Copy texture to pixels. Involves video memory to main memory transfer (slow). + */ + public void get(int[] pixels) { + if (pixels == null) { + throw new RuntimeException("Trying to copy texture to null pixels array"); + } + if (pixels.length != width * height) { + throw new RuntimeException("Trying to copy texture to pixels array of " + + "wrong size"); + } + + if (tempFbo == null) { + tempFbo = new FrameBuffer(pg, glWidth, glHeight); + } + + // Attaching the texture to the color buffer of a FBO, binding the FBO and + // reading the pixels from the current draw buffer (which is the color + // buffer of the FBO). + tempFbo.setColorBuffer(this); + pg.pushFramebuffer(); + pg.setFramebuffer(tempFbo); + tempFbo.readPixels(); + pg.popFramebuffer(); + + tempFbo.getPixels(pixels); + convertToARGB(pixels); + + if (invertedX) flipArrayOnX(pixels, 1); + if (invertedY) flipArrayOnY(pixels, 1); + } + + + //////////////////////////////////////////////////////////// + + // Put methods (the source texture is not resized to cover the entire + // destination). + + + public void put(Texture tex) { + copyTexture(tex, 0, 0, tex.width, tex.height, false); + } + + + public void put(Texture tex, int x, int y, int w, int h) { + copyTexture(tex, x, y, w, h, false); + } + + + public void put(int texTarget, int texName, int texWidth, int texHeight, + int w, int h) { + copyTexture(texTarget, texName, texWidth, texHeight, 0, 0, w, h, false); + } + + + public void put(int texTarget, int texName, int texWidth, int texHeight, + int target, int tex, int x, int y, int w, int h) { + copyTexture(texTarget, texName, texWidth, texHeight, x, y, w, h, false); + } + + + //////////////////////////////////////////////////////////// + + // Get OpenGL parameters + + + /** + * Returns true or false whether or not the texture is using mipmaps. + * @return boolean + */ + public boolean usingMipmaps() { + return usingMipmaps; + } + + + public void usingMipmaps(boolean mipmaps, int sampling) { + int glMagFilter0 = glMagFilter; + int glMinFilter0 = glMinFilter; + if (mipmaps) { + if (sampling == POINT) { + glMagFilter = PGL.NEAREST; + glMinFilter = PGL.NEAREST; + usingMipmaps = false; + } else if (sampling == LINEAR) { + glMagFilter = PGL.NEAREST; + glMinFilter = + PGL.MIPMAPS_ENABLED ? PGL.LINEAR_MIPMAP_NEAREST : PGL.LINEAR; + usingMipmaps = true; + } else if (sampling == BILINEAR) { + glMagFilter = PGL.LINEAR; + glMinFilter = + PGL.MIPMAPS_ENABLED ? PGL.LINEAR_MIPMAP_NEAREST : PGL.LINEAR; + usingMipmaps = true; + } else if (sampling == TRILINEAR) { + glMagFilter = PGL.LINEAR; + glMinFilter = + PGL.MIPMAPS_ENABLED ? PGL.LINEAR_MIPMAP_LINEAR : PGL.LINEAR; + usingMipmaps = true; + } else { + throw new RuntimeException("Unknown texture filtering mode"); + } + } else { + usingMipmaps = false; + if (sampling == POINT) { + glMagFilter = PGL.NEAREST; + glMinFilter = PGL.NEAREST; + } else if (sampling == LINEAR) { + glMagFilter = PGL.NEAREST; + glMinFilter = PGL.LINEAR; + } else if (sampling == BILINEAR || sampling == TRILINEAR) { + glMagFilter = PGL.LINEAR; + glMinFilter = PGL.LINEAR; + } else { + throw new RuntimeException("Unknown texture filtering mode"); + } + } + + if (glMagFilter0 != glMagFilter || glMinFilter0 != glMinFilter) { + bind(); + pgl.texParameteri(glTarget, PGL.TEXTURE_MIN_FILTER, glMinFilter); + pgl.texParameteri(glTarget, PGL.TEXTURE_MAG_FILTER, glMagFilter); + if (usingMipmaps) { + if (PGraphicsOpenGL.autoMipmapGenSupported) { + pgl.generateMipmap(glTarget); + } else { + manualMipmap(); + } + } + unbind(); + } + } + + + /** + * Returns true or false whether or not the texture is using repeat wrap mode + * along either U or V directions. + * @return boolean + */ + public boolean usingRepeat() { + return usingRepeat; + } + + + public void usingRepeat(boolean repeat) { + if (repeat) { + glWrapS = PGL.REPEAT; + glWrapT = PGL.REPEAT; + usingRepeat = true; + } else { + glWrapS = PGL.CLAMP_TO_EDGE; + glWrapT = PGL.CLAMP_TO_EDGE; + usingRepeat = false; + } + + bind(); + pgl.texParameteri(glTarget, PGL.TEXTURE_WRAP_S, glWrapS); + pgl.texParameteri(glTarget, PGL.TEXTURE_WRAP_T, glWrapT); + unbind(); + } + + + /** + * Returns the maximum possible value for the texture coordinate U + * (horizontal). + * @return float + */ + public float maxTexcoordU() { + return maxTexcoordU; + } + + + /** + * Returns the maximum possible value for the texture coordinate V (vertical). + * @return float + */ + public float maxTexcoordV() { + return maxTexcoordV; + } + + + /** + * Returns true if the texture is inverted along the horizontal direction. + * @return boolean; + */ + public boolean invertedX() { + return invertedX; + } + + + /** + * Sets the texture as inverted or not along the horizontal direction. + * @param v boolean; + */ + public void invertedX(boolean v) { + invertedX = v; + } + + + /** + * Returns true if the texture is inverted along the vertical direction. + * @return boolean; + */ + public boolean invertedY() { + return invertedY; + } + + + /** + * Sets the texture as inverted or not along the vertical direction. + * @param v boolean; + */ + public void invertedY(boolean v) { + invertedY = v; + } + + + public int currentSampling() { + if (glMagFilter == PGL.NEAREST && glMinFilter == PGL.NEAREST) { + return POINT; + } else if (glMagFilter == PGL.NEAREST && + glMinFilter == (PGL.MIPMAPS_ENABLED ? PGL.LINEAR_MIPMAP_NEAREST : PGL.LINEAR)) { + return LINEAR; + } else if (glMagFilter == PGL.LINEAR && + glMinFilter == (PGL.MIPMAPS_ENABLED ? PGL.LINEAR_MIPMAP_NEAREST : PGL.LINEAR)) { + return BILINEAR; + } else if (glMagFilter == PGL.LINEAR && + glMinFilter == PGL.LINEAR_MIPMAP_LINEAR) { + return TRILINEAR; + } else { + return -1; + } + } + + //////////////////////////////////////////////////////////// + + // Bind/unbind + + + public void bind() { + // Binding a texture automatically enables texturing for the + // texture target from that moment onwards. Unbinding the texture + // won't disable texturing. + if (!pgl.texturingIsEnabled(glTarget)) { + pgl.enableTexturing(glTarget); + } + pgl.bindTexture(glTarget, glName); + bound = true; + } + + + public void unbind() { + if (pgl.textureIsBound(glTarget, glName)) { + // We don't want to unbind another texture + // that might be bound instead of this one. + if (!pgl.texturingIsEnabled(glTarget)) { + pgl.enableTexturing(glTarget); + pgl.bindTexture(glTarget, 0); + pgl.disableTexturing(glTarget); + } else { + pgl.bindTexture(glTarget, 0); + } + } + bound = false; + } + + + public boolean bound() { + // A true result might not necessarily mean that texturing is enabled + // (a texture can be bound to the target, but texturing is disabled). + return bound; + } + + + ////////////////////////////////////////////////////////////// + + // Modified flag + + + public boolean isModified() { + return modified; + } + + + public void setModified() { + modified = true; + } + + + public void setModified(boolean m) { + modified = m; + } + + + public int getModifiedX1() { + return mx1; + } + + + public int getModifiedX2() { + return mx2; + } + + + public int getModifiedY1() { + return my1; + } + + + public int getModifiedY2() { + return my2; + } + + + public void updateTexels() { + updateTexelsImpl(0, 0, width, height); + } + + + public void updateTexels(int x, int y, int w, int h) { + updateTexelsImpl(x, y, w, h); + } + + + protected void updateTexelsImpl(int x, int y, int w, int h) { + int x2 = x + w; + int y2 = y + h; + + if (!modified) { + mx1 = PApplet.max(0, x); + mx2 = PApplet.min(width - 1, x2); + my1 = PApplet.max(0, y); + my2 = PApplet.min(height - 1, y2); + modified = true; + + } else { + if (x < mx1) mx1 = PApplet.max(0, x); + if (x > mx2) mx2 = PApplet.min(width - 1, x); + if (y < my1) my1 = PApplet.max(0, y); + if (y > my2) my2 = y; + + if (x2 < mx1) mx1 = PApplet.max(0, x2); + if (x2 > mx2) mx2 = PApplet.min(width - 1, x2); + if (y2 < my1) my1 = PApplet.max(0, y2); + if (y2 > my2) my2 = PApplet.min(height - 1, y2); + } + } + + + protected void loadPixels(int len) { + if (rgbaPixels == null || rgbaPixels.length < len) { + rgbaPixels = new int[len]; + } + } + + + protected void updatePixelBuffer(int[] pixels) { + pixelBuffer = PGL.updateIntBuffer(pixelBuffer, pixels, true); + pixBufUpdateCount++; + } + + + protected void manualMipmap() { + // TODO: finish manual mipmap generation, + // https://github.com/processing/processing/issues/3335 + } + + + //////////////////////////////////////////////////////////// + + // Buffer sink interface. + + + public void setBufferSource(Object source) { + bufferSource = source; + getSourceMethods(); + } + + + public void copyBufferFromSource(Object natRef, ByteBuffer byteBuf, + int w, int h) { + if (bufferCache == null) { + bufferCache = new LinkedList(); + } + + if (bufferCache.size() + 1 <= MAX_BUFFER_CACHE_SIZE) { + bufferCache.add(new BufferData(natRef, byteBuf.asIntBuffer(), w, h)); + } else { + // The buffer cache reached the maximum size, so we just dispose + // the new buffer by adding it to the list of used buffers. + try { + usedBuffers.add(new BufferData(natRef, byteBuf.asIntBuffer(), w, h)); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + + public void disposeSourceBuffer() { + if (usedBuffers == null) return; + + while (0 < usedBuffers.size()) { + BufferData data = null; + try { + data = usedBuffers.remove(0); + } catch (NoSuchElementException ex) { + PGraphics.showWarning("Cannot remove used buffer"); + } + if (data != null) { + data.dispose(); + } + } + } + + public void getBufferPixels(int[] pixels) { + // We get the buffer either from the used buffers or the cache, giving + // priority to the used buffers. Why? Because the used buffer was already + // transferred to the texture, so the pixels should be in sync with the + // texture. + BufferData data = null; + if (usedBuffers != null && 0 < usedBuffers.size()) { + data = usedBuffers.getLast(); + } else if (bufferCache != null && 0 < bufferCache.size()) { + data = bufferCache.getLast(); + } + if (data != null) { + if ((data.w != width) || (data.h != height)) { + init(data.w, data.h); + } + + data.rgbBuf.rewind(); + data.rgbBuf.get(pixels); + convertToARGB(pixels); + + // In order to avoid a cached buffer to overwrite the texture when the + // renderer draws the texture, and hence put the pixels put of sync, we + // simply empty the cache. + if (usedBuffers == null) { + usedBuffers = new LinkedList(); + } + while (0 < bufferCache.size()) { + data = bufferCache.remove(0); + usedBuffers.add(data); + } + } + } + + + public boolean hasBufferSource() { + return bufferSource != null; + } + + + public boolean hasBuffers() { + return bufferSource != null && bufferCache != null && + 0 < bufferCache.size(); + } + + + protected boolean bufferUpdate() { + BufferData data = null; + try { + data = bufferCache.remove(0); + } catch (NoSuchElementException ex) { + PGraphics.showWarning("Don't have pixel data to copy to texture"); + } + + if (data != null) { + if ((data.w != width) || (data.h != height)) { + init(data.w, data.h); + } + data.rgbBuf.rewind(); + setNative(data.rgbBuf, 0, 0, width, height); + + // Putting the buffer in the used buffers list to dispose at the end of + // draw. + if (usedBuffers == null) { + usedBuffers = new LinkedList(); + } + usedBuffers.add(data); + + return true; + } else { + return false; + } + } + + + protected void getSourceMethods() { + try { + disposeBufferMethod = bufferSource.getClass(). + getMethod("disposeBuffer", new Class[] { Object.class }); + } catch (Exception e) { + throw new RuntimeException("Provided source object doesn't have a " + + "disposeBuffer method."); + } + } + + + //////////////////////////////////////////////////////////// + + // Utilities + + + /** + * Flips intArray along the X axis. + * @param intArray int[] + * @param mult int + */ + protected void flipArrayOnX(int[] intArray, int mult) { + int index = 0; + int xindex = mult * (width - 1); + for (int x = 0; x < width / 2; x++) { + for (int y = 0; y < height; y++) { + int i = index + mult * y * width; + int j = xindex + mult * y * width; + + for (int c = 0; c < mult; c++) { + int temp = intArray[i]; + intArray[i] = intArray[j]; + intArray[j] = temp; + + i++; + j++; + } + + } + index += mult; + xindex -= mult; + } + } + + + /** + * Flips intArray along the Y axis. + * @param intArray int[] + * @param mult int + */ + protected void flipArrayOnY(int[] intArray, int mult) { + int index = 0; + int yindex = mult * (height - 1) * width; + for (int y = 0; y < height / 2; y++) { + for (int x = 0; x < mult * width; x++) { + int temp = intArray[index]; + intArray[index] = intArray[yindex]; + intArray[yindex] = temp; + + index++; + yindex++; + } + yindex -= mult * width * 2; + } + } + + + /** + * Reorders a pixel array in the given format into the order required by + * OpenGL (RGBA) and stores it into rgbaPixels. The width and height + * parameters are used in the YUV420 to RBGBA conversion. + * @param pixels int[] + * @param format int + * @param w int + * @param h int + */ + protected void convertToRGBA(int[] pixels, int format, int w, int h) { + if (PGL.BIG_ENDIAN) { + switch (format) { + case ALPHA: + // Converting from xxxA into RGBA. RGB is set to white + // (0xFFFFFF, i.e.: (255, 255, 255)) + for (int i = 0; i< pixels.length; i++) { + rgbaPixels[i] = 0xFFFFFF00 | pixels[i]; + } + break; + case RGB: + // Converting xRGB into RGBA. A is set to 0xFF (255, full opacity). + for (int i = 0; i< pixels.length; i++) { + int pixel = pixels[i]; + rgbaPixels[i] = (pixel << 8) | 0xFF; + } + break; + case ARGB: + // Converting ARGB into RGBA. Shifting RGB to 8 bits to the left, + // and bringing A to the first byte. + for (int i = 0; i< pixels.length; i++) { + int pixel = pixels[i]; + rgbaPixels[i] = (pixel << 8) | ((pixel >> 24) & 0xFF); + } + break; + } + } else { + // LITTLE_ENDIAN + // ARGB native, and RGBA opengl means ABGR on windows + // for the most part just need to swap two components here + // the sun.cpu.endian here might be "false", oddly enough.. + // (that's why just using an "else", rather than check for "little") + switch (format) { + case ALPHA: + // Converting xxxA into ARGB, with RGB set to white. + for (int i = 0; i< pixels.length; i++) { + rgbaPixels[i] = (pixels[i] << 24) | 0x00FFFFFF; + } + break; + case RGB: + // We need to convert xRGB into ABGR, + // so R and B must be swapped, and the x just made 0xFF. + for (int i = 0; i< pixels.length; i++) { + int pixel = pixels[i]; + rgbaPixels[i] = 0xFF000000 | + ((pixel & 0xFF) << 16) | ((pixel & 0xFF0000) >> 16) | + (pixel & 0x0000FF00); + } + break; + case ARGB: + // We need to convert ARGB into ABGR, + // so R and B must be swapped, A and G just brought back in. + for (int i = 0; i < pixels.length; i++) { + int pixel = pixels[i]; + rgbaPixels[i] = ((pixel & 0xFF) << 16) | ((pixel & 0xFF0000) >> 16) | + (pixel & 0xFF00FF00); + } + break; + } + } + rgbaPixUpdateCount++; + } + + + /** + * Reorders an OpenGL pixel array (RGBA) into ARGB. The array must be + * of size width * height. + * @param pixels int[] + */ + protected void convertToARGB(int[] pixels) { + int t = 0; + int p = 0; + if (PGL.BIG_ENDIAN) { + // RGBA to ARGB conversion: shifting RGB 8 bits to the right, + // and placing A 24 bits to the left. + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int pixel = pixels[p++]; + pixels[t++] = (pixel >>> 8) | ((pixel << 24) & 0xFF000000); + } + } + } else { + // We have to convert ABGR into ARGB, so R and B must be swapped, + // A and G just brought back in. + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int pixel = pixels[p++]; + pixels[t++] = ((pixel & 0xFF) << 16) | ((pixel & 0xFF0000) >> 16) | + (pixel & 0xFF00FF00); + } + } + } + } + + + /////////////////////////////////////////////////////////// + + // Allocate/release texture. + + + protected void setSize(int w, int h) { + width = w; + height = h; + + if (PGraphicsOpenGL.npotTexSupported) { + glWidth = w; + glHeight = h; + } else { + glWidth = PGL.nextPowerOfTwo(w); + glHeight = PGL.nextPowerOfTwo(h); + } + + if (glWidth > PGraphicsOpenGL.maxTextureSize || + glHeight > PGraphicsOpenGL.maxTextureSize) { + glWidth = glHeight = 0; + throw new RuntimeException("Image width and height cannot be" + + " larger than " + + PGraphicsOpenGL.maxTextureSize + + " with this graphics card."); + } + + // If non-power-of-two textures are not supported, and the specified width + // or height is non-power-of-two, then glWidth (glHeight) will be greater + // than w (h) because it is chosen to be the next power of two, and this + // quotient will give the appropriate maximum texture coordinate value given + // this situation. + maxTexcoordU = (float)width / glWidth; + maxTexcoordV = (float)height / glHeight; + } + + + /** + * Allocates the opengl texture object. + */ + protected void allocate() { + dispose(); // Just in the case this object is being re-allocated. + + boolean enabledTex = false; + if (!pgl.texturingIsEnabled(glTarget)) { + pgl.enableTexturing(glTarget); + enabledTex = true; + } + + context = pgl.getCurrentContext(); + glres = new GLResourceTexture(this); + + pgl.bindTexture(glTarget, glName); + pgl.texParameteri(glTarget, PGL.TEXTURE_MIN_FILTER, glMinFilter); + pgl.texParameteri(glTarget, PGL.TEXTURE_MAG_FILTER, glMagFilter); + pgl.texParameteri(glTarget, PGL.TEXTURE_WRAP_S, glWrapS); + pgl.texParameteri(glTarget, PGL.TEXTURE_WRAP_T, glWrapT); + if (PGraphicsOpenGL.anisoSamplingSupported) { + pgl.texParameterf(glTarget, PGL.TEXTURE_MAX_ANISOTROPY, + PGraphicsOpenGL.maxAnisoAmount); + } + + // First, we use glTexImage2D to set the full size of the texture (glW/glH + // might be diff from w/h in the case that the GPU doesn't support NPOT + // textures) + pgl.texImage2D(glTarget, 0, glFormat, glWidth, glHeight, 0, + PGL.RGBA, PGL.UNSIGNED_BYTE, null); + + // Makes sure that the texture buffer in video memory doesn't contain + // any garbage. + pgl.initTexture(glTarget, PGL.RGBA, width, height); + + pgl.bindTexture(glTarget, 0); + if (enabledTex) { + pgl.disableTexturing(glTarget); + } + bound = false; + } + + + /** + * Marks the texture object for deletion. + */ + protected void dispose() { + if (glres != null) { + glres.dispose(); + glres = null; + glName = 0; + } + } + + + protected boolean contextIsOutdated() { + boolean outdated = !pgl.contextIsCurrent(context); + if (outdated) { + dispose(); + } + return outdated; + } + + + public void colorBuffer(boolean value) { + colorBuffer = value; + } + + + public boolean colorBuffer() { + return colorBuffer; + } + + + /////////////////////////////////////////////////////////// + + // Utilities. + + + // Copies source texture tex into this. + protected void copyTexture(Texture tex, int x, int y, int w, int h, + boolean scale) { + if (tex == null) { + throw new RuntimeException("Source texture is null"); + } + + if (tempFbo == null) { + tempFbo = new FrameBuffer(pg, glWidth, glHeight); + } + + // This texture is the color (destination) buffer of the FBO. + tempFbo.setColorBuffer(this); + tempFbo.disableDepthTest(); + + // FBO copy: + pg.pushFramebuffer(); + pg.setFramebuffer(tempFbo); + // Replaces anything that this texture might contain in the area being + // replaced by the new one. + pg.pushStyle(); + pg.blendMode(REPLACE); + if (scale) { + // Rendering tex into "this", and scaling the source rectangle + // to cover the entire destination region. + pgl.drawTexture(tex.glTarget, tex.glName, tex.glWidth, tex.glHeight, + 0, 0, tempFbo.width, tempFbo.height, 1, + x, y, x + w, y + h, 0, 0, width, height); + + } else { + // Rendering tex into "this" but without scaling so the contents + // of the source texture fall in the corresponding texels of the + // destination. + pgl.drawTexture(tex.glTarget, tex.glName, tex.glWidth, tex.glHeight, + 0, 0, tempFbo.width, tempFbo.height, 1, + x, y, x + w, y + h, x, y, x + w, y + h); + } + pgl.flush(); // Needed to make sure that the change in this texture is + // available immediately. + pg.popStyle(); + pg.popFramebuffer(); + updateTexels(x, y, w, h); + } + + + // Copies source texture tex into this. + protected void copyTexture(int texTarget, int texName, + int texWidth, int texHeight, + int x, int y, int w, int h, boolean scale) { + if (tempFbo == null) { + tempFbo = new FrameBuffer(pg, glWidth, glHeight); + } + + // This texture is the color (destination) buffer of the FBO. + tempFbo.setColorBuffer(this); + tempFbo.disableDepthTest(); + + // FBO copy: + pg.pushFramebuffer(); + pg.setFramebuffer(tempFbo); + // Replaces anything that this texture might contain in the area being + // replaced by the new one. + pg.pushStyle(); + pg.blendMode(REPLACE); + if (scale) { + // Rendering tex into "this", and scaling the source rectangle + // to cover the entire destination region. + pgl.drawTexture(texTarget, texName, texWidth, texHeight, + 0, 0, tempFbo.width, tempFbo.height, + x, y, w, h, 0, 0, width, height); + + } else { + // Rendering tex into "this" but without scaling so the contents + // of the source texture fall in the corresponding texels of the + // destination. + pgl.drawTexture(texTarget, texName, texWidth, texHeight, + 0, 0, tempFbo.width, tempFbo.height, + x, y, w, h, x, y, w, h); + } + pgl.flush(); // Needed to make sure that the change in this texture is + // available immediately. + pg.popStyle(); + pg.popFramebuffer(); + updateTexels(x, y, w, h); + } + + + protected void copyObject(Texture src) { + // The OpenGL texture of this object is replaced with the one from the + // source object, so we delete the former to avoid resource wasting. + dispose(); + + width = src.width; + height = src.height; + + glName = src.glName; + glTarget = src.glTarget; + glFormat = src.glFormat; + glMinFilter = src.glMinFilter; + glMagFilter = src.glMagFilter; + + glWidth= src.glWidth; + glHeight = src.glHeight; + + usingMipmaps = src.usingMipmaps; + usingRepeat = src.usingRepeat; + maxTexcoordU = src.maxTexcoordU; + maxTexcoordV = src.maxTexcoordV; + + invertedX = src.invertedX; + invertedY = src.invertedY; + } + + + // Releases the memory used by pixelBuffer either if the buffer hasn't been + // used many times yet, or if the JVM is running low in free memory. + protected void releasePixelBuffer() { + double freeMB = Runtime.getRuntime().freeMemory() / 1E6; + if (pixBufUpdateCount < MAX_UPDATES || freeMB < MIN_MEMORY) { + pixelBuffer = null; + } + } + + + // Releases the memory used by rgbaPixels either if the array hasn't been + // used many times yet, or if the JVM is running low in free memory. + protected void releaseRGBAPixels() { + double freeMB = Runtime.getRuntime().freeMemory() / 1E6; + if (rgbaPixUpdateCount < MAX_UPDATES || freeMB < MIN_MEMORY) { + rgbaPixels = null; + } + } + + + /////////////////////////////////////////////////////////// + + // Parameter handling + + + public Parameters getParameters() { + Parameters res = new Parameters(); + + if (glTarget == PGL.TEXTURE_2D) { + res.target = TEX2D; + } + + if (glFormat == PGL.RGB) { + res.format = RGB; + } else if (glFormat == PGL.RGBA) { + res.format = ARGB; + } else if (glFormat == PGL.ALPHA) { + res.format = ALPHA; + } + + if (glMagFilter == PGL.NEAREST && glMinFilter == PGL.NEAREST) { + res.sampling = POINT; + res.mipmaps = false; + } else if (glMagFilter == PGL.NEAREST && glMinFilter == PGL.LINEAR) { + res.sampling = LINEAR; + res.mipmaps = false; + } else if (glMagFilter == PGL.NEAREST && + glMinFilter == PGL.LINEAR_MIPMAP_NEAREST) { + res.sampling = LINEAR; + res.mipmaps = true; + } else if (glMagFilter == PGL.LINEAR && glMinFilter == PGL.LINEAR) { + res.sampling = BILINEAR; + res.mipmaps = false; + } else if (glMagFilter == PGL.LINEAR && + glMinFilter == PGL.LINEAR_MIPMAP_NEAREST) { + res.sampling = BILINEAR; + res.mipmaps = true; + } else if (glMagFilter == PGL.LINEAR && + glMinFilter == PGL.LINEAR_MIPMAP_LINEAR) { + res.sampling = TRILINEAR; + res.mipmaps = true; + } + + if (glWrapS == PGL.CLAMP_TO_EDGE) { + res.wrapU = CLAMP; + } else if (glWrapS == PGL.REPEAT) { + res.wrapU = REPEAT; + } + + if (glWrapT == PGL.CLAMP_TO_EDGE) { + res.wrapV = CLAMP; + } else if (glWrapT == PGL.REPEAT) { + res.wrapV = REPEAT; + } + + return res; + } + + + /** + * Sets texture target and internal format according to the target and + * type specified. + * @param target int + * @param params GLTextureParameters + */ + protected void setParameters(Parameters params) { + if (params.target == TEX2D) { + glTarget = PGL.TEXTURE_2D; + } else { + throw new RuntimeException("Unknown texture target"); + } + + if (params.format == RGB) { + glFormat = PGL.RGB; + } else if (params.format == ARGB) { + glFormat = PGL.RGBA; + } else if (params.format == ALPHA) { + glFormat = PGL.ALPHA; + } else { + throw new RuntimeException("Unknown texture format"); + } + + boolean mipmaps = params.mipmaps && PGL.MIPMAPS_ENABLED; + if (mipmaps && !PGraphicsOpenGL.autoMipmapGenSupported) { + PGraphics.showWarning("Mipmaps were requested but automatic mipmap " + + "generation is not supported and manual " + + "generation still not implemented, so mipmaps " + + "will be disabled."); + mipmaps = false; + } + + if (params.sampling == POINT) { + glMagFilter = PGL.NEAREST; + glMinFilter = PGL.NEAREST; + } else if (params.sampling == LINEAR) { + glMagFilter = PGL.NEAREST; + glMinFilter = mipmaps ? PGL.LINEAR_MIPMAP_NEAREST : PGL.LINEAR; + } else if (params.sampling == BILINEAR) { + glMagFilter = PGL.LINEAR; + glMinFilter = mipmaps ? PGL.LINEAR_MIPMAP_NEAREST : PGL.LINEAR; + } else if (params.sampling == TRILINEAR) { + glMagFilter = PGL.LINEAR; + glMinFilter = mipmaps ? PGL.LINEAR_MIPMAP_LINEAR : PGL.LINEAR; + } else { + throw new RuntimeException("Unknown texture filtering mode"); + } + + if (params.wrapU == CLAMP) { + glWrapS = PGL.CLAMP_TO_EDGE; + } else if (params.wrapU == REPEAT) { + glWrapS = PGL.REPEAT; + } else { + throw new RuntimeException("Unknown wrapping mode"); + } + + if (params.wrapV == CLAMP) { + glWrapT = PGL.CLAMP_TO_EDGE; + } else if (params.wrapV == REPEAT) { + glWrapT = PGL.REPEAT; + } else { + throw new RuntimeException("Unknown wrapping mode"); + } + + usingMipmaps = glMinFilter == PGL.LINEAR_MIPMAP_NEAREST || + glMinFilter == PGL.LINEAR_MIPMAP_LINEAR; + + usingRepeat = glWrapS == PGL.REPEAT || glWrapT == PGL.REPEAT; + + invertedX = false; + invertedY = false; + } + + + protected void fillEdges(int x, int y, int w, int h) { + if ((width < glWidth || height < glHeight) && (x + w == width || y + h == height)) { + if (x + w == width) { + int ew = glWidth - width; + edgePixels = new int[h * ew]; + for (int i = 0; i < h; i++) { + int c = rgbaPixels[i * w + (w - 1)]; + Arrays.fill(edgePixels, i * ew, (i + 1) * ew, c); + } + edgeBuffer = PGL.updateIntBuffer(edgeBuffer, edgePixels, true); + pgl.texSubImage2D(glTarget, 0, width, y, ew, h, PGL.RGBA, + PGL.UNSIGNED_BYTE, edgeBuffer); + } + + if (y + h == height) { + int eh = glHeight - height; + edgePixels = new int[eh * w]; + for (int i = 0; i < eh; i++) { + System.arraycopy(rgbaPixels, (h - 1) * w, edgePixels, i * w, w); + } + edgeBuffer = PGL.updateIntBuffer(edgeBuffer, edgePixels, true); + pgl.texSubImage2D(glTarget, 0, x, height, w, eh, PGL.RGBA, + PGL.UNSIGNED_BYTE, edgeBuffer); + } + + if (x + w == width && y + h == height) { + int ew = glWidth - width; + int eh = glHeight - height; + int c = rgbaPixels[w * h - 1]; + edgePixels = new int[eh * ew]; + Arrays.fill(edgePixels, 0, eh * ew, c); + edgeBuffer = PGL.updateIntBuffer(edgeBuffer, edgePixels, true); + pgl.texSubImage2D(glTarget, 0, width, height, ew, eh, PGL.RGBA, + PGL.UNSIGNED_BYTE, edgeBuffer); + } + } + } + + /////////////////////////////////////////////////////////////////////////// + + // Parameters object + + + /** + * This class stores the parameters for a texture: target, internal format, + * minimization filter and magnification filter. + */ + static public class Parameters { + /** + * Texture target. + */ + public int target; + + /** + * Texture internal format. + */ + public int format; + + /** + * Texture filtering (POINT, LINEAR, BILINEAR or TRILINEAR). + */ + public int sampling; + + /** + * Use mipmaps or not. + */ + public boolean mipmaps; + + /** + * Wrapping mode along U. + */ + public int wrapU; + + /** + * Wrapping mode along V. + */ + public int wrapV; + + /** + * Sets all the parameters to default values. + */ + public Parameters() { + this.target = TEX2D; + this.format = ARGB; + this.sampling = BILINEAR; + this.mipmaps = true; + this.wrapU = CLAMP; + this.wrapV = CLAMP; + } + + public Parameters(int format) { + this.target = TEX2D; + this.format = format; + this.sampling = BILINEAR; + this.mipmaps = true; + this.wrapU = CLAMP; + this.wrapV = CLAMP; + } + + public Parameters(int format, int sampling) { + this.target = TEX2D; + this.format = format; + this.sampling = sampling; + this.mipmaps = true; + this.wrapU = CLAMP; + this.wrapV = CLAMP; + } + + public Parameters(int format, int sampling, boolean mipmaps) { + this.target = TEX2D; + this.format = format; + this.mipmaps = mipmaps; + if (sampling == TRILINEAR && !mipmaps) { + this.sampling = BILINEAR; + } else { + this.sampling = sampling; + } + this.wrapU = CLAMP; + this.wrapV = CLAMP; + } + + public Parameters(int format, int sampling, boolean mipmaps, int wrap) { + this.target = TEX2D; + this.format = format; + this.mipmaps = mipmaps; + if (sampling == TRILINEAR && !mipmaps) { + this.sampling = BILINEAR; + } else { + this.sampling = sampling; + } + this.wrapU = wrap; + this.wrapV = wrap; + } + + public Parameters(Parameters src) { + set(src); + } + + public void set(int format) { + this.format = format; + } + + public void set(int format, int sampling) { + this.format = format; + this.sampling = sampling; + } + + public void set(int format, int sampling, boolean mipmaps) { + this.format = format; + this.sampling = sampling; + this.mipmaps = mipmaps; + } + + public void set(Parameters src) { + this.target = src.target; + this.format = src.format; + this.sampling = src.sampling; + this.mipmaps = src.mipmaps; + this.wrapU = src.wrapU; + this.wrapV = src.wrapV; + } + } + + /** + * This class stores a buffer copied from the buffer source. + * + */ + protected class BufferData { + int w, h; + // Native buffer object. + Object natBuf; + // Buffer viewed as int. + IntBuffer rgbBuf; + + BufferData(Object nat, IntBuffer rgb, int w, int h) { + natBuf = nat; + rgbBuf = rgb; + this.w = w; + this.h = h; + } + + void dispose() { + try { + // Disposing the native buffer. + disposeBufferMethod.invoke(bufferSource, new Object[] { natBuf }); + natBuf = null; + rgbBuf = null; + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/processing/opengl/VertexBuffer.java b/src/main/java/processing/opengl/VertexBuffer.java new file mode 100644 index 0000000..6315ba0 --- /dev/null +++ b/src/main/java/processing/opengl/VertexBuffer.java @@ -0,0 +1,88 @@ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ + +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +package processing.opengl; + +import processing.opengl.PGraphicsOpenGL.GLResourceVertexBuffer; + +// TODO: need to combine with PGraphicsOpenGL.VertexAttribute +public class VertexBuffer { + static protected final int INIT_VERTEX_BUFFER_SIZE = 256; + static protected final int INIT_INDEX_BUFFER_SIZE = 512; + + public int glId; + int target; + int elementSize; + int ncoords; + boolean index; + + protected PGL pgl; // The interface between Processing and OpenGL. + protected int context; // The context that created this texture. + private GLResourceVertexBuffer glres; + + VertexBuffer(PGraphicsOpenGL pg, int target, int ncoords, int esize) { + this(pg, target, ncoords, esize, false); + } + + VertexBuffer(PGraphicsOpenGL pg, int target, int ncoords, int esize, boolean index) { + pgl = pg.pgl; + context = pgl.createEmptyContext(); + + this.target = target; + this.ncoords = ncoords; + this.elementSize = esize; + this.index = index; + create(); + init(); + } + + protected void create() { + context = pgl.getCurrentContext(); + glres = new GLResourceVertexBuffer(this); + } + + protected void init() { + int size = index ? ncoords * INIT_INDEX_BUFFER_SIZE * elementSize : + ncoords * INIT_VERTEX_BUFFER_SIZE * elementSize; + pgl.bindBuffer(target, glId); + pgl.bufferData(target, size, null, PGL.STATIC_DRAW); + } + + protected void dispose() { + if (glres != null) { + glres.dispose(); + glId = 0; + glres = null; + } + } + + protected boolean contextIsOutdated() { + boolean outdated = !pgl.contextIsCurrent(context); + if (outdated) { + dispose(); + } + return outdated; + } + +} diff --git a/src/main/java/processing/opengl/cursors/arrow.png b/src/main/java/processing/opengl/cursors/arrow.png new file mode 100644 index 0000000..c727ed4 Binary files /dev/null and b/src/main/java/processing/opengl/cursors/arrow.png differ diff --git a/src/main/java/processing/opengl/cursors/cross.png b/src/main/java/processing/opengl/cursors/cross.png new file mode 100644 index 0000000..58d96ba Binary files /dev/null and b/src/main/java/processing/opengl/cursors/cross.png differ diff --git a/src/main/java/processing/opengl/cursors/hand.png b/src/main/java/processing/opengl/cursors/hand.png new file mode 100644 index 0000000..a291c5c Binary files /dev/null and b/src/main/java/processing/opengl/cursors/hand.png differ diff --git a/src/main/java/processing/opengl/cursors/license.txt b/src/main/java/processing/opengl/cursors/license.txt new file mode 100644 index 0000000..e022dc9 --- /dev/null +++ b/src/main/java/processing/opengl/cursors/license.txt @@ -0,0 +1,27 @@ +These files are based on the DMZ Cursors package, +used with permission from Jakub Steiner +11 September 2015 + + +The MIT License (MIT) + +Copyright (c) 2006 Jakub Steiner +Copyright (c) 2006 Novell, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/main/java/processing/opengl/cursors/move.png b/src/main/java/processing/opengl/cursors/move.png new file mode 100644 index 0000000..e8060df Binary files /dev/null and b/src/main/java/processing/opengl/cursors/move.png differ diff --git a/src/main/java/processing/opengl/cursors/text.png b/src/main/java/processing/opengl/cursors/text.png new file mode 100644 index 0000000..56965c3 Binary files /dev/null and b/src/main/java/processing/opengl/cursors/text.png differ diff --git a/src/main/java/processing/opengl/cursors/wait.png b/src/main/java/processing/opengl/cursors/wait.png new file mode 100644 index 0000000..82638fb Binary files /dev/null and b/src/main/java/processing/opengl/cursors/wait.png differ diff --git a/src/main/java/processing/opengl/shaders/ColorFrag.glsl b/src/main/java/processing/opengl/shaders/ColorFrag.glsl new file mode 100644 index 0000000..59adfda --- /dev/null +++ b/src/main/java/processing/opengl/shaders/ColorFrag.glsl @@ -0,0 +1,32 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +#ifdef GL_ES +precision mediump float; +precision mediump int; +#endif + +varying vec4 vertColor; + +void main() { + gl_FragColor = vertColor; +} \ No newline at end of file diff --git a/src/main/java/processing/opengl/shaders/ColorVert.glsl b/src/main/java/processing/opengl/shaders/ColorVert.glsl new file mode 100644 index 0000000..6e8820c --- /dev/null +++ b/src/main/java/processing/opengl/shaders/ColorVert.glsl @@ -0,0 +1,34 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +uniform mat4 transformMatrix; + +attribute vec4 position; +attribute vec4 color; + +varying vec4 vertColor; + +void main() { + gl_Position = transformMatrix * position; + + vertColor = color; +} \ No newline at end of file diff --git a/src/main/java/processing/opengl/shaders/LightFrag.glsl b/src/main/java/processing/opengl/shaders/LightFrag.glsl new file mode 100644 index 0000000..b9cd447 --- /dev/null +++ b/src/main/java/processing/opengl/shaders/LightFrag.glsl @@ -0,0 +1,33 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +#ifdef GL_ES +precision mediump float; +precision mediump int; +#endif + +varying vec4 vertColor; +varying vec4 backVertColor; + +void main() { + gl_FragColor = gl_FrontFacing ? vertColor : backVertColor; +} \ No newline at end of file diff --git a/src/main/java/processing/opengl/shaders/LightVert-vc4.glsl b/src/main/java/processing/opengl/shaders/LightVert-vc4.glsl new file mode 100644 index 0000000..b96caa4 --- /dev/null +++ b/src/main/java/processing/opengl/shaders/LightVert-vc4.glsl @@ -0,0 +1,154 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +uniform mat4 modelviewMatrix; +uniform mat4 transformMatrix; +uniform mat3 normalMatrix; + +uniform int lightCount; +uniform vec4 lightPosition[8]; +uniform vec3 lightNormal[8]; +uniform vec3 lightAmbient[8]; +uniform vec3 lightDiffuse[8]; +uniform vec3 lightSpecular[8]; +uniform vec3 lightFalloff[8]; +uniform vec2 lightSpot[8]; + +attribute vec4 position; +attribute vec4 color; +attribute vec3 normal; + +attribute vec4 ambient; +attribute vec4 specular; +attribute vec4 emissive; +attribute float shininess; + +varying vec4 vertColor; +varying vec4 backVertColor; + +const float zero_float = 0.0; +const float one_float = 1.0; +const vec3 zero_vec3 = vec3(0.0); +const vec3 minus_one_vec3 = vec3(0.0-1.0); + +float falloffFactor(vec3 lightPos, vec3 vertPos, vec3 coeff) { + vec3 lpv = lightPos - vertPos; + vec3 dist = vec3(one_float); + dist.z = dot(lpv, lpv); + dist.y = sqrt(dist.z); + return one_float / dot(dist, coeff); +} + +float spotFactor(vec3 lightPos, vec3 vertPos, vec3 lightNorm, float minCos, float spotExp) { + vec3 lpv = normalize(lightPos - vertPos); + vec3 nln = minus_one_vec3 * lightNorm; + float spotCos = dot(nln, lpv); + return spotCos <= minCos ? zero_float : pow(spotCos, spotExp); +} + +float lambertFactor(vec3 lightDir, vec3 vecNormal) { + return max(zero_float, dot(lightDir, vecNormal)); +} + +float blinnPhongFactor(vec3 lightDir, vec3 vertPos, vec3 vecNormal, float shine) { + vec3 np = normalize(vertPos); + vec3 ldp = normalize(lightDir - np); + return pow(max(zero_float, dot(ldp, vecNormal)), shine); +} + +void main() { + // Vertex in clip coordinates + gl_Position = transformMatrix * position; + + // Vertex in eye coordinates + vec3 ecVertex = vec3(modelviewMatrix * position); + + // Normal vector in eye coordinates + vec3 ecNormal = normalize(normalMatrix * normal); + vec3 ecNormalInv = ecNormal * minus_one_vec3; + + // Light calculations + vec3 totalAmbient = vec3(0, 0, 0); + + vec3 totalFrontDiffuse = vec3(0, 0, 0); + vec3 totalFrontSpecular = vec3(0, 0, 0); + + vec3 totalBackDiffuse = vec3(0, 0, 0); + vec3 totalBackSpecular = vec3(0, 0, 0); + + // prevent register allocation failure by limiting ourselves to + // two lights for now + for (int i = 0; i < 2; i++) { + if (lightCount == i) break; + + vec3 lightPos = lightPosition[i].xyz; + bool isDir = lightPosition[i].w < one_float; + float spotCos = lightSpot[i].x; + float spotExp = lightSpot[i].y; + + vec3 lightDir; + float falloff; + float spotf; + + if (isDir) { + falloff = one_float; + lightDir = minus_one_vec3 * lightNormal[i]; + } else { + falloff = falloffFactor(lightPos, ecVertex, lightFalloff[i]); + lightDir = normalize(lightPos - ecVertex); + } + + spotf = spotExp > zero_float ? spotFactor(lightPos, ecVertex, lightNormal[i], + spotCos, spotExp) + : one_float; + + if (any(greaterThan(lightAmbient[i], zero_vec3))) { + totalAmbient += lightAmbient[i] * falloff; + } + + if (any(greaterThan(lightDiffuse[i], zero_vec3))) { + totalFrontDiffuse += lightDiffuse[i] * falloff * spotf * + lambertFactor(lightDir, ecNormal); + totalBackDiffuse += lightDiffuse[i] * falloff * spotf * + lambertFactor(lightDir, ecNormalInv); + } + + if (any(greaterThan(lightSpecular[i], zero_vec3))) { + totalFrontSpecular += lightSpecular[i] * falloff * spotf * + blinnPhongFactor(lightDir, ecVertex, ecNormal, shininess); + totalBackSpecular += lightSpecular[i] * falloff * spotf * + blinnPhongFactor(lightDir, ecVertex, ecNormalInv, shininess); + } + } + + // Calculating final color as result of all lights (plus emissive term). + // Transparency is determined exclusively by the diffuse component. + vertColor = vec4(totalAmbient, 0) * ambient + + vec4(totalFrontDiffuse, 1) * color + + vec4(totalFrontSpecular, 0) * specular + + vec4(emissive.rgb, 0); + + backVertColor = vec4(totalAmbient, 0) * ambient + + vec4(totalBackDiffuse, 1) * color + + vec4(totalBackSpecular, 0) * specular + + vec4(emissive.rgb, 0); +} \ No newline at end of file diff --git a/src/main/java/processing/opengl/shaders/LightVert.glsl b/src/main/java/processing/opengl/shaders/LightVert.glsl new file mode 100644 index 0000000..470fd4b --- /dev/null +++ b/src/main/java/processing/opengl/shaders/LightVert.glsl @@ -0,0 +1,151 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +uniform mat4 modelviewMatrix; +uniform mat4 transformMatrix; +uniform mat3 normalMatrix; + +uniform int lightCount; +uniform vec4 lightPosition[8]; +uniform vec3 lightNormal[8]; +uniform vec3 lightAmbient[8]; +uniform vec3 lightDiffuse[8]; +uniform vec3 lightSpecular[8]; +uniform vec3 lightFalloff[8]; +uniform vec2 lightSpot[8]; + +attribute vec4 position; +attribute vec4 color; +attribute vec3 normal; + +attribute vec4 ambient; +attribute vec4 specular; +attribute vec4 emissive; +attribute float shininess; + +varying vec4 vertColor; +varying vec4 backVertColor; + +const float zero_float = 0.0; +const float one_float = 1.0; +const vec3 zero_vec3 = vec3(0); + +float falloffFactor(vec3 lightPos, vec3 vertPos, vec3 coeff) { + vec3 lpv = lightPos - vertPos; + vec3 dist = vec3(one_float); + dist.z = dot(lpv, lpv); + dist.y = sqrt(dist.z); + return one_float / dot(dist, coeff); +} + +float spotFactor(vec3 lightPos, vec3 vertPos, vec3 lightNorm, float minCos, float spotExp) { + vec3 lpv = normalize(lightPos - vertPos); + vec3 nln = -one_float * lightNorm; + float spotCos = dot(nln, lpv); + return spotCos <= minCos ? zero_float : pow(spotCos, spotExp); +} + +float lambertFactor(vec3 lightDir, vec3 vecNormal) { + return max(zero_float, dot(lightDir, vecNormal)); +} + +float blinnPhongFactor(vec3 lightDir, vec3 vertPos, vec3 vecNormal, float shine) { + vec3 np = normalize(vertPos); + vec3 ldp = normalize(lightDir - np); + return pow(max(zero_float, dot(ldp, vecNormal)), shine); +} + +void main() { + // Vertex in clip coordinates + gl_Position = transformMatrix * position; + + // Vertex in eye coordinates + vec3 ecVertex = vec3(modelviewMatrix * position); + + // Normal vector in eye coordinates + vec3 ecNormal = normalize(normalMatrix * normal); + vec3 ecNormalInv = ecNormal * -one_float; + + // Light calculations + vec3 totalAmbient = vec3(0, 0, 0); + + vec3 totalFrontDiffuse = vec3(0, 0, 0); + vec3 totalFrontSpecular = vec3(0, 0, 0); + + vec3 totalBackDiffuse = vec3(0, 0, 0); + vec3 totalBackSpecular = vec3(0, 0, 0); + + for (int i = 0; i < 8; i++) { + if (lightCount == i) break; + + vec3 lightPos = lightPosition[i].xyz; + bool isDir = lightPosition[i].w < one_float; + float spotCos = lightSpot[i].x; + float spotExp = lightSpot[i].y; + + vec3 lightDir; + float falloff; + float spotf; + + if (isDir) { + falloff = one_float; + lightDir = -one_float * lightNormal[i]; + } else { + falloff = falloffFactor(lightPos, ecVertex, lightFalloff[i]); + lightDir = normalize(lightPos - ecVertex); + } + + spotf = spotExp > zero_float ? spotFactor(lightPos, ecVertex, lightNormal[i], + spotCos, spotExp) + : one_float; + + if (any(greaterThan(lightAmbient[i], zero_vec3))) { + totalAmbient += lightAmbient[i] * falloff; + } + + if (any(greaterThan(lightDiffuse[i], zero_vec3))) { + totalFrontDiffuse += lightDiffuse[i] * falloff * spotf * + lambertFactor(lightDir, ecNormal); + totalBackDiffuse += lightDiffuse[i] * falloff * spotf * + lambertFactor(lightDir, ecNormalInv); + } + + if (any(greaterThan(lightSpecular[i], zero_vec3))) { + totalFrontSpecular += lightSpecular[i] * falloff * spotf * + blinnPhongFactor(lightDir, ecVertex, ecNormal, shininess); + totalBackSpecular += lightSpecular[i] * falloff * spotf * + blinnPhongFactor(lightDir, ecVertex, ecNormalInv, shininess); + } + } + + // Calculating final color as result of all lights (plus emissive term). + // Transparency is determined exclusively by the diffuse component. + vertColor = vec4(totalAmbient, 0) * ambient + + vec4(totalFrontDiffuse, 1) * color + + vec4(totalFrontSpecular, 0) * specular + + vec4(emissive.rgb, 0); + + backVertColor = vec4(totalAmbient, 0) * ambient + + vec4(totalBackDiffuse, 1) * color + + vec4(totalBackSpecular, 0) * specular + + vec4(emissive.rgb, 0); +} \ No newline at end of file diff --git a/src/main/java/processing/opengl/shaders/LineFrag.glsl b/src/main/java/processing/opengl/shaders/LineFrag.glsl new file mode 100644 index 0000000..09fffcb --- /dev/null +++ b/src/main/java/processing/opengl/shaders/LineFrag.glsl @@ -0,0 +1,32 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +#ifdef GL_ES +precision mediump float; +precision mediump int; +#endif + +varying vec4 vertColor; + +void main() { + gl_FragColor = vertColor; +} \ No newline at end of file diff --git a/src/main/java/processing/opengl/shaders/LineVert.glsl b/src/main/java/processing/opengl/shaders/LineVert.glsl new file mode 100644 index 0000000..50946ba --- /dev/null +++ b/src/main/java/processing/opengl/shaders/LineVert.glsl @@ -0,0 +1,100 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +#define PROCESSING_LINE_SHADER + +uniform mat4 modelviewMatrix; +uniform mat4 projectionMatrix; + +uniform vec4 viewport; +uniform int perspective; +uniform vec3 scale; + +attribute vec4 position; +attribute vec4 color; +attribute vec4 direction; + +varying vec4 vertColor; + +void main() { + vec4 posp = modelviewMatrix * position; + vec4 posq = modelviewMatrix * (position + vec4(direction.xyz, 0)); + + // Moving vertices slightly toward the camera + // to avoid depth-fighting with the fill triangles. + // Discussed here: + // http://www.opengl.org/discussion_boards/ubbthreads.php?ubb=showflat&Number=252848 + posp.xyz = posp.xyz * scale; + posq.xyz = posq.xyz * scale; + + vec4 p = projectionMatrix * posp; + vec4 q = projectionMatrix * posq; + + // formula to convert from clip space (range -1..1) to screen space (range 0..[width or height]) + // screen_p = (p.xy/p.w + <1,1>) * 0.5 * viewport.zw + + // prevent division by W by transforming the tangent formula (div by 0 causes + // the line to disappear, see https://github.com/processing/processing/issues/5183) + // t = screen_q - screen_p + // + // tangent is normalized and we don't care which direction it points to (+-) + // t = +- normalize( screen_q - screen_p ) + // t = +- normalize( (q.xy/q.w+<1,1>)*0.5*viewport.zw - (p.xy/p.w+<1,1>)*0.5*viewport.zw ) + // + // extract common factor, <1,1> - <1,1> cancels out + // t = +- normalize( (q.xy/q.w - p.xy/p.w) * 0.5 * viewport.zw ) + // + // convert to common divisor + // t = +- normalize( ((q.xy*p.w - p.xy*q.w) / (p.w*q.w)) * 0.5 * viewport.zw ) + // + // remove the common scalar divisor/factor, not needed due to normalize and +- + // (keep viewport - can't remove because it has different components for x and y + // and corrects for aspect ratio, see https://github.com/processing/processing/issues/5181) + // t = +- normalize( (q.xy*p.w - p.xy*q.w) * viewport.zw ) + + vec2 tangent = (q.xy*p.w - p.xy*q.w) * viewport.zw; + + // don't normalize zero vector (line join triangles and lines perpendicular to the eye plane) + tangent = length(tangent) == 0.0 ? vec2(0.0, 0.0) : normalize(tangent); + + // flip tangent to normal (it's already normalized) + vec2 normal = vec2(-tangent.y, tangent.x); + + float thickness = direction.w; + vec2 offset = normal * thickness; + + // Perspective --- + // convert from world to clip by multiplying with projection scaling factor + // to get the right thickness (see https://github.com/processing/processing/issues/5182) + // invert Y, projections in Processing invert Y + vec2 perspScale = (projectionMatrix * vec4(1, -1, 0, 0)).xy; + + // No Perspective --- + // multiply by W (to cancel out division by W later in the pipeline) and + // convert from screen to clip (derived from clip to screen above) + vec2 noPerspScale = p.w / (0.5 * viewport.zw); + + gl_Position.xy = p.xy + offset.xy * mix(noPerspScale, perspScale, float(perspective > 0)); + gl_Position.zw = p.zw; + + vertColor = color; +} diff --git a/src/main/java/processing/opengl/shaders/MaskFrag.glsl b/src/main/java/processing/opengl/shaders/MaskFrag.glsl new file mode 100644 index 0000000..74fad83 --- /dev/null +++ b/src/main/java/processing/opengl/shaders/MaskFrag.glsl @@ -0,0 +1,40 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +#ifdef GL_ES +precision mediump float; +precision mediump int; +#endif + +#define PROCESSING_TEXTURE_SHADER + +uniform sampler2D texture; +uniform sampler2D mask; + +varying vec4 vertTexCoord; + +void main() { + vec3 texColor = texture2D(texture, vertTexCoord.st).rgb; + vec3 maskColor = texture2D(mask, vertTexCoord.st).rgb; + float luminance = dot(maskColor, vec3(0.2126, 0.7152, 0.0722)); + gl_FragColor = vec4(texColor, luminance); +} \ No newline at end of file diff --git a/src/main/java/processing/opengl/shaders/PointFrag.glsl b/src/main/java/processing/opengl/shaders/PointFrag.glsl new file mode 100644 index 0000000..59adfda --- /dev/null +++ b/src/main/java/processing/opengl/shaders/PointFrag.glsl @@ -0,0 +1,32 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +#ifdef GL_ES +precision mediump float; +precision mediump int; +#endif + +varying vec4 vertColor; + +void main() { + gl_FragColor = vertColor; +} \ No newline at end of file diff --git a/src/main/java/processing/opengl/shaders/PointVert.glsl b/src/main/java/processing/opengl/shaders/PointVert.glsl new file mode 100644 index 0000000..8249e90 --- /dev/null +++ b/src/main/java/processing/opengl/shaders/PointVert.glsl @@ -0,0 +1,56 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +uniform mat4 projectionMatrix; +uniform mat4 modelviewMatrix; + +uniform vec4 viewport; +uniform int perspective; + +attribute vec4 position; +attribute vec4 color; +attribute vec2 offset; + +varying vec4 vertColor; + +void main() { + vec4 pos = modelviewMatrix * position; + vec4 clip = projectionMatrix * pos; + + // Perspective --- + // convert from world to clip by multiplying with projection scaling factor + // invert Y, projections in Processing invert Y + vec2 perspScale = (projectionMatrix * vec4(1, -1, 0, 0)).xy; + + // formula to convert from clip space (range -1..1) to screen space (range 0..[width or height]) + // screen_p = (p.xy/p.w + <1,1>) * 0.5 * viewport.zw + + // No Perspective --- + // multiply by W (to cancel out division by W later in the pipeline) and + // convert from screen to clip (derived from clip to screen above) + vec2 noPerspScale = clip.w / (0.5 * viewport.zw); + + gl_Position.xy = clip.xy + offset.xy * mix(noPerspScale, perspScale, float(perspective > 0)); + gl_Position.zw = clip.zw; + + vertColor = color; +} diff --git a/src/main/java/processing/opengl/shaders/TexFrag.glsl b/src/main/java/processing/opengl/shaders/TexFrag.glsl new file mode 100644 index 0000000..5e236ee --- /dev/null +++ b/src/main/java/processing/opengl/shaders/TexFrag.glsl @@ -0,0 +1,37 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +#ifdef GL_ES +precision mediump float; +precision mediump int; +#endif + +uniform sampler2D texture; + +uniform vec2 texOffset; + +varying vec4 vertColor; +varying vec4 vertTexCoord; + +void main() { + gl_FragColor = texture2D(texture, vertTexCoord.st) * vertColor; +} \ No newline at end of file diff --git a/src/main/java/processing/opengl/shaders/TexLightFrag.glsl b/src/main/java/processing/opengl/shaders/TexLightFrag.glsl new file mode 100644 index 0000000..1e9cc7e --- /dev/null +++ b/src/main/java/processing/opengl/shaders/TexLightFrag.glsl @@ -0,0 +1,37 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ +#ifdef GL_ES +precision mediump float; +precision mediump int; +#endif + +uniform sampler2D texture; + +uniform vec2 texOffset; + +varying vec4 vertColor; +varying vec4 backVertColor; +varying vec4 vertTexCoord; + +void main() { + gl_FragColor = texture2D(texture, vertTexCoord.st) * (gl_FrontFacing ? vertColor : backVertColor); +} \ No newline at end of file diff --git a/src/main/java/processing/opengl/shaders/TexLightVert-vc4.glsl b/src/main/java/processing/opengl/shaders/TexLightVert-vc4.glsl new file mode 100644 index 0000000..51e88ab --- /dev/null +++ b/src/main/java/processing/opengl/shaders/TexLightVert-vc4.glsl @@ -0,0 +1,160 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +uniform mat4 modelviewMatrix; +uniform mat4 transformMatrix; +uniform mat3 normalMatrix; +uniform mat4 texMatrix; + +uniform int lightCount; +uniform vec4 lightPosition[8]; +uniform vec3 lightNormal[8]; +uniform vec3 lightAmbient[8]; +uniform vec3 lightDiffuse[8]; +uniform vec3 lightSpecular[8]; +uniform vec3 lightFalloff[8]; +uniform vec2 lightSpot[8]; + +attribute vec4 position; +attribute vec4 color; +attribute vec3 normal; +attribute vec2 texCoord; + +attribute vec4 ambient; +attribute vec4 specular; +attribute vec4 emissive; +attribute float shininess; + +varying vec4 vertColor; +varying vec4 backVertColor; +varying vec4 vertTexCoord; + +const float zero_float = 0.0; +const float one_float = 1.0; +const vec3 zero_vec3 = vec3(0.0); +const vec3 minus_one_vec3 = vec3(0.0-1.0); + +float falloffFactor(vec3 lightPos, vec3 vertPos, vec3 coeff) { + vec3 lpv = lightPos - vertPos; + vec3 dist = vec3(one_float); + dist.z = dot(lpv, lpv); + dist.y = sqrt(dist.z); + return one_float / dot(dist, coeff); +} + +float spotFactor(vec3 lightPos, vec3 vertPos, vec3 lightNorm, float minCos, float spotExp) { + vec3 lpv = normalize(lightPos - vertPos); + vec3 nln = minus_one_vec3 * lightNorm; + float spotCos = dot(nln, lpv); + return spotCos <= minCos ? zero_float : pow(spotCos, spotExp); +} + +float lambertFactor(vec3 lightDir, vec3 vecNormal) { + return max(zero_float, dot(lightDir, vecNormal)); +} + +float blinnPhongFactor(vec3 lightDir, vec3 vertPos, vec3 vecNormal, float shine) { + vec3 np = normalize(vertPos); + vec3 ldp = normalize(lightDir - np); + return pow(max(zero_float, dot(ldp, vecNormal)), shine); +} + +void main() { + // Vertex in clip coordinates + gl_Position = transformMatrix * position; + + // Vertex in eye coordinates + vec3 ecVertex = vec3(modelviewMatrix * position); + + // Normal vector in eye coordinates + vec3 ecNormal = normalize(normalMatrix * normal); + vec3 ecNormalInv = ecNormal * minus_one_vec3; + + // Light calculations + vec3 totalAmbient = vec3(0, 0, 0); + + vec3 totalFrontDiffuse = vec3(0, 0, 0); + vec3 totalFrontSpecular = vec3(0, 0, 0); + + vec3 totalBackDiffuse = vec3(0, 0, 0); + vec3 totalBackSpecular = vec3(0, 0, 0); + + // prevent register allocation failure by limiting ourselves to + // two lights for now + for (int i = 0; i < 2; i++) { + if (lightCount == i) break; + + vec3 lightPos = lightPosition[i].xyz; + bool isDir = lightPosition[i].w < one_float; + float spotCos = lightSpot[i].x; + float spotExp = lightSpot[i].y; + + vec3 lightDir; + float falloff; + float spotf; + + if (isDir) { + falloff = one_float; + lightDir = minus_one_vec3 * lightNormal[i]; + } else { + falloff = falloffFactor(lightPos, ecVertex, lightFalloff[i]); + lightDir = normalize(lightPos - ecVertex); + } + + spotf = spotExp > zero_float ? spotFactor(lightPos, ecVertex, lightNormal[i], + spotCos, spotExp) + : one_float; + + if (any(greaterThan(lightAmbient[i], zero_vec3))) { + totalAmbient += lightAmbient[i] * falloff; + } + + if (any(greaterThan(lightDiffuse[i], zero_vec3))) { + totalFrontDiffuse += lightDiffuse[i] * falloff * spotf * + lambertFactor(lightDir, ecNormal); + totalBackDiffuse += lightDiffuse[i] * falloff * spotf * + lambertFactor(lightDir, ecNormalInv); + } + + if (any(greaterThan(lightSpecular[i], zero_vec3))) { + totalFrontSpecular += lightSpecular[i] * falloff * spotf * + blinnPhongFactor(lightDir, ecVertex, ecNormal, shininess); + totalBackSpecular += lightSpecular[i] * falloff * spotf * + blinnPhongFactor(lightDir, ecVertex, ecNormalInv, shininess); + } + } + + // Calculating final color as result of all lights (plus emissive term). + // Transparency is determined exclusively by the diffuse component. + vertColor = vec4(totalAmbient, 0) * ambient + + vec4(totalFrontDiffuse, 1) * color + + vec4(totalFrontSpecular, 0) * specular + + vec4(emissive.rgb, 0); + + backVertColor = vec4(totalAmbient, 0) * ambient + + vec4(totalBackDiffuse, 1) * color + + vec4(totalBackSpecular, 0) * specular + + vec4(emissive.rgb, 0); + + // Calculating texture coordinates, with r and q set both to one + vertTexCoord = texMatrix * vec4(texCoord, 1.0, 1.0); +} diff --git a/src/main/java/processing/opengl/shaders/TexLightVert.glsl b/src/main/java/processing/opengl/shaders/TexLightVert.glsl new file mode 100644 index 0000000..6ef7d4b --- /dev/null +++ b/src/main/java/processing/opengl/shaders/TexLightVert.glsl @@ -0,0 +1,157 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +uniform mat4 modelviewMatrix; +uniform mat4 transformMatrix; +uniform mat3 normalMatrix; +uniform mat4 texMatrix; + +uniform int lightCount; +uniform vec4 lightPosition[8]; +uniform vec3 lightNormal[8]; +uniform vec3 lightAmbient[8]; +uniform vec3 lightDiffuse[8]; +uniform vec3 lightSpecular[8]; +uniform vec3 lightFalloff[8]; +uniform vec2 lightSpot[8]; + +attribute vec4 position; +attribute vec4 color; +attribute vec3 normal; +attribute vec2 texCoord; + +attribute vec4 ambient; +attribute vec4 specular; +attribute vec4 emissive; +attribute float shininess; + +varying vec4 vertColor; +varying vec4 backVertColor; +varying vec4 vertTexCoord; + +const float zero_float = 0.0; +const float one_float = 1.0; +const vec3 zero_vec3 = vec3(0); + +float falloffFactor(vec3 lightPos, vec3 vertPos, vec3 coeff) { + vec3 lpv = lightPos - vertPos; + vec3 dist = vec3(one_float); + dist.z = dot(lpv, lpv); + dist.y = sqrt(dist.z); + return one_float / dot(dist, coeff); +} + +float spotFactor(vec3 lightPos, vec3 vertPos, vec3 lightNorm, float minCos, float spotExp) { + vec3 lpv = normalize(lightPos - vertPos); + vec3 nln = -one_float * lightNorm; + float spotCos = dot(nln, lpv); + return spotCos <= minCos ? zero_float : pow(spotCos, spotExp); +} + +float lambertFactor(vec3 lightDir, vec3 vecNormal) { + return max(zero_float, dot(lightDir, vecNormal)); +} + +float blinnPhongFactor(vec3 lightDir, vec3 vertPos, vec3 vecNormal, float shine) { + vec3 np = normalize(vertPos); + vec3 ldp = normalize(lightDir - np); + return pow(max(zero_float, dot(ldp, vecNormal)), shine); +} + +void main() { + // Vertex in clip coordinates + gl_Position = transformMatrix * position; + + // Vertex in eye coordinates + vec3 ecVertex = vec3(modelviewMatrix * position); + + // Normal vector in eye coordinates + vec3 ecNormal = normalize(normalMatrix * normal); + vec3 ecNormalInv = ecNormal * -one_float; + + // Light calculations + vec3 totalAmbient = vec3(0, 0, 0); + + vec3 totalFrontDiffuse = vec3(0, 0, 0); + vec3 totalFrontSpecular = vec3(0, 0, 0); + + vec3 totalBackDiffuse = vec3(0, 0, 0); + vec3 totalBackSpecular = vec3(0, 0, 0); + + for (int i = 0; i < 8; i++) { + if (lightCount == i) break; + + vec3 lightPos = lightPosition[i].xyz; + bool isDir = lightPosition[i].w < one_float; + float spotCos = lightSpot[i].x; + float spotExp = lightSpot[i].y; + + vec3 lightDir; + float falloff; + float spotf; + + if (isDir) { + falloff = one_float; + lightDir = -one_float * lightNormal[i]; + } else { + falloff = falloffFactor(lightPos, ecVertex, lightFalloff[i]); + lightDir = normalize(lightPos - ecVertex); + } + + spotf = spotExp > zero_float ? spotFactor(lightPos, ecVertex, lightNormal[i], + spotCos, spotExp) + : one_float; + + if (any(greaterThan(lightAmbient[i], zero_vec3))) { + totalAmbient += lightAmbient[i] * falloff; + } + + if (any(greaterThan(lightDiffuse[i], zero_vec3))) { + totalFrontDiffuse += lightDiffuse[i] * falloff * spotf * + lambertFactor(lightDir, ecNormal); + totalBackDiffuse += lightDiffuse[i] * falloff * spotf * + lambertFactor(lightDir, ecNormalInv); + } + + if (any(greaterThan(lightSpecular[i], zero_vec3))) { + totalFrontSpecular += lightSpecular[i] * falloff * spotf * + blinnPhongFactor(lightDir, ecVertex, ecNormal, shininess); + totalBackSpecular += lightSpecular[i] * falloff * spotf * + blinnPhongFactor(lightDir, ecVertex, ecNormalInv, shininess); + } + } + + // Calculating final color as result of all lights (plus emissive term). + // Transparency is determined exclusively by the diffuse component. + vertColor = vec4(totalAmbient, 0) * ambient + + vec4(totalFrontDiffuse, 1) * color + + vec4(totalFrontSpecular, 0) * specular + + vec4(emissive.rgb, 0); + + backVertColor = vec4(totalAmbient, 0) * ambient + + vec4(totalBackDiffuse, 1) * color + + vec4(totalBackSpecular, 0) * specular + + vec4(emissive.rgb, 0); + + // Calculating texture coordinates, with r and q set both to one + vertTexCoord = texMatrix * vec4(texCoord, 1.0, 1.0); +} diff --git a/src/main/java/processing/opengl/shaders/TexVert.glsl b/src/main/java/processing/opengl/shaders/TexVert.glsl new file mode 100644 index 0000000..3bc62e3 --- /dev/null +++ b/src/main/java/processing/opengl/shaders/TexVert.glsl @@ -0,0 +1,38 @@ +/* + Part of the Processing project - http://processing.org + + Copyright (c) 2012-15 The Processing Foundation + Copyright (c) 2004-12 Ben Fry and Casey Reas + Copyright (c) 2001-04 Massachusetts Institute of Technology + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation, version 2.1. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 59 Temple Place, Suite 330, + Boston, MA 02111-1307 USA +*/ + +uniform mat4 transformMatrix; +uniform mat4 texMatrix; + +attribute vec4 position; +attribute vec4 color; +attribute vec2 texCoord; + +varying vec4 vertColor; +varying vec4 vertTexCoord; + +void main() { + gl_Position = transformMatrix * position; + + vertColor = color; + vertTexCoord = texMatrix * vec4(texCoord, 1.0, 1.0); +} \ No newline at end of file diff --git a/src/main/resources/icon/icon-1024.png b/src/main/resources/icon/icon-1024.png new file mode 100644 index 0000000..e3f73f1 Binary files /dev/null and b/src/main/resources/icon/icon-1024.png differ diff --git a/src/main/resources/icon/icon-128.png b/src/main/resources/icon/icon-128.png new file mode 100644 index 0000000..c155a34 Binary files /dev/null and b/src/main/resources/icon/icon-128.png differ diff --git a/src/main/resources/icon/icon-16.png b/src/main/resources/icon/icon-16.png new file mode 100644 index 0000000..0ff675b Binary files /dev/null and b/src/main/resources/icon/icon-16.png differ diff --git a/src/main/resources/icon/icon-256.png b/src/main/resources/icon/icon-256.png new file mode 100644 index 0000000..833f316 Binary files /dev/null and b/src/main/resources/icon/icon-256.png differ diff --git a/src/main/resources/icon/icon-32.png b/src/main/resources/icon/icon-32.png new file mode 100644 index 0000000..c642ee6 Binary files /dev/null and b/src/main/resources/icon/icon-32.png differ diff --git a/src/main/resources/icon/icon-48.png b/src/main/resources/icon/icon-48.png new file mode 100644 index 0000000..6b32675 Binary files /dev/null and b/src/main/resources/icon/icon-48.png differ diff --git a/src/main/resources/icon/icon-512.png b/src/main/resources/icon/icon-512.png new file mode 100644 index 0000000..f4f0d84 Binary files /dev/null and b/src/main/resources/icon/icon-512.png differ diff --git a/src/main/resources/icon/icon-64.png b/src/main/resources/icon/icon-64.png new file mode 100644 index 0000000..1f8dc9d Binary files /dev/null and b/src/main/resources/icon/icon-64.png differ diff --git a/src/main/resources/license.txt b/src/main/resources/license.txt new file mode 100644 index 0000000..8cc6be2 --- /dev/null +++ b/src/main/resources/license.txt @@ -0,0 +1,508 @@ +We use GPL v2 for the parts of the project that we've developed ourselves. +For the 'core' library, it's LGPL, for everything else, it's GPL. + +Over the course of many years of development, files being moved around, +and other code being added and removed, the license information has become +quite a mess. Please help us clean this up so that others are properly +credited and licenses are consistently/correctly noted: +https://github.com/processing/processing/issues/224 + + +..................................................................... + + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + +..................................................................... + + +the original document can be found at: +http://oss.software.ibm.com/developerworks/opensource/license10.html + + +IBM Public License Version 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS IBM +PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF +THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + + +1. DEFINITIONS + +"Contribution" means: + +a) in the case of International Business Machines Corporation ("IBM"), +the Original Program, and + +b) in the case of each Contributor, + +i) changes to the Program, and + +ii) additions to the Program; +where such changes and/or additions to the Program originate from and +are distributed by that particular Contributor. A Contribution +'originates' from a Contributor if it was added to the Program by such +Contributor itself or anyone acting on such Contributor's +behalf. Contributions do not include additions to the Program which: +(i) are separate modules of software distributed in conjunction with +the Program under their own license agreement, and (ii) are not +derivative works of the Program. + +"Contributor" means IBM and any other entity that distributes the +Program. + +"Licensed Patents " mean patent claims licensable by a Contributor +which are necessarily infringed by the use or sale of its Contribution +alone or when combined with the Program. + + +"Original Program" means the original version of the software +accompanying this Agreement as released by IBM, including source code, +object code and documentation, if any. + +"Program" means the Original Program and Contributions. + +"Recipient" means anyone who receives the Program under this +Agreement, including all Contributors. + + +2. GRANT OF RIGHTS + +a) Subject to the terms of this Agreement, each Contributor hereby +grants Recipient a non-exclusive, worldwide, royalty-free copyright +license to reproduce, prepare derivative works of, publicly display, +publicly perform, distribute and sublicense the Contribution of such +Contributor, if any, and such derivative works, in source code and +object code form. + +b) Subject to the terms of this Agreement, each Contributor hereby +grants Recipient a non-exclusive, worldwide, royalty-free patent +license under Licensed Patents to make, use, sell, offer to sell, +import and otherwise transfer the Contribution of such Contributor, if +any, in source code and object code form. This patent license shall +apply to the combination of the Contribution and the Program if, at +the time the Contribution is added by the Contributor, such addition +of the Contribution causes such combination to be covered by the +Licensed Patents. The patent license shall not apply to any other +combinations which include the Contribution. No hardware per se is +licensed hereunder. + +c) Recipient understands that although each Contributor grants the +licenses to its Contributions set forth herein, no assurances are +provided by any Contributor that the Program does not infringe the +patent or other intellectual property rights of any other entity. Each +Contributor disclaims any liability to Recipient for claims brought by +any other entity based on infringement of intellectual property rights +or otherwise. As a condition to exercising the rights and licenses +granted hereunder, each Recipient hereby assumes sole responsibility +to secure any other intellectual property rights needed, if any. For +example, if a third party patent license is required to allow +Recipient to distribute the Program, it is Recipient's responsibility +to acquire that license before distributing the Program. + +d) Each Contributor represents that to its knowledge it has sufficient +copyright rights in its Contribution, if any, to grant the copyright +license set forth in this Agreement. + + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form +under its own license agreement, provided that: + +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: + +i) effectively disclaims on behalf of all Contributors all warranties +and conditions, express and implied, including warranties or +conditions of title and non-infringement, and implied warranties or +conditions of merchantability and fitness for a particular purpose; + +ii) effectively excludes on behalf of all Contributors all liability +for damages, including direct, indirect, special, incidental and +consequential damages, such as lost profits; + +iii) states that any provisions which differ from this Agreement are +offered by that Contributor alone and not by any other party; and + +iv) states that source code for the Program is available from such +Contributor, and informs licensees how to obtain it in a reasonable +manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: +a) it must be made available under this Agreement; and +b) a copy of this Agreement must be included with each copy of the +Program. + +Each Contributor must include the following in a conspicuous location +in the Program: + +Copyright 2003, International Business Machines Corporation and +others. All Rights Reserved. + +In addition, each Contributor must identify itself as the originator +of its Contribution, if any, in a manner that reasonably allows +subsequent Recipients to identify the originator of the Contribution. + + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain +responsibilities with respect to end users, business partners and the +like. While this license is intended to facilitate the commercial use +of the Program, the Contributor who includes the Program in a +commercial product offering should do so in a manner which does not +create potential liability for other Contributors. Therefore, if a +Contributor includes the Program in a commercial product offering, +such Contributor ("Commercial Contributor") hereby agrees to defend +and indemnify every other Contributor ("Indemnified Contributor") +against any losses, damages and costs (collectively "Losses") arising +from claims, lawsuits and other legal actions brought by a third party +against the Indemnified Contributor to the extent caused by the acts +or omissions of such Commercial Contributor in connection with its +distribution of the Program in a commercial product offering. The +obligations in this section do not apply to any claims or Losses +relating to any actual or alleged intellectual property +infringement. In order to qualify, an Indemnified Contributor must: a) +promptly notify the Commercial Contributor in writing of such claim, +and b) allow the Commercial Contributor to control, and cooperate with +the Commercial Contributor in, the defense and any related settlement +negotiations. The Indemnified Contributor may participate in any such +claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those +performance claims and warranties, and if a court requires any other +Contributor to pay any damages as a result, the Commercial Contributor +must pay those damages. + + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS +PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY +WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY +OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely +responsible for determining the appropriateness of using and +distributing the Program and assumes all risks associated with its +exercise of rights under this Agreement, including but not limited to +the risks and costs of program errors, compliance with applicable +laws, damage to or loss of data, programs or equipment, and +unavailability or interruption of operations. + + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR +ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR +DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED +HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and +enforceable. + +If Recipient institutes patent litigation against a Contributor with +respect to a patent applicable to software (including a cross-claim or +counterclaim in a lawsuit), then any patent licenses granted by that +Contributor to such Recipient under this Agreement shall terminate as +of the date such litigation is filed. In addition, If Recipient +institutes patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Program +itself (excluding combinations of the Program with other software or +hardware