├── .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
--------------------------------------------------------------------------------