├── .gitignore ├── README.md ├── brainstorm.txt ├── general_utilities.py ├── license.txt ├── __init__.py └── contour_utilities.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | *.pyc 10 | 11 | # Packages # 12 | ############ 13 | # it's better to unpack these files and commit the raw source 14 | # git has its own built in compression methods 15 | *.7z 16 | *.dmg 17 | *.gz 18 | *.iso 19 | *.jar 20 | *.rar 21 | *.tar 22 | *.zip 23 | 24 | # Logs and databases # 25 | ###################### 26 | *.log 27 | *.sql 28 | *.sqlite 29 | 30 | # OS generated files # 31 | ###################### 32 | .DS_Store 33 | .DS_Store? 34 | ._* 35 | .Spotlight-V100 36 | .Trashes 37 | ehthumbs.db 38 | Thumbs.db 39 | .project 40 | .pydevproject 41 | __pycache__/__init__.cpython-33.pyc 42 | __pycache__/contour_classes.cpython-33.pyc 43 | __pycache__/contour_utilities.cpython-33.pyc 44 | contour_modals.py 45 | __pycache__/contour_modals.cpython-33.pyc 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CG Cookie Contours Retopology Tool 2 | ========== 3 | 4 | ## Discontinued :warning: 5 | Contours has been combined with Polystrips and is now offered as a part of RetopoFlow: https://cgcookiemarkets.com/all-products/retopoflow/ 6 | 7 | This version is no longer maintained. 8 | 9 | --------------- 10 | 11 | The Contours Retopology tool is an addon for Blender that provides quick and easy ways retopologize cylindrical forms. Use cases include organic forms, such as arms, legs, tentacles, tails, horns, etc. 12 | 13 | The tool works by drawing strokes perpendicular to the form to define the contour of the shape. Immediately upon drawing the first stroke, a preview mesh is generated, showing you exactly what you’ll get. You can draw as many strokes as you like, in any order, from any direction. 14 | 15 | The add-on is available for purchase from the Blender Market: http://cgcookiemarkets.com/blender/all-products/contours-retopology-tool/ 16 | 17 | Documentation: http://cgcookiemarkets.com/blender/all-products/contours-retopology-tool/?view=docs 18 | 19 | Support Forums: http://cgcookiemarkets.com/blender/all-products/contours-retopology-tool/?view=support 20 | 21 | -------------------------------------------------------------------------------- /brainstorm.txt: -------------------------------------------------------------------------------- 1 | Thought Explosion and Concepts 2 | Seems like there are lots of different applications of contours and toplogy 3 | and it has been tackled or investigates a lot if differnet ways 4 | 5 | Cross Sections 6 | 7 | Countour Lines 8 | 9 | Features Lines and Points 10 | 11 | Silhouettes 12 | 13 | 14 | 15 | Links 16 | #cross sections and silhouettes 17 | http://blenderartists.org/forum/showthread.php?280868-Cross-section-script-ported-to-2-6 18 | http://blenderartists.org/forum/showthread.php?207420-Rewriting-the-Cross-Section-Script 19 | http://blenderartists.org/forum/showthread.php?283721-Mesh-Silhouette-My-first-Bmesh-Operator 20 | 21 | # 22 | 23 | #Mesh intersection script 24 | http://airplanes3d.net/scripts-253_e.xml 25 | 26 | 27 | #there may be some low level API code useful for projections, intersections etc 28 | http://wiki.blender.org/index.php/Doc:2.6/Manual/Modeling/Meshes/Editing/Subdividing/Knife_Subdivide 29 | http://wiki.blender.org/index.php/Doc:2.6/Manual/Modeling/Meshes#Mesh_Analysis 30 | 31 | 32 | Literature Resources and Papers 33 | http://www.maths.ed.ac.uk/~aar/papers/vanwijk.pdf 34 | 35 | 36 | 37 | Contact Info for Devs 38 | 39 | Patrick Moore 40 | patrick.moore.bu@gmail.com 41 | 252-723-3456 -------------------------------------------------------------------------------- /general_utilities.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (C) 2014 Plasmasolutions 3 | software@plasmasolutions.de 4 | 5 | Created by Thomas Beck 6 | Donated to CGCookie and the world 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | ''' 21 | 22 | #This class makes it easier to be install location independent 23 | import sys 24 | import os 25 | 26 | 27 | class AddonLocator(object): 28 | def __init__(self): 29 | self.fullInitPath = __file__ 30 | self.FolderPath = os.path.dirname(self.fullInitPath) 31 | self.FolderName = os.path.basename(self.FolderPath) 32 | 33 | def AppendPath(self): 34 | sys.path.append(self.FolderPath) 35 | print("Addon path has been registered into system path for this session") -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 51 Franklin St, Fifth Floor, Boston, MA 02110, USA 6 | 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | Preamble 11 | 12 | The licenses for most software are designed to take away your 13 | freedom to share and change it. By contrast, the GNU General Public 14 | License is intended to guarantee your freedom to share and change free 15 | software--to make sure the software is free for all its users. This 16 | General Public License applies to most of the Free Software 17 | Foundation's software and to any other program whose authors commit to 18 | using it. (Some other Free Software Foundation software is covered by 19 | the GNU Library General Public License instead.) You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | this service if you wish), that you receive source code or can get it 26 | if you want it, that you can change the software or use pieces of it 27 | in new free programs; and that you know you can do these things. 28 | 29 | To protect your rights, we need to make restrictions that forbid 30 | anyone to deny you these rights or to ask you to surrender the rights. 31 | These restrictions translate to certain responsibilities for you if you 32 | distribute copies of the software, or if you modify it. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must give the recipients all the rights that 36 | you have. You must make sure that they, too, receive or can get the 37 | source code. And you must show them these terms so they know their 38 | rights. 39 | 40 | We protect your rights with two steps: (1) copyright the software, and 41 | (2) offer you this license which gives you legal permission to copy, 42 | distribute and/or modify the software. 43 | 44 | Also, for each author's protection and ours, we want to make certain 45 | that everyone understands that there is no warranty for this free 46 | software. If the software is modified by someone else and passed on, we 47 | want its recipients to know that what they have is not the original, so 48 | that any problems introduced by others will not reflect on the original 49 | authors' reputations. 50 | 51 | Finally, any free program is threatened constantly by software 52 | patents. We wish to avoid the danger that redistributors of a free 53 | program will individually obtain patent licenses, in effect making the 54 | program proprietary. To prevent this, we have made it clear that any 55 | patent must be licensed for everyone's free use or not licensed at all. 56 | 57 | The precise terms and conditions for copying, distribution and 58 | modification follow. 59 | 60 | GNU GENERAL PUBLIC LICENSE 61 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 62 | 63 | 0. This License applies to any program or other work which contains 64 | a notice placed by the copyright holder saying it may be distributed 65 | under the terms of this General Public License. The "Program", below, 66 | refers to any such program or work, and a "work based on the Program" 67 | means either the Program or any derivative work under copyright law: 68 | that is to say, a work containing the Program or a portion of it, 69 | either verbatim or with modifications and/or translated into another 70 | language. (Hereinafter, translation is included without limitation in 71 | the term "modification".) Each licensee is addressed as "you". 72 | 73 | Activities other than copying, distribution and modification are not 74 | covered by this License; they are outside its scope. The act of 75 | running the Program is not restricted, and the output from the Program 76 | is covered only if its contents constitute a work based on the 77 | Program (independent of having been made by running the Program). 78 | Whether that is true depends on what the Program does. 79 | 80 | 1. You may copy and distribute verbatim copies of the Program's 81 | source code as you receive it, in any medium, provided that you 82 | conspicuously and appropriately publish on each copy an appropriate 83 | copyright notice and disclaimer of warranty; keep intact all the 84 | notices that refer to this License and to the absence of any warranty; 85 | and give any other recipients of the Program a copy of this License 86 | along with the Program. 87 | 88 | You may charge a fee for the physical act of transferring a copy, and 89 | you may at your option offer warranty protection in exchange for a fee. 90 | 91 | 2. You may modify your copy or copies of the Program or any portion 92 | of it, thus forming a work based on the Program, and copy and 93 | distribute such modifications or work under the terms of Section 1 94 | above, provided that you also meet all of these conditions: 95 | 96 | a) You must cause the modified files to carry prominent notices 97 | stating that you changed the files and the date of any change. 98 | 99 | b) You must cause any work that you distribute or publish, that in 100 | whole or in part contains or is derived from the Program or any 101 | part thereof, to be licensed as a whole at no charge to all third 102 | parties under the terms of this License. 103 | 104 | c) If the modified program normally reads commands interactively 105 | when run, you must cause it, when started running for such 106 | interactive use in the most ordinary way, to print or display an 107 | announcement including an appropriate copyright notice and a 108 | notice that there is no warranty (or else, saying that you provide 109 | a warranty) and that users may redistribute the program under 110 | these conditions, and telling the user how to view a copy of this 111 | License. (Exception: if the Program itself is interactive but 112 | does not normally print such an announcement, your work based on 113 | the Program is not required to print an announcement.) 114 | 115 | These requirements apply to the modified work as a whole. If 116 | identifiable sections of that work are not derived from the Program, 117 | and can be reasonably considered independent and separate works in 118 | themselves, then this License, and its terms, do not apply to those 119 | sections when you distribute them as separate works. But when you 120 | distribute the same sections as part of a whole which is a work based 121 | on the Program, the distribution of the whole must be on the terms of 122 | this License, whose permissions for other licensees extend to the 123 | entire whole, and thus to each and every part regardless of who wrote it. 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (C) 2013 CG Cookie 3 | http://cgcookie.com 4 | hello@cgcookie.com 5 | 6 | Created by Patrick Moore 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | ''' 21 | 22 | bl_info = { 23 | "name": "Contour Retopology Tool", 24 | "description": "A tool to retopologize forms quickly with contour strokes.", 25 | "author": "Jonathan Williamson, Patrick Moore", 26 | "version": (1, 2, 2), 27 | "blender": (2, 7, 2), 28 | "location": "View 3D > Tool Shelf", 29 | "warning": '', # Used for warning icon and text in addons panel 30 | "wiki_url": "http://cgcookie.com/blender/docs/contour-retopology/", 31 | "tracker_url": "https://github.com/CGCookie/retopology/issues?labels=Bug&milestone=1&state=open", 32 | "category": "3D View"} 33 | 34 | # Add the current __file__ path to the search path 35 | import sys 36 | import os 37 | sys.path.append(os.path.dirname(__file__)) 38 | 39 | import copy 40 | import math 41 | import time 42 | from mathutils import Vector 43 | from mathutils.geometry import intersect_line_plane, intersect_point_line 44 | 45 | import blf 46 | import bmesh 47 | import bpy 48 | from bpy_extras.view3d_utils import location_3d_to_region_2d, region_2d_to_vector_3d, region_2d_to_location_3d 49 | from bpy.types import Operator, AddonPreferences 50 | from bpy.props import EnumProperty, StringProperty, BoolProperty, IntProperty, FloatVectorProperty, FloatProperty 51 | 52 | import contour_utilities 53 | import general_utilities 54 | from contour_classes import ContourCutLine, ExistingVertList, CutLineManipulatorWidget, ContourCutSeries, ContourStatePreserver 55 | 56 | from lib import common_drawing 57 | 58 | # Create a class that contains all location information for addons 59 | AL = general_utilities.AddonLocator() 60 | 61 | # A place to store stokes for later 62 | global contour_cache 63 | contour_cache = {} 64 | contour_undo_cache = [] 65 | 66 | # Store any temporary triangulated objects 67 | # Store the bmesh to prevent recalcing bmesh each time :-) 68 | global contour_mesh_cache 69 | contour_mesh_cache = {} 70 | 71 | 72 | def object_validation(ob): 73 | me = ob.data 74 | 75 | # Get object data to act as a hash 76 | counts = (len(me.vertices), len(me.edges), len(me.polygons), len(ob.modifiers)) 77 | bbox = (tuple(min(v.co for v in me.vertices)), tuple(max(v.co for v in me.vertices))) 78 | vsum = tuple(sum((v.co for v in me.vertices), Vector((0, 0, 0)))) 79 | 80 | return (ob.name, counts, bbox, vsum) 81 | 82 | 83 | def is_object_valid(ob): 84 | global contour_mesh_cache 85 | if 'valid' not in contour_mesh_cache: 86 | return False 87 | return contour_mesh_cache['valid'] == object_validation(ob) 88 | 89 | 90 | def write_mesh_cache(orig_ob, tmp_ob, bme): 91 | print('writing mesh cache') 92 | global contour_mesh_cache 93 | clear_mesh_cache() 94 | contour_mesh_cache['valid'] = object_validation(orig_ob) 95 | contour_mesh_cache['bme'] = bme 96 | contour_mesh_cache['tmp'] = tmp_ob 97 | 98 | 99 | def clear_mesh_cache(): 100 | print('clearing mesh cache') 101 | 102 | global contour_mesh_cache 103 | 104 | if 'valid' in contour_mesh_cache and contour_mesh_cache['valid']: 105 | del contour_mesh_cache['valid'] 106 | 107 | if 'bme' in contour_mesh_cache and contour_mesh_cache['bme']: 108 | bme_old = contour_mesh_cache['bme'] 109 | bme_old.free() 110 | del contour_mesh_cache['bme'] 111 | 112 | if 'tmp' in contour_mesh_cache and contour_mesh_cache['tmp']: 113 | old_obj = contour_mesh_cache['tmp'] 114 | #context.scene.objects.unlink(self.tmp_ob) 115 | old_me = old_obj.data 116 | old_obj.user_clear() 117 | if old_obj and old_obj.name in bpy.data.objects: 118 | bpy.data.objects.remove(old_obj) 119 | if old_me and old_me.name in bpy.data.meshes: 120 | bpy.data.meshes.remove(old_me) 121 | del contour_mesh_cache['tmp'] 122 | 123 | 124 | class ContourToolsAddonPreferences(AddonPreferences): 125 | bl_idname = __name__ 126 | 127 | simple_vert_inds = BoolProperty( 128 | name="Simple Inds", 129 | default=False, 130 | ) 131 | 132 | vert_inds = BoolProperty( 133 | name="Vert Inds", 134 | description="Display indices of the raw contour verts", 135 | default=False, 136 | ) 137 | 138 | show_verts = BoolProperty( 139 | name="Show Raw Verts", 140 | description="Display the raw contour verts", 141 | default=False, 142 | ) 143 | 144 | show_edges = BoolProperty( 145 | name="Show Span Edges", 146 | description="Display the extracted mesh edges. Usually only turned off for debugging", 147 | default=True, 148 | ) 149 | 150 | show_cut_indices = BoolProperty( 151 | name="Show Cut Indices", 152 | description="Display the order the operator stores cuts. Usually only turned on for debugging", 153 | default=False, 154 | ) 155 | 156 | show_ring_edges = BoolProperty( 157 | name="Show Ring Edges", 158 | description="Display the extracted mesh edges. Usually only turned off for debugging", 159 | default=True, 160 | ) 161 | 162 | draw_widget = BoolProperty( 163 | name="Draw Widget", 164 | description="Turn display of widget on or off", 165 | default=True, 166 | ) 167 | 168 | debug = IntProperty( 169 | name="Debug Level", 170 | default=1, 171 | min=0, 172 | max=4, 173 | ) 174 | 175 | show_backbone = BoolProperty( 176 | name="show_backbone", 177 | description="Show Cut Series Backbone", 178 | default=False) 179 | 180 | show_nodes = BoolProperty( 181 | name="show_nodes", 182 | description="Show Cut Nodes", 183 | default=False) 184 | 185 | show_ring_inds = BoolProperty( 186 | name="show_ring_inds", 187 | description="Show Ring Indices", 188 | default=False) 189 | 190 | show_axes = BoolProperty( 191 | name="show_axes", 192 | description="Show Cut Axes", 193 | default=False) 194 | 195 | show_debug = BoolProperty( 196 | name="Show Debug Settings", 197 | description="Show the debug settings, useful for troubleshooting", 198 | default=False, 199 | ) 200 | 201 | vert_size = IntProperty( 202 | name="Vertex Size", 203 | default=4, 204 | min=1, 205 | max=10, 206 | ) 207 | 208 | edge_thick = IntProperty( 209 | name="Edge Thickness", 210 | default=1, 211 | min=1, 212 | max=10, 213 | ) 214 | 215 | theme = EnumProperty( 216 | items=[ 217 | ('blue', 'Blue', 'Blue color scheme'), 218 | ('green', 'Green', 'Green color scheme'), 219 | ('orange', 'Orange', 'Orange color scheme'), 220 | ], 221 | name='theme', 222 | default='blue' 223 | ) 224 | 225 | 226 | def rgba_to_float(r, g, b, a): 227 | return (r/255.0, g/255.0, b/255.0, a/255.0) 228 | 229 | theme_colors_active = { 230 | 'blue': rgba_to_float(105, 246, 113, 255), 231 | 'green': rgba_to_float(102, 165, 240, 255), 232 | 'orange': rgba_to_float(102, 165, 240, 255) 233 | } 234 | theme_colors_mesh = { 235 | 'blue': rgba_to_float(102, 165, 240, 255), 236 | 'green': rgba_to_float(105, 246, 113, 255), 237 | 'orange': rgba_to_float(254, 145, 0, 255) 238 | } 239 | 240 | raw_vert_size = IntProperty( 241 | name="Raw Vertex Size", 242 | default=1, 243 | min=1, 244 | max=10, 245 | ) 246 | 247 | handle_size = IntProperty( 248 | name="Handle Vertex Size", 249 | default=8, 250 | min=1, 251 | max=10, 252 | ) 253 | 254 | line_thick = IntProperty( 255 | name="Line Thickness", 256 | default=1, 257 | min=1, 258 | max=10, 259 | ) 260 | 261 | stroke_thick = IntProperty( 262 | name="Stroke Thickness", 263 | description="Width of stroke lines drawn by user", 264 | default=1, 265 | min=1, 266 | max=10, 267 | ) 268 | 269 | auto_align = BoolProperty( 270 | name="Automatically Align Vertices", 271 | description="Attempt to automatically align vertices in adjoining edgeloops. Improves outcome, but slows performance", 272 | default=True, 273 | ) 274 | 275 | live_update = BoolProperty( 276 | name="Live Update", 277 | description="Will live update the mesh preview when transforming cut lines. Looks good, but can get slow on large meshes", 278 | default=True, 279 | ) 280 | 281 | use_x_ray = BoolProperty( 282 | name="X-Ray", 283 | description='Enable X-Ray on Retopo-mesh upon creation', 284 | default=False, 285 | ) 286 | 287 | use_perspective = BoolProperty( 288 | name="Use Perspective", 289 | description='Make non parallel cuts project from the same view to improve expected outcome', 290 | default=True, 291 | ) 292 | 293 | new_method = BoolProperty( 294 | name="New Method", 295 | description="Use robust cutting, may be slower, more accurate on dense meshes", 296 | default=True, 297 | ) 298 | 299 | # TODO Theme this out nicely :-) 300 | widget_color = FloatVectorProperty(name="Widget Color", description="Choose Widget color", min=0, max=1, default=(0,0,1), subtype="COLOR") 301 | widget_color2 = FloatVectorProperty(name="Widget Color", description="Choose Widget color", min=0, max=1, default=(1,0,0), subtype="COLOR") 302 | widget_color3 = FloatVectorProperty(name="Widget Color", description="Choose Widget color", min=0, max=1, default=(0,1,0), subtype="COLOR") 303 | widget_color4 = FloatVectorProperty(name="Widget Color", description="Choose Widget color", min=0, max=1, default=(0,0.2,.8), subtype="COLOR") 304 | widget_color5 = FloatVectorProperty(name="Widget Color", description="Choose Widget color", min=0, max=1, default=(.9,.1,0), subtype="COLOR") 305 | 306 | widget_radius = IntProperty( 307 | name="Widget Radius", 308 | description="Size of cutline widget radius", 309 | default=25, 310 | min=20, 311 | max=100, 312 | ) 313 | 314 | widget_radius_inner = IntProperty( 315 | name="Widget Inner Radius", 316 | description="Size of cutline widget inner radius", 317 | default=10, 318 | min=5, 319 | max=30, 320 | ) 321 | 322 | widget_thickness = IntProperty( 323 | name="Widget Line Thickness", 324 | description="Width of lines used to draw widget", 325 | default=2, 326 | min=1, 327 | max=10, 328 | ) 329 | 330 | widget_thickness2 = IntProperty( 331 | name="Widget 2nd Line Thick", 332 | description="Width of lines used to draw widget", 333 | default=4, 334 | min=1, 335 | max=10, 336 | ) 337 | 338 | arrow_size = IntProperty( 339 | name="Arrow Size", 340 | default=12, 341 | min=5, 342 | max=50, 343 | ) 344 | 345 | arrow_size2 = IntProperty( 346 | name="Translate Arrow Size", 347 | default=10, 348 | min=5, 349 | max=50, 350 | ) 351 | 352 | vertex_count = IntProperty( 353 | name="Vertex Count", 354 | description="The Number of Vertices Per Edge Ring", 355 | default=10, 356 | min=3, 357 | max=250, 358 | ) 359 | 360 | ring_count = IntProperty( 361 | name="Ring Count", 362 | description="The Number of Segments Per Guide Stroke", 363 | default=10, 364 | min=3, 365 | max=100, 366 | ) 367 | 368 | cyclic = BoolProperty( 369 | name="Cyclic", 370 | description="Make contour loops cyclic", 371 | default=False) 372 | 373 | recover = BoolProperty( 374 | name="Recover", 375 | description="Recover strokes from last session", 376 | default=False) 377 | 378 | recover_clip = IntProperty( 379 | name="Recover Clip", 380 | description="Number of cuts to leave out, usually set to 0 or 1", 381 | default=1, 382 | min=0, 383 | max=10, 384 | ) 385 | 386 | search_factor = FloatProperty( 387 | name="Search Factor", 388 | description="Factor of existing segment length to connect a new cut", 389 | default=5, 390 | min=0, 391 | max=30, 392 | ) 393 | 394 | intersect_threshold = FloatProperty( 395 | name="Intersect Factor", 396 | description="Stringence for connecting new strokes", 397 | default=1.0, 398 | min=0.000001, 399 | max=1, 400 | ) 401 | 402 | merge_threshold = FloatProperty( 403 | name="Intersect Factor", 404 | description="Distance below which to snap strokes together", 405 | default=1.0, 406 | min=0.000001, 407 | max=1, 408 | ) 409 | 410 | cull_factor = IntProperty( 411 | name="Cull Factor", 412 | description="Fraction of screen drawn points to throw away. Bigger = less detail", 413 | default=4, 414 | min=1, 415 | max=10, 416 | ) 417 | 418 | smooth_factor = IntProperty( 419 | name="Smooth Factor", 420 | description="Number of iterations to smooth drawn strokes", 421 | default=5, 422 | min=1, 423 | max=10, 424 | ) 425 | 426 | feature_factor = IntProperty( 427 | name="Smooth Factor", 428 | description="Fraction of sketch bounding box to be considered feature. Bigger = More Detail", 429 | default=4, 430 | min=1, 431 | max=20, 432 | ) 433 | 434 | extend_radius = IntProperty( 435 | name="Snap/Extend Radius", 436 | default=20, 437 | min=5, 438 | max=100, 439 | ) 440 | 441 | undo_depth = IntProperty( 442 | name="Undo Depth", 443 | default=10, 444 | min=0, 445 | max=100, 446 | ) 447 | 448 | 449 | def draw(self, context): 450 | layout = self.layout 451 | 452 | # Interaction Settings 453 | row = layout.row(align=True) 454 | row.prop(self, "auto_align") 455 | row.prop(self, "live_update") 456 | row.prop(self, "use_perspective") 457 | 458 | row = layout.row() 459 | row.prop(self, "use_x_ray", "Enable X-Ray at Mesh Creation") 460 | 461 | # Theme testing 462 | row = layout.row(align=True) 463 | row.prop(self, "theme", "Theme") 464 | 465 | # Visualization Settings 466 | box = layout.box().column(align=False) 467 | row = box.row() 468 | row.label(text="Stroke And Loop Settings") 469 | 470 | row = box.row(align=False) 471 | row.prop(self, "handle_size", text="Handle Size") 472 | row.prop(self, "stroke_thick", text="Stroke Thickness") 473 | 474 | row = box.row(align=False) 475 | row.prop(self, "show_edges", text="Show Edge Loops") 476 | row.prop(self, "line_thick", text ="Edge Thickness") 477 | 478 | row = box.row(align=False) 479 | row.prop(self, "show_ring_edges", text="Show Edge Rings") 480 | row.prop(self, "vert_size") 481 | 482 | row = box.row(align=True) 483 | row.prop(self, "show_cut_indices", text = "Edge Indices") 484 | 485 | # Widget Settings 486 | box = layout.box().column(align=False) 487 | row = box.row() 488 | row.label(text="Widget Settings") 489 | 490 | row = box.row() 491 | row.prop(self,"draw_widget", text = "Display Widget") 492 | 493 | if self.draw_widget: 494 | row = box.row() 495 | row.prop(self, "widget_radius", text="Radius") 496 | row.prop(self,"widget_radius_inner", text="Active Radius") 497 | 498 | row = box.row() 499 | row.prop(self, "widget_thickness", text="Line Thickness") 500 | row.prop(self, "widget_thickness2", text="2nd Line Thickness") 501 | row.prop(self, "arrow_size", text="Arrow Size") 502 | row.prop(self, "arrow_size2", text="Translate Arrow Size") 503 | 504 | row = box.row() 505 | row.prop(self, "widget_color", text="Color 1") 506 | row.prop(self, "widget_color2", text="Color 2") 507 | row.prop(self, "widget_color3", text="Color 3") 508 | row.prop(self, "widget_color4", text="Color 4") 509 | row.prop(self, "widget_color5", text="Color 5") 510 | 511 | # Debug Settings 512 | box = layout.box().column(align=False) 513 | row = box.row() 514 | row.label(text="Debug Settings") 515 | 516 | row = box.row() 517 | row.prop(self, "show_debug", text="Show Debug Settings") 518 | 519 | if self.show_debug: 520 | row = box.row() 521 | row.prop(self, "new_method") 522 | row.prop(self, "debug") 523 | 524 | row = box.row() 525 | row.prop(self, "vert_inds", text="Show Vertex Indices") 526 | row.prop(self, "simple_vert_inds", text="Show Simple Indices") 527 | 528 | row = box.row() 529 | row.prop(self, "show_verts", text="Show Raw Vertices") 530 | row.prop(self, "raw_vert_size") 531 | 532 | row = box.row() 533 | row.prop(self, "show_backbone", text="Show Backbone") 534 | row.prop(self, "show_nodes", text="Show Cut Nodes") 535 | row.prop(self, "show_ring_inds", text="Show Ring Indices") 536 | 537 | 538 | class CGCOOKIE_OT_retopo_contour_panel(bpy.types.Panel): 539 | '''Retopologize Forms with Contour Strokes''' 540 | bl_category = "Retopology" 541 | bl_label = "Contour Retopolgy" 542 | bl_space_type = 'VIEW_3D' 543 | bl_region_type = 'TOOLS' 544 | 545 | @classmethod 546 | def poll(cls, context): 547 | mode = bpy.context.mode 548 | obj = context.active_object 549 | return (obj and obj.type == 'MESH' and mode in ('OBJECT', 'EDIT_MESH')) 550 | 551 | 552 | def draw(self, context): 553 | layout = self.layout 554 | col = layout.column(align=True) 555 | 556 | cgc_contour = context.user_preferences.addons[AL.FolderName].preferences 557 | 558 | if 'EDIT' in context.mode and len(context.selected_objects) != 2: 559 | col.label(text='No 2nd Object!') 560 | 561 | col.operator("cgcookie.retop_contour", icon='IPO_LINEAR') 562 | col.prop(cgc_contour, "vertex_count") 563 | 564 | col = layout.column() 565 | col.label("Guide Mode:") 566 | col.prop(cgc_contour, "ring_count") 567 | 568 | # Commenting out for now until this is further improved and made to work again ### 569 | # row = box.row() 570 | # row.prop(cgc_contour, "cyclic") 571 | 572 | col = layout.column() 573 | col.label("Cache:") 574 | 575 | row = layout.row() 576 | row.prop(cgc_contour, "recover") 577 | 578 | if cgc_contour.recover: 579 | row.prop(cgc_contour, "recover_clip") 580 | 581 | row = layout.row() 582 | row.operator("cgcookie.clear_cache", text = "Clear Cache", icon = 'CANCEL') 583 | 584 | 585 | class CGCOOKIE_OT_retopo_contour_menu(bpy.types.Menu): 586 | bl_label = "Retopology" 587 | bl_space_type = 'VIEW_3D' 588 | bl_idname = "object.retopology_menu" 589 | 590 | def draw(self, context): 591 | layout = self.layout 592 | 593 | layout.operator_context = 'INVOKE_DEFAULT' 594 | 595 | cgc_contour = context.user_preferences.addons[AL.FolderName].preferences 596 | 597 | layout.operator("cgcookie.retop_contour") 598 | 599 | 600 | class CGCOOKIE_OT_retopo_cache_clear(bpy.types.Operator): 601 | '''Removes the temporary object and mesh data from the cache. Do this if you have altered your original form in any way''' 602 | bl_idname = "cgcookie.clear_cache" 603 | bl_label = "Clear Contour Cache" 604 | 605 | def execute(self,context): 606 | 607 | clear_mesh_cache() 608 | return {'FINISHED'} 609 | 610 | 611 | def retopo_draw_callback(self, context): 612 | 613 | settings = context.user_preferences.addons[AL.FolderName].preferences 614 | 615 | stroke_color = settings.theme_colors_active[settings.theme] 616 | 617 | if (self.post_update or self.modal_state == 'NAVIGATING') and context.space_data.use_occlude_geometry: 618 | for path in self.cut_paths: 619 | path.update_visibility(context, self.original_form) 620 | for cut_line in path.cuts: 621 | cut_line.update_visibility(context, self.original_form) 622 | 623 | self.post_update = False 624 | 625 | for i, c_cut in enumerate(self.cut_lines): 626 | if self.widget_interaction and self.drag_target == c_cut: 627 | interact = True 628 | else: 629 | interact = False 630 | 631 | c_cut.draw(context, settings, three_dimensional=self.navigating, interacting=interact) 632 | 633 | if c_cut.verts_simple != [] and settings.show_cut_indices: 634 | loc = location_3d_to_region_2d(context.region, context.space_data.region_3d, c_cut.verts_simple[0]) 635 | blf.position(0, loc[0], loc[1], 0) 636 | blf.draw(0, str(i)) 637 | 638 | if self.cut_line_widget and settings.draw_widget: 639 | self.cut_line_widget.draw(context) 640 | 641 | if len(self.draw_cache): 642 | # Draw guide line 643 | common_drawing.draw_polyline_from_points(context, self.draw_cache, stroke_color, 2, "GL_LINE_STIPPLE") 644 | 645 | if len(self.cut_paths): 646 | for path in self.cut_paths: 647 | path.draw(context, path=True, nodes=settings.show_nodes, rings=True, follows=True, backbone=settings.show_backbone) 648 | 649 | if len(self.snap_circle): 650 | # Draw snap circle 651 | contour_utilities.draw_polyline_from_points(context, self.snap_circle, self.snap_color, 2, "GL_LINE_SMOOTH") 652 | 653 | 654 | class CGCOOKIE_OT_retopo_contour(bpy.types.Operator): 655 | '''Draw Perpendicular Strokes to Retopologize Cylindrical Forms''' 656 | bl_idname = "cgcookie.retop_contour" 657 | bl_label = "Draw Contours" 658 | 659 | @classmethod 660 | def poll(cls, context): 661 | if context.mode not in {'EDIT_MESH','OBJECT'}: 662 | return False 663 | 664 | if context.active_object: 665 | if context.mode == 'EDIT_MESH': 666 | if len(context.selected_objects) > 1: 667 | return True 668 | else: 669 | return False 670 | else: 671 | return context.object.type == 'MESH' 672 | else: 673 | return False 674 | 675 | def hover_guide_mode(self, context, settings, event): 676 | ''' 677 | handles mouse selection, hovering, highlighting 678 | and snapping when the mouse moves in guide 679 | mode 680 | ''' 681 | 682 | stroke_color = settings.theme_colors_active[settings.theme] 683 | mesh_color = settings.theme_colors_mesh[settings.theme] 684 | 685 | # Identify hover target for highlighting 686 | if self.cut_paths != []: 687 | target_at_all = False 688 | breakout = False 689 | for path in self.cut_paths: 690 | if not path.select: 691 | path.unhighlight(settings) 692 | for c_cut in path.cuts: 693 | h_target = c_cut.active_element(context,event.mouse_region_x,event.mouse_region_y) 694 | if h_target: 695 | path.highlight(settings) 696 | target_at_all = True 697 | self.hover_target = path 698 | breakout = True 699 | break 700 | 701 | if breakout: 702 | break 703 | 704 | if not target_at_all: 705 | self.hover_target = None 706 | 707 | # Assess snap points 708 | if self.cut_paths != [] and not self.force_new: 709 | rv3d = context.space_data.region_3d 710 | breakout = False 711 | snapped = False 712 | for path in self.cut_paths: 713 | 714 | end_cuts = [] 715 | if not path.existing_head and len(path.cuts): 716 | end_cuts.append(path.cuts[0]) 717 | if not path.existing_tail and len(path.cuts): 718 | end_cuts.append(path.cuts[-1]) 719 | 720 | if path.existing_head and not len(path.cuts): 721 | end_cuts.append(path.existing_head) 722 | 723 | for n, end_cut in enumerate(end_cuts): 724 | 725 | # Potential verts to snap to 726 | snaps = [v for i, v in enumerate(end_cut.verts_simple) if end_cut.verts_simple_visible[i]] 727 | # The screen versions os those 728 | screen_snaps = [location_3d_to_region_2d(context.region,rv3d,snap) for snap in snaps] 729 | 730 | mouse = Vector((event.mouse_region_x,event.mouse_region_y)) 731 | dists = [(mouse - snap).length for snap in screen_snaps] 732 | 733 | if len(dists): 734 | best = min(dists) 735 | if best < 2 * settings.extend_radius and best > 4: #TODO unify selection mouse pixel radius. 736 | 737 | best_vert = screen_snaps[dists.index(best)] 738 | view_z = rv3d.view_rotation * Vector((0,0,1)) 739 | if view_z.dot(end_cut.plane_no) > -0.75 and view_z.dot(end_cut.plane_no) < 0.75: 740 | 741 | imx = rv3d.view_matrix.inverted() 742 | normal_3d = imx.transposed() * end_cut.plane_no 743 | if n == 1 or len(end_cuts) == 1: 744 | normal_3d = -1 * normal_3d 745 | screen_no = Vector((normal_3d[0],normal_3d[1])) 746 | angle = math.atan2(screen_no[1],screen_no[0]) - 1/2 * math.pi 747 | left = angle + math.pi 748 | right = angle 749 | self.snap = [path, end_cut] 750 | 751 | if end_cut.desc == 'CUT_LINE' and len(path.cuts) > 1: 752 | 753 | self.snap_circle = contour_utilities.pi_slice(best_vert[0], best_vert[1], settings.extend_radius, 0.1 * settings.extend_radius, left, right, 20, t_fan=True) 754 | self.snap_circle.append(self.snap_circle[0]) 755 | else: 756 | self.snap_circle = contour_utilities.simple_circle(best_vert[0], best_vert[1], settings.extend_radius, 20) 757 | self.snap_circle.append(self.snap_circle[0]) 758 | 759 | breakout = True 760 | if best < settings.extend_radius: 761 | snapped = True 762 | self.snap_color = (stroke_color) 763 | 764 | else: 765 | alpha = 1 - best/(2*settings.extend_radius) 766 | self.snap_color = (mesh_color) 767 | 768 | break 769 | 770 | if breakout: 771 | break 772 | 773 | if not breakout: 774 | self.snap = [] 775 | self.snap_circle = [] 776 | 777 | 778 | def hover_loop_mode(self, context, settings, event): 779 | ''' 780 | Handles mouse selection and hovering 781 | ''' 782 | #identify hover target for highlighting 783 | if self.cut_paths != []: 784 | 785 | new_target = False 786 | target_at_all = False 787 | 788 | for path in self.cut_paths: 789 | for c_cut in path.cuts: 790 | if not c_cut.select: 791 | c_cut.unhighlight(settings) 792 | 793 | h_target = c_cut.active_element(context,event.mouse_region_x,event.mouse_region_y) 794 | if h_target: 795 | c_cut.highlight(settings) 796 | target_at_all = True 797 | 798 | if (h_target != self.hover_target) or (h_target.select and not self.cut_line_widget): 799 | 800 | self.hover_target = h_target 801 | if self.hover_target.desc == 'CUT_LINE': 802 | 803 | if self.hover_target.select: 804 | for possible_parent in self.cut_paths: 805 | if self.hover_target in possible_parent.cuts: 806 | parent_path = possible_parent 807 | break 808 | 809 | self.cut_line_widget = CutLineManipulatorWidget(context, 810 | settings, 811 | self.original_form, self.bme, 812 | self.hover_target, 813 | parent_path, 814 | event.mouse_region_x, 815 | event.mouse_region_y) 816 | self.cut_line_widget.derive_screen(context) 817 | 818 | else: 819 | self.cut_line_widget = None 820 | 821 | else: 822 | if self.cut_line_widget: 823 | self.cut_line_widget.x = event.mouse_region_x 824 | self.cut_line_widget.y = event.mouse_region_y 825 | self.cut_line_widget.derive_screen(context) 826 | #elif not c_cut.select: 827 | #c_cut.geom_color = (settings.geom_rgb[0],settings.geom_rgb[1],settings.geom_rgb[2],1) 828 | if not target_at_all: 829 | self.hover_target = None 830 | self.cut_line_widget = None 831 | 832 | 833 | def new_path_from_draw(self, context, settings): 834 | ''' 835 | package all the steps needed to make a new path 836 | TODO: What if errors? 837 | ''' 838 | path = ContourCutSeries(context, self.draw_cache, 839 | segments=settings.ring_count, 840 | ring_segments=settings.vertex_count, 841 | cull_factor=settings.cull_factor, 842 | smooth_factor=settings.smooth_factor, 843 | feature_factor=settings.feature_factor) 844 | 845 | 846 | path.ray_cast_path(context, self.original_form) 847 | if len(path.raw_world) == 0: 848 | print('NO RAW PATH') 849 | return None 850 | path.find_knots() 851 | 852 | if self.snap != [] and not self.force_new: 853 | merge_series = self.snap[0] 854 | merge_ring = self.snap[1] 855 | 856 | path.snap_merge_into_other(merge_series, merge_ring, context, self.original_form, self.bme) 857 | 858 | return merge_series 859 | 860 | path.smooth_path(context, ob = self.original_form) 861 | path.create_cut_nodes(context) 862 | path.snap_to_object(self.original_form, raw=False, world=False, cuts=True) 863 | path.cuts_on_path(context, self.original_form, self.bme) 864 | path.connect_cuts_to_make_mesh(self.original_form) 865 | path.backbone_from_cuts(context, self.original_form, self.bme) 866 | path.update_visibility(context, self.original_form) 867 | if path.cuts: 868 | # TODO: should this ever be empty? 869 | path.cuts[-1].do_select(settings) 870 | 871 | self.cut_paths.append(path) 872 | 873 | return path 874 | 875 | 876 | def click_new_cut(self, context, settings, event): 877 | 878 | new_cut = ContourCutLine(event.mouse_region_x, event.mouse_region_y) 879 | 880 | for path in self.cut_paths: 881 | for cut in path.cuts: 882 | cut.deselect(settings) 883 | 884 | new_cut.do_select(settings) 885 | self.cut_lines.append(new_cut) 886 | 887 | return new_cut 888 | 889 | 890 | def release_place_cut(self, context, settings, event): 891 | self.selected.tail.x = event.mouse_region_x 892 | self.selected.tail.y = event.mouse_region_y 893 | 894 | width = Vector((self.selected.head.x, self.selected.head.y)) - Vector((self.selected.tail.x, self.selected.tail.y)) 895 | 896 | # Prevent small errant strokes 897 | if width.length < 20: #TODO: Setting for minimum pixel width 898 | self.cut_lines.remove(self.selected) 899 | self.selected = None 900 | print('Placed cut is too short') 901 | return 902 | 903 | # Hit the mesh for the first time 904 | hit = self.selected.hit_object(context, self.original_form, method='VIEW') 905 | 906 | if not hit: 907 | self.cut_lines.remove(self.selected) 908 | self.selected = None 909 | print('Placed cut did not hit the mesh') 910 | return 911 | 912 | self.selected.cut_object(context, self.original_form, self.bme) 913 | self.selected.simplify_cross(self.segments) 914 | self.selected.update_com() 915 | self.selected.update_screen_coords(context) 916 | self.selected.head = None 917 | self.selected.tail = None 918 | 919 | if not len(self.selected.verts) or not len(self.selected.verts_simple): 920 | self.selected = None 921 | print('cut failure') #TODO, header text message. 922 | 923 | return 924 | 925 | if settings.debug > 1: 926 | print('release_place_cut') 927 | print('len(self.cut_paths) = %d' % len(self.cut_paths)) 928 | print('self.force_new = ' + str(self.force_new)) 929 | 930 | if self.cut_paths != [] and not self.force_new: 931 | for path in self.cut_paths: 932 | if path.insert_new_cut(context, self.original_form, self.bme, self.selected, search=settings.search_factor): 933 | # The cut belongs to the series now 934 | path.connect_cuts_to_make_mesh(self.original_form) 935 | path.update_visibility(context, self.original_form) 936 | path.seg_lock = True 937 | path.do_select(settings) 938 | path.unhighlight(settings) 939 | self.selected_path = path 940 | self.cut_lines.remove(self.selected) 941 | for other_path in self.cut_paths: 942 | if other_path != self.selected_path: 943 | other_path.deselect(settings) 944 | # No need to search for more paths 945 | return 946 | 947 | # Create a blank segment 948 | path = ContourCutSeries(context, [], 949 | cull_factor=settings.cull_factor, 950 | smooth_factor=settings.smooth_factor, 951 | feature_factor=settings.feature_factor) 952 | 953 | path.insert_new_cut(context, self.original_form, self.bme, self.selected, search=settings.search_factor) 954 | path.seg_lock = False # Not locked yet...not until a 2nd cut is added in loop mode 955 | path.segments = 1 956 | path.ring_segments = len(self.selected.verts_simple) 957 | path.connect_cuts_to_make_mesh(self.original_form) 958 | path.update_visibility(context, self.original_form) 959 | 960 | for other_path in self.cut_paths: 961 | other_path.deselect(settings) 962 | 963 | self.cut_paths.append(path) 964 | self.selected_path = path 965 | path.do_select(settings) 966 | 967 | self.cut_lines.remove(self.selected) 968 | self.force_new = False 969 | 970 | 971 | def finish_mesh(self, context): 972 | back_to_edit = (context.mode == 'EDIT_MESH') 973 | 974 | # This is where all the magic happens 975 | print('pushing data into bmesh') 976 | for path in self.cut_paths: 977 | path.push_data_into_bmesh(context, self.destination_ob, self.dest_bme, self.original_form, self.dest_me) 978 | 979 | if back_to_edit: 980 | print('updating edit mesh') 981 | bmesh.update_edit_mesh(self.dest_me, tessface=False, destructive=True) 982 | 983 | else: 984 | # Write the data into the object 985 | print('write data into the object') 986 | self.dest_bme.to_mesh(self.dest_me) 987 | 988 | # Remember we created a new object 989 | print('link destination object') 990 | context.scene.objects.link(self.destination_ob) 991 | 992 | print('select and make active') 993 | self.destination_ob.select = True 994 | context.scene.objects.active = self.destination_ob 995 | 996 | if context.space_data.local_view: 997 | view_loc = context.space_data.region_3d.view_location.copy() 998 | view_rot = context.space_data.region_3d.view_rotation.copy() 999 | view_dist = context.space_data.region_3d.view_distance 1000 | bpy.ops.view3d.localview() 1001 | bpy.ops.view3d.localview() 1002 | #context.space_data.region_3d.view_matrix = mx_copy 1003 | context.space_data.region_3d.view_location = view_loc 1004 | context.space_data.region_3d.view_rotation = view_rot 1005 | context.space_data.region_3d.view_distance = view_dist 1006 | context.space_data.region_3d.update() 1007 | 1008 | print('wrap up') 1009 | context.area.header_text_set() 1010 | contour_utilities.callback_cleanup(self,context) 1011 | if self._timer: 1012 | context.window_manager.event_timer_remove(self._timer) 1013 | 1014 | print('finished mesh!') 1015 | return {'FINISHED'} 1016 | 1017 | 1018 | def widget_transform(self, context, settings, event): 1019 | 1020 | self.cut_line_widget.user_interaction(context, event.mouse_region_x, event.mouse_region_y, shift=event.shift) 1021 | 1022 | self.selected.cut_object(context, self.original_form, self.bme) 1023 | self.selected.simplify_cross(self.selected_path.ring_segments) 1024 | self.selected.update_com() 1025 | self.selected_path.align_cut(self.selected, mode='BETWEEN', fine_grain=True) 1026 | 1027 | self.selected_path.connect_cuts_to_make_mesh(self.original_form) 1028 | self.selected_path.update_visibility(context, self.original_form) 1029 | 1030 | self.temporary_message_start(context, 'WIDGET_TRANSFORM: ' + str(self.cut_line_widget.transform_mode)) 1031 | 1032 | 1033 | def guide_arrow_shift(self, context, event): 1034 | if event.type == 'LEFT_ARROW': 1035 | for cut in self.selected_path.cuts: 1036 | cut.shift += 0.05 1037 | cut.simplify_cross(self.selected_path.ring_segments) 1038 | else: 1039 | for cut in self.selected_path.cuts: 1040 | cut.shift += -0.05 1041 | cut.simplify_cross(self.selected_path.ring_segments) 1042 | 1043 | self.selected_path.connect_cuts_to_make_mesh(self.original_form) 1044 | self.selected_path.update_visibility(context, self.original_form) 1045 | 1046 | 1047 | def loop_arrow_shift(self, context, event): 1048 | if event.type == 'LEFT_ARROW': 1049 | self.selected.shift += 0.05 1050 | 1051 | else: 1052 | self.selected.shift += -0.05 1053 | 1054 | self.selected.simplify_cross(self.selected_path.ring_segments) 1055 | self.selected_path.connect_cuts_to_make_mesh(self.original_form) 1056 | self.selected_path.update_backbone(context, self.original_form, self.bme, self.selected, insert=False) 1057 | self.selected_path.update_visibility(context, self.original_form) 1058 | 1059 | self.temporary_message_start(context, self.mode +': Shift ' + str(self.selected.shift)) 1060 | 1061 | 1062 | def loop_align_modal(self, context, event): 1063 | if not event.ctrl and not event.shift: 1064 | act = 'BETWEEN' 1065 | 1066 | # Align ahead 1067 | elif event.ctrl and not event.shift: 1068 | act = 'FORWARD' 1069 | 1070 | # Align behind 1071 | elif event.shift and not event.ctrl: 1072 | act = 'BACKWARD' 1073 | 1074 | self.selected_path.align_cut(self.selected, mode=act, fine_grain=True) 1075 | self.selected.simplify_cross(self.selected_path.ring_segments) 1076 | 1077 | self.selected_path.connect_cuts_to_make_mesh(self.original_form) 1078 | self.selected_path.update_backbone(context, self.original_form, self.bme, self.selected, insert=False) 1079 | self.selected_path.update_visibility(context, self.original_form) 1080 | self.temporary_message_start(context, 'Align Loop: %s' % act) 1081 | 1082 | 1083 | def loop_hotkey_modal(self,context,event): 1084 | 1085 | if self.hot_key == 'G': 1086 | self.cut_line_widget = CutLineManipulatorWidget(context, 1087 | self.settings, 1088 | self.original_form, self.bme, 1089 | self.selected, 1090 | self.selected_path, 1091 | event.mouse_region_x,event.mouse_region_y, 1092 | hotkey=self.hot_key) 1093 | self.cut_line_widget.transform_mode = 'EDGE_SLIDE' 1094 | 1095 | elif self.hot_key == 'R': 1096 | # TODO...if CoM is off screen, then what? 1097 | screen_pivot = location_3d_to_region_2d(context.region,context.space_data.region_3d,self.selected.plane_com) 1098 | self.cut_line_widget = CutLineManipulatorWidget(context, self.settings, 1099 | self.original_form, self.bme, 1100 | self.selected, 1101 | self.selected_path, 1102 | screen_pivot[0],screen_pivot[1], 1103 | hotkey = self.hot_key) 1104 | self.cut_line_widget.transform_mode = 'ROTATE_VIEW' 1105 | 1106 | self.cut_line_widget.initial_x = event.mouse_region_x 1107 | self.cut_line_widget.initial_y = event.mouse_region_y 1108 | self.cut_line_widget.derive_screen(context) 1109 | 1110 | 1111 | def temporary_message_start(self, context, message): 1112 | self.msg_start_time = time.time() 1113 | if not self._timer: 1114 | self._timer = context.window_manager.event_timer_add(0.1, context.window) 1115 | 1116 | context.area.header_text_set(text = message) 1117 | 1118 | 1119 | def modal(self, context, event): 1120 | context.area.tag_redraw() 1121 | settings = context.user_preferences.addons[AL.FolderName].preferences 1122 | 1123 | if event.type == 'Z' and event.ctrl and event.value == 'PRESS': 1124 | self.temporary_message_start(context, "Undo Action") 1125 | self.undo_action() 1126 | 1127 | # Check messages 1128 | if event.type == 'TIMER': 1129 | now = time.time() 1130 | if now - self.msg_start_time > self.msg_duration: 1131 | if self._timer: 1132 | context.window_manager.event_timer_remove(self._timer) 1133 | self._timer = None 1134 | 1135 | if self.mode == 'GUIDE': 1136 | context.area.header_text_set(text=self.guide_msg) 1137 | else: 1138 | context.area.header_text_set(text=self.loop_msg) 1139 | 1140 | if self.modal_state == 'NAVIGATING': 1141 | 1142 | if (event.type in {'MOUSEMOVE', 1143 | 'MIDDLEMOUSE', 1144 | 'NUMPAD_2', 1145 | 'NUMPAD_4', 1146 | 'NUMPAD_6', 1147 | 'NUMPAD_8', 1148 | 'NUMPAD_1', 1149 | 'NUMPAD_3', 1150 | 'NUMPAD_5', 1151 | 'NUMPAD_7', 1152 | 'NUMPAD_9'} and event.value == 'RELEASE'): 1153 | 1154 | self.modal_state = 'WAITING' 1155 | self.post_update = True 1156 | return {'PASS_THROUGH'} 1157 | 1158 | if (event.type in {'TRACKPADPAN', 'TRACKPADZOOM'} or event.type.startswith('NDOF_')): 1159 | 1160 | self.modal_state = 'WAITING' 1161 | self.post_update = True 1162 | return {'PASS_THROUGH'} 1163 | 1164 | if self.mode == 'LOOP': 1165 | 1166 | if self.modal_state == 'WAITING': 1167 | 1168 | if (event.type in {'ESC','RIGHT_MOUSE'} and 1169 | event.value == 'PRESS'): 1170 | 1171 | context.area.header_text_set() 1172 | contour_utilities.callback_cleanup(self,context) 1173 | if self._timer: 1174 | context.window_manager.event_timer_remove(self._timer) 1175 | 1176 | return {'CANCELLED'} 1177 | 1178 | elif (event.type == 'TAB' and 1179 | event.value == 'PRESS'): 1180 | 1181 | self.mode = 'GUIDE' 1182 | self.selected = None #WHY? 1183 | if self.selected_path: 1184 | self.selected_path.highlight(settings) 1185 | 1186 | if self._timer: 1187 | context.window_manager.event_timer_remove(self._timer) 1188 | self._timer = None 1189 | 1190 | context.area.header_text_set(text=self.guide_msg) 1191 | 1192 | elif event.type == 'N' and event.value == 'PRESS': 1193 | self.force_new = self.force_new != True 1194 | #self.selected_path = None 1195 | self.snap = None 1196 | 1197 | self.temporary_message_start(context, self.mode +': FORCE NEW: ' + str(self.force_new)) 1198 | return {'RUNNING_MODAL'} 1199 | 1200 | elif (event.type in {'RET', 'NUMPAD_ENTER'} and 1201 | event.value == 'PRESS'): 1202 | 1203 | return self.finish_mesh(context) 1204 | 1205 | if event.type == 'MOUSEMOVE': 1206 | self.hover_loop_mode(context, settings, event) 1207 | 1208 | elif (event.type == 'C' and 1209 | event.value == 'PRESS'): 1210 | 1211 | bpy.ops.view3d.view_center_cursor() 1212 | self.temporary_message_start(context, 'Center View to Cursor') 1213 | return {'RUNNING_MODAL'} 1214 | 1215 | elif event.type == 'S' and event.value == 'PRESS' and event.shift: 1216 | if self.selected: 1217 | context.scene.cursor_location = self.selected.plane_com 1218 | self.temporary_message_start(context, 'Cursor to selected loop or segment') 1219 | 1220 | # Navigation Keys 1221 | elif (event.type in {'MIDDLEMOUSE', 1222 | 'NUMPAD_2', 1223 | 'NUMPAD_4', 1224 | 'NUMPAD_6', 1225 | 'NUMPAD_8', 1226 | 'NUMPAD_1', 1227 | 'NUMPAD_3', 1228 | 'NUMPAD_5', 1229 | 'NUMPAD_7', 1230 | 'NUMPAD_9'} and event.value == 'PRESS'): 1231 | 1232 | self.modal_state = 'NAVIGATING' 1233 | self.post_update = True 1234 | self.temporary_message_start(context, self.mode + ': NAVIGATING') 1235 | 1236 | return {'PASS_THROUGH'} 1237 | 1238 | elif (event.type in {'TRACKPADPAN', 'TRACKPADZOOM'} or event.type.startswith('NDOF_')): 1239 | 1240 | self.modal_state = 'NAVIGATING' 1241 | self.post_update = True 1242 | self.temporary_message_start(context, 'NAVIGATING') 1243 | 1244 | return {'PASS_THROUGH'} 1245 | 1246 | # Zoom Keys 1247 | elif (event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'} and not 1248 | (event.ctrl or event.shift)): 1249 | 1250 | self.post_update = True 1251 | return{'PASS_THROUGH'} 1252 | 1253 | elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': 1254 | 1255 | if self.hover_target and self.hover_target != self.selected: 1256 | 1257 | self.selected = self.hover_target 1258 | if not event.shift: 1259 | for path in self.cut_paths: 1260 | for cut in path.cuts: 1261 | cut.deselect(settings) 1262 | if self.selected in path.cuts and path != self.selected_path: 1263 | path.do_select(settings) 1264 | path.unhighlight(settings) 1265 | self.selected_path = path 1266 | else: 1267 | path.deselect(settings) 1268 | 1269 | # Select the ring 1270 | self.hover_target.do_select(settings) 1271 | 1272 | elif self.hover_target and self.hover_target == self.selected: 1273 | 1274 | self.create_undo_snapshot('WIDGET_TRANSFORM') 1275 | self.modal_state = 'WIDGET_TRANSFORM' 1276 | # Sometimes, there is not a widget from the hover? 1277 | self.cut_line_widget = CutLineManipulatorWidget(context, 1278 | settings, 1279 | self.original_form, self.bme, 1280 | self.hover_target, 1281 | self.selected_path, 1282 | event.mouse_region_x, 1283 | event.mouse_region_y) 1284 | self.cut_line_widget.derive_screen(context) 1285 | 1286 | else: 1287 | self.create_undo_snapshot('CUTTING') 1288 | self.modal_state = 'CUTTING' 1289 | self.temporary_message_start(context, self.mode + ': CUTTING') 1290 | # Make a new cut and handle it with self.selected 1291 | self.selected = self.click_new_cut(context, settings, event) 1292 | 1293 | return {'RUNNING_MODAL'} 1294 | 1295 | if self.selected: 1296 | #print(event.type + " " + event.value) 1297 | 1298 | #G -> HOTKEY 1299 | if event.type == 'G' and event.value == 'PRESS': 1300 | 1301 | self.create_undo_snapshot('HOTKEY_TRANSFORM') 1302 | self.modal_state = 'HOTKEY_TRANSFORM' 1303 | self.hot_key = 'G' 1304 | self.loop_hotkey_modal(context,event) 1305 | self.temporary_message_start(context, self.mode + ':Hotkey Grab') 1306 | return {'RUNNING_MODAL'} 1307 | #R -> HOTKEY 1308 | if event.type == 'R' and event.value == 'PRESS': 1309 | 1310 | self.create_undo_snapshot('HOTKEY_TRANSFORM') 1311 | self.modal_state = 'HOTKEY_TRANSFORM' 1312 | self.hot_key = 'R' 1313 | self.loop_hotkey_modal(context,event) 1314 | self.temporary_message_start(context, self.mode + ':Hotkey Rotate') 1315 | return {'RUNNING_MODAL'} 1316 | 1317 | #X, DEL -> DELETE 1318 | elif event.type == 'X' and event.value == 'PRESS': 1319 | 1320 | self.create_undo_snapshot('DELETE') 1321 | if len(self.selected_path.cuts) > 1 or (len(self.selected_path.cuts) == 1 and self.selected_path.existing_head): 1322 | self.selected_path.remove_cut(context, self.original_form, self.bme, self.selected) 1323 | self.selected_path.connect_cuts_to_make_mesh(self.original_form) 1324 | self.selected_path.update_visibility(context, self.original_form) 1325 | self.selected_path.backbone_from_cuts(context, self.original_form, self.bme) 1326 | 1327 | else: 1328 | self.cut_paths.remove(self.selected_path) 1329 | self.selected_path = None 1330 | 1331 | self.selected = None 1332 | self.temporary_message_start(context, self.mode + ': DELETE') 1333 | 1334 | #S -> CURSOR SELECTED CoM 1335 | 1336 | #LEFT_ARROW, RIGHT_ARROW to shift 1337 | elif (event.type in {'LEFT_ARROW', 'RIGHT_ARROW'} and 1338 | event.value == 'PRESS'): 1339 | self.create_undo_snapshot('LOOP_SHIFT') 1340 | self.loop_arrow_shift(context,event) 1341 | 1342 | return {'RUNNING_MODAL'} 1343 | 1344 | elif event.type == 'A' and event.value == 'PRESS': 1345 | self.create_undo_snapshot('ALIGN') 1346 | self.loop_align_modal(context,event) 1347 | 1348 | return {'RUNNING_MODAL'} 1349 | 1350 | elif ((event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'} and event.ctrl) or 1351 | (event.type in {'NUMPAD_PLUS','NUMPAD_MINUS'} and event.value == 'PRESS') and event.ctrl): 1352 | 1353 | self.create_undo_snapshot('RING_SEGMENTS') 1354 | if not self.selected_path.ring_lock: 1355 | old_segments = self.selected_path.ring_segments 1356 | self.selected_path.ring_segments += 1 - 2 * (event.type == 'WHEELDOWNMOUSE' or event.type == 'NUMPAD_MINUS') 1357 | if self.selected_path.ring_segments < 3: 1358 | self.selected_path.ring_segments = 3 1359 | 1360 | for cut in self.selected_path.cuts: 1361 | new_bulk_shift = round(cut.shift * old_segments/self.selected_path.ring_segments) 1362 | new_fine_shift = old_segments/self.selected_path.ring_segments * cut.shift - new_bulk_shift 1363 | 1364 | new_shift = self.selected_path.ring_segments/old_segments * cut.shift 1365 | 1366 | print(new_shift - new_bulk_shift - new_fine_shift) 1367 | cut.shift = new_shift 1368 | cut.simplify_cross(self.selected_path.ring_segments) 1369 | 1370 | self.selected_path.backbone_from_cuts(context, self.original_form, self.bme) 1371 | self.selected_path.connect_cuts_to_make_mesh(self.original_form) 1372 | self.selected_path.update_visibility(context, self.original_form) 1373 | 1374 | self.temporary_message_start(context, self.mode +': RING SEGMENTS %i' %self.selected_path.ring_segments) 1375 | self.msg_start_time = time.time() 1376 | else: 1377 | self.temporary_message_start(context, self.mode +': RING SEGMENTS: Can not be changed. Path Locked') 1378 | 1379 | #else: 1380 | #let the user know the path is locked 1381 | #header message set 1382 | 1383 | return {'RUNNING_MODAL'} 1384 | #if hover == selected: 1385 | #LEFTCLICK -> WIDGET 1386 | 1387 | return {'RUNNING_MODAL'} 1388 | 1389 | elif self.modal_state == 'CUTTING': 1390 | 1391 | if event.type == 'MOUSEMOVE': 1392 | #pass mouse coords to widget 1393 | x = str(event.mouse_region_x) 1394 | y = str(event.mouse_region_y) 1395 | message = self.mode + ':CUTTING: X: ' + x + ' Y: ' + y 1396 | context.area.header_text_set(text=message) 1397 | 1398 | self.selected.tail.x = event.mouse_region_x 1399 | self.selected.tail.y = event.mouse_region_y 1400 | #self.seleted.screen_to_world(context) 1401 | 1402 | return {'RUNNING_MODAL'} 1403 | 1404 | elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': 1405 | 1406 | #the new cut is created 1407 | #the new cut is assessed to be placed into an existing series 1408 | #the new cut is assessed to be an extension of selected gemometry 1409 | #the new cut is assessed to become the beginning of a new path 1410 | self.release_place_cut(context, settings, event) 1411 | 1412 | # We return to waiting 1413 | self.modal_state = 'WAITING' 1414 | return {'RUNNING_MODAL'} 1415 | 1416 | elif self.modal_state == 'HOTKEY_TRANSFORM': 1417 | if self.hot_key == 'G': 1418 | action = 'Grab' 1419 | elif self.hot_key == 'R': 1420 | action = 'Rotate' 1421 | 1422 | if event.shift: 1423 | action = 'FINE CONTROL ' + action 1424 | 1425 | if event.type == 'MOUSEMOVE': 1426 | # Pass mouse coords to widget 1427 | x = str(event.mouse_region_x) 1428 | y = str(event.mouse_region_y) 1429 | message = self.mode + ": " + action + ": X: " + x + ' Y: ' + y 1430 | self.temporary_message_start(context, message) 1431 | 1432 | # Widget.user_interaction 1433 | self.cut_line_widget.user_interaction(context, event.mouse_region_x,event.mouse_region_y) 1434 | self.selected.cut_object(context, self.original_form, self.bme) 1435 | self.selected.simplify_cross(self.selected_path.ring_segments) 1436 | self.selected.update_com() 1437 | self.selected_path.align_cut(self.selected, mode='BETWEEN', fine_grain=True) 1438 | 1439 | self.selected_path.connect_cuts_to_make_mesh(self.original_form) 1440 | self.selected_path.update_visibility(context, self.original_form) 1441 | return {'RUNNING_MODAL'} 1442 | 1443 | 1444 | #LEFTMOUSE event.value == 'PRESS':#RET, ENTER 1445 | if (event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER'} and 1446 | event.value == 'PRESS'): 1447 | #confirm transform 1448 | #recut, align, visibility?, and update the segment 1449 | self.selected_path.update_backbone(context, self.original_form, self.bme, self.selected, insert=False) 1450 | self.modal_state = 'WAITING' 1451 | return {'RUNNING_MODAL'} 1452 | 1453 | if (event.type in {'ESC', 'RIGHTMOUSE'} and 1454 | event.value == 'PRESS'): 1455 | self.cut_line_widget.cancel_transform() 1456 | self.selected.cut_object(context, self.original_form, self.bme) 1457 | self.selected.simplify_cross(self.selected_path.ring_segments) 1458 | self.selected_path.align_cut(self.selected, mode='BETWEEN', fine_grain=True) 1459 | self.selected.update_com() 1460 | 1461 | self.selected_path.connect_cuts_to_make_mesh(self.original_form) 1462 | self.selected_path.update_visibility(context, self.original_form) 1463 | self.modal_state = 'WAITING' 1464 | return {'RUNNING_MODAL'} 1465 | 1466 | elif self.modal_state == 'WIDGET_TRANSFORM': 1467 | 1468 | # Mouse move 1469 | if event.type == 'MOUSEMOVE': 1470 | if event.shift: 1471 | action = 'FINE WIDGET' 1472 | else: 1473 | action = 'WIDGET' 1474 | 1475 | self.widget_transform(context, settings, event) 1476 | 1477 | return {'RUNNING_MODAL'} 1478 | 1479 | elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': 1480 | #destroy the widget 1481 | self.cut_line_widget = None 1482 | self.modal_state = 'WAITING' 1483 | self.selected_path.update_backbone(context, self.original_form, self.bme, self.selected, insert=False) 1484 | 1485 | return {'RUNNING_MODAL'} 1486 | 1487 | elif event.type in {'RIGHTMOUSE', 'ESC'} and event.value == 'PRESS' and self.hot_key: 1488 | self.cut_line_widget.cancel_transform() 1489 | self.selected.cut_object(context, self.original_form, self.bme) 1490 | self.selected.simplify_cross(self.selected_path.ring_segments) 1491 | self.selected.update_com() 1492 | 1493 | self.selected_path.connect_cuts_to_make_mesh(self.original_form) 1494 | self.selected_path.update_visibility(context, self.original_form) 1495 | 1496 | return {'RUNNING_MODAL'} 1497 | 1498 | return{'RUNNING_MODAL'} 1499 | 1500 | if self.mode == 'GUIDE': 1501 | 1502 | if self.modal_state == 'WAITING': 1503 | # Navigation Keys 1504 | if (event.type in {'MIDDLEMOUSE', 1505 | 'NUMPAD_2', 1506 | 'NUMPAD_4', 1507 | 'NUMPAD_6', 1508 | 'NUMPAD_8', 1509 | 'NUMPAD_1', 1510 | 'NUMPAD_3', 1511 | 'NUMPAD_5', 1512 | 'NUMPAD_7', 1513 | 'NUMPAD_9'} and event.value == 'PRESS'): 1514 | 1515 | self.modal_state = 'NAVIGATING' 1516 | self.post_update = True 1517 | self.temporary_message_start(context, 'NAVIGATING') 1518 | 1519 | return {'PASS_THROUGH'} 1520 | 1521 | elif (event.type in {'ESC','RIGHT_MOUSE'} and 1522 | event.value == 'PRESS'): 1523 | 1524 | context.area.header_text_set() 1525 | contour_utilities.callback_cleanup(self,context) 1526 | if self._timer: 1527 | context.window_manager.event_timer_remove(self._timer) 1528 | 1529 | return {'CANCELLED'} 1530 | 1531 | elif (event.type in {'RET', 'NUMPAD_ENTER'} and 1532 | event.value == 'PRESS'): 1533 | 1534 | return self.finish_mesh(context) 1535 | 1536 | elif (event.type in {'TRACKPADPAN', 'TRACKPADZOOM'} or event.type.startswith('NDOF_')): 1537 | 1538 | self.modal_state = 'NAVIGATING' 1539 | self.post_update = True 1540 | self.temporary_message_start(context, 'NAVIGATING') 1541 | 1542 | return {'PASS_THROUGH'} 1543 | 1544 | # ZOOM KEYS 1545 | elif (event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'} and not 1546 | (event.ctrl or event.shift)): 1547 | 1548 | self.post_update = True 1549 | self.temporary_message_start(context, 'ZOOM') 1550 | return{'PASS_THROUGH'} 1551 | 1552 | elif event.type == 'TAB' and event.value == 'PRESS': 1553 | self.mode = 'LOOP' 1554 | self.snap_circle = [] 1555 | 1556 | if self.selected_path: 1557 | self.selected_path.unhighlight(settings) 1558 | 1559 | if self._timer: 1560 | context.window_manager.event_timer_remove(self._timer) 1561 | self._timer = None 1562 | 1563 | context.area.header_text_set(text = self.loop_msg) 1564 | return {'RUNNING_MODAL'} 1565 | 1566 | elif event.type == 'C' and event.value == 'PRESS': 1567 | #center cursor 1568 | bpy.ops.view3d.view_center_cursor() 1569 | self.temporary_message_start(context, 'Center View to Cursor') 1570 | return {'RUNNING_MODAL'} 1571 | 1572 | elif event.type == 'N' and event.value == 'PRESS': 1573 | self.force_new = self.force_new != True 1574 | #self.selected_path = None 1575 | self.snap = None 1576 | 1577 | self.temporary_message_start(context, self.mode +': FORCE NEW: ' + str(self.force_new)) 1578 | return {'RUNNING_MODAL'} 1579 | 1580 | elif event.type == 'MOUSEMOVE': 1581 | 1582 | self.hover_guide_mode(context, settings, event) 1583 | 1584 | return {'RUNNING_MODAL'} 1585 | 1586 | elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': 1587 | if self.hover_target and self.hover_target.desc == 'CUT SERIES': 1588 | self.hover_target.do_select(settings) 1589 | self.selected_path = self.hover_target 1590 | 1591 | for path in self.cut_paths: 1592 | if path != self.hover_target: 1593 | path.deselect(settings) 1594 | else: 1595 | self.create_undo_snapshot('DRAW_PATH') 1596 | self.modal_state = 'DRAWING' 1597 | self.temporary_message_start(context, 'DRAWING') 1598 | 1599 | return {'RUNNING_MODAL'} 1600 | 1601 | if self.selected_path: 1602 | 1603 | if event.type in {'X', 'DEL'} and event.value == 'PRESS': 1604 | 1605 | self.create_undo_snapshot('DELETE') 1606 | self.cut_paths.remove(self.selected_path) 1607 | self.selected_path = None 1608 | self.modal_state = 'WAITING' 1609 | self.temporary_message_start(context, 'DELETED PATH') 1610 | 1611 | return {'RUNNING_MODAL'} 1612 | 1613 | elif (event.type in {'LEFT_ARROW', 'RIGHT_ARROW'} and 1614 | event.value == 'PRESS'): 1615 | 1616 | self.create_undo_snapshot('PATH_SHIFT') 1617 | self.guide_arrow_shift(context, event) 1618 | 1619 | # Shift entire segment 1620 | self.temporary_message_start(context, 'Shift entire segment') 1621 | return {'RUNNING_MODAL'} 1622 | 1623 | elif ((event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'} and event.ctrl) or 1624 | (event.type in {'NUMPAD_PLUS','NUMPAD_MINUS'} and event.value == 'PRESS')): 1625 | 1626 | # If not selected_path.lock: 1627 | # TODO: path.locked 1628 | # TODO: dont recalc the path when no change happens 1629 | if event.type in {'WHEELUPMOUSE','NUMPAD_PLUS'}: 1630 | if not self.selected_path.seg_lock: 1631 | self.create_undo_snapshot('PATH_SEGMENTS') 1632 | self.selected_path.segments += 1 1633 | elif event.type in {'WHEELDOWNMOUSE', 'NUMPAD_MINUS'} and self.selected_path.segments > 3: 1634 | if not self.selected_path.seg_lock: 1635 | self.create_undo_snapshot('PATH_SEGMENTS') 1636 | self.selected_path.segments -= 1 1637 | 1638 | if not self.selected_path.seg_lock: 1639 | self.selected_path.create_cut_nodes(context) 1640 | self.selected_path.snap_to_object(self.original_form, raw = False, world = False, cuts = True) 1641 | self.selected_path.cuts_on_path(context, self.original_form, self.bme) 1642 | self.selected_path.connect_cuts_to_make_mesh(self.original_form) 1643 | self.selected_path.update_visibility(context, self.original_form) 1644 | self.selected_path.backbone_from_cuts(context, self.original_form, self.bme) 1645 | #selected will hold old reference because all cuts are recreated (dumbly, it should just be the in between ones) 1646 | self.selected = self.selected_path.cuts[-1] 1647 | self.temporary_message_start(context, 'PATH SEGMENTS: %i' % self.selected_path.segments) 1648 | 1649 | else: 1650 | self.temporary_message_start(context, 'PATH SEGMENTS: Path is locked, cannot adjust segments') 1651 | return {'RUNNING_MODAL'} 1652 | 1653 | elif event.type == 'S' and event.value == 'PRESS': 1654 | 1655 | if event.shift: 1656 | self.create_undo_snapshot('SMOOTH') 1657 | #path.smooth_normals 1658 | self.selected_path.average_normals(context, self.original_form, self.bme) 1659 | self.selected_path.connect_cuts_to_make_mesh(self.original_form) 1660 | self.selected_path.backbone_from_cuts(context, self.original_form, self.bme) 1661 | self.temporary_message_start(context, 'Smooth normals based on drawn path') 1662 | 1663 | elif event.ctrl: 1664 | self.create_undo_snapshot('SMOOTH') 1665 | #smooth CoM path 1666 | self.temporary_message_start(context, 'Smooth normals based on CoM path') 1667 | self.selected_path.smooth_normals_com(context, self.original_form, self.bme, iterations = 2) 1668 | self.selected_path.connect_cuts_to_make_mesh(self.original_form) 1669 | self.selected_path.backbone_from_cuts(context, self.original_form, self.bme) 1670 | elif event.alt: 1671 | self.create_undo_snapshot('SMOOTH') 1672 | #path.interpolate_endpoints 1673 | self.temporary_message_start(context, 'Smoothly interpolate normals between the endpoints') 1674 | self.selected_path.interpolate_endpoints(context, self.original_form, self.bme) 1675 | self.selected_path.connect_cuts_to_make_mesh(self.original_form) 1676 | self.selected_path.backbone_from_cuts(context, self.original_form, self.bme) 1677 | 1678 | else: 1679 | half = math.floor(len(self.selected_path.cuts)/2) 1680 | 1681 | if math.fmod(len(self.selected_path.cuts), 2): #5 segments is 6 rings 1682 | loc = 0.5 * (self.selected_path.cuts[half].plane_com + self.selected_path.cuts[half+1].plane_com) 1683 | else: 1684 | loc = self.selected_path.cuts[half].plane_com 1685 | 1686 | context.scene.cursor_location = loc 1687 | 1688 | return{'RUNNING_MODAL'} 1689 | 1690 | if self.modal_state == 'DRAWING': 1691 | 1692 | if event.type == 'MOUSEMOVE': 1693 | action = 'GUIDE MODE: Drawing' 1694 | x = str(event.mouse_region_x) 1695 | y = str(event.mouse_region_y) 1696 | # Record screen drawing 1697 | self.draw_cache.append((event.mouse_region_x,event.mouse_region_y)) 1698 | 1699 | return {'RUNNING_MODAL'} 1700 | 1701 | if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': 1702 | if len(self.draw_cache) > 10: 1703 | 1704 | for path in self.cut_paths: 1705 | path.deselect(settings) 1706 | 1707 | self.selected_path = self.new_path_from_draw(context, settings) 1708 | if self.selected_path: 1709 | self.selected_path.do_select(settings) 1710 | if self.selected_path.cuts: 1711 | self.selected = self.selected_path.cuts[-1] 1712 | else: 1713 | self.selected = None 1714 | if self.selected: 1715 | self.selected.do_select(settings) 1716 | 1717 | self.drag = False #TODO: is self.drag still needed? 1718 | self.force_new = False 1719 | 1720 | self.draw_cache = [] 1721 | 1722 | self.modal_state = 'WAITING' 1723 | return{'RUNNING_MODAL'} 1724 | 1725 | return{'RUNNING_MODAL'} 1726 | 1727 | 1728 | def create_undo_snapshot(self, action): 1729 | ''' 1730 | saves data and operator state snapshot 1731 | for undoing 1732 | 1733 | TODO: perhaps pop/append are not fastest way 1734 | deque? 1735 | prepare a list and keep track of which entity to 1736 | replace? 1737 | ''' 1738 | 1739 | repeated_actions = {'LOOP_SHIFT', 'PATH_SHIFT', 'PATH_SEGMENTS', 'LOOP_SEGMENTS'} 1740 | 1741 | if action in repeated_actions: 1742 | if action == contour_undo_cache[-1][2]: 1743 | print('repeatable...dont take snapshot') 1744 | return 1745 | 1746 | print('undo: ' + action) 1747 | cut_data = copy.deepcopy(self.cut_paths) 1748 | # Perhaps I don't even need to copy this? 1749 | state = copy.deepcopy(ContourStatePreserver(self)) 1750 | contour_undo_cache.append((cut_data, state, action)) 1751 | 1752 | if len(contour_undo_cache) > self.settings.undo_depth: 1753 | contour_undo_cache.pop(0) 1754 | 1755 | 1756 | def undo_action(self): 1757 | 1758 | if len(contour_undo_cache) > 0: 1759 | cut_data, op_state, action = contour_undo_cache.pop() 1760 | 1761 | self.cut_paths = cut_data 1762 | op_state.push_state(self) 1763 | 1764 | 1765 | def invoke(self, context, event): 1766 | # HINT you are in contours code 1767 | # TODO Settings harmon CODE REVIEW 1768 | settings = context.user_preferences.addons[AL.FolderName].preferences 1769 | 1770 | if context.space_data.viewport_shade in {'WIREFRAME','BOUNDBOX'}: 1771 | self.report({'ERROR'}, 'Viewport shading must be at lease SOLID') 1772 | return {'CANCELLED'} 1773 | 1774 | self.valid_cut_inds = [] 1775 | self.existing_loops = [] 1776 | 1777 | # This is a cache for any cut line whose connectivity 1778 | # has not been established. 1779 | self.cut_lines = [] 1780 | 1781 | # A list of all the cut paths (segments) 1782 | self.cut_paths = [] 1783 | # A list to store screen coords when drawing 1784 | self.draw_cache = [] 1785 | 1786 | # TODO Settings harmony CODE REVIEW 1787 | self.settings = settings 1788 | 1789 | # Default verts in a loop (spans) 1790 | self.segments = settings.vertex_count 1791 | # Default number of loops in a segment 1792 | self.guide_cuts = settings.ring_count 1793 | 1794 | # If edit mode 1795 | if context.mode == 'EDIT_MESH': 1796 | 1797 | # Retopo mesh is the active object 1798 | self.destination_ob = context.object #TODO: Clarify destination_ob as retopo_on consistent with design doc 1799 | 1800 | # Get the destination mesh data 1801 | self.dest_me = self.destination_ob.data 1802 | 1803 | # We will build this bmesh using from editmesh 1804 | self.dest_bme = bmesh.from_edit_mesh(self.dest_me) 1805 | 1806 | # The selected object will be the original form 1807 | # Or we wil pull the mesh cache 1808 | target = [ob for ob in context.selected_objects if ob.name != context.object.name][0] 1809 | 1810 | # This is a simple set of recorded properties meant to help detect 1811 | # If the mesh we are using is the same as the one in the cache. 1812 | is_valid = is_object_valid(target) 1813 | if is_valid: 1814 | use_cache = True 1815 | print('willing and able to use the cache!') 1816 | else: 1817 | use_cache = False #later, we will double check for ngons and things 1818 | clear_mesh_cache() 1819 | self.original_form = target 1820 | 1821 | # Count and collect the selected edges if any 1822 | ed_inds = [ed.index for ed in self.dest_bme.edges if ed.select] 1823 | 1824 | self.existing_loops = [] 1825 | if len(ed_inds): 1826 | vert_loops = contour_utilities.edge_loops_from_bmedges(self.dest_bme, ed_inds) 1827 | 1828 | if len(vert_loops) > 1: 1829 | self.report({'WARNING'}, 'Only one edge loop will be used for extension') 1830 | print('there are %i edge loops selected' % len(vert_loops)) 1831 | 1832 | # For loop in vert_loops: 1833 | # Until multi loops are supported, do this 1834 | loop = vert_loops[0] 1835 | if loop[-1] != loop[0] and len(list(set(loop))) != len(loop): 1836 | self.report({'WARNING'},'Edge loop selection has extra parts! Excluding this loop') 1837 | 1838 | else: 1839 | lverts = [self.dest_bme.verts[i] for i in loop] 1840 | 1841 | existing_loop =ExistingVertList(context, 1842 | lverts, 1843 | loop, 1844 | self.destination_ob.matrix_world, 1845 | key_type='INDS') 1846 | 1847 | # Make a blank path with just an existing head 1848 | path = ContourCutSeries(context, [], 1849 | cull_factor=settings.cull_factor, 1850 | smooth_factor=settings.smooth_factor, 1851 | feature_factor=settings.feature_factor) 1852 | 1853 | path.existing_head = existing_loop 1854 | path.seg_lock = False 1855 | path.ring_lock = True 1856 | path.ring_segments = len(existing_loop.verts_simple) 1857 | path.connect_cuts_to_make_mesh(target) 1858 | path.update_visibility(context, target) 1859 | 1860 | #path.update_visibility(context, self.original_form) 1861 | 1862 | self.cut_paths.append(path) 1863 | self.existing_loops.append(existing_loop) 1864 | 1865 | elif context.mode == 'OBJECT': 1866 | 1867 | # Make the irrelevant variables None 1868 | self.sel_edges = None 1869 | self.sel_verts = None 1870 | self.existing_cut = None 1871 | 1872 | # The active object will be the target 1873 | target = context.object 1874 | 1875 | is_valid = is_object_valid(target) 1876 | has_tmp = 'ContourTMP' in bpy.data.objects and bpy.data.objects['ContourTMP'].data 1877 | 1878 | if is_valid and has_tmp: 1879 | use_cache = True 1880 | else: 1881 | use_cache = False 1882 | self.original_form = target #TODO: Clarify original_form as reference_form consistent with design doc 1883 | 1884 | # No temp bmesh needed in object mode 1885 | # We will create a new obeject 1886 | self.tmp_bme = None 1887 | 1888 | # New blank mesh data 1889 | self.dest_me = bpy.data.meshes.new(target.name + "_recontour") 1890 | 1891 | # New object to hold mesh data 1892 | self.destination_ob = bpy.data.objects.new(target.name + "_recontour",self.dest_me) #this is an empty currently 1893 | self.destination_ob.matrix_world = target.matrix_world 1894 | self.destination_ob.update_tag() 1895 | 1896 | # Destination bmesh to operate on 1897 | self.dest_bme = bmesh.new() 1898 | self.dest_bme.from_mesh(self.dest_me) 1899 | 1900 | # Get the info about the original form 1901 | #and convert it to a bmesh for fast connectivity info 1902 | #or load the previous bme to save even more time 1903 | 1904 | if use_cache: 1905 | start = time.time() 1906 | print('the cache is valid for use!') 1907 | 1908 | self.bme = contour_mesh_cache['bme'] 1909 | print('loaded old bme in %f' % (time.time() - start)) 1910 | 1911 | start = time.time() 1912 | 1913 | self.tmp_ob = contour_mesh_cache['tmp'] 1914 | print('loaded old tmp ob in %f' % (time.time() - start)) 1915 | 1916 | if self.tmp_ob: 1917 | self.original_form = self.tmp_ob 1918 | else: 1919 | self.original_form = target 1920 | 1921 | else: 1922 | start = time.time() 1923 | 1924 | # Clear any old saved data 1925 | clear_mesh_cache() 1926 | 1927 | me = self.original_form.to_mesh(scene=context.scene, apply_modifiers=True, settings='PREVIEW') 1928 | me.update() 1929 | 1930 | self.bme = bmesh.new() 1931 | self.bme.from_mesh(me) 1932 | 1933 | # Check for ngons, and if there are any...triangulate just the ngons 1934 | #this mainly stems from the obj.ray_cast function returning triangulate 1935 | #results and that it makes my cross section method easier. 1936 | ngons = [] 1937 | for f in self.bme.faces: 1938 | if len(f.verts) > 4: 1939 | ngons.append(f) 1940 | if len(ngons) or len(self.original_form.modifiers) > 0: 1941 | print('Ngons or modifiers detected this is a real hassle just so you know') 1942 | 1943 | if len(ngons): 1944 | #new_geom = bmesh.ops.triangulate(self.bme, faces = ngons, use_beauty = True) 1945 | new_geom = bmesh.ops.triangulate(self.bme, faces=ngons, quad_method=0, ngon_method=1) 1946 | new_faces = new_geom['faces'] 1947 | 1948 | new_me = bpy.data.meshes.new('tmp_recontour_mesh') 1949 | self.bme.to_mesh(new_me) 1950 | new_me.update() 1951 | 1952 | self.tmp_ob = bpy.data.objects.new('ContourTMP', new_me) 1953 | 1954 | # I think this is needed to generate the data for raycasting 1955 | #there may be some other way to update the object 1956 | context.scene.objects.link(self.tmp_ob) 1957 | self.tmp_ob.update_tag() 1958 | context.scene.update() #this will slow things down 1959 | context.scene.objects.unlink(self.tmp_ob) 1960 | self.tmp_ob.matrix_world = self.original_form.matrix_world 1961 | 1962 | ###THIS IS A HUGELY IMPORTANT THING TO NOTICE!### 1963 | #so maybe I need to make it more apparent or write it differnetly# 1964 | #We are using a temporary duplicate to handle ray casting 1965 | #and triangulation 1966 | self.original_form = self.tmp_ob 1967 | 1968 | else: 1969 | self.tmp_ob = None 1970 | 1971 | #store this stuff for next time. We will most likely use it again 1972 | #keep in mind, in some instances, tmp_ob is self.original orm 1973 | #where as in others is it unique. We want to use "target" here to 1974 | #record validation because that is the the active or selected object 1975 | #which is visible in the scene with a unique name. 1976 | write_mesh_cache(target, self.tmp_ob, self.bme) 1977 | print('derived new bme and any triangulations in %f' % (time.time() - start)) 1978 | 1979 | message = "Segments: %i" % self.segments 1980 | context.area.header_text_set(text=message) 1981 | 1982 | # Here is where we will cache verts edges and faces 1983 | # Unti lthe user confirms and we output a real mesh. 1984 | self.verts = [] 1985 | self.edges = [] 1986 | self.faces = [] 1987 | 1988 | if settings.use_x_ray: 1989 | self.orig_x_ray = self.destination_ob.show_x_ray 1990 | self.destination_ob.show_x_ray = True 1991 | 1992 | ####MODE, UI, DRAWING, and MODAL variables### 1993 | self.mode = 'LOOP' 1994 | #'LOOP' or 'GUIDE' 1995 | 1996 | self.modal_state = 'WAITING' 1997 | 1998 | # Does the user want to extend an existing cut or make a new segment 1999 | self.force_new = False 2000 | 2001 | # Is the mouse clicked and held down 2002 | self.drag = False 2003 | self.navigating = False 2004 | self.post_update = False 2005 | 2006 | # What is the user dragging..a cutline, a handle etc 2007 | self.drag_target = None 2008 | 2009 | # Potential item for snapping in 2010 | self.snap = [] 2011 | self.snap_circle = [] 2012 | self.snap_color = (1, 0, 0, 1) 2013 | 2014 | # What is the mouse over top of currently 2015 | self.hover_target = None 2016 | # Keep track of selected cut_line and path 2017 | self.selected = None #TODO: Change this to selected_loop 2018 | if len(self.cut_paths) == 0: 2019 | self.selected_path = None #TODO: change this to selected_segment 2020 | else: 2021 | print('there is a selected_path') 2022 | self.selected_path = self.cut_paths[-1] #this would be an existing path from selected geom in editmode 2023 | 2024 | self.cut_line_widget = None #An object of Class "CutLineManipulator" or None 2025 | self.widget_interaction = False #Being in the state of interacting with a widget o 2026 | self.hot_key = None #Keep track of which hotkey was pressed 2027 | self.draw = False #Being in the state of drawing a guide stroke 2028 | 2029 | self.loop_msg = 'LOOP MODE: LMB: Select Stroke, X: Delete Sroke, , G: Translate, R: Rotate, Ctrl/Shift + A: Align, Shift + S: Cursor to Stroke, C: View to Cursor, N: Force New Segment, TAB: toggle Guide mode' 2030 | self.guide_msg = 'GUIDE MODE: LMB to Draw or Select, Ctrl/Shift/ALT + S to smooth, WHEEL or +/- to increase/decrease segments, TAB: toggle Loop mode' 2031 | context.area.header_text_set(self.loop_msg) 2032 | 2033 | if settings.recover and is_valid: 2034 | print('loading cache!') 2035 | self.undo_action() 2036 | else: 2037 | contour_undo_cache = [] 2038 | 2039 | # Add in the draw callback and modal method 2040 | self._handle = bpy.types.SpaceView3D.draw_handler_add(retopo_draw_callback, (self, context), 'WINDOW', 'POST_PIXEL') 2041 | 2042 | # Timer for temporary messages 2043 | self._timer = None 2044 | self.msg_start_time = time.time() 2045 | self.msg_duration = 0.75 2046 | 2047 | context.window_manager.modal_handler_add(self) 2048 | 2049 | return {'RUNNING_MODAL'} 2050 | 2051 | # Used to store keymaps for addon 2052 | addon_keymaps = [] 2053 | 2054 | # Registration 2055 | def register(): 2056 | bpy.utils.register_class(ContourToolsAddonPreferences) 2057 | bpy.utils.register_class(CGCOOKIE_OT_retopo_contour_panel) 2058 | bpy.utils.register_class(CGCOOKIE_OT_retopo_cache_clear) 2059 | bpy.utils.register_class(CGCOOKIE_OT_retopo_contour) 2060 | bpy.utils.register_class(CGCOOKIE_OT_retopo_contour_menu) 2061 | 2062 | # Create the addon hotkeys 2063 | kc = bpy.context.window_manager.keyconfigs.addon 2064 | 2065 | # Create the mode switch menu hotkey 2066 | km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') 2067 | kmi = km.keymap_items.new('wm.call_menu', 'V', 'PRESS', ctrl=True, shift=True) 2068 | kmi.properties.name = 'object.retopology_menu' 2069 | kmi.active = True 2070 | addon_keymaps.append((km, kmi)) 2071 | 2072 | 2073 | # Unregistration 2074 | def unregister(): 2075 | clear_mesh_cache() 2076 | bpy.utils.unregister_class(CGCOOKIE_OT_retopo_contour_menu) 2077 | bpy.utils.unregister_class(CGCOOKIE_OT_retopo_contour) 2078 | bpy.utils.unregister_class(CGCOOKIE_OT_retopo_cache_clear) 2079 | bpy.utils.unregister_class(CGCOOKIE_OT_retopo_contour_panel) 2080 | bpy.utils.unregister_class(ContourToolsAddonPreferences) 2081 | 2082 | # Remove addon hotkeys 2083 | for km, kmi in addon_keymaps: 2084 | km.keymap_items.remove(kmi) 2085 | addon_keymaps.clear() 2086 | -------------------------------------------------------------------------------- /contour_utilities.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (C) 2013 CG Cookie 3 | http://cgcookie.com 4 | hello@cgcookie.com 5 | 6 | Created by Patrick Moore 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | ''' 21 | 22 | import time 23 | import random 24 | import math 25 | from collections import deque 26 | from itertools import chain,combinations 27 | from mathutils import Vector, Matrix, Quaternion 28 | from mathutils.geometry import intersect_line_plane, intersect_point_line, distance_point_to_plane, intersect_line_line_2d, intersect_line_line 29 | 30 | import bgl 31 | import blf 32 | import bpy 33 | import bmesh 34 | 35 | from bpy_extras import view3d_utils 36 | from bpy_extras.view3d_utils import location_3d_to_region_2d, region_2d_to_vector_3d, region_2d_to_location_3d, region_2d_to_origin_3d 37 | 38 | 39 | def callback_register(self, context): 40 | #if str(bpy.app.build_revision)[2:7].lower == "unkno" or eval(str(bpy.app.build_revision)[2:7]) >= 53207: 41 | self._handle = bpy.types.SpaceView3D.draw_handler_add(self.menu.draw, (self, context), 'WINDOW', 'POST_PIXEL') 42 | #else: 43 | #self._handle = context.region.callback_add(self.menu.draw, (self, context), 'POST_PIXEL') 44 | #return None 45 | 46 | def callback_cleanup(self, context): 47 | #if str(bpy.app.build_revision)[2:7].lower() == "unkno" or eval(str(bpy.app.build_revision)[2:7]) >= 53207: 48 | bpy.types.SpaceView3D.draw_handler_remove(self._handle, "WINDOW") 49 | #else: 50 | #context.region.callback_remove(self._handle) 51 | #return None 52 | 53 | def bgl_col(rgb, alpha): 54 | ''' 55 | takes a Vector of len 3 (eg, a color setting) 56 | returns a 4 item tuple (r,g,b,a) for use with 57 | bgl drawing. 58 | ''' 59 | #TODO Test variables for acceptability 60 | color = (rgb[0], rgb[1], rgb[2], alpha) 61 | 62 | return color 63 | 64 | def draw_points(context, points, color, size): 65 | ''' 66 | draw a bunch of dots 67 | args: 68 | points: a list of tuples representing x,y SCREEN coordinate eg [(10,30),(11,31),...] 69 | color: tuple (r,g,b,a) 70 | size: integer? maybe a float 71 | ''' 72 | 73 | bgl.glColor4f(*color) 74 | bgl.glPointSize(size) 75 | bgl.glBegin(bgl.GL_POINTS) 76 | for coord in points: 77 | bgl.glVertex2f(*coord) 78 | 79 | bgl.glEnd() 80 | return 81 | 82 | def edge_loops_from_bmedges(bmesh, bm_edges): 83 | """ 84 | Edge loops defined by edges 85 | 86 | Takes [mesh edge indices] or a list of edges and returns the edge loops 87 | 88 | return a list of vertex indices. 89 | [ [1, 6, 7, 2], ...] 90 | 91 | closed loops have matching start and end values. 92 | """ 93 | line_polys = [] 94 | edges = bm_edges.copy() 95 | 96 | while edges: 97 | current_edge = bmesh.edges[edges.pop()] 98 | vert_e, vert_st = current_edge.verts[:] 99 | vert_end, vert_start = vert_e.index, vert_st.index 100 | line_poly = [vert_start, vert_end] 101 | 102 | ok = True 103 | while ok: 104 | ok = False 105 | #for i, ed in enumerate(edges): 106 | i = len(edges) 107 | while i: 108 | i -= 1 109 | ed = bmesh.edges[edges[i]] 110 | v_1, v_2 = ed.verts 111 | v1, v2 = v_1.index, v_2.index 112 | if v1 == vert_end: 113 | line_poly.append(v2) 114 | vert_end = line_poly[-1] 115 | ok = 1 116 | del edges[i] 117 | # break 118 | elif v2 == vert_end: 119 | line_poly.append(v1) 120 | vert_end = line_poly[-1] 121 | ok = 1 122 | del edges[i] 123 | #break 124 | elif v1 == vert_start: 125 | line_poly.insert(0, v2) 126 | vert_start = line_poly[0] 127 | ok = 1 128 | del edges[i] 129 | # break 130 | elif v2 == vert_start: 131 | line_poly.insert(0, v1) 132 | vert_start = line_poly[0] 133 | ok = 1 134 | del edges[i] 135 | #break 136 | line_polys.append(line_poly) 137 | 138 | return line_polys 139 | 140 | def perp_vector_point_line(pt1, pt2, ptn): 141 | ''' 142 | Vector bwettn pointn and line between point1 143 | and point2 144 | args: 145 | pt1, and pt1 are Vectors representing line segment 146 | 147 | return Vector 148 | 149 | pt1 ------------------- pt 150 | ^ 151 | | 152 | | 153 | |<-----this vector 154 | | 155 | ptn 156 | ''' 157 | pt_on_line = intersect_point_line(ptn, pt1, pt2)[0] 158 | alt_vect = pt_on_line - ptn 159 | 160 | return alt_vect 161 | 162 | def altitude(point1, point2, pointn): 163 | edge1 = point2 - point1 164 | edge2 = pointn - point1 165 | if edge2.length == 0: 166 | altitude = 0 167 | return altitude 168 | if edge1.length == 0: 169 | altitude = edge2.length 170 | return altitude 171 | alpha = edge1.angle(edge2) 172 | altitude = math.sin(alpha) * edge2.length 173 | 174 | return altitude 175 | 176 | # iterate through verts 177 | def iterate(points, newVerts, error,method = 1): 178 | ''' 179 | args: 180 | points - list of vectors in order representing locations on a curve 181 | newVerts - list of indices? (mapping to arg: points) of aready identified "new" verts 182 | error - distance obove/below chord which makes vert considered a feature 183 | 184 | return: 185 | new - list of vertex indicies (mappint to arg points) representing identified feature points 186 | or 187 | false - no new feature points identified...algorithm is finished. 188 | ''' 189 | new = [] 190 | for newIndex in range(len(newVerts)-1): 191 | bigVert = 0 192 | alti_store = 0 193 | for i, point in enumerate(points[newVerts[newIndex]+1:newVerts[newIndex+1]]): 194 | if method == 1: 195 | alti = perp_vector_point_line(points[newVerts[newIndex]], points[newVerts[newIndex+1]], point).length 196 | else: 197 | alti = altitude(points[newVerts[newIndex]], points[newVerts[newIndex+1]], point) 198 | 199 | if alti > alti_store: 200 | alti_store = alti 201 | if alti_store >= error: 202 | bigVert = i+1+newVerts[newIndex] 203 | if bigVert: 204 | new.append(bigVert) 205 | if new == []: 206 | return False 207 | return new 208 | 209 | #### get SplineVertIndices to keep 210 | def simplify_RDP(splineVerts, error, method = 1): 211 | ''' 212 | Reduces a curve or polyline based on altitude changes globally and w.r.t. neighbors 213 | args: 214 | splineVerts - list of vectors representing locations along the spline/line path 215 | error - altitude above global/neighbors which allows point to be considered a feature 216 | return: 217 | newVerts - a list of indicies of the simplified representation of the curve (in order, mapping to arg-splineVerts) 218 | ''' 219 | 220 | start = time.time() 221 | 222 | # set first and last vert 223 | newVerts = [0, len(splineVerts)-1] 224 | 225 | # iterate through the points 226 | new = 1 227 | while new != False: 228 | new = iterate(splineVerts, newVerts, error, method = method) 229 | if new: 230 | newVerts += new 231 | newVerts.sort() 232 | 233 | print('finished simplification with method %i in %f seconds' % (method, time.time() - start)) 234 | return newVerts 235 | 236 | def ray_cast_visible(verts, ob, rv3d): 237 | ''' 238 | returns list of Boolean values indicating whether the corresponding vert 239 | is visible (not occluded by object) in region associated with rv3d 240 | ''' 241 | view_dir = rv3d.view_rotation * Vector((0,0,1)) 242 | imx = ob.matrix_world.inverted() 243 | 244 | if rv3d.is_perspective: 245 | eyeloc = Vector(rv3d.view_matrix.inverted().col[3][:3]) #this is brilliant, thanks Gert 246 | eyeloc_local = imx*eyeloc 247 | source = [eyeloc_local for vert in verts] 248 | target = [imx*(vert+ 0.01*view_dir) for vert in verts] 249 | else: 250 | source = [imx*(vert+10000*view_dir) for vert in verts] 251 | target = [imx*(vert+ 0.01*view_dir) for vert in verts] 252 | 253 | return [ob.ray_cast(s,t)[2]==-1 for s,t in zip(source,target)] 254 | 255 | def ray_cast_region2d(region, rv3d, screen_coord, ob, settings): 256 | ''' 257 | performs ray casting on object given region, rv3d, and coords wrt region. 258 | returns tuple of ray vector (from coords of region) and hit info 259 | ''' 260 | mx = ob.matrix_world 261 | imx = mx.inverted() 262 | 263 | if True: 264 | # JD's attempt at correcting bug #48 265 | ray_vector = region_2d_to_vector_3d(region, rv3d, screen_coord).normalized() 266 | ray_origin = region_2d_to_origin_3d(region, rv3d, screen_coord) 267 | if not rv3d.is_perspective: 268 | # need to back up the ray's origin, because ortho projection has front and back 269 | # projection planes at inf 270 | ray_origin = ray_origin + ray_vector * 1000 271 | ray_target = ray_origin - ray_vector * 2000 272 | ray_vector = -ray_vector # why does this need to be negated? 273 | else: 274 | ray_target = ray_origin + ray_vector * 1000 275 | #TODO: make a max ray depth or pull this depth from clip depth 276 | else: 277 | ray_vector = region_2d_to_vector_3d(region, rv3d, screen_coord) 278 | ray_origin = region_2d_to_origin_3d(region, rv3d, screen_coord) 279 | ray_target = ray_origin + (10000 * ray_vector) #TODO: make a max ray depth or pull this depth from clip depth 280 | 281 | ray_start_local = imx * ray_origin 282 | ray_target_local = imx * ray_target 283 | 284 | if settings.debug > 1: 285 | print('ray_persp = ' + str(rv3d.is_perspective)) 286 | print('ray_origin = ' + str(ray_origin)) 287 | print('ray_target = ' + str(ray_target)) 288 | print('ray_vector = ' + str(ray_vector)) 289 | print('ray_diff = ' + str((ray_target - ray_origin).normalized())) 290 | print('start: ' + str(ray_start_local)) 291 | print('target: ' + str(ray_target_local)) 292 | 293 | hit = ob.ray_cast(ray_start_local, ray_target_local) 294 | 295 | return (ray_vector, hit) 296 | 297 | 298 | def relax(verts, factor = .75, in_place = True): 299 | ''' 300 | verts is a list of Vectors 301 | first and last vert will not be changes 302 | 303 | this should modify the list in place 304 | however I have it returning verts? 305 | ''' 306 | 307 | L = len(verts) 308 | if L < 4: 309 | print('not enough verts to relax') 310 | return verts 311 | 312 | 313 | deltas = [Vector((0,0,0))] * L 314 | 315 | for i in range(1,L-1): 316 | 317 | d = .5 * (verts[i-1] + verts[i+1]) - verts[i] 318 | deltas[i] = factor * d 319 | 320 | if in_place: 321 | for i in range(1,L-1): 322 | verts[i] += deltas[i] 323 | 324 | return True 325 | else: 326 | new_verts = verts.copy() 327 | for i in range(1,L-1): 328 | new_verts[i] += deltas[i] 329 | 330 | return new_verts 331 | 332 | def pi_slice(x,y,r1,r2,thta1,thta2,res,t_fan = False): 333 | ''' 334 | args: 335 | x,y - center coordinate 336 | r1, r2 inner and outer radius 337 | thta1: beginning of the slice 0 = to the right 338 | thta2: end of the slice (ccw direction) 339 | ''' 340 | points = [[0,0]]*(2*res + 2) #the two arcs 341 | 342 | for i in range(0,res+1): 343 | diff = math.fmod(thta2-thta1 + 4*math.pi, 2*math.pi) 344 | x1 = math.cos(thta1 + i*diff/res) 345 | y1 = math.sin(thta1 + i*diff/res) 346 | 347 | points[i]=[r1*x1 + x,r1*y1 + y] 348 | points[(2*res) - i+1] =[x1*r2 + x, y1*r2 + y] 349 | 350 | if t_fan: #need to shift order so GL_TRIANGLE_FAN can draw concavity 351 | new_0 = math.floor(1.5*(2*res+2)) 352 | points = list_shift(points, new_0) 353 | 354 | return(points) 355 | 356 | def draw_outline_or_region(mode, points, color): 357 | ''' 358 | arg: 359 | mode - either bgl.GL_POLYGON or bgl.GL_LINE_LOOP 360 | color - will need to be set beforehand using theme colors. eg 361 | bgl.glColor4f(self.ri, self.gi, self.bi, self.ai) 362 | ''' 363 | 364 | bgl.glColor4f(color[0],color[1],color[2],color[3]) 365 | if mode == 'GL_LINE_LOOP': 366 | bgl.glBegin(bgl.GL_LINE_LOOP) 367 | else: 368 | bgl.glBegin(bgl.GL_POLYGON) 369 | 370 | # start with corner right-bottom 371 | for i in range(0,len(points)): 372 | bgl.glVertex2f(points[i][0],points[i][1]) 373 | 374 | bgl.glEnd() 375 | 376 | def arrow_primitive(x,y,ang,tail_l, head_l, head_w, tail_w): 377 | 378 | #primitive 379 | #notice the order so that the arrow can be filled 380 | #in by traingle fan or GL quad arrow[0:4] and arrow [4:] 381 | prim = [Vector((-tail_w,tail_l)), 382 | Vector((-tail_w, 0)), 383 | Vector((tail_w, 0)), 384 | Vector((tail_w, tail_l)), 385 | Vector((head_w,tail_l)), 386 | Vector((0,tail_l + head_l)), 387 | Vector((-head_w,tail_l))] 388 | 389 | #rotation 390 | rmatrix = Matrix.Rotation(ang,2) 391 | 392 | #translation 393 | T = Vector((x,y)) 394 | 395 | arrow = [[None]] * 7 396 | for i, loc in enumerate(prim): 397 | arrow[i] = T + rmatrix * loc 398 | 399 | return arrow 400 | 401 | def arc_arrow(x,y,r1,thta1,thta2,res, arrow_size, arrow_angle, ccw = True): 402 | ''' 403 | args: 404 | x,y - center coordinate of cark 405 | r1 = radius of arc 406 | thta1: beginning of the arc 0 = to the right 407 | thta2: end of the arc (ccw direction) 408 | arrow_size = length of arrow point 409 | 410 | ccw = True draw the arrow 411 | ''' 412 | points = [Vector((0,0))]*(res +1) #The arc + 2 arrow points 413 | 414 | for i in range(0,res+1): 415 | #able to accept negative values? 416 | diff = math.fmod(thta2-thta1 + 2*math.pi, 2*math.pi) 417 | x1 = math.cos(thta1 + i*diff/res) 418 | y1 = math.sin(thta1 + i*diff/res) 419 | 420 | points[i]=Vector((r1*x1 + x,r1*y1 + y)) 421 | 422 | if not ccw: 423 | points.reverse() 424 | 425 | end_tan = points[-2] - points[-1] 426 | end_tan.normalize() 427 | 428 | #perpendicular vector to tangent 429 | arrow_perp_1 = Vector((-end_tan[1],end_tan[0])) 430 | arrow_perp_2 = Vector((end_tan[1],-end_tan[0])) 431 | 432 | op_ov_adj = (math.tan(arrow_angle/2))**2 433 | arrow_side_1 = end_tan + op_ov_adj * arrow_perp_1 434 | arrow_side_2 = end_tan + op_ov_adj * arrow_perp_2 435 | 436 | arrow_side_1.normalize() 437 | arrow_side_2.normalize() 438 | 439 | points.append(points[-1] + arrow_size * arrow_side_1) 440 | points.append(points[-2] + arrow_size * arrow_side_2) 441 | 442 | return(points) 443 | 444 | def simple_circle(x,y,r,res): 445 | ''' 446 | args: 447 | x,y - center coordinate of cark 448 | r1 = radius of arc 449 | ''' 450 | points = [Vector((0,0))]*res #The arc + 2 arrow points 451 | 452 | for i in range(0,res): 453 | theta = i * 2 * math.pi / res 454 | x1 = math.cos(theta) 455 | y1 = math.sin(theta) 456 | 457 | points[i]=Vector((r * x1 + x, r * y1 + y)) 458 | 459 | return(points) 460 | 461 | def draw_3d_points(context, points, color, size): 462 | ''' 463 | draw a bunch of dots 464 | args: 465 | points: a list of tuples representing x,y SCREEN coordinate eg [(10,30),(11,31),...] 466 | color: tuple (r,g,b,a) 467 | size: integer? maybe a float 468 | ''' 469 | points_2d = [location_3d_to_region_2d(context.region, context.space_data.region_3d, loc) for loc in points] 470 | 471 | bgl.glColor4f(*color) 472 | bgl.glPointSize(size) 473 | bgl.glBegin(bgl.GL_POINTS) 474 | for coord in points_2d: 475 | #TODO: Debug this problem....perhaps loc_3d is returning points off of the screen. 476 | if coord: 477 | bgl.glVertex2f(*coord) 478 | else: 479 | print('how the f did nones get in here') 480 | print(coord) 481 | 482 | bgl.glEnd() 483 | return 484 | 485 | def draw_polyline_from_points(context, points, color, thickness, LINE_TYPE): 486 | ''' 487 | a simple way to draw a line 488 | args: 489 | points: a list of tuples representing x,y SCREEN coordinate eg [(10,30),(11,31),...] 490 | color: tuple (r,g,b,a) 491 | thickness: integer? maybe a float 492 | LINE_TYPE: eg...bgl.GL_LINE_STIPPLE or 493 | ''' 494 | 495 | if LINE_TYPE == "GL_LINE_STIPPLE": 496 | bgl.glLineStipple(4, 0x5555) #play with this later 497 | bgl.glEnable(bgl.GL_LINE_STIPPLE) 498 | 499 | 500 | current_width = bgl.GL_LINE_WIDTH 501 | bgl.glColor4f(*color) 502 | bgl.glLineWidth(thickness) 503 | bgl.glBegin(bgl.GL_LINE_STRIP) 504 | 505 | for coord in points: 506 | bgl.glVertex2f(*coord) 507 | 508 | bgl.glEnd() 509 | bgl.glLineWidth(1) 510 | if LINE_TYPE == "GL_LINE_STIPPLE": 511 | bgl.glDisable(bgl.GL_LINE_STIPPLE) 512 | bgl.glEnable(bgl.GL_BLEND) # back to uninterupted lines 513 | 514 | return 515 | 516 | def draw_polyline_from_3dpoints(context, points_3d, color, thickness, LINE_TYPE): 517 | ''' 518 | a simple way to draw a line 519 | slow...becuase it must convert to screen every time 520 | but allows you to pan and zoom around 521 | 522 | args: 523 | points_3d: a list of tuples representing x,y SCREEN coordinate eg [(10,30),(11,31),...] 524 | color: tuple (r,g,b,a) 525 | thickness: integer? maybe a float 526 | LINE_TYPE: eg...bgl.GL_LINE_STIPPLE or 527 | ''' 528 | points = [location_3d_to_region_2d(context.region, context.space_data.region_3d, loc) for loc in points_3d] 529 | if LINE_TYPE == "GL_LINE_STIPPLE": 530 | bgl.glLineStipple(4, 0x5555) #play with this later 531 | bgl.glEnable(bgl.GL_LINE_STIPPLE) 532 | 533 | bgl.glColor4f(*color) 534 | bgl.glLineWidth(thickness) 535 | bgl.glBegin(bgl.GL_LINE_STRIP) 536 | for coord in points: 537 | bgl.glVertex2f(*coord) 538 | 539 | bgl.glEnd() 540 | 541 | if LINE_TYPE == "GL_LINE_STIPPLE": 542 | bgl.glDisable(bgl.GL_LINE_STIPPLE) 543 | bgl.glEnable(bgl.GL_BLEND) # back to uninterupted lines 544 | bgl.glLineWidth(1) 545 | return 546 | 547 | def draw_quads_from_3dpoints(context, points_3d, color): 548 | ''' 549 | a simple way to draw a set of quads 550 | slow...becuase it must convert to screen every time 551 | but allows you to pan and zoom around 552 | 553 | args: 554 | points_3d: a list of tuples as x,y,z 555 | color: tuple (r,g,b,a) 556 | ''' 557 | points = [location_3d_to_region_2d(context.region, context.space_data.region_3d, loc) for loc in points_3d] 558 | bgl.glEnable(bgl.GL_BLEND) 559 | bgl.glColor4f(*color) 560 | bgl.glBegin(bgl.GL_QUADS) 561 | for coord in points: 562 | bgl.glVertex2f(*coord) 563 | bgl.glEnd() 564 | return 565 | 566 | def get_path_length(verts): 567 | ''' 568 | sum up the length of a string of vertices 569 | ''' 570 | l_tot = 0 571 | if len(verts) < 2: 572 | return 0 573 | 574 | for i in range(0,len(verts)-1): 575 | d = verts[i+1] - verts[i] 576 | l_tot += d.length 577 | 578 | return l_tot 579 | 580 | def get_com(verts): 581 | ''' 582 | args: 583 | verts- a list of vectors to be included in the calc 584 | mx- thw world matrix of the object, if empty assumes unity 585 | 586 | ''' 587 | COM = Vector((0,0,0)) 588 | l = len(verts) 589 | for v in verts: 590 | COM += v 591 | COM =(COM/l) 592 | 593 | return COM 594 | 595 | def approx_radius(verts, COM): 596 | ''' 597 | avg distance 598 | ''' 599 | l = len(verts) 600 | app_rad = 0 601 | for v in verts: 602 | R = COM - v 603 | app_rad += R.length 604 | 605 | app_rad = 1/l * app_rad 606 | 607 | return app_rad 608 | 609 | def verts_bbox(verts): 610 | xs = [v[0] for v in verts] 611 | ys = [v[1] for v in verts] 612 | zs = [v[2] for v in verts] 613 | return (min(xs), max(xs), min(ys), max(ys), min(zs), max(zs)) 614 | 615 | def diagonal_verts(verts): 616 | xs = [v[0] for v in verts] 617 | ys = [v[1] for v in verts] 618 | zs = [v[2] for v in verts] 619 | 620 | dx = max(xs) - min(xs) 621 | dy = max(ys) - min(ys) 622 | dz = max(zs) - min(zs) 623 | 624 | diag = math.pow((dx**2 + dy**2 + dz**2),.5) 625 | 626 | return diag 627 | 628 | 629 | def calculate_com_normal(locs): 630 | ''' 631 | computes a center of mass (CoM) and a normal of provided roughly planar locs 632 | notes: 633 | - uses random sampling 634 | - does not assume a particular order of locs 635 | - may compute the negative of "true" normal 636 | ''' 637 | com = sum((loc for loc in locs), Vector((0,0,0))) / len(locs) 638 | # get locations wrt to com 639 | llocs = [loc-com for loc in locs] 640 | ac = Vector((0,0,0)) 641 | first = True 642 | for i in range(len(locs)): 643 | lp0,lp1 = random.sample(llocs,2) 644 | c = lp0.cross(lp1).normalized() 645 | if first: 646 | ac = c 647 | first = False 648 | else: 649 | if ac.dot(c) < 0: 650 | ac -= c 651 | else: 652 | ac += c 653 | return (com, ac.normalized()) 654 | 655 | #TODO: CREDIT 656 | #TODO: LINK 657 | def calculate_best_plane(locs): 658 | 659 | # calculating the center of masss 660 | com = Vector() 661 | for loc in locs: 662 | com += loc 663 | com /= len(locs) 664 | x, y, z = com 665 | 666 | 667 | # creating the covariance matrix 668 | mat = Matrix(((0.0, 0.0, 0.0), 669 | (0.0, 0.0, 0.0), 670 | (0.0, 0.0, 0.0), 671 | )) 672 | 673 | for loc in locs: 674 | mat[0][0] += (loc[0]-x)**2 675 | mat[1][0] += (loc[0]-x)*(loc[1]-y) 676 | mat[2][0] += (loc[0]-x)*(loc[2]-z) 677 | mat[0][1] += (loc[1]-y)*(loc[0]-x) 678 | mat[1][1] += (loc[1]-y)**2 679 | mat[2][1] += (loc[1]-y)*(loc[2]-z) 680 | mat[0][2] += (loc[2]-z)*(loc[0]-x) 681 | mat[1][2] += (loc[2]-z)*(loc[1]-y) 682 | mat[2][2] += (loc[2]-z)**2 683 | 684 | # calculating the normal to the plane 685 | normal = False 686 | try: 687 | mat.invert() 688 | except: 689 | if sum(mat[0]) == 0.0: 690 | normal = Vector((1.0, 0.0, 0.0)) 691 | elif sum(mat[1]) == 0.0: 692 | normal = Vector((0.0, 1.0, 0.0)) 693 | elif sum(mat[2]) == 0.0: 694 | normal = Vector((0.0, 0.0, 1.0)) 695 | if not normal: 696 | # warning! this is different from .normalize() 697 | itermax = 500 698 | iter = 0 699 | vec = Vector((1.0, 1.0, 1.0)) 700 | vec2 = (mat * vec)/(mat * vec).length 701 | while vec != vec2 and iter= 0 and check[1] <= 1: 775 | 776 | 777 | 778 | #the vert coord index = the face indices it came from 779 | edge_mapping[len(verts)] = [f.index for f in ed.link_faces] 780 | verts.append(v) 781 | 782 | if debug: 783 | n = len(times) 784 | times.append(time.time()) 785 | print('calced intersections %f sec' % (times[n]-times[n-1])) 786 | 787 | #iterate through smartly to create edge keys 788 | for i in range(0,len(verts)): 789 | a_faces = set(edge_mapping[i]) 790 | for m in range(i,len(verts)): 791 | if m != i: 792 | b_faces = set(edge_mapping[m]) 793 | if a_faces & b_faces: 794 | eds.append((i,m)) 795 | 796 | if debug: 797 | n = len(times) 798 | times.append(time.time()) 799 | #print('calced connectivity %f sec' % (times[n]-times[n-1])) 800 | 801 | if len(verts): 802 | #new_me = bpy.data.meshes.new('Cross Section') 803 | #new_me.from_pydata(verts,eds,[]) 804 | 805 | 806 | #if debug: 807 | #n = len(times) 808 | #times.append(time.time()) 809 | #print('Total Time: %f sec' % (times[-1]-times[0])) 810 | 811 | return (verts, eds) 812 | else: 813 | return None 814 | 815 | def cross_edge(A,B,pt,no): 816 | ''' 817 | wrapper of intersect_line_plane that limits intersection 818 | to within the line segment. 819 | 820 | args: 821 | A - Vector endpoint of line segment 822 | B - Vector enpoint of line segment 823 | pt - pt on plane to intersect 824 | no - normal of plane to intersect 825 | 826 | return: 827 | list [Intersection Type, Intersection Point, Intersection Point2] 828 | eg... ['CROSS',Vector((0,1,0)), None] 829 | eg... ['POINT',Vector((0,1,0)), None] 830 | eg....['COPLANAR', Vector((0,1,0)),Vector((0,2,0))] 831 | eg....[None,None,None] 832 | ''' 833 | 834 | ret_val = [None]*3 #list [intersect type, pt 1, pt 2] 835 | V = B - A #vect representation of the edge 836 | proj = V.project(no).length 837 | 838 | #perp to normal = parallel to plane 839 | #worst case is a coplanar issue where the whole face is coplanar..we will get there 840 | if proj == 0: 841 | 842 | #test coplanar 843 | #don't test both points. We have already tested once for paralellism 844 | #simply proving one out of two points is/isn't in the plane will 845 | #prove/disprove coplanar 846 | p_to_A = A - pt 847 | #truly, we could precalc all these projections to save time but use mem. 848 | #because in the multiple edges coplanar case, we wil be testing 849 | #their verts over and over again that share edges. So for a mesh with 850 | #a lot of n poles, precalcing the vert projections may save time! 851 | #Hint to future self, look at Nfaces vs Nedges vs Nverts 852 | #may prove to be a good predictor of which method to use. 853 | a_proj = p_to_A.project(no).length 854 | 855 | if a_proj == 0: 856 | print('special case co planar edge') 857 | ret_val = ['COPLANAR',A,B] 858 | 859 | else: 860 | 861 | #this handles the one point on plane case 862 | v = intersect_line_plane(A,B,pt,no) 863 | 864 | if v: 865 | check = intersect_point_line(v,A,B) 866 | if check[1] > 0 and check[1] < 1: #this is the purest cross...no co-points 867 | #the vert coord index = the face indices it came from 868 | ret_val = ['CROSS',v,None] 869 | 870 | elif check[1] == 0 or check[1] == 1: 871 | print('special case coplanar point') 872 | #now add all edges that have that point into the already checked list 873 | #this takes care of poles 874 | ret_val = ['POINT',v,None] 875 | 876 | return ret_val 877 | 878 | def outside_loop_2d(loop): 879 | ''' 880 | args: 881 | loop: list of 882 | type-Vector or type-tuple 883 | returns: 884 | outside = a location outside bound of loop 885 | type-tuple 886 | ''' 887 | 888 | xs = [v[0] for v in loop] 889 | ys = [v[1] for v in loop] 890 | 891 | maxx = max(xs) 892 | maxy = max(ys) 893 | bound = (1.1*maxx, 1.1*maxy) 894 | return bound 895 | 896 | def bound_box(verts): 897 | ''' 898 | takes a list of vectors of any dimension 899 | returns a list of (min,max) pairs 900 | ''' 901 | if len(verts) < 4: 902 | return verts 903 | 904 | dim = len(verts[0]) 905 | 906 | bounds = [] 907 | for i in range(0,dim): 908 | components = [v[i] for v in verts] 909 | low = min(components) 910 | high = max(components) 911 | 912 | bounds.append((low,high)) 913 | 914 | return bounds 915 | 916 | def diagonal(bounds): 917 | ''' 918 | returns the diagonal dimension of min/max 919 | pairs of bounds. Will generalize to N dimensions 920 | however only really meaningful for 2 or 3 dim vectors 921 | ''' 922 | diag = 0 923 | for min_max in bounds: 924 | l = min_max[1] - min_max[0] 925 | diag += l * l 926 | 927 | diag = diag ** .5 928 | 929 | return diag 930 | 931 | #adapted from opendentalcad then to pie menus now here 932 | 933 | def point_inside_loop2d(loop, point): 934 | ''' 935 | args: 936 | loop: list of vertices representing loop 937 | type-tuple or type-Vector 938 | point: location of point to be tested 939 | type-tuple or type-Vector 940 | 941 | return: 942 | True if point is inside loop 943 | ''' 944 | #test arguments type 945 | ptype = str(type(point)) 946 | ltype = str(type(loop[0])) 947 | nverts = len(loop) 948 | 949 | if 'Vector' not in ptype: 950 | point = Vector(point) 951 | 952 | if 'Vector' not in ltype: 953 | for i in range(0,nverts): 954 | loop[i] = Vector(loop[i]) 955 | 956 | #find a point outside the loop and count intersections 957 | out = Vector(outside_loop_2d(loop)) 958 | intersections = 0 959 | for i in range(0,nverts): 960 | a = Vector(loop[i-1]) 961 | b = Vector(loop[i]) 962 | if intersect_line_line_2d(point,out,a,b): 963 | intersections += 1 964 | 965 | inside = False 966 | if math.fmod(intersections,2): 967 | inside = True 968 | 969 | return inside 970 | 971 | def generic_axes_from_plane_normal(p_pt, no): 972 | ''' 973 | will take a point on a plane and normal vector 974 | and return two orthogonal vectors which create 975 | a right handed coordinate system with z axist aligned 976 | to plane normal 977 | ''' 978 | 979 | #get the equation of a plane ax + by + cz = D 980 | #Given point P, normal N ...any point R in plane satisfies 981 | # Nx * (Rx - Px) + Ny * (Ry - Py) + Nz * (Rz - Pz) = 0 982 | #now pick any xy, yz or xz and solve for the other point 983 | 984 | a = no[0] 985 | b = no[1] 986 | c = no[2] 987 | 988 | Px = p_pt[0] 989 | Py = p_pt[1] 990 | Pz = p_pt[2] 991 | 992 | D = a * Px + b * Py + c * Pz 993 | 994 | #generate a randomply perturbed R from the known p_pt 995 | R = p_pt + Vector((random.random(), random.random(), random.random())) 996 | 997 | #z = D/c - a/c * x - b/c * y 998 | if c != 0: 999 | Rz = D/c - a/c * R[0] - b/c * R[1] 1000 | R[2] = Rz 1001 | 1002 | #y = D/b - a/b * x - c/b * z 1003 | elif b!= 0: 1004 | Ry = D/b - a/b * R[0] - c/b * R[2] 1005 | R[1] = Ry 1006 | #x = D/a - b/a * y - c/a * z 1007 | elif a != 0: 1008 | Rx = D/a - b/a * R[1] - c/a * R[2] 1009 | R[0] = Rz 1010 | else: 1011 | print('undefined plane you wanker!') 1012 | return(False) 1013 | 1014 | #now R represents some other point in the plane 1015 | #we will use this to define an arbitrary local 1016 | #x' y' and z' 1017 | X_prime = R - p_pt 1018 | X_prime.normalize() 1019 | 1020 | Y_prime = no.cross(X_prime) 1021 | Y_prime.normalize() 1022 | 1023 | return (X_prime, Y_prime) 1024 | 1025 | def point_inside_loop_almost3D(pt, verts, no, p_pt = None, threshold = .01, debug = False, bbox = False): 1026 | ''' 1027 | http://blenderartists.org/forum/showthread.php?259085-Brainstorming-for-Virtual-Buttons&highlight=point+inside+loop 1028 | args: 1029 | pt - 3d point to test of type Mathutils.Vector 1030 | verts - 3d points representing the loop 1031 | TODO: verts[0] == verts[-1] or implied? 1032 | list with elements of type Mathutils.Vector 1033 | no - plane normal 1034 | plane_pt - a point on the plane. 1035 | if None, COM of verts will be used 1036 | threshold - maximum distance to consider pt "coplanar" 1037 | default = .01 1038 | 1039 | debug - Bool, default False. Will print performance if True 1040 | 1041 | return: Bool True if point is inside the loop 1042 | ''' 1043 | if debug: 1044 | start = time.time() 1045 | #sanity checks 1046 | if len(verts) < 3: 1047 | print('loop must have 3 verts to be a loop and even then its sketchy') 1048 | return False 1049 | 1050 | if no.length == 0: 1051 | print('normal vector must be non zero') 1052 | return False 1053 | 1054 | if not p_pt: 1055 | p_pt = get_com(verts) 1056 | 1057 | if distance_point_to_plane(pt, p_pt, no) > threshold: 1058 | return False 1059 | 1060 | (X_prime, Y_prime) = generic_axes_from_plane_normal(p_pt, no) 1061 | 1062 | verts_prime = [] 1063 | 1064 | for v in verts: 1065 | v_trans = v - p_pt 1066 | vx = v_trans.dot(X_prime) 1067 | vy = v_trans.dot(Y_prime) 1068 | verts_prime.append(Vector((vx, vy))) 1069 | 1070 | bounds = bound_box(verts_prime) 1071 | 1072 | bound_loop = [Vector((bounds[0][0],bounds[1][0])), 1073 | Vector((bounds[0][1],bounds[1][0])), 1074 | Vector((bounds[0][1],bounds[1][1])), 1075 | Vector((bounds[0][0],bounds[1][1]))] 1076 | #transform the test point into the new plane x,y space 1077 | pt_trans = pt - p_pt 1078 | pt_prime = Vector((pt_trans.dot(X_prime), pt_trans.dot(Y_prime))) 1079 | 1080 | if bbox: 1081 | print('intersected the bbox') 1082 | pt_in_loop = point_inside_loop2d(bound_loop, pt_prime) 1083 | else: 1084 | pt_in_loop = point_inside_loop2d(verts_prime, pt_prime) 1085 | 1086 | return pt_in_loop 1087 | 1088 | def face_cycle(face, pt, no, prev_eds, verts):#, connection): 1089 | ''' 1090 | args: 1091 | face - Blender BMFace 1092 | pt - Vector, point on plane 1093 | no - Vector, normal of plane 1094 | 1095 | 1096 | These arguments will be modified 1097 | prev_eds - MUTABLE list of previous edges already tested in the bmesh 1098 | verts - MUTABLE list of Vectors representing vertex coords 1099 | connection - MUTABLE dictionary of vert indices and face connections 1100 | 1101 | return: 1102 | element - either a BMVert or a BMFace depending on what it finds. 1103 | ''' 1104 | if len(face.edges) > 4: 1105 | ngon = True 1106 | print('oh sh** an ngon') 1107 | else: 1108 | ngon = False 1109 | 1110 | for ed in face.edges: 1111 | if ed.index not in prev_eds: 1112 | prev_eds.append(ed.index) 1113 | A = ed.verts[0].co 1114 | B = ed.verts[1].co 1115 | result = cross_edge(A, B, pt, no) 1116 | 1117 | if result[0] == 'CROSS': 1118 | 1119 | #connection[len(verts)] = [f.index for f in ed.link_faces] 1120 | verts.append(result[1]) 1121 | next_faces = [newf for newf in ed.link_faces if newf.index != face.index] 1122 | if len(next_faces): 1123 | return next_faces[0] 1124 | else: 1125 | #guess we got to a non manifold edge 1126 | print('found end of mesh!') 1127 | return None 1128 | 1129 | elif result[0] == 'POINT': 1130 | if result[1] == A: 1131 | co_point = ed.verts[0] 1132 | else: 1133 | co_point = ed.verts[1] 1134 | 1135 | #connection[len(verts)] = [f.index for f in co_point.link_faces] #notice we take the face loop around the point! 1136 | verts.append(result[1]) #store the "intersection" 1137 | 1138 | return co_point 1139 | 1140 | def vert_cycle(vert, pt, no, prev_eds, verts):#, connection): 1141 | ''' 1142 | args: 1143 | vert - Blender BMVert 1144 | pt - Vector, point on plane 1145 | no - Vector, normal of plane 1146 | 1147 | 1148 | These arguments will be modified 1149 | prev_eds - MUTABLE list of previous edges already tested in the bmesh 1150 | verts - MUTABLE list of Vectors representing vertex coords 1151 | connection - MUTABLE dictionary of vert indices and face connections 1152 | 1153 | return: 1154 | element - either a BMVert or a BMFace depending on what it finds. 1155 | ''' 1156 | 1157 | for f in vert.link_faces: 1158 | for ed in f.edges: 1159 | if ed.index not in prev_eds: 1160 | prev_eds.append(ed.index) 1161 | A = ed.verts[0].co 1162 | B = ed.verts[1].co 1163 | result = cross_edge(A, B, pt, no) 1164 | 1165 | if result[0] == 'CROSS': 1166 | #connection[len(verts)] = [f.index for f in ed.link_faces] 1167 | verts.append(result[1]) 1168 | next_faces = [newf for newf in ed.link_faces if newf.index != f.index] 1169 | if len(next_faces): 1170 | #return face to try face cycle 1171 | return next_faces[0] 1172 | else: 1173 | #guess we got to a non manifold edge 1174 | print('found end of mesh!') 1175 | return None 1176 | 1177 | elif result[0] == 'COPLANAR': 1178 | cop_face = 0 1179 | for face in ed.link_faces: 1180 | if face.normal.cross(no) == 0: 1181 | cop_face += 1 1182 | print('found a coplanar face') 1183 | 1184 | if cop_face == 2: 1185 | #we have two coplanar faces with a coplanar edge 1186 | #this makes our cross section fail from a loop perspective 1187 | print("double coplanar face error, stopping here") 1188 | return None 1189 | 1190 | else: 1191 | #jump down line to the next vert 1192 | if ed.verts[0].index == vert.index: 1193 | element = ed.verts[1] 1194 | 1195 | else: 1196 | element = ed.verts[0] 1197 | 1198 | #add the new vert coord into the mix 1199 | #connection[len(verts)] = [f.index for f in element.link_faces] 1200 | verts.append(element.co) 1201 | 1202 | #return the vert to repeat the vert cycle 1203 | return element 1204 | 1205 | def space_evenly_on_path(verts, edges, segments, shift = 0, debug = False): #prev deved for Open Dental CAD 1206 | ''' 1207 | Gives evenly spaced location along a string of verts 1208 | Assumes that nverts > nsegments 1209 | Assumes verts are ORDERED along path 1210 | Assumes edges are ordered coherently 1211 | Yes these are lazy assumptions, but the way I build my data 1212 | guarantees these assumptions so deal with it. 1213 | 1214 | args: 1215 | verts - list of vert locations type Mathutils.Vector 1216 | eds - list of index pairs type tuple(integer) eg (3,5). 1217 | should look like this though [(0,1),(1,2),(2,3),(3,4),(4,0)] 1218 | segments - number of segments to divide path into 1219 | shift - for cyclic verts chains, shifting the verts along 1220 | the loop can provide better alignment with previous 1221 | loops. This should be -1 to 1 representing a percentage of segment length. 1222 | Eg, a shift of .5 with 8 segments will shift the verts 1/16th of the loop length 1223 | 1224 | return 1225 | new_verts - list of new Vert Locations type list[Mathutils.Vector] 1226 | ''' 1227 | 1228 | if len(verts) < 2: 1229 | print('this is crazy, there are not enough verts to do anything!') 1230 | return verts 1231 | 1232 | if segments >= len(verts): 1233 | print('more segments requested than original verts') 1234 | 1235 | 1236 | #determine if cyclic or not, first vert same as last vert 1237 | if 0 in edges[-1]: 1238 | cyclic = True 1239 | 1240 | else: 1241 | cyclic = False 1242 | #zero out the shift in case the vert chain insn't cyclic 1243 | if shift != 0: #not PEP but it shows that we want shift = 0 1244 | print('not shifting because this is not a cyclic vert chain') 1245 | shift = 0 1246 | 1247 | #calc_length 1248 | arch_len = 0 1249 | cumulative_lengths = [0]#TODO, make this the right size and dont append 1250 | for i in range(0,len(verts)-1): 1251 | v0 = verts[i] 1252 | v1 = verts[i+1] 1253 | V = v1-v0 1254 | arch_len += V.length 1255 | cumulative_lengths.append(arch_len) 1256 | 1257 | if cyclic: 1258 | v0 = verts[-1] 1259 | v1 = verts[0] 1260 | V = v1-v0 1261 | arch_len += V.length 1262 | cumulative_lengths.append(arch_len) 1263 | #print(cumulative_lengths) 1264 | 1265 | #identify vert indicies of import 1266 | #this will be the largest vert which lies at 1267 | #no further than the desired fraction of the curve 1268 | 1269 | #initialze new vert array and seal the end points 1270 | if cyclic: 1271 | new_verts = [[None]]*(segments) 1272 | #new_verts[0] = verts[0] 1273 | 1274 | else: 1275 | new_verts = [[None]]*(segments + 1) 1276 | new_verts[0] = verts[0] 1277 | new_verts[-1] = verts[-1] 1278 | 1279 | 1280 | n = 0 #index to save some looping through the cumulative lengths list 1281 | #now we are leaving it 0 becase we may end up needing the beginning of the loop last 1282 | #and if we are subdividing, we may hit the same cumulative lenght several times. 1283 | #for now, use the slow and generic way, later developsomething smarter. 1284 | for i in range(0,segments- 1 + cyclic * 1): 1285 | desired_length_raw = (i + 1 + cyclic * -1)/segments * arch_len + shift * arch_len / segments 1286 | #print('the length we desire for the %i segment is %f compared to the total length which is %f' % (i, desired_length_raw, arch_len)) 1287 | #like a mod function, but for non integers? 1288 | if desired_length_raw > arch_len: 1289 | desired_length = desired_length_raw - arch_len 1290 | elif desired_length_raw < 0: 1291 | desired_length = arch_len + desired_length_raw #this is the end, + a negative number 1292 | else: 1293 | desired_length = desired_length_raw 1294 | 1295 | #find the original vert with the largets legnth 1296 | #not greater than the desired length 1297 | #I used to set n = J after each iteration 1298 | for j in range(n, len(verts)+1): 1299 | 1300 | if cumulative_lengths[j] > desired_length: 1301 | #print('found a greater length at vert %i' % j) 1302 | #this was supposed to save us some iterations so that 1303 | #we don't have to start at the beginning each time.... 1304 | #if j >= 1: 1305 | #n = j - 1 #going one back allows us to space multiple verts on one edge 1306 | #else: 1307 | #n = 0 1308 | break 1309 | 1310 | extra = desired_length - cumulative_lengths[j-1] 1311 | if j == len(verts): 1312 | new_verts[i + 1 + cyclic * -1] = verts[j-1] + extra * (verts[0]-verts[j-1]).normalized() 1313 | else: 1314 | new_verts[i + 1 + cyclic * -1] = verts[j-1] + extra * (verts[j]-verts[j-1]).normalized() 1315 | 1316 | eds = [] 1317 | 1318 | for i in range(0,len(new_verts)-1): 1319 | eds.append((i,i+1)) 1320 | if cyclic: 1321 | #close the loop 1322 | eds.append((i+1,0)) 1323 | if debug: 1324 | print(cumulative_lengths) 1325 | print(arch_len) 1326 | print(eds) 1327 | 1328 | return new_verts, eds 1329 | 1330 | def list_shift(seq, n): 1331 | n = n % len(seq) 1332 | return seq[n:] + seq[:n] 1333 | 1334 | def concatenate(*lists): 1335 | lengths = map(len,lists) 1336 | newlen = sum(lengths) 1337 | newlist = [None]*newlen 1338 | start = 0 1339 | end = 0 1340 | for l,n in zip(lists,lengths): 1341 | end+=n 1342 | newlist[start:end] = l 1343 | start+=n 1344 | return newlist 1345 | 1346 | def find_doubles(seq): 1347 | seen = set() 1348 | seen_add = seen.add 1349 | # adds all elements it doesn't know yet to seen and all other to seen_twice 1350 | seen_twice = set(x for x in seq if x in seen or seen_add(x)) 1351 | # turn the set into a list (as requested) 1352 | return list(seen_twice) 1353 | 1354 | def alignment_quality_perpendicular(verts_1, verts_2, eds_1, eds_2): 1355 | ''' 1356 | Calculates a quality measure of the alignment of edge loops. 1357 | Ideally we want any connectors between loops to be as perpendicular 1358 | to the loop as possible. Assume the loops are aligned properly in 1359 | direction around the loop. 1360 | 1361 | args: 1362 | verts_1: list of Vectors 1363 | verts_2: list of Vectors 1364 | 1365 | eds_1: connectivity of the first loop, really just to test loop or line 1366 | eds_2: connectivity of 2nd loops, really just to test for loop or line 1367 | 1368 | ''' 1369 | 1370 | if 0 in eds_1[-1]: 1371 | cyclic = True 1372 | print('cyclic vert chain') 1373 | else: 1374 | cyclic = False 1375 | 1376 | if len(verts_1) != len(verts_2): 1377 | print(len(verts_1)) 1378 | print(len(verts_2)) 1379 | print('non uniform loops, stopping until your developer gets smarter') 1380 | return 1381 | 1382 | 1383 | #since the loops in our case are guaranteed planar 1384 | #because they come from cross sections, we can find 1385 | #the plane normal very easily 1386 | V1_0 = verts_1[1] - verts_1[0] 1387 | V1_1 = verts_1[2] - verts_1[1] 1388 | 1389 | V2_0 = verts_2[1] - verts_2[0] 1390 | V2_1 = verts_2[2] - verts_2[1] 1391 | 1392 | no_1 = V1_0.cross(V1_1) 1393 | no_1.normalize() 1394 | no_2 = V2_0.cross(V2_1) 1395 | no_2.normalize() 1396 | 1397 | if no_1.dot(no_2) < 0: 1398 | no_2 = -1 * no_2 1399 | 1400 | #average the two directions 1401 | ideal_direction = no_1.lerp(no_1,.5) 1402 | 1403 | def point_in_tri(P, A, B, C): 1404 | ''' 1405 | 1406 | ''' 1407 | #straight from http://www.blackpawn.com/texts/pointinpoly/ 1408 | # Compute vectors 1409 | v0 = C - A 1410 | v1 = B - A 1411 | v2 = P - A 1412 | 1413 | #Compute dot products 1414 | dot00 = v0.dot(v0) 1415 | dot01 = v0.dot(v1) 1416 | dot02 = v0.dot(v2) 1417 | dot11 = v1.dot(v1) 1418 | dot12 = v1.dot(v2) 1419 | 1420 | #Compute barycentric coordinates 1421 | invDenom = 1 / (dot00 * dot11 - dot01 * dot01) 1422 | u = (dot11 * dot02 - dot01 * dot12) * invDenom 1423 | v = (dot00 * dot12 - dot01 * dot02) * invDenom 1424 | 1425 | #Check if point is in triangle 1426 | return (u >= 0) & (v >= 0) & (u + v < 1) 1427 | 1428 | def com_mid_ray_test(new_cut, established_cut, obj, search_factor = .5): 1429 | ''' 1430 | function used to test intial validity of a connection 1431 | between two cuts. 1432 | 1433 | args: 1434 | new_cut: a ContourCutLine 1435 | existing_cut: ContourCutLine 1436 | obj: The retopo object 1437 | search_factor: percentage of object bbox diagonal to search 1438 | aim: False or angle that new cut COM must fall within compared 1439 | to existing plane normal. Eg...pi/4 would be a 45 degree 1440 | aiming cone 1441 | 1442 | 1443 | returns: Bool 1444 | ''' 1445 | 1446 | 1447 | A = established_cut.plane_com #the COM of the cut loop 1448 | B = new_cut.plane_com #the COM of the other cut loop 1449 | C = .5 * (A + B) #the midpoint of the line between them 1450 | 1451 | 1452 | #pick a vert roughly in the middle 1453 | n = math.floor(len(established_cut.verts_simple)/2) 1454 | 1455 | 1456 | ray = A - established_cut.verts_simple[n] 1457 | 1458 | #just in case the vert IS the center of mass :-( 1459 | if ray.length < .0001 and n != 0: 1460 | ray = A - established_cut.verts_simple[n-1] 1461 | 1462 | ray.normalize() 1463 | 1464 | 1465 | #limit serach to some fraction of the object bbox diagonal 1466 | #search_radius = 1/2 * search_factor * obj.dimensions.length 1467 | search_radius = 100 1468 | imx = obj.matrix_world.inverted() 1469 | 1470 | hit = obj.ray_cast(imx * (C + search_radius * ray), imx * (C - search_radius * ray)) 1471 | 1472 | if hit[2] != -1: 1473 | return True 1474 | else: 1475 | return False 1476 | 1477 | def com_line_cross_test(com1, com2, pt, no, factor = 2): 1478 | ''' 1479 | test used to make sure a cut is reasoably between 1480 | 2 other cuts 1481 | 1482 | higher factor requires better aligned cuts 1483 | ''' 1484 | 1485 | v = intersect_line_plane(com1,com2,pt,no) 1486 | if v: 1487 | #if the distance between the intersection is less than 1488 | #than 1/factor the distance between the current pair 1489 | #than this pair is invalide because there is a loop 1490 | #in between 1491 | check = intersect_point_line(v,com1,com2) 1492 | invalid_length = (com2 - com1).length/factor #length beyond which an intersection is invalid 1493 | test_length = (v - pt).length 1494 | 1495 | #this makes sure the plane is between A and B 1496 | #meaning the test plane is between the two COM's 1497 | in_between = check[1] >= 0 and check[1] <= 1 1498 | 1499 | if in_between and test_length < invalid_length: 1500 | return True 1501 | 1502 | def discrete_curl(verts, z): #Adapted from Open Dental CAD by Patrick Moore 1503 | ''' 1504 | calculates the curl relative to the direction given. 1505 | It should be ~ +2pi or -2pi depending on whether the loop 1506 | progresses clockwise or anticlockwise when viewed in the 1507 | z direction. If the loop goes around twice it could be 4pi 6pi etc 1508 | This is useful for making sure loops are indexed in the same direction. 1509 | 1510 | args: 1511 | verts: a list of Vectors representing locations 1512 | z: a vector representing the direction to compare the curl to 1513 | 1514 | ''' 1515 | if len(verts) < 3: 1516 | print('not possible for this to be a loop!') 1517 | return None 1518 | 1519 | curl = 0 1520 | 1521 | #just in case the vert chain has the last vert 1522 | #duplicated. We will need to not double the 1523 | #last one 1524 | closed = False 1525 | if verts[-1] == verts[0]: 1526 | closed = True 1527 | 1528 | for n in range(0,len(verts) - 1*closed): 1529 | 1530 | a = int(math.fmod(n - 1, len(verts))) 1531 | b = n 1532 | c = int(math.fmod(n + 1, len(verts))) 1533 | #Vec representation of the two edges 1534 | V0 = (verts[b] - verts[a]) 1535 | V1 = (verts[c] - verts[b]) 1536 | 1537 | #projection into the plane perpendicular to z 1538 | #eg, the XY plane 1539 | T0 = V0 - V0.project(z) 1540 | T1 = V1 - V1.project(z) 1541 | 1542 | #cross product 1543 | cross = T0.cross(T1) 1544 | sign = 1 1545 | if cross.dot(z) < 0: 1546 | sign = -1 1547 | 1548 | rot = T0.rotation_difference(T1) 1549 | ang = rot.angle 1550 | curl = curl + ang*sign 1551 | 1552 | return curl 1553 | 1554 | def rot_between_vecs(v1,v2, factor = 1): 1555 | ''' 1556 | args: 1557 | v1 - Vector Init 1558 | v2 - Vector Final 1559 | 1560 | factor - will interpolate between them. [0,1] 1561 | 1562 | returns the quaternion representing rotation between v1 to v2 1563 | 1564 | v2 = quat * v1 1565 | 1566 | notes: doesn't test for parallel vecs 1567 | ''' 1568 | v1.normalize() 1569 | v2.normalize() 1570 | angle = factor * v1.angle(v2) 1571 | axis = v1.cross(v2) 1572 | axis.normalize() 1573 | sin = math.sin(angle/2) 1574 | cos = math.cos(angle/2) 1575 | 1576 | quat = Quaternion((cos, sin*axis[0], sin*axis[1], sin*axis[2])) 1577 | 1578 | return quat 1579 | 1580 | def circ(point1, point2, point3): 1581 | '''find the x,y and radius for the circle through the 3 points''' 1582 | ax = point1[0] 1583 | ay = point1[1] 1584 | ax = point1[0] 1585 | ay = point1[1] 1586 | bx = point2[0] 1587 | by = point2[1] 1588 | cx = point3[0] 1589 | cy = point3[1] 1590 | 1591 | if (ax*by-ax*cy-cx*by+cy*bx-bx*ay+cx*ay) != 0: 1592 | x=.5*(-pow(ay, 2)*cy+pow(ay, 2)*by-ay*pow(bx, 2)\ 1593 | -ay*pow(by, 2)+ay*pow(cy, 2)+ay*pow(cx, 2)-\ 1594 | pow(cx, 2)*by+pow(ax, 2)*by+pow(bx, 2)*\ 1595 | cy-pow(ax, 2)*cy-pow(cy, 2)*by+cy*pow(by, 2))\ 1596 | /(ax*by-ax*cy-cx*by+cy*bx-bx*ay+cx*ay) 1597 | y=-.5*(-pow(ax, 2)*cx+pow(ax, 2)*bx-ax*pow(by, 2)\ 1598 | -ax*pow(bx, 2)+ax*pow(cx, 2)+ax*pow(cy, 2)-\ 1599 | pow(cy, 2)*bx+pow(ay, 2)*bx+pow(by, 2)*cx\ 1600 | -pow(ay, 2)*cx-pow(cx, 2)*bx+cx*pow(bx, 2))\ 1601 | /(ax*by-ax*cy-cx*by+cy*bx-bx*ay+cx*ay) 1602 | else: 1603 | return False 1604 | 1605 | r=pow(pow(x-ax, 2)+pow(y-ay, 2), .5) 1606 | 1607 | return x, y, r 1608 | 1609 | def findpoint(eq1, eq2, point1, point2): 1610 | '''find the centroid of the overlapping part of two circles 1611 | from their equations''' 1612 | thetabeg = math.acos((point1[0]-eq1[0])/eq1[2]) 1613 | thetaend = math.acos((point2[0]-eq1[0])/eq1[2]) 1614 | mid1x = eq1[2]*math.cos((thetabeg+thetaend)/2)+eq1[0] 1615 | thetaybeg = math.asin((point1[1]-eq1[1])/eq1[2]) 1616 | thetayend = math.asin((point2[1]-eq1[1])/eq1[2]) 1617 | mid1y = eq1[2]*math.sin((thetaybeg+thetayend)/2)+eq1[1] 1618 | 1619 | thetabeg2 = math.acos((point1[0]-eq2[0])/eq2[2]) 1620 | thetaend2 = math.acos((point2[0]-eq2[0])/eq2[2]) 1621 | mid2x = eq2[2]*math.cos((thetabeg2+thetaend2)/2)+eq2[0] 1622 | thetaybeg2 = math.asin((point1[1]-eq2[1])/eq2[2]) 1623 | thetayend2 = math.asin((point2[1]-eq2[1])/eq2[2]) 1624 | mid2y = eq2[2]*math.sin((thetaybeg2+thetayend2)/2)+eq2[1] 1625 | return [(mid2x+mid1x)/2, (mid2y+mid1y)/2] 1626 | 1627 | def interp_curve(curve, iterations): 1628 | ''' Ordered list of points, the first and last affect the shape 1629 | of the curve but are not connected though drawn''' 1630 | 1631 | new_curve = curve.copy() 1632 | 1633 | for j in range(0, iterations): 1634 | newpoints = [] 1635 | for i in range(0, len(new_curve)-3): 1636 | eq = circ(new_curve[i], new_curve[i+1], new_curve[i+2]) 1637 | eq2 = circ(new_curve[i+1], new_curve[i+2], new_curve[i+3]) 1638 | if eq == False or eq2 == False: 1639 | newpoints.append([(new_curve[i+1][0]+new_curve[i+2][0])/2, (new_curve[i+1][1]+new_curve[i+2][1])/2]) 1640 | else: 1641 | newpoints.append(findpoint(eq, eq2, new_curve[i+1], new_curve[i+2])) 1642 | for point in newpoints: 1643 | point[0] = int(round(point[0])) 1644 | point[1] = int(round(point[1])) 1645 | for m in range(0, len(newpoints)): 1646 | new_curve.insert(2*m+2, newpoints[m]) 1647 | 1648 | def nearest_point(test_vert, vert_list): 1649 | ''' 1650 | find the closest point to a test vert from a 1651 | list of vertices 1652 | 1653 | Brute force 1654 | Not fast 1655 | not smart 1656 | 1657 | return index in list 1658 | ''' 1659 | 1660 | lens = [None]*len(vert_list) 1661 | 1662 | for i,v in enumerate(vert_list): 1663 | R = test_vert - v 1664 | lens[i] = R.length 1665 | 1666 | smallest = min(lens) 1667 | n = lens.index(smallest) 1668 | 1669 | return n 1670 | 1671 | def intersect_paths(path1, path2, cyclic1 = False, cyclic2 = False, threshold = .00001): 1672 | ''' 1673 | intersects vert paths 1674 | 1675 | returns a list of intersections (verts) 1676 | returns a list of vert index pairs that corresponds to the 1677 | first vert of the edge in path1 and path 2 where the intersection 1678 | occurs 1679 | 1680 | eg...if the 10th of path 1 intersectts with the 5th edge of path 2 1681 | 1682 | return [[intersection verst],[inds],[inds]] 1683 | 1684 | Special cases are not handled well. Eg..dont instersect two 1685 | clover leaf paths! 1686 | 1687 | ''' 1688 | 1689 | intersections = [] 1690 | inds_1 = [] 1691 | inds_2 = [] 1692 | 1693 | for i in range(0,len(path1) + 1*cyclic1 - 1): 1694 | 1695 | n = int(math.fmod(i+1, len(path1))) 1696 | v1 = path1[n] 1697 | v2 = path1[i] 1698 | for j in range(0,len(path2) + 1*cyclic2 - 1): 1699 | 1700 | m = int(math.fmod(j+1, len(path2))) 1701 | v3 = path2[m] 1702 | v4 = path2[j] 1703 | 1704 | #closes point on path1 edge, closes_point on path 2 edge 1705 | 1706 | intersect = intersect_line_line(v1,v2,v3,v4) 1707 | 1708 | if intersect: 1709 | #make sure the intersection is within the segment 1710 | inter_1 = intersect[0] 1711 | verif1 = intersect_point_line(inter_1, v1,v2) 1712 | 1713 | inter_2 = intersect[1] 1714 | verif2 = intersect_point_line(inter_1, v3,v4) 1715 | 1716 | diff = inter_2 - inter_1 1717 | if diff.length < threshold and verif1[1] > 0 and verif2[1] > 0 and verif1[1] < 1 and verif2[1] < 1: 1718 | intersections.append(.5 * (inter_1 + inter_2)) 1719 | inds_1.append(i) 1720 | inds_2.append(j) 1721 | 1722 | if len(set(inds_1)) != len(inds_1): 1723 | print(' ') 1724 | print('HELP, HELP, HELP, HELP, HELP, HELP, HELP, HELP, HELP,') 1725 | print('there are multiple of the same index in intersection 1') 1726 | print(inds_1) 1727 | print(inds_2) 1728 | print(intersections) 1729 | doubles = find_doubles(inds_1) 1730 | ind = inds_1.index(doubles[0],1) 1731 | 1732 | inds_1.pop(ind) 1733 | inds_2.pop(ind) 1734 | intersections.pop(ind) 1735 | 1736 | if len(set(inds_2)) != len(inds_2): 1737 | print(' ') 1738 | print('HELP, HELP, HELP, HELP, HELP, HELP, HELP, HELP, HELP,') 1739 | print('HELP, there are multipl of the same index in intersection 2') 1740 | print(inds_2) 1741 | print(inds_1) 1742 | print(intersections) 1743 | 1744 | doubles = find_doubles(inds_2) 1745 | ind = inds_2.index(doubles[0],1) 1746 | 1747 | inds_1.pop(ind) 1748 | inds_2.pop(ind) 1749 | intersections.pop(ind) 1750 | 1751 | return intersections, inds_1, inds_2 1752 | 1753 | def fit_path_to_endpoints(path,v0,v1): 1754 | ''' 1755 | will rescale/rotate/tranlsate a path to fit between v0 and v1 1756 | v0 is starting poin corrseponding to path[0] 1757 | v1 is endpoint 1758 | ''' 1759 | new_path = path.copy() 1760 | 1761 | vi_0 = path[0] 1762 | vi_1 = path[-1] 1763 | 1764 | net_initial = vi_1 - vi_0 1765 | net_final = v1 - v0 1766 | 1767 | scale = net_final.length/net_initial.length 1768 | rot = rot_between_vecs(net_initial,net_final) 1769 | 1770 | 1771 | for i, v in enumerate(new_path): 1772 | new_path[i] = rot * v - vi_0 1773 | 1774 | for i, v in enumerate(new_path): 1775 | new_path[i] = scale * v 1776 | 1777 | trans = v0 - new_path[0] 1778 | 1779 | for i, v in enumerate(new_path): 1780 | new_path[i] += trans 1781 | 1782 | return new_path 1783 | 1784 | def pole_detector(bme): 1785 | 1786 | pole_inds = [] 1787 | 1788 | for vert in bme.verts: 1789 | if len(vert.link_edges) in {3,5,6}: 1790 | pole_inds.append(vert.index) 1791 | 1792 | return pole_inds 1793 | 1794 | def mix_path(path1,path2,pct = .5): 1795 | ''' 1796 | will produce a blended path between path1 and 2 by 1797 | interpolating each point along the path. 1798 | 1799 | will interpolate based on index at the moment, not based on pctg down the curve 1800 | 1801 | pct is weight for path 1. 1802 | ''' 1803 | 1804 | if len(path1) != len(path2): 1805 | print('eror until smarter programmer') 1806 | return path1 1807 | 1808 | new_path = [0]*len(path1) 1809 | 1810 | for i, v in enumerate(path1): 1811 | new_path[i] = v + pct * (path2[i] - v) 1812 | 1813 | return new_path 1814 | 1815 | def align_edge_loops(verts_1, verts_2, eds_1, eds_2): 1816 | ''' 1817 | Modifies vert order and edge indices to provide best 1818 | bridge between edge_loop1 and edge_loop2 1819 | 1820 | args: 1821 | verts_1: list of Vectors 1822 | verts_2: list of Vectors 1823 | 1824 | eds_1: connectivity of the first loop, really just to test loop or line 1825 | eds_2: connectivity of 2nd loops, really just to test for loop or line 1826 | 1827 | return: 1828 | verts_2 1829 | ''' 1830 | print('testing alignment') 1831 | if 0 in eds_1[-1]: 1832 | cyclic = True 1833 | print('cyclic vert chain') 1834 | else: 1835 | cyclic = False 1836 | 1837 | if len(verts_1) != len(verts_2): 1838 | print(len(verts_1)) 1839 | print(len(verts_2)) 1840 | print('non uniform loops, stopping until your developer gets smarter') 1841 | return verts_2 1842 | 1843 | 1844 | #turns out, sum of diagonals is > than semi perimeter 1845 | #lets exploit this (only true if quad is pretty much flat) 1846 | #if we have paths reversed...our indices will give us diagonals 1847 | #instead of perimeter 1848 | #D1_O = verts_2[0] - verts_1[0] 1849 | #D2_O = verts_2[-1] - verts_1[-1] 1850 | #D1_R = verts_2[0] - verts_1[-1] 1851 | #D2_R = verts_2[-1] - verts_1[0] 1852 | 1853 | #original_length = D1_O.length + D2_O.length 1854 | #reverse_length = D1_R.length + D2_R.length 1855 | #if reverse_length < original_length: 1856 | #verts_2.reverse() 1857 | #print('reversing') 1858 | 1859 | if cyclic: 1860 | #another test to verify loop direction is to take 1861 | #something reminiscint of the curl 1862 | #since the loops in our case are guaranteed planar 1863 | #(they come from cross sections) we can find a direction 1864 | #from which to take the curl pretty easily. Apologies to 1865 | #any real mathemeticians reading this becuase I just 1866 | #bastardized all these math terms. 1867 | V1_0 = verts_1[1] - verts_1[0] 1868 | V1_1 = verts_1[2] - verts_1[1] 1869 | 1870 | V2_0 = verts_2[1] - verts_2[0] 1871 | V2_1 = verts_2[2] - verts_2[1] 1872 | 1873 | no_1 = V1_0.cross(V1_1) 1874 | no_1.normalize() 1875 | no_2 = V2_0.cross(V2_1) 1876 | no_2.normalize() 1877 | 1878 | #we have no idea which way we will get 1879 | #so just pick the directions which are 1880 | #pointed in the general same direction 1881 | if no_1.dot(no_2) < 0: 1882 | no_2 = -1 * no_2 1883 | 1884 | #average the two directions 1885 | ideal_direction = no_1.lerp(no_1,.5) 1886 | 1887 | curl_1 = discrete_curl(verts_1, ideal_direction) 1888 | curl_2 = discrete_curl(verts_2, ideal_direction) 1889 | 1890 | if curl_1 * curl_2 < 0: 1891 | print('reversing loop 2') 1892 | print('curl1: %f and curl2: %f' % (curl_1,curl_2)) 1893 | verts_2.reverse() 1894 | 1895 | else: 1896 | #if the segement is not cyclic 1897 | #all we have to do is compare the endpoints 1898 | Vtotal_1 = verts_1[-1] - verts_1[0] 1899 | Vtotal_2 = verts_2[-1] - verts_2[0] 1900 | 1901 | if Vtotal_1.dot(Vtotal_2) < 0: 1902 | print('reversing path 2') 1903 | verts_2.reverse() 1904 | 1905 | #iterate all verts and "handshake problem" them 1906 | #into a dictionary? That's not very effecient! 1907 | edge_len_dict = {} 1908 | for i in range(0,len(verts_1)): 1909 | for n in range(0,len(verts_2)): 1910 | edge = (i,n) 1911 | vect = verts_2[n] - verts_1[i] 1912 | edge_len_dict[edge] = vect.length 1913 | 1914 | shift_lengths = [] 1915 | #shift_cross = [] 1916 | for shift in range(0,len(verts_2)): 1917 | tmp_len = 0 1918 | #tmp_cross = 0 1919 | for i in range(0, len(verts_2)): 1920 | shift_mod = int(math.fmod(i+shift, len(verts_2))) 1921 | tmp_len += edge_len_dict[(i,shift_mod)] 1922 | shift_lengths.append(tmp_len) 1923 | 1924 | final_shift = shift_lengths.index(min(shift_lengths)) 1925 | if final_shift != 0: 1926 | print("shifting verst by %i" % final_shift) 1927 | verts_2 = list_shift(verts_2, final_shift) 1928 | 1929 | return verts_2 1930 | 1931 | def cross_section_until_plane(bme, mx, point, normal, seed, pt_stop, normal_stop, max_tests = 10000, debug = True): 1932 | ''' 1933 | Takes a mesh and associated world matrix of the object and returns a cross secion in local 1934 | space which stops when it intersects the plane defined by pt_stop, normal_stop 1935 | 1936 | Args: 1937 | bme: Blender BMesh 1938 | mx: World matrix of the object to be cut(type Mathutils.Matrix) 1939 | point: any point on the cut plane in world coords (type Mathutils.Vector) 1940 | normal: cut plane normal direction in world(type Mathutisl.Vector) 1941 | seed: face index, typically achieved by raycast. 1942 | a known face which intersects the cutplane. 1943 | pt_stop: point on the plane defined to stop cutting. World Coords 1944 | (type Mathutils.Vector) 1945 | normal_stop: normal direction of the plane defined to stop cutting. World COORD 1946 | 1947 | Return: 1948 | list[Vector()] in local coords 1949 | ''' 1950 | 1951 | times = [] 1952 | times.append(time.time()) 1953 | 1954 | imx = mx.inverted() 1955 | pt = imx * point 1956 | pt_stop_local = imx * pt_stop 1957 | no = imx.to_3x3() * normal 1958 | normal_stop_local = imx.to_3x3() * normal_stop 1959 | 1960 | verts = {} 1961 | plane_hit = {} 1962 | 1963 | seeds = [] 1964 | prev_eds = [] 1965 | 1966 | #the simplest expected result is that we find 2 edges 1967 | for ed in bme.faces[seed].edges: 1968 | prev_eds.append(ed.index) 1969 | 1970 | A = ed.verts[0].co 1971 | B = ed.verts[1].co 1972 | result = cross_edge(A, B, pt, no) 1973 | 1974 | if result[0] and result[0] != 'CROSS': 1975 | print('got an anomoly') 1976 | print(result[0]) 1977 | 1978 | #here we are only tesing the good cases 1979 | if result[0] == 'CROSS': 1980 | #create a list to hold the verst we find from this seed 1981 | #start with the a point....go toward b 1982 | #TODO: CODE REVIEW...this looks like stupid code. 1983 | potential_faces = [face for face in ed.link_faces if face.index != seed] 1984 | if len(potential_faces): 1985 | f = potential_faces[0] 1986 | seeds.append(f) 1987 | verts[f.index] = [pt, result[1]] 1988 | 1989 | #TODO: debug and return values? 1990 | if len(seeds) == 0: 1991 | print('failure to find a direction to start with') 1992 | return None 1993 | 1994 | total_tests = 0 1995 | for initial_element in seeds: 1996 | element_tests = 0 1997 | element = initial_element 1998 | stop_test = None 1999 | while element and total_tests < max_tests and not stop_test: 2000 | total_tests += 1 2001 | element_tests += 1 2002 | 2003 | if type(element) == bmesh.types.BMFace: 2004 | element = face_cycle(element, pt, no, prev_eds, verts[initial_element.index]) 2005 | 2006 | elif type(element) == bmesh.types.BMVert: 2007 | print('do we ever use the vert cycle?') 2008 | element = vert_cycle(element, pt, no, prev_eds, verts[initial_element.index]) 2009 | 2010 | if element: 2011 | A = verts[initial_element.index][-2] 2012 | B = verts[initial_element.index][-1] 2013 | cross = cross_edge(A, B, pt_stop_local, normal_stop_local) 2014 | stop_test = cross[0] 2015 | if stop_test: 2016 | prev_eds.pop() #will need to retest this edge in case we come around a full loop 2017 | verts[initial_element.index].pop() 2018 | verts[initial_element.index].append(cross[1]) 2019 | plane_hit[initial_element.index] = True 2020 | 2021 | else: 2022 | plane_hit[initial_element.index] = False 2023 | 2024 | if total_tests-2 > max_tests: 2025 | print('maxed out tests') 2026 | 2027 | print('completed %i tests in this seed search' % element_tests) 2028 | 2029 | 2030 | #this iterates the keys in verts 2031 | if len(verts): 2032 | plane_chains = [verts[key] for key in verts if len(verts[key]) >= 2 and plane_hit[key]] 2033 | loose_chains = [verts[key] for key in verts if len(verts[key]) >= 2 and not plane_hit[key]] 2034 | 2035 | print('%i chains hit the plane' % len(plane_chains)) 2036 | print('%i chains did not hit the plane' % len(loose_chains)) 2037 | 2038 | 2039 | #loose chains only 2040 | if len(plane_chains) == 0 and len(loose_chains): 2041 | if len(loose_chains) == 1: 2042 | print('one loose chain') 2043 | return loose_chains[0] 2044 | else: 2045 | print('best loose chain') 2046 | return min(loose_chains, key=lambda x: distance_point_to_plane(x[-1], pt_stop_local, normal_stop_local)) 2047 | 2048 | 2049 | if len(plane_chains) == 1: 2050 | print('single plane chain') 2051 | return plane_chains[0] 2052 | 2053 | 2054 | if len(plane_chains) > 1: 2055 | print('best plane chain') 2056 | return min(plane_chains, key=lambda x: get_path_length(x)) 2057 | #if one of each: 2058 | #return the one what hit the plane 2059 | #plane chains only 2060 | #pick the shortest one 2061 | else: 2062 | print('failed to find a cut that hit the plane...perhaps we dont intersect that plane') 2063 | return None 2064 | 2065 | def cross_section_2_seeds(bme, mx, point, normal, pt_a, seed_index_a, pt_b, seed_index_b, max_tests = 10000, debug = True): 2066 | ''' 2067 | Takes a mesh and associated world matrix of the object and returns a cross secion in local 2068 | space. 2069 | 2070 | Args: 2071 | bme: Blender BMesh 2072 | mx: World matrix (type Mathutils.Matrix) 2073 | point: any point on the cut plane in world coords (type Mathutils.Vector) 2074 | normal: plane normal direction (type Mathutisl.Vector) 2075 | seed: face index, typically achieved by raycast 2076 | exclude_edges: list of edge indices (usually already tested from previous iterations) 2077 | ''' 2078 | 2079 | times = [] 2080 | times.append(time.time()) 2081 | 2082 | imx = mx.inverted() 2083 | pt = imx * point 2084 | no = imx.to_3x3() * normal 2085 | 2086 | 2087 | #we will store vert chains here 2088 | #indexed by the face they start with 2089 | #after the initial seed facc 2090 | #___________________ 2091 | #| | | | 2092 | #| 1 |init | 2 | 2093 | #|_____|_____|______| 2094 | # 2095 | verts = {} 2096 | 2097 | 2098 | #we need to test all edges of both faces for plane intersection 2099 | #we should get intersections, because we define the plane 2100 | #initially between the two seeds 2101 | 2102 | seeds = [] 2103 | prev_eds = [] 2104 | 2105 | #the simplest expected result is that we find 2 edges 2106 | for ed in bme.faces[seed_index_a].edges: 2107 | 2108 | 2109 | prev_eds.append(ed.index) 2110 | 2111 | A = ed.verts[0].co 2112 | B = ed.verts[1].co 2113 | result = cross_edge(A, B, pt, no) 2114 | 2115 | 2116 | if result[0] and result[0] != 'CROSS': 2117 | print('got an anomoly') 2118 | print(result[0]) 2119 | print('that is the result ^') 2120 | #here we are only tesing the good cases 2121 | if result[0] == 'CROSS': 2122 | #create a list to hold the verst we find from this seed 2123 | #start with the a point....go toward b 2124 | 2125 | 2126 | #TODO: CODE REVIEW...this looks like stupid code. 2127 | potential_faces = [face for face in ed.link_faces if face.index != seed_index_a] 2128 | if len(potential_faces): 2129 | f = potential_faces[0] 2130 | seeds.append(f) 2131 | 2132 | #we will keep track of our growing vert chains 2133 | #based on the face they start with 2134 | verts[f.index] = [pt_a] 2135 | verts[f.index].append(result[1]) 2136 | 2137 | 2138 | #we now have 1 or two faces on either side of seed_face_a 2139 | #now we walk until we do or dont find seed_face_b 2140 | #this is a brute force, and we make no assumptions about which 2141 | #direction is better to head in first. 2142 | total_tests = 0 2143 | for initial_element in seeds: #this will go both ways if they dont meet up. 2144 | element_tests = 0 2145 | element = initial_element 2146 | stop_test = None 2147 | while element and total_tests < max_tests and stop_test != seed_index_b: 2148 | total_tests += 1 2149 | element_tests += 1 2150 | #first, we know that this face is not coplanar..that's good 2151 | #if new_face.no.cross(no) == 0: 2152 | #print('coplanar face, stopping calcs until your programmer gets smarter') 2153 | #return None 2154 | if type(element) == bmesh.types.BMFace: 2155 | element = face_cycle(element, pt, no, prev_eds, verts[initial_element.index])#, edge_mapping) 2156 | if element: 2157 | stop_test = element.index 2158 | else: 2159 | stop_test = None 2160 | 2161 | elif type(element) == bmesh.types.BMVert: 2162 | print('do we ever use the vert cycle?') 2163 | element = vert_cycle(element, pt, no, prev_eds, verts[initial_element.index])#, edge_mapping) 2164 | stop_test = None 2165 | 2166 | if stop_test == seed_index_b: 2167 | print('found the other face!') 2168 | verts[initial_element.index].append(pt_b) 2169 | print('%i vertices found so far' % len(verts[initial_element.index])) 2170 | 2171 | else: 2172 | #trash the vert data...we aren't interested 2173 | #if we want to do other stuff later...we can 2174 | #for now we will go on to the other side of 2175 | #the seed face 2176 | print('I think we made a loop w/o finding the intiial ege?') 2177 | print('Perhaps we found a mesh edge?') 2178 | #del verts[initial_element.index] 2179 | 2180 | if total_tests-2 > max_tests: 2181 | print('maxed out tests') 2182 | 2183 | #print('completed %i tests in this seed search' % element_tests) 2184 | 2185 | 2186 | #this iterates the keys in verts 2187 | #i have kept the keys consistent for 2188 | #verts 2189 | if len(verts): 2190 | 2191 | #print('picking the shortest path by elements') 2192 | #print('later we will return both paths to allow') 2193 | #print('sorting by path length or by proximity to view') 2194 | 2195 | chains = [verts[key] for key in verts if len(verts[key]) > 2] 2196 | if len(chains): 2197 | sizes = [len(chain) for chain in chains] 2198 | #print(sizes) 2199 | best = min(sizes) 2200 | ind = sizes.index(best) 2201 | 2202 | return chains[ind] 2203 | else: 2204 | print('failure no chains > 2 verts') 2205 | return [] 2206 | 2207 | else: 2208 | print('failed to find connection in either direction...perhaps points arent coplanar') 2209 | return [] 2210 | 2211 | 2212 | def cross_section_seed_ver0(bme, mx, 2213 | point, normal, 2214 | seed_index, 2215 | max_tests = 10000, debug = True): 2216 | ''' 2217 | Takes a mesh and associated world matrix of the object and returns a cross secion in local 2218 | space. 2219 | 2220 | Args: 2221 | bme: Blender BMesh 2222 | mx: World matrix (type Mathutils.Matrix) 2223 | point: any point on the cut plane in world coords (type Mathutils.Vector) 2224 | normal: plane normal direction (type Mathutisl.Vector) 2225 | seed_index: face index, typically achieved by raycast 2226 | self_stop: a normal vector which defines a plane to stop cutting 2227 | direction: Vector which the cut should start traveling. 2228 | exclude_edges: list of edge indices (usually already tested from previous iterations) 2229 | ''' 2230 | 2231 | times = [] 2232 | times.append(time.time()) 2233 | 2234 | verts =[] 2235 | eds = [] 2236 | 2237 | #convert point and normal into local coords 2238 | imx = mx.inverted() 2239 | pt = imx * point 2240 | no = imx.to_3x3() * normal #local normal 2241 | 2242 | #edge_mapping = {} #perhaps we should use bmesh becaus it stores the great cycles..answer yup 2243 | 2244 | #first initial search around seeded face. 2245 | #if none, we may go back to brute force 2246 | #but prolly not :-) 2247 | seed_search = 0 2248 | prev_eds = [] 2249 | seeds =[] 2250 | 2251 | if seed_index > len(bme.faces) - 1: 2252 | print('looks like we hit an Ngon, tentative support') 2253 | 2254 | #perhaps this should be done before we pass bme to this op? 2255 | #we may perhaps need to re raycast the new faces? 2256 | ngons = [] 2257 | for f in bme.faces: 2258 | if len(f.verts) > 4: 2259 | ngons.append(f) 2260 | 2261 | #we should never get to this point because we are pre 2262 | #triangulating the ngons before this function in the 2263 | #final work flow but this leaves no chance and keeps 2264 | #options to reuse this in later work 2265 | if len(ngons): 2266 | new_geom = bmesh.ops.triangulate(bme, faces = ngons, use_beauty = True) 2267 | new_faces = new_geom['faces'] 2268 | 2269 | #now we must find a new seed index since we have added new geometry 2270 | for f in new_faces: 2271 | if point_in_tri(pt, f.verts[0].co, f.verts[1].co, f.verts[2].co): 2272 | print('found the point int he tri') 2273 | if distance_point_to_plane(pt, f.verts[0].co, f.normal) < .0000001: 2274 | seed_index = f.index 2275 | print('found a new index to start with') 2276 | break 2277 | 2278 | for ed in bme.faces[seed_index].edges: 2279 | seed_search += 1 2280 | prev_eds.append(ed.index) 2281 | 2282 | A = ed.verts[0].co 2283 | B = ed.verts[1].co 2284 | result = cross_edge(A, B, pt, no) 2285 | if result[0] == 'CROSS': 2286 | potential_faces = [face for face in ed.link_faces if face.index != seed_index] 2287 | 2288 | if len(potential_faces): 2289 | f = potential_faces[0] 2290 | verts.append(result[1]) 2291 | seeds.append(f) 2292 | 2293 | if not len(seeds): 2294 | print('cancelling until your programmer gets smarter') 2295 | return (None,None) 2296 | 2297 | #we have found one edge that crosses, now, baring any terrible disconnections in the mesh, 2298 | #we traverse through the link faces, wandering our way through....removing edges from our list 2299 | total_tests = 0 2300 | 2301 | #We find A then B then start at A... so there is a 2302 | #reverse in the vert order at the middle. 2303 | verts.reverse() 2304 | for element in seeds: #this will go both ways if they dont meet up. 2305 | element_tests = 0 2306 | while element and total_tests < max_tests: 2307 | total_tests += 1 2308 | element_tests += 1 2309 | #first, we know that this face is not coplanar..that's good 2310 | #if new_face.no.cross(no) == 0: 2311 | #print('coplanar face, stopping calcs until your programmer gets smarter') 2312 | #return None 2313 | if type(element) == bmesh.types.BMFace: 2314 | element = face_cycle(element, pt, no, prev_eds, verts)#, edge_mapping) 2315 | 2316 | elif type(element) == bmesh.types.BMVert: 2317 | element = vert_cycle(element, pt, no, prev_eds, verts)#, edge_mapping) 2318 | 2319 | #print('completed %i tests in this seed search' % element_tests) 2320 | #print('%i vertices found so far' % len(verts)) 2321 | 2322 | 2323 | #The following tests for a closed loop 2324 | #if the loop found itself on the first go round, the last test 2325 | #will only get one try, and find no new crosses 2326 | #trivially, mast make sure that the first seed we found wasn't 2327 | #on a non manifold edge, which should never happen 2328 | #TODO: find a better way to determine this. Currently we dont preserve 2329 | #enough information 2330 | closed_loop = element_tests == 1 and len(seeds) == 2 2331 | 2332 | 2333 | if debug: 2334 | n = len(times) 2335 | times.append(time.time()) 2336 | #print('calced intersections %f sec' % (times[n]-times[n-1])) 2337 | 2338 | #iterate through smartly to create edge keys 2339 | #no longer have to do this...verts are created in order 2340 | 2341 | if closed_loop: 2342 | for i in range(0,len(verts)-1): 2343 | eds.append((i,i+1)) 2344 | 2345 | #the edge loop closure 2346 | eds.append((i+1,0)) 2347 | 2348 | else: 2349 | #two more verts found than total tests 2350 | #one vert per element test in the last loop 2351 | 2352 | 2353 | #split the loop into the verts into the first seed and 2nd seed 2354 | seed_1_verts = verts[:len(verts)-(element_tests)] #yikes maybe this index math is right 2355 | seed_2_verts = verts[len(verts)-(element_tests):] 2356 | seed_2_verts.reverse() 2357 | seed_2_verts.extend(seed_1_verts) 2358 | 2359 | for i in range(0,len(seed_1_verts)-1): 2360 | eds.append((i,i+1)) 2361 | 2362 | verts = seed_2_verts 2363 | if debug: 2364 | n = len(times) 2365 | times.append(time.time()) 2366 | #print('calced connectivity %f sec' % (times[n]-times[n-1])) 2367 | 2368 | if len(verts): 2369 | 2370 | return (verts, eds) 2371 | else: 2372 | return (None, None) 2373 | 2374 | 2375 | 2376 | def find_bmedges_crossing_plane(pt, no, edges, epsilon): 2377 | ''' 2378 | returns list of edges that *cross* plane and corresponding intersection points 2379 | ''' 2380 | 2381 | coords = {} 2382 | for edge in edges: 2383 | v0,v1 = edge.verts 2384 | if v0 not in coords: coords[v0] = no.dot(v0.co-pt) 2385 | if v1 not in coords: coords[v1] = no.dot(v1.co-pt) 2386 | #print(str(coords)) 2387 | 2388 | ret = [] 2389 | for edge in edges: 2390 | v0,v1 = edge.verts 2391 | s0,s1 = coords[v0],coords[v1] 2392 | if s0 > epsilon and s1 > epsilon: continue 2393 | if s0 < -epsilon and s1 < -epsilon: continue 2394 | #if not ((s0>epsilon and s1<-epsilon) or (s0<-epsilon and s1>epsilon)): # edge cross plane? 2395 | # continue 2396 | 2397 | i = intersect_line_plane(v0.co, v1.co, pt, no) 2398 | ret += [(edge,i)] 2399 | return ret 2400 | 2401 | def find_distant_bmedge_crossing_plane(pt, no, edges, epsilon, eind_from, co_from): 2402 | ''' 2403 | returns the farthest edge that *crosses* plane and corresponding intersection point 2404 | ''' 2405 | 2406 | if(len(edges)==3): 2407 | # shortcut (no need to find farthest... just find first) 2408 | for edge in edges: 2409 | if edge.index == eind_from: continue 2410 | v0,v1 = edge.verts 2411 | co0,co1 = v0.co,v1.co 2412 | s0,s1 = no.dot(co0 - pt), no.dot(co1 - pt) 2413 | no_cross = not ((s0>epsilon and s1<-epsilon) or (s0<-epsilon and s1>epsilon)) 2414 | if no_cross: continue 2415 | i = intersect_line_plane(co0, co1, pt, no) 2416 | return (edge,i) 2417 | 2418 | d_max,edge_max,i_max = -1.0,None,None 2419 | for edge in edges: 2420 | if edge.index == eind_from: continue 2421 | 2422 | v0,v1 = edge.verts 2423 | co0,co1 = v0.co, v1.co 2424 | s0,s1 = no.dot(co0 - pt), no.dot(co1 - pt) 2425 | if s0 > epsilon and s1 > epsilon: continue 2426 | if s0 < -epsilon and s1 < -epsilon: continue 2427 | #if not ((s0>epsilon and s1<-epsilon) or (s0<-epsilon and s1>epsilon)): # edge cross plane? 2428 | # continue 2429 | 2430 | i = intersect_line_plane(co0, co1, pt, no) 2431 | d = (co_from - i).length 2432 | if d > d_max: d_max,edge_max,i_max = d,edge,i 2433 | return (edge_max,i_max) 2434 | 2435 | def cross_section_walker(bme, pt, no, find_from, eind_from, co_from, epsilon): 2436 | ''' 2437 | returns tuple (verts,looped) by walking around a bmesh near the given plane 2438 | verts is list of verts as the intersections of edges and cutting plane (in order) 2439 | looped is bool indicating if walk wrapped around bmesh 2440 | ''' 2441 | 2442 | # returned values 2443 | verts = [co_from] 2444 | looped = False 2445 | 2446 | # track what we've seen 2447 | finds_dict = {find_from: 0} 2448 | 2449 | # get blender version 2450 | bver = '%03d.%03d.%03d' % (bpy.app.version[0],bpy.app.version[1],bpy.app.version[2]) 2451 | 2452 | if bver > '002.072.000': 2453 | bme.edges.ensure_lookup_table(); 2454 | 2455 | f_cur = next(f for f in bme.edges[eind_from].link_faces if f.index != find_from) 2456 | find_current = f_cur.index 2457 | 2458 | while True: 2459 | # find farthest point 2460 | edge,i = find_distant_bmedge_crossing_plane(pt, no, f_cur.edges, epsilon, eind_from, co_from) 2461 | verts += [i] 2462 | if len(edge.link_faces) == 1: break # hit end? 2463 | 2464 | # get next face, edge, co 2465 | f_next = next(f for f in edge.link_faces if f.index != find_current) 2466 | find_next = f_next.index 2467 | eind_next = edge.index 2468 | co_next = i 2469 | 2470 | if find_next in finds_dict: # looped 2471 | looped = True 2472 | if finds_dict[find_next] != 0: 2473 | # loop is P-shaped (loop with a tail) 2474 | verts = verts[finds_dict[find_next]:] # clip off tail 2475 | break 2476 | 2477 | # leave breadcrumb 2478 | finds_dict[find_next] = len(finds_dict) 2479 | 2480 | find_from = find_current 2481 | eind_from = eind_next 2482 | co_from = co_next 2483 | 2484 | f_cur = f_next 2485 | find_current = find_next 2486 | 2487 | return (verts,looped) 2488 | 2489 | def cross_section_seed_ver1(bme, mx, 2490 | point, normal, 2491 | seed_index, 2492 | max_tests = 10000, debug = True): 2493 | 2494 | # data to be returned 2495 | verts,edges = [],[] 2496 | 2497 | # max distance a coplanar vertex can be from plane 2498 | epsilon = 0.0000000001 2499 | 2500 | #convert plane defn (point and normal) into local coords 2501 | imx = mx.inverted() 2502 | pt = imx * point 2503 | no = (imx.to_3x3() * normal).normalized() 2504 | 2505 | # get blender version 2506 | bver = '%03d.%03d.%03d' % (bpy.app.version[0],bpy.app.version[1],bpy.app.version[2]) 2507 | 2508 | if bver > '002.072.000': 2509 | bme.faces.ensure_lookup_table(); 2510 | 2511 | # make sure that plane crosses face! 2512 | lco = [v.co for v in bme.faces[seed_index].verts] 2513 | ld = [no.dot(co - pt) for co in lco] 2514 | if all(d > epsilon for d in ld) or all(d < -epsilon for d in ld): # does face cross plane? 2515 | # shift pt so plane crosses face 2516 | shift_dist = (min(ld)+epsilon) if ld[0] > epsilon else (max(ld)-epsilon) 2517 | pt += no * shift_dist 2518 | print('>>> shifting') 2519 | print('>>> ' + str(ld)) 2520 | print('>>> ' + shift_dist) 2521 | print('>>> ' + no*shift_dist) 2522 | 2523 | # find intersections of edges and cutting plane 2524 | bmface = bme.faces[seed_index] 2525 | bmedges = bmface.edges 2526 | ei_init = find_bmedges_crossing_plane(pt, no, bmedges, epsilon) 2527 | 2528 | if len(ei_init) < 2: 2529 | print('warning: it should not reach here! len(ei_init) = %d' % len(ei_init)) 2530 | print('lengths = ' + str([(edge.verts[0].co-edge.verts[1].co).length for edge in bmedges])) 2531 | return (None,None) 2532 | elif len(ei_init) == 2: 2533 | # simple case 2534 | ei0_max, ei1_max = ei_init 2535 | else: 2536 | # convex polygon 2537 | # find two farthest points 2538 | d_max, ei0_max, ei1_max = -1.0, None, None 2539 | for ei0,ei1 in combinations(ei_init, 2): 2540 | d = (ei0[1] - ei1[1]).length 2541 | if d > d_max: d_max,ei0_max,ei1_max = d,ei0,ei1 2542 | 2543 | # start walking one way around bmesh 2544 | verts0,looped = cross_section_walker(bme, pt, no, seed_index, ei0_max[0].index, ei0_max[1], epsilon) 2545 | 2546 | if looped: 2547 | # looped around on self, so we're done! 2548 | verts = verts0 2549 | nv = len(verts) 2550 | edges = [(i,(i+1)%nv) for i in range(nv)] 2551 | 2552 | return (verts, edges) 2553 | 2554 | # did not loop around, so start walking the other way 2555 | verts1,looped = cross_section_walker(bme, pt, no, seed_index, ei1_max[0].index, ei1_max[1], epsilon) 2556 | 2557 | if looped: 2558 | # looped around on self!? 2559 | print('warning: looped one way but not the other') 2560 | verts = verts1 2561 | nv = len(verts) 2562 | edges = [(i,(i+1)%nv) for i in range(nv)] 2563 | 2564 | return (verts, edges) 2565 | 2566 | # combine two walks 2567 | verts = list(reversed(verts0)) + verts1 2568 | nv = len(verts) 2569 | edges = [(i,i+1) for i in range(nv-1)] 2570 | 2571 | return (verts, edges) 2572 | 2573 | 2574 | 2575 | def cross_section_seed(bme, mx, 2576 | point, normal, 2577 | seed_index, 2578 | max_tests = 10000, debug = True, method = False): 2579 | ''' 2580 | Takes a mesh and associated world matrix of the object and returns a cross secion in local 2581 | space. 2582 | 2583 | Args: 2584 | bme: Blender BMesh 2585 | mx: World matrix (type Mathutils.Matrix) 2586 | point: any point on the cut plane in world coords (type Mathutils.Vector) 2587 | normal: plane normal direction (type Mathutisl.Vector) 2588 | seed_index: face index, typically achieved by raycast 2589 | self_stop: a normal vector which defines a plane to stop cutting 2590 | direction: Vector which the cut should start traveling. 2591 | exclude_edges: list of edge indices (usually already tested from previous iterations) 2592 | ''' 2593 | 2594 | start = time.time() 2595 | 2596 | if not method: 2597 | ret = cross_section_seed_ver0(bme, mx, point, normal, seed_index, max_tests, debug) 2598 | 2599 | else: 2600 | ret = cross_section_seed_ver1(bme, mx, point, normal, seed_index, max_tests, debug) 2601 | 2602 | calc_time = time.time() 2603 | 2604 | print('the new method was used: %r' % method) 2605 | if ret[0]: 2606 | print('%i verts were found in %f seconds' % (len(ret[0]), (calc_time - start))) 2607 | else: 2608 | print('Cutting failed') 2609 | 2610 | return ret 2611 | 2612 | def cross_section_seed_direction(bme, mx, 2613 | point, normal, 2614 | seed_index, direction, 2615 | stop_plane = None, 2616 | max_tests = 10000, 2617 | debug = True): 2618 | ''' 2619 | Takes a bmesh and associated world matrix of the object and 2620 | returns a cross secion in local space. 2621 | bmesh should not have any ngons (tris and quads only). 2622 | If original bmesh has ngons, triangulate the bmesh 2623 | or a copy of the bmesh first. 2624 | 2625 | Args: 2626 | bme: Blender BMesh 2627 | mx: World matrix (type Mathutils.Matrix) 2628 | point: any point on the cut plane in world coords (type Mathutils.Vector) 2629 | normal: plane normal direction (type Mathutisl.Vector) 2630 | seed_index: face index, typically achieved by raycast 2631 | direction: Vector which the cut should start traveling. 2632 | 2633 | stop_plane = [stop_pt, stop_no] a 2 item list of 2 vectors defining a plane to stop at 2634 | ''' 2635 | 2636 | times = [] 2637 | times.append(time.time()) 2638 | 2639 | #convert point and normal directoin into local coords 2640 | imx = mx.inverted() 2641 | pt = imx * point 2642 | no = imx.to_3x3() * normal #local normal 2643 | direct = imx.to_3x3() * direction 2644 | direct.normalize() 2645 | 2646 | if stop_plane: 2647 | stop_pt = imx * stop_plane[0] 2648 | stop_no = imx.to_3x3() * stop_plane[1] 2649 | 2650 | prev_eds = [] 2651 | seeds = {} #a list of 0,1, or 2 edges. 2652 | 2653 | #return values 2654 | verts =[] 2655 | eds = [] 2656 | 2657 | for ed in bme.faces[seed_index].edges: #should be 3 or 4 edges 2658 | prev_eds.append(ed.index) 2659 | A = ed.verts[0].co 2660 | B = ed.verts[1].co 2661 | result = cross_edge(A, B, pt, no) 2662 | if result[0] == 'CROSS': 2663 | 2664 | verts.append(result[1]) 2665 | potential_faces = [face for face in ed.link_faces if face.index != seed_index] 2666 | 2667 | if len(potential_faces): 2668 | 2669 | f = potential_faces[0] 2670 | seeds[len(verts)-1] = f 2671 | 2672 | else: 2673 | seeds[len(verts)-1] = None 2674 | print('seed face is an edge of mesh face') 2675 | 2676 | if len(verts) < 2: 2677 | print('critical error, probably machine error (len(verts) = %d)' % len(verts)) 2678 | #TODO: debug and dump relevant info 2679 | return (None, None) 2680 | 2681 | elif len(verts) > 2: 2682 | print('critial error probably concave ngon or something (len(verts) = %d)' % len(verts)) 2683 | #TODO: debug and dump relevant info 2684 | return (None, None) 2685 | 2686 | else: 2687 | headed = verts[0] - verts[1] 2688 | headed.normalize() 2689 | 2690 | if headed.dot(direct) > .1: 2691 | element = seeds[0] 2692 | verts.pop(1) 2693 | verts.insert(0,pt) 2694 | 2695 | if not element: 2696 | return (verts,[(0,1)]) 2697 | else: 2698 | element = seeds[1] 2699 | verts.pop(0) 2700 | verts.insert(0,pt) 2701 | 2702 | if not element: 2703 | return (verts,[(0,1)]) 2704 | 2705 | total_tests = 0 2706 | stop_test = False 2707 | 2708 | while element and total_tests < max_tests and not stop_test: 2709 | total_tests += 1 2710 | #first, we know that this face is not coplanar..that's good 2711 | #if new_face.no.cross(no) == 0: 2712 | #print('coplanar face, stopping calcs until your programmer gets smarter') 2713 | #return None 2714 | if type(element) == bmesh.types.BMFace: 2715 | element = face_cycle(element, pt, no, prev_eds, verts)#, edge_mapping) 2716 | 2717 | elif type(element) == bmesh.types.BMVert: 2718 | #TODO: I would like to debug if we hit a 2719 | #vert 2720 | element = vert_cycle(element, pt, no, prev_eds, verts)#, edge_mapping) 2721 | 2722 | if element and stop_plane and total_tests > 1: 2723 | A = verts[-2] 2724 | B = verts[-1] 2725 | cross = cross_edge(A, B, stop_pt, stop_no) 2726 | stop_test = cross[0] 2727 | if stop_test: 2728 | prev_eds.pop() #will need to retest this edge in case we come around a full loop 2729 | verts.pop() 2730 | verts.append(cross[1]) 2731 | 2732 | #verts are created in order 2733 | for i in range(0,len(verts)-1): 2734 | eds.append((i,i+1)) 2735 | 2736 | if debug: 2737 | n = len(times) 2738 | times.append(time.time()) 2739 | #print('calced connectivity %f sec' % (times[n]-times[n-1])) 2740 | 2741 | if len(verts): 2742 | return (verts, eds) 2743 | else: 2744 | return (None, None) 2745 | 2746 | 2747 | def intersect_path_plane(verts, pt, no, mode = 'FIRST'): 2748 | ''' 2749 | Inds the intersection of a vert chain with a plane 2750 | for cyclic vert paths duplicate end vert.. 2751 | may add cyclic test later. 2752 | mode will determine if only the first intersection is returned 2753 | or all the intersections of a path with a plane. 2754 | 2755 | args: 2756 | verts: list of vectors type mathutils.Vector 2757 | pt: plane pt 2758 | no: plane normal for intersection 2759 | mode: enum in 'FIRST', 'ALL' 2760 | 2761 | return: 2762 | a list of intersections or None 2763 | ''' 2764 | 2765 | #TODO: input quality checks for variables 2766 | 2767 | intersects = [] 2768 | n = len(verts) if verts else 0 2769 | 2770 | for i in range(0,n-1): 2771 | cross = cross_edge(verts[i], verts[i+1], pt, no) 2772 | 2773 | if cross[0]: 2774 | intersects.append(cross[1]) 2775 | 2776 | if mode == 'FIRST': 2777 | break 2778 | 2779 | if len(intersects) == 0: 2780 | intersects = [None] 2781 | 2782 | return intersects --------------------------------------------------------------------------------