diff options
author | zeffii <tetha.z@gmail.com> | 2016-08-01 00:23:44 +0300 |
---|---|---|
committer | zeffii <tetha.z@gmail.com> | 2016-08-01 00:23:44 +0300 |
commit | 3ce78656947b1b03125f5ca588a50878dc1a0e2a (patch) | |
tree | b3521b7606997ed1c33e8bd84e81d748f7ef00c4 /mesh_tiny_cad | |
parent | 96a957faf31e15ed4922b9cb544fd154b01dee5e (diff) |
move to release
Diffstat (limited to 'mesh_tiny_cad')
-rw-r--r-- | mesh_tiny_cad/BIX.py | 103 | ||||
-rw-r--r-- | mesh_tiny_cad/CCEN.py | 167 | ||||
-rw-r--r-- | mesh_tiny_cad/CFG.py | 84 | ||||
-rw-r--r-- | mesh_tiny_cad/E2F.py | 95 | ||||
-rw-r--r-- | mesh_tiny_cad/README.md | 91 | ||||
-rw-r--r-- | mesh_tiny_cad/V2X.py | 72 | ||||
-rw-r--r-- | mesh_tiny_cad/VTX.py | 183 | ||||
-rw-r--r-- | mesh_tiny_cad/XALL.py | 189 | ||||
-rw-r--r-- | mesh_tiny_cad/__init__.py | 75 | ||||
-rw-r--r-- | mesh_tiny_cad/cad_module.py | 172 | ||||
-rw-r--r-- | mesh_tiny_cad/icons/BIX.png | bin | 0 -> 3089 bytes | |||
-rw-r--r-- | mesh_tiny_cad/icons/CCEN.png | bin | 0 -> 4434 bytes | |||
-rw-r--r-- | mesh_tiny_cad/icons/E2F.png | bin | 0 -> 4867 bytes | |||
-rw-r--r-- | mesh_tiny_cad/icons/V2X.png | bin | 0 -> 2583 bytes | |||
-rw-r--r-- | mesh_tiny_cad/icons/VTX.png | bin | 0 -> 3263 bytes | |||
-rw-r--r-- | mesh_tiny_cad/icons/XALL.png | bin | 0 -> 4177 bytes |
16 files changed, 1231 insertions, 0 deletions
diff --git a/mesh_tiny_cad/BIX.py b/mesh_tiny_cad/BIX.py new file mode 100644 index 00000000..4f021896 --- /dev/null +++ b/mesh_tiny_cad/BIX.py @@ -0,0 +1,103 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + + +import bpy +import bmesh +from . import cad_module as cm + + +def add_line_to_bisection(self): + + obj = bpy.context.object + me = obj.data + bm = bmesh.from_edit_mesh(me) + + if hasattr(bm.verts, "ensure_lookup_table"): + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + + edges = [e for e in bm.edges if e.select and not e.hide] + + if not len(edges) == 2: + msg = "select two coplanar non parallel edges" + self.report({"WARNING"}, msg) + return + + [[v1, v2], [v3, v4]] = [[v.co for v in e.verts] for e in edges] + print('vectors found:\n', v1, '\n', v2, '\n', v3, '\n', v4) + + dist1 = (v1 - v2).length + dist2 = (v3 - v4).length + bdist = min([dist1, dist2]) + edge1 = (v1, v2) + edge2 = (v3, v4) + + if not cm.test_coplanar(edge1, edge2): + msg = "edges must be coplanar non parallel edges" + self.report({"WARNING"}, msg) + return + + # get pt and pick fartest vertex from (projected) intersections + pt = cm.get_intersection(edge1, edge2) + far1 = v2 if (v1 - pt).length < (v2 - pt).length else v1 + far2 = v4 if (v3 - pt).length < (v4 - pt).length else v3 + # print('intersection: ', pt) + + dex1 = far1 - pt + dex2 = far2 - pt + dex1 = dex1 * (bdist / dex1.length) + dex2 = dex2 * (bdist / dex2.length) + pt2 = pt + (dex1).lerp(dex2, 0.5) + # print('bisector point:', pt2) + + pt3 = pt2.lerp(pt, 2.0) + + vec1 = bm.verts.new(pt2) + vec2 = bm.verts.new(pt) + vec3 = bm.verts.new(pt3) + bm.edges.new((vec1, vec2)) + bm.edges.new((vec2, vec3)) + bmesh.update_edit_mesh(me) + # print("done") + + +class TCLineOnBisection(bpy.types.Operator): + '''Generate the bisector of two selected edges''' + bl_idname = 'tinycad.linetobisect' + bl_label = 'BIX line to bisector' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + obj = context.active_object + return all([obj is not None, obj.type == 'MESH', obj.mode == 'EDIT']) + + def execute(self, context): + add_line_to_bisection(self) + return {'FINISHED'} + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) diff --git a/mesh_tiny_cad/CCEN.py b/mesh_tiny_cad/CCEN.py new file mode 100644 index 00000000..f625504b --- /dev/null +++ b/mesh_tiny_cad/CCEN.py @@ -0,0 +1,167 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + + +import math + +import bpy +import bmesh +import mathutils +from mathutils import geometry +from mathutils import Vector + + +def generate_bmesh_repr(p1, v1, axis, num_verts): + ''' + p1: center of circle (local coordinates) + v1: first vertex of circle in (local coordinates) + axis: orientation matrix + origin: obj.location + ''' + props = bpy.context.scene.tinycad_props + rescale = props.rescale + + # generate geometry up front + chain = [] + gamma = 2 * math.pi / num_verts + for i in range(num_verts + 1): + theta = gamma * i + mat_rot = mathutils.Matrix.Rotation(theta, 4, axis) + local_point = (mat_rot * ((v1 - p1) * rescale)) + chain.append(local_point + p1) + + obj = bpy.context.edit_object + me = obj.data + bm = bmesh.from_edit_mesh(me) + + # add verts + v_refs = [] + for p in chain: + v = bm.verts.new(p) + v.select = False # this might be a default.. redundant? + v_refs.append(v) + + # join verts, daisy chain + num_verts = len(v_refs) + for i in range(num_verts): + idx1 = i + idx2 = (i + 1) % num_verts + bm.edges.new([v_refs[idx1], v_refs[idx2]]) + + bmesh.update_edit_mesh(me, True) + + +def generate_3PT(pts, obj, nv, mode=1): + mw = obj.matrix_world + V = Vector + nv = max(3, nv) + + # construction + v1, v2, v3, v4 = V(pts[0]), V(pts[1]), V(pts[1]), V(pts[2]) + edge1_mid = v1.lerp(v2, 0.5) + edge2_mid = v3.lerp(v4, 0.5) + axis = geometry.normal(v1, v2, v4) + mat_rot = mathutils.Matrix.Rotation(math.radians(90.0), 4, axis) + + # triangle edges + v1_ = ((v1 - edge1_mid) * mat_rot) + edge1_mid + v2_ = ((v2 - edge1_mid) * mat_rot) + edge1_mid + v3_ = ((v3 - edge2_mid) * mat_rot) + edge2_mid + v4_ = ((v4 - edge2_mid) * mat_rot) + edge2_mid + + r = geometry.intersect_line_line(v1_, v2_, v3_, v4_) + if r: + p1, _ = r + cp = mw * p1 + bpy.context.scene.cursor_location = cp + + if mode == 0: + pass + + elif mode == 1: + generate_bmesh_repr(p1, v1, axis, nv) + + else: + print('not on a circle') + + +def get_three_verts_from_selection(obj): + me = obj.data + bm = bmesh.from_edit_mesh(me) + + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + + return [v.co[:] for v in bm.verts if v.select] + + +def dispatch(context, mode=0): + try: + obj = context.edit_object + pts = get_three_verts_from_selection(obj) + props = context.scene.tinycad_props + generate_3PT(pts, obj, props.num_verts, mode) + except: + print('dispatch failed', mode) + + +class TCCallBackCCEN(bpy.types.Operator): + bl_idname = 'tinycad.reset_circlescale' + bl_label = 'CCEN circle reset' + bl_options = {'REGISTER'} + + def execute(self, context): + context.scene.tinycad_props.rescale = 1 + return {'FINISHED'} + + +class TCCircleCenter(bpy.types.Operator): + '''Recreate a Circle from 3 selected verts, move 3dcursor its center''' + + bl_idname = 'tinycad.circlecenter' + bl_label = 'CCEN circle center from selected' + bl_options = {'REGISTER', 'UNDO'} + + def draw(self, context): + scn = context.scene + l = self.layout + col = l.column() + + col.prop(scn.tinycad_props, 'num_verts', text='num verts') + row = col.row(align=True) + row.prop(scn.tinycad_props, 'rescale', text='rescale') + row.operator('tinycad.reset_circlescale', text="", icon="LINK") + + @classmethod + def poll(cls, context): + obj = context.edit_object + return obj is not None and obj.type == 'MESH' + + def execute(self, context): + dispatch(context, mode=1) + return {'FINISHED'} + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) diff --git a/mesh_tiny_cad/CFG.py b/mesh_tiny_cad/CFG.py new file mode 100644 index 00000000..ed703a2f --- /dev/null +++ b/mesh_tiny_cad/CFG.py @@ -0,0 +1,84 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + + +import os +import bpy + +ICONS = 'BIX CCEN V2X VTX XALL E2F'.split(' ') +icon_collection = {} + + +class TinyCADProperties(bpy.types.PropertyGroup): + + num_verts = bpy.props.IntProperty( + min=3, max=60, default=12) + + rescale = bpy.props.FloatProperty( + default=1.0, + precision=4, + min=0.0001) + + +class VIEW3D_MT_edit_mesh_tinycad(bpy.types.Menu): + bl_label = "TinyCAD" + + @classmethod + def poll(cls, context): + return bool(context.object) + + def draw(self, context): + + pcoll = icon_collection["main"] + + def cicon(name): + return pcoll[name].icon_id + + op = self.layout.operator + op('tinycad.autovtx', text='VTX | AUTO', icon_value=cicon('VTX')) + op('tinycad.vertintersect', text='V2X | Vertex at intersection', icon_value=cicon('V2X')) + op('tinycad.intersectall', text='XALL | Intersect selected edges', icon_value=cicon('XALL')) + op('tinycad.linetobisect', text='BIX | Bisector of 2 planar edges', icon_value=cicon('BIX')) + op('tinycad.circlecenter', text='CCEN | Resurrect circle center', icon_value=cicon('CCEN')) + op('tinycad.edge_to_face', text='E2F | Extend Edge to Face', icon_value=cicon('E2F')) + + +def register_icons(): + import bpy.utils.previews + pcoll = bpy.utils.previews.new() + icons_dir = os.path.join(os.path.dirname(__file__), "icons") + for icon_name in ICONS: + pcoll.load(icon_name, os.path.join(icons_dir, icon_name + '.png'), 'IMAGE') + + icon_collection["main"] = pcoll + + +def unregister_icons(): + for pcoll in icon_collection.values(): + bpy.utils.previews.remove(pcoll) + icon_collection.clear() + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) diff --git a/mesh_tiny_cad/E2F.py b/mesh_tiny_cad/E2F.py new file mode 100644 index 00000000..984d5035 --- /dev/null +++ b/mesh_tiny_cad/E2F.py @@ -0,0 +1,95 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + + +import bpy +import bmesh +from mathutils.geometry import intersect_line_plane + + +def failure_message(self): + self.report({"WARNING"}, 'select 1 face and 1 detached edge') + + +def extend_vertex(self): + + obj = bpy.context.edit_object + me = obj.data + bm = bmesh.from_edit_mesh(me) + verts = bm.verts + faces = bm.faces + + planes = [f for f in faces if f.select] + if (len(planes) > 1) or (len(planes) == 0): + failure_message(self) + return + + plane = planes[0] + plane_vert_indices = [v for v in plane.verts[:]] + all_selected_vert_indices = [v for v in verts if v.select] + + M = set(plane_vert_indices) + N = set(all_selected_vert_indices) + O = N.difference(M) + O = list(O) + + if not len(O) == 2: + failure_message(self) + return + + (v1_ref, v1), (v2_ref, v2) = [(i, i.co) for i in O] + + plane_co = plane.calc_center_median() + plane_no = plane.normal + + new_co = intersect_line_plane(v1, v2, plane_co, plane_no, False) + new_vertex = verts.new(new_co) + + A_len = (v1 - new_co).length + B_len = (v2 - new_co).length + + vertex_reference = v1_ref if (A_len < B_len) else v2_ref + bm.edges.new([vertex_reference, new_vertex]) + + bmesh.update_edit_mesh(me, True) + + +class TCEdgeToFace(bpy.types.Operator): + '''Extend selected edge towards projected intersection with a selected face''' + bl_idname = 'tinycad.edge_to_face' + bl_label = 'E2F edge to face' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + ob = context.object + return all([bool(ob), ob.type == 'MESH', ob.mode == 'EDIT']) + + def execute(self, context): + extend_vertex(self) + return {'FINISHED'} + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) diff --git a/mesh_tiny_cad/README.md b/mesh_tiny_cad/README.md new file mode 100644 index 00000000..ec37658e --- /dev/null +++ b/mesh_tiny_cad/README.md @@ -0,0 +1,91 @@ +Blender CAD utils +================= + +A tiny subset of unmissable CAD functions for Blender 3d. +Addon [page on blender.org/wiki](http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Modeling/mesh_tinyCAD) (Which has most of the same info] + +### Installation + +Download the [`installable stable release zip` here](https://github.com/zeffii/mesh_tinyCAD/archive/v1_2_4.zip) + + +__________________ + + +### OK, what's this all about? + +Dedicated CAD software speeds up drafting significantly with functions like: `Extend`, `Trim`, `Intersect`, `Fillet /w radius` and `Offset /w distance`. At the moment of this writing many of these functions aren't included by default in regular distributions on Blender.org, so i've coded scripts to perform a few of the main features that I missed most. + +My scripts have shortnames: `VTX, V2X, XALL, BIX, CCEN` and are described separately in sections below. `Fillet` and `Offset` are written by zmj100 and can be found [here](http://blenderartists.org/forum/showthread.php?179375). + + +Since I started this repository: Vertex Fillet / Bevel was added to master. So no more need for a separate addon. (Ctrl+Shift+b) + +### VTX + +The VTX script has lived in contrib distributions of Blender since 2010, with relatively minor changes. The feedback from BlenderArtists has been [overwhelmingly positive](http://blenderartists.org/forum/showthread.php?204836-CAD-Addon-Edge-Tools-(blender-2-6x)). I'm not going to claim it's bug free, but finding any showstopping issues has proven difficult. It now performs V, T or X selection automatically. + +Expect full freedom of orientation, but stuff must really intersect within error margins (`1.5E-6` = tolerance). These kinds of functions are handy for drawing construction lines and fixing up geometry. + + - V : extending two edges towards their _calculated_ intersection point. + ![V](http://i.imgur.com/zBSciFf.png) + + - T : extending the path of one edge towards another edge. + ![T](http://i.imgur.com/CDH5oHm.png) + + - X : two edges intersect, their intersection gets a weld vertex. You now have 4 edges and 5 vertices. + ![X](http://i.imgur.com/kqtX9OE.png) + + +- Select two edges +- hit `Spacebar` and type `vtx` ..select `autoVTX` +- Bam. the rest is taken care of. + + +### X ALL + +Intersect all, it programatically goes through all selected edges and slices them all using any found intersections, then welds them. + + - XALL is fast! + ![Imgur](http://i.imgur.com/1I7totI.gif) + - Select as many edges as you want to intersect. + - hit `spacebar` and type `xa` ..select `XALL intersect all edges` + +### V2X (Vertex to Intersection) + +This might be a niche accessory, but sometimes all you want is a vertex positioned on the intersection of two edges. Nothing fancy. + +### BIX (generate Bisector) + +Creates a single edge which is the bisect of two edges. +![Imgur](http://i.imgur.com/uzyv1Mv.gif) + +### CCEN (Circle Centers) + +Given either + +- two adjacent edges on the circumference of an incomplete circle +- or three vertices (not required to be adjacent) + +this operator will places the 3d cursor at original center of that circle. + +![imgur](https://cloud.githubusercontent.com/assets/619340/5595657/2786f984-9279-11e4-9dff-9db5d5a52a52.gif) + +updated version may become a modal operator to generate a full set of circle vertices, with variable vertex count. + +![imgur demo](https://cloud.githubusercontent.com/assets/619340/5602194/ce613c96-933d-11e4-9879-d2cfc686cb69.gif) + +### E2F (Extend Edge to Selected Face, Edge 2 Face) + +Select a single Edge and a single Polygon (ngon, tri, quad) within the same Object. Execute `W > TinyCAD > E2F` + +![image](https://cloud.githubusercontent.com/assets/619340/12091278/2884820e-b2f6-11e5-9f1b-37ebfdf10cfc.png) + + +### Why on github? + +The issue tracker, use it. + +- Let me know if these things are broken in new releases. Why? I don't update Blender as often as some so am oblivious to the slow evolution. +- If you can make a valid argument for extra functionality and it seems like something I might use or be able to implement for fun, it's going to happen. +- I'm always open to pull requests (just don't expect instant approval of something massive, we can talk..you can use your gift of persuasion and sharp objectivism) diff --git a/mesh_tiny_cad/V2X.py b/mesh_tiny_cad/V2X.py new file mode 100644 index 00000000..69eaeb8b --- /dev/null +++ b/mesh_tiny_cad/V2X.py @@ -0,0 +1,72 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + + +import bpy +import bmesh +from mathutils import geometry + + +def add_vertex_to_intersection(): + + obj = bpy.context.object + me = obj.data + bm = bmesh.from_edit_mesh(me) + + edges = [e for e in bm.edges if e.select] + + if len(edges) == 2: + [[v1, v2], [v3, v4]] = [[v.co for v in e.verts] for e in edges] + + iv = geometry.intersect_line_line(v1, v2, v3, v4) + if iv: + iv = (iv[0] + iv[1]) / 2 + bm.verts.new(iv) + + # precaution? + if hasattr(bm.verts, "ensure_lookup_table"): + bm.verts.ensure_lookup_table() + + bm.verts[-1].select = True + bmesh.update_edit_mesh(me) + + +class TCVert2Intersection(bpy.types.Operator): + '''Add a vertex at the intersection (projected or real) of two selected edges''' + bl_idname = 'tinycad.vertintersect' + bl_label = 'V2X vertex to intersection' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + obj = context.active_object + return obj is not None and obj.type == 'MESH' and obj.mode == 'EDIT' + + def execute(self, context): + add_vertex_to_intersection() + return {'FINISHED'} + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) diff --git a/mesh_tiny_cad/VTX.py b/mesh_tiny_cad/VTX.py new file mode 100644 index 00000000..8a8c4e8d --- /dev/null +++ b/mesh_tiny_cad/VTX.py @@ -0,0 +1,183 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + + +import bpy +import bmesh +import sys + +from . import cad_module as cm + +messages = { + 'SHARED_VERTEX': 'Shared Vertex, no intersection possible', + 'PARALLEL_EDGES': 'Edges Parallel, no intersection possible', + 'NON_PLANAR_EDGES': 'Non Planar Edges, no clean intersection point' +} + + +def add_edges(bm, pt, idxs, fdp): + ''' + this function is a disaster -- + index updates and ensure_lookup_table() are called before this function + and after, and i've tried doing this less verbose but results tend to be + less predictable. I'm obviously a terrible coder, but can only spend so + much time figuring out this stuff. + ''' + + v1 = bm.verts.new(pt) + + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.verts.index_update() + + try: + for e in idxs: + bm.edges.index_update() + v2 = bm.verts[e] + bm.edges.new((v1, v2)) + + bm.edges.index_update() + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + + except Exception as err: + print('some failure: details') + for l in fdp: + print(l) + + sys.stderr.write('ERROR: %s\n' % str(err)) + print(sys.exc_info()[-1].tb_frame.f_code) + print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno)) + + +def remove_earmarked_edges(bm, earmarked): + edges_select = [e for e in bm.edges if e.index in earmarked] + bmesh.ops.delete(bm, geom=edges_select, context=2) + + +def perform_vtx(bm, pt, edges, pts, vertex_indices): + idx1, idx2 = edges[0].index, edges[1].index + fdp = pt, edges, pts, vertex_indices + + # this list will hold those edges that pt lies on + edges_indices = cm.find_intersecting_edges(bm, pt, idx1, idx2) + mode = 'VTX'[len(edges_indices)] + + if mode == 'V': + cl_vert1 = cm.closest_idx(pt, edges[0]) + cl_vert2 = cm.closest_idx(pt, edges[1]) + add_edges(bm, pt, [cl_vert1, cl_vert2], fdp) + + elif mode == 'T': + to_edge_idx = edges_indices[0] + from_edge_idx = idx1 if to_edge_idx == idx2 else idx2 + + cl_vert = cm.closest_idx(pt, bm.edges[from_edge_idx]) + to_vert1, to_vert2 = cm.vert_idxs_from_edge_idx(bm, to_edge_idx) + add_edges(bm, pt, [cl_vert, to_vert1, to_vert2], fdp) + + elif mode == 'X': + add_edges(bm, pt, vertex_indices, fdp) + + # final refresh before returning to user. + if edges_indices: + remove_earmarked_edges(bm, edges_indices) + + bm.edges.index_update() + return bm + + +def do_vtx_if_appropriate(bm, edges): + vertex_indices = cm.get_vert_indices_from_bmedges(edges) + + # test 1, are there shared vers? if so return non-viable + if not len(set(vertex_indices)) == 4: + return {'SHARED_VERTEX'} + + # test 2, is parallel? + p1, p2, p3, p4 = [bm.verts[i].co for i in vertex_indices] + point = cm.get_intersection([p1, p2], [p3, p4]) + if not point: + return {'PARALLEL_EDGES'} + + # test 3, coplanar edges? + coplanar = cm.test_coplanar([p1, p2], [p3, p4]) + if not coplanar: + return {'NON_PLANAR_EDGES'} + + # point must lie on an edge or the virtual extention of an edge + bm = perform_vtx(bm, point, edges, (p1, p2, p3, p4), vertex_indices) + return bm + + +class TCAutoVTX(bpy.types.Operator): + '''Weld intersecting edges, project converging edges towards their intersection''' + bl_idname = 'tinycad.autovtx' + bl_label = 'VTX autoVTX' + + @classmethod + def poll(cls, context): + obj = context.active_object + return bool(obj) and obj.type == 'MESH' + + def cancel_message(self, msg): + print(msg) + self.report({"WARNING"}, msg) + return {'CANCELLED'} + + def execute(self, context): + + # final attempt to enter unfragmented bm/mesh + # ghastly, but what can I do? it works with these + # fails without. + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') + + obj = context.active_object + me = obj.data + + bm = bmesh.from_edit_mesh(me) + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + + edges = [e for e in bm.edges if e.select and not e.hide] + + if len(edges) == 2: + message = do_vtx_if_appropriate(bm, edges) + if isinstance(message, set): + msg = messages.get(message.pop()) + return self.cancel_message(msg) + bm = message + else: + return self.cancel_message('select two edges!') + + bm.verts.index_update() + bm.edges.index_update() + bmesh.update_edit_mesh(me, True) + + return {'FINISHED'} + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) diff --git a/mesh_tiny_cad/XALL.py b/mesh_tiny_cad/XALL.py new file mode 100644 index 00000000..c1605393 --- /dev/null +++ b/mesh_tiny_cad/XALL.py @@ -0,0 +1,189 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + + +import bpy +import bmesh +from mathutils.geometry import intersect_line_line as LineIntersect + +import itertools +from collections import defaultdict +from . import cad_module as cm + + +def order_points(edge, point_list): + ''' order these edges from distance to v1, then + sandwich the sorted list with v1, v2 ''' + v1, v2 = edge + def dist(co): + return (v1 - co).length + point_list = sorted(point_list, key=dist) + return [v1] + point_list + [v2] + + +def remove_permutations_that_share_a_vertex(bm, permutations): + ''' Get useful Permutations ''' + final_permutations = [] + for edges in permutations: + raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges) + if cm.duplicates(raw_vert_indices): + continue + + # reaches this point if they do not share. + final_permutations.append(edges) + + return final_permutations + + +def get_valid_permutations(bm, edge_indices): + raw_permutations = itertools.permutations(edge_indices, 2) + permutations = [r for r in raw_permutations if r[0] < r[1]] + return remove_permutations_that_share_a_vertex(bm, permutations) + + +def can_skip(closest_points, vert_vectors): + '''this checks if the intersection lies on both edges, returns True + when criteria are not met, and thus this point can be skipped''' + if not closest_points: + return True + if not isinstance(closest_points[0].x, float): + return True + if cm.num_edges_point_lies_on(closest_points[0], vert_vectors) < 2: + return True + + # if this distance is larger than than VTX_PRECISION, we can skip it. + cpa, cpb = closest_points + return (cpa - cpb).length > cm.CAD_prefs.VTX_PRECISION + + +def get_intersection_dictionary(bm, edge_indices): + + if hasattr(bm.verts, "ensure_lookup_table"): + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + + permutations = get_valid_permutations(bm, edge_indices) + + k = defaultdict(list) + d = defaultdict(list) + + for edges in permutations: + raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges) + vert_vectors = cm.vectors_from_indices(bm, raw_vert_indices) + + points = LineIntersect(*vert_vectors) + + # some can be skipped. (NaN, None, not on both edges) + if can_skip(points, vert_vectors): + continue + + # reaches this point only when an intersection happens on both edges. + [k[edge].append(points[0]) for edge in edges] + + # k will contain a dict of edge indices and points found on those edges. + for edge_idx, unordered_points in k.items(): + tv1, tv2 = bm.edges[edge_idx].verts + v1 = bm.verts[tv1.index].co + v2 = bm.verts[tv2.index].co + ordered_points = order_points((v1, v2), unordered_points) + d[edge_idx].extend(ordered_points) + + return d + + +def update_mesh(obj, d): + ''' Make new geometry (delete old first) ''' + + bpy.ops.mesh.delete(type='EDGE') + bpy.ops.object.editmode_toggle() + + oe = obj.data.edges + ov = obj.data.vertices + vert_count = len(ov) + edge_count = len(oe) + + for old_edge, point_list in d.items(): + num_points = len(point_list) + num_edges_to_add = num_points - 1 + + for i in range(num_edges_to_add): + oe.add(1) + ov.add(2) + + ov[vert_count].co = point_list[i] + ov[vert_count + 1].co = point_list[i + 1] + + oe[edge_count].vertices = [vert_count, vert_count + 1] + vert_count = len(ov) + edge_count = len(oe) + + # set edit mode + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.remove_doubles( + threshold=cm.CAD_prefs.VTX_DOUBLES_THRSHLD, + use_unselected=False) + + +def unselect_nonintersecting(bm, d_edges, edge_indices): + if len(edge_indices) > len(d_edges): + reserved_edges = set(edge_indices) - set(d_edges) + for edge in reserved_edges: + bm.edges[edge].select = False + print("unselected {}, non intersecting edges".format(reserved_edges)) + + +class TCIntersectAllEdges(bpy.types.Operator): + '''Adds a vertex at the intersections of all selected edges''' + bl_idname = 'tinycad.intersectall' + bl_label = 'XALL intersect all edges' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + obj = context.active_object + return obj is not None and obj.type == 'MESH' and obj.mode == 'EDIT' + + def execute(self, context): + # must force edge selection mode here + bpy.context.tool_settings.mesh_select_mode = (False, True, False) + + obj = context.active_object + if obj.mode == "EDIT": + bm = bmesh.from_edit_mesh(obj.data) + + selected_edges = [edge for edge in bm.edges if edge.select] + edge_indices = [i.index for i in selected_edges] + + d = get_intersection_dictionary(bm, edge_indices) + + unselect_nonintersecting(bm, d.keys(), edge_indices) + update_mesh(obj, d) + else: + print('must be in edit mode') + + return {'FINISHED'} + + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) diff --git a/mesh_tiny_cad/__init__.py b/mesh_tiny_cad/__init__.py new file mode 100644 index 00000000..4c8e7100 --- /dev/null +++ b/mesh_tiny_cad/__init__.py @@ -0,0 +1,75 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + + +bl_info = { + "name": "tinyCAD Mesh tools", + "author": "zeffii (aka Dealga McArdle)", + "version": (1, 3, 0), + "blender": (2, 7, 7), + "category": "Mesh", + "location": "View3D > EditMode > (w) Specials", + "wiki_url": "http://zeffii.github.io/mesh_tiny_cad/", + "tracker_url": "https://github.com/zeffii/mesh_tiny_cad/issues" +} + + +if "bpy" in locals(): + if 'VTX' in locals(): + + print('tinyCAD: detected reload event.') + import importlib + + try: + modules = (CFG, VTX, V2X, XALL, BIX, CCEN, E2F) + for m in modules: + importlib.reload(m) + print("tinyCAD: reloaded modules, all systems operational") + + except Exception as E: + print('reload failed with error:') + print(E) + + +import bpy + +from .CFG import TinyCADProperties +from .CFG import register_icons, unregister_icons +from . import VTX, V2X, XALL, BIX, CCEN, E2F + + +def menu_func(self, context): + self.layout.menu("VIEW3D_MT_edit_mesh_tinycad") + self.layout.separator() + + +def register(): + register_icons() + bpy.utils.register_module(__name__) + bpy.types.Scene.tinycad_props = bpy.props.PointerProperty( + name="TinyCAD props", type=TinyCADProperties) + bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func) + + +def unregister(): + bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func) + bpy.utils.unregister_module(__name__) + del bpy.types.Scene.tinycad_props + unregister_icons() diff --git a/mesh_tiny_cad/cad_module.py b/mesh_tiny_cad/cad_module.py new file mode 100644 index 00000000..575a6896 --- /dev/null +++ b/mesh_tiny_cad/cad_module.py @@ -0,0 +1,172 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + + +import bmesh + +from mathutils import Vector, geometry +from mathutils.geometry import intersect_line_line as LineIntersect +from mathutils.geometry import intersect_point_line as PtLineIntersect + + +class CAD_prefs: + VTX_PRECISION = 1.0e-5 + VTX_DOUBLES_THRSHLD = 0.0001 + + +def point_on_edge(p, edge): + ''' + > p: vector + > edge: tuple of 2 vectors + < returns: True / False if a point happens to lie on an edge + ''' + pt, _percent = PtLineIntersect(p, *edge) + on_line = (pt - p).length < CAD_prefs.VTX_PRECISION + return on_line and (0.0 <= _percent <= 1.0) + + +def line_from_edge_intersect(edge1, edge2): + ''' + > takes 2 tuples, each tuple contains 2 vectors + - prepares input for sending to intersect_line_line + < returns output of intersect_line_line + ''' + [p1, p2], [p3, p4] = edge1, edge2 + return LineIntersect(p1, p2, p3, p4) + + +def get_intersection(edge1, edge2): + ''' + > takes 2 tuples, each tuple contains 2 vectors + < returns the point halfway on line. See intersect_line_line + ''' + line = line_from_edge_intersect(edge1, edge2) + if line: + return (line[0] + line[1]) / 2 + + +def test_coplanar(edge1, edge2): + ''' + the line that describes the shortest line between the two edges + would be short if the lines intersect mathematically. If this + line is longer than the VTX_PRECISION then they are either + coplanar or parallel. + ''' + line = line_from_edge_intersect(edge1, edge2) + if line: + return (line[0] - line[1]).length < CAD_prefs.VTX_PRECISION + + +def closest_idx(pt, e): + ''' + > pt: vector + > e: bmesh edge + < returns: returns index of vertex closest to pt. + + if both points in e are equally far from pt, then v1 is returned. + ''' + if isinstance(e, bmesh.types.BMEdge): + ev = e.verts + v1 = ev[0].co + v2 = ev[1].co + distance_test = (v1 - pt).length <= (v2 - pt).length + return ev[0].index if distance_test else ev[1].index + + print("received {0}, check expected input in docstring ".format(e)) + + +def closest_vector(pt, e): + ''' + > pt: vector + > e: 2 vector tuple + < returns: + pt, 2 vector tuple: returns closest vector to pt + + if both points in e are equally far from pt, then v1 is returned. + ''' + if isinstance(e, tuple) and all([isinstance(co, Vector) for co in e]): + v1, v2 = e + distance_test = (v1 - pt).length <= (v2 - pt).length + return v1 if distance_test else v2 + + print("received {0}, check expected input in docstring ".format(e)) + + +def coords_tuple_from_edge_idx(bm, idx): + ''' bm is a bmesh representation ''' + return tuple(v.co for v in bm.edges[idx].verts) + + +def vectors_from_indices(bm, raw_vert_indices): + ''' bm is a bmesh representation ''' + return [bm.verts[i].co for i in raw_vert_indices] + + +def vertex_indices_from_edges_tuple(bm, edge_tuple): + ''' + > bm: is a bmesh representation + > edge_tuple: contains two edge indices. + < returns the vertex indices of edge_tuple + ''' + def k(v, w): + return bm.edges[edge_tuple[v]].verts[w].index + + return [k(i >> 1, i % 2) for i in range(4)] + + +def get_vert_indices_from_bmedges(edges): + ''' + > bmedges: a list of two bm edges + < returns the vertex indices of edge_tuple as a flat list. + ''' + temp_edges = [] + print(edges) + for e in edges: + for v in e.verts: + temp_edges.append(v.index) + return temp_edges + + +def num_edges_point_lies_on(pt, edges): + ''' returns the number of edges that a point lies on. ''' + res = [point_on_edge(pt, edge) for edge in [edges[:2], edges[2:]]] + return len([i for i in res if i]) + + +def find_intersecting_edges(bm, pt, idx1, idx2): + ''' + > pt: Vector + > idx1, ix2: edge indices + < returns the list of edge indices where pt is on those edges + ''' + if not pt: + return [] + idxs = [idx1, idx2] + edges = [coords_tuple_from_edge_idx(bm, idx) for idx in idxs] + return [idx for edge, idx in zip(edges, idxs) if point_on_edge(pt, edge)] + + +def duplicates(indices): + return len(set(indices)) < 4 + + +def vert_idxs_from_edge_idx(bm, idx): + edge = bm.edges[idx] + return edge.verts[0].index, edge.verts[1].index diff --git a/mesh_tiny_cad/icons/BIX.png b/mesh_tiny_cad/icons/BIX.png Binary files differnew file mode 100644 index 00000000..73bcfcd8 --- /dev/null +++ b/mesh_tiny_cad/icons/BIX.png diff --git a/mesh_tiny_cad/icons/CCEN.png b/mesh_tiny_cad/icons/CCEN.png Binary files differnew file mode 100644 index 00000000..51d426e4 --- /dev/null +++ b/mesh_tiny_cad/icons/CCEN.png diff --git a/mesh_tiny_cad/icons/E2F.png b/mesh_tiny_cad/icons/E2F.png Binary files differnew file mode 100644 index 00000000..e4006f8e --- /dev/null +++ b/mesh_tiny_cad/icons/E2F.png diff --git a/mesh_tiny_cad/icons/V2X.png b/mesh_tiny_cad/icons/V2X.png Binary files differnew file mode 100644 index 00000000..21d11679 --- /dev/null +++ b/mesh_tiny_cad/icons/V2X.png diff --git a/mesh_tiny_cad/icons/VTX.png b/mesh_tiny_cad/icons/VTX.png Binary files differnew file mode 100644 index 00000000..6e08247e --- /dev/null +++ b/mesh_tiny_cad/icons/VTX.png diff --git a/mesh_tiny_cad/icons/XALL.png b/mesh_tiny_cad/icons/XALL.png Binary files differnew file mode 100644 index 00000000..534a977e --- /dev/null +++ b/mesh_tiny_cad/icons/XALL.png |