move to release

This commit is contained in:
Dealga McArdle 2016-07-31 23:23:44 +02:00
parent 96a957faf3
commit 3ce7865694
16 changed files with 1231 additions and 0 deletions

103
mesh_tiny_cad/BIX.py Normal file
View File

@ -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__)

167
mesh_tiny_cad/CCEN.py Normal file
View File

@ -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__)

84
mesh_tiny_cad/CFG.py Normal file
View File

@ -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__)

95
mesh_tiny_cad/E2F.py Normal file
View File

@ -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__)

91
mesh_tiny_cad/README.md Normal file
View File

@ -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)

72
mesh_tiny_cad/V2X.py Normal file
View File

@ -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__)

183
mesh_tiny_cad/VTX.py Normal file
View File

@ -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__)

189
mesh_tiny_cad/XALL.py Normal file
View File

@ -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__)

75
mesh_tiny_cad/__init__.py Normal file
View File

@ -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()

172
mesh_tiny_cad/cad_module.py Normal file
View File

@ -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

BIN
mesh_tiny_cad/icons/BIX.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
mesh_tiny_cad/icons/E2F.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
mesh_tiny_cad/icons/V2X.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
mesh_tiny_cad/icons/VTX.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB