From a4b4c07685dd2bd2aba8892b5f1fa90d30b0ed7a Mon Sep 17 00:00:00 2001 From: Luca Bonavita Date: Wed, 21 Jul 2010 11:26:03 +0000 Subject: == blender 2.53 release == - tagging trunk/ and contrib/ os we know what worked at 2.53 release time - only tags/2_53_release should be used for blender release of course [[Split portion of a mixed commit.]] --- add_curve_aceous_galore.py | 1156 +++++++++++++++++++ add_curve_torus_knots.py | 363 ++++++ add_mesh_3d_function_surface.py | 648 +++++++++++ add_mesh_extras.py | 791 +++++++++++++ add_mesh_gears.py | 949 ++++++++++++++++ add_mesh_gemstones.py | 480 ++++++++ add_mesh_pipe_joint.py | 1300 +++++++++++++++++++++ add_mesh_solid.py | 913 +++++++++++++++ add_mesh_twisted_torus.py | 366 ++++++ curve_simplify.py | 597 ++++++++++ io_anim_camera.py | 159 +++ io_export_directx_x.py | 1077 ++++++++++++++++++ io_export_unreal_psk_psa.py | 1603 ++++++++++++++++++++++++++ io_import_images_as_planes.py | 528 +++++++++ io_import_scene_mhx.py | 2246 +++++++++++++++++++++++++++++++++++++ io_import_scene_unreal_psk.py | 595 ++++++++++ io_mesh_raw/__init__.py | 63 ++ io_mesh_raw/export_raw.py | 112 ++ io_mesh_raw/import_raw.py | 139 +++ io_mesh_stl/__init__.py | 152 +++ io_mesh_stl/blender_utils.py | 50 + io_mesh_stl/stl_utils.py | 228 ++++ mesh_relax.py | 128 +++ mesh_surface_sketch.py | 827 ++++++++++++++ object_add_chain.py | 156 +++ object_cloud_gen.py | 679 +++++++++++ object_fracture/__init__.py | 86 ++ object_fracture/data.blend | Bin 0 -> 232271 bytes object_fracture/fracture_ops.py | 492 ++++++++ object_fracture/fracture_setup.py | 74 ++ render_renderfarmfi.py | 799 +++++++++++++ space_view3d_align_tools.py | 361 ++++++ space_view3d_materials_utils.py | 704 ++++++++++++ space_view3d_panel_measure.py | 989 ++++++++++++++++ space_view3d_property_chart.py | 232 ++++ space_view3d_spacebar_menu.py | 1501 +++++++++++++++++++++++++ 36 files changed, 21543 insertions(+) create mode 100644 add_curve_aceous_galore.py create mode 100644 add_curve_torus_knots.py create mode 100644 add_mesh_3d_function_surface.py create mode 100644 add_mesh_extras.py create mode 100644 add_mesh_gears.py create mode 100644 add_mesh_gemstones.py create mode 100644 add_mesh_pipe_joint.py create mode 100644 add_mesh_solid.py create mode 100644 add_mesh_twisted_torus.py create mode 100644 curve_simplify.py create mode 100644 io_anim_camera.py create mode 100644 io_export_directx_x.py create mode 100644 io_export_unreal_psk_psa.py create mode 100644 io_import_images_as_planes.py create mode 100644 io_import_scene_mhx.py create mode 100644 io_import_scene_unreal_psk.py create mode 100644 io_mesh_raw/__init__.py create mode 100644 io_mesh_raw/export_raw.py create mode 100644 io_mesh_raw/import_raw.py create mode 100644 io_mesh_stl/__init__.py create mode 100644 io_mesh_stl/blender_utils.py create mode 100644 io_mesh_stl/stl_utils.py create mode 100644 mesh_relax.py create mode 100644 mesh_surface_sketch.py create mode 100644 object_add_chain.py create mode 100644 object_cloud_gen.py create mode 100644 object_fracture/__init__.py create mode 100644 object_fracture/data.blend create mode 100644 object_fracture/fracture_ops.py create mode 100644 object_fracture/fracture_setup.py create mode 100644 render_renderfarmfi.py create mode 100644 space_view3d_align_tools.py create mode 100644 space_view3d_materials_utils.py create mode 100644 space_view3d_panel_measure.py create mode 100644 space_view3d_property_chart.py create mode 100644 space_view3d_spacebar_menu.py diff --git a/add_curve_aceous_galore.py b/add_curve_aceous_galore.py new file mode 100644 index 00000000..0b546d5d --- /dev/null +++ b/add_curve_aceous_galore.py @@ -0,0 +1,1156 @@ +# ##### 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 ##### + + +bl_addon_info = { + 'name': 'Add Curve: Curveaceous Galore!', + 'author': 'Jimmy Hazevoet, testscreenings', + 'version': '0.1', + 'blender': (2, 5, 3), + 'location': 'View3D > Add > Curve', + 'description': 'Adds many types of curves', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Curve/Curves_Galore', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=22404&group_id=153&atid=469', + 'category': 'Add Curve'} + + +##------------------------------------------------------------ +#### import modules +import bpy +from bpy.props import * +from mathutils import * +from math import * +###------------------------------------------------------------ +#### Some functions to use with others: +###------------------------------------------------------------ +''' +#------------------------------------------------------------ +# Generate random number: +def randnum( low=0.0, high=1.0, seed=0 ): + """ + randnum( low=0.0, high=1.0, seed=0 ) + + Create random number + + Parameters: + low - lower range + (type=float) + high - higher range + (type=float) + seed - the random seed number, if seed is 0, the current time will be used instead + (type=int) + Returns: + a random number + (type=float) + """ + + s = Noise.setRandomSeed( seed ) + rnum = Noise.random() + rnum = rnum*(high-low) + rnum = rnum+low + return rnum + + + +#------------------------------------------------------------ +# Make some noise: +def vTurbNoise((x,y,z), iScale=0.25, Size=1.0, Depth=6, Hard=0, Basis=0, Seed=0 ): + """ + vTurbNoise((x,y,z), iScale=0.25, Size=1.0, Depth=6, Hard=0, Basis=0, Seed=0 ) + + Create randomised vTurbulence noise + + Parameters: + xyz - (x,y,z) float values. + (type=3-float tuple) + iScale - noise intensity scale + (type=float) + Size - noise size + (type=float) + Depth - number of noise values added. + (type=int) + Hard - noise hardness: 0 - soft noise; 1 - hard noise + (type=int) + basis - type of noise used for turbulence + (type=int) + Seed - the random seed number, if seed is 0, the current time will be used instead + (type=int) + Returns: + the generated turbulence vector. + (type=3-float list) + """ + + rand = randnum(-100,100,Seed) + if Basis ==9: Basis = 14 + vTurb = Noise.vTurbulence(( x/Size+rand, y/Size+rand, z/Size+rand ), Depth, Hard, Basis ) + tx = vTurb[0]*iScale + ty = vTurb[1]*iScale + tz = vTurb[2]*iScale + return tx,ty,tz + + + +#------------------------------------------------------------ +# Axis: ( used in 3DCurve Turbulence ) +def AxisFlip((x,y,z), x_axis=1, y_axis=1, z_axis=1, flip=0 ): + if flip != 0: + flip *= -1 + else: flip = 1 + x *= x_axis*flip + y *= y_axis*flip + z *= z_axis*flip + return x,y,z +''' + +###------------------------------------------------------------------- +#### 2D Curve shape functions: +###------------------------------------------------------------------- + +##------------------------------------------------------------ +# 2DCurve: Profile: L, H, T, U, Z +def ProfileCurve(type=0, a=0.25, b=0.25): + """ + ProfileCurve( type=0, a=0.25, b=0.25 ) + + Create profile curve + + Parameters: + type - select profile type, L, H, T, U, Z + (type=int) + a - a scaling parameter + (type=float) + b - b scaling parameter + (type=float) + Returns: + a list with lists of x,y,z coordinates for curve points, [[x,y,z],[x,y,z],...n] + (type=list) + """ + + newpoints = [] + if type ==1: + ## H: + a*=0.5 + b*=0.5 + newpoints = [ [ -1.0, 1.0, 0.0 ], [ -1.0+a, 1.0, 0.0 ], + [ -1.0+a, b, 0.0 ], [ 1.0-a, b, 0.0 ], [ 1.0-a, 1.0, 0.0 ], + [ 1.0, 1.0, 0.0 ], [ 1.0, -1.0, 0.0 ], [ 1.0-a, -1.0, 0.0 ], + [ 1.0-a, -b, 0.0 ], [ -1.0+a, -b, 0.0 ], [ -1.0+a, -1.0, 0.0 ], + [ -1.0, -1.0, 0.0 ] ] + elif type ==2: + ## T: + a*=0.5 + newpoints = [ [ -1.0, 1.0, 0.0 ], [ 1.0, 1.0, 0.0 ], + [ 1.0, 1.0-b, 0.0 ], [ a, 1.0-b, 0.0 ], [ a, -1.0, 0.0 ], + [ -a, -1.0, 0.0 ], [ -a, 1.0-b, 0.0 ], [ -1.0, 1.0-b, 0.0 ] ] + elif type ==3: + ## U: + a*=0.5 + newpoints = [ [ -1.0, 1.0, 0.0 ], [ -1.0+a, 1.0, 0.0 ], + [ -1.0+a, -1.0+b, 0.0 ], [ 1.0-a, -1.0+b, 0.0 ], [ 1.0-a, 1.0, 0.0 ], + [ 1.0, 1.0, 0.0 ], [ 1.0, -1.0, 0.0 ], [ -1.0, -1.0, 0.0 ] ] + elif type ==4: + ## Z: + a*=0.5 + newpoints = [ [ -0.5, 1.0, 0.0 ], [ a, 1.0, 0.0 ], + [ a, -1.0+b, 0.0 ], [ 1.0, -1.0+b, 0.0 ], [ 1.0, -1.0, 0.0 ], + [ -a, -1.0, 0.0 ], [ -a, 1.0-b, 0.0 ], [ -1.0, 1.0-b, 0.0 ], + [ -1.0, 1.0, 0.0 ] ] + else: + ## L: + newpoints = [ [ -1.0, 1.0, 0.0 ], [ -1.0+a, 1.0, 0.0 ], + [ -1.0+a, -1.0+b, 0.0 ], [ 1.0, -1.0+b, 0.0 ], + [ 1.0, -1.0, 0.0 ], [ -1.0, -1.0, 0.0 ] ] + return newpoints + +##------------------------------------------------------------ +# 2DCurve: Miscellaneous.: Diamond, Arrow1, Arrow2, Square, .... +def MiscCurve(type=1, a=1.0, b=0.5, c=90.0): + """ + MiscCurve( type=1, a=1.0, b=0.5, c=90.0 ) + + Create miscellaneous curves + + Parameters: + type - select type, Diamond, Arrow1, Arrow2, Square + (type=int) + a - a scaling parameter + (type=float) + b - b scaling parameter + (type=float) + c - c scaling parameter + (type=float) + doesn't seem to do anything + Returns: + a list with lists of x,y,z coordinates for curve points, [[x,y,z],[x,y,z],...n] + (type=list) + """ + + newpoints = [] + a*=0.5 + b*=0.5 + if type ==1: + ## diamond: + newpoints = [ [ 0.0, b, 0.0 ], [ a, 0.0, 0.0 ], [ 0.0, -b, 0.0 ], [ -a, 0.0, 0.0 ] ] + elif type ==2: + ## Arrow1: + newpoints = [ [ -a, b, 0.0 ], [ a, 0.0, 0.0 ], [ -a, -b, 0.0 ], [ 0.0, 0.0, 0.0 ] ] + elif type ==3: + ## Arrow2: + newpoints = [ [ -1.0, b, 0.0 ], [ -1.0+a, b, 0.0 ], + [ -1.0+a, 1.0, 0.0 ], [ 1.0, 0.0, 0.0 ], + [ -1.0+a, -1.0, 0.0 ], [ -1.0+a, -b, 0.0 ], + [ -1.0, -b, 0.0 ] ] + elif type ==4: + ## Rounded square: + newpoints = [ [ -a, b-b*0.2, 0.0 ], [ -a+a*0.05, b-b*0.05, 0.0 ], [ -a+a*0.2, b, 0.0 ], + [ a-a*0.2, b, 0.0 ], [ a-a*0.05, b-b*0.05, 0.0 ], [ a, b-b*0.2, 0.0 ], + [ a, -b+b*0.2, 0.0 ], [ a-a*0.05, -b+b*0.05, 0.0 ], [ a-a*0.2, -b, 0.0 ], + [ -a+a*0.2, -b, 0.0 ], [ -a+a*0.05, -b+b*0.05, 0.0 ], [ -a, -b+b*0.2, 0.0 ] ] + + #elif type ==15: + ## : + #newpoints = [ [ x,y,z ] ] + else: + ## Square: + newpoints = [ [ -a, b, 0.0 ], [ a, b, 0.0 ], [ a, -b, 0.0 ], [ -a, -b, 0.0 ] ] + return newpoints + +##------------------------------------------------------------ +# 2DCurve: Star: +def StarCurve(starpoints=8, innerradius=0.5, outerradius=1.0, twist=0.0): + """ + StarCurve( starpoints=8, innerradius=0.5, outerradius=1.0, twist=0.0 ) + + Create star shaped curve + + Parameters: + starpoints - the number of points + (type=int) + innerradius - innerradius + (type=float) + outerradius - outerradius + (type=float) + twist - twist amount + (type=float) + Returns: + a list with lists of x,y,z coordinates for curve points, [[x,y,z],[x,y,z],...n] + (type=list) + """ + + newpoints = [] + step = (2.0/(starpoints)) + i = 0 + while i < starpoints: + t = (i*step) + x1 = cos(t*pi)*outerradius + y1 = sin(t*pi)*outerradius + newpoints.append([x1,y1,0]) + x2 = cos(t*pi+(pi/starpoints+twist))*innerradius + y2 = sin(t*pi+(pi/starpoints+twist))*innerradius + newpoints.append([x2,y2,0]) + i+=1 + return newpoints + +##------------------------------------------------------------ +# 2DCurve: Flower: +def FlowerCurve(petals=8, innerradius=0.5, outerradius=1.0, petalwidth=2.0): + """ + FlowerCurve( petals=8, innerradius=0.5, outerradius=1.0, petalwidth=2.0 ) + + Create flower shaped curve + + Parameters: + petals - the number of petals + (type=int) + innerradius - innerradius + (type=float) + outerradius - outerradius + (type=float) + petalwidth - width of petals + (type=float) + Returns: + a list with lists of x,y,z coordinates for curve points, [[x,y,z],[x,y,z],...n] + (type=list) + """ + + newpoints = [] + step = (2.0/(petals)) + pet = (step/pi*2)*petalwidth + i = 0 + while i < petals: + t = (i*step) + x1 = cos(t*pi-(pi/petals))*innerradius + y1 = sin(t*pi-(pi/petals))*innerradius + newpoints.append([x1,y1,0]) + x2 = cos(t*pi-pet)*outerradius + y2 = sin(t*pi-pet)*outerradius + newpoints.append([x2,y2,0]) + x3 = cos(t*pi+pet)*outerradius + y3 = sin(t*pi+pet)*outerradius + newpoints.append([x3,y3,0]) + i+=1 + return newpoints + +##------------------------------------------------------------ +# 2DCurve: Arc,Sector,Segment,Ring: +def ArcCurve(sides=6, startangle=0.0, endangle=90.0, innerradius=0.5, outerradius=1.0, type=3): + """ + ArcCurve( sides=6, startangle=0.0, endangle=90.0, innerradius=0.5, outerradius=1.0, type=3 ) + + Create arc shaped curve + + Parameters: + sides - number of sides + (type=int) + startangle - startangle + (type=float) + endangle - endangle + (type=float) + innerradius - innerradius + (type=float) + outerradius - outerradius + (type=float) + type - select type Arc,Sector,Segment,Ring + (type=int) + Returns: + a list with lists of x,y,z coordinates for curve points, [[x,y,z],[x,y,z],...n] + (type=list) + """ + + newpoints = [] + sides += 1 + angle = (2.0*(1.0/360.0)) + endangle-=startangle + step = ((angle*endangle)/(sides-1)) + i = 0 + while i < sides: + t = (i*step) + angle*startangle + x1 = sin(t*pi)*outerradius + y1 = cos(t*pi)*outerradius + newpoints.append([x1,y1,0]) + i+=1 + + #if type ==0: + # Arc: turn cyclic curve flag off! + + # Segment: + if type ==2: + newpoints.append([0,0,0]) + # Ring: + elif type ==3: + j=sides-1 + while j > -1: + t = (j*step) + angle*startangle + x2 = sin(t*pi)*innerradius + y2 = cos(t*pi)*innerradius + newpoints.append([x2,y2,0]) + j-=1 + return newpoints + +##------------------------------------------------------------ +# 2DCurve: Cog wheel: +def CogCurve(theeth=8, innerradius=0.8, middleradius=0.95, outerradius=1.0, bevel=0.5): + """ + CogCurve( theeth=8, innerradius=0.8, middleradius=0.95, outerradius=1.0, bevel=0.5 ) + + Create cog wheel shaped curve + + Parameters: + theeth - number of theeth + (type=int) + innerradius - innerradius + (type=float) + middleradius - middleradius + (type=float) + outerradius - outerradius + (type=float) + bevel - bevel amount + (type=float) + Returns: + a list with lists of x,y,z coordinates for curve points, [[x,y,z],[x,y,z],...n] + (type=list) + """ + + newpoints = [] + step = (2.0/(theeth)) + pet = (step/pi*2) + bevel = 1.0-bevel + i = 0 + while i < theeth: + t = (i*step) + x1 = cos(t*pi-(pi/theeth)-pet)*innerradius + y1 = sin(t*pi-(pi/theeth)-pet)*innerradius + newpoints.append([x1,y1,0]) + x2 = cos(t*pi-(pi/theeth)+pet)*innerradius + y2 = sin(t*pi-(pi/theeth)+pet)*innerradius + newpoints.append([x2,y2,0]) + x3 = cos(t*pi-pet)*middleradius + y3 = sin(t*pi-pet)*middleradius + newpoints.append([x3,y3,0]) + x4 = cos(t*pi-(pet*bevel))*outerradius + y4 = sin(t*pi-(pet*bevel))*outerradius + newpoints.append([x4,y4,0]) + x5 = cos(t*pi+(pet*bevel))*outerradius + y5 = sin(t*pi+(pet*bevel))*outerradius + newpoints.append([x5,y5,0]) + x6 = cos(t*pi+pet)*middleradius + y6 = sin(t*pi+pet)*middleradius + newpoints.append([x6,y6,0]) + i+=1 + return newpoints + +##------------------------------------------------------------ +# 2DCurve: nSide: +def nSideCurve(sides=6, radius=1.0): + """ + nSideCurve( sides=6, radius=1.0 ) + + Create n-sided curve + + Parameters: + sides - number of sides + (type=int) + radius - radius + (type=float) + Returns: + a list with lists of x,y,z coordinates for curve points, [[x,y,z],[x,y,z],...n] + (type=list) + """ + + newpoints = [] + step = (2.0/(sides)) + i = 0 + while i < sides: + t = (i*step) + x = sin(t*pi)*radius + y = cos(t*pi)*radius + newpoints.append([x,y,0]) + i+=1 + return newpoints + +''' +##------------------------------------------------------------ +# 2DCurve: Splat: +def SplatCurve(sides=24, scale=1.0, seed=0, basis=0, radius=1.0): + """ + SplatCurve( sides=24, scale=1.0, seed=0, basis=0, radius=1.0 ) + + Create splat curve + + Parameters: + sides - number of sides + (type=int) + scale - noise size + (type=float) + seed - noise random seed + (type=int) + basis - noise basis + (type=int) + radius - radius + (type=float) + Returns: + a list with lists of x,y,z coordinates for curve points, [[x,y,z],[x,y,z],...n] + (type=list) + """ + + newpoints = [] + step = (2.0/(sides)) + i = 0 + while i < sides: + t = (i*step) + turb = vTurbNoise(t, 1.0, scale, 6, 0, basis, seed ) + turb = turb[2] * 0.5 + 0.5 + x = sin(t*pi)*radius * turb + y = cos(t*pi)*radius * turb + newpoints.append([x,y,0]) + i+=1 + return newpoints +''' +###----------------------------------------------------------- +#### 3D curve shape functions: +###----------------------------------------------------------- + +###------------------------------------------------------------ +# 3DCurve: Helix: +def HelixCurve( number=100, height=2.0, startangle=0.0, endangle=360.0, width=1.0, a=0.0, b=0.0 ): + """ + HelixCurve( number=100, height=2.0, startangle=0.0, endangle=360.0, width=1.0, a=0.0, b=0.0 ) + + Create helix curve + + Parameters: + number - the number of points + (type=int) + height - height + (type=float) + startangle - startangle + (type=float) + endangle - endangle + (type=float) + width - width + (type=float) + a - a + (type=float) + b - b + (type=float) + Returns: + a list with lists of x,y,z coordinates for curve points, [[x,y,z],[x,y,z],...n] + (type=list) + """ + + newpoints = [] + angle = (2.0/360.0)*(endangle-startangle) + step = angle/(number-1) + h = height/angle + start = (startangle*2.0/360.0) + a/=angle + i = 0 + while i < number: + t = ( i*step+start ) + x = sin( (t*pi) ) * ( 1.0 + cos( t * pi * a - ( b * pi ) ) ) * ( 0.25 * width ) + y = cos( (t*pi) ) * ( 1.0 + cos( t * pi * a - ( b * pi ) ) ) * ( 0.25 * width ) + z = ( t * h ) -h*start + newpoints.append([x,y,z]) + i+=1 + return newpoints + +###------------------------------------------------------------ ? +# 3DCurve: Cycloid: Cycloid, Epicycloid, Hypocycloid +def CycloidCurve( number=24, length=2.0, type=0, a=1.0, b=1.0, startangle=0.0, endangle=360.0 ): + """ + CycloidCurve( number=24, length=2.0, type=0, a=1.0, b=1.0, startangle=0.0, endangle=360.0 ) + + Create a Cycloid, Epicycloid or Hypocycloid curve + + Parameters: + number - the number of points + (type=int) + length - length of curve + (type=float) + type - types: Cycloid, Epicycloid, Hypocycloid + (type=int) + Returns: + a list with lists of x,y,z coordinates for curve points, [[x,y,z],[x,y,z],...n] + (type=list) + """ + + newpoints = [] + angle = (2.0/360.0)*(endangle-startangle) + step = angle/(number-1) + #h = height/angle + d = length + start = (startangle*2.0/360.0) + a/=angle + i = 0 + if type == 0: # Epitrochoid + while i < number: + t = ( i*step+start ) + x = ((a + b) * cos(t*pi)) - (d * cos(((a+b)/b)*t*pi)) + y = ((a + b) * sin(t*pi)) - (d * sin(((a+b)/b)*t*pi)) + z = 0 # ( t * h ) -h*start + newpoints.append([x,y,z]) + i+=1 + + else: + newpoints = [[-1,-1,0], [-1,1,0], [1,1,0], [1,-1,0]] + return newpoints + +##------------------------------------------------------------ +# calculates the matrix for the new object +# depending on user pref +def align_matrix(context): + loc = TranslationMatrix(context.scene.cursor_location) + obj_align = context.user_preferences.edit.object_align + if (context.space_data.type == 'VIEW_3D' + and obj_align == 'VIEW'): + rot = context.space_data.region_3d.view_matrix.rotation_part().invert().resize4x4() + else: + rot = Matrix() + align_matrix = loc * rot + return align_matrix + +##------------------------------------------------------------ +#### Curve creation functions +# sets bezierhandles to auto +def setBezierHandles(obj, mode = 'AUTOMATIC'): + scene = bpy.context.scene + if obj.type != 'CURVE': + return + scene.objects.active = obj + bpy.ops.object.mode_set(mode='EDIT', toggle=True) + bpy.ops.curve.select_all(action='SELECT') + bpy.ops.curve.handle_type_set(type=mode) + bpy.ops.object.mode_set(mode='OBJECT', toggle=True) + +# get array of vertcoordinates acording to splinetype +def vertsToPoints(Verts, splineType): + # main vars + vertArray = [] + + # array for BEZIER spline output (V3) + if splineType == 'BEZIER': + for v in Verts: + vertArray += v + + # array for nonBEZIER output (V4) + else: + for v in Verts: + vertArray += v + if splineType == 'NURBS': + vertArray.append(1) #for nurbs w=1 + else: #for poly w=0 + vertArray.append(0) + return vertArray + +# create new CurveObject from vertarray and splineType +def createCurve(vertArray, options, curveOptions, align_matrix): + # options to vars + splineType = options[0] # output splineType 'POLY' 'NURBS' 'BEZIER' + name = options[1] # GalloreType as name + + # create curve + scene = bpy.context.scene + newCurve = bpy.data.curves.new(name, type = 'CURVE') # curvedatablock + newSpline = newCurve.splines.new(type = splineType) # spline + + # create spline from vertarray + if splineType == 'BEZIER': + newSpline.bezier_points.add(int(len(vertArray)*0.33)) + newSpline.bezier_points.foreach_set('co', vertArray) + else: + newSpline.points.add(int(len(vertArray)*0.25 - 1)) + newSpline.points.foreach_set('co', vertArray) + newSpline.endpoint_u = True + + # set curveOptions + shape = curveOptions[0] + cyclic_u = curveOptions[1] + endp_u = curveOptions[2] + order_u = curveOptions[3] + handleType = curveOptions[4] + + newCurve.dimensions = shape + newSpline.cyclic_u = cyclic_u + newSpline.endpoint_u = endp_u + newSpline.order_u = order_u + + # create object with newCurve + new_obj = bpy.data.objects.new(name, newCurve) # object + scene.objects.link(new_obj) # place in active scene + new_obj.select = True # set as selected + scene.objects.active = new_obj # set as active + new_obj.matrix_world = align_matrix # apply matrix + + # set bezierhandles + if splineType == 'BEZIER': + setBezierHandles(new_obj, handleType) + + return + +##------------------------------------------------------------ +# Main Function +def main(context, options, curveOptions, align_matrix): + # deselect all objects + bpy.ops.object.select_all(action='DESELECT') + + # options + galType = options[1] + splineType = options[0] + innerRadius = options[9] + middleRadius = options[10] + outerRadius = options[11] + + # get verts + if galType == 'Profile': + verts = ProfileCurve(options[2], options[3], options[4]) + if galType == 'Miscellaneous': + verts = MiscCurve(options[5], options[6], options[7], options[8]) + if galType == 'Flower': + verts = FlowerCurve(options[12], innerRadius, outerRadius, options[13]) + if galType == 'Star': + verts = StarCurve(options[14], innerRadius, outerRadius, options[15]) + if galType == 'Arc': + verts = ArcCurve(options[16], options[17], options[18], innerRadius, outerRadius, options[19]) + if galType == 'Cogwheel': + verts = CogCurve(options[20], innerRadius, middleRadius, outerRadius, options[21]) + if galType == 'Nsided': + verts = nSideCurve(options[22], outerRadius) +# if galType == 'Splat': +# verts = SplatCurve(options[23], options[24], options[25], options[26], outerRadius) + if galType == 'Helix': + verts = HelixCurve(options[27], options[28], options[29], options[30], options[31], options[32], options[33]) + if galType == 'Cycloid': + verts = CycloidCurve(options[34], options[35], options[36], options[37], options[38], options[39], options[40]) + + # turn verts into array + vertArray = vertsToPoints(verts, splineType) + + # create object + createCurve(vertArray, options, curveOptions, align_matrix) + + return + +class Curveaceous_galore(bpy.types.Operator): + '''''' + bl_idname = "curveaceous_galore" + bl_label = "Curveaceous galore" + bl_options = {'REGISTER', 'UNDO'} + bl_description = "adds many types of curves" + + # align_matrix for the invoke + align_matrix = Matrix() + + #### general properties + GalloreTypes = [ + ('Profile', 'Profile', 'Profile'), + ('Miscellaneous', 'Miscellaneous', 'Miscellaneous'), + ('Flower', 'Flower', 'Flower'), + ('Star', 'Star', 'Star'), + ('Arc', 'Arc', 'Arc'), + ('Cogwheel', 'Cogwheel', 'Cogwheel'), + ('Nsided', 'Nsided', 'Nsided'), +# ('Splat', 'Splat', 'Splat'), + ('Cycloid', 'Cycloid', 'Cycloid'), + ('Helix', 'Helix (3D)', 'Helix')] + GalloreType = EnumProperty(name="Type", + description="Form of Curve to create", + items=GalloreTypes) + SplineTypes = [ + ('POLY', 'Poly', 'POLY'), + ('NURBS', 'Nurbs', 'NURBS'), + ('BEZIER', 'Bezier', 'BEZIER')] + outputType = EnumProperty(name="Output splines", + description="Type of splines to output", + items=SplineTypes) + + #### Curve Options + shapeItems = [ + ('2D', '2D', '2D'), + ('3D', '3D', '3D')] + shape = EnumProperty(name="2D / 3D", + items=shapeItems, + description="2D or 3D Curve") + cyclic_u = BoolProperty(name="Cyclic", + default=True, + description="make curve closed") + endp_u = BoolProperty(name="endpoint_u", + default=True, + description="stretch to endpoints") + order_u = IntProperty(name="order_u", + default=4, + min=2, soft_min=2, + max=6, soft_max=6, + description="Order of nurbs spline") + bezHandles = [ + ('VECTOR', 'Vector', 'VECTOR'), + ('AUTOMATIC', 'Auto', 'AUTOMATIC')] + handleType = EnumProperty(name="Handle type", + description="bezier handles type", + items=bezHandles) + + #### ProfileCurve properties + ProfileCurveType = IntProperty(name="Type", + min=1, soft_min=1, + max=5, soft_max=5, + default=1, + description="Type of ProfileCurve") + ProfileCurvevar1 = FloatProperty(name="var_1", + default=0.25, + description="var1 of ProfileCurve") + ProfileCurvevar2 = FloatProperty(name="var_2", + default=0.25, + description="var2 of ProfileCurve") + + #### MiscCurve properties + MiscCurveType = IntProperty(name="Type", + min=1, soft_min=1, + max=5, soft_max=5, + default=1, + description="Type of ProfileCurve") + MiscCurvevar1 = FloatProperty(name="var_1", + default=1.0, + description="var1 of ProfileCurve") + MiscCurvevar2 = FloatProperty(name="var_2", + default=0.5, + description="var2 of ProfileCurve") + MiscCurvevar3 = FloatProperty(name="var_3", # doesn't seem to do anything + default=90.0, + description="var3 of ProfileCurve") + + #### Common properties + innerRadius = FloatProperty(name="Inner radius", + default=0.5, + min=0, soft_min=0, + description="Inner radius") + middleRadius = FloatProperty(name="Middle radius", + default=0.95, + min=0, soft_min=0, + description="Middle radius") + outerRadius = FloatProperty(name="Outer radius", + default=1.0, + min=0, soft_min=0, + description="Outer radius") + + #### Flower properties + petals = IntProperty(name="Petals", + default=8, + min=2, soft_min=2, + description="Number of petals") + petalWidth = FloatProperty(name="Petal width", + default=2.0, + min=0.01, soft_min=0.01, + description="Petal width") + + #### Star properties + starPoints = IntProperty(name="Star points", + default=8, + min=2, soft_min=2, + description="Number of star points") + starTwist = FloatProperty(name="Twist", + default=0.0, + description="Twist") + + #### Arc properties + arcSides = IntProperty(name="Arc sides", + default=6, + min=1, soft_min=1, + description="Sides of arc") + startAngle = FloatProperty(name="Start angle", + default=0.0, + description="Start angle") + endAngle = FloatProperty(name="End angle", + default=90.0, + description="End angle") + arcType = IntProperty(name="Arc type", + default=3, + min=1, soft_min=1, + max=3, soft_max=3, + description="Sides of arc") + + #### Cogwheel properties + teeth = IntProperty(name="Teeth", + default=8, + min=2, soft_min=2, + description="number of teeth") + bevel = FloatProperty(name="Bevel", + default=0.5, + min=0, soft_min=0, + max=1, soft_max=1, + description="Bevel") + + #### Nsided property + Nsides = IntProperty(name="Sides", + default=8, + min=3, soft_min=3, + description="Number of sides") + + #### Splat properties + splatSides = IntProperty(name="Splat sides", + default=24, + min=3, soft_min=3, + description="Splat sides") + splatScale = FloatProperty(name="Splat scale", + default=1.0, + min=0, soft_min=0, + description="Splat scale") + seed = IntProperty(name="Seed", + default=0, + min=0, soft_min=0, + description="Seed") + basis = IntProperty(name="Basis", + default=0, + min=0, soft_min=0, + description="Basis") + + #### Helix properties + helixPoints = IntProperty(name="resolution", + default=100, + min=3, soft_min=3, + description="resolution") + helixHeight = FloatProperty(name="Height", + default=2.0, + min=0, soft_min=0, + description="Helix height") + helixStart = FloatProperty(name="Start angle", + default=0.0, + description="Helix start angle") + helixEnd = FloatProperty(name="Endangle", + default=360.0, + description="Helix end angle") + helixWidth = FloatProperty(name="Width", + default=1.0, + description="Helix width") + helix_a = FloatProperty(name="var_1", + default=0.0, + description="Helix var1") + helix_b = FloatProperty(name="var_2", + default=0.0, + description="Helix var2") + + #### Cycloid properties + cycloPoints = IntProperty(name="Resolution", + default=100, + min=3, soft_min=3, + description="Resolution") + cyclo_d = FloatProperty(name="var_3", + default=1.5, + description="Cycloid var3") + cycloType = IntProperty(name="Type", + default=0, + min=0, soft_min=0, + max=0, soft_max=0, + description="resolution") + cyclo_a = FloatProperty(name="var_1", + default=5.0, + min=0.01, soft_min=0.01, + description="Cycloid var1") + cyclo_b = FloatProperty(name="var_2", + default=0.5, + min=0.01, soft_min=0.01, + description="Cycloid var2") + cycloStart = FloatProperty(name="Start angle", + default=0.0, + description="Cycloid start angle") + cycloEnd = FloatProperty(name="End angle", + default=360.0, + description="Cycloid end angle") + + ##### DRAW ##### + def draw(self, context): + props = self.properties + layout = self.layout + + # general options + col = layout.column() + col.prop(props, 'GalloreType') + col.label(text=props.GalloreType+" Options") + + # options per GalloreType + box = layout.box() + if props.GalloreType == 'Profile': + box.prop(props, 'ProfileCurveType') + box.prop(props, 'ProfileCurvevar1') + box.prop(props, 'ProfileCurvevar2') + if props.GalloreType == 'Miscellaneous': + box.prop(props, 'MiscCurveType') + box.prop(props, 'MiscCurvevar1') + box.prop(props, 'MiscCurvevar2') + #box.prop(props, 'MiscCurvevar3') # doesn't seem to do anything + if props.GalloreType == 'Flower': + box.prop(props, 'petals') + box.prop(props, 'petalWidth') + box.prop(props, 'innerRadius') + box.prop(props, 'outerRadius') + if props.GalloreType == 'Star': + box.prop(props, 'starPoints') + box.prop(props, 'starTwist') + box.prop(props, 'innerRadius') + box.prop(props, 'outerRadius') + if props.GalloreType == 'Arc': + box.prop(props, 'arcSides') + box.prop(props, 'arcType') # has only one Type? + box.prop(props, 'startAngle') + box.prop(props, 'endAngle') + box.prop(props, 'innerRadius') # doesn't seem to do anything + box.prop(props, 'outerRadius') + if props.GalloreType == 'Cogwheel': + box.prop(props, 'teeth') + box.prop(props, 'bevel') + box.prop(props, 'innerRadius') + box.prop(props, 'middleRadius') + box.prop(props, 'outerRadius') + if props.GalloreType == 'Nsided': + box.prop(props, 'Nsides') + box.prop(props, 'outerRadius', text='Radius') + ''' + if props.GalloreType == 'Splat': + box.prop(props, 'splatSides') + box.prop(props, 'outerRadius') + box.prop(props, 'splatScale') + box.prop(props, 'seed') + box.prop(props, 'basis') + ''' + if props.GalloreType == 'Helix': + box.prop(props, 'helixPoints') + box.prop(props, 'helixHeight') + box.prop(props, 'helixWidth') + box.prop(props, 'helixStart') + box.prop(props, 'helixEnd') + box.prop(props, 'helix_a') + box.prop(props, 'helix_b') + if props.GalloreType == 'Cycloid': + box.prop(props, 'cycloPoints') + #box.prop(props, 'cycloType') # needs the other types first + box.prop(props, 'cycloStart') + box.prop(props, 'cycloEnd') + box.prop(props, 'cyclo_a') + box.prop(props, 'cyclo_b') + box.prop(props, 'cyclo_d') + + col = layout.column() + col.label(text="Output Curve Type") + row = layout.row() + row.prop(props, 'outputType', expand=True) + col = layout.column() + col.label(text="Curve Options") + + # output options + box = layout.box() + if props.outputType == 'NURBS': + box.row().prop(props, 'shape', expand=True) + box.prop(props, 'cyclic_u') + #box.prop(props, 'endp_u') + box.prop(props, 'order_u') + + if props.outputType == 'POLY': + box.row().prop(props, 'shape', expand=True) + box.prop(props, 'cyclic_u') + + if props.outputType == 'BEZIER': + box.row().prop(props, 'shape', expand=True) + box.row().prop(props, 'handleType', expand=True) + box.prop(props, 'cyclic_u') + + + ##### POLL ##### + def poll(self, context): + return context.scene != None + + ##### EXECUTE ##### + def execute(self, context): + # turn off undo + undo = bpy.context.user_preferences.edit.global_undo + bpy.context.user_preferences.edit.global_undo = False + + props = self.properties + if props.GalloreType in ['Helix', 'Cycloid']: + props.shape = '3D' + if props.GalloreType in ['Helix']: + props.cyclic_u = False + + # Options + options = [ + # general properties + props.outputType, #0 + props.GalloreType, #1 + # ProfileCurve properties + props.ProfileCurveType, #2 + props.ProfileCurvevar1, #3 + props.ProfileCurvevar2, #4 + # MiscCurve properties + props.MiscCurveType, #5 + props.MiscCurvevar1, #6 + props.MiscCurvevar2, #7 + props.MiscCurvevar3, #8 + # Common properties + props.innerRadius, #9 + props.middleRadius, #10 + props.outerRadius, #11 + # Flower properties + props.petals, #12 + props.petalWidth, #13 + # Star properties + props.starPoints, #14 + props.starTwist, #15 + # Arc properties + props.arcSides, #16 + props.startAngle, #17 + props.endAngle, #18 + props.arcType, #19 + # Cogwheel properties + props.teeth, #20 + props.bevel, #21 + # Nsided property + props.Nsides, #22 + # Splat properties + props.splatSides, #23 + props.splatScale, #24 + props.seed, #25 + props.basis, #26 + # Helix properties + props.helixPoints, #27 + props.helixHeight, #28 + props.helixStart, #29 + props.helixEnd, #30 + props.helixWidth, #31 + props.helix_a, #32 + props.helix_b, #33 + # Cycloid properties + props.cycloPoints, #34 + props.cyclo_d, #35 + props.cycloType, #36 + props.cyclo_a, #37 + props.cyclo_b, #38 + props.cycloStart, #39 + props.cycloEnd #40 + ] + + # Curve options + curveOptions = [ + props.shape, #0 + props.cyclic_u, #1 + props.endp_u, #2 + props.order_u, #4 + props.handleType #5 + ] + + # main function + main(context, options, curveOptions, self.align_matrix) + + # restore pre operator undo state + bpy.context.user_preferences.edit.global_undo = undo + + return {'FINISHED'} + + ##### INVOKE ##### + def invoke(self, context, event): + # store creation_matrix + self.align_matrix = align_matrix(context) + self.execute(context) + + return {'FINISHED'} + +################################################################################ +##### REGISTER ##### + +Curveaceous_galore_button = (lambda self, context: self.layout.operator + (Curveaceous_galore.bl_idname, text="curvatures gallore", icon="PLUGIN")) + +classes = [ +Curveaceous_galore + ] + +def register(): + register = bpy.types.register + for cls in classes: + register(cls) + + bpy.types.INFO_MT_curve_add.append(Curveaceous_galore_button) + +def unregister(): + unregister = bpy.types.unregister + for cls in classes: + unregister(cls) + + bpy.types.INFO_MT_curve_add.remove(Curveaceous_galore_button) + +if __name__ == "__main__": + register() diff --git a/add_curve_torus_knots.py b/add_curve_torus_knots.py new file mode 100644 index 00000000..9dc6ac8e --- /dev/null +++ b/add_curve_torus_knots.py @@ -0,0 +1,363 @@ +# ##### 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 ##### + + +bl_addon_info = { + 'name': 'Add Curve: Torus Knots', + 'author': 'testscreenings', + 'version': '0.1', + 'blender': (2, 5, 3), + 'location': 'View3D > Add > Curve', + 'description': 'Adds many types of knots', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Curve/Torus_Knot', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=22403&group_id=153&atid=469', + 'category': 'Add Curve'} + + +##------------------------------------------------------------ +#### import modules +import bpy +from bpy.props import * +from mathutils import * +from math import * + +##------------------------------------------------------------ +# calculates the matrix for the new object +# depending on user pref +def align_matrix(context): + loc = TranslationMatrix(context.scene.cursor_location) + obj_align = context.user_preferences.edit.object_align + if (context.space_data.type == 'VIEW_3D' + and obj_align == 'VIEW'): + rot = context.space_data.region_3d.view_matrix.rotation_part().invert().resize4x4() + else: + rot = Matrix() + align_matrix = loc * rot + return align_matrix + +##------------------------------------------------------------ +#### Curve creation functions + +# get array of vertcoordinates acording to splinetype +def vertsToPoints(Verts): + vertArray = [] + + for v in Verts: + vertArray += v + vertArray.append(1) #for nurbs w=1 + + return vertArray + +# create new CurveObject from vertarray and splineType +def createCurve(vertArray, GEO, align_matrix): + # options to vars + splineType = 'NURBS' + name = 'Torus_Knot' + + # create curve + scene = bpy.context.scene + newCurve = bpy.data.curves.new(name, type = 'CURVE') # curvedatablock + newSpline = newCurve.splines.new(type = splineType) # spline + + # create spline from vertarray + newSpline.points.add(int(len(vertArray)*0.25 - 1)) + newSpline.points.foreach_set('co', vertArray) + newSpline.endpoint_u = True + + # Curve settings + newCurve.dimensions = '3D' + newSpline.cyclic_u = True + newSpline.endpoint_u = True + newSpline.order_u = 4 + + # GEO Options + surf = GEO[0] + bDepth = GEO[1] + bRes = GEO[2] + extrude = GEO[3] + width = GEO[4] + res = GEO[5] + + if surf: + newCurve.bevel_depth = bDepth + newCurve.bevel_resolution = bRes + newCurve.front = False + newCurve.back = False + newCurve.extrude = extrude + newCurve.width = width + newCurve.resolution_u = res + + # create object with newCurve + new_obj = bpy.data.objects.new(name, newCurve) # object + scene.objects.link(new_obj) # place in active scene + new_obj.select = True # set as selected + scene.objects.active = new_obj # set as active + new_obj.matrix_world = align_matrix # apply matrix + + return + +######################################################################## +####################### Knot Definitions ############################### +######################################################################## + +#### TORUS KNOT +def Torus_Knot_Curve(p=2, q=3, w=1, res=24, formula=0, h=1, u=1 ,v=1, rounds=2): + newPoints = [] + angle = 2*rounds + step = angle/(res-1) + scale = h + height = w + + for i in range(res-1): + t = ( i*step*pi) + + x = (2 * scale + cos((q*t)/p*v)) * cos(t * u) + y = (2 * scale + cos((q*t)/p*v)) * sin(t * u) + z = sin(q*t/p) * height + + newPoints.append([x,y,z]) + + return newPoints + +##------------------------------------------------------------ +# Main Function +def main(context, param, GEO, options, align_matrix): + # deselect all objects + bpy.ops.object.select_all(action='DESELECT') + + # options + splineType = 'NURBS' + + + # get verts + verts = Torus_Knot_Curve(param[0], param[1], param[2], param[3], param[4], + param[5], param[6], param[7], param[8]) + + # turn verts into array + vertArray = vertsToPoints(verts) + + # create object + createCurve(vertArray, GEO, align_matrix) + + return + +class torus_knot_plus(bpy.types.Operator): + '''''' + bl_idname = "torus_knot_plus" + bl_label = "Torus Knot +" + bl_options = {'REGISTER', 'UNDO'} + bl_description = "adds many types of knots" + + # align_matrix for the invoke + align_matrix = Matrix() + + #### general options + options_plus = BoolProperty(name="plus options", + default=False, + description="Show more options (the plus part).") + + #### GEO Options + geo_surf = BoolProperty(name="Surface", + default=True) + geo_bDepth = FloatProperty(name="bevel", + default=0.08, + min=0, soft_min=0) + geo_bRes = IntProperty(name="bevel res", + default=2, + min=0, soft_min=0, + max=4, soft_max=4) + geo_extrude = FloatProperty(name="extrude", + default=0.0, + min=0, soft_min=0) + geo_width = FloatProperty(name="width", + default=1.0, + min=0, soft_min=0) + geo_res = IntProperty(name="resolution", + default=12, + min=1, soft_min=1) + + + #### Parameters + torus_res = IntProperty(name="Resoulution", + default=200, + min=3, soft_min=3, + description='Resolution, Number of controlverticies.') + torus_p = IntProperty(name="p", + default=2, + min=1, soft_min=1, + #max=1, soft_max=1, + description="p") + torus_q = IntProperty(name="q", + default=3, + min=1, soft_min=1, + #max=1, soft_max=1, + description="q") + torus_w = FloatProperty(name="Height", + default=1, + #min=0, soft_min=0, + #max=1, soft_max=1, + description="Height in Z") + torus_h = FloatProperty(name="Scale", + default=1, + #min=0, soft_min=0, + #max=1, soft_max=1, + description="Scale, in XY") + torus_u = IntProperty(name="u", + default=1, + min=1, soft_min=1, + #max=1, soft_max=1, + description="u") + torus_v = IntProperty(name="v", + default=1, + min=1, soft_min=1, + #max=1, soft_max=1, + description="v") + torus_formula = IntProperty(name="Variation", + default=0, + min=0, soft_min=0, + max=10, soft_max=10) + torus_rounds = IntProperty(name="Rounds", + default=2, + min=1, soft_min=1, + #max=1, soft_max=1, + description="Rounds") + + ##### DRAW ##### + def draw(self, context): + props = self.properties + layout = self.layout + + # general options + col = layout.column() + #col.prop(props, 'KnotType') waits for more knottypes + col.label(text="Torus Knot Parameters") + + # Parameters + box = layout.box() + box.prop(props, 'torus_res') + box.prop(props, 'torus_w') + box.prop(props, 'torus_h') + box.prop(props, 'torus_p') + box.prop(props, 'torus_q') + box.prop(props, 'options_plus') + if props.options_plus: + box.prop(props, 'torus_u') + box.prop(props, 'torus_v') + box.prop(props, 'torus_rounds') + + # surface Options + col = layout.column() + col.label(text="Geometry Options") + box = layout.box() + box.prop(props, 'geo_surf') + if props.geo_surf: + box.prop(props, 'geo_bDepth') + box.prop(props, 'geo_bRes') + box.prop(props, 'geo_extrude') + #box.prop(props, 'geo_width') # not really good + box.prop(props, 'geo_res') + + ##### POLL ##### + def poll(self, context): + return context.scene != None + + ##### EXECUTE ##### + def execute(self, context): + # turn off undo + undo = bpy.context.user_preferences.edit.global_undo + bpy.context.user_preferences.edit.global_undo = False + + props = self.properties + + if not props.options_plus: + props.torus_rounds = props.torus_p + + # Parameters + param = [ + props.torus_p, #0 + props.torus_q, #1 + props.torus_w, #2 + props.torus_res, #3 + props.torus_formula, #4 + props.torus_h, #5 + props.torus_u, #6 + props.torus_v, #7 + props.torus_rounds #8 + ] + + # GEO Options + GEO = [ + props.geo_surf, #0 + props.geo_bDepth, #1 + props.geo_bRes, #2 + props.geo_extrude, #3 + props.geo_width, #4 + props.geo_res #5 + ] + + # Options + options = [ + # general properties + ] + + + # main function + main(context, param, GEO, options, self.align_matrix) + + # restore pre operator undo state + bpy.context.user_preferences.edit.global_undo = undo + + return {'FINISHED'} + + ##### INVOKE ##### + def invoke(self, context, event): + # store creation_matrix + self.align_matrix = align_matrix(context) + self.execute(context) + + return {'FINISHED'} + +################################################################################ +##### REGISTER ##### + +torus_knot_plus_button = (lambda self, context: self.layout.operator + (torus_knot_plus.bl_idname, text="Torus Knot +", icon="PLUGIN")) + +classes = [ +torus_knot_plus + ] + +def register(): + register = bpy.types.register + for cls in classes: + register(cls) + + bpy.types.INFO_MT_curve_add.append(torus_knot_plus_button) + +def unregister(): + unregister = bpy.types.unregister + for cls in classes: + unregister(cls) + + bpy.types.INFO_MT_curve_add.remove(torus_knot_plus_button) + +if __name__ == "__main__": + register() diff --git a/add_mesh_3d_function_surface.py b/add_mesh_3d_function_surface.py new file mode 100644 index 00000000..79a01ae4 --- /dev/null +++ b/add_mesh_3d_function_surface.py @@ -0,0 +1,648 @@ +# ##### 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 ##### + +""" +Z Function Surface + +This script lets the user create a surface where the z coordinate +is a function of the x and y coordinates. + + z = f(x,y) + +X,Y,Z Function Surface + +This script lets the user create a surface where the x, y and z +coordinates are defiend by a function. + + x = f(u,v) + y = f(u,v) + z = f(u,v) + +Usage: +You have to activated the script in the "Add-Ons" tab (user preferences). +The functionality can then be accessed via the +"Add Mesh" -> "Z Function Surface" +and +"Add Mesh" -> "X,Y,Z Function Surface" +menu. + +Version history: +v0.3.5 - createFaces can now "Flip" faces and create fan/star like faces. +v0.3.4 - Updated store_recall_properties, apply_object_align + and create_mesh_object. + Changed how recall data is stored. +v0.3.3 - API change Mathutils -> mathutils (r557) +v0.3.2 - Various fixes&streamlining by ideasman42/Campbell Barton. + r544 Compile expressions for faster execution + r544 Use operator reports for errors too + r544 Avoid type checks by converting to a float, errors + converting to a float are reported too. + Fixed an error Campbell overlooked (appending tuples to an + array, not single values) Thamnks for the report wild_doogy. + Added 'description' field, updated 'wiki_url'. + Made the script PEP8 compatible again. +v0.3.1 - Use hidden "edit" property for "recall" operator. + Bugfix: Z Function was mixing up div_x and div_y +v0.3 - X,Y,Z Function Surface (by Ed Mackey & tuga3d). + Renamed old function to "Z Function Surface". + Align the geometry to the view if the user preference says so. + Store recall properties in newly created object. +v0.2.3 - Use bl_addon_info for Add-On information. +v0.2.2 - Fixed Add-On registration text. +v0.2.1 - Fixed some new API stuff. + Mainly we now have the register/unregister functions. + Also the new() function for objects now accepts a mesh object. + Changed the script so it can be managed from the "Add-Ons" tab + in the user preferences. + Added dummy "PLUGIN" icon. + Corrected FSF address. + Clean up of tooltips. +v0.2 - Added security check for eval() function + Check return value of eval() for complex numbers. +v0.1.1 - Use 'CANCELLED' return value when failing. + Updated web links. +v0.1 - Initial revision. + +More Links: +http://gitorious.org/blender-scripts/blender-3d-function-surface +http://blenderartists.org/forum/showthread.php?t=179043 + +""" + + +bl_addon_info = { + 'name': 'Add Mesh: 3D Function Surfaces', + 'author': 'Buerbaum Martin (Pontiac)', + 'version': '0.3.5', + 'blender': (2, 5, 3), + 'location': 'View3D > Add > Mesh > Z Function Surface &' \ + ' XYZ Function Surface', + 'description': 'Create Objects using Math Formulas', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Add_Mesh/Add_3d_Function_Surface', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=21444&group_id=153&atid=469', + 'category': 'Add Mesh'} + +import bpy +from mathutils import * +from math import * +from bpy.props import * + +# List of safe functions for eval() +safe_list = ['math', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'cosh', + 'degrees', 'e', 'exp', 'fabs', 'floor', 'fmod', 'frexp', 'hypot', + 'ldexp', 'log', 'log10', 'modf', 'pi', 'pow', 'radians', + 'sin', 'sinh', 'sqrt', 'tan', 'tanh'] + +# Use the list to filter the local namespace +safe_dict = dict([(k, globals().get(k, None)) for k in safe_list]) + + +# Stores the values of a list of properties and the +# operator id in a property group ('recall_op') inside the object. +# Could (in theory) be used for non-objects. +# Note: Replaces any existing property group with the same name! +# ob ... Object to store the properties in. +# op ... The operator that should be used. +# op_args ... A dictionary with valid Blender +# properties (operator arguments/parameters). + + +# calculates the matrix for the new object +# depending on user pref +def align_matrix(context): + loc = TranslationMatrix(context.scene.cursor_location) + obj_align = context.user_preferences.edit.object_align + if (context.space_data.type == 'VIEW_3D' + and obj_align == 'VIEW'): + rot = context.space_data.region_3d.view_matrix.rotation_part().invert().resize4x4() + else: + rot = Matrix() + align_matrix = loc * rot + return align_matrix + + +# Create a new mesh (object) from verts/edges/faces. +# verts/edges/faces ... List of vertices/edges/faces for the +# new mesh (as used in from_pydata). +# name ... Name of the new mesh (& object). +# edit ... Replace existing mesh data. +# Note: Using "edit" will destroy/delete existing mesh data. +def create_mesh_object(context, verts, edges, faces, name, edit, align_matrix): + scene = context.scene + obj_act = scene.objects.active + + # Can't edit anything, unless we have an active obj. + if edit and not obj_act: + return None + + # Create new mesh + mesh = bpy.data.meshes.new(name) + + # Make a mesh from a list of verts/edges/faces. + mesh.from_pydata(verts, edges, faces) + + # Update mesh geometry after adding stuff. + mesh.update() + + # Deselect all objects. + bpy.ops.object.select_all(action='DESELECT') + + if edit: + # Replace geometry of existing object + + # Use the active obj and select it. + ob_new = obj_act + ob_new.select = True + + if obj_act.mode == 'OBJECT': + # Get existing mesh datablock. + old_mesh = ob_new.data + + # Set object data to nothing + ob_new.data = None + + # Clear users of existing mesh datablock. + old_mesh.user_clear() + + # Remove old mesh datablock if no users are left. + if (old_mesh.users == 0): + bpy.data.meshes.remove(old_mesh) + + # Assign new mesh datablock. + ob_new.data = mesh + + else: + # Create new object + ob_new = bpy.data.objects.new(name, mesh) + + # Link new object to the given scene and select it. + scene.objects.link(ob_new) + ob_new.select = True + + # Place the object at the 3D cursor location. + # apply viewRotaion + ob_new.matrix_world = align_matrix + + if obj_act and obj_act.mode == 'EDIT': + if not edit: + # We are in EditMode, switch to ObjectMode. + bpy.ops.object.mode_set(mode='OBJECT') + + # Select the active object as well. + obj_act.select = True + + # Apply location of new object. + scene.update() + + # Join new object into the active. + bpy.ops.object.join() + + # Switching back to EditMode. + bpy.ops.object.mode_set(mode='EDIT') + + ob_new = obj_act + + else: + # We are in ObjectMode. + # Make the new object the active one. + scene.objects.active = ob_new + + return ob_new + + +# A very simple "bridge" tool. +# Connects two equally long vertex rows with faces. +# Returns a list of the new faces (list of lists) +# +# vertIdx1 ... First vertex list (list of vertex indices). +# vertIdx2 ... Second vertex list (list of vertex indices). +# closed ... Creates a loop (first & last are closed). +# flipped ... Invert the normal of the face(s). +# +# Note: You can set vertIdx1 to a single vertex index to create +# a fan/star of faces. +# Note: If both vertex idx list are the same length they have +# to have at least 2 vertices. +def createFaces(vertIdx1, vertIdx2, closed=False, flipped=False): + faces = [] + + if not vertIdx1 or not vertIdx2: + return None + + if len(vertIdx1) < 2 and len(vertIdx2) < 2: + return None + + fan = False + if (len(vertIdx1) != len(vertIdx2)): + if (len(vertIdx1) == 1 and len(vertIdx2) > 1): + fan = True + else: + return None + + total = len(vertIdx2) + + if closed: + # Bridge the start with the end. + if flipped: + face = [ + vertIdx1[0], + vertIdx2[0], + vertIdx2[total - 1]] + if not fan: + face.append(vertIdx1[total - 1]) + faces.append(face) + + else: + face = [vertIdx2[0], vertIdx1[0]] + if not fan: + face.append(vertIdx1[total - 1]) + face.append(vertIdx2[total - 1]) + faces.append(face) + + # Bridge the rest of the faces. + for num in range(total - 1): + if flipped: + if fan: + face = [vertIdx2[num], vertIdx1[0], vertIdx2[num + 1]] + else: + face = [vertIdx2[num], vertIdx1[num], + vertIdx1[num + 1], vertIdx2[num + 1]] + faces.append(face) + else: + if fan: + face = [vertIdx1[0], vertIdx2[num], vertIdx2[num + 1]] + else: + face = [vertIdx1[num], vertIdx2[num], + vertIdx2[num + 1], vertIdx1[num + 1]] + faces.append(face) + + return faces + + +class AddZFunctionSurface(bpy.types.Operator): + '''Add a surface defined defined by a function z=f(x,y)''' + bl_idname = "mesh.primitive_z_function_surface" + bl_label = "Add Z Function Surface" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + + equation = StringProperty(name="Z Equation", + description="Equation for z=f(x,y)", + default="1 - ( x**2 + y**2 )") + + div_x = IntProperty(name="X Subdivisions", + description="Number of vertices in x direction.", + default=16, + min=3, + max=256) + div_y = IntProperty(name="Y Subdivisions", + description="Number of vertices in y direction.", + default=16, + min=3, + max=256) + + size_x = FloatProperty(name="X Size", + description="Size of the x axis.", + default=2.0, + min=0.01, + max=100.0, + unit="LENGTH") + size_y = FloatProperty(name="Y Size", + description="Size of the y axis.", + default=2.0, + min=0.01, + max=100.0, + unit="LENGTH") + align_matrix = Matrix() + + def execute(self, context): + edit = self.properties.edit + equation = self.properties.equation + div_x = self.properties.div_x + div_y = self.properties.div_y + size_x = self.properties.size_x + size_y = self.properties.size_y + + verts = [] + faces = [] + + delta_x = size_x / float(div_x - 1) + delta_y = size_y / float(div_y - 1) + start_x = -(size_x / 2.0) + start_y = -(size_y / 2.0) + + edgeloop_prev = [] + + try: + expr_args = ( + compile(equation, __file__, 'eval'), + {"__builtins__": None}, + safe_dict) + except: + import traceback + self.report({'ERROR'}, "Error parsing expression: " + + traceback.format_exc(limit=1)) + return {'CANCELLED'} + + for row_x in range(div_x): + edgeloop_cur = [] + x = start_x + row_x * delta_x + + for row_y in range(div_y): + y = start_y + row_y * delta_y + z = 0.0 + + safe_dict['x'] = x + safe_dict['y'] = y + + # Try to evaluate the equation. + try: + z = float(eval(*expr_args)) + except: + import traceback + self.report({'ERROR'}, "Error evaluating expression: " + + traceback.format_exc(limit=1)) + return {'CANCELLED'} + + edgeloop_cur.append(len(verts)) + verts.append((x, y, z)) + + if len(edgeloop_prev) > 0: + faces_row = createFaces(edgeloop_prev, edgeloop_cur) + faces.extend(faces_row) + + edgeloop_prev = edgeloop_cur + + obj = create_mesh_object(context, verts, [], faces, "Z Function", edit, self.align_matrix) + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +def xyz_function_surface_faces(self, x_eq, y_eq, z_eq, + range_u_min, range_u_max, range_u_step, wrap_u, + range_v_min, range_v_max, range_v_step, wrap_v): + + verts = [] + faces = [] + + uStep = (range_u_max - range_u_min) / range_u_step + vStep = (range_v_max - range_v_min) / range_v_step + + uRange = range_u_step + if range_u_step == 0: + uRange = uRange + 1 + + vRange = range_v_step + if range_v_step == 0: + vRange = vRange + 1 + + try: + expr_args_x = ( + compile(x_eq, __file__.replace(".py", "_x.py"), 'eval'), + {"__builtins__": None}, + safe_dict) + expr_args_y = ( + compile(y_eq, __file__.replace(".py", "_y.py"), 'eval'), + {"__builtins__": None}, + safe_dict) + expr_args_z = ( + compile(z_eq, __file__.replace(".py", "_z.py"), 'eval'), + {"__builtins__": None}, + safe_dict) + except: + import traceback + self.report({'ERROR'}, "Error parsing expression: " + + traceback.format_exc(limit=1)) + return [], [] + + for vN in range(vRange): + v = range_v_min + (vN * vStep) + + for uN in range(uRange): + u = range_u_min + (uN * uStep) + + safe_dict['u'] = u + safe_dict['v'] = v + + # Try to evaluate the equation. + try: + verts.append(( + float(eval(*expr_args_x)), + float(eval(*expr_args_y)), + float(eval(*expr_args_z)))) + + except: + import traceback + self.report({'ERROR'}, "Error evaluating expression: " + + traceback.format_exc(limit=1)) + return [], [] + + for vN in range(1, range_v_step + 1): + vThis = vN + + if (vThis >= vRange): + if wrap_v: + vThis = 0 + else: + continue + + for uN in range(1, range_u_step + 1): + uThis = uN + + if (uThis >= uRange): + if wrap_u: + uThis = 0 + else: + continue + + faces.append([(vThis * uRange) + uThis, + (vThis * uRange) + uN - 1, + ((vN - 1) * uRange) + uN - 1, + ((vN - 1) * uRange) + uThis]) + + return verts, faces + + +# Original Script "Parametric.py" by Ed Mackey. +# -> http://www.blinken.com/blender-plugins.php +# Partly converted for Blender 2.5 by tuga3d. +# +# Sphere: +# x = sin(2*pi*u)*sin(pi*v) +# y = cos(2*pi*u)*sin(pi*v) +# z = cos(pi*v) +# u_min = v_min = 0 +# u_max = v_max = 1 +# +# "Snail shell" +# x = 1.2**v*(sin(u)**2 *sin(v)) +# y = 1.2**v*(sin(u)*cos(u)) +# z = 1.2**v*(sin(u)**2 *cos(v)) +# u_min = 0 +# u_max = pi +# v_min = -pi/4, +# v max = 5*pi/2 +class AddXYZFunctionSurface(bpy.types.Operator): + '''Add a surface defined defined by 3 functions:''' \ + + ''' x=f(u,v), y=f(u,v) and z=f(u,v)''' + bl_idname = "mesh.primitive_xyz_function_surface" + bl_label = "Add X,Y,Z Function Surface" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + + x_eq = StringProperty(name="X Equation", + description="Equation for x=f(u,v)", + default="1.2**v*(sin(u)**2 *sin(v))") + + y_eq = StringProperty(name="Y Equation", + description="Equation for y=f(u,v)", + default="1.2**v*(sin(u)*cos(u))") + + z_eq = StringProperty(name="Z Equation", + description="Equation for z=f(u,v)", + default="1.2**v*(sin(u)**2 *cos(v))") + + range_u_min = FloatProperty(name="U min", + description="Minimum U value. Lower boundary of U range.", + min=-100.00, + max=0.00, + default=0.00) + + range_u_max = FloatProperty(name="U max", + description="Maximum U value. Upper boundary of U range.", + min=0.00, + max=100.00, + default=pi) + + range_u_step = IntProperty(name="U step", + description="U Subdivisions", + min=1, + max=1024, + default=32) + + wrap_u = BoolProperty(name="U wrap", + description="U Wrap around", + default=True) + + range_v_min = FloatProperty(name="V min", + description="Minimum V value. Lower boundary of V range.", + min=-100.00, + max=0.00, + default=-pi / 4) + + range_v_max = FloatProperty(name="V max", + description="Maximum V value. Upper boundary of V range.", + min=0.00, + max=100.00, + default=5 * pi / 2) + + range_v_step = IntProperty(name="V step", + description="V Subdivisions", + min=1, + max=1024, + default=32) + + wrap_v = BoolProperty(name="V wrap", + description="V Wrap around", + default=False) + + align_matrix = Matrix() + + def execute(self, context): + props = self.properties + + verts, faces = xyz_function_surface_faces( + self, + props.x_eq, + props.y_eq, + props.z_eq, + props.range_u_min, + props.range_u_max, + props.range_u_step, + props.wrap_u, + props.range_v_min, + props.range_v_max, + props.range_v_step, + props.wrap_v) + + if not verts: + return {'CANCELLED'} + + obj = create_mesh_object(context, verts, [], faces, + "XYZ Function", props.edit, self.align_matrix) + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + + +################################ +import space_info + +# Define "3D Function Surface" menu +menu_func_z = (lambda self, context: self.layout.operator( + AddZFunctionSurface.bl_idname, + text="Z Function Surface", + icon="PLUGIN")) +menu_func_xyz = (lambda self, context: self.layout.operator( + AddXYZFunctionSurface.bl_idname, + text="X,Y,Z Function Surface", + icon="PLUGIN")) + + +def register(): + # Register the operators/menus. + bpy.types.register(AddZFunctionSurface) + bpy.types.register(AddXYZFunctionSurface) + + # Add menus to the "Add Mesh" menu + space_info.INFO_MT_mesh_add.append(menu_func_z) + space_info.INFO_MT_mesh_add.append(menu_func_xyz) + + +def unregister(): + # Unregister the operators/menus. + bpy.types.unregister(AddZFunctionSurface) + bpy.types.unregister(AddXYZFunctionSurface) + + # Remove menus from the "Add Mesh" menu. + space_info.INFO_MT_mesh_add.remove(menu_func_z) + space_info.INFO_MT_mesh_add.remove(menu_func_xyz) + +if __name__ == "__main__": + register() diff --git a/add_mesh_extras.py b/add_mesh_extras.py new file mode 100644 index 00000000..ccf74a4a --- /dev/null +++ b/add_mesh_extras.py @@ -0,0 +1,791 @@ +# ##### 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 ##### + +bl_addon_info = { + 'name': 'Add Mesh: Extras', + 'author': 'Pontiac, Fourmadmen, meta-androcto', + 'version': '0.3', + 'blender': (2, 5, 3), + 'location': 'View3D > Add > Mesh > Extras', + 'description': 'Adds Star, Wedge, Sqorus & Spindle objects.', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Add_Mesh/Add_Extra', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=22457&group_id=153&atid=469', + 'category': 'Add Mesh'} + +import bpy +from mathutils import * +from math import * +from bpy.props import * + +# calculates the matrix for the new object +# depending on user pref +def align_matrix(context): + loc = TranslationMatrix(context.scene.cursor_location) + obj_align = context.user_preferences.edit.object_align + if (context.space_data.type == 'VIEW_3D' + and obj_align == 'VIEW'): + rot = context.space_data.region_3d.view_matrix.rotation_part().invert().resize4x4() + else: + rot = Matrix() + align_matrix = loc * rot + return align_matrix + +# Create a new mesh (object) from verts/edges/faces. +# verts/edges/faces ... List of vertices/edges/faces for the +# new mesh (as used in from_pydata). +# name ... Name of the new mesh (& object). +# edit ... Replace existing mesh data. +# Note: Using "edit" will destroy/delete existing mesh data. +def create_mesh_object(context, verts, edges, faces, name, edit, align_matrix): + scene = context.scene + obj_act = scene.objects.active + + # Can't edit anything, unless we have an active obj. + if edit and not obj_act: + return None + + # Create new mesh + mesh = bpy.data.meshes.new(name) + + # Make a mesh from a list of verts/edges/faces. + mesh.from_pydata(verts, edges, faces) + + # Update mesh geometry after adding stuff. + mesh.update() + + # Deselect all objects. + bpy.ops.object.select_all(action='DESELECT') + + if edit: + # Replace geometry of existing object + + # Use the active obj and select it. + ob_new = obj_act + ob_new.select = True + + if obj_act.mode == 'OBJECT': + # Get existing mesh datablock. + old_mesh = ob_new.data + + # Set object data to nothing + ob_new.data = None + + # Clear users of existing mesh datablock. + old_mesh.user_clear() + + # Remove old mesh datablock if no users are left. + if (old_mesh.users == 0): + bpy.data.meshes.remove(old_mesh) + + # Assign new mesh datablock. + ob_new.data = mesh + + else: + # Create new object + ob_new = bpy.data.objects.new(name, mesh) + + # Link new object to the given scene and select it. + scene.objects.link(ob_new) + ob_new.select = True + + # Place the object at the 3D cursor location. + # apply viewRotaion + ob_new.matrix_world = align_matrix + + if obj_act and obj_act.mode == 'EDIT': + if not edit: + # We are in EditMode, switch to ObjectMode. + bpy.ops.object.mode_set(mode='OBJECT') + + # Select the active object as well. + obj_act.select = True + + # Apply location of new object. + scene.update() + + # Join new object into the active. + bpy.ops.object.join() + + # Switching back to EditMode. + bpy.ops.object.mode_set(mode='EDIT') + + ob_new = obj_act + + else: + # We are in ObjectMode. + # Make the new object the active one. + scene.objects.active = ob_new + + return ob_new + + +# A very simple "bridge" tool. +# Connects two equally long vertex rows with faces. +# Returns a list of the new faces (list of lists) +# +# vertIdx1 ... First vertex list (list of vertex indices). +# vertIdx2 ... Second vertex list (list of vertex indices). +# closed ... Creates a loop (first & last are closed). +# flipped ... Invert the normal of the face(s). +# +# Note: You can set vertIdx1 to a single vertex index to create +# a fan/star of faces. +# Note: If both vertex idx list are the same length they have +# to have at least 2 vertices. +def createFaces(vertIdx1, vertIdx2, closed=False, flipped=False): + faces = [] + + if not vertIdx1 or not vertIdx2: + return None + + if len(vertIdx1) < 2 and len(vertIdx2) < 2: + return None + + fan = False + if (len(vertIdx1) != len(vertIdx2)): + if (len(vertIdx1) == 1 and len(vertIdx2) > 1): + fan = True + else: + return None + + total = len(vertIdx2) + + if closed: + # Bridge the start with the end. + if flipped: + face = [ + vertIdx1[0], + vertIdx2[0], + vertIdx2[total - 1]] + if not fan: + face.append(vertIdx1[total - 1]) + faces.append(face) + + else: + face = [vertIdx2[0], vertIdx1[0]] + if not fan: + face.append(vertIdx1[total - 1]) + face.append(vertIdx2[total - 1]) + faces.append(face) + + # Bridge the rest of the faces. + for num in range(total - 1): + if flipped: + if fan: + face = [vertIdx2[num], vertIdx1[0], vertIdx2[num + 1]] + else: + face = [vertIdx2[num], vertIdx1[num], + vertIdx1[num + 1], vertIdx2[num + 1]] + faces.append(face) + else: + if fan: + face = [vertIdx1[0], vertIdx2[num], vertIdx2[num + 1]] + else: + face = [vertIdx1[num], vertIdx2[num], + vertIdx2[num + 1], vertIdx1[num + 1]] + faces.append(face) + + return faces + + +# @todo Clean up vertex&face creation process a bit. +def add_sqorus(hole_size, subdivide): + verts = [] + faces = [] + + size = 2.0 + + thickness = (size - hole_size) / 2.0 + distances = [ + -size / 2.0, + -size / 2.0 + thickness, + size / 2.0 - thickness, + size / 2.0] + + if subdivide: + for i in range(4): + y = distances[i] + + for j in range(4): + x = distances[j] + + verts.append(Vector((x, y, size / 2.0))) + verts.append(Vector((x, y, -size / 2.0))) + + # Top outer loop (vertex indices) + vIdx_out_up = [0, 2, 4, 6, 14, 22, 30, 28, 26, 24, 16, 8] + # Lower outer loop (vertex indices) + vIdx_out_low = [i + 1 for i in vIdx_out_up] + + faces_outside = createFaces(vIdx_out_up, vIdx_out_low, closed=True) + faces.extend(faces_outside) + + # Top inner loop (vertex indices) + vIdx_inner_up = [10, 12, 20, 18] + + # Lower inner loop (vertex indices) + vIdx_inner_low = [i + 1 for i in vIdx_inner_up] + + faces_inside = createFaces(vIdx_inner_up, vIdx_inner_low, + closed=True, flipped=True) + faces.extend(faces_inside) + + row1_top = [0, 8, 16, 24] + row2_top = [i + 2 for i in row1_top] + row3_top = [i + 2 for i in row2_top] + row4_top = [i + 2 for i in row3_top] + + faces_top1 = createFaces(row1_top, row2_top) + faces.extend(faces_top1) + faces_top2_side1 = createFaces(row2_top[:2], row3_top[:2]) + faces.extend(faces_top2_side1) + faces_top2_side2 = createFaces(row2_top[2:], row3_top[2:]) + faces.extend(faces_top2_side2) + faces_top3 = createFaces(row3_top, row4_top) + faces.extend(faces_top3) + + row1_bot = [1, 9, 17, 25] + row2_bot = [i + 2 for i in row1_bot] + row3_bot = [i + 2 for i in row2_bot] + row4_bot = [i + 2 for i in row3_bot] + + faces_bot1 = createFaces(row1_bot, row2_bot, flipped=True) + faces.extend(faces_bot1) + faces_bot2_side1 = createFaces(row2_bot[:2], row3_bot[:2], + flipped=True) + faces.extend(faces_bot2_side1) + faces_bot2_side2 = createFaces(row2_bot[2:], row3_bot[2:], + flipped=True) + faces.extend(faces_bot2_side2) + faces_bot3 = createFaces(row3_bot, row4_bot, flipped=True) + faces.extend(faces_bot3) + + else: + # Do not subdivde outer faces + + vIdx_out_up = [] + vIdx_out_low = [] + vIdx_in_up = [] + vIdx_in_low = [] + + for i in range(4): + y = distances[i] + + for j in range(4): + x = distances[j] + + append = False + inner = False + # Outer + if (i in [0, 3] and j in [0, 3]): + append = True + + # Inner + if (i in [1, 2] and j in [1, 2]): + append = True + inner = True + + if append: + vert_up = len(verts) + verts.append(Vector((x, y, size / 2.0))) + vert_low = len(verts) + verts.append(Vector((x, y, -size / 2.0))) + + if inner: + vIdx_in_up.append(vert_up) + vIdx_in_low.append(vert_low) + + else: + vIdx_out_up.append(vert_up) + vIdx_out_low.append(vert_low) + + # Flip last two vertices + vIdx_out_up = vIdx_out_up[:2] + list(reversed(vIdx_out_up[2:])) + vIdx_out_low = vIdx_out_low[:2] + list(reversed(vIdx_out_low[2:])) + vIdx_in_up = vIdx_in_up[:2] + list(reversed(vIdx_in_up[2:])) + vIdx_in_low = vIdx_in_low[:2] + list(reversed(vIdx_in_low[2:])) + + # Create faces + faces_top = createFaces(vIdx_in_up, vIdx_out_up, closed=True) + faces.extend(faces_top) + faces_bottom = createFaces(vIdx_out_low, vIdx_in_low, closed=True) + faces.extend(faces_bottom) + faces_inside = createFaces(vIdx_in_low, vIdx_in_up, closed=True) + faces.extend(faces_inside) + faces_outside = createFaces(vIdx_out_up, vIdx_out_low, closed=True) + faces.extend(faces_outside) + + return verts, faces + + +def add_wedge(size_x, size_y, size_z): + verts = [] + faces = [] + + size_x /= 2.0 + size_y /= 2.0 + size_z /= 2.0 + + vIdx_top = [] + vIdx_bot = [] + + vIdx_top.append(len(verts)) + verts.append(Vector((-size_x, -size_y, size_z))) + vIdx_bot.append(len(verts)) + verts.append(Vector((-size_x, -size_y, -size_z))) + + vIdx_top.append(len(verts)) + verts.append(Vector((size_x, -size_y, size_z))) + vIdx_bot.append(len(verts)) + verts.append(Vector((size_x, -size_y, -size_z))) + + vIdx_top.append(len(verts)) + verts.append(Vector((-size_x, size_y, size_z))) + vIdx_bot.append(len(verts)) + verts.append(Vector((-size_x, size_y, -size_z))) + + faces.append(vIdx_top) + faces.append(vIdx_bot) + faces_outside = createFaces(vIdx_top, vIdx_bot, closed=True) + faces.extend(faces_outside) + + return verts, faces + +def add_spindle(segments, radius, height, cap_height): + verts = [] + faces = [] + + tot_verts = segments * 2 + 2 + + half_height = height / 2.0 + + # Upper tip + idx_upper_tip = len(verts) + verts.append(Vector((0, 0, half_height + cap_height))) + + # Lower tip + idx_lower_tip = len(verts) + verts.append(Vector((0.0, 0.0, -half_height - cap_height))) + + upper_edgeloop = [] + lower_edgeloop = [] + for index in range(segments): + mtx = RotationMatrix(2.0 * pi * float(index) / segments, 3, 'Z') + + # Calculate index & location of upper verte4x tip. + idx_up = len(verts) + upper_edgeloop.append(idx_up) + verts.append(Vector((radius, 0.0, half_height)) * mtx) + + if height > 0: + idx_low = len(verts) + lower_edgeloop.append(idx_low) + verts.append(Vector((radius, 0.0, -half_height)) * mtx) + + # Create faces for the upper tip. + tip_up_faces = createFaces([idx_upper_tip], upper_edgeloop, + closed=True, flipped=True) + faces.extend(tip_up_faces) + + if height > 0: + # Create faces for the middle cylinder. + cyl_faces = createFaces(lower_edgeloop, upper_edgeloop, closed=True) + faces.extend(cyl_faces) + + # Create faces for the lower tip. + tip_low_faces = createFaces([idx_lower_tip], lower_edgeloop, + closed=True) + faces.extend(tip_low_faces) + + else: + # Skipping middle part/cylinder (height=0). + + # Create faces for the lower tip. + tip_low_faces = createFaces([idx_lower_tip], upper_edgeloop, + closed=True) + faces.extend(tip_low_faces) + + return verts, faces + +def add_star(points, outer_radius, inner_radius, height): + PI_2 = pi * 2 + z_axis = (0, 0, 1) + + verts = [] + faces = [] + + segments = points * 2 + + half_height = height / 2.0 + + vert_idx_top = len(verts) + verts.append(Vector((0.0, 0.0, half_height))) + + vert_idx_bottom = len(verts) + verts.append(Vector((0.0, 0.0, -half_height))) + + edgeloop_top = [] + edgeloop_bottom = [] + + for index in range(segments): + quat = Quaternion(z_axis, (index / segments) * PI_2) + + if index % 2: + # Uneven + radius = outer_radius + else: + # Even + radius = inner_radius + + edgeloop_top.append(len(verts)) + vec = Vector((radius, 0, half_height)) * quat + verts.append(vec) + + edgeloop_bottom.append(len(verts)) + vec = Vector((radius, 0, -half_height)) * quat + verts.append(vec) + + + + faces_top = createFaces([vert_idx_top], edgeloop_top, closed=True) + faces_outside = createFaces(edgeloop_top, edgeloop_bottom, closed=True) + faces_bottom = createFaces([vert_idx_bottom], edgeloop_bottom, + flipped=True, closed=True) + + faces.extend(faces_top) + faces.extend(faces_outside) + faces.extend(faces_bottom) + + return verts, faces + +def trapezohedron(s,r,h): + """ + s = segments + r = base radius + h = tip height + """ + + # calculate constants + a = 2*pi/(2*s) # angle between points along the equator + l = r*cos(a) # helper for e + e = h*(r-l)/(l+r) # the z offset for each vector along the equator so faces are planar + + # rotation for the points + quat = Quaternion((0,0,1),a) + + # first 3 vectors, every next one is calculated from the last, and the z-value is negated + verts = [Vector(i) for i in [(0,0,h),(0,0,-h),(r,0,e)]] + for i in range(2*s-1): + verts.append(verts[-1]*quat) # rotate further "a" radians around the z-axis + verts[-1].z *= -1 # negate last z-value to account for the zigzag + + faces = [] + for i in range(2,2+2*s,2): + n = [i+1,i+2,i+3] # vertices in current section + for j in range(3): # check whether the numbers dont go over len(verts) + if n[j]>=2*s+2: n[j]-=2*s # if so, subtract len(verts)-2 + + # add faces of current section + faces.append([0,i]+n[:2]) + faces.append([1,n[2],n[1],n[0]]) + + return verts,faces + +class AddSqorus(bpy.types.Operator): + '''Add a sqorus mesh.''' + bl_idname = "mesh.primitive_sqorus_add" + bl_label = "Add Sqorus" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + hole_size = FloatProperty(name="Hole Size", + description="Size of the Hole", + min=0.01, + max=1.99, + default=2.0 / 3.0) + subdivide = BoolProperty(name="Subdivide Outside", + description="Enable to subdivide the faces on the outside." \ + " This results in equally spaced vertices.", + default=True) + align_matrix = Matrix() + + def execute(self, context): + props = self.properties + + # Create mesh geometry + verts, faces = add_sqorus( + props.hole_size, + props.subdivide) + + # Create mesh object (and meshdata) + obj = create_mesh_object(context, verts, [], faces, "Sqorus", + props.edit, self.align_matrix) + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +class AddWedge(bpy.types.Operator): + '''Add a wedge mesh.''' + bl_idname = "mesh.primitive_wedge_add" + bl_label = "Add Wedge" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + size_x = FloatProperty(name="Size X", + description="Size along the X axis", + min=0.01, + max=9999.0, + default=2.0) + size_y = FloatProperty(name="Size Y", + description="Size along the Y axis", + min=0.01, + max=9999.0, + default=2.0) + size_z = FloatProperty(name="Size Z", + description="Size along the Z axis", + min=0.01, + max=9999.0, + default=2.00) + align_matrix = Matrix() + + def execute(self, context): + props = self.properties + + verts, faces = add_wedge( + props.size_x, + props.size_y, + props.size_z) + + obj = create_mesh_object(context, verts, [], faces, "Wedge", + props.edit, self.align_matrix) + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +class AddSpindle(bpy.types.Operator): + '''Add a spindle mesh.''' + bl_idname = "mesh.primitive_spindle_add" + bl_label = "Add Spindle" + bl_description = "Create a spindle mesh." + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + segments = IntProperty(name="Segments", + description="Number of segments of the spindle", + min=3, + max=512, + default=32) + radius = FloatProperty(name="Radius", + description="Radius of the spindle", + min=0.01, + max=9999.0, + default=1.0) + height = FloatProperty(name="Height", + description="Height of the spindle", + min=0.0, + max=100.0, + default=1.0) + cap_height = FloatProperty(name="Cap Height", + description="Cap height of the spindle", + min=-9999.0, + max=9999.0, + default=0.5) + align_matrix = Matrix() + + def execute(self, context): + props = self.properties + + verts, faces = add_spindle( + props.segments, + props.radius, + props.height, + props.cap_height) + + obj = create_mesh_object(context, verts, [], faces, "Spindle", + props.edit, self.align_matrix) + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +class AddStar(bpy.types.Operator): + '''Add a star mesh.''' + bl_idname = "mesh.primitive_star_add" + bl_label = "Add Star" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + points = IntProperty(name="Points", + description="Number of points for the star", + min=2, + max=256, + default=5) + outer_radius = FloatProperty(name="Outer Radius", + description="Outer radius of the star", + min=0.01, + max=9999.0, + default=1.0) + innter_radius = FloatProperty(name="Inner Radius", + description="Inner radius of the star", + min=0.01, + max=9999.0, + default=0.5) + height = FloatProperty(name="Height", + description="Height of the star", + min=0.01, + max=9999.0, + default=0.5) + align_matrix = Matrix() + + def execute(self, context): + props = self.properties + + verts, faces = add_star( + props.points, + props.outer_radius, + props.innter_radius, + props.height) + + obj = create_mesh_object(context, verts, [], faces, "Star", + props.edit, self.align_matrix) + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +class AddTrapezohedron(bpy.types.Operator): + """Add a trapezohedron""" + bl_idname = "mesh.primitive_trapezohedron_add" + bl_label = "Add trapezohedron" + bl_description = "Create one of the regular solids" + bl_options = {'REGISTER', 'UNDO'} + + segments = IntProperty(name = "Segments", + description = "Number of repeated segments", + default = 4, min = 2, max = 256) + radius = FloatProperty(name = "Base radius", + description = "Radius of the middle", + default = 1.0, min = 0.01, max = 100.0) + height = FloatProperty(name = "Tip height", + description = "Height of the tip", + default = 1, min = 0.01, max = 100.0) + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + align_matrix = Matrix() + def execute(self,context): + props = self.properties + # generate mesh + verts,faces = trapezohedron(props.segments, + props.radius, + props.height) + + obj = create_mesh_object(context, verts, [], faces, "Trapazohedron", + props.edit, self.align_matrix) + + return {'FINISHED'} + +class INFO_MT_mesh_extras_add(bpy.types.Menu): + # Define the "Extras" menu + bl_idname = "INFO_MT_mesh_extras_add" + bl_label = "Extras" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + layout.operator("mesh.primitive_sqorus_add", + text="Sqorus") + layout.operator("mesh.primitive_wedge_add", + text="Wedge") + layout.operator("mesh.primitive_spindle_add", + text="Spindle") + layout.operator("mesh.primitive_star_add", + text="Star") + layout.operator("mesh.primitive_trapezohedron_add", + text="Trapezohedron") + + +# Register all operators and panels +import space_info + +# Define "Gemstones" menu +menu_func = (lambda self, + context: self.layout.menu("INFO_MT_mesh_extras_add", icon="PLUGIN")) + + +def register(): + # Register the operators/menus. + bpy.types.register(AddSqorus) + bpy.types.register(AddWedge) + bpy.types.register(AddSpindle) + bpy.types.register(AddStar) + bpy.types.register(AddTrapezohedron) + bpy.types.register(INFO_MT_mesh_extras_add) + + # Add "Gemstones" menu to the "Add Mesh" menu + space_info.INFO_MT_mesh_add.append(menu_func) + + +def unregister(): + # Unregister the operators/menus. + bpy.types.unregister(AddSqorus) + bpy.types.unregister(AddWedge) + bpy.types.unregister(AddSpindle) + bpy.types.unregister(AddStar) + bpy.types.unregister(AddTrapezohedron) + bpy.types.unregister(INFO_MT_mesh_extras_add) + + # Remove "Gemstones" menu from the "Add Mesh" menu. + space_info.INFO_MT_mesh_add.remove(menu_func) + +if __name__ == "__main__": + register() diff --git a/add_mesh_gears.py b/add_mesh_gears.py new file mode 100644 index 00000000..92d4e125 --- /dev/null +++ b/add_mesh_gears.py @@ -0,0 +1,949 @@ +# add_mesh_gear.py (c) 2009, 2010 Michel J. Anders (varkenvarken) +# +# ***** 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +""" +What was needed to port it from 2.49 -> 2.50 alpha 0? + +The basic functions that calculate the geometry (verts and faces) are mostly +unchanged (add_tooth, add_spoke, add_gear) + +Also, the vertex group API is changed a little bit but the concepts +are the same: +========= +vertexgroup = ob.add_vertex_group('NAME_OF_VERTEXGROUP') +for i in vertexgroup_vertex_indices: + ob.add_vertex_to_group(i, vertexgroup, weight, 'ADD') +========= + +Now for some reason the name does not 'stick' and we have to set it this way: +vertexgroup.name = 'NAME_OF_VERTEXGROUP' + +Conversion to 2.50 also meant we could simply do away with our crude user +interface. +Just definining the appropriate properties in the AddGear() operator will +display the properties in the Blender GUI with the added benefit of making +it interactive: changing a property will redo the AddGear() operator providing +the user with instant feedback. + +Finally we had to convert/throw away some print statements to print functions +as Blender nows uses Python 3.x + +The code to actually implement the AddGear() function is mostly copied from +add_mesh_torus() (distributed with Blender). +""" + +bl_addon_info = { + 'name': 'Add Mesh: Gears', + 'author': 'Michel J. Anders (varkenvarken)', + 'version': '2.4.1', + 'blender': (2, 5, 3), + 'location': 'View3D > Add > Mesh > Gears ', + 'description': 'Adds a mesh Gear to the Add Mesh menu', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Add_Mesh/Add_Gear', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=21732&group_id=153&atid=469', + 'category': 'Add Mesh'} + +import bpy +import mathutils +from math import * +from bpy.props import * + +# calculates the matrix for the new object +# depending on user pref +def align_matrix(context): + loc = mathutils.TranslationMatrix(context.scene.cursor_location) + obj_align = context.user_preferences.edit.object_align + if (context.space_data.type == 'VIEW_3D' + and obj_align == 'VIEW'): + rot = context.space_data.region_3d.view_matrix.rotation_part().invert().resize4x4() + else: + rot = mathutils.Matrix() + align_matrix = loc * rot + return align_matrix + +# Create a new mesh (object) from verts/edges/faces. +# verts/edges/faces ... List of vertices/edges/faces for the +# new mesh (as used in from_pydata). +# name ... Name of the new mesh (& object). +# edit ... Replace existing mesh data. +# Note: Using "edit" will destroy/delete existing mesh data. +def create_mesh_object(context, verts, edges, faces, name, edit, align_matrix): + scene = context.scene + obj_act = scene.objects.active + + # Can't edit anything, unless we have an active obj. + if edit and not obj_act: + return None + + # Create new mesh + mesh = bpy.data.meshes.new(name) + + # Make a mesh from a list of verts/edges/faces. + mesh.from_pydata(verts, edges, faces) + + # Update mesh geometry after adding stuff. + mesh.update() + + # Deselect all objects. + bpy.ops.object.select_all(action='DESELECT') + + if edit: + # Replace geometry of existing object + + # Use the active obj and select it. + ob_new = obj_act + ob_new.select = True + + if obj_act.mode == 'OBJECT': + # Get existing mesh datablock. + old_mesh = ob_new.data + + # Set object data to nothing + ob_new.data = None + + # Clear users of existing mesh datablock. + old_mesh.user_clear() + + # Remove old mesh datablock if no users are left. + if (old_mesh.users == 0): + bpy.data.meshes.remove(old_mesh) + + # Assign new mesh datablock. + ob_new.data = mesh + + else: + # Create new object + ob_new = bpy.data.objects.new(name, mesh) + + # Link new object to the given scene and select it. + scene.objects.link(ob_new) + ob_new.select = True + + # Place the object at the 3D cursor location. + # apply viewRotaion + ob_new.matrix_world = align_matrix + + + if obj_act and obj_act.mode == 'EDIT': + if not edit: + # We are in EditMode, switch to ObjectMode. + bpy.ops.object.mode_set(mode='OBJECT') + + # Select the active object as well. + obj_act.select = True + + # Apply location of new object. + scene.update() + + # Join new object into the active. + bpy.ops.object.join() + + # Switching back to EditMode. + bpy.ops.object.mode_set(mode='EDIT') + + ob_new = obj_act + + else: + # We are in ObjectMode. + # Make the new object the active one. + scene.objects.active = ob_new + + return ob_new + + +# A very simple "bridge" tool. +# Connects two equally long vertex rows with faces. +# Returns a list of the new faces (list of lists) +# +# vertIdx1 ... First vertex list (list of vertex indices). +# vertIdx2 ... Second vertex list (list of vertex indices). +# closed ... Creates a loop (first & last are closed). +# flipped ... Invert the normal of the face(s). +# +# Note: You can set vertIdx1 to a single vertex index to create +# a fan/star of faces. +# Note: If both vertex idx list are the same length they have +# to have at least 2 vertices. +def createFaces(vertIdx1, vertIdx2, closed=False, flipped=False): + faces = [] + + if not vertIdx1 or not vertIdx2: + return None + + if len(vertIdx1) < 2 and len(vertIdx2) < 2: + return None + + fan = False + if (len(vertIdx1) != len(vertIdx2)): + if (len(vertIdx1) == 1 and len(vertIdx2) > 1): + fan = True + else: + return None + + total = len(vertIdx2) + + if closed: + # Bridge the start with the end. + if flipped: + face = [ + vertIdx1[0], + vertIdx2[0], + vertIdx2[total - 1]] + if not fan: + face.append(vertIdx1[total - 1]) + faces.append(face) + + else: + face = [vertIdx2[0], vertIdx1[0]] + if not fan: + face.append(vertIdx1[total - 1]) + face.append(vertIdx2[total - 1]) + faces.append(face) + + # Bridge the rest of the faces. + for num in range(total - 1): + if flipped: + if fan: + face = [vertIdx2[num], vertIdx1[0], vertIdx2[num + 1]] + else: + face = [vertIdx2[num], vertIdx1[num], + vertIdx1[num + 1], vertIdx2[num + 1]] + faces.append(face) + else: + if fan: + face = [vertIdx1[0], vertIdx2[num], vertIdx2[num + 1]] + else: + face = [vertIdx1[num], vertIdx2[num], + vertIdx2[num + 1], vertIdx1[num + 1]] + faces.append(face) + + return faces + + +# Calculate the vertex coordinates for a single +# section of a gear tooth. +# Returns 4 lists of vertex coords (list of tuples): +# *-*---*---* (1.) verts_inner_base +# | | | | +# *-*---*---* (2.) verts_outer_base +# | | | +# *---*---* (3.) verts_middle_tooth +# \ | / +# *-*-* (4.) verts_tip_tooth +# +# a +# t +# d +# radius +# Ad +# De +# base +# p_angle +# rack +# crown +def add_tooth(a, t, d, radius, Ad, De, base, p_angle, rack=0, crown=0.0): + A = [a, a + t / 4, a + t / 2, a + 3 * t / 4] + C = [cos(i) for i in A] + S = [sin(i) for i in A] + + Ra = radius + Ad + Rd = radius - De + Rb = Rd - base + + # Pressure angle calc + O = Ad * tan(p_angle) + p_angle = atan(O / Ra) + + if radius < 0: + p_angle = -p_angle + + if rack: + S = [sin(t / 4) * I for I in range(-2, 3)] + Sp = [0, sin(-t / 4 + p_angle), 0, sin(t / 4 - p_angle)] + + verts_inner_base = [(Rb, radius * S[I], d) for I in range(4)] + verts_outer_base = [(Rd, radius * S[I], d) for I in range(4)] + verts_middle_tooth = [(radius, radius * S[I], d) for I in range(1, 4)] + verts_tip_tooth = [(Ra, radius * Sp[I], d) for I in range(1, 4)] + + else: + Cp = [ + 0, + cos(a + t / 4 + p_angle), + cos(a + t / 2), + cos(a + 3 * t / 4 - p_angle)] + Sp = [0, + sin(a + t / 4 + p_angle), + sin(a + t / 2), + sin(a + 3 * t / 4 - p_angle)] + + verts_inner_base = [(Rb * C[I], Rb * S[I], d) + for I in range(4)] + verts_outer_base = [(Rd * C[I], Rd * S[I], d) + for I in range(4)] + verts_middle_tooth = [(radius * C[I], radius * S[I], d + crown / 3) + for I in range(1, 4)] + verts_tip_tooth = [(Ra * Cp[I], Ra * Sp[I], d + crown) + for I in range(1, 4)] + + return (verts_inner_base, verts_outer_base, + verts_middle_tooth, verts_tip_tooth) + + +# EXPERIMENTAL Calculate the vertex coordinates for a single +# section of a gearspoke. +# Returns them as a list of tuples. +# +# a +# t +# d +# radius +# De +# base +# s +# w +# l +# gap +# width +# +# @todo Finish this. +def add_spoke(a, t, d, radius, De, base, s, w, l, gap=0, width=19): + Rd = radius - De + Rb = Rd - base + Rl = Rb + + verts = [] + edgefaces = [] + edgefaces2 = [] + sf = [] + + if not gap: + for N in range(width, 1, -2): + edgefaces.append(len(verts)) + ts = t / 4 + tm = a + 2 * ts + te = asin(w / Rb) + td = te - ts + t4 = ts + td * (width - N) / (width - 3.0) + A = [tm + (i - int(N / 2)) * t4 for i in range(N)] + C = [cos(i) for i in A] + S = [sin(i) for i in A] + + verts.extend([(Rb * I, Rb * J, d) for (I, J) in zip(C, S)]) + edgefaces2.append(len(verts) - 1) + + Rb = Rb - s + + n = 0 + for N in range(width, 3, -2): + sf.extend([(i + n, i + 1 + n, i + 2 + n, i + N + n) + for i in range(0, N - 1, 2)]) + sf.extend([(i + 2 + n, i + N + n, i + N + 1 + n, i + N + 2 + n) + for i in range(0, N - 3, 2)]) + + n = n + N + + return verts, edgefaces, edgefaces2, sf + + +# Create gear geometry. +# Returns: +# * A list of vertices (list of tuples) +# * A list of faces (list of lists) +# * A list (group) of vertices of the tip (list of vertex indices). +# * A list (group) of vertices of the valley (list of vertex indices). +# +# teethNum ... Number of teeth on the gear. +# radius ... Radius of the gear, negative for crown gear +# Ad ... Addendum, extent of tooth above radius. +# De ... Dedendum, extent of tooth below radius. +# base ... Base, extent of gear below radius. +# p_angle ... Pressure angle. Skewness of tooth tip. (radiant) +# width ... Width, thickness of gear. +# skew ... Skew of teeth. (radiant) +# conangle ... Conical angle of gear. (radiant) +# rack +# crown ... Inward pointing extend of crown teeth. +# +# inner radius = radius - (De + base) +def add_gear(teethNum, radius, Ad, De, base, p_angle, + width=1, skew=0, conangle=0, rack=0, crown=0.0): + + if teethNum < 2: + return None, None, None, None + + t = 2 * pi / teethNum + + if rack: + teethNum = 1 + + scale = (radius - 2 * width * tan(conangle)) / radius + + verts = [] + faces = [] + vgroup_top = [] # Vertex group of top/tip? vertices. + vgroup_valley = [] # Vertex group of valley vertices + + verts_bridge_prev = [] + for toothCnt in range(teethNum): + a = toothCnt * t + + verts_bridge_start = [] + verts_bridge_end = [] + + verts_outside_top = [] + verts_outside_bottom = [] + for (s, d, c, top) \ + in [(0, -width, 1, True), \ + (skew, width, scale, False)]: + + verts1, verts2, verts3, verts4 = add_tooth(a + s, t, d, + radius * c, Ad * c, De * c, base * c, p_angle, + rack, crown) + + vertsIdx1 = list(range(len(verts), len(verts) + len(verts1))) + verts.extend(verts1) + vertsIdx2 = list(range(len(verts), len(verts) + len(verts2))) + verts.extend(verts2) + vertsIdx3 = list(range(len(verts), len(verts) + len(verts3))) + verts.extend(verts3) + vertsIdx4 = list(range(len(verts), len(verts) + len(verts4))) + verts.extend(verts4) + + verts_outside = [] + verts_outside.extend(vertsIdx2[:2]) + verts_outside.append(vertsIdx3[0]) + verts_outside.extend(vertsIdx4) + verts_outside.append(vertsIdx3[-1]) + verts_outside.append(vertsIdx2[-1]) + + if top: + #verts_inside_top = vertsIdx1 + verts_outside_top = verts_outside + + verts_bridge_start.append(vertsIdx1[0]) + verts_bridge_start.append(vertsIdx2[0]) + verts_bridge_end.append(vertsIdx1[-1]) + verts_bridge_end.append(vertsIdx2[-1]) + + else: + #verts_inside_bottom = vertsIdx1 + verts_outside_bottom = verts_outside + + verts_bridge_start.append(vertsIdx2[0]) + verts_bridge_start.append(vertsIdx1[0]) + verts_bridge_end.append(vertsIdx2[-1]) + verts_bridge_end.append(vertsIdx1[-1]) + + # Valley = first 2 vertices of outer base: + vgroup_valley.extend(vertsIdx2[:1]) + # Top/tip vertices: + vgroup_top.extend(vertsIdx4) + + faces_tooth_middle_top = createFaces(vertsIdx2[1:], vertsIdx3, + flipped=top) + faces_tooth_outer_top = createFaces(vertsIdx3, vertsIdx4, + flipped=top) + + faces_base_top = createFaces(vertsIdx1, vertsIdx2, flipped=top) + faces.extend(faces_base_top) + + faces.extend(faces_tooth_middle_top) + faces.extend(faces_tooth_outer_top) + + #faces_inside = createFaces(verts_inside_top, verts_inside_bottom) + #faces.extend(faces_inside) + + faces_outside = createFaces(verts_outside_top, verts_outside_bottom, + flipped=True) + faces.extend(faces_outside) + + if toothCnt == 0: + verts_bridge_first = verts_bridge_start + + # Bridge one tooth to the next + if verts_bridge_prev: + faces_bridge = createFaces(verts_bridge_prev, verts_bridge_start) + #, closed=True (for "inside" faces) + faces.extend(faces_bridge) + + # Remember "end" vertices for next tooth. + verts_bridge_prev = verts_bridge_end + + # Bridge the first to the last tooth. + faces_bridge_f_l = createFaces(verts_bridge_prev, verts_bridge_first) + #, closed=True (for "inside" faces) + faces.extend(faces_bridge_f_l) + + return verts, faces, vgroup_top, vgroup_valley + + +# Create spokes geometry. +# Returns: +# * A list of vertices (list of tuples) +# * A list of faces (list of lists) +# +# teethNum ... Number of teeth on the gear. +# radius ... Radius of the gear, negative for crown gear +# De ... Dedendum, extent of tooth below radius. +# base ... Base, extent of gear below radius. +# width ... Width, thickness of gear. +# conangle ... Conical angle of gear. (radiant) +# rack +# spoke +# spbevel +# spwidth +# splength +# spresol +# +# @todo Finish this +# @todo Create a function that takes a "Gear" and creates a +# matching "Gear Spokes" object. +def add_spokes(teethNum, radius, De, base, width=1, conangle=0, rack=0, + spoke=3, spbevel=0.1, spwidth=0.2, splength=1.0, spresol=9): + + if teethNum < 2: + return None, None, None, None + + if spoke < 2: + return None, None, None, None + + t = 2 * pi / teethNum + + if rack: + teethNum = 1 + + scale = (radius - 2 * width * tan(conangle)) / radius + + verts = [] + faces = [] + + c = scale # debug + + fl = len(verts) + for toothCnt in range(teethNum): + a = toothCnt * t + s = 0 # For test + + if toothCnt % spoke == 0: + for d in (-width, width): + sv, edgefaces, edgefaces2, sf = add_spoke(a + s, t, d, + radius * c, De * c, base * c, + spbevel, spwidth, splength, 0, spresol) + verts.extend(sv) + faces.extend([[j + fl for j in i] for i in sf]) + fl += len(sv) + + d1 = fl - len(sv) + d2 = fl - 2 * len(sv) + + faces.extend([(i + d2, j + d2, j + d1, i + d1) + for (i, j) in zip(edgefaces[:-1], edgefaces[1:])]) + faces.extend([(i + d2, j + d2, j + d1, i + d1) + for (i, j) in zip(edgefaces2[:-1], edgefaces2[1:])]) + + else: + for d in (-width, width): + sv, edgefaces, edgefaces2, sf = add_spoke(a + s, t, d, + radius * c, De * c, base * c, + spbevel, spwidth, splength, 1, spresol) + + verts.extend(sv) + fl += len(sv) + + d1 = fl - len(sv) + d2 = fl - 2 * len(sv) + + faces.extend([[i + d2, i + 1 + d2, i + 1 + d1, i + d1] + for (i) in range(0, 3)]) + faces.extend([[i + d2, i + 1 + d2, i + 1 + d1, i + d1] + for (i) in range(5, 8)]) + + return verts, faces + + +# Create worm geometry. +# Returns: +# * A list of vertices +# * A list of faces +# * A list (group) of vertices of the tip +# * A list (group) of vertices of the valley +# +# teethNum ... Number of teeth on the worm +# radius ... Radius of the gear, negative for crown gear +# Ad ... Addendum, extent of tooth above radius. +# De ... Dedendum, extent of tooth below radius. +# p_angle ... Pressure angle. Skewness of tooth tip. (radiant) +# width ... Width, thickness of gear. +# crown ... Inward pointing extend of crown teeth. +# +# @todo: Fix teethNum. Some numbers are not possible yet. +# @todo: Create start & end geoemtry (closing faces) +def add_worm(teethNum, rowNum, radius, Ad, De, p_angle, + width=1, skew=radians(11.25), crown=0.0): + + worm = teethNum + teethNum = 24 + + t = 2 * pi / teethNum + + verts = [] + faces = [] + vgroup_top = [] # Vertex group of top/tip? vertices. + vgroup_valley = [] # Vertex group of valley vertices + + #width = width / 2.0 + + edgeloop_prev = [] + for Row in range(rowNum): + edgeloop = [] + + for toothCnt in range(teethNum): + a = toothCnt * t + + s = Row * skew + d = Row * width + c = 1 + + isTooth = False + if toothCnt % (teethNum / worm) != 0: + # Flat + verts1, verts2, verts3, verts4 = add_tooth(a + s, t, d, + radius - De, 0.0, 0.0, 0, p_angle) + + # Ignore other verts than the "other base". + verts1 = verts3 = verts4 = [] + + else: + # Tooth + isTooth = True + verts1, verts2, verts3, verts4 = add_tooth(a + s, t, d, + radius * c, Ad * c, De * c, 0 * c, p_angle, 0, crown) + + # Remove various unneeded verts (if we are "inside" the tooth) + del(verts2[2]) # Central vertex in the base of the tooth. + del(verts3[1]) # Central vertex in the middle of the tooth. + + vertsIdx2 = list(range(len(verts), len(verts) + len(verts2))) + verts.extend(verts2) + vertsIdx3 = list(range(len(verts), len(verts) + len(verts3))) + verts.extend(verts3) + vertsIdx4 = list(range(len(verts), len(verts) + len(verts4))) + verts.extend(verts4) + + if isTooth: + verts_current = [] + verts_current.extend(vertsIdx2[:2]) + verts_current.append(vertsIdx3[0]) + verts_current.extend(vertsIdx4) + verts_current.append(vertsIdx3[-1]) + verts_current.append(vertsIdx2[-1]) + + # Valley = first 2 vertices of outer base: + vgroup_valley.extend(vertsIdx2[:1]) + # Top/tip vertices: + vgroup_top.extend(vertsIdx4) + + else: + # Flat + verts_current = vertsIdx2 + + # Valley - all of them. + vgroup_valley.extend(vertsIdx2) + + edgeloop.extend(verts_current) + + # Create faces between rings/rows. + if edgeloop_prev: + faces_row = createFaces(edgeloop, edgeloop_prev, closed=True) + faces.extend(faces_row) + + # Remember last ring/row of vertices for next ring/row iteration. + edgeloop_prev = edgeloop + + return verts, faces, vgroup_top, vgroup_valley + + +class AddGear(bpy.types.Operator): + '''Add a gear mesh.''' + bl_idname = "mesh.primitive_gear" + bl_label = "Add Gear" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + number_of_teeth = IntProperty(name="Number of Teeth", + description="Number of teeth on the gear", + min=2, + max=265, + default=12) + radius = FloatProperty(name="Radius", + description="Radius of the gear, negative for crown gear", + min=-100.0, + max=100.0, + default=1.0) + addendum = FloatProperty(name="Addendum", + description="Addendum, extent of tooth above radius", + min=0.01, + max=100.0, + default=0.1) + dedendum = FloatProperty(name="Dedendum", + description="Dedendum, extent of tooth below radius", + min=0.0, + max=100.0, + default=0.1) + angle = FloatProperty(name="Pressure Angle", + description="Pressure angle, skewness of tooth tip (degrees)", + min=0.0, + max=45.0, + default=20.0) + base = FloatProperty(name="Base", + description="Base, extent of gear below radius", + min=0.0, + max=100.0, + default=0.2) + width = FloatProperty(name="Width", + description="Width, thickness of gear", + min=0.05, + max=100.0, + default=0.2) + skew = FloatProperty(name="Skewness", + description="Skew of teeth (degrees)", + min=-90.0, + max=90.0, + default=0.0) + conangle = FloatProperty(name="Conical angle", + description="Conical angle of gear (degrees)", + min=0.0, + max=90.0, + default=0.0) + crown = FloatProperty(name="Crown", + description="Inward pointing extend of crown teeth", + min=0.0, + max=100.0, + default=0.0) + align_matrix = mathutils.Matrix() + + def draw(self, context): + props = self.properties + layout = self.layout + box = layout.box() + box.prop(props, 'number_of_teeth') + box = layout.box() + box.prop(props, 'radius') + box.prop(props, 'width') + box.prop(props, 'base') + box = layout.box() + box.prop(props, 'dedendum') + box.prop(props, 'addendum') + box = layout.box() + box.prop(props, 'angle') + box.prop(props, 'skew') + box.prop(props, 'conangle') + box.prop(props, 'crown') + + + def execute(self, context): + props = self.properties + + verts, faces, verts_tip, verts_valley = add_gear( + props.number_of_teeth, + props.radius, + props.addendum, + props.dedendum, + props.base, + radians(props.angle), + width=props.width, + skew=radians(props.skew), + conangle=radians(props.conangle), + crown=props.crown) + + # Actually create the mesh object from this geometry data. + obj = create_mesh_object(context, verts, [], faces, "Gear", props.edit, self.align_matrix) + + # Create vertex groups from stored vertices. + tipGroup = obj.add_vertex_group('Tips') + for vert in verts_tip: + obj.add_vertex_to_group(vert, tipGroup, 1.0, 'ADD') + + valleyGroup = obj.add_vertex_group('Valleys') + for vert in verts_valley: + obj.add_vertex_to_group(vert, valleyGroup, 1.0, 'ADD') + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +class AddWormGear(bpy.types.Operator): + '''Add a worm gear mesh.''' + bl_idname = "mesh.primitive_worm_gear" + bl_label = "Add Worm Gear" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + number_of_teeth = IntProperty(name="Number of Teeth", + description="Number of teeth on the gear", + min=2, + max=265, + default=12) + number_of_rows = IntProperty(name="Number of Rows", + description="Number of rows on the worm gear", + min=2, + max=265, + default=32) + radius = FloatProperty(name="Radius", + description="Radius of the gear, negative for crown gear", + min=-100.0, + max=100.0, + default=1.0) + addendum = FloatProperty(name="Addendum", + description="Addendum, extent of tooth above radius", + min=0.01, + max=100.0, + default=0.1) + dedendum = FloatProperty(name="Dedendum", + description="Dedendum, extent of tooth below radius", + min=0.0, + max=100.0, + default=0.1) + angle = FloatProperty(name="Pressure Angle", + description="Pressure angle, skewness of tooth tip (degrees)", + min=0.0, + max=45.0, + default=20.0) + row_height = FloatProperty(name="Row Height", + description="Height of each Row", + min=0.05, + max=100.0, + default=0.2) + skew = FloatProperty(name="Skewness per Row", + description="Skew of each row (degrees)", + min=-90.0, + max=90.0, + default=11.25) + crown = FloatProperty(name="Crown", + description="Inward pointing extend of crown teeth", + min=0.0, + max=100.0, + default=0.0) + align_matrix = mathutils.Matrix() + + def draw(self, context): + props = self.properties + layout = self.layout + box = layout.box() + box.prop(props, 'number_of_teeth') + box.prop(props, 'number_of_rows') + box.prop(props, 'radius') + box.prop(props, 'row_height') + box = layout.box() + box.prop(props, 'addendum') + box.prop(props, 'dedendum') + box = layout.box() + box.prop(props, 'angle') + box.prop(props, 'skew') + box.prop(props, 'crown') + + def execute(self, context): + props = self.properties + + verts, faces, verts_tip, verts_valley = add_worm( + props.number_of_teeth, + props.number_of_rows, + props.radius, + props.addendum, + props.dedendum, + radians(props.angle), + width=props.row_height, + skew=radians(props.skew), + crown=props.crown) + + # Actually create the mesh object from this geometry data. + obj = create_mesh_object(context, verts, [], faces, "Worm Gear", + props.edit, self.align_matrix) + + # Create vertex groups from stored vertices. + tipGroup = obj.add_vertex_group('Tips') + for vert in verts_tip: + obj.add_vertex_to_group(vert, tipGroup, 1.0, 'ADD') + + valleyGroup = obj.add_vertex_group('Valleys') + for vert in verts_valley: + obj.add_vertex_to_group(vert, valleyGroup, 1.0, 'ADD') + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +class INFO_MT_mesh_gears_add(bpy.types.Menu): + # Define the "Gears" menu + bl_idname = "INFO_MT_mesh_gears_add" + bl_label = "Gears" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + layout.operator("mesh.primitive_gear", + text="Gear") + layout.operator("mesh.primitive_worm_gear", + text="Worm") + + +# Define "Gears" menu +menu_func = (lambda self, + context: self.layout.menu("INFO_MT_mesh_gears_add", icon="PLUGIN")) + + +def register(): + bpy.types.register(AddGear) + bpy.types.register(AddWormGear) + bpy.types.register(INFO_MT_mesh_gears_add) + + # Add "Gears" entry to the "Add Mesh" menu. + bpy.types.INFO_MT_mesh_add.append(menu_func) + + +def unregister(): + bpy.types.unregister(AddGear) + bpy.types.unregister(AddWormGear) + bpy.types.unregister(INFO_MT_mesh_gears_add) + + # Remove "Gears" entry from the "Add Mesh" menu. + bpy.types.INFO_MT_mesh_add.remove(menu_func) + +if __name__ == "__main__": + register() diff --git a/add_mesh_gemstones.py b/add_mesh_gemstones.py new file mode 100644 index 00000000..0b27829e --- /dev/null +++ b/add_mesh_gemstones.py @@ -0,0 +1,480 @@ +# ##### 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 ##### + +import bpy +from mathutils import * +from math import * +from bpy.props import * + +bl_addon_info = { + 'name': 'Add Mesh: Gemstones', + 'author': 'Pontiac, Fourmadmen, Dreampainter', + 'version': '0.3', + 'blender': (2, 5, 3), + 'location': 'View3D > Add > Mesh > Gemstones', + 'description': 'Adds various gemstone (Diamond & Gem) meshes.', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Add_Mesh/Add_Gemstones', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=21432&group_id=153&atid=469', + 'category': 'Add Mesh'} + +# calculates the matrix for the new object +# depending on user pref +def align_matrix(context): + loc = TranslationMatrix(context.scene.cursor_location) + obj_align = context.user_preferences.edit.object_align + if (context.space_data.type == 'VIEW_3D' + and obj_align == 'VIEW'): + rot = context.space_data.region_3d.view_matrix.rotation_part().invert().resize4x4() + else: + rot = Matrix() + align_matrix = loc * rot + return align_matrix + +# Create a new mesh (object) from verts/edges/faces. +# verts/edges/faces ... List of vertices/edges/faces for the +# new mesh (as used in from_pydata). +# name ... Name of the new mesh (& object). +# edit ... Replace existing mesh data. +# Note: Using "edit" will destroy/delete existing mesh data. +def create_mesh_object(context, verts, edges, faces, name, edit, align_matrix): + scene = context.scene + obj_act = scene.objects.active + + # Can't edit anything, unless we have an active obj. + if edit and not obj_act: + return None + + # Create new mesh + mesh = bpy.data.meshes.new(name) + + # Make a mesh from a list of verts/edges/faces. + mesh.from_pydata(verts, edges, faces) + + # Update mesh geometry after adding stuff. + mesh.update() + + # Deselect all objects. + bpy.ops.object.select_all(action='DESELECT') + + if edit: + # Replace geometry of existing object + + # Use the active obj and select it. + ob_new = obj_act + ob_new.select = True + + if obj_act.mode == 'OBJECT': + # Get existing mesh datablock. + old_mesh = ob_new.data + + # Set object data to nothing + ob_new.data = None + + # Clear users of existing mesh datablock. + old_mesh.user_clear() + + # Remove old mesh datablock if no users are left. + if (old_mesh.users == 0): + bpy.data.meshes.remove(old_mesh) + + # Assign new mesh datablock. + ob_new.data = mesh + + else: + # Create new object + ob_new = bpy.data.objects.new(name, mesh) + + # Link new object to the given scene and select it. + scene.objects.link(ob_new) + ob_new.select = True + + # Place the object at the 3D cursor location. + # apply viewRotaion + ob_new.matrix_world = align_matrix + + if obj_act and obj_act.mode == 'EDIT': + if not edit: + # We are in EditMode, switch to ObjectMode. + bpy.ops.object.mode_set(mode='OBJECT') + + # Select the active object as well. + obj_act.select = True + + # Apply location of new object. + scene.update() + + # Join new object into the active. + bpy.ops.object.join() + + # Switching back to EditMode. + bpy.ops.object.mode_set(mode='EDIT') + + ob_new = obj_act + + else: + # We are in ObjectMode. + # Make the new object the active one. + scene.objects.active = ob_new + + return ob_new + + +# A very simple "bridge" tool. +# Connects two equally long vertex rows with faces. +# Returns a list of the new faces (list of lists) +# +# vertIdx1 ... First vertex list (list of vertex indices). +# vertIdx2 ... Second vertex list (list of vertex indices). +# closed ... Creates a loop (first & last are closed). +# flipped ... Invert the normal of the face(s). +# +# Note: You can set vertIdx1 to a single vertex index to create +# a fan/star of faces. +# Note: If both vertex idx list are the same length they have +# to have at least 2 vertices. +def createFaces(vertIdx1, vertIdx2, closed=False, flipped=False): + faces = [] + + if not vertIdx1 or not vertIdx2: + return None + + if len(vertIdx1) < 2 and len(vertIdx2) < 2: + return None + + fan = False + if (len(vertIdx1) != len(vertIdx2)): + if (len(vertIdx1) == 1 and len(vertIdx2) > 1): + fan = True + else: + return None + + total = len(vertIdx2) + + if closed: + # Bridge the start with the end. + if flipped: + face = [ + vertIdx1[0], + vertIdx2[0], + vertIdx2[total - 1]] + if not fan: + face.append(vertIdx1[total - 1]) + faces.append(face) + + else: + face = [vertIdx2[0], vertIdx1[0]] + if not fan: + face.append(vertIdx1[total - 1]) + face.append(vertIdx2[total - 1]) + faces.append(face) + + # Bridge the rest of the faces. + for num in range(total - 1): + if flipped: + if fan: + face = [vertIdx2[num], vertIdx1[0], vertIdx2[num + 1]] + else: + face = [vertIdx2[num], vertIdx1[num], + vertIdx1[num + 1], vertIdx2[num + 1]] + faces.append(face) + else: + if fan: + face = [vertIdx1[0], vertIdx2[num], vertIdx2[num + 1]] + else: + face = [vertIdx1[num], vertIdx2[num], + vertIdx2[num + 1], vertIdx1[num + 1]] + faces.append(face) + + return faces + + +# @todo Clean up vertex&face creation process a bit. +def add_gem(r1, r2, seg, h1, h2): + """ + r1 = pavilion radius + r2 = crown radius + seg = number of segments + h1 = pavilion height + h2 = crown height + Generates the vertices and faces of the gem + """ + + verts = [] + + a = 2.0 * pi / seg # Angle between segments + offset = a / 2.0 # Middle between segments + + r3 = ((r1 + r2) / 2.0) / cos(offset) # Middle of crown + r4 = (r1 / 2.0) / cos(offset) # Middle of pavilion + h3 = h2 / 2.0 # Middle of crown height + h4 = -h1 / 2.0 # Middle of pavilion height + + # Tip + vert_tip = len(verts) + verts.append(Vector((0.0, 0.0, -h1))) + + # Middle vertex of the flat side (crown) + vert_flat = len(verts) + verts.append(Vector((0.0, 0.0, h2))) + + edgeloop_flat = [] + for i in range(seg): + s1 = sin(i * a) + s2 = sin(offset + i * a) + c1 = cos(i * a) + c2 = cos(offset + i * a) + + verts.append((r4 * s1, r4 * c1, h4)) # Middle of pavilion + verts.append((r1 * s2, r1 * c2, 0.0)) # Pavilion + verts.append((r3 * s1, r3 * c1, h3)) # Middle crown + edgeloop_flat.append(len(verts)) + verts.append((r2 * s2, r2 * c2, h2)) # Crown + + faces = [] + + for index in range(seg): + i = index * 4 + j = ((index + 1) % seg) * 4 + + faces.append([j + 2, vert_tip, i + 2, i + 3]) # Tip -> Middle of pav + faces.append([j + 2, i + 3, j + 3]) # Middle of pav -> pav + faces.append([j + 3, i + 3, j + 4]) # Pav -> Middle crown + faces.append([j + 4, i + 3, i + 4, i + 5]) # Crown quads + faces.append([j + 4, i + 5, j + 5]) # Middle crown -> crown + + faces_flat = createFaces([vert_flat], edgeloop_flat, closed=True) + faces.extend(faces_flat) + + return verts, faces + + +def add_diamond(segments, girdle_radius, table_radius, + crown_height, pavilion_height): + + PI_2 = pi * 2.0 + z_axis = (0.0, 0.0, -1.0) + + verts = [] + faces = [] + + height_flat = crown_height + height_middle = 0.0 + height_tip = -pavilion_height + + # Middle vertex of the flat side (crown) + vert_flat = len(verts) + verts.append(Vector((0.0, 0.0, height_flat))) + + # Tip + vert_tip = len(verts) + verts.append(Vector((0.0, 0.0, height_tip))) + + verts_flat = [] + verts_girdle = [] + + for index in range(segments): + quat = Quaternion(z_axis, (index / segments) * PI_2) + + angle = PI_2 * index / segments + + # Row for flat side + verts_flat.append(len(verts)) + vec = Vector((table_radius, 0.0, height_flat)) * quat + verts.append(vec) + + # Row for the middle/girdle + verts_girdle.append(len(verts)) + vec = Vector((girdle_radius, 0.0, height_middle)) * quat + verts.append(vec) + + # Flat face + faces_flat = createFaces([vert_flat], verts_flat, closed=True, + flipped=True) + # Side face + faces_side = createFaces(verts_girdle, verts_flat, closed=True) + # Tip faces + faces_tip = createFaces([vert_tip], verts_girdle, closed=True) + + faces.extend(faces_tip) + faces.extend(faces_side) + faces.extend(faces_flat) + + return verts, faces + + +class AddDiamond(bpy.types.Operator): + '''Add a diamond mesh.''' + bl_idname = "mesh.primitive_diamond_add" + bl_label = "Add Diamond" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + segments = IntProperty(name="Segments", + description="Number of segments for the diamond", + min=3, + max=256, + default=32) + girdle_radius = FloatProperty(name="Girdle Radius", + description="Girdle radius of the diamond", + min=0.01, + max=9999.0, + default=1.0) + table_radius = FloatProperty(name="Table Radius", + description="Girdle radius of the diamond", + min=0.01, + max=9999.0, + default=0.6) + crown_height = FloatProperty(name="Crown Height", + description="Crown height of the diamond", + min=0.01, + max=9999.0, + default=0.35) + pavilion_height = FloatProperty(name="Pavilion Height", + description="Pavilion height of the diamond", + min=0.01, + max=9999.0, + default=0.8) + align_matrix = Matrix() + + def execute(self, context): + props = self.properties + verts, faces = add_diamond(props.segments, + props.girdle_radius, + props.table_radius, + props.crown_height, + props.pavilion_height) + + obj = create_mesh_object(context, verts, [], faces, + "Diamond", props.edit, self.align_matrix) + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +class AddGem(bpy.types.Operator): + """Add a diamond gem""" + bl_idname = "mesh.primitive_gem_add" + bl_label = "Add Gem" + bl_description = "Create an offset faceted gem." + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + segments = IntProperty(name="Segments", + description="Longitudial segmentation", + min=3, + max=265, + default=8,) + pavilion_radius = FloatProperty(name="Radius", + description="Radius of the gem", + min=0.01, + max=9999.0, + default=1.0) + crown_radius = FloatProperty(name="Table Radius", + description="Radius of the table(top).", + min=0.01, + max=9999.0, + default=0.6) + crown_height = FloatProperty(name="Table height", + description="Height of the top half.", + min=0.01, + max=9999.0, + default=0.35) + pavilion_height = FloatProperty(name="Pavilion height", + description="Height of bottom half.", + min=0.01, + max=9999.0, + default=0.8) + align_matrix = Matrix() + + def execute(self, context): + props = self.properties + + # create mesh + verts, faces = add_gem( + props.pavilion_radius, + props.crown_radius, + props.segments, + props.pavilion_height, + props.crown_height) + + obj = create_mesh_object(context, verts, [], faces, "Gem", props.edit, self.align_matrix) + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +class INFO_MT_mesh_gemstones_add(bpy.types.Menu): + # Define the "Gemstones" menu + bl_idname = "INFO_MT_mesh_gemstones_add" + bl_label = "Gemstones" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + layout.operator("mesh.primitive_diamond_add", + text="Diamond") + layout.operator("mesh.primitive_gem_add", + text="Gem") + + +# Register all operators and panels +import space_info + +# Define "Gemstones" menu +menu_func = (lambda self, + context: self.layout.menu("INFO_MT_mesh_gemstones_add", icon="PLUGIN")) + + +def register(): + # Register the operators/menus. + bpy.types.register(AddDiamond) + bpy.types.register(AddGem) + bpy.types.register(INFO_MT_mesh_gemstones_add) + + # Add "Gemstones" menu to the "Add Mesh" menu + space_info.INFO_MT_mesh_add.append(menu_func) + + +def unregister(): + # Unregister the operators/menus. + bpy.types.unregister(AddDiamond) + bpy.types.unregister(AddGem) + bpy.types.unregister(INFO_MT_mesh_gemstones_add) + + # Remove "Gemstones" menu from the "Add Mesh" menu. + space_info.INFO_MT_mesh_add.remove(menu_func) + +if __name__ == "__main__": + register() diff --git a/add_mesh_pipe_joint.py b/add_mesh_pipe_joint.py new file mode 100644 index 00000000..97f7d2e5 --- /dev/null +++ b/add_mesh_pipe_joint.py @@ -0,0 +1,1300 @@ +# ##### 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 ##### + +""" +Pipe Joints +This script lets the user create various types of pipe joints. + +Usage: +You have to activated the script in the "Add-Ons" tab (user preferences). +The functionality can then be accessed via the +"Add Mesh" -> "Pipe Joints" menu. +Note: Currently only the "Elbow" type supports odd number of vertices. + +Version history: +v0.10.5 - createFaces can now create fan/star like faces. +v0.10.4 - Updated the function "createFaces" a bit. No functional changes. +v0.10.3 - Updated store_recall_properties, apply_object_align + and create_mesh_object. + Changed how recall data is stored. + Added 'description'. +v0.10.2 - API change Mathutils -> mathutils (r557) + Fixed wiki url. +v0.10.1 - Use hidden "edit" property for "recall" operator. +v0.10 - Store "recall" properties in the created objects. + Align the geometry to the view if the user preference says so. +v0.9.10 - Use bl_addon_info for Add-On information. +v0.9.9 - Changed the script so it can be managed from the "Add-Ons" tab in + the user preferences. + Added dummy "PLUGIN" icon. +v0.9.8 - Fixed some new API stuff. + Mainly we now have the register/unregister functions. + Also the new() function for objects now accepts a mesh object. + Corrected FSF address. + Clean up of tooltips. +v0.9.7 - Use "unit" settings for angles as well. + This also lets me use radiant for all internal values.. +v0.9.6 - Use "unit" settings (i.e. none/metric/imperial). +v0.9.5 - Use mesh.from_pydata() for geometry creation. + So we can remove unpack_list and unpack_face_list again. +v0.9.4 - Creating of the pipe now works in mesh edit mode too. + Thanks to ideasman42 (Campbell Barton) for his nice work + on the torus script code :-). +v0.9.3 - Changed to a saner vertex/polygon creation process (previously + my usage of add_geometry could only do quads) + For this I've copied the functions unpack_list and unpack_face_list + from import_scene_obj.py. + Elbow joint actually supports 3 vertices per circle. + Various comments. + Script _should_ now be PEP8 compatible. +v0.9.2 - Converted from tabs to spaces (4 spaces per tab). +v0.9.1 - Converted add_mesh and add_object to their new counterparts + "bpy.data.meshes.new() and "bpy.data.objects.new()" +v0.9 - Converted to 2.5. Made mostly pep8 compatible (exept for tabs and + stuff the check-script didn't catch). +v0.8.5 - Fixed bug in Elbow joint. Same problem as in 0.8.1 +v0.8.4 - Fixed bug in Y joint. Same problem as in 0.8.1 +v0.8.3 - Fixed bug in N joint. Same problem as in 0.8.1 +v0.8.2 - Fixed bug in X (cross) joint. Same problem as in 0.8.1 +v0.8.1 - Fixed bug in T joint. Angles greater than 90 deg combined with a + radius != 1 resulted in bad geometry (the radius was not taken into + account when calculating the joint vertices). +v0.8 - Added N-Joint. + Removed all uses of baseJointLocZ. It just clutters the code. +v0.7 - Added cross joint +v0.6 - No visible changes. Lots of internal ones though + (complete redesign of face creation process). + As a bonus the code is a bit easier to read now. + Added a nice&simple little "bridge" function + (createFaces) for these changes. +v0.5.1 - Made it possible to create asymmetric Y joints. + Renamed the 2 Wye Joints to something more fitting and unique. + One is now the Tee joint, the second one remains the Wye joint. +v0.5 - Added real Y joint. +v0.4.3 - Added check for odd vertex numbers. They are not (yet) supported. +v0.4.2 - Added pipe length to the GUI. +v0.4.1 - Removed the unfinished menu entries for now. +v0.4 - Tried to clean up the face creation in addTeeJoint +v0.3 - Code for wye (Y) shape (straight pipe with "branch" for now) +v0.2 - Restructured to allow different types of pipe (joints). +v0.1 - Initial revision. + +More links: +http://gitorious.org/blender-scripts/blender-pipe-joint-script +http://blenderartists.org/forum/showthread.php?t=154394 + +TODO: + +Use a rotation matrix for rotating the circle vertices: +rotation_matrix = mathutils.RotationMatrix(-math.pi/2, 4, 'x') +mesh.transform(rotation_matrix) +""" + +bl_addon_info = { + 'name': 'Add Mesh: Pipe Joints', + 'author': 'Buerbaum Martin (Pontiac)', + 'version': '0.10.5', + 'blender': (2, 5, 3), + 'location': 'View3D > Add > Mesh > Pipe Joint', + 'description': 'Adds 5 pipe Joint types to the Add Mesh menu', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Add_Mesh/Add_Pipe_Joints', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=21443&group_id=153&atid=469', + 'category': 'Add Mesh'} + +import bpy +import mathutils +from math import * +from bpy.props import * + + +# Apply view rotation to objects if "Align To" for +# new objects was set to "VIEW" in the User Preference. +# Is now handled in the invoke functions +# calculates the matrix for the new object +# depending on user pref +def align_matrix(context): + loc = mathutils.TranslationMatrix(context.scene.cursor_location) + obj_align = context.user_preferences.edit.object_align + if (context.space_data.type == 'VIEW_3D' + and obj_align == 'VIEW'): + rot = context.space_data.region_3d.view_matrix.rotation_part().invert().resize4x4() + else: + rot = mathutils.Matrix() + align_matrix = loc * rot + return align_matrix +# Create a new mesh (object) from verts/edges/faces. +# verts/edges/faces ... List of vertices/edges/faces for the +# new mesh (as used in from_pydata). +# name ... Name of the new mesh (& object). +# edit ... Replace existing mesh data. +# Note: Using "edit" will destroy/delete existing mesh data. +def create_mesh_object(context, verts, edges, faces, name, edit, align_matrix): + scene = context.scene + obj_act = scene.objects.active + + # Can't edit anything, unless we have an active obj. + if edit and not obj_act: + return None + + # Create new mesh + mesh = bpy.data.meshes.new(name) + + # Make a mesh from a list of verts/edges/faces. + mesh.from_pydata(verts, edges, faces) + + # Update mesh geometry after adding stuff. + mesh.update() + + # Deselect all objects. + bpy.ops.object.select_all(action='DESELECT') + + if edit: + # Replace geometry of existing object + + # Use the active obj and select it. + ob_new = obj_act + ob_new.selected = True + + if obj_act.mode == 'OBJECT': + # Get existing mesh datablock. + old_mesh = ob_new.data + + # Set object data to nothing + ob_new.data = None + + # Clear users of existing mesh datablock. + old_mesh.user_clear() + + # Remove old mesh datablock if no users are left. + if (old_mesh.users == 0): + bpy.data.meshes.remove(old_mesh) + + # Assign new mesh datablock. + ob_new.data = mesh + + else: + # Create new object + ob_new = bpy.data.objects.new(name, mesh) + + # Link new object to the given scene and select it. + scene.objects.link(ob_new) + ob_new.select = True + + # Place the object at the 3D cursor location. + ob_new.matrix_world = align_matrix + + if obj_act and obj_act.mode == 'EDIT': + if not edit: + # We are in EditMode, switch to ObjectMode. + bpy.ops.object.mode_set(mode='OBJECT') + + # Select the active object as well. + obj_act.select = True + + # Apply location of new object. + scene.update() + + # Join new object into the active. + bpy.ops.object.join() + + # Switching back to EditMode. + bpy.ops.object.mode_set(mode='EDIT') + + ob_new = obj_act + + else: + # We are in ObjectMode. + # Make the new object the active one. + scene.objects.active = ob_new + + return ob_new + + +# A very simple "bridge" tool. +# Connects two equally long vertex rows with faces. +# Returns a list of the new faces (list of lists) +# +# vertIdx1 ... First vertex list (list of vertex indices). +# vertIdx2 ... Second vertex list (list of vertex indices). +# closed ... Creates a loop (first & last are closed). +# flipped ... Invert the normal of the face(s). +# +# Note: You can set vertIdx1 to a single vertex index to create +# a fan/star of faces. +# Note: If both vertex idx list are the same length they have +# to have at least 2 vertices. +def createFaces(vertIdx1, vertIdx2, closed=False, flipped=False): + faces = [] + + if not vertIdx1 or not vertIdx2: + return None + + if len(vertIdx1) < 2 and len(vertIdx2) < 2: + return None + + fan = False + if (len(vertIdx1) != len(vertIdx2)): + if (len(vertIdx1) == 1 and len(vertIdx2) > 1): + fan = True + else: + return None + + total = len(vertIdx2) + + if closed: + # Bridge the start with the end. + if flipped: + face = [ + vertIdx1[0], + vertIdx2[0], + vertIdx2[total - 1]] + if not fan: + face.append(vertIdx1[total - 1]) + faces.append(face) + + else: + face = [vertIdx2[0], vertIdx1[0]] + if not fan: + face.append(vertIdx1[total - 1]) + face.append(vertIdx2[total - 1]) + faces.append(face) + + # Bridge the rest of the faces. + for num in range(total - 1): + if flipped: + if fan: + face = [vertIdx2[num], vertIdx1[0], vertIdx2[num + 1]] + else: + face = [vertIdx2[num], vertIdx1[num], + vertIdx1[num + 1], vertIdx2[num + 1]] + faces.append(face) + else: + if fan: + face = [vertIdx1[0], vertIdx2[num], vertIdx2[num + 1]] + else: + face = [vertIdx1[num], vertIdx2[num], + vertIdx2[num + 1], vertIdx1[num + 1]] + faces.append(face) + + return faces + + +class AddElbowJoint(bpy.types.Operator): + # Create the vertices and polygons for a simple elbow (bent pipe). + '''Add an Elbow pipe mesh''' + bl_idname = "mesh.primitive_elbow_joint_add" + bl_label = "Add Pipe Elbow" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + + radius = FloatProperty(name="Radius", + description="The radius of the pipe.", + default=1.0, + min=0.01, + max=100.0, + unit="LENGTH") + div = IntProperty(name="Divisions", + description="Number of vertices (divisions).", + default=32, min=3, max=256) + + angle = FloatProperty(name="Angle", + description="The angle of the branching pipe (i.e. the 'arm')." \ + " Measured from the center line of the main pipe.", + default=radians(45.0), + min=radians(-179.9), + max=radians(179.9), + unit="ROTATION") + + startLength = FloatProperty(name="Length Start", + description="Length of the beginning of the pipe.", + default=3.0, + min=0.01, + max=100.0, + unit="LENGTH") + endLength = FloatProperty(name="End Length", + description="Length of the end of the pipe.", + default=3.0, + min=0.01, + max=100.0, + unit="LENGTH") + align_matrix = mathutils.Matrix() + + def execute(self, context): + edit = self.properties.edit + + radius = self.properties.radius + div = self.properties.div + + angle = self.properties.angle + + startLength = self.properties.startLength + endLength = self.properties.endLength + + verts = [] + faces = [] + + loop1 = [] # The starting circle + loop2 = [] # The elbow circle + loop3 = [] # The end circle + + # Create start circle + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + locX = sin(curVertAngle) + locY = cos(curVertAngle) + locZ = -startLength + loop1.append(len(verts)) + verts.append([locX * radius, locY * radius, locZ]) + + # Create deformed joint circle + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + locX = sin(curVertAngle) + locY = cos(curVertAngle) + locZ = locX * tan(angle / 2.0) + loop2.append(len(verts)) + verts.append([locX * radius, locY * radius, locZ * radius]) + + # Create end circle + baseEndLocX = -endLength * sin(angle) + baseEndLocZ = endLength * cos(angle) + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + # Create circle + locX = sin(curVertAngle) * radius + locY = cos(curVertAngle) * radius + locZ = 0.0 + + # Rotate circle + locZ = locX * cos(pi / 2.0 - angle) + locX = locX * sin(pi / 2.0 - angle) + + loop3.append(len(verts)) + # Translate and add circle vertices to the list. + verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ]) + + # Create faces + faces.extend(createFaces(loop1, loop2, closed=True)) + faces.extend(createFaces(loop2, loop3, closed=True)) + + obj = create_mesh_object(context, verts, [], faces, + "Elbow Joint", edit, self.align_matrix) + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +class AddTeeJoint(bpy.types.Operator): + # Create the vertices and polygons for a simple tee (T) joint. + # The base arm of the T can be positioned in an angle if needed though. + '''Add a Tee-Joint mesh''' + bl_idname = "mesh.primitive_tee_joint_add" + bl_label = "Add Pipe Tee-Joint" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + + radius = FloatProperty(name="Radius", + description="The radius of the pipe.", + default=1.0, + min=0.01, + max=100.0, + unit="LENGTH") + div = IntProperty(name="Divisions", + description="Number of vertices (divisions).", + default=32, + min=4, + max=256) + + angle = FloatProperty(name="Angle", + description="The angle of the branching pipe (i.e. the 'arm')." \ + " Measured from the center line of the main pipe.", + default=radians(90.0), + min=radians(0.1), + max=radians(179.9), + unit="ROTATION") + + startLength = FloatProperty(name="Length Start", + description="Length of the beginning of the" \ + " main pipe (the straight one).", + default=3.0, + min=0.01, + max=100.0, + unit="LENGTH") + endLength = FloatProperty(name="End Length", + description="Length of the end of the" \ + " main pipe (the straight one).", + default=3.0, + min=0.01, + max=100.0, + unit="LENGTH") + branchLength = FloatProperty(name="Arm Length", + description="Length of the arm pipe (the bent one).", + default=3.0, + min=0.01, + max=100.0, + unit="LENGTH") + align_matrix = mathutils.Matrix() + + def execute(self, context): + edit = self.properties.edit + + radius = self.properties.radius + div = self.properties.div + + angle = self.properties.angle + + startLength = self.properties.startLength + endLength = self.properties.endLength + branchLength = self.properties.branchLength + + if (div % 2): + # Odd vertice number not supported (yet). + return {'CANCELLED'} + + verts = [] + faces = [] + + # List of vert indices of each cross section + loopMainStart = [] # Vert indices for the + # beginning of the main pipe. + loopJoint1 = [] # Vert indices for joint that is used + # to connect the joint & loopMainStart. + loopJoint2 = [] # Vert indices for joint that is used + # to connect the joint & loopArm. + loopJoint3 = [] # Vert index for joint that is used + # to connect the joint & loopMainEnd. + loopArm = [] # Vert indices for the end of the arm. + loopMainEnd = [] # Vert indices for the + # end of the main pipe. + + # Create start circle (main pipe) + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + locX = sin(curVertAngle) + locY = cos(curVertAngle) + locZ = -startLength + loopMainStart.append(len(verts)) + verts.append([locX * radius, locY * radius, locZ]) + + # Create deformed joint circle + vertTemp1 = None + vertTemp2 = None + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + locX = sin(curVertAngle) + locY = cos(curVertAngle) + + if vertIdx == 0: + vertTemp1 = len(verts) + if vertIdx == div / 2: + # @todo: This will possibly break if we + # ever support odd divisions. + vertTemp2 = len(verts) + + loopJoint1.append(len(verts)) + if (vertIdx < div / 2): + # Straight side of main pipe. + locZ = 0 + loopJoint3.append(len(verts)) + else: + # Branching side + locZ = locX * tan(angle / 2.0) + loopJoint2.append(len(verts)) + + verts.append([locX * radius, locY * radius, locZ * radius]) + + # Create 2. deformed joint (half-)circle + loopTemp = [] + for vertIdx in range(div): + if (vertIdx > div / 2): + curVertAngle = vertIdx * (2.0 * pi / div) + locX = sin(curVertAngle) + locY = -cos(curVertAngle) + locZ = -(radius * locX * tan((pi - angle) / 2.0)) + loopTemp.append(len(verts)) + verts.append([locX * radius, locY * radius, locZ]) + + loopTemp2 = loopTemp[:] + + # Finalise 2. loop + loopTemp.reverse() + loopTemp.append(vertTemp1) + loopJoint2.reverse() + loopJoint2.extend(loopTemp) + loopJoint2.reverse() + + # Finalise 3. loop + loopTemp2.append(vertTemp2) + loopTemp2.reverse() + loopJoint3.extend(loopTemp2) + + # Create end circle (branching pipe) + baseEndLocX = -branchLength * sin(angle) + baseEndLocZ = branchLength * cos(angle) + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + # Create circle + locX = sin(curVertAngle) * radius + locY = cos(curVertAngle) * radius + locZ = 0.0 + + # Rotate circle + locZ = locX * cos(pi / 2.0 - angle) + locX = locX * sin(pi / 2.0 - angle) + + loopArm.append(len(verts)) + + # Add translated circle. + verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ]) + + # Create end circle (main pipe) + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + locX = sin(curVertAngle) + locY = cos(curVertAngle) + locZ = endLength + loopMainEnd.append(len(verts)) + verts.append([locX * radius, locY * radius, locZ]) + + # Create faces + faces.extend(createFaces(loopMainStart, loopJoint1, closed=True)) + faces.extend(createFaces(loopJoint2, loopArm, closed=True)) + faces.extend(createFaces(loopJoint3, loopMainEnd, closed=True)) + + obj = create_mesh_object(context, verts, [], faces, "Tee Joint", edit, self.align_matrix) + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +class AddWyeJoint(bpy.types.Operator): + '''Add a Wye-Joint mesh''' + bl_idname = "mesh.primitive_wye_joint_add" + bl_label = "Add Pipe Wye-Joint" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + + radius = FloatProperty(name="Radius", + description="The radius of the pipe.", + default=1.0, + min=0.01, + max=100.0, + unit="LENGTH") + div = IntProperty(name="Divisions", + description="Number of vertices (divisions).", + default=32, + min=4, + max=256) + + angle1 = FloatProperty(name="Angle 1", + description="The angle of the 1. branching pipe." \ + " Measured from the center line of the main pipe.", + default=radians(45.0), + min=radians(-179.9), + max=radians(179.9), + unit="ROTATION") + angle2 = FloatProperty(name="Angle 2", + description="The angle of the 2. branching pipe." \ + " Measured from the center line of the main pipe.", + default=radians(45.0), + min=radians(-179.9), + max=radians(179.9), + unit="ROTATION") + + startLength = FloatProperty(name="Length Start", + description="Length of the beginning of the" \ + " main pipe (the straight one).", + default=3.0, + min=0.01, + max=100.0, + unit="LENGTH") + branch1Length = FloatProperty(name="Length Arm 1", + description="Length of the 1. arm.", + default=3.0, + min=0.01, + max=100.0, + unit="LENGTH") + branch2Length = FloatProperty(name="Length Arm 2", + description="Length of the 2. arm.", + default=3.0, + min=0.01, + max=100.0, + unit="LENGTH") + align_matrix = mathutils.Matrix() + + def execute(self, context): + edit = self.properties.edit + + radius = self.properties.radius + div = self.properties.div + + angle1 = self.properties.angle1 + angle2 = self.properties.angle2 + + startLength = self.properties.startLength + branch1Length = self.properties.branch1Length + branch2Length = self.properties.branch2Length + + if (div % 2): + # Odd vertice number not supported (yet). + return {'CANCELLED'} + + verts = [] + faces = [] + + # List of vert indices of each cross section + loopMainStart = [] # Vert indices for + # the beginning of the main pipe. + loopJoint1 = [] # Vert index for joint that is used + # to connect the joint & loopMainStart. + loopJoint2 = [] # Vert index for joint that + # is used to connect the joint & loopArm1. + loopJoint3 = [] # Vert index for joint that is + # used to connect the joint & loopArm2. + loopArm1 = [] # Vert idxs for end of the 1. arm. + loopArm2 = [] # Vert idxs for end of the 2. arm. + + # Create start circle + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + locX = sin(curVertAngle) + locY = cos(curVertAngle) + locZ = -startLength + loopMainStart.append(len(verts)) + verts.append([locX * radius, locY * radius, locZ]) + + # Create deformed joint circle + vertTemp1 = None + vertTemp2 = None + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + locX = sin(curVertAngle) + locY = cos(curVertAngle) + + if vertIdx == 0: + vertTemp2 = len(verts) + if vertIdx == div / 2: + # @todo: This will possibly break if we + # ever support odd divisions. + vertTemp1 = len(verts) + + loopJoint1.append(len(verts)) + if (vertIdx > div / 2): + locZ = locX * tan(angle1 / 2.0) + loopJoint2.append(len(verts)) + else: + locZ = locX * tan(-angle2 / 2.0) + loopJoint3.append(len(verts)) + + verts.append([locX * radius, locY * radius, locZ * radius]) + + # Create 2. deformed joint (half-)circle + loopTemp = [] + angleJoint = (angle2 - angle1) / 2.0 + for vertIdx in range(div): + if (vertIdx > div / 2): + curVertAngle = vertIdx * (2.0 * pi / div) + + locX = (-sin(curVertAngle) * sin(angleJoint) + / sin(angle2 - angleJoint)) + locY = -cos(curVertAngle) + locZ = (-(sin(curVertAngle) * cos(angleJoint) + / sin(angle2 - angleJoint))) + + loopTemp.append(len(verts)) + verts.append([locX * radius, locY * radius, locZ * radius]) + + loopTemp2 = loopTemp[:] + + # Finalise 2. loop + loopTemp.append(vertTemp1) + loopTemp.reverse() + loopTemp.append(vertTemp2) + loopJoint2.reverse() + loopJoint2.extend(loopTemp) + loopJoint2.reverse() + + # Finalise 3. loop + loopTemp2.reverse() + loopJoint3.extend(loopTemp2) + + # Create end circle (1. branching pipe) + baseEndLocX = -branch1Length * sin(angle1) + baseEndLocZ = branch1Length * cos(angle1) + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + # Create circle + locX = sin(curVertAngle) * radius + locY = cos(curVertAngle) * radius + locZ = 0.0 + + # Rotate circle + locZ = locX * cos(pi / 2.0 - angle1) + locX = locX * sin(pi / 2.0 - angle1) + + loopArm1.append(len(verts)) + # Add translated circle. + verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ]) + + # Create end circle (2. branching pipe) + baseEndLocX = branch2Length * sin(angle2) + baseEndLocZ = branch2Length * cos(angle2) + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + # Create circle + locX = sin(curVertAngle) * radius + locY = cos(curVertAngle) * radius + locZ = 0.0 + + # Rotate circle + locZ = locX * cos(pi / 2.0 + angle2) + locX = locX * sin(pi / 2.0 + angle2) + + loopArm2.append(len(verts)) + # Add translated circle + verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ]) + + # Create faces + faces.extend(createFaces(loopMainStart, loopJoint1, closed=True)) + faces.extend(createFaces(loopJoint2, loopArm1, closed=True)) + faces.extend(createFaces(loopJoint3, loopArm2, closed=True)) + + obj = create_mesh_object(context, verts, [], faces, "Wye Joint", edit, self.align_matrix) + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +class AddCrossJoint(bpy.types.Operator): + '''Add a Cross-Joint mesh''' + # Create the vertices and polygons for a coss (+ or X) pipe joint. + bl_idname = "mesh.primitive_cross_joint_add" + bl_label = "Add Pipe Cross-Joint" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + + radius = FloatProperty(name="Radius", + description="The radius of the pipe.", + default=1.0, + min=0.01, + max=100.0, + unit="LENGTH") + div = IntProperty(name="Divisions", + description="Number of vertices (divisions).", + default=32, + min=4, + max=256) + + angle1 = FloatProperty(name="Angle 1", + description="The angle of the 1. arm (from the main axis).", + default=radians(90.0), + min=radians(-179.9), + max=radians(179.9), + unit="ROTATION") + angle2 = FloatProperty(name="Angle 2", + description="The angle of the 2. arm (from the main axis).", + default=radians(90.0), + min=radians(-179.9), + max=radians(179.9), + unit="ROTATION") + angle3 = FloatProperty(name="Angle 3 (center)", + description="The angle of the center arm (from the main axis).", + default=radians(0.0), + min=radians(-179.9), + max=radians(179.9), + unit="ROTATION") + + startLength = FloatProperty(name="Length Start", + description="Length of the beginning of the " \ + "main pipe (the straight one).", + default=3.0, + min=0.01, + max=100.0, + unit="LENGTH") + branch1Length = FloatProperty(name="Length Arm 1", + description="Length of the 1. arm.", + default=3.0, + min=0.01, + max=100.0, + unit="LENGTH") + branch2Length = FloatProperty(name="Length Arm 2", + description="Length of the 2. arm.", + default=3.0, + min=0.01, + max=100.0, + unit="LENGTH") + branch3Length = FloatProperty(name="Length Arm 3 (center)", + description="Length of the center arm.", + default=3.0, + min=0.01, + max=100.0, + unit="LENGTH") + align_matrix = mathutils.Matrix() + + def execute(self, context): + edit = self.properties.edit + + radius = self.properties.radius + div = self.properties.div + + angle1 = self.properties.angle1 + angle2 = self.properties.angle2 + angle3 = self.properties.angle3 + + startLength = self.properties.startLength + branch1Length = self.properties.branch1Length + branch2Length = self.properties.branch2Length + branch3Length = self.properties.branch3Length + if (div % 2): + # Odd vertice number not supported (yet). + return {'CANCELLED'} + + verts = [] + faces = [] + + # List of vert indices of each cross section + loopMainStart = [] # Vert indices for the + # beginning of the main pipe. + loopJoint1 = [] # Vert index for joint that is used + # to connect the joint & loopMainStart. + loopJoint2 = [] # Vert index for joint that is used + # to connect the joint & loopArm1. + loopJoint3 = [] # Vert index for joint that is used + # to connect the joint & loopArm2. + loopJoint4 = [] # Vert index for joint that is used + # to connect the joint & loopArm3. + loopArm1 = [] # Vert idxs for the end of the 1. arm. + loopArm2 = [] # Vert idxs for the end of the 2. arm. + loopArm3 = [] # Vert idxs for the center arm end. + + # Create start circle + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + locX = sin(curVertAngle) + locY = cos(curVertAngle) + locZ = -startLength + loopMainStart.append(len(verts)) + verts.append([locX * radius, locY * radius, locZ]) + + # Create 1. deformed joint circle + vertTemp1 = None + vertTemp2 = None + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + locX = sin(curVertAngle) + locY = cos(curVertAngle) + + if vertIdx == 0: + vertTemp2 = len(verts) + if vertIdx == div / 2: + # @todo: This will possibly break if we + # ever support odd divisions. + vertTemp1 = len(verts) + + loopJoint1.append(len(verts)) + if (vertIdx > div / 2): + locZ = locX * tan(angle1 / 2.0) + loopJoint2.append(len(verts)) + else: + locZ = locX * tan(-angle2 / 2.0) + loopJoint3.append(len(verts)) + + verts.append([locX * radius, locY * radius, locZ * radius]) + + loopTemp2 = loopJoint2[:] + + # Create 2. deformed joint circle + loopTempA = [] + loopTempB = [] + angleJoint1 = (angle1 - angle3) / 2.0 + angleJoint2 = (angle2 + angle3) / 2.0 + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + + # Skip pole vertices + # @todo: This will possibly break if + # we ever support odd divisions. + if not (vertIdx == 0) and not (vertIdx == div / 2): + + if (vertIdx > div / 2): + angleJoint = angleJoint1 + angle = angle1 + Z = -1.0 + loopTempA.append(len(verts)) + + else: + angleJoint = angleJoint2 + angle = angle2 + Z = 1.0 + loopTempB.append(len(verts)) + + locX = (sin(curVertAngle) * sin(angleJoint) + / sin(angle - angleJoint)) + locY = -cos(curVertAngle) + locZ = (Z * (sin(curVertAngle) * cos(angleJoint) + / sin(angle - angleJoint))) + + verts.append([locX * radius, locY * radius, locZ * radius]) + + loopTempA2 = loopTempA[:] + loopTempB2 = loopTempB[:] + loopTempB3 = loopTempB[:] + + # Finalise 2. loop + loopTempA.append(vertTemp1) + loopTempA.reverse() + loopTempA.append(vertTemp2) + loopJoint2.reverse() + loopJoint2.extend(loopTempA) + loopJoint2.reverse() + + # Finalise 3. loop + loopJoint3.extend(loopTempB3) + + # Finalise 4. loop + loopTempA2.append(vertTemp1) + loopTempA2.reverse() + loopTempB2.append(vertTemp2) + loopJoint4.extend(reversed(loopTempB2)) + loopJoint4.extend(loopTempA2) + + # Create end circle (1. branching pipe) + baseEndLocX = -branch1Length * sin(angle1) + baseEndLocZ = branch1Length * cos(angle1) + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + # Create circle + locX = sin(curVertAngle) * radius + locY = cos(curVertAngle) * radius + locZ = 0.0 + + # Rotate circle + locZ = locX * cos(pi / 2.0 - angle1) + locX = locX * sin(pi / 2.0 - angle1) + + loopArm1.append(len(verts)) + # Add translated circle. + verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ]) + + # Create end circle (2. branching pipe) + baseEndLocX = branch2Length * sin(angle2) + baseEndLocZ = branch2Length * cos(angle2) + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + # Create circle + locX = sin(curVertAngle) * radius + locY = cos(curVertAngle) * radius + locZ = 0.0 + + # Rotate circle + locZ = locX * cos(pi / 2.0 + angle2) + locX = locX * sin(pi / 2.0 + angle2) + + loopArm2.append(len(verts)) + # Add translated circle + verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ]) + + # Create end circle (center pipe) + baseEndLocX = branch3Length * sin(angle3) + baseEndLocZ = branch3Length * cos(angle3) + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + # Create circle + locX = sin(curVertAngle) * radius + locY = cos(curVertAngle) * radius + locZ = 0.0 + + # Rotate circle + locZ = locX * cos(pi / 2.0 + angle3) + locX = locX * sin(pi / 2.0 + angle3) + + loopArm3.append(len(verts)) + # Add translated circle + verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ]) + + # Create faces + faces.extend(createFaces(loopMainStart, loopJoint1, closed=True)) + faces.extend(createFaces(loopJoint2, loopArm1, closed=True)) + faces.extend(createFaces(loopJoint3, loopArm2, closed=True)) + faces.extend(createFaces(loopJoint4, loopArm3, closed=True)) + + obj = create_mesh_object(context, verts, [], faces, + "Cross Joint", edit, self.align_matrix) + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +class AddNJoint(bpy.types.Operator): + '''Add a N-Joint mesh''' + # Create the vertices and polygons for a regular n-joint. + bl_idname = "mesh.primitive_n_joint_add" + bl_label = "Add Pipe N-Joint" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + + radius = FloatProperty(name="Radius", + description="The radius of the pipe.", + default=1.0, + min=0.01, + max=100.0, + unit="LENGTH") + div = IntProperty(name="Divisions", + description="Number of vertices (divisions).", + default=32, + min=4, + max=256) + number = IntProperty(name="Arms/Joints", + description="Number of joints/arms", + default=5, + min=2, + max=99999) + length = FloatProperty(name="Length", + description="Length of each joint/arm", + default=3.0, + min=0.01, + max=100.0, + unit="LENGTH") + align_matrix = mathutils.Matrix() + + def execute(self, context): + edit = self.properties.edit + radius = self.properties.radius + div = self.properties.div + number = self.properties.number + length = self.properties.length + + if (div % 2): + # Odd vertice number not supported (yet). + return {'CANCELLED'} + + if (number < 2): + return {'CANCELLED'} + + verts = [] + faces = [] + + loopsEndCircles = [] + loopsJointsTemp = [] + loopsJoints = [] + + vertTemp1 = None + vertTemp2 = None + + angleDiv = (2.0 * pi / number) + + # Create vertices for the end circles. + for num in range(number): + circle = [] + # Create start circle + angle = num * angleDiv + + baseEndLocX = length * sin(angle) + baseEndLocZ = length * cos(angle) + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + # Create circle + locX = sin(curVertAngle) * radius + locY = cos(curVertAngle) * radius + locZ = 0.0 + + # Rotate circle + locZ = locX * cos(pi / 2.0 + angle) + locX = locX * sin(pi / 2.0 + angle) + + circle.append(len(verts)) + # Add translated circle + verts.append([baseEndLocX + locX, locY, baseEndLocZ + locZ]) + + loopsEndCircles.append(circle) + + # Create vertices for the joint circles. + loopJoint = [] + for vertIdx in range(div): + curVertAngle = vertIdx * (2.0 * pi / div) + locX = sin(curVertAngle) + locY = cos(curVertAngle) + + skipVert = False + # Store pole vertices + if vertIdx == 0: + if (num == 0): + vertTemp2 = len(verts) + else: + skipVert = True + elif vertIdx == div / 2: + # @todo: This will possibly break if we + # ever support odd divisions. + if (num == 0): + vertTemp1 = len(verts) + else: + skipVert = True + + if not skipVert: + if (vertIdx > div / 2): + locZ = -locX * tan((pi - angleDiv) / 2.0) + loopJoint.append(len(verts)) + + # Rotate the vert + cosAng = cos(-angle) + sinAng = sin(-angle) + LocXnew = locX * cosAng - locZ * sinAng + LocZnew = locZ * cosAng + locX * sinAng + locZ = LocZnew + locX = LocXnew + + verts.append([ + locX * radius, + locY * radius, + locZ * radius]) + else: + # These two vertices will only be + # added the very first time. + if vertIdx == 0 or vertIdx == div / 2: + verts.append([locX * radius, locY * radius, locZ]) + + loopsJointsTemp.append(loopJoint) + + # Create complete loops (loopsJoints) out of the + # double number of half loops in loopsJointsTemp. + for halfLoopIdx in range(len(loopsJointsTemp)): + if (halfLoopIdx == len(loopsJointsTemp) - 1): + idx1 = halfLoopIdx + idx2 = 0 + else: + idx1 = halfLoopIdx + idx2 = halfLoopIdx + 1 + + loopJoint = [] + loopJoint.append(vertTemp2) + loopJoint.extend(reversed(loopsJointsTemp[idx2])) + loopJoint.append(vertTemp1) + loopJoint.extend(loopsJointsTemp[idx1]) + + loopsJoints.append(loopJoint) + + # Create faces from the two + # loop arrays (loopsJoints -> loopsEndCircles). + for loopIdx in range(len(loopsEndCircles)): + faces.extend( + createFaces(loopsJoints[loopIdx], + loopsEndCircles[loopIdx], closed=True)) + + obj = create_mesh_object(context, verts, [], faces, "N Joint", edit, self.align_matrix) + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +class INFO_MT_mesh_pipe_joints_add(bpy.types.Menu): + # Define the "Pipe Joints" menu + bl_idname = "INFO_MT_mesh_pipe_joints_add" + bl_label = "Pipe Joints" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + layout.operator("mesh.primitive_elbow_joint_add", + text="Pipe Elbow") + layout.operator("mesh.primitive_tee_joint_add", + text="Pipe T-Joint") + layout.operator("mesh.primitive_wye_joint_add", + text="Pipe Y-Joint") + layout.operator("mesh.primitive_cross_joint_add", + text="Pipe Cross-Joint") + layout.operator("mesh.primitive_n_joint_add", + text="Pipe N-Joint") + +################################ + +import space_info + +# Define "Pipe Joints" menu +menu_func = (lambda self, + context: self.layout.menu("INFO_MT_mesh_pipe_joints_add", icon="PLUGIN")) + + +def register(): + # Register the operators/menus. + bpy.types.register(AddElbowJoint) + bpy.types.register(AddTeeJoint) + bpy.types.register(AddWyeJoint) + bpy.types.register(AddCrossJoint) + bpy.types.register(AddNJoint) + bpy.types.register(INFO_MT_mesh_pipe_joints_add) + + # Add "Pipe Joints" menu to the "Add Mesh" menu + space_info.INFO_MT_mesh_add.append(menu_func) + + +def unregister(): + # Unregister the operators/menus. + bpy.types.unregister(AddElbowJoint) + bpy.types.unregister(AddTeeJoint) + bpy.types.unregister(AddWyeJoint) + bpy.types.unregister(AddCrossJoint) + bpy.types.unregister(AddNJoint) + bpy.types.unregister(INFO_MT_mesh_pipe_joints_add) + + # Remove "Pipe Joints" menu from the "Add Mesh" menu. + space_info.INFO_MT_mesh_add.remove(menu_func) + +if __name__ == "__main__": + register() diff --git a/add_mesh_solid.py b/add_mesh_solid.py new file mode 100644 index 00000000..579bb954 --- /dev/null +++ b/add_mesh_solid.py @@ -0,0 +1,913 @@ +# ***** 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** + + +bl_addon_info = { + 'name': 'Add Mesh: Regular Solids', + 'author': 'DreamPainter', + 'version': '1', + 'blender': (2, 5, 3), + 'location': 'View3D > Add > Mesh > Regular Solids', + 'description': 'Add a Regular Solid mesh.', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Add_Mesh/Add_Solid', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=22405&group_id=153&atid=469', + 'category': 'Add Mesh'} + + +import bpy +from bpy.props import FloatProperty,EnumProperty,BoolProperty +from math import sqrt +from mathutils import Vector,Matrix +#from rawMeshUtils import * +from functools import reduce + +# Apply view rotation to objects if "Align To" for +# new objects was set to "VIEW" in the User Preference. +def apply_object_align(context, ob): + obj_align = bpy.context.user_preferences.edit.object_align + + if (context.space_data.type == 'VIEW_3D' + and obj_align == 'VIEW'): + view3d = context.space_data + region = view3d.region_3d + viewMatrix = region.view_matrix + rot = viewMatrix.rotation_part() + ob.rotation_euler = rot.invert().to_euler() + + +# Create a new mesh (object) from verts/edges/faces. +# verts/edges/faces ... List of vertices/edges/faces for the +# new mesh (as used in from_pydata). +# name ... Name of the new mesh (& object). +# edit ... Replace existing mesh data. +# Note: Using "edit" will destroy/delete existing mesh data. +def create_mesh_object(context, verts, edges, faces, name, edit): + scene = context.scene + obj_act = scene.objects.active + + # Can't edit anything, unless we have an active obj. + if edit and not obj_act: + return None + + # Create new mesh + mesh = bpy.data.meshes.new(name) + + # Make a mesh from a list of verts/edges/faces. + mesh.from_pydata(verts, edges, faces) + + # Update mesh geometry after adding stuff. + mesh.update() + + # Deselect all objects. + bpy.ops.object.select_all(action='DESELECT') + + if edit: + # Replace geometry of existing object + + # Use the active obj and select it. + ob_new = obj_act + ob_new.select = True + + if obj_act.mode == 'OBJECT': + # Get existing mesh datablock. + old_mesh = ob_new.data + + # Set object data to nothing + ob_new.data = None + + # Clear users of existing mesh datablock. + old_mesh.user_clear() + + # Remove old mesh datablock if no users are left. + if (old_mesh.users == 0): + bpy.data.meshes.remove(old_mesh) + + # Assign new mesh datablock. + ob_new.data = mesh + + else: + # Create new object + ob_new = bpy.data.objects.new(name, mesh) + + # Link new object to the given scene and select it. + scene.objects.link(ob_new) + ob_new.select = True + + # Place the object at the 3D cursor location. + ob_new.location = scene.cursor_location + + apply_object_align(context, ob_new) + + if obj_act and obj_act.mode == 'EDIT': + if not edit: + # We are in EditMode, switch to ObjectMode. + bpy.ops.object.mode_set(mode='OBJECT') + + # Select the active object as well. + obj_act.select = True + + # Apply location of new object. + scene.update() + + # Join new object into the active. + bpy.ops.object.join() + + # Switching back to EditMode. + bpy.ops.object.mode_set(mode='EDIT') + + ob_new = obj_act + + else: + # We are in ObjectMode. + # Make the new object the active one. + scene.objects.active = ob_new + + return ob_new + + +# A very simple "bridge" tool. +# Connects two equally long vertex rows with faces. +# Returns a list of the new faces (list of lists) +# +# vertIdx1 ... First vertex list (list of vertex indices). +# vertIdx2 ... Second vertex list (list of vertex indices). +# closed ... Creates a loop (first & last are closed). +# flipped ... Invert the normal of the face(s). +# +# Note: You can set vertIdx1 to a single vertex index to create +# a fan/star of faces. +# Note: If both vertex idx list are the same length they have +# to have at least 2 vertices. +def createFaces(vertIdx1, vertIdx2, closed=False, flipped=False): + faces = [] + + if not vertIdx1 or not vertIdx2: + return None + + if len(vertIdx1) < 2 and len(vertIdx2) < 2: + return None + + fan = False + if (len(vertIdx1) != len(vertIdx2)): + if (len(vertIdx1) == 1 and len(vertIdx2) > 1): + fan = True + else: + return None + + total = len(vertIdx2) + + if closed: + # Bridge the start with the end. + if flipped: + face = [ + vertIdx1[0], + vertIdx2[0], + vertIdx2[total - 1]] + if not fan: + face.append(vertIdx1[total - 1]) + faces.append(face) + + else: + face = [vertIdx2[0], vertIdx1[0]] + if not fan: + face.append(vertIdx1[total - 1]) + face.append(vertIdx2[total - 1]) + faces.append(face) + + # Bridge the rest of the faces. + for num in range(total - 1): + if flipped: + if fan: + face = [vertIdx2[num], vertIdx1[0], vertIdx2[num + 1]] + else: + face = [vertIdx2[num], vertIdx1[num], + vertIdx1[num + 1], vertIdx2[num + 1]] + faces.append(face) + else: + if fan: + face = [vertIdx1[0], vertIdx2[num], vertIdx2[num + 1]] + else: + face = [vertIdx1[num], vertIdx2[num], + vertIdx2[num + 1], vertIdx1[num + 1]] + faces.append(face) + + return faces +# this function creates a chain of quads and, when necessary, a remaining tri +# for each polygon created in this script. be aware though, that this function +# assumes each polygon is convex. +# poly: list of faces, or a single face, like those +# needed for mesh.from_pydata. +# returns the tesselated faces. +def createPolys(poly): + # check for faces + if len(poly) == 0: + return [] + # one or more faces + if type(poly[0]) == type(1): + poly = [poly] # if only one, make it a list of one face + faces = [] + for i in poly: + l = len(i) + # let all faces of 3 or 4 verts be + if l < 5: + faces.append(i) + # split all polygons in half and bridge the two halves + else: + half = int(l/2) + f = createFaces(i[:half],[i[-1-j] for j in range(half)]) + faces.extend(f) + # if the polygon has an odd number of verts, add the last tri + if l%2 == 1: + faces.append([i[half-1],i[half],i[half+1]]) + return faces + +# function to make the reduce function work as a workaround to sum a list of vectors +def Asum(list): + return reduce(lambda a,b: a+b, list) + +# creates the 5 platonic solids as a base for the rest +# plato: should be one of {"4","6","8","12","20"}. decides what solid the +# outcome will be. +# returns a list of vertices and faces and the appropriate name +def source(plato): + verts = [] + faces = [] + + # Tetrahedron + if plato == "4": + # Calculate the necessary constants + s = sqrt(2)/3.0 + t = -1/3 + u = sqrt(6)/3 + + # create the vertices and faces + v = [(0,0,1),(2*s,0,t),(-s,u,t),(-s,-u,t)] + faces = [[0,1,2],[0,2,3],[0,3,1],[1,3,2]] + + # Hexahedron (cube) + elif plato == "6": + # Calculate the necessary constants + s = 1/sqrt(3) + + # create the vertices and faces + v = [(-s,-s,-s),(s,-s,-s),(s,s,-s),(-s,s,-s),(-s,-s,s),(s,-s,s),(s,s,s),(-s,s,s)] + faces = [[0,3,2,1],[0,1,5,4],[0,4,7,3],[6,5,1,2],[6,2,3,7],[6,7,4,5]] + + # Octahedron + elif plato == "8": + # create the vertices and faces + v = [(1,0,0),(-1,0,0),(0,1,0),(0,-1,0),(0,0,1),(0,0,-1)] + faces = [[4,0,2],[4,2,1],[4,1,3],[4,3,0],[5,2,0],[5,1,2],[5,3,1],[5,0,3]] + + # Dodecahedron + elif plato == "12": + # Calculate the necessary constants + s = 1/sqrt(3) + t = sqrt((3-sqrt(5))/6) + u = sqrt((3+sqrt(5))/6) + + # create the vertices and faces + v = [(s,s,s),(s,s,-s),(s,-s,s),(s,-s,-s),(-s,s,s),(-s,s,-s),(-s,-s,s),(-s,-s,-s), + (t,u,0),(-t,u,0),(t,-u,0),(-t,-u,0),(u,0,t),(u,0,-t),(-u,0,t),(-u,0,-t),(0,t,u), + (0,-t,u),(0,t,-u),(0,-t,-u)] + faces = [[0,8,9,4,16],[0,12,13,1,8],[0,16,17,2,12],[8,1,18,5,9],[12,2,10,3,13], + [16,4,14,6,17],[9,5,15,14,4],[6,11,10,2,17],[3,19,18,1,13],[7,15,5,18,19], + [7,11,6,14,15],[7,19,3,10,11]] + + # Icosahedron + elif plato == "20": + # Calculate the necessary constants + s = (1+sqrt(5))/2 + t = sqrt(1+s*s) + s = s/t + t = 1/t + + # create the vertices and faces + v = [(s,t,0),(-s,t,0),(s,-t,0),(-s,-t,0),(t,0,s),(t,0,-s),(-t,0,s),(-t,0,-s), + (0,s,t),(0,-s,t),(0,s,-t),(0,-s,-t)] + faces = [[0,8,4],[0,5,10],[2,4,9],[2,11,5],[1,6,8],[1,10,7],[3,9,6],[3,7,11], + [0,10,8],[1,8,10],[2,9,11],[3,11,9],[4,2,0],[5,0,2],[6,1,3],[7,3,1], + [8,6,4],[9,4,6],[10,5,7],[11,7,5]] + + # handles faulty values of plato + else: + print("Choose keyword 'plato' from {'4','6','8','12','20'}") + return None + + # convert the tuples to Vectors + verts = [Vector(i) for i in v] + + return verts,faces + +# processes the raw data from source +def createSolid(plato,vtrunc,etrunc,dual,snub): + verts = [] + faces = [] + edges = [] + # the duals from each platonic solid + dualSource = {"4":"4", + "6":"8", + "8":"6", + "12":"20", + "20":"12"} + + # constants saving space and readability + vtrunc *= 0.5 + etrunc *= 0.5 + supposed_size = 0 + noSnub = (snub == "0") or (etrunc == 0.5) or (etrunc == 0) + lSnub = (snub == "L") and (0 < etrunc < 0.5) + rSnub = (snub == "R") and (0 < etrunc < 0.5) + + # no truncation + if vtrunc == 0: + if dual: # dual is as simple as another, but mirrored platonic solid + vInput,fInput = source(dualSource[plato]) + supposed_size = Asum([vInput[i] for i in fInput[0]]).length/len(fInput[0]) + vInput = [-i*supposed_size for i in vInput] # mirror it + return vInput,fInput + return source(plato) + # simple truncation of the source + elif 0.5 >= vtrunc > 0: + vInput,fInput = source(plato) + # truncation is now equal to simple truncation of the dual of the source + elif vtrunc > 0.5: + vInput,fInput = source(dualSource[plato]) + supposed_size = Asum([vInput[i] for i in fInput[0]]).length/len(fInput[0]) + # account for the source being a dual + vtrunc = 1-vtrunc + if vtrunc == 0: # no truncation + if dual: + vInput,fInput = source(plato) + vInput = [i*supposed_size for i in vInput] + return vInput,fInput,sourceName + vInput = [-i*supposed_size for i in vInput] + return vInput,fInput + + # generate a database for creating the faces. this exists out of a list for + # every vertex in the source + # 0 : vertex id + # 1 : vertices connected to this vertex, listed ccw(Counter Clock Wise) + # 2 : vertices generated to form the faces of this vertex + # 3 : faces connected to this vertex, listed ccw + # 4 : dictionairy containing the verts used by the connected faces + # 5 : list of edges that use this vertex, listed ccw + # 6 : dictionairy containing the verts used by the connected edges + v = [[i,[],[],[],{},[],{}] for i in range(len(vInput))] + + # this piece of code, generates the database and the lists in ccw order + for x in range(len(fInput)): + i = fInput[x] + # in every faces, check which vertices connect the each vert and sort + # in ccw order + for j in range(-1,len(i)-1): + # only generate an edge dict, if edge truncation is needed + if etrunc: + # list edges as [min,max], to evade confusion + first = min([i[j-1],i[j]]) + last = max([i[j-1],i[j]]) + # if an edge is not allready in, add it and give the index + try: + y = edges.index([first,last]) + except: + edges.append([first,last]) + y = len(edges)-1 + # add a dict item + v[i[j]][6][str(y)] = [0,0] + # the vertex before and after the current vertex, check whether they + # are allready in the database + after = i[j+1] not in v[i[j]][1] + before = i[j-1] not in v[i[j]][1] + # sort them and add faces and, when necessary, edges in the database + if after: + if before: + v[i[j]][1].append(i[j+1]) + v[i[j]][1].append(i[j-1]) + v[i[j]][3].append(x) + if etrunc: v[i[j]][5].append(y) + else: + z = v[i[j]][1].index(i[j-1]) + v[i[j]][1].insert(z,i[j+1]) + v[i[j]][3].insert(z,x) + if etrunc: v[i[j]][5].insert(z,y) + else: + z = v[i[j]][1].index(i[j+1]) + v[i[j]][3].insert(z,x) + if etrunc: v[i[j]][5].insert(z,y) + if before: + v[i[j]][1].insert(z+1,i[j-1]) + # add the current face to the current vertex in the dict + v[i[j]][4][str(x)] = [0,0] + + # generate vert-only truncated vertices by linear interpolation + for i in v: + for j in range(len(i[1])): + verts.append(vInput[i[0]]*(1-vtrunc)+vInput[i[1][j]]*vtrunc) + l = len(verts)-1 + # face resulting from truncating this vertex + i[2].append(l) + # this vertex is used by both faces using this edge + i[4][str(i[3][j])][1] = l + i[4][str(i[3][j-1])][0] = l + + # only truncate edges when needed + vert_faces = [] + if etrunc: + # generate a new list of vertices, by linear interpolating each vert-face + nVerts = [] + for i in v: + f = [] + # weird range so we dont run out of array bounds + for j in range(-1,len(i[2])-1): + # making use of the fact that the snub operation takes only + # one of the two vertices per edge. so rSnub only takes the + # first, lSnub only takes the second, and noSnub takes both + if rSnub or noSnub: + # interpolate + nVerts.append((1-etrunc)*verts[i[2][j]] + etrunc*verts[i[2][j-1]]) + # add last vertex to the vert-face, face-face and edge-face + l = len(nVerts)-1 + f.append(l) + i[4][str(i[3][j-1])][0] = l + i[6][str(i[5][j-1])][1] = l + if lSnub or noSnub: + # interpolate + nVerts.append((1-etrunc)*verts[i[2][j]] + etrunc*verts[i[2][j+1]]) + # add last vertex to the vert-face, face-face and edge-face + l = len(nVerts)-1 + f.append(l) + i[4][str(i[3][j])][1] = l + i[6][str(i[5][j-1])][0] = l + # add vert-face + vert_faces.append(f) + + # snub operator creates 2 tri's instead of a planar quad, needing the + # next piece of code. making use of the dictionairy to create them. + if lSnub or rSnub: + edge_faces = [] + for x in range(len(edges)): + one = v[edges[x][0]] # the first vertex of this edge + two = v[edges[x][1]] # the second + # using max() since the dict consists of one filled spot and one + # empty('cause only one vert is created) + f = [max(two[6][str(x)]),max(one[6][str(x)])] + index = one[5].index(x) + # create this tri from the middle line and the the previous edge + # on this vertex + if lSnub: + f.append(max(one[6][str(one[5][index-1])])) + else: # or in this case, the next + if index+1 >= len(one[5]): index = -1 + f.append(max(one[6][str(one[5][index+1])])) + edge_faces.append(f) + + # do the same for the other end of the edge + f = [max(one[6][str(x)]),max(two[6][str(x)])] + index = two[5].index(x) + if lSnub: + f.append(max(two[6][str(two[5][index-1])])) + else: + if index+1 >= len(one[5]): index = -1 + f.append(max(two[6][str(two[5][index+1])])) + edge_faces.append(f) + else: + # generate edge-faces from the dictionairy, simple quads for noSnub + edge_faces = [] + for i in range(len(edges)): + f = [] + for j in edges[i]: + f.extend(v[j][6][str(i)]) + edge_faces.append(f) + verts = nVerts + else: + # generate vert-faces for non-edge-truncation + vert_faces = [i[2] for i in v] + + # calculate supposed vertex length to ensure continuity + if supposed_size: + supposed_size *= len(vert_faces[0])/Asum([verts[i] for i in vert_faces[0]]).length + verts = [-i*supposed_size for i in verts] + + # generate face-faces by looking up the old verts and replacing them with + # the vertices in the dictionairy + face_faces = [] + for x in range(len(fInput)): + f = [] + for j in fInput[x]: + # again using the fact, that only one of the two verts is used + # for snub operation + if rSnub and etrunc: + f.append(v[j][4][str(x)][0]) + elif lSnub and etrunc: + f.append(v[j][4][str(x)][1]) + else: + # for cool graphics, comment the first line and uncomment the second line + # then work the vTrunc property, leave the other properties at 0 + # (can also change 0 to 1 in second line to change from ccw to cw) + f.extend(v[j][4][str(x)]) # first + #f.append(v[j][4][str(x)][0]) # second + face_faces.append(f) + + if dual: + # create verts by taking the average of all vertices that make up each + # face. do it in this order to ease the following face creation + nVerts = [] + for i in vert_faces: + nVerts.append(Asum([verts[j] for j in i])/len(i)) + if etrunc: + eStart = len(nVerts) + for i in edge_faces: + nVerts.append(Asum([verts[j] for j in i])/len(i)) + fStart = len(nVerts) + for i in face_faces: + nVerts.append(Asum([verts[j] for j in i])/len(i)) + # the special face generation for snub duals, it sucks, even i dont get it + if lSnub or rSnub: + for x in range(len(fInput)): + i = fInput[x] + for j in range(-1,len(i)-1): + + if i[j] > i[j+1]: + eNext = edges.index([i[j+1],i[j]]) + [a,b] = [1,0] + else: + eNext = edges.index([i[j],i[j+1]]) + [a,b] = [0,1] + if i[j] > i[j-1]: + ePrev = edges.index([i[j-1],i[j]]) + [c,d] = [0,1] + else: + ePrev = edges.index([i[j],i[j-1]]) + [c,d] = [1,0] + if lSnub: + f = [eStart+2*eNext+b,eStart+2*eNext+a,i[j]] + f.append(eStart+2*ePrev+d) + f.append(fStart + x) + else: + f = [eStart+2*ePrev+c,eStart+2*ePrev+d,i[j]] + f.append(eStart+2*eNext+a) + f.append(fStart + x) + if supposed_size: faces.append(f) + else: faces.append(f[2:]+f[:2]) + else: + # for noSnub situations, the face generation is somewhat easier. + # first calculate what order faces must be added to ensure convex solids + # this by calculating the angle between the middle of the four vertices + # and the first face. if the face is above the middle, use that diagonal + # otherwise use the other diagonal + if etrunc: + f = [v[0][0],eStart+v[0][5][-1],fStart+v[0][3][0],eStart+v[0][5][0]] + else: + f = [v[0][0],fStart+v[0][3][0],v[0][1][0],fStart+v[0][3][-1]] + p = [nVerts[i] for i in f] + mid = 0.25*Asum(p) + norm = (p[1]-p[0]).cross(p[2]-p[0]) + dot = norm.dot(mid-p[0])/(norm.length*(mid-p[0]).length) + tollerance = 0.001 # ~ cos(0.06 degrees) + if ((dot > tollerance) and (not supposed_size)) or ((dot < -tollerance) and (supposed_size)): + direction = 1 # first diagonal + elif ((dot < -tollerance) and (not supposed_size)) or ((dot > tollerance) and (supposed_size)): + direction = -1 # second diagonal + else: + direction = 0 # no diagonal, face is planar (somewhat) + + if etrunc: # for every vertex + for i in v: # add the face, consisting of the vert,edge,next + # edge and face between those edges + for j in range(len(i[1])): + f = [i[0],eStart+i[5][j-1],fStart+i[3][j],eStart+i[5][j]] + if direction == 1: # first diagonal + faces.extend([[f[0],f[1],f[3]],[f[1],f[2],f[3]]]) + elif direction == -1: # first diagonal + faces.extend([[f[0],f[1],f[2]],[f[0],f[2],f[3]]]) + else: + faces.append(f) # no diagonal + else: + for i in v: # for every vertex + for j in range(len(i[1])): + if i[0] < i[1][j]: # face consists of vert, vert on other + # end of edge and both faces using that + # edge, so exclude verts allready used + f = [i[0],fStart+i[3][j], i[1][j],fStart+i[3][j-1]] + if direction == -1: # secong diagonal + faces.extend([[f[0],f[1],f[3]],[f[1],f[2],f[3]]]) + elif direction == 1: # first diagonal + faces.extend([[f[0],f[1],f[2]],[f[0],f[2],f[3]]]) + else: + faces.append(f) # no diagonal + verts = nVerts # use new vertices + else: + # concatenate all faces, since they dont have to be used sepperately anymore + faces = face_faces + if etrunc: faces += edge_faces + faces += vert_faces + + return verts,faces + + +class Solids(bpy.types.Operator): + """Add one of the (regular) solids (mesh)""" + bl_idname = "mesh.primitive_solid_add" + bl_label = "(Regular) solids" + bl_description = "Add one of the platoic or archimedean solids" + bl_options = {'REGISTER', 'UNDO'} + + source = EnumProperty(items = (("4","Tetrahedron",""), + ("6","Hexahedron",""), + ("8","Octahedron",""), + ("12","Dodecahedron",""), + ("20","Icosahedron","")), + name = "Source", + description = "Starting point of your solid") + size = FloatProperty(name = "Size", + description = "Radius of the sphere through the vertices", + min = 0.01, + soft_min = 0.01, + max = 100, + soft_max = 100, + default = 1.0) + vTrunc = FloatProperty(name = "Vertex Truncation", + description = "Ammount of vertex truncation", + min = 0.0, + soft_min = 0.0, + max = 2.0, + soft_max = 2.0, + default = 0.0, + precision = 3, + step = 0.5) + eTrunc = FloatProperty(name = "Edge Truncation", + description = "Ammount of edge truncation", + min = 0.0, + soft_min = 0.0, + max = 1.0, + soft_max = 1.0, + default = 0.0, + precision = 3, + step = 0.2) + snub = EnumProperty(items = (("0","No Snub",""), + ("L","Left Snub",""), + ("R","Right Snub","")), + name = "Snub", + description = "Create the snub version") + dual = BoolProperty(name="Dual", + description="Create the dual of the current solid", + default=False) + keepSize = BoolProperty(name="Keep Size", + description="Keep the whole solid at a constant size", + default=False) + preset = EnumProperty(items = (("0","Custom",""), + ("t4","Truncated Tetrahedron",""), + ("r4","Cuboctahedron",""), + ("t6","Truncated Cube",""), + ("t8","Truncated Octahedron",""), + ("b6","Rhombicuboctahedron",""), + ("c6","Truncated Cuboctahedron",""), + ("s6","Snub Cube",""), + ("r12","Icosidodecahedron",""), + ("t12","Truncated Dodecahedron",""), + ("t20","Truncated Icosahedron",""), + ("b12","Rhombicosidodecahedron",""), + ("c12","Truncated Icosidodecahedron",""), + ("s12","Snub Dodecahedron",""), + ("dt4","Triakis Tetrahedron",""), + ("dr4","Rhombic Dodecahedron",""), + ("dt6","Triakis Octahedron",""), + ("dt8","Triakis Hexahedron",""), + ("db6","Deltoidal Icositetrahedron",""), + ("dc6","Disdyakis Dodecahedron",""), + ("ds6","Pentagonal Icositetrahedron",""), + ("dr12","Rhombic Triacontahedron",""), + ("dt12","Triakis Icosahedron",""), + ("dt20","Pentakis Dodecahedron",""), + ("db12","Deltoidal Hexecontahedron",""), + ("dc12","Disdyakis Triacontahedron",""), + ("ds12","Pentagonal Hexecontahedron",""), + ("c","Cube",""), + ("sb","Soccer ball","")), + name = "Presets", + description = "Parameters for some hard names") + + # actual preset values + p = {"t4":["4",2/3,0,0,"0"], + "r4":["4",1,1,0,"0"], + "t6":["6",2/3,0,0,"0"], + "t8":["8",2/3,0,0,"0"], + "b6":["6",1.0938,1,0,"0"], + "c6":["6",1.0572,0.585786,0,"0"], + "s6":["6",1.0875,0.704,0,"L"], + "r12":["12",1,0,0,"0"], + "t12":["12",2/3,0,0,"0"], + "t20":["20",2/3,0,0,"0"], + "b12":["12",1.1338,1,0,"0"], + "c12":["20",0.921,0.553,0,"0"], + "s12":["12",1.1235,0.68,0,"L"], + "dt4":["4",2/3,0,1,"0"], + "dr4":["4",1,2/3,1,"0"], + "dt6":["6",4/3,0,1,"0"], + "dt8":["8",1,0,1,"0"], + "db6":["6",1.0938,0.756,1,"0"], + "dc6":["6",1,1,1,"0"], + "ds6":["6",1.0875,0.704,1,"L"], + "dr12":["12",1.54,0,1,"0"], + "dt12":["12",5/3,0,1,"0"], + "dt20":["20",2/3,0,1,"0"], + "db12":["12",1,0.912,1,"0"], + "dc12":["20",0.921,1,1,"0"], + "ds12":["12",1.1235,0.68,1,"L"], + "c":["6",0,0,0,"0"], + "sb":["20",2/3,0,0,"0"]} + + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + + def execute(self,context): + # turn off undo for better performance (3 - 5x faster), also makes sure + # that mesh ops are undoable and entire script acts as one operator + bpy.context.user_preferences.edit.global_undo = False + + props = self.properties + + #if preset, set preset + if props.preset != "0": + using = self.p[props.preset] + props.source = using[0] + props.vTrunc = using[1] + props.eTrunc = using[2] + props.dual = using[3] + props.snub = using[4] + props.preset = "0" + + # generate mesh + verts,faces = createSolid(props.source, + props.vTrunc, + props.eTrunc, + props.dual, + props.snub) + + # turn n-gons in quads and tri's + faces = createPolys(faces) + + # resize to normal size, or if keepSize, make sure all verts are of length 'size' + if props.keepSize: + rad = props.size/verts[0].length + else: rad = props.size + verts = [i*rad for i in verts] + + # generate object + obj = create_mesh_object(context,verts,[],faces,"Solid",props.edit) + + # vertices will be on top of each other in some cases, + # so remove doubles then + if ((props.vTrunc == 1) and (props.eTrunc == 0)) or (props.eTrunc == 1): + current_mode = obj.mode + if current_mode == 'OBJECT': + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.remove_doubles() + bpy.ops.object.mode_set(mode=current_mode) + + # snub duals suck, so make all normals point outwards + if props.dual and (props.snub != "0"): + current_mode = obj.mode + if current_mode == 'OBJECT': + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.normals_make_consistent() + bpy.ops.object.mode_set(mode=current_mode) + + # turn undo back on + bpy.context.user_preferences.edit.global_undo = True + + return {'FINISHED'} + +class Solids_add_menu(bpy.types.Menu): + """Define the menu with presets""" + bl_idname = "Solids_add_menu" + bl_label = "Solids" + + def draw(self,context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + layout.operator(Solids.bl_idname, text = "Solid") + layout.menu(PlatonicMenu.bl_idname, text = "Platonic") + layout.menu(ArchiMenu.bl_idname, text = "Archimeadean") + layout.menu(CatalanMenu.bl_idname, text = "Catalan") + layout.menu(OtherMenu.bl_idname, text = "Others") + +class PlatonicMenu(bpy.types.Menu): + """Define Platonic menu""" + bl_idname = "Platonic_calls" + bl_label = "Platonic" + + def draw(self,context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + layout.operator(Solids.bl_idname, text = "Tetrahedron").source = "4" + layout.operator(Solids.bl_idname, text = "Hexahedron").source = "6" + layout.operator(Solids.bl_idname, text = "Octahedron").source = "8" + layout.operator(Solids.bl_idname, text = "Dodecahedron").source = "12" + layout.operator(Solids.bl_idname, text = "Icosahedron").source = "20" + +class ArchiMenu(bpy.types.Menu): + """Defines Achimedean preset menu""" + bl_idname = "Achimedean_calls" + bl_label = "Archimedean" + + def draw(self,context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + layout.operator(Solids.bl_idname, text = "Truncated Tetrahedron").preset = "t4" + layout.operator(Solids.bl_idname, text = "Cuboctahedron").preset = "r4" + layout.operator(Solids.bl_idname, text = "Truncated Cube").preset = "t6" + layout.operator(Solids.bl_idname, text = "Truncated Octahedron").preset = "t8" + layout.operator(Solids.bl_idname, text = "Rhombicuboctahedron").preset = "b6" + layout.operator(Solids.bl_idname, text = "Truncated Cuboctahedron").preset = "c6" + layout.operator(Solids.bl_idname, text = "Snub Cube").preset = "s6" + layout.operator(Solids.bl_idname, text = "Icosidodecahedron").preset = "r12" + layout.operator(Solids.bl_idname, text = "Truncated Dodecahedron").preset = "t12" + layout.operator(Solids.bl_idname, text = "Truncated Icosahedron").preset = "t20" + layout.operator(Solids.bl_idname, text = "Rhombicosidodecahedron").preset = "b12" + layout.operator(Solids.bl_idname, text = "Truncated Icosidodecahedron").preset = "c12" + layout.operator(Solids.bl_idname, text = "Snub Dodecahedron").preset = "s12" + +class CatalanMenu(bpy.types.Menu): + """Defines Catalan preset menu""" + bl_idname = "Catalan_calls" + bl_label = "Catalan" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + layout.operator(Solids.bl_idname, text = "Triakis Tetrahedron").preset = "dt4" + layout.operator(Solids.bl_idname, text = "Rhombic Dodecahedron").preset = "dr4" + layout.operator(Solids.bl_idname, text = "Triakis Octahedron").preset = "dt6" + layout.operator(Solids.bl_idname, text = "Triakis Hexahedron").preset = "dt8" + layout.operator(Solids.bl_idname, text = "Deltoidal Icositetrahedron").preset = "db6" + layout.operator(Solids.bl_idname, text = "Disdyakis Dodecahedron").preset = "dc6" + layout.operator(Solids.bl_idname, text = "Pentagonal Icositetrahedron").preset = "ds6" + layout.operator(Solids.bl_idname, text = "Rhombic Triacontahedron").preset = "dr12" + layout.operator(Solids.bl_idname, text = "Triakis Icosahedron").preset = "dt12" + layout.operator(Solids.bl_idname, text = "Pentakis Dodecahedron").preset = "dt20" + layout.operator(Solids.bl_idname, text = "Deltoidal Hexecontahedron").preset = "dt20" + layout.operator(Solids.bl_idname, text = "Disdyakis Triacontahedron").preset = "db12" + layout.operator(Solids.bl_idname, text = "Pentagonal Hexecontahedron").preset = "ds12" + +class OtherMenu(bpy.types.Menu): + """Defines Others preset menu""" + bl_idname = "Others_calls" + bl_label = "Others" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + layout.operator(Solids.bl_idname, text = "Cube").preset = "c" + layout.operator(Solids.bl_idname, text = "Soccer ball").preset = "sb" + + +import space_info + +classes = [ + Solids, + Solids_add_menu, + PlatonicMenu, + ArchiMenu, + CatalanMenu, + OtherMenu +] + +menu_func = (lambda self, + context: self.layout.menu(Solids_add_menu.bl_idname, icon="PLUGIN")) + +def register(): + for i in classes: + bpy.types.register(i) + space_info.INFO_MT_mesh_add.append(menu_func) + +def unregister(): + for i in classes: + bpy.types.unregister(i) + space_info.INFO_MT_mesh_add.remove(menu_func) + +if __name__ == "__main__": + register() diff --git a/add_mesh_twisted_torus.py b/add_mesh_twisted_torus.py new file mode 100644 index 00000000..e2f5b89f --- /dev/null +++ b/add_mesh_twisted_torus.py @@ -0,0 +1,366 @@ +# add_mesh_twisted_torus.py Copyright (C) 2009-2010, Paulo Gomes +# tuga3d {at} gmail {dot} com +# add twisted torus to the blender 2.50 add->mesh menu +# ***** 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +""" + +Usage: + +* Launch from Add Mesh menu + +* Modify parameters as desired or keep defaults + +""" + +bl_addon_info = { + 'name': 'Add Mesh: Twisted Torus', + 'author': 'Paulo_Gomes', + 'version': '0.11', + 'blender': (2, 5, 3), + 'location': 'View3D > Add > Mesh ', + 'description': 'Adds a mesh Twisted Torus to the Add Mesh menu', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Add_Mesh/Add_Twisted_Torus', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=21622&group_id=153&atid=469', + 'category': 'Add Mesh'} + +import bpy +from bpy.props import * + +import mathutils +from mathutils import * +from math import cos, sin, pi + +# calculates the matrix for the new object +# depending on user pref +def align_matrix(context): + loc = TranslationMatrix(context.scene.cursor_location) + obj_align = context.user_preferences.edit.object_align + if (context.space_data.type == 'VIEW_3D' + and obj_align == 'VIEW'): + rot = context.space_data.region_3d.view_matrix.rotation_part().invert().resize4x4() + else: + rot = Matrix() + align_matrix = loc * rot + return align_matrix + + +# Create a new mesh (object) from verts/edges/faces. +# verts/edges/faces ... List of vertices/edges/faces for the +# new mesh (as used in from_pydata). +# name ... Name of the new mesh (& object). +# edit ... Replace existing mesh data. +# Note: Using "edit" will destroy/delete existing mesh data. +def create_mesh_object(context, verts, edges, faces, name, edit, align_matrix): + scene = context.scene + obj_act = scene.objects.active + + # Can't edit anything, unless we have an active obj. + if edit and not obj_act: + return None + + # Create new mesh + mesh = bpy.data.meshes.new(name) + + # Make a mesh from a list of verts/edges/faces. + mesh.from_pydata(verts, edges, faces) + + # Update mesh geometry after adding stuff. + mesh.update() + + # Deselect all objects. + bpy.ops.object.select_all(action='DESELECT') + + if edit: + # Replace geometry of existing object + + # Use the active obj and select it. + ob_new = obj_act + ob_new.select = True + + if obj_act.mode == 'OBJECT': + # Get existing mesh datablock. + old_mesh = ob_new.data + + # Set object data to nothing + ob_new.data = None + + # Clear users of existing mesh datablock. + old_mesh.user_clear() + + # Remove old mesh datablock if no users are left. + if (old_mesh.users == 0): + bpy.data.meshes.remove(old_mesh) + + # Assign new mesh datablock. + ob_new.data = mesh + + else: + # Create new object + ob_new = bpy.data.objects.new(name, mesh) + + # Link new object to the given scene and select it. + scene.objects.link(ob_new) + ob_new.select = True + + # Place the object at the 3D cursor location. + # apply viewRotaion + ob_new.matrix_world = align_matrix + + if obj_act and obj_act.mode == 'EDIT': + if not edit: + # We are in EditMode, switch to ObjectMode. + bpy.ops.object.mode_set(mode='OBJECT') + + # Select the active object as well. + obj_act.select = True + + # Apply location of new object. + scene.update() + + # Join new object into the active. + bpy.ops.object.join() + + # Switching back to EditMode. + bpy.ops.object.mode_set(mode='EDIT') + + ob_new = obj_act + + else: + # We are in ObjectMode. + # Make the new object the active one. + scene.objects.active = ob_new + + return ob_new + + +# A very simple "bridge" tool. +# Connects two equally long vertex rows with faces. +# Returns a list of the new faces (list of lists) +# +# vertIdx1 ... First vertex list (list of vertex indices). +# vertIdx2 ... Second vertex list (list of vertex indices). +# closed ... Creates a loop (first & last are closed). +# flipped ... Invert the normal of the face(s). +# +# Note: You can set vertIdx1 to a single vertex index to create +# a fan/star of faces. +# Note: If both vertex idx list are the same length they have +# to have at least 2 vertices. +def createFaces(vertIdx1, vertIdx2, closed=False, flipped=False): + faces = [] + + if not vertIdx1 or not vertIdx2: + return None + + if len(vertIdx1) < 2 and len(vertIdx2) < 2: + return None + + fan = False + if (len(vertIdx1) != len(vertIdx2)): + if (len(vertIdx1) == 1 and len(vertIdx2) > 1): + fan = True + else: + return None + + total = len(vertIdx2) + + if closed: + # Bridge the start with the end. + if flipped: + face = [ + vertIdx1[0], + vertIdx2[0], + vertIdx2[total - 1]] + if not fan: + face.append(vertIdx1[total - 1]) + faces.append(face) + + else: + face = [vertIdx2[0], vertIdx1[0]] + if not fan: + face.append(vertIdx1[total - 1]) + face.append(vertIdx2[total - 1]) + faces.append(face) + + # Bridge the rest of the faces. + for num in range(total - 1): + if flipped: + if fan: + face = [vertIdx2[num], vertIdx1[0], vertIdx2[num + 1]] + else: + face = [vertIdx2[num], vertIdx1[num], + vertIdx1[num + 1], vertIdx2[num + 1]] + faces.append(face) + else: + if fan: + face = [vertIdx1[0], vertIdx2[num], vertIdx2[num + 1]] + else: + face = [vertIdx1[num], vertIdx2[num], + vertIdx2[num + 1], vertIdx1[num + 1]] + faces.append(face) + + return faces + + +def add_twisted_torus(major_rad, minor_rad, major_seg, minor_seg, twists): + PI_2 = pi * 2.0 + z_axis = (0.0, 0.0, 1.0) + + verts = [] + faces = [] + + edgeloop_prev = [] + for major_index in range(major_seg): + quat = Quaternion(z_axis, (major_index / major_seg) * PI_2) + rot_twists = PI_2 * major_index / major_seg * twists + + edgeloop = [] + + # Create section ring + for minor_index in range(minor_seg): + angle = (PI_2 * minor_index / minor_seg) + rot_twists + + vec = Vector(( + major_rad + (cos(angle) * minor_rad), + 0.0, + sin(angle) * minor_rad)) + vec = vec * quat + + edgeloop.append(len(verts)) + verts.append(vec) + + # Remember very first edgeloop. + if major_index == 0: + edgeloop_first = edgeloop + + # Bridge last with current ring + if edgeloop_prev: + f = createFaces(edgeloop_prev, edgeloop, closed=True) + faces.extend(f) + + edgeloop_prev = edgeloop + + # Bridge first and last ring + f = createFaces(edgeloop_prev, edgeloop_first, closed=True) + faces.extend(f) + + return verts, faces + + +class AddTwistedTorus(bpy.types.Operator): + '''Add a torus mesh''' + bl_idname = "mesh.primitive_twisted_torus_add" + bl_label = "Add Torus" + bl_options = {'REGISTER', 'UNDO'} + + # edit - Whether to add or update. + edit = BoolProperty(name="", + description="", + default=False, + options={'HIDDEN'}) + major_radius = FloatProperty(name="Major Radius", + description="Radius from the origin to the" \ + " center of the cross section", + min=0.01, + max=100.0, + default=1.0) + minor_radius = FloatProperty(name="Minor Radius", + description="Radius of the torus' cross section", + min=0.01, + max=100.0, + default=0.25) + major_segments = IntProperty(name="Major Segments", + description="Number of segments for the main ring of the torus", + min=3, + max=256, + default=48) + minor_segments = IntProperty(name="Minor Segments", + description="Number of segments for the minor ring of the torus", + min=3, + max=256, + default=12) + twists = IntProperty(name="Twists", + description="Number of twists of the torus", + min=0, + max=256, + default=1) + + use_abso = BoolProperty(name="Use Int+Ext Controls", + description="Use the Int / Ext controls for torus dimensions", + default=False) + abso_major_rad = FloatProperty(name="Exterior Radius", + description="Total Exterior Radius of the torus", + min=0.01, + max=100.0, + default=1.0) + abso_minor_rad = FloatProperty(name="Inside Radius", + description="Total Interior Radius of the torus", + min=0.01, + max=100.0, + default=0.5) + align_matrix = Matrix() + + def execute(self, context): + props = self.properties + + if props.use_abso == True: + extra_helper = (props.abso_major_rad - props.abso_minor_rad) * 0.5 + props.major_radius = props.abso_minor_rad + extra_helper + props.minor_radius = extra_helper + + verts, faces = add_twisted_torus( + props.major_radius, + props.minor_radius, + props.major_segments, + props.minor_segments, + props.twists) + + # Actually create the mesh object from this geometry data. + obj = create_mesh_object(context, verts, [], faces, "TwistedTorus", + props.edit, self.align_matrix) + + return {'FINISHED'} + + def invoke(self, context, event): + self.align_matrix = align_matrix(context) + self.execute(context) + return {'FINISHED'} + +# Add to the menu +menu_func = (lambda self, + context: self.layout.operator(AddTwistedTorus.bl_idname, + text="Twisted Torus", icon='MESH_DONUT')) + + +def register(): + bpy.types.register(AddTwistedTorus) + bpy.types.INFO_MT_mesh_add.append(menu_func) + + +def unregister(): + bpy.types.unregister(AddTwistedTorus) + bpy.types.INFO_MT_mesh_add.remove(menu_func) + +if __name__ == "__main__": + register() diff --git a/curve_simplify.py b/curve_simplify.py new file mode 100644 index 00000000..175fffb9 --- /dev/null +++ b/curve_simplify.py @@ -0,0 +1,597 @@ +# ##### 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 ##### + +""" +This script simplifies Curves. +""" + +bl_addon_info = { + 'name': 'Curve: simplify curves', + 'author': 'testscreenings', + 'version': '1', + 'blender': (2, 5, 3), + 'location': 'Toolshelf > search > simplify curves', + 'description': 'This script simplifies 3D curves and fcurves', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Curve/Curve_Simplify', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=22327&group_id=153&atid=468', + 'category': 'Add Curve'} + + +#################################################### +import bpy +from bpy.props import * +import mathutils +import math + +############################## +#### simplipoly algorithm #### +############################## +# get SplineVertIndicies to keep +def simplypoly(splineVerts, options): + # main vars + newVerts = [] # list of vertindices to keep + points = splineVerts # list of 3dVectors + pointCurva = [] # table with curvatures + curvatures = [] # averaged curvatures per vert + for p in points: + pointCurva.append([]) + order = options[3] # order of sliding beziercurves + k_thresh = options[2] # curvature threshold + dis_error = options[6] # additional distance error + + # get curvatures per vert + for i, point in enumerate(points[:-(order-1)]): + BVerts = points[i:i+order] + for b, BVert in enumerate(BVerts[1:-1]): + deriv1 = getDerivative(BVerts, 1/(order-1), order-1) + deriv2 = getDerivative(BVerts, 1/(order-1), order-2) + curva = getCurvature(deriv1, deriv2) + pointCurva[i+b+1].append(curva) + + # average the curvatures + for i in range(len(points)): + avgCurva = sum(pointCurva[i]) / (order-1) + curvatures.append(avgCurva) + + # get distancevalues per vert - same as Ramer-Douglas-Peucker + # but for every vert + distances = [0.0] #first vert is always kept + for i, point in enumerate(points[1:-1]): + dist = altitude(points[i], points[i+2], points[i+1]) + distances.append(dist) + distances.append(0.0) # last vert is always kept + + # generate list of vertindicies to keep + # tested against averaged curvatures and distances of neighbour verts + newVerts.append(0) # first vert is always kept + for i, curv in enumerate(curvatures): + if (curv >= k_thresh*0.01 + or distances[i] >= dis_error*0.1): + newVerts.append(i) + newVerts.append(len(curvatures)-1) # last vert is always kept + + return newVerts + +# get binomial coefficient +def binom(n, m): + b = [0] * (n+1) + b[0] = 1 + for i in range(1, n+1): + b[i] = 1 + j = i-1 + while j > 0: + b[j] += b[j-1] + j-= 1 + return b[m] + +# get nth derivative of order(len(verts)) bezier curve +def getDerivative(verts, t, nth): + order = len(verts) - 1 - nth + QVerts = [] + + if nth: + for i in range(nth): + if QVerts: + verts = QVerts + derivVerts = [] + for i in range(len(verts)-1): + derivVerts.append(verts[i+1] - verts[i]) + QVerts = derivVerts + else: + QVerts = verts + + if len(verts[0]) == 3: + point = mathutils.Vector((0, 0, 0)) + if len(verts[0]) == 2: + point = mathutils.Vector((0, 0)) + + for i, vert in enumerate(QVerts): + point += binom(order, i) * math.pow(t, i) * math.pow(1-t, order-i) * vert + deriv = point + + return deriv + +# get curvature from first, second derivative +def getCurvature(deriv1, deriv2): + if deriv1.length == 0: # in case of points in straight line + curvature = 0 + return curvature + curvature = (deriv1.cross(deriv2)).length / math.pow(deriv1.length, 3) + return curvature + +######################################### +#### Ramer-Douglas-Peucker algorithm #### +######################################### +# get altitude of vert +def altitude(point1, point2, pointn): + edge1 = point2 - point1 + edge2 = pointn - point1 + if edge2.length == 0: + altitude = 0 + return altitude + if edge1.length == 0: + altitude = edge2.length + return altitude + alpha = edge1.angle(edge2) + altitude = math.sin(alpha) * edge2.length + return altitude + +# iterate through verts +def iterate(points, newVerts, error): + new = [] + for newIndex in range(len(newVerts)-1): + bigVert = 0 + alti_store = 0 + for i, point in enumerate(points[newVerts[newIndex]+1:newVerts[newIndex+1]]): + alti = altitude(points[newVerts[newIndex]], points[newVerts[newIndex+1]], point) + if alti > alti_store: + alti_store = alti + if alti_store >= error: + bigVert = i+1+newVerts[newIndex] + if bigVert: + new.append(bigVert) + if new == []: + return False + return new + +#### get SplineVertIndicies to keep +def simplify_RDP(splineVerts, options): + #main vars + error = options[4] + + # set first and last vert + newVerts = [0, len(splineVerts)-1] + + # iterate through the points + new = 1 + while new != False: + new = iterate(splineVerts, newVerts, error) + if new: + newVerts += new + newVerts.sort() + return newVerts + +########################## +#### CURVE GENERATION #### +########################## +# set bezierhandles to auto +def setBezierHandles(newCurve): + scene = bpy.context.scene + bpy.ops.object.mode_set(mode='EDIT', toggle=True) + bpy.ops.curve.select_all(action='SELECT') + bpy.ops.curve.handle_type_set(type='AUTOMATIC') + bpy.ops.object.mode_set(mode='OBJECT', toggle=True) + +# get array of new coords for new spline from vertindices +def vertsToPoints(newVerts, splineVerts, splineType): + # main vars + newPoints = [] + + # array for BEZIER spline output + if splineType == 'BEZIER': + for v in newVerts: + newPoints += splineVerts[v].to_tuple() + + # array for nonBEZIER output + else: + for v in newVerts: + newPoints += (splineVerts[v].to_tuple()) + if splineType == 'NURBS': + newPoints.append(1) #for nurbs w=1 + else: #for poly w=0 + newPoints.append(0) + return newPoints + +######################### +#### MAIN OPERATIONS #### +######################### + +def main(context, obj, options): + #print("\n_______START_______") + # main vars + mode = options[0] + output = options[1] + degreeOut = options[5] + keepShort = options[7] + bpy.ops.object.select_all(action='DESELECT') + scene = context.scene + splines = obj.data.splines.values() + + # create curvedatablock + curve = bpy.data.curves.new("simple_"+obj.name, type = 'CURVE') + + # go through splines + for spline_i, spline in enumerate(splines): + # test if spline is a long enough + if len(spline.points) >= 7 or keepShort: + #check what type of spline to create + if output == 'INPUT': + splineType = spline.type + else: + splineType = output + + # get vec3 list to simplify + if spline.type == 'BEZIER': # get bezierverts + splineVerts = [splineVert.co.copy() + for splineVert in spline.bezier_points.values()] + + else: # verts from all other types of curves + splineVerts = [splineVert.co.copy().resize3D() + for splineVert in spline.points.values()] + + # simplify spline according to mode + if mode == 'distance': + newVerts = simplify_RDP(splineVerts, options) + + if mode == 'curvature': + newVerts = simplypoly(splineVerts, options) + + # convert indicies into vectors3D + newPoints = vertsToPoints(newVerts, splineVerts, splineType) + + # create new spline + newSpline = curve.splines.new(type = splineType) + + # put newPoints into spline according to type + if splineType == 'BEZIER': + newSpline.bezier_points.add(int(len(newPoints)*0.33)) + newSpline.bezier_points.foreach_set('co', newPoints) + else: + newSpline.points.add(int(len(newPoints)*0.25 - 1)) + newSpline.points.foreach_set('co', newPoints) + + # set degree of outputNurbsCurve + if output == 'NURBS': + newSpline.order_u = degreeOut + + # splineoptions + newSpline.endpoint_u = spline.endpoint_u + + # create ne object and put into scene + newCurve = bpy.data.objects.new("simple_"+obj.name, curve) + scene.objects.link(newCurve) + newCurve.select = True + scene.objects.active = newCurve + newCurve.matrix_world = obj.matrix_world + + # set bezierhandles to auto + setBezierHandles(newCurve) + + #print("________END________\n") + return + +################## +## get preoperator fcurves +def getFcurveData(obj): + fcurves = [] + for fc in obj.animation_data.action.fcurves: + if fc.select: + fcVerts = [vcVert.co.copy().resize3D() + for vcVert in fc.keyframe_points.values()] + fcurves.append(fcVerts) + return fcurves + +def selectedfcurves(obj): + fcurves_sel = [] + for i, fc in enumerate(obj.animation_data.action.fcurves): + if fc.select: + fcurves_sel.append(fc) + return fcurves_sel + +########################################################### +## fCurves Main +def fcurves_simplify(context, obj, options, fcurves): + # main vars + mode = options[0] + scene = context.scene + fcurves_obj = obj.animation_data.action.fcurves + + #get indicies of selected fcurves + fcurve_sel = selectedfcurves(obj) + + # go through fcurves + for fcurve_i, fcurve in enumerate(fcurves): + # test if fcurve is long enough + if len(fcurve) >= 7: + + # simplify spline according to mode + if mode == 'distance': + newVerts = simplify_RDP(fcurve, options) + + if mode == 'curvature': + newVerts = simplypoly(fcurve, options) + + # convert indicies into vectors3D + newPoints = [] + + #this is different from the main() function for normal curves, different api... + for v in newVerts: + newPoints.append(fcurve[v]) + + #remove all points from curve first + for i in range(len(fcurve)-1,0,-1): + fcurve_sel[fcurve_i].keyframe_points.remove(fcurve_sel[fcurve_i].keyframe_points[i]) + # put newPoints into fcurve + for v in newPoints: + fcurve_sel[fcurve_i].keyframe_points.add(frame=v[0],value=v[1]) + #fcurve.points.foreach_set('co', newPoints) + return + +################################################# +#### ANIMATION CURVES OPERATOR ################## +################################################# +class GRAPH_OT_simplify(bpy.types.Operator): + '''''' + bl_idname = "graph.simplify" + bl_label = "simplifiy f-curves" + bl_description = "simplify selected f-curves" + bl_options = {'REGISTER', 'UNDO'} + + ## Properties + opModes = [ + ('distance', 'distance', 'distance'), + ('curvature', 'curvature', 'curvature')] + mode = EnumProperty(name="Mode", + description="choose algorithm to use", + items=opModes) + k_thresh = FloatProperty(name="k", + min=0, soft_min=0, + default=0, precision=3, + description="threshold") + pointsNr = IntProperty(name="n", + min=5, soft_min=5, + max=16, soft_max=9, + default=5, + description="degree of curve to get averaged curvatures") + error = FloatProperty(name="error", + description="maximum error to allow - distance", + min=0.0, soft_min=0.0, + default=0, precision=3) + degreeOut = IntProperty(name="degree", + min=3, soft_min=3, + max=7, soft_max=7, + default=5, + description="degree of new curve") + dis_error = FloatProperty(name="distance error", + description="maximum error in Blenderunits to allow - distance", + min=0, soft_min=0, + default=0.0, precision=3) + fcurves = [] + + ''' Remove curvature mode as long as it isnn't significantly improved + + def draw(self, context): + props = self.properties + layout = self.layout + col = layout.column() + col.label('Mode:') + col.prop(props, 'mode', expand=True) + if self.properties.mode == 'distance': + box = layout.box() + box.label(props.mode, icon='ARROW_LEFTRIGHT') + box.prop(props, 'error', expand=True) + if self.properties.mode == 'curvature': + box = layout.box() + box.label('degree', icon='SMOOTHCURVE') + box.prop(props, 'pointsNr', expand=True) + box.label('threshold', icon='PARTICLE_PATH') + box.prop(props, 'k_thresh', expand=True) + box.label('distance', icon='ARROW_LEFTRIGHT') + box.prop(props, 'dis_error', expand=True) + col = layout.column() + ''' + + def draw(self, context): + props = self.properties + layout = self.layout + col = layout.column() + col.prop(props, 'error', expand=True) + + ## Check for animdata + def poll(self, context): + obj = context.active_object + fcurves = False + if obj: + animdata = obj.animation_data + if animdata: + act = animdata.action + if act: + fcurves = act.fcurves + return (obj and fcurves) + + ## execute + def execute(self, context): + #print("------START------") + + options = [ + self.properties.mode, #0 + self.properties.mode, #1 + self.properties.k_thresh, #2 + self.properties.pointsNr, #3 + self.properties.error, #4 + self.properties.degreeOut, #6 + self.properties.dis_error] #7 + + obj = context.active_object + + if not self.fcurves: + self.fcurves = getFcurveData(obj) + + fcurves_simplify(context, obj, options, self.fcurves) + + #print("-------END-------") + return {'FINISHED'} + +########################### +##### Curves OPERATOR ##### +########################### +class CURVE_OT_simplify(bpy.types.Operator): + '''''' + bl_idname = "curve.simplify" + bl_label = "simplifiy curves" + bl_description = "simplify curves" + bl_options = {'REGISTER', 'UNDO'} + + ## Properties + opModes = [ + ('distance', 'distance', 'distance'), + ('curvature', 'curvature', 'curvature')] + mode = EnumProperty(name="Mode", + description="choose algorithm to use", + items=opModes) + SplineTypes = [ + ('INPUT', 'Input', 'same type as input spline'), + ('NURBS', 'Nurbs', 'NURBS'), + ('BEZIER', 'Bezier', 'BEZIER'), + ('POLY', 'Poly', 'POLY')] + output = EnumProperty(name="Output splines", + description="Type of splines to output", + items=SplineTypes) + k_thresh = FloatProperty(name="k", + min=0, soft_min=0, + default=0, precision=3, + description="threshold") + pointsNr = IntProperty(name="n", + min=5, soft_min=5, + max=9, soft_max=9, + default=5, + description="degree of curve to get averaged curvatures") + error = FloatProperty(name="error in Bu", + description="maximum error in Blenderunits to allow - distance", + min=0, soft_min=0, + default=0.0, precision=3) + degreeOut = IntProperty(name="degree", + min=3, soft_min=3, + max=7, soft_max=7, + default=5, + description="degree of new curve") + dis_error = FloatProperty(name="distance error", + description="maximum error in Blenderunits to allow - distance", + min=0, soft_min=0, + default=0.0) + keepShort = BoolProperty(name="keep short Splines", + description="keep short splines (less then 7 points)", + default=True) + + ''' Remove curvature mode as long as it isnn't significantly improved + + def draw(self, context): + props = self.properties + layout = self.layout + col = layout.column() + col.label('Mode:') + col.prop(props, 'mode', expand=True) + if self.properties.mode == 'distance': + box = layout.box() + box.label(props.mode, icon='ARROW_LEFTRIGHT') + box.prop(props, 'error', expand=True) + if self.properties.mode == 'curvature': + box = layout.box() + box.label('degree', icon='SMOOTHCURVE') + box.prop(props, 'pointsNr', expand=True) + box.label('threshold', icon='PARTICLE_PATH') + box.prop(props, 'k_thresh', expand=True) + box.label('distance', icon='ARROW_LEFTRIGHT') + box.prop(props, 'dis_error', expand=True) + col = layout.column() + col.separator() + col.prop(props, 'output', text='Output', icon='OUTLINER_OB_CURVE') + if props.output == 'NURBS': + col.prop(props, 'degreeOut', expand=True) + col.prop(props, 'keepShort', expand=True) + ''' + + def draw(self, context): + props = self.properties + layout = self.layout + col = layout.column() + col.prop(props, 'error', expand=True) + col.prop(props, 'output', text='Output', icon='OUTLINER_OB_CURVE') + if props.output == 'NURBS': + col.prop(props, 'degreeOut', expand=True) + col.prop(props, 'keepShort', expand=True) + + + ## Check for curve + def poll(self, context): + obj = context.active_object + return (obj and obj.type == 'CURVE') + + ## execute + def execute(self, context): + #print("------START------") + + options = [ + self.properties.mode, #0 + self.properties.output, #1 + self.properties.k_thresh, #2 + self.properties.pointsNr, #3 + self.properties.error, #4 + self.properties.degreeOut, #5 + self.properties.dis_error, #6 + self.properties.keepShort] #7 + + + bpy.context.user_preferences.edit.global_undo = False + + bpy.ops.object.mode_set(mode='OBJECT', toggle=True) + obj = context.active_object + + main(context, obj, options) + + bpy.context.user_preferences.edit.global_undo = True + + #print("-------END-------") + return {'FINISHED'} + +################################################# +#### REGISTER ################################### +################################################# +def register(): + bpy.types.register(CURVE_OT_simplify) + bpy.types.register(GRAPH_OT_simplify) + +def unregister(): + bpy.types.unregister(CURVE_OT_simplify) + bpy.types.unregister(GRAPH_OT_simplify) + +if __name__ == "__main__": + register() diff --git a/io_anim_camera.py b/io_anim_camera.py new file mode 100644 index 00000000..23e59fe8 --- /dev/null +++ b/io_anim_camera.py @@ -0,0 +1,159 @@ +# ##### 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 ##### + +# + +bl_addon_info = { + 'name': 'Export: Camera Animation', + 'author': 'Campbell Barton', + 'version': '0.1', + 'blender': (2, 5, 3), + 'location': 'File > Export > Camera Animation', + 'description': 'Export Cameras & Markers', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/File_I-O/Camera_Animation', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=22835&group_id=153&atid=469', + 'category': 'Import/Export'} + + +import bpy + + +def writeCameras(context, filepath, frame_start, frame_end, only_selected=False): + + data_attrs = ['lens', 'shift_x', 'shift_y', 'dof_distance', 'clip_start', 'clip_end', 'draw_size'] + obj_attrs = ['hide_render'] + + fw = open(filepath, 'w').write + + scene = bpy.context.scene + + cameras = [] + + for obj in scene.objects: + if only_selected and not obj.select: + continue + if obj.type != 'CAMERA': + continue + + cameras.append((obj, obj.data)) + + frame_range = range(frame_start, frame_end + 1) + + fw("cameras = {}\n") + fw("scene = bpy.context.scene\n") + fw("frame = scene.frame_current - 1\n") + fw("\n") + + for obj, obj_data in cameras: + fw("data = bpy.data.cameras.new('%s')\n" % obj.name) + for attr in data_attrs: + fw("data.%s = %s\n" % (attr, repr(getattr(obj_data, attr)))) + + fw("obj = bpy.data.objects.new('%s', data)\n" % obj.name) + + for attr in obj_attrs: + fw("obj.%s = %s\n" % (attr, repr(getattr(obj, attr)))) + + fw("scene.objects.link(obj)\n") + fw("cameras['%s'] = obj\n" % obj.name) + fw("\n") + + + for f in frame_range: + scene.set_frame(f) + fw("# new frame\n") + fw("scene.set_frame(%d + frame)\n" % f) + + for obj, obj_data in cameras: + fw("obj = cameras['%s']\n" % obj.name) + + matrix = obj.matrix_world.copy() + fw("obj.location = %s\n" % repr(tuple(matrix.translation_part()))) + fw("obj.scale = %s\n" % repr(tuple(matrix.scale_part()))) + fw("obj.rotation_euler = %s\n" % repr(tuple(matrix.to_euler()))) + + fw("obj.keyframe_insert('location')\n") + fw("obj.keyframe_insert('scale')\n") + fw("obj.keyframe_insert('rotation_euler')\n") + + # only key the angle + fw("data = obj.data\n") + fw("data.lens = %s\n" % obj_data.lens) + fw("data.keyframe_insert('lens')\n") + + fw("\n") + + # now markers + fw("# markers\n") + for marker in scene.timeline_markers: + fw("marker = scene.timeline_markers.add('%s')\n" % marker.name) + fw("marker.frame = %d + frame\n" % marker.frame) + + # will fail if the cameras not selected + if marker.camera: + fw("marker.camera = cameras.get('%s')\n" % marker.camera.name) + fw("\n") + + +from bpy.props import * + + +class CameraExporter(bpy.types.Operator): + '''Save a python script which re-creartes cameras and markers elsewhere''' + bl_idname = "export_animation.cameras" + bl_label = "Export Camera & Markers" + + filepath = StringProperty(name="File Path", description="File path used for importing the RAW file", maxlen=1024, default="") + + frame_start = IntProperty(name="Start Frame", + description="Start frame for export", + default=1, min=1, max=300000) + frame_end = IntProperty(name="End Frame", + description="End frame for export", + default=250, min=1, max=300000) + only_selected = BoolProperty(name="Only Selected", + default=True) + + def execute(self, context): + writeCameras(context, self.properties.filepath, self.properties.frame_start, self.properties.frame_end, self.properties.only_selected) + return {'FINISHED'} + + def invoke(self, context, event): + self.properties.frame_start = context.scene.frame_start + self.properties.frame_end = context.scene.frame_end + + wm = context.manager + wm.add_fileselect(self) + return {'RUNNING_MODAL'} + + +def menu_export(self, context): + import os + default_path = os.path.splitext(bpy.data.filepath)[0] + ".py" + self.layout.operator(CameraExporter.bl_idname, text="Cameras & Markers (.py)").filepath = default_path + + +def register(): + bpy.types.register(CameraExporter) + bpy.types.INFO_MT_file_export.append(menu_export) + +if __name__ == "__main__": + register() diff --git a/io_export_directx_x.py b/io_export_directx_x.py new file mode 100644 index 00000000..7fb56308 --- /dev/null +++ b/io_export_directx_x.py @@ -0,0 +1,1077 @@ +# ***** 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 3 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, see . +# All rights reserved. +# ***** GPL LICENSE BLOCK ***** + +bl_addon_info = { + 'name': 'Export: DirectX Model Format (.x)', + 'author': 'Chris Foster (Kira Vakaan)', + 'version': '1.4', + 'blender': (2, 5, 3), + 'location': 'File > Export', + 'description': 'Export to the DirectX Model Format (.x)', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/File_I-O/DirectX_Exporter', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=22795&group_id=153&atid=469', + 'category': 'Import/Export'} + + +import os +from math import radians + +import bpy +from mathutils import * + +#Container for the exporter settings +class DirectXExporterSettings: + def __init__(self, + context, + FilePath, + CoordinateSystem=1, + RotateX=True, + FlipNormals=False, + ApplyModifiers=False, + IncludeFrameRate=False, + ExportTextures=True, + ExportArmatures=False, + ExportAnimation=0, + ExportMode=1, + Verbose=False): + self.context = context + self.FilePath = FilePath + self.CoordinateSystem = int(CoordinateSystem) + self.RotateX = RotateX + self.FlipNormals = FlipNormals + self.ApplyModifiers = ApplyModifiers + self.IncludeFrameRate = IncludeFrameRate + self.ExportTextures = ExportTextures + self.ExportArmatures = ExportArmatures + self.ExportAnimation = int(ExportAnimation) + self.ExportMode = int(ExportMode) + self.Verbose = Verbose + + +def LegalName(Name): + NewName = Name.replace(".", "_") + NewName = NewName.replace(" ", "_") + if NewName[0].isdigit() or NewName in ["ARRAY", + "DWORD", + "UCHAR", + "BINARY", + "FLOAT", + "ULONGLONG", + "BINARY_RESOURCE", + "SDWORD", + "UNICODE", + "CHAR", + "STRING", + "WORD", + "CSTRING", + "SWORD", + "DOUBLE", + "TEMPLATE"]: + NewName = "_" + NewName + return NewName + + +def ExportDirectX(Config): + print("----------\nExporting to {}".format(Config.FilePath)) + if Config.Verbose: + print("Opening File...", end=" ") + Config.File = open(Config.FilePath, "w") + if Config.Verbose: + print("Done") + + if Config.Verbose: + print("Generating Object list for export...", end=" ") + if Config.ExportMode == 1: + Config.ExportList = [Object for Object in Config.context.scene.objects + if Object.type in ("ARMATURE", "EMPTY", "MESH") + and Object.parent == None] + else: + ExportList = [Object for Object in Config.context.selected_objects + if Object.type in ("ARMATURE", "EMPTY", "MESH")] + Config.ExportList = [Object for Object in ExportList + if Object.parent not in ExportList] + if Config.Verbose: + print("Done") + + if Config.Verbose: + print("Setting up...", end=" ") + Config.SystemMatrix = Matrix() + if Config.RotateX: + Config.SystemMatrix *= RotationMatrix(radians(-90), 4, "X") + if Config.CoordinateSystem == 1: + Config.SystemMatrix *= ScaleMatrix(-1, 4, Vector((0, 1, 0))) + Config.InverseSystemMatrix = Config.SystemMatrix.copy().invert() + + #Used for animating rotations + Config.SystemQuaternion = Quaternion((1,0,0,0)) + if Config.RotateX: + Config.SystemQuaternion = RotationMatrix(radians(-90), 3, "X").to_quat() + Config.InverseSystemQuaternion = Config.SystemQuaternion.copy().inverse() + Config.FlipZ = -1 if Config.CoordinateSystem == 1 else 1 + + if Config.ExportAnimation: + CurrentFrame = bpy.context.scene.frame_current + bpy.context.scene.frame_current = bpy.context.scene.frame_current + if Config.Verbose: + print("Done") + + if Config.Verbose: + print("Writing Header...", end=" ") + WriteHeader(Config) + if Config.Verbose: + print("Done") + + Config.Whitespace = 0 + Config.ObjectList = [] + if Config.Verbose: + print("Writing Objects...") + WriteObjects(Config, Config.ExportList) + if Config.Verbose: + print("Done") + + if Config.ExportAnimation: + if Config.IncludeFrameRate: + if Config.Verbose: + print("Writing Frame Rate...", end=" ") + Config.File.write("{}AnimTicksPerSecond {{\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + Config.File.write("{}{};\n".format(" " * Config.Whitespace, int(bpy.context.scene.render.fps / bpy.context.scene.render.fps_base))) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + if Config.Verbose: + print("Done") + if Config.Verbose: + print("Writing Animation...") + if Config.ExportAnimation==1: + WriteKeyedAnimationSet(Config) + else: + WriteFullAnimationSet(Config) + bpy.context.scene.frame_current = CurrentFrame + if Config.Verbose: + print("Done") + + CloseFile(Config) + print("Finished") + + +def GetObjectChildren(Parent): + return [Object for Object in Parent.children + if Object.type in ("ARMATURE", "EMPTY", "MESH")] + +#Returns the vertex count of Mesh, counting each vertex for every face. +def GetMeshVertexCount(Mesh): + VertexCount = 0 + for Face in Mesh.faces: + VertexCount += len(Face.verts) + return VertexCount + +#Returns the file path of first image texture from Material. +def GetMaterialTexture(Material): + if Material: + #Create a list of Textures that have type "IMAGE" + ImageTextures = [Material.texture_slots[TextureSlot].texture for TextureSlot in Material.texture_slots.keys() if Material.texture_slots[TextureSlot].texture.type == "IMAGE"] + #Refine a new list with only image textures that have a file source + ImageFiles = [os.path.basename(Texture.image.filepath) for Texture in ImageTextures if Texture.image.source == "FILE"] + if ImageFiles: + return ImageFiles[0] + return None + + +def WriteHeader(Config): + Config.File.write("xof 0303txt 0032\n\n") + if Config.ExportArmatures: + Config.File.write("template XSkinMeshHeader {\n\ + <3cf169ce-ff7c-44ab-93c0-f78f62d172e2>\n\ + WORD nMaxSkinWeightsPerVertex;\n\ + WORD nMaxSkinWeightsPerFace;\n\ + WORD nBones;\n\ +}\n\n\ +template SkinWeights {\n\ + <6f0d123b-bad2-4167-a0d0-80224f25fabb>\n\ + STRING transformNodeName;\n\ + DWORD nWeights;\n\ + array DWORD vertexIndices[nWeights];\n\ + array float weights[nWeights];\n\ + Matrix4x4 matrixOffset;\n\ +}\n\n") + + +def WriteObjects(Config, ObjectList): + Config.ObjectList += ObjectList + + for Object in ObjectList: + if Config.Verbose: + print(" Writing Object: {}...".format(Object.name)) + Config.File.write("{}Frame {} {{\n".format(" " * Config.Whitespace, LegalName(Object.name))) + + Config.Whitespace += 1 + if Config.Verbose: + print(" Writing Local Matrix...", end=" ") + WriteLocalMatrix(Config, Object) + if Config.Verbose: + print("Done") + + if Config.ExportArmatures and Object.type == "ARMATURE": + Armature = Object.data + ParentList = [Bone for Bone in Armature.bones if Bone.parent == None] + if Config.Verbose: + print(" Writing Armature Bones...") + WriteArmatureBones(Config, Object, ParentList) + if Config.Verbose: + print(" Done") + + ChildList = GetObjectChildren(Object) + if Config.Verbose: + print(" Writing Children...") + WriteObjects(Config, ChildList) + if Config.Verbose: + print(" Done Writing Children") + + if Object.type == "MESH": + if Config.Verbose: + print(" Generating Mesh...", end=" ") + if Config.ApplyModifiers: + if Config.ExportArmatures: + #Create a copy of the object and remove all armature modifiers so an unshaped + #mesh can be created from it. + Object2 = Object.copy() + for Modifier in [Modifier for Modifier in Object2.modifiers if Modifier.type == "ARMATURE"]: + Object2.modifiers.remove(Modifier) + Mesh = Object2.create_mesh(bpy.context.scene, True, "PREVIEW") + else: + Mesh = Object.create_mesh(bpy.context.scene, True, "PREVIEW") + else: + Mesh = Object.create_mesh(bpy.context.scene, False, "PREVIEW") + if Config.Verbose: + print("Done") + print(" Writing Mesh...") + WriteMesh(Config, Object, Mesh) + if Config.Verbose: + print(" Done") + if Config.ApplyModifiers and Config.ExportArmatures: + bpy.data.objects.remove(Object2) + bpy.data.meshes.remove(Mesh) + + Config.Whitespace -= 1 + Config.File.write("{}}} //End of {}\n".format(" " * Config.Whitespace, LegalName(Object.name))) + if Config.Verbose: + print(" Done Writing Object: {}".format(Object.name)) + + +def WriteLocalMatrix(Config, Object): + LocalMatrix = Config.SystemMatrix * Object.matrix_local * Config.InverseSystemMatrix + + Config.File.write("{}FrameTransformMatrix {{\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + Config.File.write("{}{:9f},{:9f},{:9f},{:9f},\n".format(" " * Config.Whitespace, LocalMatrix[0][0], LocalMatrix[0][1], LocalMatrix[0][2], LocalMatrix[0][3])) + Config.File.write("{}{:9f},{:9f},{:9f},{:9f},\n".format(" " * Config.Whitespace, LocalMatrix[1][0], LocalMatrix[1][1], LocalMatrix[1][2], LocalMatrix[1][3])) + Config.File.write("{}{:9f},{:9f},{:9f},{:9f},\n".format(" " * Config.Whitespace, LocalMatrix[2][0], LocalMatrix[2][1], LocalMatrix[2][2], LocalMatrix[2][3])) + Config.File.write("{}{:9f},{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, LocalMatrix[3][0], LocalMatrix[3][1], LocalMatrix[3][2], LocalMatrix[3][3])) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + + +def WriteArmatureBones(Config, Object, ChildList): + PoseBones = Object.pose.bones + for Bone in ChildList: + if Config.Verbose: + print(" Writing Bone: {}...".format(Bone.name), end=" ") + Config.File.write("{}Frame {} {{\n".format(" " * Config.Whitespace, LegalName(Object.name) + "_" + LegalName(Bone.name))) + Config.Whitespace += 1 + + PoseBone = PoseBones[Bone.name] + + if Bone.parent: + BoneMatrix = (PoseBone.parent.matrix * + RotationMatrix(radians(-90), 4, "X")).invert() + else: + BoneMatrix = Matrix() + + BoneMatrix *= PoseBone.matrix * RotationMatrix(radians(-90), 4, "X") + BoneMatrix = Config.SystemMatrix * BoneMatrix * Config.InverseSystemMatrix + + Config.File.write("{}FrameTransformMatrix {{\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + Config.File.write("{}{:9f},{:9f},{:9f},{:9f},\n".format(" " * Config.Whitespace, BoneMatrix[0][0], BoneMatrix[0][1], BoneMatrix[0][2], BoneMatrix[0][3])) + Config.File.write("{}{:9f},{:9f},{:9f},{:9f},\n".format(" " * Config.Whitespace, BoneMatrix[1][0], BoneMatrix[1][1], BoneMatrix[1][2], BoneMatrix[1][3])) + Config.File.write("{}{:9f},{:9f},{:9f},{:9f},\n".format(" " * Config.Whitespace, BoneMatrix[2][0], BoneMatrix[2][1], BoneMatrix[2][2], BoneMatrix[2][3])) + Config.File.write("{}{:9f},{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, BoneMatrix[3][0], BoneMatrix[3][1], BoneMatrix[3][2], BoneMatrix[3][3])) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + + if Config.Verbose: + print("Done") + WriteArmatureBones(Config, Object, Bone.children) + Config.Whitespace -= 1 + + Config.File.write("{}}} //End of {}\n".format(" " * Config.Whitespace, LegalName(Object.name) + "_" + LegalName(Bone.name))) + + +def WriteMesh(Config, Object, Mesh): + Config.File.write("{}Mesh {{ //{} Mesh\n".format(" " * Config.Whitespace, LegalName(Mesh.name))) + Config.Whitespace += 1 + + if Config.Verbose: + print(" Writing Mesh Vertices...", end=" ") + WriteMeshVertices(Config, Mesh) + if Config.Verbose: + print("Done\n Writing Mesh Normals...", end=" ") + WriteMeshNormals(Config, Mesh) + if Config.Verbose: + print("Done\n Writing Mesh Materials...", end=" ") + WriteMeshMaterials(Config, Mesh) + if Config.Verbose: + print("Done") + if Mesh.uv_textures: + if Config.Verbose: + print(" Writing Mesh UV Coordinates...", end=" ") + WriteMeshUVCoordinates(Config, Mesh) + if Config.Verbose: + print("Done") + if Config.ExportArmatures: + if Config.Verbose: + print(" Writing Mesh Skin Weights...", end=" ") + WriteMeshSkinWeights(Config, Object, Mesh) + if Config.Verbose: + print("Done") + + Config.Whitespace -= 1 + Config.File.write("{}}} //End of {} Mesh\n".format(" " * Config.Whitespace, LegalName(Mesh.name))) + + +def WriteMeshVertices(Config, Mesh): + Index = 0 + VertexCount = GetMeshVertexCount(Mesh) + Config.File.write("{}{};\n".format(" " * Config.Whitespace, VertexCount)) + + for Face in Mesh.faces: + Vertices = list(Face.verts) + + if Config.CoordinateSystem == 1: + Vertices = Vertices[::-1] + for Vertex in [Mesh.verts[Vertex] for Vertex in Vertices]: + Position = Config.SystemMatrix * Vertex.co + Config.File.write("{}{:9f};{:9f};{:9f};".format(" " * Config.Whitespace, Position[0], Position[1], Position[2])) + Index += 1 + if Index == VertexCount: + Config.File.write(";\n") + else: + Config.File.write(",\n") + + Index = 0 + Config.File.write("{}{};\n".format(" " * Config.Whitespace, len(Mesh.faces))) + + for Face in Mesh.faces: + Config.File.write("{}{};".format(" " * Config.Whitespace, len(Face.verts))) + for Vertex in Face.verts: + Config.File.write("{};".format(Index)) + Index += 1 + if Index == VertexCount: + Config.File.write(";\n") + else: + Config.File.write(",\n") + + +def WriteMeshNormals(Config, Mesh): + Config.File.write("{}MeshNormals {{ //{} Normals\n".format(" " * Config.Whitespace, LegalName(Mesh.name))) + Config.Whitespace += 1 + + Index = 0 + VertexCount = GetMeshVertexCount(Mesh) + Config.File.write("{}{};\n".format(" " * Config.Whitespace, VertexCount)) + + for Face in Mesh.faces: + Vertices = list(Face.verts) + + if Config.CoordinateSystem == 1: + Vertices = Vertices[::-1] + for Vertex in [Mesh.verts[Vertex] for Vertex in Vertices]: + if Face.smooth: + Normal = Config.SystemMatrix * Vertex.normal + else: + Normal = Config.SystemMatrix * Face.normal + if Config.FlipNormals: + Normal = -Normal + Config.File.write("{}{:9f};{:9f};{:9f};".format(" " * Config.Whitespace, Normal[0], Normal[1], Normal[2])) + Index += 1 + if Index == VertexCount: + Config.File.write(";\n") + else: + Config.File.write(",\n") + + Index = 0 + Config.File.write("{}{};\n".format(" " * Config.Whitespace, len(Mesh.faces))) + + for Face in Mesh.faces: + Config.File.write("{}{};".format(" " * Config.Whitespace, len(Face.verts))) + for Vertex in Face.verts: + Config.File.write("{};".format(Index)) + Index += 1 + if Index == VertexCount: + Config.File.write(";\n") + else: + Config.File.write(",\n") + Config.Whitespace -= 1 + Config.File.write("{}}} //End of {} Normals\n".format(" " * Config.Whitespace, LegalName(Mesh.name))) + + +def WriteMeshMaterials(Config, Mesh): + Config.File.write("{}MeshMaterialList {{ //{} Material List\n".format(" " * Config.Whitespace, LegalName(Mesh.name))) + Config.Whitespace += 1 + + Materials = Mesh.materials + if Materials.keys(): + MaterialIndexes = {} + for Face in Mesh.faces: + if Materials[Face.material_index] not in MaterialIndexes: + MaterialIndexes[Materials[Face.material_index]] = len(MaterialIndexes) + + FaceCount = len(Mesh.faces) + Index = 0 + Config.File.write("{}{};\n{}{};\n".format(" " * Config.Whitespace, len(MaterialIndexes), " " * Config.Whitespace, FaceCount)) + for Face in Mesh.faces: + Config.File.write("{}{}".format(" " * Config.Whitespace, MaterialIndexes[Materials[Face.material_index]])) + Index += 1 + if Index == FaceCount: + Config.File.write(";;\n") + else: + Config.File.write(",\n") + + Materials = [Item[::-1] for Item in MaterialIndexes.items()] + Materials.sort() + for Material in Materials: + WriteMaterial(Config, Material[1]) + else: + Config.File.write("{}1;\n{}1;\n{}0;;\n".format(" " * Config.Whitespace, " " * Config.Whitespace, " " * Config.Whitespace)) + WriteMaterial(Config) + + Config.Whitespace -= 1 + Config.File.write("{}}} //End of {} Material List\n".format(" " * Config.Whitespace, LegalName(Mesh.name))) + + +def WriteMaterial(Config, Material=None): + if Material: + Config.File.write("{}Material {} {{\n".format(" " * Config.Whitespace, LegalName(Material.name))) + Config.Whitespace += 1 + + Diffuse = list(Material.diffuse_color) + Diffuse.append(Material.alpha) + Specularity = Material.specular_intensity + Specular = list(Material.specular_color) + + Config.File.write("{}{:9f};{:9f};{:9f};{:9f};;\n".format(" " * Config.Whitespace, Diffuse[0], Diffuse[1], Diffuse[2], Diffuse[3])) + Config.File.write("{}{:9f};\n".format(" " * Config.Whitespace, 2 * (1.0 - Specularity))) + Config.File.write("{}{:9f};{:9f};{:9f};;\n".format(" " * Config.Whitespace, Specular[0], Specular[1], Specular[2])) + else: + Config.File.write("{}Material Default_Material {{\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + Config.File.write("{} 1.000000; 1.000000; 1.000000; 1.000000;;\n".format(" " * Config.Whitespace)) + Config.File.write("{} 1.500000;\n".format(" " * Config.Whitespace)) + Config.File.write("{} 1.000000; 1.000000; 1.000000;;\n".format(" " * Config.Whitespace)) + Config.File.write("{} 0.000000; 0.000000; 0.000000;;\n".format(" " * Config.Whitespace)) + if Config.ExportTextures: + Texture = GetMaterialTexture(Material) + if Texture: + Config.File.write("{}TextureFilename {{\"{}\";}}\n".format(" " * Config.Whitespace, Texture)) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + + +def WriteMeshUVCoordinates(Config, Mesh): + Config.File.write("{}MeshTextureCoords {{ //{} UV Coordinates\n".format(" " * Config.Whitespace, LegalName(Mesh.name))) + Config.Whitespace += 1 + + UVCoordinates = None + for UV in Mesh.uv_textures: + if UV.active_render: + UVCoordinates = UV.data + break + + Index = 0 + VertexCount = GetMeshVertexCount(Mesh) + Config.File.write("{}{};\n".format(" " * Config.Whitespace, VertexCount)) + + for Face in UVCoordinates: + Vertices = [] + for Vertex in Face.uv: + Vertices.append(tuple(Vertex)) + if Config.CoordinateSystem == 1: + Vertices = Vertices[::-1] + for Vertex in Vertices: + Config.File.write("{}{:9f};{:9f};".format(" " * Config.Whitespace, Vertex[0], 1 - Vertex[1])) + Index += 1 + if Index == VertexCount: + Config.File.write(";\n") + else: + Config.File.write(",\n") + Config.Whitespace -= 1 + Config.File.write("{}}} //End of {} UV Coordinates\n".format(" " * Config.Whitespace, LegalName(Mesh.name))) + + +def WriteMeshSkinWeights(Config, Object, Mesh): + ArmatureList = [Modifier for Modifier in Object.modifiers if Modifier.type == "ARMATURE"] + if ArmatureList: + ArmatureObject = ArmatureList[0].object + ArmatureBones = ArmatureObject.data.bones + + PoseBones = ArmatureObject.pose.bones + + MaxInfluences = 0 + UsedBones = set() + #Maps bones to a list of vertices they affect + VertexGroups = {} + + for Vertex in Mesh.verts: + #BoneInfluences contains the bones of the armature that affect the current vertex + BoneInfluences = [PoseBones[Object.vertex_groups[Group.group].name] for Group in Vertex.groups if Object.vertex_groups[Group.group].name in PoseBones] + if len(BoneInfluences) > MaxInfluences: + MaxInfluences = len(BoneInfluences) + for Bone in BoneInfluences: + UsedBones.add(Bone) + if Bone not in VertexGroups: + VertexGroups[Bone] = [Vertex] + else: + VertexGroups[Bone].append(Vertex) + BoneCount = len(UsedBones) + + Config.File.write("{}XSkinMeshHeader {{\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + Config.File.write("{}{};\n{}{};\n{}{};\n".format(" " * Config.Whitespace, MaxInfluences, " " * Config.Whitespace, MaxInfluences * 3, " " * Config.Whitespace, BoneCount)) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + + for Bone in UsedBones: + VertexCount = 0 + VertexIndexes = [Vertex.index for Vertex in VertexGroups[Bone]] + for Face in Mesh.faces: + for Vertex in Face.verts: + if Vertex in VertexIndexes: + VertexCount += 1 + + Config.File.write("{}SkinWeights {{\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + Config.File.write("{}\"{}\";\n{}{};\n".format(" " * Config.Whitespace, LegalName(ArmatureObject.name) + "_" + LegalName(Bone.name), " " * Config.Whitespace, VertexCount)) + + VertexWeights = [] + Index = 0 + WrittenIndexes = 0 + for Face in Mesh.faces: + FaceVertices = list(Face.verts) + if Config.CoordinateSystem == 1: + FaceVertices = FaceVertices[::-1] + for Vertex in FaceVertices: + if Vertex in VertexIndexes: + Config.File.write("{}{}".format(" " * Config.Whitespace, Index)) + + GroupIndexes = {Object.vertex_groups[Group.group].name: Index for Index, Group in enumerate(Mesh.verts[Vertex].groups) if Object.vertex_groups[Group.group].name in PoseBones} + + WeightTotal = 0.0 + for Weight in [Group.weight for Group in Mesh.verts[Vertex].groups if Object.vertex_groups[Group.group].name in PoseBones]: + WeightTotal += Weight + + if WeightTotal: + VertexWeights.append(Mesh.verts[Vertex].groups[GroupIndexes[Bone.name]].weight / WeightTotal) + else: + VertexWeights.append(0.0) + + WrittenIndexes += 1 + if WrittenIndexes == VertexCount: + Config.File.write(";\n") + else: + Config.File.write(",\n") + Index += 1 + + for Index, Weight in enumerate(VertexWeights): + Config.File.write("{}{:8f}".format(" " * Config.Whitespace, Weight)) + if Index == (VertexCount - 1): + Config.File.write(";\n") + else: + Config.File.write(",\n") + + RestBone = ArmatureBones[Bone.name] + + #BoneMatrix transforms mesh vertices into the space of the bone. + #Here are the final transformations in order: + # - Object Space to World Space + # - World Space to Armature Space + # - Armature Space to Bone Space (The bone matrix needs to be rotated 90 degrees to align with Blender's world axes) + #This way, when BoneMatrix is transformed by the bone's Frame matrix, the vertices will be in their final world position. + + BoneMatrix = (RestBone.matrix_local * RotationMatrix(radians(-90), 4, "X")).invert() + BoneMatrix *= ArmatureObject.matrix_world.copy().invert() + BoneMatrix *= Object.matrix_world + + BoneMatrix = Config.SystemMatrix * BoneMatrix * Config.InverseSystemMatrix + + Config.File.write("{}{:9f},{:9f},{:9f},{:9f},\n".format(" " * Config.Whitespace, BoneMatrix[0][0], BoneMatrix[0][1], BoneMatrix[0][2], BoneMatrix[0][3])) + Config.File.write("{}{:9f},{:9f},{:9f},{:9f},\n".format(" " * Config.Whitespace, BoneMatrix[1][0], BoneMatrix[1][1], BoneMatrix[1][2], BoneMatrix[1][3])) + Config.File.write("{}{:9f},{:9f},{:9f},{:9f},\n".format(" " * Config.Whitespace, BoneMatrix[2][0], BoneMatrix[2][1], BoneMatrix[2][2], BoneMatrix[2][3])) + Config.File.write("{}{:9f},{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, BoneMatrix[3][0], BoneMatrix[3][1], BoneMatrix[3][2], BoneMatrix[3][3])) + Config.Whitespace -= 1 + Config.File.write("{}}} //End of {} Skin Weights\n".format(" " * Config.Whitespace, LegalName(ArmatureObject.name) + "_" + LegalName(Bone.name))) + + +def WriteKeyedAnimationSet(Config): + Config.File.write("{}AnimationSet {{\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + for Object in [Object for Object in Config.ObjectList if Object.animation_data]: + if Config.Verbose: + print(" Writing Animation Data for Object: {}".format(Object.name)) + Action = Object.animation_data.action + if Action: + PositionFCurves = [None, None, None] + RotationFCurves = [None, None, None] + ScaleFCurves = [None, None, None] + for FCurve in Action.fcurves: + if FCurve.data_path == "location": + PositionFCurves[FCurve.array_index] = FCurve + elif FCurve.data_path == "rotation_euler": + RotationFCurves[FCurve.array_index] = FCurve + elif FCurve.data_path == "scale": + ScaleFCurves[FCurve.array_index] = FCurve + if [FCurve for FCurve in PositionFCurves + RotationFCurves + ScaleFCurves if FCurve]: + Config.File.write("{}Animation {{\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + Config.File.write("{}{{{}}}\n".format(" " * Config.Whitespace, LegalName(Object.name))) + + #Position + if Config.Verbose: + print(" Writing Position...", end=" ") + AllKeyframes = set() + for Index, FCurve in enumerate(PositionFCurves): + if FCurve: + Keyframes = [] + for Keyframe in FCurve.keyframe_points: + Keyframes.append(Keyframe.co) + AllKeyframes.add(int(Keyframe.co[0])) + PositionFCurves[Index] = {int(Keyframe): Value for Keyframe, Value in Keyframes} + Config.File.write("{}AnimationKey {{ //Position\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + AllKeyframes = list(AllKeyframes) + AllKeyframes.sort() + if len(AllKeyframes): + Config.File.write("{}2;\n{}{};\n".format(" " * Config.Whitespace, " " * Config.Whitespace, len(AllKeyframes))) + for Keyframe in AllKeyframes: + bpy.context.scene.set_frame(Keyframe) + Position = Vector() + Position[0] = ((PositionFCurves[0][Keyframe] if Keyframe in PositionFCurves[0] else Object.location[0]) if PositionFCurves[0] else Object.location[0]) + Position[1] = ((PositionFCurves[1][Keyframe] if Keyframe in PositionFCurves[1] else Object.location[1]) if PositionFCurves[1] else Object.location[1]) + Position[2] = ((PositionFCurves[2][Keyframe] if Keyframe in PositionFCurves[2] else Object.location[2]) if PositionFCurves[2] else Object.location[2]) + Position = Config.SystemMatrix * Position + Config.File.write("{}{}{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, (str(Keyframe - bpy.context.scene.frame_start) + ";3;").ljust(8), Position[0], Position[1], Position[2])) + else: + Config.File.write("{}2;\n{}1;\n".format(" " * Config.Whitespace, " " * Config.Whitespace)) + bpy.context.scene.set_frame(bpy.context.scene.frame_start) + Position = Config.SystemMatrix * Object.location + Config.File.write("{}{}{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, ("0;3;").ljust(8), Position[0], Position[1], Position[2])) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + if Config.Verbose: + print("Done") + + #Rotation + if Config.Verbose: + print(" Writing Rotation...", end=" ") + AllKeyframes = set() + for Index, FCurve in enumerate(RotationFCurves): + if FCurve: + Keyframes = [] + for Keyframe in FCurve.keyframe_points: + Keyframes.append(Keyframe.co) + AllKeyframes.add(int(Keyframe.co[0])) + RotationFCurves[Index] = {int(Keyframe): Value for Keyframe, Value in Keyframes} + Config.File.write("{}AnimationKey {{ //Rotation\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + AllKeyframes = list(AllKeyframes) + AllKeyframes.sort() + if len(AllKeyframes): + Config.File.write("{}0;\n{}{};\n".format(" " * Config.Whitespace, " " * Config.Whitespace, len(AllKeyframes))) + for Keyframe in AllKeyframes: + bpy.context.scene.set_frame(Keyframe) + Rotation = Euler() + Rotation[0] = ((RotationFCurves[0][Keyframe] if Keyframe in RotationFCurves[0] else Object.rotation_euler[0]) if RotationFCurves[0] else Object.rotation_euler[0]) + Rotation[1] = ((RotationFCurves[1][Keyframe] if Keyframe in RotationFCurves[1] else Object.rotation_euler[1]) if RotationFCurves[1] else Object.rotation_euler[1]) + Rotation[2] = ((RotationFCurves[2][Keyframe] if Keyframe in RotationFCurves[2] else Object.rotation_euler[2]) if RotationFCurves[2] else Object.rotation_euler[2]) + Rotation = (Config.SystemMatrix * (Rotation.to_matrix().to_4x4()) * Config.InverseSystemMatrix).to_quat() + Config.File.write("{}{}{:9f},{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, (str(Keyframe - bpy.context.scene.frame_start) + ";4;").ljust(8), - Rotation[0], Rotation[1], Rotation[2], Rotation[3])) + else: + Config.File.write("{}0;\n{}1;\n".format(" " * Config.Whitespace, " " * Config.Whitespace)) + bpy.context.scene.set_frame(bpy.context.scene.frame_start) + Rotation = (Config.SystemMatrix * (Object.rotation_euler.to_matrix().to_4x4()) * Config.InverseSystemMatrix).to_quat() + Config.File.write("{}{}{:9f},{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, ("0;4;").ljust(8), -Rotation[0], Rotation[1], Rotation[2], Rotation[3])) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + if Config.Verbose: + print("Done") + + #Scale + if Config.Verbose: + print(" Writing Scale...", end=" ") + AllKeyframes = set() + for Index, FCurve in enumerate(ScaleFCurves): + if FCurve: + Keyframes = [] + for Keyframe in FCurve.keyframe_points: + Keyframes.append(Keyframe.co) + AllKeyframes.add(int(Keyframe.co[0])) + ScaleFCurves[Index] = {int(Keyframe): Value for Keyframe, Value in Keyframes} + Config.File.write("{}AnimationKey {{ //Scale\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + AllKeyframes = list(AllKeyframes) + AllKeyframes.sort() + if len(AllKeyframes): + Config.File.write("{}1;\n{}{};\n".format(" " * Config.Whitespace, " " * Config.Whitespace, len(AllKeyframes))) + for Keyframe in AllKeyframes: + bpy.context.scene.set_frame(Keyframe) + Scale = Vector() + Scale[0] = ((ScaleFCurves[0][Keyframe] if Keyframe in ScaleFCurves[0] else Object.scale[0]) if ScaleFCurves[0] else Object.scale[0]) + Scale[1] = ((ScaleFCurves[1][Keyframe] if Keyframe in ScaleFCurves[1] else Object.scale[1]) if ScaleFCurves[1] else Object.scale[1]) + Scale[2] = ((ScaleFCurves[2][Keyframe] if Keyframe in ScaleFCurves[2] else Object.scale[2]) if ScaleFCurves[2] else Object.scale[2]) + Scale = Config.SystemMatrix * Scale + Config.File.write("{}{}{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, (str(Keyframe - bpy.context.scene.frame_start) + ";3;").ljust(8), Scale[0], Scale[1], Scale[2])) + else: + Config.File.write("{}1;\n{}1;\n".format(" " * Config.Whitespace, " " * Config.Whitespace)) + bpy.context.scene.set_frame(bpy.context.scene.frame_start) + Scale = Config.SystemMatrix * Object.scale + Config.File.write("{}{}{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, ("0;3;").ljust(8), Scale[0], Scale[1], Scale[2])) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + if Config.Verbose: + print("Done") + + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + else: + if Config.Verbose: + print(" Object has no useable animation data.") + + if Config.ExportArmatures and Object.type == "ARMATURE": + if Config.Verbose: + print(" Writing Armature Bone Animation Data...") + PoseBones = Object.pose.bones + for Bone in PoseBones: + if Config.Verbose: + print(" Writing Bone: {}...".format(Bone.name)) + PositionFCurves = [None, None, None] + RotationFCurves = [None, None, None, None] + ScaleFCurves = [None, None, None] + for FCurve in Action.fcurves: + if FCurve.data_path == "pose.bones[\"{}\"].location".format(Bone.name): + PositionFCurves[FCurve.array_index] = FCurve + elif FCurve.data_path == "pose.bones[\"{}\"].rotation_quaternion".format(Bone.name): + RotationFCurves[FCurve.array_index] = FCurve + elif FCurve.data_path == "pose.bones[\"{}\"].scale".format(Bone.name): + ScaleFCurves[FCurve.array_index] = FCurve + if not [FCurve for FCurve in PositionFCurves + RotationFCurves + ScaleFCurves if FCurve]: + if Config.Verbose: + print(" Bone has no useable animation data.\n Done") + continue + + Config.File.write("{}Animation {{\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + Config.File.write("{}{{{}}}\n".format(" " * Config.Whitespace, LegalName(Object.name) + "_" + LegalName(Bone.name))) + + #Position + if Config.Verbose: + print(" Writing Position...", end=" ") + AllKeyframes = set() + for Index, FCurve in enumerate(PositionFCurves): + if FCurve: + Keyframes = [] + for Keyframe in FCurve.keyframe_points: + Keyframes.append(Keyframe.co) + AllKeyframes.add(int(Keyframe.co[0])) + PositionFCurves[Index] = {int(Keyframe): Value for Keyframe, Value in Keyframes} + Config.File.write("{}AnimationKey {{ //Position\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + AllKeyframes = list(AllKeyframes) + AllKeyframes.sort() + if not len(AllKeyframes): + AllKeyframes = [bpy.context.scene.frame_start] + Config.File.write("{}2;\n{}{};\n".format(" " * Config.Whitespace, " " * Config.Whitespace, len(AllKeyframes))) + for Keyframe in AllKeyframes: + bpy.context.scene.set_frame(Keyframe) + + if Bone.parent: + PoseMatrix = (Bone.parent.matrix * RotationMatrix(radians(-90), 4, "X")).invert() + else: + PoseMatrix = Matrix() + PoseMatrix *= Bone.matrix * RotationMatrix(radians(-90), 4, "X") + + PoseMatrix = Config.SystemMatrix * PoseMatrix * Config.InverseSystemMatrix + + Position = PoseMatrix.translation_part() + Config.File.write("{}{}{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, (str(Keyframe - bpy.context.scene.frame_start) + ";3;").ljust(8), Position[0], Position[1], Position[2])) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + if Config.Verbose: + print("Done") + + #Rotation + if Config.Verbose: + print(" Writing Rotation...", end=" ") + AllKeyframes = set() + for Index, FCurve in enumerate(RotationFCurves): + if FCurve: + Keyframes = [] + for Keyframe in FCurve.keyframe_points: + Keyframes.append(Keyframe.co) + AllKeyframes.add(int(Keyframe.co[0])) + RotationFCurves[Index] = {int(Keyframe): Value for Keyframe, Value in Keyframes} + Config.File.write("{}AnimationKey {{ //Rotation\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + AllKeyframes = list(AllKeyframes) + AllKeyframes.sort() + if not len(AllKeyframes): + AllKeyframes = [bpy.context.scene.frame_start] + Config.File.write("{}0;\n{}{};\n".format(" " * Config.Whitespace, " " * Config.Whitespace, len(AllKeyframes))) + for Keyframe in AllKeyframes: + bpy.context.scene.set_frame(Keyframe) + + if Bone.parent: + PoseMatrix = (Bone.parent.matrix * RotationMatrix(radians(-90), 4, "X")).invert() + else: + PoseMatrix = Matrix() + PoseMatrix *= Bone.matrix * RotationMatrix(radians(-90), 4, "X") + + PoseMatrix = Config.SystemMatrix * PoseMatrix * Config.InverseSystemMatrix + + Rotation = PoseMatrix.rotation_part().to_quat() + Config.File.write("{}{}{:9f},{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, (str(Keyframe - bpy.context.scene.frame_start) + ";4;").ljust(8), -Rotation[0], Rotation[1], Rotation[2], Rotation[3])) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + if Config.Verbose: + print("Done") + + #Scale + if Config.Verbose: + print(" Writing Scale...", end=" ") + AllKeyframes = set() + for Index, FCurve in enumerate(ScaleFCurves): + if FCurve: + Keyframes = [] + for Keyframe in FCurve.keyframe_points: + Keyframes.append(Keyframe.co) + AllKeyframes.add(int(Keyframe.co[0])) + ScaleFCurves[Index] = {int(Keyframe): Value for Keyframe, Value in Keyframes} + Config.File.write("{}AnimationKey {{ //Scale\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + AllKeyframes = list(AllKeyframes) + AllKeyframes.sort() + if not len(AllKeyframes): + AllKeyframes = [bpy.context.scene.frame_start] + Config.File.write("{}1;\n{}{};\n".format(" " * Config.Whitespace, " " * Config.Whitespace, len(AllKeyframes))) + for Keyframe in AllKeyframes: + bpy.context.scene.set_frame(Keyframe) + + if Bone.parent: + PoseMatrix = (Bone.parent.matrix * RotationMatrix(radians(-90), 4, "X")).invert() + else: + PoseMatrix = Matrix() + PoseMatrix *= Bone.matrix * RotationMatrix(radians(-90), 4, "X") + + PoseMatrix = Config.SystemMatrix * PoseMatrix * Config.InverseSystemMatrix + + Scale = PoseMatrix.scale_part() + Config.File.write("{}{}{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, (str(Keyframe - bpy.context.scene.frame_start) + ";3;").ljust(8), Scale[0], Scale[1], Scale[2])) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + if Config.Verbose: + print("Done") + + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + if Config.Verbose: + print(" Done") #Done with Armature Bone + if Config.Verbose: + print(" Done") #Done with Armature Bone data + if Config.Verbose: + print(" Done") #Done with Object + + Config.Whitespace -= 1 + Config.File.write("{}}} //End of AnimationSet\n".format(" " * Config.Whitespace)) + + +def WriteFullAnimationSet(Config): + Config.File.write("{}AnimationSet {{\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + + KeyframeCount = bpy.context.scene.frame_end - bpy.context.scene.frame_start + 1 + + for Object in Config.ObjectList: + if Config.Verbose: + print(" Writing Animation Data for Object: {}".format(Object.name)) + + Config.File.write("{}Animation {{\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + Config.File.write("{}{{{}}}\n".format(" " * Config.Whitespace, LegalName(Object.name))) + + #Position + if Config.Verbose: + print(" Writing Position...", end=" ") + Config.File.write("{}AnimationKey {{ //Position\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + Config.File.write("{}2;\n{}{};\n".format(" " * Config.Whitespace, " " * Config.Whitespace, KeyframeCount)) + for Frame in range(0, KeyframeCount): + bpy.context.scene.set_frame(Frame + bpy.context.scene.frame_start) + Position = Config.SystemMatrix * Object.location + Config.File.write("{}{}{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, (str(Frame) + ";3;").ljust(8), Position[0], Position[1], Position[2])) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + if Config.Verbose: + print("Done") + + #Rotation + if Config.Verbose: + print(" Writing Rotation...", end=" ") + Config.File.write("{}AnimationKey {{ //Rotation\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + Config.File.write("{}0;\n{}{};\n".format(" " * Config.Whitespace, " " * Config.Whitespace, KeyframeCount)) + for Frame in range(0, KeyframeCount): + bpy.context.scene.set_frame(Frame + bpy.context.scene.frame_start) + Rotation = Config.SystemQuaternion.cross(Object.rotation_euler.to_quat().cross(Config.InverseSystemQuaternion)) + Config.File.write("{}{}{:9f},{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, (str(Frame) + ";4;").ljust(8), Rotation[0], Rotation[1], Rotation[2], Config.FlipZ * Rotation[3])) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + if Config.Verbose: + print("Done") + + #Scale + if Config.Verbose: + print(" Writing Scale...", end=" ") + Config.File.write("{}AnimationKey {{ //Scale\n".format(" " * Config.Whitespace)) + Config.Whitespace += 1 + Config.File.write("{}1;\n{}{};\n".format(" " * Config.Whitespace, " " * Config.Whitespace, KeyframeCount)) + for Frame in range(0, KeyframeCount): + bpy.context.scene.set_frame(Frame + bpy.context.scene.frame_start) + Scale = Config.SystemMatrix * Object.scale + Config.File.write("{}{}{:9f},{:9f},{:9f};;\n".format(" " * Config.Whitespace, (str(Frame) + ";3;").ljust(8), Scale[0], Scale[1], Scale[2])) + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + if Config.Verbose: + print("Done") + + Config.Whitespace -= 1 + Config.File.write("{}}}\n".format(" " * Config.Whitespace)) + + if Config.ExportArmatures and Object.type == "ARMATURE": + if Config.Verbose: + print(" Done") #Done with Armature Bone data + if Config.Verbose: + print(" Done") #Done with Object + + Config.Whitespace -= 1 + Config.File.write("{}}} //End of AnimationSet\n".format(" " * Config.Whitespace)) + + +def CloseFile(Config): + if Config.Verbose: + print("Closing File...", end=" ") + Config.File.close() + if Config.Verbose: + print("Done") + +CoordinateSystems = [] +CoordinateSystems.append(("1", "Left-Handed", "")) +CoordinateSystems.append(("2", "Right-Handed", "")) + +AnimationModes = [] +AnimationModes.append(("0", "None", "")) +AnimationModes.append(("1", "Keyframes Only", "")) +AnimationModes.append(("2", "Full Animation", "")) + +ExportModes = [] +ExportModes.append(("1", "All Objects", "")) +ExportModes.append(("2", "Selected Objects", "")) + +from bpy.props import * + + +class DirectXExporter(bpy.types.Operator): + """Export to the DirectX model format (.x)""" + + bl_idname = "export.directx" + bl_label = "Export DirectX" + + filepath = StringProperty() + filename = StringProperty() + directory = StringProperty() + + #Coordinate System + CoordinateSystem = EnumProperty(name="System", description="Select a coordinate system to export to", items=CoordinateSystems, default="1") + + #General Options + RotateX = BoolProperty(name="Rotate X 90 Degrees", description="Rotate the entire scene 90 degrees around the X axis so Y is up.", default=True) + FlipNormals = BoolProperty(name="Flip Normals", description="", default=False) + ApplyModifiers = BoolProperty(name="Apply Modifiers", description="Apply object modifiers before export.", default=False) + IncludeFrameRate = BoolProperty(name="Include Frame Rate", description="Include the AnimTicksPerSecond template which is used by some engines to control animation speed.", default=False) + ExportTextures = BoolProperty(name="Export Textures", description="Reference external image files to be used by the model.", default=True) + ExportArmatures = BoolProperty(name="Export Armatures", description="Export the bones of any armatures to deform meshes.", default=False) + ExportAnimation = EnumProperty(name="Animations", description="Select the type of animations to export. Only object and armature bone animations can be exported. Full Animation exports every frame.", items=AnimationModes, default="0") + + #Export Mode + ExportMode = EnumProperty(name="Export", description="Select which objects to export. Only Mesh, Empty, and Armature objects will be exported.", items=ExportModes, default="1") + + Verbose = BoolProperty(name="Verbose", description="Run the exporter in debug mode. Check the console for output.", default=False) + + def execute(self, context): + #Append .x if needed + FilePath = self.properties.filepath + if not FilePath.lower().endswith(".x"): + FilePath += ".x" + + Config = DirectXExporterSettings(context, + FilePath, + CoordinateSystem=self.properties.CoordinateSystem, + RotateX=self.properties.RotateX, + FlipNormals=self.properties.FlipNormals, + ApplyModifiers=self.properties.ApplyModifiers, + IncludeFrameRate=self.properties.IncludeFrameRate, + ExportTextures=self.properties.ExportTextures, + ExportArmatures=self.properties.ExportArmatures, + ExportAnimation=self.properties.ExportAnimation, + ExportMode=self.properties.ExportMode, + Verbose=self.properties.Verbose) + ExportDirectX(Config) + return {"FINISHED"} + + def invoke(self, context, event): + WindowManager = context.manager + WindowManager.add_fileselect(self) + return {"RUNNING_MODAL"} + + +def menu_func(self, context): + DefaultPath = os.path.splitext(bpy.data.filepath)[0] + ".x" + self.layout.operator(DirectXExporter.bl_idname, text="DirectX (.x)").filepath = DefaultPath + + +def register(): + bpy.types.register(DirectXExporter) + bpy.types.INFO_MT_file_export.append(menu_func) + + +def unregister(): + bpy.types.unregister(DirectXExporter) + bpy.types.INFO_MT_file_export.remove(menu_func) + + +if __name__ == "__main__": + register() \ No newline at end of file diff --git a/io_export_unreal_psk_psa.py b/io_export_unreal_psk_psa.py new file mode 100644 index 00000000..4434ad25 --- /dev/null +++ b/io_export_unreal_psk_psa.py @@ -0,0 +1,1603 @@ + # ***** 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 3 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, see . + # All rights reserved. + # ***** GPL LICENSE BLOCK ***** + +""" + +-- Unreal Skeletal Mesh and Animation Export (.psk and .psa) export script v0.0.1 --
+ +- NOTES: +- This script Exports To Unreal's PSK and PSA file formats for Skeletal Meshes and Animations.
+- This script DOES NOT support vertex animation! These require completely different file formats.
+ +- v0.0.1 +- Initial version + +- v0.0.2 +- This version adds support for more than one material index! + +[ - Edit by: Darknet +- v0.0.3 - v0.0.12 +- This will work on UT3 and it is a stable version that work with vehicle for testing. +- Main Bone fix no dummy needed to be there. +- Just bone issues position, rotation, and offset for psk. +- The armature bone position, rotation, and the offset of the bone is fix. It was to deal with skeleton mesh export for psk. +- Animation is fix for position, offset, rotation bone support one rotation direction when armature build. +- It will convert your mesh into triangular when exporting to psk file. +- Did not work with psa export yet. + +- v0.0.13 +- The animatoin will support different bone rotations when export the animation. + +- v0.0.14 +- Fixed Action set keys frames when there is no pose keys and it will ignore it. + +- v0.0.15 +- Fixed multiple objects when exporting to psk. Select one mesh to export to psk. +- ] + +- v0.1.1 +- Blender 2.50 svn (Support) + +Credit to: +- export_cal3d.py (Position of the Bones Format) +- blender2md5.py (Animation Translation Format) +- export_obj.py (Blender 2.5/Pyhton 3.x Format) + +- freenode #blendercoder -> user -> ideasman42 + +- Give Credit to those who work on this script. + +- http://sinsoft.com +""" + + +bl_addon_info = { + 'name': 'Export Skeleletal Mesh/Animation Data', + 'author': 'Darknet/Optimus_P-Fat/Active_Trash/Sinsoft', + 'version': '2.0', + 'blender': (2, 5, 3), + 'location': 'File > Export > Skeletal Mesh/Animation Data (.psk/.psa)', + 'description': 'Export Unreal Engine (.psk)', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/File_I-O/Unreal_psk_psa', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=21366&group_id=153&atid=469', + 'category': 'Import/Export'} + + +import os +import time +import datetime +import bpy +import mathutils +import operator + +from struct import pack, calcsize + +MENUPANELBOOL = True + +# REFERENCE MATERIAL JUST IN CASE: +# +# U = x / sqrt(x^2 + y^2 + z^2) +# V = y / sqrt(x^2 + y^2 + z^2) +# +# Triangles specifed counter clockwise for front face +# +#defines for sizeofs +SIZE_FQUAT = 16 +SIZE_FVECTOR = 12 +SIZE_VJOINTPOS = 44 +SIZE_ANIMINFOBINARY = 168 +SIZE_VCHUNKHEADER = 32 +SIZE_VMATERIAL = 88 +SIZE_VBONE = 120 +SIZE_FNAMEDBONEBINARY = 120 +SIZE_VRAWBONEINFLUENCE = 12 +SIZE_VQUATANIMKEY = 32 +SIZE_VVERTEX = 16 +SIZE_VPOINT = 12 +SIZE_VTRIANGLE = 12 + +######################################################################## +# Generic Object->Integer mapping +# the object must be usable as a dictionary key +class ObjMap: + def __init__(self): + self.dict = {} + self.next = 0 + def get(self, obj): + if obj in self.dict: + return self.dict[obj] + else: + id = self.next + self.next = self.next + 1 + self.dict[obj] = id + return id + + def items(self): + getval = operator.itemgetter(0) + getkey = operator.itemgetter(1) + return map(getval, sorted(self.dict.items(), key=getkey)) + +######################################################################## +# RG - UNREAL DATA STRUCTS - CONVERTED FROM C STRUCTS GIVEN ON UDN SITE +# provided here: http://udn.epicgames.com/Two/BinaryFormatSpecifications.html +# updated UDK (Unreal Engine 3): http://udn.epicgames.com/Three/BinaryFormatSpecifications.html +class FQuat: + def __init__(self): + self.X = 0.0 + self.Y = 0.0 + self.Z = 0.0 + self.W = 1.0 + + def dump(self): + data = pack('ffff', self.X, self.Y, self.Z, self.W) + return data + + def __cmp__(self, other): + return cmp(self.X, other.X) \ + or cmp(self.Y, other.Y) \ + or cmp(self.Z, other.Z) \ + or cmp(self.W, other.W) + + def __hash__(self): + return hash(self.X) ^ hash(self.Y) ^ hash(self.Z) ^ hash(self.W) + + def __str__(self): + return "[%f,%f,%f,%f](FQuat)" % (self.X, self.Y, self.Z, self.W) + +class FVector(object): + def __init__(self, X=0.0, Y=0.0, Z=0.0): + self.X = X + self.Y = Y + self.Z = Z + + def dump(self): + data = pack('fff', self.X, self.Y, self.Z) + return data + + def __cmp__(self, other): + return cmp(self.X, other.X) \ + or cmp(self.Y, other.Y) \ + or cmp(self.Z, other.Z) + + def _key(self): + return (type(self).__name__, self.X, self.Y, self.Z) + + def __hash__(self): + return hash(self._key()) + + def __eq__(self, other): + if not hasattr(other, '_key'): + return False + return self._key() == other._key() + + def dot(self, other): + return self.X * other.X + self.Y * other.Y + self.Z * other.Z + + def cross(self, other): + return FVector(self.Y * other.Z - self.Z * other.Y, + self.Z * other.X - self.X * other.Z, + self.X * other.Y - self.Y * other.X) + + def sub(self, other): + return FVector(self.X - other.X, + self.Y - other.Y, + self.Z - other.Z) + +class VJointPos: + def __init__(self): + self.Orientation = FQuat() + self.Position = FVector() + self.Length = 0.0 + self.XSize = 0.0 + self.YSize = 0.0 + self.ZSize = 0.0 + + def dump(self): + data = self.Orientation.dump() + self.Position.dump() + pack('4f', self.Length, self.XSize, self.YSize, self.ZSize) + return data + +class AnimInfoBinary: + def __init__(self): + self.Name = "" # length=64 + self.Group = "" # length=64 + self.TotalBones = 0 + self.RootInclude = 0 + self.KeyCompressionStyle = 0 + self.KeyQuotum = 0 + self.KeyPrediction = 0.0 + self.TrackTime = 0.0 + self.AnimRate = 0.0 + self.StartBone = 0 + self.FirstRawFrame = 0 + self.NumRawFrames = 0 + + def dump(self): + data = pack('64s64siiiifffiii', self.Name, self.Group, self.TotalBones, self.RootInclude, self.KeyCompressionStyle, self.KeyQuotum, self.KeyPrediction, self.TrackTime, self.AnimRate, self.StartBone, self.FirstRawFrame, self.NumRawFrames) + return data + +class VChunkHeader: + def __init__(self, name, type_size): + self.ChunkID = name # length=20 + self.TypeFlag = 1999801 # special value + self.DataSize = type_size + self.DataCount = 0 + + def dump(self): + data = pack('20siii', self.ChunkID, self.TypeFlag, self.DataSize, self.DataCount) + return data + +class VMaterial: + def __init__(self): + self.MaterialName = "" # length=64 + self.TextureIndex = 0 + self.PolyFlags = 0 # DWORD + self.AuxMaterial = 0 + self.AuxFlags = 0 # DWORD + self.LodBias = 0 + self.LodStyle = 0 + + def dump(self): + data = pack('64siLiLii', self.MaterialName, self.TextureIndex, self.PolyFlags, self.AuxMaterial, self.AuxFlags, self.LodBias, self.LodStyle) + return data + +class VBone: + def __init__(self): + self.Name = "" # length = 64 + self.Flags = 0 # DWORD + self.NumChildren = 0 + self.ParentIndex = 0 + self.BonePos = VJointPos() + + def dump(self): + data = pack('64sLii', self.Name, self.Flags, self.NumChildren, self.ParentIndex) + self.BonePos.dump() + return data + +#same as above - whatever - this is how Epic does it... +class FNamedBoneBinary: + def __init__(self): + self.Name = "" # length = 64 + self.Flags = 0 # DWORD + self.NumChildren = 0 + self.ParentIndex = 0 + self.BonePos = VJointPos() + + self.IsRealBone = 0 # this is set to 1 when the bone is actually a bone in the mesh and not a dummy + + def dump(self): + data = pack('64sLii', self.Name, self.Flags, self.NumChildren, self.ParentIndex) + self.BonePos.dump() + return data + +class VRawBoneInfluence: + def __init__(self): + self.Weight = 0.0 + self.PointIndex = 0 + self.BoneIndex = 0 + + def dump(self): + data = pack('fii', self.Weight, self.PointIndex, self.BoneIndex) + return data + +class VQuatAnimKey: + def __init__(self): + self.Position = FVector() + self.Orientation = FQuat() + self.Time = 0.0 + + def dump(self): + data = self.Position.dump() + self.Orientation.dump() + pack('f', self.Time) + return data + +class VVertex(object): + def __init__(self): + self.PointIndex = 0 # WORD + self.U = 0.0 + self.V = 0.0 + self.MatIndex = 0 #BYTE + self.Reserved = 0 #BYTE + + def dump(self): + data = pack('HHffBBH', self.PointIndex, 0, self.U, self.V, self.MatIndex, self.Reserved, 0) + return data + + def __cmp__(self, other): + return cmp(self.PointIndex, other.PointIndex) \ + or cmp(self.U, other.U) \ + or cmp(self.V, other.V) \ + or cmp(self.MatIndex, other.MatIndex) \ + or cmp(self.Reserved, other.Reserved) + + def _key(self): + return (type(self).__name__,self.PointIndex, self.U, self.V,self.MatIndex,self.Reserved) + + def __hash__(self): + return hash(self._key()) + + def __eq__(self, other): + if not hasattr(other, '_key'): + return False + return self._key() == other._key() + +class VPoint(object): + def __init__(self): + self.Point = FVector() + + def dump(self): + return self.Point.dump() + + def __cmp__(self, other): + return cmp(self.Point, other.Point) + + def _key(self): + return (type(self).__name__, self.Point) + + def __hash__(self): + return hash(self._key()) + + def __eq__(self, other): + if not hasattr(other, '_key'): + return False + return self._key() == other._key() + +class VTriangle: + def __init__(self): + self.WedgeIndex0 = 0 # WORD + self.WedgeIndex1 = 0 # WORD + self.WedgeIndex2 = 0 # WORD + self.MatIndex = 0 # BYTE + self.AuxMatIndex = 0 # BYTE + self.SmoothingGroups = 0 # DWORD + + def dump(self): + data = pack('HHHBBL', self.WedgeIndex0, self.WedgeIndex1, self.WedgeIndex2, self.MatIndex, self.AuxMatIndex, self.SmoothingGroups) + return data + +# END UNREAL DATA STRUCTS +######################################################################## + +######################################################################## +#RG - helper class to handle the normal way the UT files are stored +#as sections consisting of a header and then a list of data structures +class FileSection: + def __init__(self, name, type_size): + self.Header = VChunkHeader(name, type_size) + self.Data = [] # list of datatypes + + def dump(self): + data = self.Header.dump() + for i in range(len(self.Data)): + data = data + self.Data[i].dump() + return data + + def UpdateHeader(self): + self.Header.DataCount = len(self.Data) + +class PSKFile: + def __init__(self): + self.GeneralHeader = VChunkHeader("ACTRHEAD", 0) + self.Points = FileSection("PNTS0000", SIZE_VPOINT) #VPoint + self.Wedges = FileSection("VTXW0000", SIZE_VVERTEX) #VVertex + self.Faces = FileSection("FACE0000", SIZE_VTRIANGLE) #VTriangle + self.Materials = FileSection("MATT0000", SIZE_VMATERIAL) #VMaterial + self.Bones = FileSection("REFSKELT", SIZE_VBONE) #VBone + self.Influences = FileSection("RAWWEIGHTS", SIZE_VRAWBONEINFLUENCE) #VRawBoneInfluence + + #RG - this mapping is not dumped, but is used internally to store the new point indices + # for vertex groups calculated during the mesh dump, so they can be used again + # to dump bone influences during the armature dump + # + # the key in this dictionary is the VertexGroup/Bone Name, and the value + # is a list of tuples containing the new point index and the weight, in that order + # + # Layout: + # { groupname : [ (index, weight), ... ], ... } + # + # example: + # { 'MyVertexGroup' : [ (0, 1.0), (5, 1.0), (3, 0.5) ] , 'OtherGroup' : [(2, 1.0)] } + + self.VertexGroups = {} + + def AddPoint(self, p): + #print ('AddPoint') + self.Points.Data.append(p) + + def AddWedge(self, w): + #print ('AddWedge') + self.Wedges.Data.append(w) + + def AddFace(self, f): + #print ('AddFace') + self.Faces.Data.append(f) + + def AddMaterial(self, m): + #print ('AddMaterial') + self.Materials.Data.append(m) + + def AddBone(self, b): + #print ('AddBone [%s]: Position: (x=%f, y=%f, z=%f) Rotation=(%f,%f,%f,%f)' % (b.Name, b.BonePos.Position.X, b.BonePos.Position.Y, b.BonePos.Position.Z, b.BonePos.Orientation.X,b.BonePos.Orientation.Y,b.BonePos.Orientation.Z,b.BonePos.Orientation.W)) + self.Bones.Data.append(b) + + def AddInfluence(self, i): + #print ('AddInfluence') + self.Influences.Data.append(i) + + def UpdateHeaders(self): + self.Points.UpdateHeader() + self.Wedges.UpdateHeader() + self.Faces.UpdateHeader() + self.Materials.UpdateHeader() + self.Bones.UpdateHeader() + self.Influences.UpdateHeader() + + def dump(self): + self.UpdateHeaders() + data = self.GeneralHeader.dump() + self.Points.dump() + self.Wedges.dump() + self.Faces.dump() + self.Materials.dump() + self.Bones.dump() + self.Influences.dump() + return data + + def GetMatByIndex(self, mat_index): + if mat_index >= 0 and len(self.Materials.Data) > mat_index: + return self.Materials.Data[mat_index] + else: + m = VMaterial() + m.MaterialName = "Mat%i" % mat_index + self.AddMaterial(m) + return m + + def PrintOut(self): + print ("--- PSK FILE EXPORTED ---") + print ('point count: %i' % len(self.Points.Data)) + print ('wedge count: %i' % len(self.Wedges.Data)) + print ('face count: %i' % len(self.Faces.Data)) + print ('material count: %i' % len(self.Materials.Data)) + print ('bone count: %i' % len(self.Bones.Data)) + print ('inlfuence count: %i' % len(self.Influences.Data)) + print ('-------------------------') + +# PSA FILE NOTES FROM UDN: +# +# The raw key array holds all the keys for all the bones in all the specified sequences, +# organized as follows: +# For each AnimInfoBinary's sequence there are [Number of bones] times [Number of frames keys] +# in the VQuatAnimKeys, laid out as tracks of [numframes] keys for each bone in the order of +# the bones as defined in the array of FnamedBoneBinary in the PSA. +# +# Once the data from the PSK (now digested into native skeletal mesh) and PSA (digested into +# a native animation object containing one or more sequences) are associated together at runtime, +# bones are linked up by name. Any bone in a skeleton (from the PSK) that finds no partner in +# the animation sequence (from the PSA) will assume its reference pose stance ( as defined in +# the offsets & rotations that are in the VBones making up the reference skeleton from the PSK) + +class PSAFile: + def __init__(self): + self.GeneralHeader = VChunkHeader("ANIMHEAD", 0) + self.Bones = FileSection("BONENAMES", SIZE_FNAMEDBONEBINARY) #FNamedBoneBinary + self.Animations = FileSection("ANIMINFO", SIZE_ANIMINFOBINARY) #AnimInfoBinary + self.RawKeys = FileSection("ANIMKEYS", SIZE_VQUATANIMKEY) #VQuatAnimKey + + # this will take the format of key=Bone Name, value = (BoneIndex, Bone Object) + # THIS IS NOT DUMPED + self.BoneLookup = {} + + def dump(self): + data = self.Generalheader.dump() + self.Bones.dump() + self.Animations.dump() + self.RawKeys.dump() + return data + + def AddBone(self, b): + #LOUD + #print "AddBone: " + b.Name + self.Bones.Data.append(b) + + def AddAnimation(self, a): + #LOUD + #print "AddAnimation: %s, TotalBones: %i, AnimRate: %f, NumRawFrames: %i, TrackTime: %f" % (a.Name, a.TotalBones, a.AnimRate, a.NumRawFrames, a.TrackTime) + self.Animations.Data.append(a) + + def AddRawKey(self, k): + #LOUD + #print "AddRawKey [%i]: Time: %f, Quat: x=%f, y=%f, z=%f, w=%f, Position: x=%f, y=%f, z=%f" % (len(self.RawKeys.Data), k.Time, k.Orientation.X, k.Orientation.Y, k.Orientation.Z, k.Orientation.W, k.Position.X, k.Position.Y, k.Position.Z) + self.RawKeys.Data.append(k) + + def UpdateHeaders(self): + self.Bones.UpdateHeader() + self.Animations.UpdateHeader() + self.RawKeys.UpdateHeader() + + def GetBoneByIndex(self, bone_index): + if bone_index >= 0 and len(self.Bones.Data) > bone_index: + return self.Bones.Data[bone_index] + + def IsEmpty(self): + return (len(self.Bones.Data) == 0 or len(self.Animations.Data) == 0) + + def StoreBone(self, b): + self.BoneLookup[b.Name] = [-1, b] + + def UseBone(self, bone_name): + if bone_name in self.BoneLookup: + bone_data = self.BoneLookup[bone_name] + + if bone_data[0] == -1: + bone_data[0] = len(self.Bones.Data) + self.AddBone(bone_data[1]) + #self.Bones.Data.append(bone_data[1]) + + return bone_data[0] + + def GetBoneByName(self, bone_name): + if bone_name in self.BoneLookup: + bone_data = self.BoneLookup[bone_name] + return bone_data[1] + + def GetBoneIndex(self, bone_name): + if bone_name in self.BoneLookup: + bone_data = self.BoneLookup[bone_name] + return bone_data[0] + + def dump(self): + self.UpdateHeaders() + data = self.GeneralHeader.dump() + self.Bones.dump() + self.Animations.dump() + self.RawKeys.dump() + return data + + def PrintOut(self): + print ('--- PSA FILE EXPORTED ---') + print ('bone count: %i' % len(self.Bones.Data)) + print ('animation count: %i' % len(self.Animations.Data)) + print ('rawkey count: %i' % len(self.RawKeys.Data)) + print ('-------------------------') + +#################################### +# helpers to create bone structs +def make_vbone(name, parent_index, child_count, orientation_quat, position_vect): + bone = VBone() + bone.Name = name + bone.ParentIndex = parent_index + bone.NumChildren = child_count + bone.BonePos.Orientation = orientation_quat + bone.BonePos.Position.X = position_vect.x + bone.BonePos.Position.Y = position_vect.y + bone.BonePos.Position.Z = position_vect.z + + #these values seem to be ignored? + #bone.BonePos.Length = tail.length + #bone.BonePos.XSize = tail.x + #bone.BonePos.YSize = tail.y + #bone.BonePos.ZSize = tail.z + + return bone + +def make_namedbonebinary(name, parent_index, child_count, orientation_quat, position_vect, is_real): + bone = FNamedBoneBinary() + bone.Name = name + bone.ParentIndex = parent_index + bone.NumChildren = child_count + bone.BonePos.Orientation = orientation_quat + bone.BonePos.Position.X = position_vect.x + bone.BonePos.Position.Y = position_vect.y + bone.BonePos.Position.Z = position_vect.z + bone.IsRealBone = is_real + return bone + +################################################## +#RG - check to make sure face isnt a line +#The face has to be triangle not a line +def is_1d_face(blender_face,mesh): + #ID Vertex of id point + v0 = blender_face.verts[0] + v1 = blender_face.verts[1] + v2 = blender_face.verts[2] + + return (mesh.verts[v0].co == mesh.verts[v1].co or \ + mesh.verts[v1].co == mesh.verts[v2].co or \ + mesh.verts[v2].co == mesh.verts[v0].co) + return False + +################################################## +# http://en.wikibooks.org/wiki/Blender_3D:_Blending_Into_Python/Cookbook#Triangulate_NMesh + +#blender 2.50 format using the Operators/command convert the mesh to tri mesh +def triangulateNMesh(object): + bneedtri = False + scene = bpy.context.scene + bpy.ops.object.mode_set(mode='OBJECT') + for i in scene.objects: i.select = False #deselect all objects + object.select = True + scene.objects.active = object #set the mesh object to current + bpy.ops.object.mode_set(mode='OBJECT') + print("Checking mesh if needs to convert quad to Tri...") + for face in object.data.faces: + if (len(face.verts) > 3): + bneedtri = True + break + + bpy.ops.object.mode_set(mode='OBJECT') + if bneedtri == True: + print("Converting quad to tri mesh...") + me_da = object.data.copy() #copy data + me_ob = object.copy() #copy object + #note two copy two types else it will use the current data or mesh + me_ob.data = me_da + bpy.context.scene.objects.link(me_ob)#link the object to the scene #current object location + for i in scene.objects: i.select = False #deselect all objects + me_ob.select = True + scene.objects.active = me_ob #set the mesh object to current + bpy.ops.object.mode_set(mode='EDIT') #Operators + bpy.ops.mesh.select_all(action='SELECT')#select all the face/vertex/edge + bpy.ops.mesh.quads_convert_to_tris() #Operators + bpy.context.scene.update() + bpy.ops.object.mode_set(mode='OBJECT') # set it in object + bpy.context.scene.unrealtriangulatebool = True + print("Triangulate Mesh Done!") + else: + print("No need to convert tri mesh.") + me_ob = object + return me_ob + +#Blender Bone Index +class BBone: + def __init__(self): + self.bone = "" + self.index = 0 +bonedata = [] +BBCount = 0 +#deal with mesh bones groups vertex point +def BoneIndex(bone): + global BBCount, bonedata + #print("//==============") + #print(bone.name , "ID:",BBCount) + BB = BBone() + BB.bone = bone.name + BB.index = BBCount + bonedata.append(BB) + BBCount += 1 + for current_child_bone in bone.children: + BoneIndex(current_child_bone) + +def BoneIndexArmature(blender_armature): + global BBCount + #print("\n Buildng bone before mesh \n") + #objectbone = blender_armature.pose #Armature bone + #print(blender_armature) + objectbone = blender_armature[0].pose + #print(dir(ArmatureData)) + + for bone in objectbone.bones: + if(bone.parent == None): + BoneIndex(bone) + #BBCount += 1 + break + +# Actual object parsing functions +def parse_meshes(blender_meshes, psk_file): + #this is use to call the bone name and the index array for group index matches + global bonedata + #print("BONE DATA",len(bonedata)) + print ("----- parsing meshes -----") + print("Number of Object Meshes:",len(blender_meshes)) + for current_obj in blender_meshes: #number of mesh that should be one mesh here + + current_obj = triangulateNMesh(current_obj) + #print(dir(current_obj)) + print("Mesh Name:",current_obj.name) + current_mesh = current_obj.data + + #if len(current_obj.materials) > 0: + # object_mat = current_obj.materials[0] + object_material_index = current_obj.active_material_index + + points = ObjMap() + wedges = ObjMap() + + discarded_face_count = 0 + print (" -- Dumping Mesh Faces -- LEN:", len(current_mesh.faces)) + for current_face in current_mesh.faces: + #print ' -- Dumping UVs -- ' + #print current_face.uv_textures + + if len(current_face.verts) != 3: + raise RuntimeError("Non-triangular face (%i)" % len(current_face.verts)) + + #No Triangulate Yet + # if len(current_face.verts) != 3: + # raise RuntimeError("Non-triangular face (%i)" % len(current_face.verts)) + # #TODO: add two fake faces made of triangles? + + #RG - apparently blender sometimes has problems when you do quad to triangle + # conversion, and ends up creating faces that have only TWO points - + # one of the points is simply in the vertex list for the face twice. + # This is bad, since we can't get a real face normal for a LINE, we need + # a plane for this. So, before we add the face to the list of real faces, + # ensure that the face is actually a plane, and not a line. If it is not + # planar, just discard it and notify the user in the console after we're + # done dumping the rest of the faces + + if not is_1d_face(current_face,current_mesh): + #print("faces") + wedge_list = [] + vect_list = [] + + #get or create the current material + m = psk_file.GetMatByIndex(object_material_index) + + face_index = current_face.index + has_UV = False + faceUV = None + + if len(current_mesh.uv_textures) > 0: + has_UV = True + #print("face index: ",face_index) + #faceUV = current_mesh.active_uv_texture.data[face_index]#UVs for current face + #faceUV = current_mesh.active_uv_texture.data[0]#UVs for current face + #print(face_index,"<[FACE NUMBER") + uv_layer = current_mesh.active_uv_texture + faceUV = uv_layer.data[face_index] + #print("============================") + #size(data) is number of texture faces. Each face has UVs + #print("DATA face uv: ",len(faceUV.uv), " >> ",(faceUV.uv[0][0])) + + for i in range(3): + vert_index = current_face.verts[i] + vert = current_mesh.verts[vert_index] + uv = [] + #assumes 3 UVs Per face (for now). + if (has_UV): + if len(faceUV.uv) != 3: + print ("WARNING: Current face is missing UV coordinates - writing 0,0...") + print ("WARNING: Face has more than 3 UVs - writing 0,0...") + uv = [0.0, 0.0] + else: + #uv.append(faceUV.uv[i][0]) + #uv.append(faceUV.uv[i][1]) + uv = [faceUV.uv[i][0],faceUV.uv[i][1]] #OR bottom works better # 24 for cube + #uv = list(faceUV.uv[i]) #30 just cube + else: + print ("No UVs?") + uv = [0.0, 0.0] + #print("UV >",uv) + #uv = [0.0, 0.0] #over ride uv that is not fixed + #print(uv) + #flip V coordinate because UEd requires it and DOESN'T flip it on its own like it + #does with the mesh Y coordinates. + #this is otherwise known as MAGIC-2 + uv[1] = 1.0 - uv[1] + + #deal with the min and max value + #if value is over the set limit it will null the uv texture + if (uv[0] > 1): + uv[0] = 1 + if (uv[0] < 0): + uv[0] = 0 + if (uv[1] > 1): + uv[1] = 1 + if (uv[1] < 0): + uv[1] = 0 + + + # RE - Append untransformed vector (for normal calc below) + # TODO: convert to Blender.Mathutils + vect_list.append(FVector(vert.co.x, vert.co.y, vert.co.z)) + + # Transform position for export + #vpos = vert.co * object_material_index + vpos = vert.co * current_obj.matrix_local + # Create the point + p = VPoint() + p.Point.X = vpos.x + p.Point.Y = vpos.y + p.Point.Z = vpos.z + + # Create the wedge + w = VVertex() + w.MatIndex = object_material_index + w.PointIndex = points.get(p) # get index from map + #Set UV TEXTURE + w.U = uv[0] + w.V = uv[1] + index_wedge = wedges.get(w) + wedge_list.append(index_wedge) + + #print results + #print 'result PointIndex=%i, U=%f, V=%f, wedge_index=%i' % ( + # w.PointIndex, + # w.U, + # w.V, + # wedge_index) + + # Determine face vertex order + # get normal from blender + no = current_face.normal + + # TODO: convert to Blender.Mathutils + # convert to FVector + norm = FVector(no[0], no[1], no[2]) + + # Calculate the normal of the face in blender order + tnorm = vect_list[1].sub(vect_list[0]).cross(vect_list[2].sub(vect_list[1])) + + # RE - dot the normal from blender order against the blender normal + # this gives the product of the two vectors' lengths along the blender normal axis + # all that matters is the sign + dot = norm.dot(tnorm) + + # print results + #print 'face norm: (%f,%f,%f), tnorm=(%f,%f,%f), dot=%f' % ( + # norm.X, norm.Y, norm.Z, + # tnorm.X, tnorm.Y, tnorm.Z, + # dot) + + tri = VTriangle() + # RE - magic: if the dot product above > 0, order the vertices 2, 1, 0 + # if the dot product above < 0, order the vertices 0, 1, 2 + # if the dot product is 0, then blender's normal is coplanar with the face + # and we cannot deduce which side of the face is the outside of the mesh + if (dot > 0): + (tri.WedgeIndex2, tri.WedgeIndex1, tri.WedgeIndex0) = wedge_list + elif (dot < 0): + (tri.WedgeIndex0, tri.WedgeIndex1, tri.WedgeIndex2) = wedge_list + else: + dindex0 = current_face.verts[0]; + dindex1 = current_face.verts[1]; + dindex2 = current_face.verts[2]; + raise RuntimeError("normal vector coplanar with face! points:", current_mesh.verts[dindex0].co, current_mesh.verts[dindex1].co, current_mesh.verts[dindex2].co) + + tri.MatIndex = object_material_index + #print(tri) + psk_file.AddFace(tri) + + else: + discarded_face_count = discarded_face_count + 1 + + print (" -- Dumping Mesh Points -- LEN:",len(points.dict)) + for point in points.items(): + psk_file.AddPoint(point) + print (" -- Dumping Mesh Wedge -- LEN:",len(wedges.dict)) + for wedge in wedges.items(): + psk_file.AddWedge(wedge) + + #RG - if we happend upon any non-planar faces above that we've discarded, + # just let the user know we discarded them here in case they want + # to investigate + + if discarded_face_count > 0: + print ("INFO: Discarded %i non-planar faces." % (discarded_face_count)) + + #RG - walk through the vertex groups and find the indexes into the PSK points array + #for them, then store that index and the weight as a tuple in a new list of + #verts for the group that we can look up later by bone name, since Blender matches + #verts to bones for influences by having the VertexGroup named the same thing as + #the bone + + #vertex group. + for bonegroup in bonedata: + #print("bone gourp build:",bonegroup.bone) + vert_list = [] + for current_vert in current_mesh.verts: + #print("INDEX V:",current_vert.index) + vert_index = current_vert.index + for vgroup in current_vert.groups:#vertex groupd id + vert_weight = vgroup.weight + if(bonegroup.index == vgroup.group): + p = VPoint() + vpos = current_vert.co * current_obj.matrix_local + p.Point.X = vpos.x + p.Point.Y = vpos.y + p.Point.Z = vpos.z + #print(current_vert.co) + point_index = points.get(p) #point index + v_item = (point_index, vert_weight) + vert_list.append(v_item) + #bone name, [point id and wieght] + #print("Add Vertex Group:",bonegroup.bone, " No. Points:",len(vert_list)) + psk_file.VertexGroups[bonegroup.bone] = vert_list + + #unrealtriangulatebool #this will remove the mesh from the scene + if (bpy.context.scene.unrealtriangulatebool == True): + print("Remove tmp Mesh [ " ,current_obj.name, " ] from scene >" ,(bpy.context.scene.unrealtriangulatebool )) + bpy.ops.object.mode_set(mode='OBJECT') # set it in object + bpy.context.scene.objects.unlink(current_obj) + +def make_fquat(bquat): + quat = FQuat() + + #flip handedness for UT = set x,y,z to negative (rotate in other direction) + quat.X = -bquat.x + quat.Y = -bquat.y + quat.Z = -bquat.z + + quat.W = bquat.w + return quat + +def make_fquat_default(bquat): + quat = FQuat() + + quat.X = bquat.x + quat.Y = bquat.y + quat.Z = bquat.z + + quat.W = bquat.w + return quat + +# ================================================================================================= +# TODO: remove this 1am hack +nbone = 0 +def parse_bone(blender_bone, psk_file, psa_file, parent_id, is_root_bone, parent_matrix, parent_root): + global nbone # look it's evil! + #print '-------------------- Dumping Bone ---------------------- ' + + #If bone does not have parent that mean it the root bone + if blender_bone.parent == None: + parent_root = blender_bone + + + child_count = len(blender_bone.children) + #child of parent + child_parent = blender_bone.parent + + if child_parent != None: + print ("--Bone Name:",blender_bone.name ," parent:" , blender_bone.parent.name, "ID:", nbone) + else: + print ("--Bone Name:",blender_bone.name ," parent: None" , "ID:", nbone) + + if child_parent != None: + quat_root = blender_bone.matrix + quat = make_fquat(quat_root.to_quat()) + + quat_parent = child_parent.matrix.to_quat().inverse() + parent_head = child_parent.head * quat_parent + parent_tail = child_parent.tail * quat_parent + + set_position = (parent_tail - parent_head) + blender_bone.head + else: + # ROOT BONE + #This for root + set_position = blender_bone.head * parent_matrix #ARMATURE OBJECT Locction + rot_mat = blender_bone.matrix * parent_matrix.rotation_part() #ARMATURE OBJECT Rotation + #print(dir(rot_mat)) + + quat = make_fquat_default(rot_mat.to_quat()) + + print ("[[======= FINAL POSITION:", set_position) + final_parent_id = parent_id + + #RG/RE - + #if we are not seperated by a small distance, create a dummy bone for the displacement + #this is only needed for root bones, since UT assumes a connected skeleton, and from here + #down the chain we just use "tail" as an endpoint + #if(head.length > 0.001 and is_root_bone == 1): + if(0): + pb = make_vbone("dummy_" + blender_bone.name, parent_id, 1, FQuat(), tail) + psk_file.AddBone(pb) + pbb = make_namedbonebinary("dummy_" + blender_bone.name, parent_id, 1, FQuat(), tail, 0) + psa_file.StoreBone(pbb) + final_parent_id = nbone + nbone = nbone + 1 + #tail = tail-head + + my_id = nbone + + pb = make_vbone(blender_bone.name, final_parent_id, child_count, quat, set_position) + psk_file.AddBone(pb) + pbb = make_namedbonebinary(blender_bone.name, final_parent_id, child_count, quat, set_position, 1) + psa_file.StoreBone(pbb) + + nbone = nbone + 1 + + #RG - dump influences for this bone - use the data we collected in the mesh dump phase + # to map our bones to vertex groups + #print("///////////////////////") + #print("set influence") + if blender_bone.name in psk_file.VertexGroups: + vertex_list = psk_file.VertexGroups[blender_bone.name] + #print("vertex list:", len(vertex_list), " of >" ,blender_bone.name ) + for vertex_data in vertex_list: + #print("set influence vettex") + point_index = vertex_data[0] + vertex_weight = vertex_data[1] + influence = VRawBoneInfluence() + influence.Weight = vertex_weight + #influence.BoneIndex = my_id + influence.BoneIndex = my_id + influence.PointIndex = point_index + #print(influence) + #print ('Adding Bone Influence for [%s] = Point Index=%i, Weight=%f' % (blender_bone.name, point_index, vertex_weight)) + #print("adding influence") + psk_file.AddInfluence(influence) + + #blender_bone.matrix_local + #recursively dump child bones + mainparent = parent_matrix + #if len(blender_bone.children) > 0: + for current_child_bone in blender_bone.children: + parse_bone(current_child_bone, psk_file, psa_file, my_id, 0, mainparent, parent_root) + +def parse_armature(blender_armature, psk_file, psa_file): + print ("----- parsing armature -----") + print ('blender_armature length: %i' % (len(blender_armature))) + + #magic 0 sized root bone for UT - this is where all armature dummy bones will attach + #dont increment nbone here because we initialize it to 1 (hackity hackity hack) + + #count top level bones first. NOT EFFICIENT. + child_count = 0 + for current_obj in blender_armature: + current_armature = current_obj.data + bones = [x for x in current_armature.bones if not x.parent == None] + child_count += len(bones) + + for current_obj in blender_armature: + print ("Current Armature Name: " + current_obj.name) + current_armature = current_obj.data + #armature_id = make_armature_bone(current_obj, psk_file, psa_file) + + #we dont want children here - only the top level bones of the armature itself + #we will recursively dump the child bones as we dump these bones + """ + bones = [x for x in current_armature.bones if not x.parent == None] + #will ingore this part of the ocde + """ + for current_bone in current_armature.bones: #list the bone. #note this will list all the bones. + if(current_bone.parent == None): + parse_bone(current_bone, psk_file, psa_file, 0, 0, current_obj.matrix_local, None) + break + +# get blender objects by type +def get_blender_objects(objects, intype): + return [x for x in objects if x.type == intype] + +#strips current extension (if any) from filename and replaces it with extension passed in +def make_filename_ext(filename, extension): + new_filename = '' + extension_index = filename.find('.') + + if extension_index == -1: + new_filename = filename + extension + else: + new_filename = filename[0:extension_index] + extension + + return new_filename + +# returns the quaternion Grassman product a*b +# this is the same as the rotation a(b(x)) +# (ie. the same as B*A if A and B are matrices representing +# the rotations described by quaternions a and b) +def grassman(a, b): + return mathutils.Quaternion( + a.w*b.w - a.x*b.x - a.y*b.y - a.z*b.z, + a.w*b.x + a.x*b.w + a.y*b.z - a.z*b.y, + a.w*b.y - a.x*b.z + a.y*b.w + a.z*b.x, + a.w*b.z + a.x*b.y - a.y*b.x + a.z*b.w) + +def parse_animation(blender_scene, blender_armatures, psa_file): + #to do list: + #need to list the action sets + #need to check if there animation + #need to check if animation is has one frame then exit it + print ('\n----- parsing animation -----') + ##print(dir(blender_scene)) + + #print(dir(blender_armatures)) + + render_data = blender_scene.render + bHaveAction = True + + anim_rate = render_data.fps + + #print("dir:",dir(blender_scene)) + #print(dir(bpy.data.actions)) + #print("dir:",dir(bpy.data.actions[0])) + + + print("==== Blender Settings ====") + print ('Scene: %s Start Frame: %i, End Frame: %i' % (blender_scene.name, blender_scene.frame_start, blender_scene.frame_end)) + print ('Frames Per Sec: %i' % anim_rate) + print ("Default FPS: 24" ) + + cur_frame_index = 0 + + #print(dir(bpy.data.actions)) + #print(dir(bpy.context.scene.set)) + + #list of armature objects + for arm in blender_armatures: + #check if there animation data from armature or something + #print(dir(arm.animation_data)) + #print("[["+dir(arm.animation_data.action)) + if not arm.animation_data: + print("======================================") + print("Check Animation Data: None") + print("Armature has no animation, skipping...") + print("======================================") + break + + if not arm.animation_data.action: + print("======================================") + print("Check Action: None") + print("Armature has no animation, skipping...") + print("======================================") + break + act = arm.animation_data.action + #print(dir(act)) + action_name = act.name + + if not len(act.fcurves): + print("//===========================================================") + print("// None bone pose set keys for this action set... skipping...") + print("//===========================================================") + bHaveAction = False + + #this deal with action export control + if bHaveAction == True: + print("") + print("==== Action Set ====") + print("Action Name:",action_name) + #look for min and max frame that current set keys + framemin, framemax = act.get_frame_range() + #print("max frame:",framemax) + start_frame = framemin + end_frame = framemax + scene_frames = range(start_frame, end_frame+1) + frame_count = len(scene_frames) + #=================================================== + anim = AnimInfoBinary() + anim.Name = action_name + anim.Group = "" #what is group? + anim.NumRawFrames = frame_count + anim.AnimRate = anim_rate + anim.FirstRawFrame = cur_frame_index + #=================================================== + count_previous_keys = len(psa_file.RawKeys.Data) + print("Frame Key Set Count:",frame_count, "Total Frame:",frame_count) + #print("init action bones...") + unique_bone_indexes = {} + # bone lookup table + bones_lookup = {} + + #build bone node for animation keys needed to be set + for bone in arm.data.bones: + bones_lookup[bone.name] = bone + #print("bone name:",bone.name) + frame_count = len(scene_frames) + #print ('Frame Count: %i' % frame_count) + pose_data = arm.pose + + #these must be ordered in the order the bones will show up in the PSA file! + ordered_bones = {} + ordered_bones = sorted([(psa_file.UseBone(x.name), x) for x in pose_data.bones], key=operator.itemgetter(0)) + + ############################# + # ORDERED FRAME, BONE + #for frame in scene_frames: + + for i in range(frame_count): + frame = scene_frames[i] + #LOUD + #print ("==== outputting frame %i ===" % frame) + + if frame_count > i+1: + next_frame = scene_frames[i+1] + #print "This Frame: %i, Next Frame: %i" % (frame, next_frame) + else: + next_frame = -1 + #print "This Frame: %i, Next Frame: NONE" % frame + + #frame start from 1 as number one from blender + blender_scene.set_frame(frame) + + cur_frame_index = cur_frame_index + 1 + for bone_data in ordered_bones: + bone_index = bone_data[0] + pose_bone = bone_data[1] + #print("[=====POSE NAME:",pose_bone.name) + + #print("LENG >>.",len(bones_lookup)) + blender_bone = bones_lookup[pose_bone.name] + + #just need the total unique bones used, later for this AnimInfoBinary + unique_bone_indexes[bone_index] = bone_index + #LOUD + #print ("-------------------", pose_bone.name) + head = pose_bone.head + + posebonemat = mathutils.Matrix(pose_bone.matrix) + parent_pose = pose_bone.parent + if parent_pose != None: + parentposemat = mathutils.Matrix(parent_pose.matrix) + #blender 2.4X it been flip around with new 2.50 (mat1 * mat2) should now be (mat2 * mat1) + posebonemat = parentposemat.invert() * posebonemat + head = posebonemat.translation_part() + quat = posebonemat.to_quat().normalize() + vkey = VQuatAnimKey() + vkey.Position.X = head.x + vkey.Position.Y = head.y + vkey.Position.Z = head.z + + if parent_pose != None: + quat = make_fquat(quat) + else: + quat = make_fquat_default(quat) + + vkey.Orientation = quat + #print("Head:",head) + #print("Orientation",quat) + + #time from now till next frame = diff / framesPerSec + if next_frame >= 0: + diff = next_frame - frame + else: + diff = 1.0 + + #print ("Diff = ", diff) + vkey.Time = float(diff)/float(anim_rate) + + psa_file.AddRawKey(vkey) + + #done looping frames + #done looping armatures + #continue adding animInfoBinary counts here + + anim.TotalBones = len(unique_bone_indexes) + print("Bones Count:",anim.TotalBones) + anim.TrackTime = float(frame_count) / anim.AnimRate + print("Time Track Frame:",anim.TrackTime) + psa_file.AddAnimation(anim) + print("==== Finish Action Build(s) ====") + +exportmessage = "Export Finish" + +def fs_callback(filename, context, user_setting): + #this deal with repeat export and the reset settings + global bonedata, BBCount, nbone, exportmessage + bonedata = []#clear array + BBCount = 0 + nbone = 0 + + start_time = time.clock() + + print ("========EXPORTING TO UNREAL SKELETAL MESH FORMATS========\r\n") + print("Blender Version:", bpy.app.version_string) + + psk = PSKFile() + psa = PSAFile() + + #sanity check - this should already have the extension, but just in case, we'll give it one if it doesn't + psk_filename = make_filename_ext(filename, '.psk') + + #make the psa filename + psa_filename = make_filename_ext(filename, '.psa') + + print ('PSK File: ' + psk_filename) + print ('PSA File: ' + psa_filename) + + barmature = True + bmesh = True + blender_meshes = [] + blender_armature = [] + selectmesh = [] + selectarmature = [] + + current_scene = context.scene + cur_frame = current_scene.frame_current #store current frame before we start walking them during animation parse + objects = current_scene.objects + + print("Checking object count...") + for next_obj in objects: + if next_obj.type == 'MESH': + blender_meshes.append(next_obj) + if (next_obj.select): + #print("mesh object select") + selectmesh.append(next_obj) + if next_obj.type == 'ARMATURE': + blender_armature.append(next_obj) + if (next_obj.select): + #print("armature object select") + selectarmature.append(next_obj) + + print("Mesh Count:",len(blender_meshes)," Armature Count:",len(blender_armature)) + print("====================================") + print("Checking Mesh Condtion(s):") + if len(blender_meshes) == 1: + print(" - One Mesh Scene") + elif (len(blender_meshes) > 1) and (len(selectmesh) == 1): + print(" - One Mesh [Select]") + else: + print(" - Too Many Meshes!") + print(" - Select One Mesh Object!") + bmesh = False + print("====================================") + print("Checking Armature Condtion(s):") + if len(blender_armature) == 1: + print(" - One Armature Scene") + elif (len(blender_armature) > 1) and (len(selectarmature) == 1): + print(" - One Armature [Select]") + else: + print(" - Too Armature Meshes!") + print(" - Select One Armature Object Only!") + barmature = False + + if (bmesh == False) or (barmature == False): + exportmessage = "Export Fail! Check Log." + print("=================================") + print("= Export Fail! =") + print("=================================") + else: + exportmessage = "Export Finish!" + #need to build a temp bone index for mesh group vertex + BoneIndexArmature(blender_armature) + + try: + ####################### + # STEP 1: MESH DUMP + # we build the vertexes, wedges, and faces in here, as well as a vertexgroup lookup table + # for the armature parse + print("//===============================") + print("// STEP 1") + print("//===============================") + parse_meshes(blender_meshes, psk) + except: + context.scene.set_frame(cur_frame) #set frame back to original frame + print ("Exception during Mesh Parse") + raise + + try: + ####################### + # STEP 2: ARMATURE DUMP + # IMPORTANT: do this AFTER parsing meshes - we need to use the vertex group data from + # the mesh parse in here to generate bone influences + print("//===============================") + print("// STEP 2") + print("//===============================") + parse_armature(blender_armature, psk, psa) + + except: + context.scene.set_frame(cur_frame) #set frame back to original frame + print ("Exception during Armature Parse") + raise + + try: + ####################### + # STEP 3: ANIMATION DUMP + # IMPORTANT: do AFTER parsing bones - we need to do bone lookups in here during animation frames + print("//===============================") + print("// STEP 3") + print("//===============================") + parse_animation(current_scene, blender_armature, psa) + + except: + context.scene.set_frame(cur_frame) #set frame back to original frame + print ("Exception during Animation Parse") + raise + + # reset current frame + + context.scene.set_frame(cur_frame) #set frame back to original frame + + ########################## + # FILE WRITE + print("//===========================================") + print("// bExportPsk:",bpy.context.scene.unrealexportpsk," bExportPsa:",bpy.context.scene.unrealexportpsa) + print("//===========================================") + if bpy.context.scene.unrealexportpsk == True: + print("Writing Skeleton Mesh Data...") + #RG - dump psk file + psk.PrintOut() + file = open(psk_filename, "wb") + file.write(psk.dump()) + file.close() + print ("Successfully Exported File: " + psk_filename) + if bpy.context.scene.unrealexportpsa == True: + print("Writing Animaiton Data...") + #RG - dump psa file + if not psa.IsEmpty(): + psa.PrintOut() + file = open(psa_filename, "wb") + file.write(psa.dump()) + file.close() + print ("Successfully Exported File: " + psa_filename) + else: + print ("No Animations (.psa file) to Export") + + print ('PSK/PSA Export Script finished in %.2f seconds' % (time.clock() - start_time)) + + #MSG BOX EXPORT COMPLETE + #... + + #DONE + print ("PSK/PSA Export Complete") + +def write_data(path, context, user_setting): + print("//============================") + print("// running psk/psa export...") + print("//============================") + fs_callback(path, context, user_setting) + pass + +from bpy.props import * + +exporttypedata = [] + +# [index,text field,0] #or something like that +exporttypedata.append(("0","PSK","Export PSK")) +exporttypedata.append(("1","PSA","Export PSA")) +exporttypedata.append(("2","ALL","Export ALL")) + +IntProperty= bpy.types.Scene.IntProperty + +IntProperty(attr="unrealfpsrate", name="fps rate", + description="Set the frame per second (fps) for unreal.", + default=24,min=1,max=100) + +bpy.types.Scene.EnumProperty( attr="unrealexport_settings", + name="Export:", + description="Select a export settings (psk/psa/all)...", + items = exporttypedata, default = '0') + +bpy.types.Scene.BoolProperty( attr="unrealtriangulatebool", + name="Triangulate Mesh", + description="Convert Quad to Tri Mesh Boolean...", + default=False) + +bpy.types.Scene.BoolProperty( attr="unrealactionexportall", + name="All Actions", + description="This let you export all actions from current armature.[Not Build Yet]", + default=False) + +bpy.types.Scene.BoolProperty( attr="unrealexportpsk", + name="bool export psa", + description="bool for exporting this psk format", + default=False) + +bpy.types.Scene.BoolProperty( attr="unrealexportpsa", + name="bool export psa", + description="bool for exporting this psa format", + default=False) + +class ExportUDKAnimData(bpy.types.Operator): + global exportmessage + '''Export Skeleton Mesh / Animation Data file(s)''' + bl_idname = "export.udk_anim_data" # this is important since its how bpy.ops.export.udk_anim_data is constructed + bl_label = "Export PSK/PSA" + __doc__ = "One mesh and one armature else select one mesh or armature to be exported." + + # List of operator properties, the attributes will be assigned + # to the class instance from the operator settings before calling. + + filepath = StringProperty(name="File Path", description="Filepath used for exporting the PSA file", maxlen= 1024, default= "") + use_setting = BoolProperty(name="No Options Yet", description="No Options Yet", default= True) + pskexportbool = BoolProperty(name="Export PSK", description="Export Skeletal Mesh", default= True) + psaexportbool = BoolProperty(name="Export PSA", description="Export Action Set (Animation Data)", default= True) + actionexportall = BoolProperty(name="All Actions", description="This will export all the actions that matches the current armature.", default=False) + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + #check if skeleton mesh is needed to be exported + if (self.properties.pskexportbool): + bpy.context.scene.unrealexportpsk = True + else: + bpy.context.scene.unrealexportpsk = False + #check if animation data is needed to be exported + if (self.properties.psaexportbool): + bpy.context.scene.unrealexportpsa = True + else: + bpy.context.scene.unrealexportpsa = False + + write_data(self.properties.filepath, context, self.properties.use_setting) + + self.report({'WARNING', 'INFO'}, exportmessage) + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.manager + wm.add_fileselect(self) + return {'RUNNING_MODAL'} + +def menu_func(self, context): + bpy.context.scene.unrealexportpsk = True + bpy.context.scene.unrealexportpsa = True + default_path = os.path.splitext(bpy.data.filepath)[0] + ".psk" + self.layout.operator("export.udk_anim_data", text="Skeleton Mesh / Animation Data (.psk/.psa)").filepath = default_path + + +class VIEW3D_PT_unrealtools_objectmode(bpy.types.Panel): + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_label = "Unreal Tools" + + def poll(self, context): + return context.active_object + + def draw(self, context): + layout = self.layout + #layout.label(text="Unreal Tools") + rd = context.scene + #drop box + layout.prop(rd, "unrealexport_settings",expand=True) + #layout.prop(rd, "unrealexport_settings") + #button + layout.operator("object.UnrealExport") + #FPS #it use the real data from your scene + layout.prop(rd.render, "fps") + + layout.prop(rd, "unrealactionexportall") + #row = layout.row() + #row.label(text="Action Set(s)(not build)") + #for action in bpy.data.actions: + #print(dir( action)) + #print(action.get_frame_range()) + #row = layout.row() + #row.prop(action, "name") + + #print(dir(action.groups[0])) + #for g in action.groups:#those are bones + #print("group...") + #print(dir(g)) + #print("////////////") + #print((g.name)) + #print("////////////") + + #row.label(text="Active:" + action.select) + btrimesh = False + +class OBJECT_OT_UnrealExport(bpy.types.Operator): + global exportmessage + bl_idname = "OBJECT_OT_UnrealExport" + bl_label = "Unreal Export" + __doc__ = "Select export setting for .psk/.psa or both." + + def invoke(self, context, event): + #path = StringProperty(name="File Path", description="File path used for exporting the PSA file", maxlen= 1024, default= "") + print("Init Export Script:") + if(int(bpy.context.scene.unrealexport_settings) == 0): + bpy.context.scene.unrealexportpsk = True + bpy.context.scene.unrealexportpsa = False + print("Exporting PSK...") + if(int(bpy.context.scene.unrealexport_settings) == 1): + bpy.context.scene.unrealexportpsk = False + bpy.context.scene.unrealexportpsa = True + print("Exporting PSA...") + if(int(bpy.context.scene.unrealexport_settings) == 2): + bpy.context.scene.unrealexportpsk = True + bpy.context.scene.unrealexportpsa = True + print("Exporting ALL...") + + default_path = os.path.splitext(bpy.data.filepath)[0] + ".psk" + fs_callback(default_path, bpy.context, False) + + #self.report({'WARNING', 'INFO'}, exportmessage) + self.report({'INFO'}, exportmessage) + return{'FINISHED'} + +def register(): + global MENUPANELBOOL + if MENUPANELBOOL: + bpy.types.register(OBJECT_OT_UnrealExport) + bpy.types.register(VIEW3D_PT_unrealtools_objectmode) + bpy.types.register(ExportUDKAnimData) + bpy.types.INFO_MT_file_export.append(menu_func) + +def unregister(): + global MENUPANELBOOL + if MENUPANELBOOL: + bpy.types.unregister(OBJECT_OT_UnrealExport) + bpy.types.unregister(VIEW3D_PT_unrealtools_objectmode) + bpy.types.unregister(ExportUDKAnimData) + bpy.types.INFO_MT_file_export.remove(menu_func) + +if __name__ == "__main__": + register() diff --git a/io_import_images_as_planes.py b/io_import_images_as_planes.py new file mode 100644 index 00000000..555c9476 --- /dev/null +++ b/io_import_images_as_planes.py @@ -0,0 +1,528 @@ +# ##### 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 ##### + +""" +This script imports images and creates Planes with them as textures. +At the moment the naming for objects, materials, textures and meshes +is derived from the imagename. + +One can either import a single image, or all images in one directory. +When imporing a directory one can either check the checkbox or leave +the filename empty. + +As a bonus one can choose to import images of only one type. +Atm this is the list of possible extensions: +extList = + ('jpeg', 'jpg', 'png', 'tga', 'tiff', 'tif', 'exr', + 'hdr', 'avi', 'mov', 'mp4', 'ogg', 'bmp', 'cin', 'dpx', 'psd') + +If someone knows a better way of telling if a file is an image which +Blender can read, please tell so ;) + +when importing images that are allready referenced they are not +reimported but the old ones reused as not to clutter the materials, +textures and image lists. +Instead the plane gets linked against an existing material. + +If one reimports images but chooses different material/texture mapping +new materials are created. +So one doesn't has to go through everything if one decides differently +after importing 236 images. + +It also has an option to translate pixeldimensions into Blenderunits. +""" + +bl_addon_info = { + 'name': 'Import: Images as Planes', + 'author': 'Florian Meyer (testscreenings)', + 'version': '0.7', + 'blender': (2, 5, 3), + 'location': 'File > Import > Images as Planes', + 'description': 'Imports images and creates planes' \ + ' with the appropiate aspect ratio.' \ + ' The images are mapped to the planes.', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Add_Mesh/Planes_from_Images', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=21751&group_id=153&atid=469', + 'category': 'Import/Export'} + +import bpy +from bpy.props import * +from os import listdir +from mathutils import Vector + + +EXT_LIST = { + 'jpeg': ['jpeg', 'jpg', 'jpe'], + 'png': ['png'], + 'tga': ['tga', 'tpic'], + 'tiff': ['tiff', 'tif'], + 'exr': ['exr'], + 'hdr': ['hdr'], + 'avi': ['avi'], + 'mov': ['mov', 'qt'], + 'mp4': ['mp4'], + 'ogg': ['ogg', 'ogv'], + 'bmp': ['bmp', 'dib'], + 'cin': ['cin'], + 'dpx': ['dpx'], + 'psd': ['psd']} + + +# Apply view rotation to objects if "Align To" for new objects +# was set to "VIEW" in the User Preference. +def apply_view_rotation(ob): + context = bpy.context + align = bpy.context.user_preferences.edit.object_align + + if (context.space_data.type == 'VIEW_3D' + and align == 'VIEW'): + view3d = context.space_data + region = view3d.region_3d + viewMatrix = region.view_matrix + rot = viewMatrix.rotation_part() + ob.rotation_euler = rot.invert().to_euler() + + +# Create plane mesh +def createPlaneMesh(dimension, img): + # x is the x-aspectRatio. + x = img.size[0] / img.size[1] + y = 1 + + if dimension[0]: + x = (img.size[0] * (1.0 / dimension[1])) * 0.5 + y = (img.size[1] * (1.0 / dimension[1])) * 0.5 + + verts = [] + faces = [] + + v1 = (-x, -y, 0) + v2 = (x, -y, 0) + v3 = (x, y, 0) + v4 = (-x, y, 0) + + verts.append(v1) + verts.append(v2) + verts.append(v3) + verts.append(v4) + + faces.append([0, 1, 2, 3]) + + return verts, faces + + +# Create plane object +def createPlaneObj(img, dimension): + scene = bpy.context.scene + + verts, faces = createPlaneMesh(dimension, img) + + me = bpy.data.meshes.new(img.name) + me.from_pydata(verts, [], faces) + me.update() + + plane = bpy.data.objects.new(img.name, me) + plane.data.add_uv_texture() + + scene.objects.link(plane) + plane.location = scene.cursor_location + apply_view_rotation(plane) + + return plane + + +# Check if a file extension matches any +# valid (i.e. recognized) image/movie format. +def isImageFile(extension): + for ext, ext_list in EXT_LIST.items(): + if extension in ext_list: + return True + + return False + + +# Get imagepaths from directory +def getImageFilesInDirectory(directory, extension): + import os + + # Get all files in the directory. + allFiles = listdir(directory) + allImages = [] + + extensions = [] + + # Import all images files? + if extension == '*': + all = True + + else: + all = False + # Get the possible extensions + extensions = EXT_LIST[extension] + + # Put all image files in the list. + for file in allFiles: + # Get the file extension (includes the ".") + e = os.path.splitext(file)[1] + + # Separate by "." and get the last list-entry. + e = e.rpartition(".")[-1] + + # Convert to lower case + e = e.lower() + + if (e in extensions + or (all and isImageFile(e))): + allImages.append(file) + + return allImages + + +# Get image datablock from the (image's) filepath. +def getImage(path): + img = [] + + # Check every Image if it is already there. + for image in bpy.data.images: + # If image with same path exists take that one. + if image.filepath == path: + img = image + + # Else create new Image and load from path. + if not img: + name = path.rpartition('\\')[2].rpartition('.')[0] + img = bpy.data.images.new(name) + img.source = 'FILE' + img.filepath = path + + return img + + +# Create/get Material +def getMaterial(tex, mapping): + mat = [] + + # Check all existing materials. + for material in bpy.data.materials: + # If a material with name and mapping + # texture with image exists, take that one... + if (material.name == tex.image.name + and tex.name in material.texture_slots + and material.mapping == mapping): + mat = material + + # ... otherwise create new one and apply mapping. + if not mat: + mat = bpy.data.materials.new(name=tex.name) + mat.add_texture(tex, texture_coordinates='UV', map_to='COLOR') + mat.mapping = mapping + mat.name = tex.name + + return mat + + +# Create/get Texture +def getTexture(path, img): + tex = [] + + # Check all existing textures. + for texture in bpy.data.textures: + # If an (image)texture with image exists, take that one... + if (texture.type == 'IMAGE' + and texture.image + and texture.image.filepath == path): + tex = texture + + # ... otherwise create a new one and apply mapping. + if not tex: + name = path.rpartition('\\')[2].rpartition('.')[0] + tex = bpy.data.textures.new(name=name) + tex.type = 'IMAGE' + tex = tex.recast_type() + tex.image = img + + return tex + + +# Custom material property - get +def mapget(self): + """Custom property of the images_as_planes addon.""" + mapping = [] + mapping.append(self.shadeless) + mapping.append(self.transparency) + mapping.append(self.alpha) + mapping.append(self.specular_alpha) + mapping.append(self.transparency_method) + + if (self.texture_slots[0] + and self.texture_slots[0].texture.type == 'IMAGE' + and self.texture_slots[0].texture.image): + mapping.append(self.texture_slots[0].texture.image.premultiply) + + else: + mapping.append("no image") + + return mapping + + +# Custom material property - set +def mapset(self, value): + self.shadeless = value[0] + self.transparency = value[1] + self.alpha = float(value[2]) + self.specular_alpha = float(value[3]) + self.transparency_method = value[4] + + if (self.texture_slots[0] + and self.texture_slots[0].texture.type == 'IMAGE' + and self.texture_slots[0].texture.image): + self.texture_slots[0].texture.image.premultiply = value[5] + if self.alpha: + self.texture_slots[0].map_alpha=True + + +bpy.types.Material.mapping = property(mapget, mapset) + + +def main(filePath, options, mapping, dimension): + images = [] + scene = bpy.context.scene + + # If "Create from Directory" (no filepath or checkbox) #### + if options['dir'] or not filePath[1]: + imageFiles = getImageFilesInDirectory(filePath[2], options['ext']) + + # Check if images are loaded and put them in the list. + for imageFile in imageFiles: + img = getImage(str(filePath[2]) + "\\" + str(imageFile)) + images.append(img) + + # Deselect all objects. + bpy.ops.object.select_all(action='DESELECT') + + # Assign/get all things. + for img in images: + # Create/get Texture + tex = getTexture(img.filepath, img) + + # Create/get Material + mat = getMaterial(tex, mapping) + + # Create Plane + plane = createPlaneObj(img, dimension) + + # Assign Material + plane.data.add_material(mat) + + # Put Image into UVTextureLayer + plane.data.uv_textures[0].data[0].image = img + plane.data.uv_textures[0].data[0].tex = True + plane.data.uv_textures[0].data[0].transp = 'ALPHA' + plane.data.uv_textures[0].data[0].twoside = True + + plane.select = True + scene.objects.active = plane + + # If "Create Single Plane" (filepath and is image) + else: + # Deselect all objects. + bpy.ops.object.select_all(action='DESELECT') + + # Check if image is loaded. + img = getImage(filePath[0]) + + # Create/get Texture + tex = getTexture(filePath[0], img) + + # Create/get Material + mat = getMaterial(tex, mapping) + + # Create Plane + plane = createPlaneObj(img, dimension) + + # Assign Material + plane.data.add_material(mat) + + # Put image into UVTextureLayer + plane.data.uv_textures[0].data[0].image = img + plane.data.uv_textures[0].data[0].tex = True + plane.data.uv_textures[0].data[0].transp = 'ALPHA' + plane.data.uv_textures[0].data[0].twoside = True + + plane.select = True + scene.objects.active = plane + + +# Operator +class ImportImagesAsPlanes(bpy.types.Operator): + '''''' + bl_idname = "import.images_as_planes" + bl_label = "Import Images as Planes" + bl_description = "Create mesh plane(s) from image files" \ + " with the appropiate aspect ratio." + bl_options = {'REGISTER', 'UNDO'} + + filepath = StringProperty(name="File Path", + description="Filepath used for importing the file", + maxlen=1024, + default="") + filename = StringProperty(name="File Name", + description="Name of the file.") + directory = StringProperty(name="Directory", + description="Directory of the file.") + fromDirectory = BoolProperty(name="All in directory", + description="Import all image files (of the selected type)" \ + " in this directory.", + default=False) + + extEnum = [ + ('*', 'All image formats', + 'Import all know image (or movie) formats.'), + ('jpeg', 'JPEG (.jpg, .jpeg, .jpe)', + 'Joint Photographic Experts Group'), + ('png', 'PNG (.png)', 'Portable Network Graphics'), + ('tga', 'Truevision TGA (.tga, tpic)', ''), + ('tiff', 'TIFF (.tif, .tiff)', 'Tagged Image File Format'), + ('exr', 'OpenEXR (.exr)', 'OpenEXR HDR imaging image file format'), + ('hdr', 'Radiance HDR (.hdr, .pic)', ''), + ('avi', 'AVI (.avi)', 'Audio Video Interleave'), + ('mov', 'QuickTime (.mov, .qt)', ''), + ('mp4', 'MPEG-4 (.mp4)', ' MPEG-4 Part 14'), + ('ogg', 'OGG Theora (.ogg, .ogv)', ''), + ('bmp', 'BMP (.bmp, .dib)', 'Windows Bitmap'), + ('cin', 'CIN (.cin)', ''), + ('dpx', 'DPX (.dpx)', 'DPX (Digital Picture Exchange)'), + ('psd', 'PSD (.psd)', 'Photoshop Document')] + extension = EnumProperty(name="Extension", + description="Only import files of this type.", + items=extEnum) + + shadeless = BoolProperty(name="Shadeless", + description="Set material to shadeless", + default=False) + transp = BoolProperty(name="Use alpha", + description="Use alphachannel for transparency.", + default=False) + premultiply = BoolProperty(name="Premultiply", + description="Premultiply image", + default=False) + + tEnum = [ + ('Z_TRANSPARENCY', + 'Z Transparency', + 'Use alpha buffer for transparent faces'), + ('RAYTRACE', + 'Raytrace', + 'Use raytracing for transparent refraction rendering.')] + transp_method = EnumProperty(name="Transp. Method", + description="Transparency Method", + items=tEnum) + useDim = BoolProperty(name="Use image dimensions", + description="Use the images pixels to derive the size of the plane.", + default=False) + factor = IntProperty(name="Pixels/BU", + description="Number of pixels per Blenderunit.", + min=1, + default=500) + + def draw(self, context): + props = self.properties + layout = self.layout + box = layout.box() + box.label('Filter:', icon='FILTER') + box.prop(props, 'fromDirectory') + box.prop(props, 'extension', icon='FILE_IMAGE') + box = layout.box() + box.label('Material mappings:', icon='MATERIAL') + box.prop(props, 'shadeless') + box.prop(props, 'transp') + box.prop(props, 'premultiply') + box.prop(props, 'transp_method', expand=True) + box = layout.box() + box.label('Plane dimensions:', icon='ARROW_LEFTRIGHT') + box.prop(props, 'useDim') + box.prop(props, 'factor', expand=True) + + def execute(self, context): + # File Path + filepath = self.properties.filepath + filename = self.properties.filename + directory = self.properties.directory + filePath = (filepath, filename, directory) + + # General Options + fromDirectory = self.properties.fromDirectory + extension = self.properties.extension + options = {'dir': fromDirectory, 'ext': extension} + + # Mapping + alphavalue = 1 + transp = self.properties.transp + if transp: + alphavalue = 0 + + shadeless = self.properties.shadeless + transp_method = self.properties.transp_method + premultiply = self.properties.premultiply + + mapping = ([shadeless, + transp, + alphavalue, + alphavalue, + transp_method, + premultiply]) + + # Use Pixelsdimensions + useDim = self.properties.useDim + factor = self.properties.factor + dimension = (useDim, factor) + + # Call Main Function + main(filePath, options, mapping, dimension) + + return {'FINISHED'} + + def invoke(self, context, event): + wm = bpy.context.manager + wm.add_fileselect(self) + + return {'RUNNING_MODAL'} + + +# Registering / Unregister +menu_func = (lambda self, context: self.layout.operator( + ImportImagesAsPlanes.bl_idname, + text="Images as Planes", + icon='PLUGIN')) + + +def register(): + bpy.types.register(ImportImagesAsPlanes) + bpy.types.INFO_MT_file_import.append(menu_func) + + +def unregister(): + bpy.types.unregister(ImportImagesAsPlanes) + bpy.types.INFO_MT_file_import.remove(menu_func) + + +if __name__ == "__main__": + register() diff --git a/io_import_scene_mhx.py b/io_import_scene_mhx.py new file mode 100644 index 00000000..9800b99c --- /dev/null +++ b/io_import_scene_mhx.py @@ -0,0 +1,2246 @@ +""" +**Project Name:** MakeHuman + +**Product Home Page:** http://www.makehuman.org/ + +**Code Home Page:** http://code.google.com/p/makehuman/ + +**Authors:** Thomas Larsson + +**Copyright(c):** MakeHuman Team 2001-2010 + +**Licensing:** GPL3 (see also http://sites.google.com/site/makehumandocs/licensing) + +**Coding Standards:** See http://sites.google.com/site/makehumandocs/developers-guide + +Abstract +MHX (MakeHuman eXchange format) importer for Blender 2.5x. +Version 0.9 + +""" + +bl_addon_info = { + 'name': 'Import MakeHuman (.mhx)', + 'author': 'Thomas Larsson', + 'version': '0.9, Make Human Alpha 5', + 'blender': (2, 5, 3), + 'location': 'File > Import', + 'description': 'Import files in the MakeHuman eXchange format (.mhx)', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/File_I-O/Make_Human', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=21872&group_id=153&atid=469', + 'category': 'Import/Export'} + +""" +Place this file in the .blender/scripts/addons dir +You have to activated the script in the "Add-Ons" tab (user preferences). +Access from the File > Import menu. +""" + +# +# +# + +import bpy +import os +import time +import mathutils +from mathutils import * +import geometry +import string + +MAJOR_VERSION = 0 +MINOR_VERSION = 9 +MHX249 = False +Blender24 = False +Blender25 = True +TexDir = "~/makehuman/exports" + +# +# +# + +theScale = 1.0 +useMesh = 1 +doSmash = 1 +verbosity = 2 +warnedTextureDir = False +warnedVersion = False + +true = True +false = False +Epsilon = 1e-6 +nErrors = 0 +theTempDatum = None + +todo = [] + +# +# toggle flags +# + +T_ArmIK = 0x01 +T_LegIK = 0x02 +T_Replace = 0x20 +T_Face = 0x40 +T_Shape = 0x80 +T_Mesh = 0x100 +T_Armature = 0x200 +T_Proxy = 0x400 +T_Panel = 0x800 + +T_Rigify = 0x1000 +T_Preset = 0x2000 +T_Symm = 0x4000 +T_MHX = 0x8000 + +toggle = T_Replace + T_ArmIK + T_LegIK + T_Mesh + T_Armature + T_Face + +# +# setFlagsAndFloats(rigFlags): +# +# Global floats +fLegIK = 0.0 +fArmIK = 0.0 +fFingerPanel = 0.0 +fFingerIK = 0.0 +fFingerCurl = 0.0 + +# rigLeg and rigArm flags +T_Toes = 0x0001 +T_GoboFoot = 0x0002 +T_InvFoot = 0x0004 + +T_FingerPanel = 0x100 +T_FingerCurl = 0x0200 +T_FingerIK = 0x0400 + + +T_LocalFKIK = 0x8000 + +rigLeg = 0 +rigArm = 0 + +def setFlagsAndFloats(rigFlags): + global toggle, rigLeg, rigArm + + (footRig, fingerRig) = rigFlags + rigLeg = 0 + rigArm = 0 + if footRig == 'Reverse foot': rigLeg |= T_InvFoot + elif footRig == 'Gobo': rigLeg |= T_GoboFoot + + if fingerRig == 'Panel': rigArm |= T_FingerPanel + elif fingerRig == 'IK': rigArm |= T_FingerIK + elif fingerRig == 'Curl': rigArm |= T_FingerCurl + + toggle |= T_Panel + + # Global floats, used as influences + global fFingerCurl, fLegIK, fArmIK, fFingerIK + + fFingerCurl = 1.0 if rigArm&T_FingerCurl else 0.0 + fLegIK = 1.0 if toggle&T_LegIK else 0.0 + fArmIK = 1.0 if toggle&T_ArmIK else 0.0 + fFingerIK = 1.0 if rigArm&T_FingerIK else 0.0 + + return + + +# +# Dictionaries +# + +loadedData = { + 'NONE' : {}, + + 'Object' : {}, + 'Mesh' : {}, + 'Armature' : {}, + 'Lamp' : {}, + 'Camera' : {}, + 'Lattice' : {}, + 'Curve' : {}, + + 'Material' : {}, + 'Image' : {}, + 'MaterialTextureSlot' : {}, + 'Texture' : {}, + + 'Bone' : {}, + 'BoneGroup' : {}, + 'Rigify' : {}, + + 'Action' : {}, + 'Group' : {}, + + 'MeshTextureFaceLayer' : {}, + 'MeshColorLayer' : {}, + 'VertexGroup' : {}, + 'ShapeKey' : {}, + 'ParticleSystem' : {}, + + 'ObjectConstraints' : {}, + 'ObjectModifiers' : {}, + 'MaterialSlot' : {}, +} + +Plural = { + 'Object' : 'objects', + 'Mesh' : 'meshes', + 'Lattice' : 'lattices', + 'Curve' : 'curves', + 'Group' : 'groups', + 'Empty' : 'empties', + 'Armature' : 'armatures', + 'Bone' : 'bones', + 'BoneGroup' : 'bone_groups', + 'Pose' : 'poses', + 'PoseBone' : 'pose_bones', + 'Material' : 'materials', + 'Texture' : 'textures', + 'Image' : 'images', + 'Camera' : 'cameras', + 'Lamp' : 'lamps', + 'World' : 'worlds', +} + +# +# Creators +# + +def uvtexCreator(me, name): + print("uvtexCreator", me, name) + me.add_uv_texture() + uvtex = me.uv_textures[-1] + uvtex.name = name + return uvtex + + +def vertcolCreator(me, name): + print("vertcolCreator", me, name) + me.add_vertex_color() + vcol = me.vertex_colors[-1] + vcol.name = name + return vcol + + +# +# loadMhx(filePath, context, flags): +# + +def loadMhx(filePath, context, flags): + global toggle + toggle = flags + readMhxFile(filePath) + return + +# +# readMhxFile(filePath, rigFlags): +# + +def readMhxFile(filePath, rigFlags): + global todo, nErrors + + fileName = os.path.expanduser(filePath) + (shortName, ext) = os.path.splitext(fileName) + if ext != ".mhx": + print("Error: Not a mhx file: " + fileName) + return + print( "Opening MHX file "+ fileName ) + time1 = time.clock() + + ignore = False + stack = [] + tokens = [] + key = "toplevel" + level = 0 + nErrors = 0 + + setFlagsAndFloats(rigFlags) + + file= open(fileName, "rU") + print( "Tokenizing" ) + lineNo = 0 + for line in file: + # print(line) + lineSplit= line.split() + lineNo += 1 + if len(lineSplit) == 0: + pass + elif lineSplit[0] == '#': + pass + elif lineSplit[0] == 'end': + try: + sub = tokens + tokens = stack.pop() + if tokens: + tokens[-1][2] = sub + level -= 1 + except: + print( "Tokenizer error at or before line %d" % lineNo ) + print( line ) + dummy = stack.pop() + elif lineSplit[-1] == ';': + if lineSplit[0] == '\\': + key = lineSplit[1] + tokens.append([key,lineSplit[2:-1],[]]) + else: + key = lineSplit[0] + tokens.append([key,lineSplit[1:-1],[]]) + else: + key = lineSplit[0] + tokens.append([key,lineSplit[1:],[]]) + stack.append(tokens) + level += 1 + tokens = [] + file.close() + + if level != 0: + raise NameError("Tokenizer out of kilter %d" % level) + clearScene() + print( "Parsing" ) + parse(tokens) + + for (expr, glbals, lcals) in todo: + try: + # print("Doing %s" % expr) + exec(expr, glbals, lcals) + except: + msg = "Failed: "+expr + print( msg ) + nErrors += 1 + #raise NameError(msg) + + print("Postprocess") + postProcess() + print("HideLayers") + hideLayers() + time2 = time.clock() + print("toggle = %x" % toggle) + msg = "File %s loaded in %g s" % (fileName, time2-time1) + if nErrors: + msg += " but there where %d errors. " % (nErrors) + print(msg) + return # loadMhx + +# +# getObject(name, var, glbals, lcals): +# + +def getObject(name, var, glbals, lcals): + try: + ob = loadedData['Object'][name] + except: + if name != "None": + expr = "%s = loadedData['Object'][name]" % var + print("Todo ", expr) + todo.append((expr, glbals, lcals)) + ob = None + return ob + +# +# parse(tokens): +# + +ifResult = False + +def parse(tokens): + global warnedVersion, MHX249, ifResult + + for (key, val, sub) in tokens: + # print("Parse %s" % key) + data = None + if key == 'MHX': + if int(val[0]) != MAJOR_VERSION and int(val[1]) != MINOR_VERSION and not warnedVersion: + print("Warning: \nThis file was created with another version of MHX\n") + warnedVersion = True + + elif key == 'MHX249': + MHX249 = eval(val[0]) + print("Blender 2.49 compatibility mode is %s\n" % MHX249) + + elif key == 'if': + try: + ifResult = eval(val[0]) + except: + ifResult = False + if ifResult: + parse(sub) + + elif key == 'elif': + if not ifResult: + try: + ifResult = eval(val[0]) + except: + ifResult = False + if ifResult: + parse(sub) + + elif key == 'else': + if not ifResult: + parse(sub) + + + elif MHX249: + pass + + elif key == 'print': + msg = concatList(val) + print(msg) + elif key == 'warn': + msg = concatList(val) + print(msg) + elif key == 'error': + msg = concatList(val) + raise NameError(msg) + elif key == "Object": + parseObject(val, sub) + elif key == "Mesh": + data = parseMesh(val, sub) + elif key == "Curve": + data = parseCurve(val, sub) + elif key == "Lattice": + data = parseLattice(val, sub) + elif key == "Group": + data = parseGroup(val, sub) + elif key == "Armature": + data = parseArmature(val, sub) + elif key == "Pose": + data = parsePose(val, sub) + elif key == "Action": + data = parseAction(val, sub) + elif key == "Material": + data = parseMaterial(val, sub) + elif key == "Texture": + data = parseTexture(val, sub) + elif key == "Image": + data = parseImage(val, sub) + elif key == "Process": + parseProcess(val, sub) + elif key == 'AnimationData': + try: + ob = loadedData['Object'][val[0]] + except: + ob = None + if ob: + bpy.context.scene.objects.active = ob + parseAnimationData(ob, sub) + elif key == 'ShapeKeys': + try: + ob = loadedData['Object'][val[0]] + except: + ob = None + if ob: + bpy.context.scene.objects.active = ob + parseShapeKeys(ob, ob.data, val, sub) + else: + data = parseDefaultType(key, val, sub) + + if data and key != 'Mesh': + print( data ) + return + +# +# parseDefaultType(typ, args, tokens): +# + +def parseDefaultType(typ, args, tokens): + global todo + + name = args[0] + data = None + expr = "bpy.data.%s.new('%s')" % (Plural[typ], name) + print(expr) + data = eval(expr) + print(" ok", data) + + bpyType = typ.capitalize() + print(bpyType, name, data) + loadedData[bpyType][name] = data + if data == None: + return None + + for (key, val, sub) in tokens: + #print("%s %s" % (key, val)) + defaultKey(key, val, sub, 'data', [], globals(), locals()) + print("Done ", data) + return data + +# +# concatList(elts) +# + +def concatList(elts): + string = "" + for elt in elts: + string += " %s" % elt + return string + +# +# parseAction(args, tokens): +# parseFCurve(fcu, args, tokens): +# parseKeyFramePoint(pt, args, tokens): +# + +def parseAction(args, tokens): + name = args[0] + if invalid(args[1]): + return + + ob = bpy.context.object + bpy.ops.object.mode_set(mode='POSE') + if ob.animation_data: + ob.animation_data.action = None + created = {} + for (key, val, sub) in tokens: + if key == 'FCurve': + prepareActionFCurve(ob, created, val, sub) + + act = ob.animation_data.action + loadedData['Action'][name] = act + if act == None: + print("Ignoring action %s" % name) + return act + act.name = name + print("Action", name, act, ob) + + for (key, val, sub) in tokens: + if key == 'FCurve': + fcu = parseActionFCurve(act, ob, val, sub) + else: + defaultKey(key, val, sub, 'act', [], globals(), locals()) + ob.animation_data.action = None + bpy.ops.object.mode_set(mode='OBJECT') + return act + +def prepareActionFCurve(ob, created, args, tokens): + dataPath = args[0] + index = args[1] + (expr, channel) = channelFromDataPath(dataPath, index) + try: + if channel in created[expr]: + return + else: + created[expr].append(channel) + except: + created[expr] = [channel] + + times = [] + for (key, val, sub) in tokens: + if key == 'kp': + times.append(int(val[0])) + + try: + data = eval(expr) + except: + print("Ignoring illegal expression: %s" % expr) + return + + n = 0 + for t in times: + #bpy.context.scene.current_frame = t + bpy.ops.anim.change_frame(frame = t) + try: + data.keyframe_insert(channel) + n += 1 + except: + pass + #print("failed", data, expr, channel) + if n != len(times): + print("Mismatch", n, len(times), expr, channel) + return + +def channelFromDataPath(dataPath, index): + words = dataPath.split(']') + if len(words) == 1: + # location + expr = "ob" + channel = dataPath + elif len(words) == 2: + # pose.bones["tongue"].location + expr = "ob.%s]" % (words[0]) + cwords = words[1].split('.') + channel = cwords[1] + elif len(words) == 3: + # pose.bones["brow.R"]["mad"] + expr = "ob.%s]" % (words[0]) + cwords = words[1].split('"') + channel = cwords[1] + # print(expr, channel, index) + return (expr, channel) + +def parseActionFCurve(act, ob, args, tokens): + dataPath = args[0] + index = args[1] + (expr, channel) = channelFromDataPath(dataPath, index) + index = int(args[1]) + + success = False + for fcu in act.fcurves: + (expr1, channel1) = channelFromDataPath(fcu.data_path, fcu.array_index) + if expr1 == expr and channel1 == channel and fcu.array_index == index: + success = True + break + if not success: + return None + + n = 0 + for (key, val, sub) in tokens: + if key == 'kp': + try: + pt = fcu.keyframe_points[n] + pt.interpolation = 'LINEAR' + pt = parseKeyFramePoint(pt, val, sub) + n += 1 + except: + pass + #print(tokens) + #raise NameError("kp", fcu, n, len(fcu.keyframe_points), val) + else: + defaultKey(key, val, sub, 'fcu', [], globals(), locals()) + return fcu + +def parseKeyFramePoint(pt, args, tokens): + pt.co = (float(args[0]), float(args[1])) + if len(args) > 2: + pt.handle1 = (float(args[2]), float(args[3])) + pt.handle2 = (float(args[3]), float(args[5])) + return pt + +# +# parseAnimationData(rna, tokens): +# parseDriver(drv, args, tokens): +# parseDriverVariable(var, args, tokens): +# + +def parseAnimationData(rna, tokens): + if 0 and toggle & T_MHX: + return + if rna.animation_data == None: + rna.animation_data_create() + adata = rna.animation_data + for (key, val, sub) in tokens: + if key == 'FCurve': + fcu = parseAnimDataFCurve(adata, rna, val, sub) + else: + defaultKey(key, val, sub, 'adata', [], globals(), locals()) + return adata + +def parseAnimDataFCurve(adata, rna, args, tokens): + if invalid(args[2]): + return + dataPath = args[0] + index = int(args[1]) + # print("parseAnimDataFCurve", adata, dataPath, index) + for (key, val, sub) in tokens: + if key == 'Driver': + fcu = parseDriver(adata, dataPath, index, rna, val, sub) + elif key == 'FModifier': + parseFModifier(fcu, val, sub) + else: + defaultKey(key, val, sub, 'fcu', [], globals(), locals()) + return fcu + +""" + fcurve = con.driver_add("influence", 0) + driver = fcurve.driver + driver.type = 'AVERAGE' +""" +def parseDriver(adata, dataPath, index, rna, args, tokens): + if dataPath[-1] == ']': + words = dataPath.split(']') + expr = "rna." + words[0] + ']' + pwords = words[1].split('"') + prop = pwords[1] + # print("prop", expr, prop) + bone = eval(expr) + return None + else: + words = dataPath.split('.') + channel = words[-1] + expr = "rna" + for n in range(len(words)-1): + expr += "." + words[n] + expr += ".driver_add('%s', index)" % channel + + # print("expr", rna, expr) + fcu = eval(expr) + drv = fcu.driver + drv.type = args[0] + for (key, val, sub) in tokens: + if key == 'DriverVariable': + var = parseDriverVariable(drv, rna, val, sub) + else: + defaultKey(key, val, sub, 'drv', [], globals(), locals()) + return fcu + +def parseDriverVariable(drv, rna, args, tokens): + var = drv.variables.new() + var.name = args[0] + var.type = args[1] + nTarget = 0 + # print("var", var, var.name, var.type) + for (key, val, sub) in tokens: + if key == 'Target': + parseDriverTarget(var, nTarget, rna, val, sub) + nTarget += 1 + else: + defaultKey(key, val, sub, 'var', [], globals(), locals()) + return var + +def parseFModifier(fcu, args, tokens): + #fmod = fcu.modifiers.new() + fmod = fcu.modifiers[0] + #fmod.type = args[0] + #print("fmod", fmod, fmod.type) + for (key, val, sub) in tokens: + defaultKey(key, val, sub, 'fmod', [], globals(), locals()) + return fmod + +""" + var = driver.variables.new() + var.name = target_bone + var.targets[0].id_type = 'OBJECT' + var.targets[0].id = obj + var.targets[0].rna_path = driver_path +""" +def parseDriverTarget(var, nTarget, rna, args, tokens): + targ = var.targets[nTarget] + # targ.rna_path = args[0] + # targ.id_type = args[1] + targ.id = loadedData['Object'][args[0]] + for (key, val, sub) in tokens: + defaultKey(key, val, sub, 'targ', [], globals(), locals()) + #print("Targ", targ, targ.id, targ.data_path, targ.id_type, targ.bone_target, targ.use_local_space_transforms) + return targ + + +# +# parseMaterial(args, ext, tokens): +# parseMTex(mat, args, tokens): +# parseTexture(args, tokens): +# + +def parseMaterial(args, tokens): + global todo + name = args[0] + #print("Parse material "+name) + mat = bpy.data.materials.new(name) + if mat == None: + return None + loadedData['Material'][name] = mat + #print("Material %s %s %s" % (mat, name, loadedData['Material'][name])) + for (key, val, sub) in tokens: + if key == 'MTex': + parseMTex(mat, val, sub) + elif key == 'Ramp': + parseRamp(mat, val, sub) + elif key == 'SSS': + parseSSS(mat, val, sub) + elif key == 'Strand': + parseStrand(mat, val, sub) + else: + exclude = ['specular_intensity', 'tangent_shading'] + defaultKey(key, val, sub, 'mat', [], globals(), locals()) + #print("Done ", mat) + + return mat + +def parseMTex(mat, args, tokens): + global todo + index = int(args[0]) + texname = args[1] + texco = args[2] + mapto = args[3] + + mat.add_texture(texture = loadedData['Texture'][texname], texture_coordinates = texco, map_to = mapto) + mtex = mat.texture_slots[index] + #mat.use_textures[index] = Bool(use) + + for (key, val, sub) in tokens: + defaultKey(key, val, sub, "mtex", [], globals(), locals()) + + return mtex + +def parseTexture(args, tokens): + global todo + if verbosity > 2: + print( "Parsing texture %s" % args ) + name = args[0] + tex = bpy.data.textures.new(name) + typ = args[1] + tex.type = typ + tex = tex.recast_type() + loadedData['Texture'][name] = tex + + for (key, val, sub) in tokens: + if key == 'Image': + try: + imgName = val[0] + img = loadedData['Image'][imgName] + tex.image = img + except: + msg = "Unable to load image '%s'" % val[0] + elif key == 'Ramp': + parseRamp(tex, val, sub) + else: + defaultKey(key, val, sub, "tex", ['use_nodes', 'use_textures', 'contrast'], globals(), locals()) + + return tex + +def parseRamp(data, args, tokens): + nvar = "data.%s" % args[0] + use = "data.use_%s = True" % args[0] + exec(use) + ramp = eval(nvar) + elts = ramp.elements + n = 0 + for (key, val, sub) in tokens: + # print("Ramp", key, val) + if key == 'Element': + elts[n].color = eval(val[0]) + elts[n].position = eval(val[1]) + n += 1 + else: + defaultKey(key, val, sub, "tex", ['use_nodes', 'use_textures', 'contrast'], globals(), locals()) + +def parseSSS(mat, args, tokens): + sss = mat.subsurface_scattering + for (key, val, sub) in tokens: + defaultKey(key, val, sub, "sss", [], globals(), locals()) + +def parseStrand(mat, args, tokens): + strand = mat.strand + for (key, val, sub) in tokens: + defaultKey(key, val, sub, "strand", [], globals(), locals()) + +# +# doLoadImage(filepath): +# loadImage(filepath): +# parseImage(args, tokens): +# + +def doLoadImage(filepath): + path1 = os.path.expanduser(filepath) + file1 = os.path.realpath(path1) + if os.path.isfile(file1): + print( "Found file "+file1 ) + try: + img = bpy.data.images.load(file1) + return img + except: + print( "Cannot read image" ) + return None + else: + print( "No file "+file1 ) + return None + + +def loadImage(filepath): + global TexDir, warnedTextureDir, loadedData + + texDir = os.path.expanduser(TexDir) + path1 = os.path.expanduser(filepath) + file1 = os.path.realpath(path1) + (path, filename) = os.path.split(file1) + (name, ext) = os.path.splitext(filename) + print( "Loading ", filepath, " = ", filename ) + + # img = doLoadImage(texDir+"/"+name+".png") + # if img: + # return img + + img = doLoadImage(texDir+"/"+filename) + if img: + return img + + # img = doLoadImage(path+"/"+name+".png") + # if img: + # return img + + img = doLoadImage(path+"/"+filename) + if img: + return img + + if warnedTextureDir: + return None + warnedTextureDir = True + return None + TexDir = Draw.PupStrInput("TexDir? ", path, 100) + + texDir = os.path.expanduser(TexDir) + img = doLoadImage(texDir+"/"+name+".png") + if img: + return img + + img = doLoadImage(TexDir+"/"+filename) + return img + +def parseImage(args, tokens): + global todo + imgName = args[0] + img = None + for (key, val, sub) in tokens: + if key == 'Filename': + filename = val[0] + for n in range(1,len(val)): + filename += " " + val[n] + img = loadImage(filename) + if img == None: + return None + img.name = imgName + else: + defaultKey(key, val, sub, "img", ['depth', 'dirty', 'has_data', 'size', 'type'], globals(), locals()) + print ("Image %s" % img ) + loadedData['Image'][imgName] = img + return img + +# +# parseObject(args, tokens): +# createObject(type, name, data, datName): +# createObjectAndData(args, typ): +# + +def parseObject(args, tokens): + if verbosity > 2: + print( "Parsing object %s" % args ) + name = args[0] + typ = args[1] + datName = args[2] + try: + data = loadedData[typ.capitalize()][datName] + except: + data = None + + if data == None and typ != 'EMPTY': + print("Failed to find data: %s %s %s" % (name, typ, datName)) + return + + try: + ob = loadedData['Object'][name] + bpy.context.scene.objects.active = ob + #print("Found data") + except: + ob = createObject(typ, name, data, datName) + if bpy.context.object != ob: + print("Context", ob, bpy.context.object, bpy.context.scene.objects.active) + # ob = foo + + for (key, val, sub) in tokens: + if key == 'Modifier': + parseModifier(ob, val, sub) + elif key == 'Constraint': + parseConstraint(ob.constraints, val, sub) + elif key == 'AnimationData': + if eval(val[0]): + parseAnimationData(ob, sub) + elif key == 'ParticleSystem': + parseParticleSystem(ob, val, sub) + else: + defaultKey(key, val, sub, "ob", ['type', 'data'], globals(), locals()) + + # Needed for updating layers + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.object.mode_set(mode='OBJECT') + return + +def createObject(typ, name, data, datName): + #print( "Creating object %s %s %s" % (typ, name, data) ) + ob = bpy.data.objects.new(name, data) + loadedData[typ][datName] = data + loadedData['Object'][name] = ob + return ob + +def linkObject(ob, data): + #print("Data", data, ob.data) + if data and ob.data == None: + ob.data = data + scn = bpy.context.scene + scn.objects.link(ob) + scn.objects.active = ob + #print("Linked object", ob) + #print("Scene", scn) + #print("Active", scn.objects.active) + #print("Context", bpy.context.object) + return ob + +def createObjectAndData(args, typ): + datName = args[0] + obName = args[1] + bpy.ops.object.add(type=typ.upper()) + ob = bpy.context.object + ob.name = obName + ob.data.name = datName + loadedData[typ][datName] = ob.data + loadedData['Object'][obName] = ob + return ob.data + + +# +# parseModifier(ob, args, tokens): +# + +def parseModifier(ob, args, tokens): + name = args[0] + typ = args[1] + if typ == 'PARTICLE_SYSTEM': + return None + mod = ob.modifiers.new(name, typ) + for (key, val, sub) in tokens: + defaultKey(key, val, sub, 'mod', [], globals(), locals()) + return mod + +# +# parseParticleSystem(ob, args, tokens): +# parseParticles(particles, args, tokens): +# parseParticle(par, args, tokens): +# + +def parseParticleSystem(ob, args, tokens): + print(ob, bpy.context.object) + pss = ob.particle_systems + print(pss, pss.values()) + name = args[0] + typ = args[1] + #psys = pss.new(name, typ) + bpy.ops.object.particle_system_add() + print(pss, pss.values()) + psys = pss[-1] + psys.name = name + psys.settings.type = typ + loadedData['ParticleSystem'][name] = psys + print("Psys", psys) + + for (key, val, sub) in tokens: + if key == 'Particles': + parseParticles(psys, val, sub) + else: + defaultKey(key, val, sub, 'psys', [], globals(), locals()) + return psys + +def parseParticles(psys, args, tokens): + particles = psys.particles + bpy.ops.particle.particle_edit_toggle() + n = 0 + for (key, val, sub) in tokens: + if key == 'Particle': + parseParticle(particles[n], val, sub) + n += 1 + else: + for par in particles: + defaultKey(key, val, sub, 'par', [], globals(), locals()) + bpy.ops.particle.particle_edit_toggle() + return particles + +def parseParticle(par, args, tokens): + n = 0 + for (key, val, sub) in tokens: + if key == 'h': + h = par.hair[n] + h.location = eval(val[0]) + h.time = int(val[1]) + h.weight = float(val[2]) + n += 1 + elif key == 'location': + par.location = eval(val[0]) + return + +# +# unpackList(list_of_tuples): +# + +def unpackList(list_of_tuples): + l = [] + for t in list_of_tuples: + l.extend(t) + return l + +# +# parseMesh (args, tokens): +# + +def parseMesh (args, tokens): + global todo + if verbosity > 2: + print( "Parsing mesh %s" % args ) + + mename = args[0] + obname = args[1] + me = bpy.data.meshes.new(mename) + ob = createObject('Mesh', obname, me, mename) + + verts = [] + edges = [] + faces = [] + vertsTex = [] + texFaces = [] + + for (key, val, sub) in tokens: + if key == 'Verts': + verts = parseVerts(sub) + elif key == 'Edges': + edges = parseEdges(sub) + elif key == 'Faces': + faces = parseFaces(sub) + + if faces: + #x = me.from_pydata(verts, [], faces) + me.add_geometry(len(verts), 0, len(faces)) + me.verts.foreach_set("co", unpackList(verts)) + me.faces.foreach_set("verts_raw", unpackList(faces)) + else: + #x = me.from_pydata(verts, edges, []) + me.add_geometry(len(verts), len(edges), 0) + me.verts.foreach_set("co", unpackList(verts)) + me.edges.foreach_set("verts", unpackList(edges)) + #print(x) + me.update() + #print(me) + linkObject(ob, me) + + mats = [] + for (key, val, sub) in tokens: + if key == 'Verts' or \ + key == 'Edges': + pass + elif key == 'Faces': + parseFaces2(sub, me) + elif key == 'MeshTextureFaceLayer': + parseUvTexture(val, sub, me) + elif key == 'MeshColorLayer': + parseVertColorLayer(val, sub, me) + elif key == 'VertexGroup': + parseVertexGroup(ob, me, val, sub) + elif key == 'ShapeKeys': + parseShapeKeys(ob, me, val, sub) + elif key == 'Material': + try: + me.add_material(loadedData['Material'][val[0]]) + except: + print("Could not add material", val[0]) + else: + defaultKey(key, val, sub, "me", [], globals(), locals()) + + return me + +# +# parseVerts(tokens): +# parseEdges(tokens): +# parseFaces(tokens): +# parseFaces2(tokens, me): +# + +def parseVerts(tokens): + verts = [] + for (key, val, sub) in tokens: + if key == 'v': + verts.append( (float(val[0]), float(val[1]), float(val[2])) ) + return verts + +def parseEdges(tokens): + edges = [] + for (key, val, sub) in tokens: + if key == 'e': + edges.append((int(val[0]), int(val[1]))) + return edges + +def parseFaces(tokens): + faces = [] + for (key, val, sub) in tokens: + if key == 'f': + if len(val) == 3: + face = [int(val[0]), int(val[1]), int(val[2]), 0] + elif len(val) == 4: + face = [int(val[0]), int(val[1]), int(val[2]), int(val[3])] + faces.append(face) + return faces + +def parseFaces2(tokens, me): + n = 0 + for (key, val, sub) in tokens: + if key == 'ft': + f = me.faces[n] + f.material_index = int(val[0]) + f.smooth = int(val[1]) + n += 1 + elif key == 'ftall': + mat = int(val[0]) + smooth = int(val[1]) + for f in me.faces: + f.material_index = mat + f.smooth = smooth + return + + +# +# parseUvTexture(args, tokens, me): +# parseUvTexData(args, tokens, uvdata): +# + +def parseUvTexture(args, tokens, me): + me.add_uv_texture() + uvtex = me.uv_textures[-1] + name = args[0] + uvtex.name = name + loadedData['MeshTextureFaceLayer'][name] = uvtex + for (key, val, sub) in tokens: + if key == 'Data': + parseUvTexData(val, sub, uvtex.data) + else: + defaultKey(key, val, sub, "uvtex", [], globals(), locals()) + return + +def parseUvTexData(args, tokens, data): + n = 0 + for (key, val, sub) in tokens: + if key == 'vt': + data[n].uv1 = (float(val[0]), float(val[1])) + data[n].uv2 = (float(val[2]), float(val[3])) + data[n].uv3 = (float(val[4]), float(val[5])) + if len(val) > 6: + data[n].uv4 = (float(val[6]), float(val[7])) + n += 1 + else: + pass + #for i in range(n): + # defaultKey(key, val, sub, "data[i]", [], globals(), locals()) + return + +# +# parseVertColorLayer(args, tokens, me): +# parseVertColorData(args, tokens, data): +# + +def parseVertColorLayer(args, tokens, me): + name = args[0] + print("VertColorLayer", name) + me.add_vertex_color() + vcol = me.vertex_colors[-1] + vcol.name = name + loadedData['MeshColorLayer'][name] = vcol + for (key, val, sub) in tokens: + if key == 'Data': + parseVertColorData(val, sub, vcol.data) + else: + defaultKey(key, val, sub, "vcol", [], globals(), locals()) + return + +def parseVertColorData(args, tokens, data): + n = 0 + for (key, val, sub) in tokens: + if key == 'cv': + data[n].color1 = eval(val[0]) + data[n].color2 = eval(val[1]) + data[n].color3 = eval(val[2]) + data[n].color4 = eval(val[3]) + n += 1 + return + + +# +# parseVertexGroup(ob, me, args, tokens): +# + +def parseVertexGroup(ob, me, args, tokens): + global toggle + if verbosity > 2: + print( "Parsing vertgroup %s" % args ) + grpName = args[0] + try: + res = eval(args[1]) + except: + res = True + if not res: + return + + if (toggle & T_Armature) or (grpName in ['Eye_L', 'Eye_R', 'Gums', 'Head', 'Jaw', 'Left', 'Middle', 'Right', 'Scalp']): + group = ob.add_vertex_group(grpName) + group.name = grpName + loadedData['VertexGroup'][grpName] = group + for (key, val, sub) in tokens: + if key == 'wv': + ob.add_vertex_to_group( int(val[0]), group, float(val[1]), 'REPLACE') + return + + +# +# parseShapeKeys(ob, me, args, tokens): +# parseShapeKey(ob, me, args, tokens): +# addShapeKey(ob, name, vgroup, tokens): +# doShape(name): +# + +def doShape(name): + if (toggle & T_Shape+T_Face) and (name == 'Basis'): + return True + else: + return (toggle & T_Face) + +def parseShapeKeys(ob, me, args, tokens): + if bpy.context.object == None: + return + for (key, val, sub) in tokens: + if key == 'ShapeKey': + parseShapeKey(ob, me, val, sub) + elif key == 'AnimationData': + if me.shape_keys: + parseAnimationData(me.shape_keys, sub) + return + + +def parseShapeKey(ob, me, args, tokens): + if verbosity > 0: + print( "Parsing ob %s shape %s" % (bpy.context.object, args[0] )) + name = args[0] + lr = args[1] + if invalid(args[2]): + return + + if lr == 'Sym' or toggle & T_Symm: + addShapeKey(ob, name, None, tokens) + elif lr == 'LR': + addShapeKey(ob, name+'_L', 'Left', tokens) + addShapeKey(ob, name+'_R', 'Right', tokens) + else: + raise NameError("ShapeKey L/R %s" % lr) + return + +def addShapeKey(ob, name, vgroup, tokens): + bpy.ops.object.shape_key_add(False) + skey = ob.active_shape_key + if name != 'Basis': + skey.relative_key = loadedData['ShapeKey']['Basis'] + skey.name = name + if vgroup: + skey.vertex_group = vgroup + loadedData['ShapeKey'][name] = skey + + for (key, val, sub) in tokens: + if key == 'sv': + index = int(val[0]) + pt = skey.data[index].co + pt[0] += float(val[1]) + pt[1] += float(val[2]) + pt[2] += float(val[3]) + else: + defaultKey(key, val, sub, "skey", [], globals(), locals()) + + return + + +# +# parseArmature (obName, args, tokens) +# + +def parseArmature (args, tokens): + global toggle, theScale + if verbosity > 2: + print( "Parsing armature %s" % args ) + + amtname = args[0] + obname = args[1] + mode = args[2] + + if mode == 'Rigify': + toggle |= T_Rigify + theScale = 0.1 + return parseRigify(amtname, obname, tokens) + + toggle &= ~T_Rigify + theScale = 1.0 + amt = bpy.data.armatures.new(amtname) + ob = createObject('Armature', obname, amt, amtname) + + linkObject(ob, amt) + print("Linked") + + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') + + heads = {} + tails = {} + for (key, val, sub) in tokens: + if key == 'Bone': + bname = val[0] + if not invalid(val[1]): + bone = amt.edit_bones.new(bname) + parseBone(bone, amt.edit_bones, sub, heads, tails) + loadedData['Bone'][bname] = bone + else: + defaultKey(key, val, sub, "amt", ['MetaRig'], globals(), locals()) + bpy.ops.object.mode_set(mode='OBJECT') + return amt + +# +# parseRigify(amtname, obname, tokens): +# + +def parseRigify(amtname, obname, tokens): + (key,val,sub) = tokens[0] + if key != 'MetaRig': + raise NameError("Expected MetaRig") + typ = val[0] + if typ == "human": + bpy.ops.object.armature_human_advanced_add() + else: + bpy.ops.pose.metarig_sample_add(type = typ) + ob = bpy.context.scene.objects.active + amt = ob.data + loadedData['Rigify'][obname] = ob + loadedData['Armature'][amtname] = amt + loadedData['Object'][obname] = ob + print("Rigify object", ob, amt) + + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') + + heads = {} + tails = {} + for (bname, bone) in amt.edit_bones.items(): + heads[bname] = 10*theScale*bone.head + tails[bname] = 10*theScale*bone.tail + + for (key, val, sub) in tokens: + if key == 'Bone': + bname = val[0] + print("Bone", bname) + try: + bone = amt.edit_bones[bname] + except: + print("Did not find bone %s" % bname) + bone = None + print(" -> ", bone) + if bone: + parseBone(bone, amt.edit_bones, sub, heads, tails) + else: + defaultKey(key, val, sub, "amt", ['MetaRig'], globals(), locals()) + bpy.ops.object.mode_set(mode='OBJECT') + return amt + +# +# parseBone(bone, bones, tokens, heads, tails): +# + +def parseBone(bone, bones, tokens, heads, tails): + global todo + + for (key, val, sub) in tokens: + if key == "head": + bone.head = (float(val[0]), float(val[1]), float(val[2])) + elif key == "tail": + bone.tail = (float(val[0]), float(val[1]), float(val[2])) + elif key == "head-as": + target = val[0] + if val[1] == 'head': + bone.head = heads[bone.name] + bones[target].head - heads[target] + elif val[1] == 'tail': + bone.head = heads[bone.name] + bones[target].tail - tails[target] + else: + raise NameError("head-as %s" % val) + elif key == "tail-as": + target = val[0] + if val[1] == 'head': + bone.tail = tails[bone.name] + bones[target].head - heads[target] + elif val[1] == 'tail': + bone.tail = tails[bone.name] + bones[target].tail - tails[target] + else: + raise NameError("tail-as %s" % val) + elif key == 'hide_select': + pass + else: + defaultKey(key, val, sub, "bone", [], globals(), locals()) + + return bone + +# +# parsePose (args, tokens): +# + +def parsePose (args, tokens): + global todo + if toggle & T_Rigify: + return + name = args[0] + ob = loadedData['Object'][name] + bpy.context.scene.objects.active = ob + bpy.ops.object.mode_set(mode='POSE') + pbones = ob.pose.bones + nGrps = 0 + for (key, val, sub) in tokens: + if key == 'Posebone': + parsePoseBone(pbones, ob, val, sub) + elif key == 'BoneGroup': + parseBoneGroup(ob.pose, nGrps, val, sub) + nGrps += 1 + elif key == 'SetProp': + bone = val[0] + prop = val[1] + value = eval(val[2]) + pb = pbones[bone] + print("Setting", pb, prop, val) + pb[prop] = value + print("Prop set", pb[prop]) + else: + defaultKey(key, val, sub, "ob.pose", [], globals(), locals()) + bpy.ops.object.mode_set(mode='OBJECT') + return ob + + +# +# parsePoseBone(pbones, args, tokens): +# parseArray(data, exts, args): +# + +def parseBoneGroup(pose, nGrps, args, tokens): + global todo + return + print( "Parsing bonegroup %s" % args ) + name = args[0] + print(dir(pose.bone_groups)) + bg = pose.bone_groups.add() + print("Created", bg) + loadedData['BoneGroup'][name] = bg + for (key, val, sub) in tokens: + defaultKey(key, val, sub, "bg", [], globals(), locals()) + return + +def parsePoseBone(pbones, ob, args, tokens): + global todo + #print( "Parsing posebone %s" % args ) + if invalid(args[1]): + return + name = args[0] + pb = pbones[name] + + # Make posebone active - don't know how to do this in pose mode + bpy.ops.object.mode_set(mode='OBJECT') + ob.data.bones.active = pb.bone + bpy.ops.object.mode_set(mode='POSE') + + for (key, val, sub) in tokens: + if key == 'Constraint': + cns = parseConstraint(pb.constraints, val, sub) + elif key == 'bpyops': + expr = "bpy.ops.%s" % val[0] + print(expr) + print("ob", bpy.context.active_object) + print("b", bpy.context.active_bone) + print("pb", bpy.context.active_pose_bone) + print("md", bpy.context.mode) + exec(expr) + print("alive") + elif key == 'ik_dof': + parseArray(pb, ["ik_dof_x", "ik_dof_y", "ik_dof_z"], val) + elif key == 'ik_limit': + parseArray(pb, ["ik_limit_x", "ik_limit_y", "ik_limit_z"], val) + elif key == 'ik_max': + parseArray(pb, ["ik_max_x", "ik_max_y", "ik_max_z"], val) + elif key == 'ik_min': + parseArray(pb, ["ik_min_x", "ik_min_y", "ik_min_z"], val) + elif key == 'ik_stiffness': + parseArray(pb, ["ik_stiffness_x", "ik_stiffness_y", "ik_stiffness_z"], val) + else: + defaultKey(key, val, sub, "pb", [], globals(), locals()) + #print("pb %s done" % name) + return + +def parseArray(data, exts, args): + n = 1 + for ext in exts: + expr = "data.%s = %s" % (ext, args[n]) + # print(expr) + exec(expr) + n += 1 + return + +# +# parseConstraint(constraints, args, tokens) +# + +def parseConstraint(constraints, args, tokens): + if invalid(args[2]): + return None + cns = constraints.new(args[1]) + #bpy.ops.pose.constraint_add(type=args[1]) + #cns = pb.constraints[-1] + + cns.name = args[0] + #print("cns", cns.name) + for (key,val,sub) in tokens: + if key == 'invert': + parseArray(cns, ["invert_x", "invert_y", "invert_z"], val) + elif key == 'use': + parseArray(cns, ["use_x", "use_y", "use_z"], val) + elif key == 'pos_lock': + parseArray(cns, ["pos_lock_x", "pos_lock_y", "pos_lock_z"], val) + elif key == 'rot_lock': + parseArray(cns, ["rot_lock_x", "rot_lock_y", "rot_lock_z"], val) + else: + defaultKey(key, val, sub, "cns", [], globals(), locals()) + #print("cns %s done" % cns.name) + return cns + +def insertInfluenceIpo(cns, bone): + global todo + if bone != 'PArmIK_L' and bone != 'PArmIK_R' and bone != 'PLegIK_L' and bone != 'PLegIK_R': + return False + + if (toggle & T_FKIK): + fcurve = cns.driver_add("influence", 0) + fcurve.driver.type = 'AVERAGE' + + var = fcurve.driver.variables.new() + var.name = bone + var.targets[0].id_type = 'OBJECT' + var.targets[0].id = getObject('HumanRig', 'var.targets[0].id', globals(), locals()) + var.targets[0].bone_target = bone + var.targets[0].transform_type = 'LOC_X' + # controller_path = fk_chain.arm_p.path_to_id() + #var.targets[0].data_path = controller_path + '["hinge"]' + + mod = fcurve.modifiers[0] + mod.poly_order = 2 + mod.coefficients[0] = 0.0 + mod.coefficients[1] = 1.0 + elif bone == 'PArmIK_L' or bone == 'PArmIK_R': + if toggle & T_ArmIK: + cns.influence = 1.0 + else: + cns.influence = 0.0 + elif bone == 'PLegIK_L' or bone == 'PLegIK_R': + if toggle & T_LegIK: + cns.influence = 1.0 + else: + cns.influence = 0.0 + + return True + +# +# parseCurve (args, tokens): +# parseNurb(cu, nNurbs, args, tokens): +# parseBezier(nurb, n, args, tokens): +# + +def parseCurve (args, tokens): + global todo + if verbosity > 2: + print( "Parsing curve %s" % args ) + cu = createObjectAndData(args, 'Curve') + + nNurbs = 0 + for (key, val, sub) in tokens: + if key == 'Nurb': + parseNurb(cu, nNurbs, val, sub) + nNurbs += 1 + else: + defaultKey(key, val, sub, "cu", [], globals(), locals()) + return + +def parseNurb(cu, nNurbs, args, tokens): + if nNurbs > 0: + bpy.ops.object.curve_add(type='BEZIER_CURVE') + print(cu.splines, list(cu.splines), nNurbs) + nurb = cu.splines[nNurbs] + nPoints = int(args[0]) + print(nurb, nPoints) + for n in range(2, nPoints): + bpy.ops.curve.extrude(mode=1) + + n = 0 + for (key, val, sub) in tokens: + if key == 'bz': + parseBezier(nurb, n, val, sub) + n += 1 + elif key == 'pt': + parsePoint(nurb, n, val, sub) + n += 1 + else: + defaultKey(key, val, sub, "nurb", [], globals(), locals()) + return + +def parseBezier(nurb, n, args, tokens): + bez = nurb[n] + bez.co = eval(args[0]) + bez.handle1 = eval(args[1]) + bez.handle1_type = args[2] + bez.handle2 = eval(args[3]) + bez.handle2_type = args[4] + return + +def parsePoint(nurb, n, args, tokens): + pt = nurb[n] + pt.co = eval(args[0]) + return + +# +# parseLattice (args, tokens): +# + +def parseLattice (args, tokens): + global todo + if verbosity > 2: + print( "Parsing lattice %s" % args ) + lat = createObjectAndData(args, 'Lattice') + for (key, val, sub) in tokens: + if key == 'Points': + parseLatticePoints(val, sub, lat.points) + else: + defaultKey(key, val, sub, "lat", [], globals(), locals()) + return + +def parseLatticePoints(args, tokens, points): + global todo + n = 0 + for (key, val, sub) in tokens: + if key == 'pt': + v = points[n].co + (x,y,z) = eval(val[0]) + v.x = x + v.y = y + v.z = z + + v = points[n].deformed_co + (x,y,z) = eval(val[1]) + v.x = x + v.y = y + v.z = z + + n += 1 + return + +# +# parseGroup (args, tokens): +# + +def parseGroup (args, tokens): + global todo + if verbosity > 2: + print( "Parsing group %s" % args ) + + grpName = args[0] + grp = bpy.data.groups.new(grpName) + loadedData['Group'][grpName] = grp + for (key, val, sub) in tokens: + if key == 'Objects': + parseGroupObjects(val, sub, grp) + else: + defaultKey(key, val, sub, "grp", [], globals(), locals()) + return + +def parseGroupObjects(args, tokens, grp): + global todo + for (key, val, sub) in tokens: + if key == 'ob': + try: + ob = loadedData['Object'][val[0]] + grp.objects.link(ob) + except: + pass + return + +# +# postProcess() +# setInfluence(bones, cnsName, w): +# + +def postProcess(): + if not toggle & T_MHX: + return + if toggle & T_Rigify: + return + for rig in loadedData['Rigify'].values(): + bpy.context.scene.objects.active = rig + print("Rigify", rig) + bpy.ops.pose.metarig_generate() + print("Metarig generated") + #bpy.context.scene.objects.unlink(rig) + rig = bpy.context.scene.objects.active + print("Rigged", rig, bpy.context.object) + ob = loadedData['Object']['Human'] + mod = ob.modifiers[0] + print(ob, mod, mod.object) + mod.object = rig + print("Rig changed", mod.object) + return + +# +# parseProcess(args, tokens): +# + +def parseProcess(args, tokens): + return + rig = loadedData['Object'][args[0]] + parents = {} + objects = [] + + for (key, val, sub) in tokens: + if key == 'Reparent': + bname = val[0] + try: + eb = ebones[bname] + parents[bname] = eb.parent.name + eb.parent = ebones[val[1]] + except: + pass + elif key == 'Bend': + print(val) + axis = val[1] + angle = float(val[2]) + mat = mathutils.RotationMatrix(angle, 4, axis) + try: + pb = pbones[val[0]] + prod = pb.matrix_local * mat + for i in range(4): + for j in range(4): + pb.matrix_local[i][j] = prod[i][j] + print("Done", pb.matrix_local) + except: + pass + elif key == 'Pose': + bpy.context.scene.objects.active = rig + bpy.ops.object.mode_set(mode='POSE') + pbones = rig.pose.bones + elif key == 'Edit': + bpy.context.scene.objects.active = rig + bpy.ops.object.mode_set(mode='EDIT') + ebones = rig.data.edit_bones + elif key == 'Object': + bpy.ops.object.mode_set(mode='OBJECT') + try: + ob = loadedData['Object'][val[0]] + objects.append((ob,sub)) + except: + ob = None + if ob: + bpy.context.scene.objects.active = ob + mod = ob.modifiers[0] + ob.modifiers.remove(mod) + for (key1, val1, sub1) in sub: + if key1 == 'Modifier': + parseModifier(ob, val1, sub1) + + for (ob,tokens) in objects: + bpy.context.scene.objects.active = ob + bpy.ops.object.visual_transform_apply() + #print("vis", list(ob.modifiers)) + bpy.ops.object.modifier_apply(apply_as='DATA', modifier='Armature') + #print("app", list(ob.modifiers)) + + bpy.context.scene.objects.active = rig + bpy.ops.object.mode_set(mode='POSE') + bpy.ops.pose.armature_apply() + bpy.ops.object.mode_set(mode='EDIT') + ebones = rig.data.edit_bones + for (bname, pname) in parents.items(): + eb = ebones[bname] + par = ebones[pname] + if eb.connected: + par.tail = eb.head + eb.parent = par + bpy.ops.object.mode_set(mode='OBJECT') + + for (ob,tokens) in objects: + bpy.context.scene.objects.active = ob + for (key, val, sub) in tokens: + if key == 'Modifier': + parseModifier(ob, val, sub) + + return + +# +# defaultKey(ext, args, tokens, var, exclude, glbals, lcals): +# + +def defaultKey(ext, args, tokens, var, exclude, glbals, lcals): + global todo + + if ext == 'Property': + expr = "%s['%s'] = %s" % (var, args[0], args[1]) + print("Property", expr) + exec(expr, glbals, lcals) + #print("execd") + return + + nvar = "%s.%s" % (var, ext) + # print(ext) + if ext in exclude: + return + #print("D", nvar) + + if len(args) == 0: + raise NameError("Key length 0: %s" % ext) + + rnaType = args[0] + if rnaType == 'Add': + print("*** Cannot Add yet ***") + return + + elif rnaType == 'Refer': + typ = args[1] + name = args[2] + data = "loadedData['%s']['%s']" % (typ, name) + + elif rnaType == 'Struct' or rnaType == 'Define': + typ = args[1] + name = args[2] + try: + data = eval(nvar, glbals, lcals) + except: + data = None + # print("Old structrna", nvar, data) + + if data == None: + try: + creator = args[3] + except: + creator = None + # print("Creator", creator, eval(var,glbals,lcals)) + + try: + rna = eval(var,glbals,lcals) + data = eval(creator) + except: + data = None + # print("New struct", nvar, typ, data) + + if rnaType == 'Define': + loadedData[typ][name] = data + + if data: + for (key, val, sub) in tokens: + defaultKey(key, val, sub, "data", [], globals(), locals()) + + print("Struct done", nvar) + return + + elif rnaType == 'PropertyRNA': + raise NameError("PropertyRNA!") + #print("PropertyRNA ", ext, var) + for (key, val, sub) in tokens: + defaultKey(ext, val, sub, nvar, [], glbals, lcals) + return + + elif rnaType == 'Array': + for n in range(1, len(args)): + expr = "%s[%d] = %s" % (nvar, n-1, args[n]) + exec(expr, glbals, lcals) + if len(args) > 0: + expr = "%s[0] = %s" % (nvar, args[1]) + exec(expr, glbals, lcals) + return + + elif rnaType == 'List': + data = [] + for (key, val, sub) in tokens: + elt = eval(val[1], glbals, lcals) + data.append(elt) + + elif rnaType == 'Matrix': + return + i = 0 + n = len(tokens) + for (key, val, sub) in tokens: + if key == 'row': + for j in range(n): + expr = "%s[%d][%d] = %g" % (nvar, i, j, float(val[j])) + exec(expr, glbals, lcals) + i += 1 + return + + else: + try: + data = loadedData[rnaType][args[1]] + #print("From loaded", rnaType, args[1], data) + return data + except: + data = rnaType + + #print(var, ext, data) + expr = "%s = %s" % (nvar, data) + try: + exec(expr, glbals, lcals) + except: + #print("Failed ",expr) + todo.append((expr, glbals, lcals)) + return + +# +# parseBoolArray(mask): +# + +def parseBoolArray(mask): + list = [] + for c in mask: + if c == '0': + list.append(False) + else: + list.append(True) + return list + +# parseMatrix(args, tokens) +# + +def parseMatrix(args, tokens): + matrix = Matrix( [1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,0,1] ) + i = 0 + for (key, val, sub) in tokens: + if key == 'row': + matrix[i][0] = float(val[0]) + matrix[i][1] = float(val[1]) + matrix[i][2] = float(val[2]) + matrix[i][3] = float(val[3]) + i += 1 + return matrix + +# +# parseDefault(data, tokens, exclude): +# + +def parseDefault(data, tokens): + for (key, val, sub) in tokens: + defaultKey(key, val, sub, "data", exclude, globals(), locals()) + + +# +# Utilities +# + +# +# extractBpyType(data): +# + +def extractBpyType(data): + typeSplit = str(type(data)).split("'") + if typeSplit[0] != ' Import ', + 'description': 'Import Unreal Engine (.psk)', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/'\ + 'Scripts/File_I-O/Unreal_psk_psa', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=21366&group_id=153&atid=469', + 'category': 'Import/Export'} + + +import bpy +import mathutils +import os +import sys +import string +import math +import re +from string import * +from struct import * +from math import * + +#from bpy.props import * + +import mathutils + +#output log in to txt file +DEBUGLOG = False + +scale = 1.0 +bonesize = 1.0 +md5_bones=[] + +def unpack_list(list_of_tuples): + l = [] + for t in list_of_tuples: + l.extend(t) + return l +""" +class md5_bone: + bone_index=0 + name="" + bindpos=[] + bindmat = mathutils.Quaternion() + parent="" + parent_index=0 + blenderbone=None + roll=0 + + def __init__(self): + self.bone_index=0 + self.name="" + self.bindpos=[0.0]*3 + self.bindmat=[None]*3 #is this how you initilize a 2d-array + for i in range(3): self.bindmat[i] = [0.0]*3 + self.parent="" + self.parent_index=0 + self.blenderbone=None + + def dump(self): + print ("bone index: ", self.bone_index) + print ("name: ", self.name) + print ("bind position: ", self.bindpos) + print ("bind translation matrix: ", self.bindmat) + print ("parent: ", self.parent) + print ("parent index: ", self.parent_index) + print ("blenderbone: ", self.blenderbone) +""" +class md5_bone: + bone_index=0 + name="" + bindpos=[] + bindmat=[] + scale = [] + parent="" + parent_index=0 + blenderbone=None + roll=0 + + def __init__(self): + self.bone_index=0 + self.name="" + self.bindpos=[0.0]*3 + self.scale=[0.0]*3 + self.bindmat=[None]*3 #is this how you initilize a 2d-array + for i in range(3): self.bindmat[i] = [0.0]*3 + self.parent="" + self.parent_index=0 + self.blenderbone=None + + def dump(self): + print ("bone index: ", self.bone_index) + print ("name: ", self.name) + print ("bind position: ", self.bindpos) + print ("bind translation matrix: ", self.bindmat) + print ("parent: ", self.parent) + print ("parent index: ", self.parent_index) + print ("blenderbone: ", self.blenderbone) + +#http://www.blender.org/forum/viewtopic.php?t=13340&sid=8b17d5de07b17960021bbd72cac0495f +def fixRollZ(b): + v = (b.tail-b.head)/b.length + b.roll -= math.degrees(math.atan2(v[0]*v[2]*(1 - v[1]),v[0]*v[0] + v[1]*v[2]*v[2])) +def fixRoll(b): + v = (b.tail-b.head)/b.length + if v[2]*v[2] > .5: + #align X-axis + b.roll += math.degrees(math.atan2(v[0]*v[2]*(1 - v[1]),v[2]*v[2] + v[1]*v[0]*v[0])) + else: + #align Z-axis + b.roll -= math.degrees(math.atan2(v[0]*v[2]*(1 - v[1]),v[0]*v[0] + v[1]*v[2]*v[2])) + +def pskimport(infile): + global DEBUGLOG + print ("--------------------------------------------------") + print ("---------SCRIPT EXECUTING PYTHON IMPORTER---------") + print ("--------------------------------------------------") + print ("Importing file: ", infile) + + md5_bones=[] + pskfile = open(infile,'rb') + if (DEBUGLOG): + logpath = infile.replace(".psk", ".txt") + print("logpath:",logpath) + logf = open(logpath,'w') + + def printlog(strdata): + if (DEBUGLOG): + logf.write(strdata) + + objName = infile.split('\\')[-1].split('.')[0] + + me_ob = bpy.data.meshes.new(objName) + print("objName:",objName) + printlog(("New Mesh = " + me_ob.name + "\n")) + #read general header + indata = unpack('20s3i',pskfile.read(32)) + #not using the general header at this time + #================================================================================================== + # vertex point + #================================================================================================== + #read the PNTS0000 header + indata = unpack('20s3i',pskfile.read(32)) + recCount = indata[3] + printlog(( "Nbr of PNTS0000 records: " + str(recCount) + "\n")) + counter = 0 + verts = [] + while counter < recCount: + counter = counter + 1 + indata = unpack('3f',pskfile.read(12)) + #print(indata[0],indata[1],indata[2]) + verts.extend([(indata[0],indata[1],indata[2])]) + #Tmsh.verts.append(NMesh.Vert(indata[0],indata[1],indata[2])) + + #================================================================================================== + # UV + #================================================================================================== + #read the VTXW0000 header + indata = unpack('20s3i',pskfile.read(32)) + recCount = indata[3] + printlog( "Nbr of VTXW0000 records: " + str(recCount)+ "\n") + counter = 0 + UVCoords = [] + #UVCoords record format = [index to PNTS, U coord, v coord] + while counter < recCount: + counter = counter + 1 + indata = unpack('hhffhh',pskfile.read(16)) + UVCoords.append([indata[0],indata[2],indata[3]]) + #print([indata[0],indata[2],indata[3]]) + #print([indata[1],indata[2],indata[3]]) + + #================================================================================================== + # Face + #================================================================================================== + #read the FACE0000 header + indata = unpack('20s3i',pskfile.read(32)) + recCount = indata[3] + printlog( "Nbr of FACE0000 records: "+ str(recCount) + "\n") + #PSK FACE0000 fields: WdgIdx1|WdgIdx2|WdgIdx3|MatIdx|AuxMatIdx|SmthGrp + #associate MatIdx to an image, associate SmthGrp to a material + SGlist = [] + counter = 0 + faces = [] + faceuv = [] + while counter < recCount: + counter = counter + 1 + indata = unpack('hhhbbi',pskfile.read(12)) + #the psk values are: nWdgIdx1|WdgIdx2|WdgIdx3|MatIdx|AuxMatIdx|SmthGrp + #indata[0] = index of UVCoords + #UVCoords[indata[0]]=[index to PNTS, U coord, v coord] + #UVCoords[indata[0]][0] = index to PNTS + PNTSA = UVCoords[indata[0]][0] + PNTSB = UVCoords[indata[1]][0] + PNTSC = UVCoords[indata[2]][0] + #print(PNTSA,PNTSB,PNTSC) #face id vertex + #faces.extend([0,1,2,0]) + faces.extend([PNTSA,PNTSB,PNTSC,0]) + uv = [] + u0 = UVCoords[indata[0]][1] + v0 = UVCoords[indata[0]][2] + uv.append([u0,v0]) + u1 = UVCoords[indata[1]][1] + v1 = UVCoords[indata[1]][2] + uv.append([u1,v1]) + u2 = UVCoords[indata[2]][1] + v2 = UVCoords[indata[2]][2] + uv.append([u2,v2]) + faceuv.append(uv) + #print("UV: ",u0,v0) + #update the uv var of the last item in the Tmsh.faces list + # which is the face just added above + ##Tmsh.faces[-1].uv = [(u0,v0),(u1,v1),(u2,v2)] + #print("smooth:",indata[5]) + #collect a list of the smoothing groups + if SGlist.count(indata[5]) == 0: + SGlist.append(indata[5]) + print("smooth:",indata[5]) + #assign a material index to the face + #Tmsh.faces[-1].materialIndex = SGlist.index(indata[5]) + printlog( "Using Materials to represent PSK Smoothing Groups...\n") + #========== + # skip something... + #========== + + #================================================================================================== + # Material + #================================================================================================== + ## + #read the MATT0000 header + indata = unpack('20s3i',pskfile.read(32)) + recCount = indata[3] + printlog("Nbr of MATT0000 records: " + str(recCount) + "\n" ) + printlog(" - Not importing any material data now. PSKs are texture wrapped! \n") + counter = 0 + while counter < recCount: + counter = counter + 1 + indata = unpack('64s6i',pskfile.read(88)) + ## + + #================================================================================================== + # Bones (Armature) + #================================================================================================== + #read the REFSKEL0 header + indata = unpack('20s3i',pskfile.read(32)) + recCount = indata[3] + printlog( "Nbr of REFSKEL0 records: " + str(recCount) + "\n") + Bns = [] + bone = [] + nobone = 0 + #================================================================================================== + # Bone Data + #================================================================================================== + counter = 0 + print ("---PRASE--BONES---") + while counter < recCount: + indata = unpack('64s3i11f',pskfile.read(120)) + #print( "DATA",str(indata)) + bone.append(indata) + + createbone = md5_bone() + #temp_name = indata[0][:30] + temp_name = indata[0] + + temp_name = bytes.decode(temp_name) + temp_name = temp_name.lstrip(" ") + temp_name = temp_name.rstrip(" ") + temp_name = temp_name.strip() + temp_name = temp_name.strip( bytes.decode(b'\x00')) + print ("temp_name:", temp_name, "||") + createbone.name = temp_name + createbone.bone_index = counter + createbone.parent_index = indata[3] + createbone.bindpos[0] = indata[8] + createbone.bindpos[1] = indata[9] + createbone.bindpos[2] = indata[10] + createbone.scale[0] = indata[12] + createbone.scale[1] = indata[13] + createbone.scale[2] = indata[14] + + #w,x,y,z + if (counter == 0):#main parent + print("no parent bone") + createbone.bindmat = mathutils.Quaternion((indata[7],indata[4],indata[5],indata[6])) + #createbone.bindmat = mathutils.Quaternion((indata[7],-indata[4],-indata[5],-indata[6])) + else:#parent + print("parent bone") + createbone.bindmat = mathutils.Quaternion((indata[7],-indata[4],-indata[5],-indata[6])) + #createbone.bindmat = mathutils.Quaternion((indata[7],indata[4],indata[5],indata[6])) + + md5_bones.append(createbone) + counter = counter + 1 + bnstr = (str(indata[0])) + Bns.append(bnstr) + + for pbone in md5_bones: + pbone.parent = md5_bones[pbone.parent_index].name + + bonecount = 0 + for armbone in bone: + temp_name = armbone[0][:30] + #print ("BONE NAME: ",len(temp_name)) + temp_name=str((temp_name)) + #temp_name = temp_name[1] + #print ("BONE NAME: ",temp_name) + bonecount +=1 + print ("-------------------------") + print ("----Creating--Armature---") + print ("-------------------------") + + #================================================================================================ + #Check armature if exist if so create or update or remove all and addnew bone + #================================================================================================ + #bpy.ops.object.mode_set(mode='OBJECT') + meshname ="ArmObject" + objectname = "armaturedata" + bfound = False + arm = None + for obj in bpy.data.objects: + if (obj.name == meshname): + bfound = True + arm = obj + break + + if bfound == False: + armdata = bpy.data.armatures.new(objectname) + ob_new = bpy.data.objects.new(meshname, armdata) + #ob_new = bpy.data.objects.new(meshname, 'ARMATURE') + #ob_new.data = armdata + bpy.context.scene.objects.link(ob_new) + #bpy.ops.object.mode_set(mode='OBJECT') + for i in bpy.context.scene.objects: i.select = False #deselect all objects + ob_new.select = True + #set current armature to edit the bone + bpy.context.scene.objects.active = ob_new + #set mode to able to edit the bone + bpy.ops.object.mode_set(mode='EDIT') + #newbone = ob_new.data.edit_bones.new('test') + #newbone.tail.y = 1 + print("creating bone(s)") + for bone in md5_bones: + #print(dir(bone)) + newbone = ob_new.data.edit_bones.new(bone.name) + #parent the bone + parentbone = None + print("bone name:",bone.name) + #note bone location is set in the real space or global not local + if bone.name != bone.parent: + + pos_x = bone.bindpos[0] + pos_y = bone.bindpos[1] + pos_z = bone.bindpos[2] + + #print( "LINKING:" , bone.parent ,"j") + parentbone = ob_new.data.edit_bones[bone.parent] + newbone.parent = parentbone + rotmatrix = bone.bindmat.to_matrix().resize4x4().rotation_part() + + #parent_head = parentbone.head * parentbone.matrix.to_quat().inverse() + #parent_tail = parentbone.tail * parentbone.matrix.to_quat().inverse() + #location=Vector(pos_x,pos_y,pos_z) + #set_position = (parent_tail - parent_head) + location + #print("tmp head:",set_position) + + #pos_x = set_position.x + #pos_y = set_position.y + #pos_z = set_position.z + + newbone.head.x = parentbone.head.x + pos_x + newbone.head.y = parentbone.head.y + pos_y + newbone.head.z = parentbone.head.z + pos_z + print("head:",newbone.head) + newbone.tail.x = parentbone.head.x + (pos_x + bonesize * rotmatrix[1][0]) + newbone.tail.y = parentbone.head.y + (pos_y + bonesize * rotmatrix[1][1]) + newbone.tail.z = parentbone.head.z + (pos_z + bonesize * rotmatrix[1][2]) + else: + rotmatrix = bone.bindmat.to_matrix().resize4x4().rotation_part() + newbone.head.x = bone.bindpos[0] + newbone.head.y = bone.bindpos[1] + newbone.head.z = bone.bindpos[2] + newbone.tail.x = bone.bindpos[0] + bonesize * rotmatrix[1][0] + newbone.tail.y = bone.bindpos[1] + bonesize * rotmatrix[1][1] + newbone.tail.z = bone.bindpos[2] + bonesize * rotmatrix[1][2] + #print("no parent") + + bpy.context.scene.update() + + #================================================================================================== + #END BONE DATA BUILD + #================================================================================================== + VtxCol = [] + for x in range(len(Bns)): + #change the overall darkness of each material in a range between 0.1 and 0.9 + tmpVal = ((float(x)+1.0)/(len(Bns))*0.7)+0.1 + tmpVal = int(tmpVal * 256) + tmpCol = [tmpVal,tmpVal,tmpVal,0] + #Change the color of each material slightly + if x % 3 == 0: + if tmpCol[0] < 128: tmpCol[0] += 60 + else: tmpCol[0] -= 60 + if x % 3 == 1: + if tmpCol[1] < 128: tmpCol[1] += 60 + else: tmpCol[1] -= 60 + if x % 3 == 2: + if tmpCol[2] < 128: tmpCol[2] += 60 + else: tmpCol[2] -= 60 + #Add the material to the mesh + VtxCol.append(tmpCol) + + #================================================================================================== + # Bone Weight + #================================================================================================== + #read the RAWW0000 header + indata = unpack('20s3i',pskfile.read(32)) + recCount = indata[3] + printlog( "Nbr of RAWW0000 records: " + str(recCount) +"\n") + #RAWW0000 fields: Weight|PntIdx|BoneIdx + RWghts = [] + counter = 0 + while counter < recCount: + counter = counter + 1 + indata = unpack('fii',pskfile.read(12)) + RWghts.append([indata[1],indata[2],indata[0]]) + #RWghts fields = PntIdx|BoneIdx|Weight + RWghts.sort() + printlog( "len(RWghts)=" + str(len(RWghts)) + "\n") + #Tmsh.update() + + #set the Vertex Colors of the faces + #face.v[n] = RWghts[0] + #RWghts[1] = index of VtxCol + """ + for x in range(len(Tmsh.faces)): + for y in range(len(Tmsh.faces[x].v)): + #find v in RWghts[n][0] + findVal = Tmsh.faces[x].v[y].index + n = 0 + while findVal != RWghts[n][0]: + n = n + 1 + TmpCol = VtxCol[RWghts[n][1]] + #check if a vertex has more than one influence + if n != len(RWghts)-1: + if RWghts[n][0] == RWghts[n+1][0]: + #if there is more than one influence, use the one with the greater influence + #for simplicity only 2 influences are checked, 2nd and 3rd influences are usually very small + if RWghts[n][2] < RWghts[n+1][2]: + TmpCol = VtxCol[RWghts[n+1][1]] + Tmsh.faces[x].col.append(NMesh.Col(TmpCol[0],TmpCol[1],TmpCol[2],0)) + """ + if (DEBUGLOG): + logf.close() + #================================================================================================== + #Building Mesh + #================================================================================================== + print("vertex:",len(verts),"faces:",len(faces)) + me_ob.add_geometry(len(verts), 0, int(len(faces)/4)) + me_ob.verts.foreach_set("co", unpack_list(verts)) + + me_ob.faces.foreach_set("verts_raw", faces) + me_ob.faces.foreach_set("smooth", [False] * len(me_ob.faces)) + me_ob.update() + + #=================================================================================================== + #UV Setup + #=================================================================================================== + texture = [] + texturename = "text1" + #print(dir(bpy.data)) + if (len(faceuv) > 0): + me_ob.add_uv_texture() #add one uv texture + for i, face in enumerate(me_ob.faces): + blender_tface= me_ob.uv_textures[0].data[i] #face + blender_tface.uv1 = faceuv[i][0] #uv = (0,0) + blender_tface.uv2 = faceuv[i][1] #uv = (0,0) + blender_tface.uv3 = faceuv[i][2] #uv = (0,0) + texture.append(me_ob.uv_textures[0]) + + #for tex in me_ob.uv_textures: + #print("mesh tex:",dir(tex)) + #print((tex.name)) + + #=================================================================================================== + #Material Setup + #=================================================================================================== + materialname = "mat" + materials = [] + + matdata = bpy.data.materials.new(materialname) + #color is 0 - 1 not in 0 - 255 + #matdata.mirror_color=(float(0.04),float(0.08),float(0.44)) + matdata.diffuse_color=(float(0.04),float(0.08),float(0.44))#blue color + #print(dir(me_ob.uv_textures[0].data)) + texdata = None + texdata = bpy.data.textures[len(bpy.data.textures)-1] + if (texdata != None): + #print(texdata.name) + #print(dir(texdata)) + texdata.name = "texturelist1" + matdata.active_texture = texdata + materials.append(matdata) + #matdata = bpy.data.materials.new(materialname) + #materials.append(matdata) + #= make sure the list isnt too big + for material in materials: + #add material to the mesh list of materials + me_ob.add_material(material) + #=================================================================================================== + # + #=================================================================================================== + obmesh = bpy.data.objects.new(objName,me_ob) + #check if there is a material to set to + if len(materials) > 0: + obmesh.active_material = materials[0] #material setup tmp + + bpy.context.scene.objects.link(obmesh) + + bpy.context.scene.update() + + print ("PSK2Blender completed") +#End of def pskimport######################### + +def getInputFilename(filename): + checktype = filename.split('\\')[-1].split('.')[1] + print ("------------",filename) + if checktype.upper() != 'PSK': + print (" Selected file = ",filename) + raise (IOError, "The selected input file is not a *.psk file") + pskimport(filename) + +from bpy.props import * + +class IMPORT_OT_psk(bpy.types.Operator): + '''Load a skeleton mesh psk File''' + bl_idname = "import_scene.psk" + bl_label = "Import PSK" + + # List of operator properties, the attributes will be assigned + # to the class instance from the operator settings before calling. + filepath = StringProperty(name="File Path", description="Filepath used for importing the OBJ file", maxlen= 1024, default= "") + + def execute(self, context): + getInputFilename(self.properties.filepath) + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.manager + wm.add_fileselect(self) + return {'RUNNING_MODAL'} + +menu_func = lambda self, context: self.layout.operator(IMPORT_OT_psk.bl_idname, text="Skeleton Mesh (.psk)") + +def register(): + bpy.types.register(IMPORT_OT_psk) + bpy.types.INFO_MT_file_import.append(menu_func) + +def unregister(): + bpy.types.unregister(IMPORT_OT_psk) + bpy.types.INFO_MT_file_import.remove(menu_func) + +if __name__ == "__main__": + register() +#note this only read the data and will not be place in the scene +#getInputFilename('C:\\blenderfiles\\BotA.psk') +#getInputFilename('C:\\blenderfiles\\AA.PSK') diff --git a/io_mesh_raw/__init__.py b/io_mesh_raw/__init__.py new file mode 100644 index 00000000..1af09c9f --- /dev/null +++ b/io_mesh_raw/__init__.py @@ -0,0 +1,63 @@ +# ##### 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 ##### + +bl_addon_info = { + 'name': 'Import/Export: Raw mesh', + 'author': 'Anthony D,Agostino (Scorpius), Aurel Wildfellner', + 'version': '0.2', + 'blender': (2, 5, 3), + 'location': 'File > Import/Export > Raw faces ', + 'description': 'Import Raw Faces (.raw format)', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/File_I-O/Raw_Mesh_IO', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=21733&group_id=153&atid=469', + 'category': 'Import/Export'} + +import bpy + + +def menu_import(self, context): + from io_mesh_raw import import_raw + self.layout.operator(import_raw.RawImporter.bl_idname, text="Raw Faces (.raw)").filepath = "*.raw" + + +def menu_export(self, context): + from io_mesh_raw import export_raw + import os + default_path = os.path.splitext(bpy.data.filepath)[0] + ".raw" + self.layout.operator(export_raw.RawExporter.bl_idname, text="Raw Faces (.raw)").filepath = default_path + + +def register(): + from io_mesh_raw import import_raw, export_raw + bpy.types.register(import_raw.RawImporter) + bpy.types.register(export_raw.RawExporter) + bpy.types.INFO_MT_file_import.append(menu_import) + bpy.types.INFO_MT_file_export.append(menu_export) + +def unregister(): + from io_mesh_raw import import_raw, export_raw + bpy.types.unregister(import_raw.RawImporter) + bpy.types.unregister(export_raw.RawExporter) + bpy.types.INFO_MT_file_import.remove(menu_import) + bpy.types.INFO_MT_file_export.remove(menu_export) + +if __name__ == "__main__": + register() diff --git a/io_mesh_raw/export_raw.py b/io_mesh_raw/export_raw.py new file mode 100644 index 00000000..59f83810 --- /dev/null +++ b/io_mesh_raw/export_raw.py @@ -0,0 +1,112 @@ +# ##### 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 ##### + +__author__ = ["Aurel Wildfellner"] +__version__ = '0.2' +__bpydoc__ = """\ +This script exports a Mesh to a RAW triangle format file. + +The raw triangle format is very simple; it has no verts or faces lists. +It's just a simple ascii text file with the vertices of each triangle +listed on each line. In addition, also quads can be exported as a line +of 12 values (this was the default before blender 2.5). Now default +settings will triangulate the mesh. + +Usage:
+ Execute this script from the "File->Export" menu. You can select +whether modifiers should be applied and if the mesh is triangulated. + +""" + +import bpy + + +def faceToTriangles(face): + triangles = [] + if (len(face) == 4): #quad + triangles.append( [ face[0], face[1], face[2] ] ) + triangles.append( [ face[2], face[3], face[0] ] ) + else: + triangles.append(face) + + return triangles + + +def faceValues(face, mesh, matrix): + fv = [] + for verti in face.verts_raw: + fv.append(matrix * mesh.verts[verti].co) + return fv + + +def faceToLine(face): + line = "" + for v in face: + line += str(v[0]) + " " + str(v[1]) + " " + str(v[2]) + " " + return line[:-1] + "\n" + + +def export_raw(filepath, applyMods, triangulate): + faces = [] + for obj in bpy.context.selected_objects: + if obj.type == 'MESH': + matrix = obj.matrix_world + + if (applyMods): + me = obj.create_mesh(True, "PREVIEW") + else: + me = obj.data + + for face in me.faces: + fv = faceValues(face, me, matrix) + if triangulate: + faces.extend(faceToTriangles(fv)) + else: + faces.append(fv) + + # write the faces to a file + file = open(filepath, "w") + for face in faces: + file.write(faceToLine(face)) + file.close() + + +from bpy.props import * + + +class RawExporter(bpy.types.Operator): + '''Save Raw triangle mesh data''' + bl_idname = "export_mesh.raw" + bl_label = "Export RAW" + + filepath = StringProperty(name="File Path", description="Filepath used for exporting the RAW file", maxlen= 1024, default= "") + check_existing = BoolProperty(name="Check Existing", description="Check and warn on overwriting existing files", default=True, options={'HIDDEN'}) + + apply_modifiers = BoolProperty(name="Apply Modifiers", description="Use transformed mesh data from each object", default=True) + triangulate = BoolProperty(name="Triangulate", description="Triangulate quads.", default=True) + + def execute(self, context): + export_raw(self.properties.filepath, self.properties.apply_modifiers, self.properties.triangulate) + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.manager + wm.add_fileselect(self) + return {'RUNNING_MODAL'} + +# package manages registering diff --git a/io_mesh_raw/import_raw.py b/io_mesh_raw/import_raw.py new file mode 100644 index 00000000..df04df9b --- /dev/null +++ b/io_mesh_raw/import_raw.py @@ -0,0 +1,139 @@ +# ##### 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 ##### + +__author__ = ["Anthony D'Agostino (Scorpius)", "Aurel Wildfellner"] +__version__ = '0.2' +__bpydoc__ = """\ +This script imports Raw Triangle File format files to Blender. + +The raw triangle format is very simple; it has no verts or faces lists. +It's just a simple ascii text file with the vertices of each triangle +listed on each line. In addition, a line with 12 values will be +imported as a quad. This may be in conflict with some other +applications, which use a raw format, but this is how it was +implemented back in blender 2.42. + +Usage:
+ Execute this script from the "File->Import" menu and choose a Raw file to +open. + +Notes:
+ Generates the standard verts and faces lists, but without duplicate +verts. Only *exact* duplicates are removed, there is no way to specify a +tolerance. +""" + + + +import bpy + +# move those to a utility modul +from import_scene_obj import unpack_face_list, unpack_list # TODO, make generic + + +def readMesh(filename, objName): + file = open(filename, "rb") + + def line_to_face(line): + # Each triplet is an xyz float + line_split = [] + try: + line_split = list(map(float, line.split())) + except: + return None + + if len(line_split) == 9: # Tri + f1, f2, f3, f4, f5, f6, f7, f8, f9 = line_split + return [(f1, f2, f3), (f4, f5, f6), (f7, f8, f9)] + elif len(line_split) == 12: # Quad + f1, f2, f3, f4, f5, f6, f7, f8, f9, A, B, C = line_split + return [(f1, f2, f3), (f4, f5, f6), (f7, f8, f9), (A, B, C)] + else: + return None + + + faces = [] + for line in file.readlines(): + face = line_to_face(line) + if face: + faces.append(face) + + file.close() + + # Generate verts and faces lists, without duplicates + verts = [] + coords = {} + index = 0 + + for f in faces: + for i, v in enumerate(f): + try: + f[i] = coords[v] + except: + f[i] = coords[v] = index + index += 1 + verts.append(v) + + mesh = bpy.data.meshes.new(objName) + mesh.add_geometry(int(len(verts)), 0, int(len(faces))) + mesh.verts.foreach_set("co", unpack_list(verts)) + mesh.faces.foreach_set("verts_raw", unpack_face_list(faces)) + + return mesh + + +def addMeshObj(mesh, objName): + scn = bpy.context.scene + + for o in scn.objects: + o.select = False + + mesh.update() + nobj = bpy.data.objects.new(objName, mesh) + scn.objects.link(nobj) + nobj.select = True + + if scn.objects.active == None or scn.objects.active.mode == 'OBJECT': + scn.objects.active = nobj + + +from bpy.props import * + +class RawImporter(bpy.types.Operator): + '''Load Raw triangle mesh data''' + bl_idname = "import_mesh.raw" + bl_label = "Import RAW" + + filepath = StringProperty(name="File Path", description="Filepath used for importing the RAW file", maxlen=1024, default="") + + def execute(self, context): + + #convert the filename to an object name + objName = bpy.utils.display_name(self.properties.filename) + + mesh = readMesh(self.properties.filepath, objName) + addMeshObj(mesh, objName) + + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.manager + wm.add_fileselect(self) + return {'RUNNING_MODAL'} + +# package manages registering diff --git a/io_mesh_stl/__init__.py b/io_mesh_stl/__init__.py new file mode 100644 index 00000000..1858c17b --- /dev/null +++ b/io_mesh_stl/__init__.py @@ -0,0 +1,152 @@ +# ##### 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 ##### + +""" +Import/Export STL files (binary or ascii) + +- Import automatically remove the doubles. +- Export can export with/without modifiers applied + +Issues: + +Import: + - Does not handle the normal of the triangles + - Does not handle endien + +Export: + - Does not do the object space transformation + - Export only one object (the selected one) +""" + +bl_addon_info = { + 'name': 'Import/Export: STL format', + 'author': 'Guillaume Bouchard (Guillaum)', + 'version': '1', + 'blender': (2, 5, 3), + 'location': 'File > Import/Export > Stl', + 'description': 'Import/Export Stl files', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/File I-O/STL', # @todo write the page + 'tracker_url': 'https://projects.blender.org/tracker/index.php?' \ + 'func=detail&aid=22837&group_id=153&atid=469', + 'category': 'Import/Export'} + +import bpy +from bpy.props import * + +from io_mesh_stl import stl_utils, blender_utils + + +class StlImporter(bpy.types.Operator): + ''' + Load STL triangle mesh data + ''' + bl_idname = "import_mesh.stl" + bl_label = "Import STL" + + filepath = StringProperty(name="File Path", + description="File path used for importing " + "the STL file", + maxlen=1024, + default="") + + def execute(self, context): + objName = bpy.utils.display_name(self.properties.filepath.split("\\")[-1].split("/")[-1]) + tris, pts = stl_utils.read_stl(self.properties.filepath) + + blender_utils.create_and_link_mesh(objName, tris, pts) + + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.manager + wm.add_fileselect(self) + + return {'RUNNING_MODAL'} + + +class StlExporter(bpy.types.Operator): + ''' + Save STL triangle mesh data from the active object + ''' + bl_idname = "export_mesh.stl" + bl_label = "Export STL" + + filepath = StringProperty(name="File Path", + description="File path used for exporting " + "the active object to STL file", + maxlen=1024, + default="") + check_existing = BoolProperty(name="Check Existing", + description="Check and warn on " + "overwriting existing files", + default=True, + options={'HIDDEN'}) + + ascii = BoolProperty(name="Ascii", + description="Save the file in ASCII file format", + default=False) + apply_modifiers = BoolProperty(name="Apply Modifiers", + description="Apply the modifiers " + "before saving", + default=True) + + def execute(self, context): + ob = context.active_object + + faces = blender_utils.faces_from_mesh(ob, + self.properties.apply_modifiers) + stl_utils.write_stl(self.properties.filepath, faces, self.properties.ascii) + + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.manager + wm.add_fileselect(self) + return {'RUNNING_MODAL'} + + +def menu_import(self, context): + self.layout.operator(StlImporter.bl_idname, + text="Stl (.stl)").filepath = "*.stl" + + +def menu_export(self, context): + import os + default_path = os.path.splitext(bpy.data.filepath)[0] + ".stl" + self.layout.operator(StlExporter.bl_idname, + text="Stl (.stl)").filepath = default_path + + +def register(): + bpy.types.register(StlImporter) + bpy.types.register(StlExporter) + bpy.types.INFO_MT_file_import.append(menu_import) + bpy.types.INFO_MT_file_export.append(menu_export) + + +def unregister(): + bpy.types.unregister(StlImporter) + bpy.types.unregister(StlExporter) + bpy.types.INFO_MT_file_import.remove(menu_import) + bpy.types.INFO_MT_file_export.remove(menu_export) + + +if __name__ == "__main__": + register() diff --git a/io_mesh_stl/blender_utils.py b/io_mesh_stl/blender_utils.py new file mode 100644 index 00000000..1bade61d --- /dev/null +++ b/io_mesh_stl/blender_utils.py @@ -0,0 +1,50 @@ +import bpy + + +def create_and_link_mesh(name, faces, points): + ''' + Create a blender mesh and object called name from a list of + *points* and *faces* and link it in the current scene. + ''' + + mesh = bpy.data.meshes.new(name) + mesh.from_pydata(points, [], faces) + + ob = bpy.data.objects.new(name, mesh) + bpy.context.scene.objects.link(ob) + + # update mesh to allow proper display + mesh.update() + + +def faces_from_mesh(ob, apply_modifier=False, triangulate=True): + ''' + From an object, return a generator over a list of faces. + + Each faces is a list of his vertexes. Each vertex is a tuple of + his coordinate. + + apply_modifier + Apply the preview modifier to the returned liste + + triangulate + Split the quad into two triangles + ''' + + # get the modifiers + mesh = ob.create_mesh(bpy.context.scene, + True, "PREVIEW") if apply_modifier else ob.data + + def iter_face_index(): + ''' + From a list of faces, return the face triangulated if needed. + ''' + for face in mesh.faces: + if triangulate and len(face.verts) == 4: + yield face.verts[:3] + yield face.verts[2:] + [face.verts[0]] + else: + yield list(face.verts) + + return ([tuple(mesh.verts[index].co) + for index in indexes] for indexes in iter_face_index()) diff --git a/io_mesh_stl/stl_utils.py b/io_mesh_stl/stl_utils.py new file mode 100644 index 00000000..34db045c --- /dev/null +++ b/io_mesh_stl/stl_utils.py @@ -0,0 +1,228 @@ +''' +Import and export STL files + +Used as a blender script, it load all the stl files in the scene: + +blender -P stl_utils.py -- file1.stl file2.stl file3.stl ... +''' + +import struct +import mmap +import contextlib +import itertools + +# TODO: endien + + +@contextlib.contextmanager +def mmap_file(filename): + ''' + Context manager over the data of an mmap'ed file (Read ONLY). + + + Example: + + with mmap_file(filename) as m: + m.read() + print m[10:50] + ''' + with open(filename, 'rb') as file: + # check http://bugs.python.org/issue8046 to have mmap context + # manager fixed in python + map = mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ) + yield map + map.close() + + +class ListDict(dict): + ''' + Set struct with order. + + You can: + - insert data into without doubles + - get the list of data in insertion order with self.list + + Like collections.OrderedDict, but quicker, can be replaced if + ODict is optimised. + ''' + + def __init__(self): + dict.__init__(self) + self.list = [] + self._len = 0 + + def add(self, item): + ''' + Add a value to the Set, return its position in it. + ''' + value = self.setdefault(item, self._len) + if value == self._len: + self.list.append(item) + self._len += 1 + + return value + + +def _binary_read(data): + # an stl binary file is + # - 80 bytes of description + # - 2 bytes of size (unsigned int) + # - size triangles : + # + # - 12 bytes of normal + # - 9 * 4 bytes of coordinate (3*3 floats) + # - 2 bytes of garbage (usually 0) + + # OFFSET for the first byte of coordinate (headers + first normal bytes) + # STRIDE between each triangle (first normal + coordinates + garbage) + OFFSET, STRIDE = 84 + 12, 12 * 4 + 2 + + # read header size, ignore description + size = struct.unpack_from('>> tris, pts = read_stl(filename, lambda x:) + >>> pts = list(pts) + >>> + >>> # print the coordinate of the triangle n + >>> print([pts[i] for i in tris[n]]) + ''' + + tris, pts = [], ListDict() + + with mmap_file(filename) as data: + # check for ascii or binary + gen = _ascii_read if data.read(5) == b'solid' else _binary_read + + for pt in gen(data): + # Add the triangle and the point. + # If the point is allready in the list of points, the + # index returned by pts.add() will be the one from the + # first equal point inserted. + tris.append([pts.add(p) for p in pt]) + + return tris, pts.list + + +if __name__ == '__main__': + import sys + import bpy + from io_mesh_stl import blender_utils + + filenames = sys.argv[sys.argv.index('--') + 1:] + + for filename in filenames: + objName = bpy.utils.display_name(filename) + tris, pts = read_stl(filename) + + blender_utils.create_and_link_mesh(objName, tris, pts) diff --git a/mesh_relax.py b/mesh_relax.py new file mode 100644 index 00000000..7d3410d7 --- /dev/null +++ b/mesh_relax.py @@ -0,0 +1,128 @@ +# mesh_relax.py Copyright (C) 2010, Fabian Fricke +# +# Relaxes selected vertices while retaining the shape as much as possible +# +# ***** 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +""" + +Usage: + +Launch from "W-menu" or from "Mesh -> Vertices -> Relax" + + +Additional links: + Author Site: http://frigi.designdevil.de + e-mail: frigi.f {at} gmail {dot} com + +""" + + +bl_addon_info = { + 'name': 'Mesh: Relax', + 'author': 'Fabian Fricke', + 'version': '1.1 2010/04/22', + 'blender': (2, 5, 3), + 'location': 'View3D > Specials > Relax ', + 'description': 'Relax the selected verts while retaining the shape', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Modeling/Relax', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=21421&group_id=153&atid=469', + 'category': 'Mesh'} + + +import bpy +from bpy.props import IntProperty + +def relax_mesh(context): + + # deselect everything that's not related + for obj in context.selected_objects: + obj.select = False + + # get active object + obj = context.active_object + + # duplicate the object so it can be used for the shrinkwrap modifier + obj.select = True # make sure the object is selected! + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.duplicate() + target = context.active_object + + # remove all other modifiers from the target + for m in range(0, len(target.modifiers)): + target.modifiers.remove(target.modifiers[0]) + + context.scene.objects.active = obj + + sw = obj.modifiers.new(type='SHRINKWRAP', name='relax_target') + sw.target = target + + # run smooth operator to relax the mesh + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.vertices_smooth() + bpy.ops.object.mode_set(mode='OBJECT') + + # apply the modifier + bpy.ops.object.modifier_apply(modifier='relax_target') + + # delete the target object + obj.select = False + target.select = True + bpy.ops.object.delete() + + # go back to initial state + obj.select = True + bpy.ops.object.mode_set(mode='EDIT') + +class Relax(bpy.types.Operator): + '''Relaxes selected vertices while retaining the shape as much as possible''' + bl_idname = 'mesh.relax' + bl_label = 'Relax' + bl_options = {'REGISTER', 'UNDO'} + + iterations = IntProperty(name="Relax iterations", + default=1, min=0, max=100, soft_min=0, soft_max=10) + + def poll(self, context): + obj = context.active_object + return (obj and obj.type == 'MESH') + + def execute(self, context): + for i in range(0,self.properties.iterations): + relax_mesh(context) + return {'FINISHED'} + +menu_func = (lambda self, context: self.layout.operator(Relax.bl_idname, text="Relax")) + +def register(): + bpy.types.register(Relax) + bpy.types.VIEW3D_MT_edit_mesh_specials.append(menu_func) + bpy.types.VIEW3D_MT_edit_mesh_vertices.append(menu_func) + +def unregister(): + bpy.types.unregister(Relax) + bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func) + bpy.types.VIEW3D_MT_edit_mesh_vertices.remove(menu_func) + +if __name__ == "__main__": + register() diff --git a/mesh_surface_sketch.py b/mesh_surface_sketch.py new file mode 100644 index 00000000..37bc2bc7 --- /dev/null +++ b/mesh_surface_sketch.py @@ -0,0 +1,827 @@ +# ##### 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ##### END GPL LICENSE BLOCK ##### + + +bl_addon_info = { + 'name': 'Mesh: Surface Sketch', + 'author': 'Eclectiel', + 'version': '0.8 Beta', + 'blender': (2, 5, 3), + 'location': 'View3D > EditMode > ToolShelf', + 'description': 'Draw meshes and re-topologies with Grease Pencil', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Mesh/Surface_Sketch', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=22062&group_id=153&atid=469', + 'category': 'Mesh'} + + +import bpy +import math + +from math import * + + +bpy.types.Scene.IntProperty(attr = "SURFSK_edges_U", name = "Cross", description = "Number of edge rings crossing the strokes (perpendicular to strokes direction)", default = 10, min = 0, max = 100000) +bpy.types.Scene.IntProperty(attr = "SURFSK_edges_V", name = "Follow", description = "Number of edge rings following the strokes (parallel to strokes direction)", default = 10, min = 0, max = 100000) +bpy.types.Scene.IntProperty(attr = "SURFSK_precision", name = "Precision", description = "Precision level of the surface calculation", default = 4, min = 0, max = 100000) +bpy.types.Scene.BoolProperty(attr = "SURFSK_keep_strokes", name = "Keep strokes", description = "Keeps the sketched strokes after adding the surface", default = False) + + + + +class View3DPanel(bpy.types.Panel): + bl_space_type = 'VIEW_3D' + bl_region_type = 'TOOLS' + + +class VIEW3D_PT_tools_SURF_SKETCH(View3DPanel): + bl_context = "mesh_edit" + bl_label = "Surface Sketching" + + def poll(self, context): + return context.active_object + + def draw(self, context): + layout = self.layout + + scn = context.scene + ob = context.object + + col = layout.column(align=True) + row = layout.row() + row.separator() + col.operator("GPENCIL_OT_SURFSK_add_surface", text="Add Surface") + col.prop(scn, "SURFSK_edges_U") + col.prop(scn, "SURFSK_edges_V") + row.separator() + col.prop(scn, "SURFSK_keep_strokes") + col.separator() + row.separator() + col.operator("GPENCIL_OT_SURFSK_strokes_to_curves", text="Strokes to curves") + + + +class GPENCIL_OT_SURFSK_add_surface(bpy.types.Operator): + bl_idname = "GPENCIL_OT_SURFSK_add_surface" + bl_label = "Surface generation from grease pencil strokes" + bl_description = "Surface generation from grease pencil strokes" + + + #### Get an ordered list of a chain of vertices. + def get_ordered_verts(self, ob, all_selected_edges_idx, all_selected_verts_idx, first_vert_idx, middle_vertex_idx): + # Order selected vertexes. + verts_ordered = [] + verts_ordered.append(self.main_object.data.verts[first_vert_idx]) + prev_v = first_vert_idx + prev_ed = None + finish_while = False + while True: + edges_non_matched = 0 + for i in all_selected_edges_idx: + if ob.data.edges[i] != prev_ed and ob.data.edges[i].verts[0] == prev_v and ob.data.edges[i].verts[1] in all_selected_verts_idx: + verts_ordered.append(self.main_object.data.verts[ob.data.edges[i].verts[1]]) + prev_v = ob.data.edges[i].verts[1] + prev_ed = ob.data.edges[i] + elif ob.data.edges[i] != prev_ed and ob.data.edges[i].verts[1] == prev_v and ob.data.edges[i].verts[0] in all_selected_verts_idx: + verts_ordered.append(self.main_object.data.verts[ob.data.edges[i].verts[0]]) + prev_v = ob.data.edges[i].verts[0] + prev_ed = ob.data.edges[i] + else: + edges_non_matched += 1 + + if edges_non_matched == len(all_selected_edges_idx): + finish_while = True + + if finish_while: + break + + if middle_vertex_idx != None: + verts_ordered.append(self.main_object.data.verts[middle_vertex_idx]) + verts_ordered.reverse() + + return verts_ordered + + + #### Calculates length of a chain of points. + def get_chain_length(self, verts_ordered): + edges_lengths = [] + edges_lengths_sum = 0 + for i in range(0, len(verts_ordered)): + if i == 0: + prev_v = verts_ordered[i] + else: + v = verts_ordered[i] + + v_difs = [prev_v.co[0] - v.co[0], prev_v.co[1] - v.co[1], prev_v.co[2] - v.co[2]] + edge_length = abs(sqrt(v_difs[0] * v_difs[0] + v_difs[1] * v_difs[1] + v_difs[2] * v_difs[2])) + + edges_lengths.append(edge_length) + edges_lengths_sum += edge_length + + prev_v = v + + return edges_lengths, edges_lengths_sum + + + #### Calculates the proportion of the edges of a chain of edges, relative to the full chain length. + def get_edges_proportions(self, edges_lengths, edges_lengths_sum, use_boundaries, fixed_edges_num): + edges_proportions = [] + if use_boundaries: + verts_count = 1 + for l in edges_lengths: + edges_proportions.append(l / edges_lengths_sum) + verts_count += 1 + else: + verts_count = 1 + for n in range(0, fixed_edges_num): + edges_proportions.append(1 / fixed_edges_num) + verts_count += 1 + + return edges_proportions + + + #### Calculates the angle between two pairs of points in space. + def orientation_difference(self, points_A_co, points_B_co): # each parameter should be a list with two elements, and each element should be a x,y,z coordinate. + vec_A = points_A_co[0] - points_A_co[1] + vec_B = points_B_co[0] - points_B_co[1] + + angle = vec_A.angle(vec_B) + + if angle > 0.5 * math.pi: + angle = abs(angle - math.pi) + + return angle + + + #### Calculate distance between two points + def pts_distance(self, p1_co, p2_co): + p_difs = [p1_co[0] - p2_co[0], p1_co[1] - p2_co[1], p1_co[2] - p2_co[2]] + distance = abs(sqrt(p_difs[0] * p_difs[0] + p_difs[1] * p_difs[1] + p_difs[2] * p_difs[2])) + + return distance + + + def execute(self, context): + #### Selected edges. + all_selected_edges_idx = [] + all_selected_verts = [] + all_verts_idx = [] + for ed in self.main_object.data.edges: + if ed.select: + all_selected_edges_idx.append(ed.index) + + # Selected vertexes. + if not ed.verts[0] in all_selected_verts: + all_selected_verts.append(self.main_object.data.verts[ed.verts[0]]) + if not ed.verts[1] in all_selected_verts: + all_selected_verts.append(self.main_object.data.verts[ed.verts[1]]) + + # All verts (both from each edge) to determine later which are at the tips (those not repeated twice). + all_verts_idx.append(ed.verts[0]) + all_verts_idx.append(ed.verts[1]) + + + #### Identify the tips and "middle-vertex" that separates U from V, if there is one. + all_chains_tips_idx = [] + for v_idx in all_verts_idx: + if all_verts_idx.count(v_idx) < 2: + all_chains_tips_idx.append(v_idx) + + edges_connected_to_tips = [] + for ed in self.main_object.data.edges: + if (ed.verts[0] in all_chains_tips_idx or ed.verts[1] in all_chains_tips_idx) and not (ed.verts[0] in all_verts_idx and ed.verts[1] in all_verts_idx): + edges_connected_to_tips.append(ed) + + middle_vertex_idx = None + tips_to_discard_idx = [] + for ed_tips in edges_connected_to_tips: + for ed_tips_b in edges_connected_to_tips: + if (ed_tips != ed_tips_b): + if ed_tips.verts[0] in all_verts_idx and (((ed_tips.verts[1] == ed_tips_b.verts[0]) or ed_tips.verts[1] == ed_tips_b.verts[1])): + middle_vertex_idx = ed_tips.verts[1] + tips_to_discard_idx.append(ed_tips.verts[0]) + elif ed_tips.verts[1] in all_verts_idx and (((ed_tips.verts[0] == ed_tips_b.verts[0]) or ed_tips.verts[0] == ed_tips_b.verts[1])): + middle_vertex_idx = ed_tips.verts[0] + tips_to_discard_idx.append(ed_tips.verts[1]) + + + #### List with pairs of verts that belong to the tips of each selection chain (row). + verts_tips_same_chain_idx = [] + if len(all_chains_tips_idx) >= 2: + checked_v = [] + for i in range(0, len(all_chains_tips_idx)): + if all_chains_tips_idx[i] not in checked_v: + v_chain = self.get_ordered_verts(self.main_object, all_selected_edges_idx, all_verts_idx, all_chains_tips_idx[i], middle_vertex_idx) + + verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index]) + + checked_v.append(v_chain[0].index) + checked_v.append(v_chain[len(v_chain) - 1].index) + + + #### Selection tips (vertices) + verts_tips_parsed_idx = [] + if len(all_chains_tips_idx) >= 2: + for spec_v_idx in all_chains_tips_idx: + if (spec_v_idx not in tips_to_discard_idx): + verts_tips_parsed_idx.append(spec_v_idx) + + + #### Identify the type of selection made by the user. + if middle_vertex_idx != None: + if len(all_chains_tips_idx) == 4: # If there are 4 tips (two selection chains) + selection_type = "TWO_CONNECTED" + else: + # The type of the selection was not identified, so the script stops. + return + else: + if len(all_chains_tips_idx) == 2: # If there are 2 tips (one selection chain) + selection_type = "SINGLE" + elif len(all_chains_tips_idx) == 4: # If there are 4 tips (two selection chains) + selection_type = "TWO_NOT_CONNECTED" + elif len(all_chains_tips_idx) == 0: + selection_type = "NO_SELECTION" + else: + # The type of the selection was not identified, so the script stops. + return + + + #### Check if it will be used grease pencil strokes or curves. + selected_objs = bpy.context.selected_objects + if len(selected_objs) > 1: + for ob in selected_objs: + if ob != bpy.context.scene.objects.active: + ob_gp_strokes = ob + using_external_curves = True + + bpy.ops.object.editmode_toggle() + else: + #### Convert grease pencil strokes to curve. + bpy.ops.gpencil.convert(type='CURVE') + ob_gp_strokes = bpy.context.object + using_external_curves = False + + bpy.ops.object.editmode_toggle() + + ob_gp_strokes.name = "SURFSK_temp_strokes" + + bpy.ops.object.select_name(name = ob_gp_strokes.name) + bpy.context.scene.objects.active = bpy.context.scene.objects[ob_gp_strokes.name] + + + #### If "Keep strokes" is active make a duplicate of the original strokes, which will be intact + if bpy.context.scene.SURFSK_keep_strokes: + bpy.ops.object.duplicate_move() + bpy.context.object.name = "SURFSK_used_strokes" + bpy.ops.object.editmode_toggle() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.object.editmode_toggle() + + bpy.ops.object.select_name(name = ob_gp_strokes.name) + bpy.context.scene.objects.active = bpy.context.scene.objects[ob_gp_strokes.name] + + + #### Enter editmode for the new curve (converted from grease pencil strokes). + bpy.ops.object.editmode_toggle() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.object.editmode_toggle() + + + selection_U_exists = False + selection_U2_exists = False + selection_V_exists = False + selection_V2_exists = False + #### Define what vertexes are at the tips of each selection and are not the middle-vertex. + if selection_type == "TWO_CONNECTED": + selection_U_exists = True + selection_V_exists = True + + # Determine which selection is Selection-U and which is Selection-V. + points_A = [] + points_B = [] + points_first_stroke_tips = [] + + points_A.append(self.main_object.data.verts[verts_tips_parsed_idx[0]].co) + points_A.append(self.main_object.data.verts[middle_vertex_idx].co) + + points_B.append(self.main_object.data.verts[verts_tips_parsed_idx[1]].co) + points_B.append(self.main_object.data.verts[middle_vertex_idx].co) + + points_first_stroke_tips.append(ob_gp_strokes.data.splines[0].bezier_points[0].co) + points_first_stroke_tips.append(ob_gp_strokes.data.splines[0].bezier_points[len(ob_gp_strokes.data.splines[0].bezier_points) - 1].co) + + angle_A = self.orientation_difference(points_A, points_first_stroke_tips) + angle_B = self.orientation_difference(points_B, points_first_stroke_tips) + + if angle_A < angle_B: + first_vert_U_idx = verts_tips_parsed_idx[0] + first_vert_V_idx = verts_tips_parsed_idx[1] + else: + first_vert_U_idx = verts_tips_parsed_idx[1] + first_vert_V_idx = verts_tips_parsed_idx[0] + + elif selection_type == "SINGLE" or selection_type == "TWO_NOT_CONNECTED": + first_sketched_point_first_stroke_co = ob_gp_strokes.data.splines[0].bezier_points[0].co + last_sketched_point_first_stroke_co = ob_gp_strokes.data.splines[0].bezier_points[len(ob_gp_strokes.data.splines[0].bezier_points) - 1].co + + first_sketched_point_last_stroke_co = ob_gp_strokes.data.splines[len(ob_gp_strokes.data.splines) - 1].bezier_points[0].co + + # The tip of the selected vertices nearest to the first point of the first sketched stroke. + prev_dist = 999999999999 + for i in range(0, len(verts_tips_same_chain_idx)): + for v_idx in range(0, len(verts_tips_same_chain_idx[i])): + dist = self.pts_distance(first_sketched_point_first_stroke_co, self.main_object.data.verts[verts_tips_same_chain_idx[i][v_idx]].co) + if dist < prev_dist: + prev_dist = dist + + nearest_tip_first_st_first_pt_idx = i + + nearest_tip_first_pair_first_pt_idx = v_idx + + # Shortest distance to the first point of the first stroke + shortest_distance_to_first_stroke = dist + + + # The tip of the selected vertices nearest to the last point of the first sketched stroke. + prev_dist = 999999999999 + for i in range(0, len(verts_tips_same_chain_idx)): + for v_idx in range(0, len(verts_tips_same_chain_idx[i])): + dist = self.pts_distance(last_sketched_point_first_stroke_co, self.main_object.data.verts[verts_tips_same_chain_idx[i][v_idx]].co) + if dist < prev_dist: + prev_dist = dist + + nearest_tip_first_st_last_pt_pair_idx = i + nearest_tip_first_st_last_pt_point_idx = v_idx + + + # The tip of the selected vertices nearest to the first point of the last sketched stroke. + prev_dist = 999999999999 + for i in range(0, len(verts_tips_same_chain_idx)): + for v_idx in range(0, len(verts_tips_same_chain_idx[i])): + dist = self.pts_distance(first_sketched_point_last_stroke_co, self.main_object.data.verts[verts_tips_same_chain_idx[i][v_idx]].co) + if dist < prev_dist: + prev_dist = dist + + nearest_tip_last_st_first_pt_pair_idx = i + nearest_tip_last_st_first_pt_point_idx = v_idx + + + points_tips = [] + points_first_stroke_tips = [] + + # Determine if the single selection will be treated as U or as V. + edges_sum = 0 + for i in all_selected_edges_idx: + edges_sum += self.pts_distance(self.main_object.data.verts[self.main_object.data.edges[i].verts[0]].co, self.main_object.data.verts[self.main_object.data.edges[i].verts[1]].co) + + average_edge_length = edges_sum / len(all_selected_edges_idx) + + + + # If the beginning of the first stroke is near enough to interpret things as an "extrude along strokes" instead of "extrude through strokes" + if shortest_distance_to_first_stroke < average_edge_length / 3: + selection_U_exists = False + selection_V_exists = True + + first_vert_V_idx = verts_tips_same_chain_idx[nearest_tip_first_st_first_pt_idx][nearest_tip_first_pair_first_pt_idx] + + if selection_type == "TWO_NOT_CONNECTED": + selection_V2_exists = True + + first_vert_V2_idx = verts_tips_same_chain_idx[nearest_tip_first_st_last_pt_pair_idx][nearest_tip_first_st_last_pt_point_idx] + + else: + selection_V2_exists = False + + else: + selection_U_exists = True + selection_V_exists = False + + points_tips.append(self.main_object.data.verts[verts_tips_same_chain_idx[nearest_tip_first_st_first_pt_idx][0]].co) + points_tips.append(self.main_object.data.verts[verts_tips_same_chain_idx[nearest_tip_first_st_first_pt_idx][1]].co) + + points_first_stroke_tips.append(ob_gp_strokes.data.splines[0].bezier_points[0].co) + points_first_stroke_tips.append(ob_gp_strokes.data.splines[0].bezier_points[len(ob_gp_strokes.data.splines[0].bezier_points) - 1].co) + + vec_A = points_tips[0] - points_tips[1] + vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1] + + # Compare the direction of the selection and the first grease pencil stroke to determine which is the "first" vertex of the selection. + if vec_A.dot(vec_B) < 0: + first_vert_U_idx = verts_tips_same_chain_idx[nearest_tip_first_st_first_pt_idx][1] + else: + first_vert_U_idx = verts_tips_same_chain_idx[nearest_tip_first_st_first_pt_idx][0] + + if selection_type == "TWO_NOT_CONNECTED": + selection_U2_exists = True + + first_vert_U2_idx = verts_tips_same_chain_idx[nearest_tip_last_st_first_pt_pair_idx][nearest_tip_last_st_first_pt_point_idx] + else: + selection_U2_exists = False + + elif selection_type == "NO_SELECTION": + selection_U_exists = False + selection_V_exists = False + + + #### Get an ordered list of the vertices of Selection-U. + if selection_U_exists: + verts_ordered_U = self.get_ordered_verts(self.main_object, all_selected_edges_idx, all_verts_idx, first_vert_U_idx, middle_vertex_idx) + + #### Get an ordered list of the vertices of Selection-U. + if selection_U2_exists: + verts_ordered_U2 = self.get_ordered_verts(self.main_object, all_selected_edges_idx, all_verts_idx, first_vert_U2_idx, middle_vertex_idx) + + #### Get an ordered list of the vertices of Selection-V. + if selection_V_exists: + verts_ordered_V = self.get_ordered_verts(self.main_object, all_selected_edges_idx, all_verts_idx, first_vert_V_idx, middle_vertex_idx) + + #### Get an ordered list of the vertices of Selection-U. + if selection_V2_exists: + verts_ordered_V2 = self.get_ordered_verts(self.main_object, all_selected_edges_idx, all_verts_idx, first_vert_V2_idx, middle_vertex_idx) + + + #### Calculate edges U proportions. + + # Sum selected edges U lengths. + edges_lengths_U = [] + edges_lengths_sum_U = 0 + + if selection_U_exists: + edges_lengths_U, edges_lengths_sum_U = self.get_chain_length(verts_ordered_U) + + # Sum selected edges V lengths. + edges_lengths_V = [] + edges_lengths_sum_V = 0 + + if selection_V_exists: + edges_lengths_V, edges_lengths_sum_V = self.get_chain_length(verts_ordered_V) + + bpy.ops.object.editmode_toggle() + for i in range(0, int(bpy.context.scene.SURFSK_precision)): + bpy.ops.curve.subdivide() + bpy.ops.object.editmode_toggle() + + # Proportions U. + edges_proportions_U = [] + edges_proportions_U = self.get_edges_proportions(edges_lengths_U, edges_lengths_sum_U, selection_U_exists, bpy.context.scene.SURFSK_edges_U) + verts_count_U = len(edges_proportions_U) + 1 + + # Proportions V. + edges_proportions_V = [] + edges_proportions_V = self.get_edges_proportions(edges_lengths_V, edges_lengths_sum_V, selection_V_exists, bpy.context.scene.SURFSK_edges_V) + verts_count_V = len(edges_proportions_V) + 1 + + + + #### Get ordered lists of points on each sketched curve that mimics the proportions of the edges in the vertex selection. + sketched_splines = ob_gp_strokes.data.splines + sketched_splines_lengths = [] + sketched_splines_parsed = [] + for sp_idx in range(0, len(sketched_splines)): + # Calculate spline length + sketched_splines_lengths.append(0) + for i in range(0, len(sketched_splines[sp_idx].bezier_points)): + if i == 0: + prev_p = sketched_splines[sp_idx].bezier_points[i] + else: + p = sketched_splines[sp_idx].bezier_points[i] + + p_difs = [prev_p.co[0] - p.co[0], prev_p.co[1] - p.co[1], prev_p.co[2] - p.co[2]] + edge_length = abs(sqrt(p_difs[0] * p_difs[0] + p_difs[1] * p_difs[1] + p_difs[2] * p_difs[2])) + + sketched_splines_lengths[sp_idx] += edge_length + + prev_p = p + + # Calculate vertex positions with apropriate edge proportions, and ordered, for each spline. + sketched_splines_parsed.append([]) + partial_spline_length = 0 + related_edge_U = 0 + edges_proportions_sum_U = 0 + edges_lengths_sum_U = 0 + for i in range(0, len(sketched_splines[sp_idx].bezier_points)): + if i == 0: + prev_p = sketched_splines[sp_idx].bezier_points[i] + sketched_splines_parsed[sp_idx].append(prev_p.co) + elif i != len(sketched_splines[sp_idx].bezier_points) - 1: + p = sketched_splines[sp_idx].bezier_points[i] + + p_difs = [prev_p.co[0] - p.co[0], prev_p.co[1] - p.co[1], prev_p.co[2] - p.co[2]] + edge_length = abs(sqrt(p_difs[0] * p_difs[0] + p_difs[1] * p_difs[1] + p_difs[2] * p_difs[2])) + + + if edges_proportions_sum_U + edges_proportions_U[related_edge_U] - ((edges_lengths_sum_U + partial_spline_length + edge_length) / sketched_splines_lengths[sp_idx]) > 0: # comparing proportions to see if the proportion in the selection is found in the spline. + partial_spline_length += edge_length + elif related_edge_U < len(edges_proportions_U) - 1: + sketched_splines_parsed[sp_idx].append(prev_p.co) + + edges_proportions_sum_U += edges_proportions_U[related_edge_U] + related_edge_U += 1 + + edges_lengths_sum_U += partial_spline_length + partial_spline_length = edge_length + + prev_p = p + else: # last point of the spline for the last edge + p = sketched_splines[sp_idx].bezier_points[len(sketched_splines[sp_idx].bezier_points) - 1] + sketched_splines_parsed[sp_idx].append(p.co) + + + #### If the selection type is "TWO_NOT_CONNECTED" replace the last point of each spline with the points in the "target" selection. + if selection_type == "TWO_NOT_CONNECTED": + if selection_U2_exists: + for i in range(0, len(sketched_splines_parsed[len(sketched_splines_parsed) - 1])): + sketched_splines_parsed[len(sketched_splines_parsed) - 1][i] = verts_ordered_U2[i].co + + + #### Create temporary curves along the "control-points" found on the sketched curves and the mesh selection. + mesh_ctrl_pts_name = "SURFSK_ctrl_pts" + me = bpy.data.meshes.new(mesh_ctrl_pts_name) + ob_ctrl_pts = bpy.data.objects.new(mesh_ctrl_pts_name, me) + ob_ctrl_pts.data = me + bpy.context.scene.objects.link(ob_ctrl_pts) + + + for i in range(0, verts_count_U): + vert_num_in_spline = 1 + + if selection_U_exists: + ob_ctrl_pts.data.add_geometry(1,0,0) + last_v = ob_ctrl_pts.data.verts[len(ob_ctrl_pts.data.verts) - 1] + last_v.co = verts_ordered_U[i].co + + vert_num_in_spline += 1 + + for sp in sketched_splines_parsed: + ob_ctrl_pts.data.add_geometry(1,0,0) + v = ob_ctrl_pts.data.verts[len(ob_ctrl_pts.data.verts) - 1] + v.co = sp[i] + + if vert_num_in_spline > 1: + ob_ctrl_pts.data.add_geometry(0,1,0) + ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].verts[0] = len(ob_ctrl_pts.data.verts) - 2 + ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].verts[1] = len(ob_ctrl_pts.data.verts) - 1 + + last_v = v + + vert_num_in_spline += 1 + + bpy.ops.object.select_name(name = ob_ctrl_pts.name) + bpy.context.scene.objects.active = bpy.data.objects[ob_ctrl_pts.name] + + + # Create curves from control points. + bpy.ops.object.convert(target='CURVE', keep_original=False) + ob_curves_surf = bpy.context.scene.objects.active + bpy.ops.object.editmode_toggle() + bpy.ops.curve.spline_type_set(type='BEZIER') + bpy.ops.curve.handle_type_set(type='AUTOMATIC') + for i in range(0, int(bpy.context.scene.SURFSK_precision)): + bpy.ops.curve.subdivide() + bpy.ops.object.editmode_toggle() + + + # Calculate the length of each final surface spline. + surface_splines = ob_curves_surf.data.splines + surface_splines_lengths = [] + surface_splines_parsed = [] + for sp_idx in range(0, len(surface_splines)): + # Calculate spline length + surface_splines_lengths.append(0) + for i in range(0, len(surface_splines[sp_idx].bezier_points)): + if i == 0: + prev_p = surface_splines[sp_idx].bezier_points[i] + else: + p = surface_splines[sp_idx].bezier_points[i] + + edge_length = self.pts_distance(prev_p.co, p.co) + + surface_splines_lengths[sp_idx] += edge_length + + prev_p = p + + bpy.ops.object.editmode_toggle() + for i in range(0, int(bpy.context.scene.SURFSK_precision)): + bpy.ops.curve.subdivide() + bpy.ops.object.editmode_toggle() + + for sp_idx in range(0, len(surface_splines)): + # Calculate vertex positions with apropriate edge proportions, and ordered, for each spline. + surface_splines_parsed.append([]) + partial_spline_length = 0 + related_edge_V = 0 + edges_proportions_sum_V = 0 + edges_lengths_sum_V = 0 + for i in range(0, len(surface_splines[sp_idx].bezier_points)): + if i == 0: + prev_p = surface_splines[sp_idx].bezier_points[i] + surface_splines_parsed[sp_idx].append(prev_p.co) + elif i != len(surface_splines[sp_idx].bezier_points) - 1: + p = surface_splines[sp_idx].bezier_points[i] + + edge_length = self.pts_distance(prev_p.co, p.co) + + if edges_proportions_sum_V + edges_proportions_V[related_edge_V] - ((edges_lengths_sum_V + partial_spline_length + edge_length) / surface_splines_lengths[sp_idx]) > 0: # comparing proportions to see if the proportion in the selection is found in the spline. + partial_spline_length += edge_length + elif related_edge_V < len(edges_proportions_V) - 1: + surface_splines_parsed[sp_idx].append(prev_p.co) + + edges_proportions_sum_V += edges_proportions_V[related_edge_V] + related_edge_V += 1 + + edges_lengths_sum_V += partial_spline_length + partial_spline_length = edge_length + + prev_p = p + else: # last point of the spline for the last edge + p = surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1] + surface_splines_parsed[sp_idx].append(p.co) + + # Set the first and last verts of each spline to the locations of the respective verts in the selections. + if selection_V_exists: + for i in range(0, len(surface_splines_parsed[0])): + surface_splines_parsed[len(surface_splines_parsed) - 1][i] = verts_ordered_V[i].co + + if selection_type == "TWO_NOT_CONNECTED": + if selection_V2_exists: + for i in range(0, len(surface_splines_parsed[0])): + surface_splines_parsed[0][i] = verts_ordered_V2[i].co + + + #### Delete object with control points and object from grease pencil convertion. + bpy.ops.object.select_name(name = ob_ctrl_pts.name) + bpy.context.scene.objects.active = bpy.data.objects[ob_ctrl_pts.name] + bpy.ops.object.delete() + + bpy.ops.object.select_name(name = ob_gp_strokes.name) + bpy.context.scene.objects.active = bpy.data.objects[ob_gp_strokes.name] + bpy.ops.object.delete() + + + + #### Generate surface. + + # Get all verts coords. + all_surface_verts_co = [] + for i in range(0, len(surface_splines_parsed)): + # Get coords of all verts and make a list with them + for pt_co in surface_splines_parsed[i]: + all_surface_verts_co.append(pt_co) + + + # Define verts for each face. + all_surface_faces = [] + for i in range(0, len(all_surface_verts_co) - len(surface_splines_parsed[0])): + if ((i + 1) / len(surface_splines_parsed[0]) != int((i + 1) / len(surface_splines_parsed[0]))): + all_surface_faces.append([i+1, i , i + len(surface_splines_parsed[0]), i + len(surface_splines_parsed[0]) + 1]) + + + # Build the mesh. + surf_me_name = "SURFSK_surface" + me_surf = bpy.data.meshes.new(surf_me_name) + + me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces) + + me_surf.update() + + ob_surface = bpy.data.objects.new(surf_me_name, me_surf) + bpy.context.scene.objects.link(ob_surface) + + + #### Join the new mesh to the main object. + ob_surface.select = True + self.main_object.select = True + bpy.context.scene.objects.active = bpy.data.objects[self.main_object.name] + bpy.ops.object.join() + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.remove_doubles(limit=0.0001) + bpy.ops.mesh.normals_make_consistent(inside=False) + bpy.ops.mesh.select_all(action='DESELECT') + + #### Delete grease pencil strokes + bpy.ops.gpencil.active_frame_delete() + + + def invoke (self, context, event): + bpy.ops.object.editmode_toggle() + bpy.ops.object.editmode_toggle() + self.main_object = bpy.context.scene.objects.active + + self.execute(context) + + return {"FINISHED"} + + + + +class GPENCIL_OT_SURFSK_strokes_to_curves(bpy.types.Operator): + bl_idname = "GPENCIL_OT_SURFSK_strokes_to_curves" + bl_label = "Convert grease pencil strokes into curves and enter edit mode" + bl_description = "Convert grease pencil strokes into curves and enter edit mode" + + + def execute(self, context): + #### Convert grease pencil strokes to curve. + bpy.ops.gpencil.convert(type='CURVE') + ob_gp_strokes = bpy.context.object + ob_gp_strokes.name = "SURFSK_strokes" + + #### Delete grease pencil strokes. + bpy.ops.object.select_name(name = self.main_object.name) + bpy.context.scene.objects.active = bpy.data.objects[self.main_object.name] + bpy.ops.gpencil.active_frame_delete() + + + bpy.ops.object.select_name(name = ob_gp_strokes.name) + bpy.context.scene.objects.active = bpy.data.objects[ob_gp_strokes.name] + + + bpy.ops.object.editmode_toggle() + bpy.ops.object.editmode_toggle() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + bpy.ops.curve.smooth() + + curve_crv = ob_gp_strokes.data + bpy.ops.curve.spline_type_set(type="BEZIER") + bpy.ops.curve.handle_type_set(type="AUTOMATIC") + bpy.data.curves[curve_crv.name].draw_handles = False + bpy.data.curves[curve_crv.name].draw_normals = False + + + def invoke (self, context, event): + self.main_object = bpy.context.object + + + self.execute(context) + + return {"FINISHED"} + + + + + +def register(): + bpy.types.register(GPENCIL_OT_SURFSK_add_surface) + bpy.types.register(GPENCIL_OT_SURFSK_strokes_to_curves) + bpy.types.register(VIEW3D_PT_tools_SURF_SKETCH) + + keymap_item_add_surf = bpy.data.window_managers[0].active_keyconfig.keymaps["3D View"].items.add("GPENCIL_OT_SURFSK_add_surface","E","PRESS", key_modifier="D") + keymap_item_stroke_to_curve = bpy.data.window_managers[0].active_keyconfig.keymaps["3D View"].items.add("GPENCIL_OT_SURFSK_strokes_to_curves","C","PRESS", key_modifier="D") + +def unregister(): + bpy.types.unregister(GPENCIL_OT_SURFSK_add_surface) + bpy.types.unregister(GPENCIL_OT_SURFSK_strokes_to_curves) + bpy.types.unregister(VIEW3D_PT_tools_SURF_SKETCH) + km = bpy.data.window_managers[0].active_keyconfig.keymaps["3D View"] + for kmi in km.items: + if kmi.idname == 'wm.call_menu': + if kmi.properties.name == "GPENCIL_OT_SURFSK_add_surface": + km.remove_item(kmi) + elif kmi.properties.name == "GPENCIL_OT_SURFSK_strokes_to_curves": + km.remove_item(kmi) + else: + continue + + +if __name__ == "__main__": + register() + diff --git a/object_add_chain.py b/object_add_chain.py new file mode 100644 index 00000000..40782725 --- /dev/null +++ b/object_add_chain.py @@ -0,0 +1,156 @@ +# ##### 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 ##### + + +bl_addon_info = { + 'name': 'Object: Add Chain', + 'author': 'Brian Hinton (Nichod)', + 'version': '0.1', + 'blender': (2, 5, 3), + 'location': 'View3D > Add > Mesh > Chain', + 'description': 'Adds Chain with curve guide for easy creation', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Object/Add_Chain', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=22203&group_id=153&atid=469', + 'category': 'Object'} + + +import bpy + +def Add_Chain(): + + + ##Adds Empty to scene + bpy.ops.object.add(type='EMPTY', view_align=False, enter_editmode=False, location=(0, 0, 0), +rotation=(0, 0, 0), layer=(True, False, False, False, False, False, +False, False, False, False, False, False, False, False, False, False, False, False, False, +False)) + + ##Changes name of Empty to rot_link adds variable emp + emp = bpy.context.object + emp.name = "rot_link" + + ##Rotate emp ~ 90 degrees + emp.rotation_euler = [1.570796, 0, 0] + + ##Adds Curve Path to scene + bpy.ops.curve.primitive_nurbs_path_add( view_align=False, enter_editmode=False, location=(0, 0, 0), +rotation=(0, 0, 0), layer=(True, False, False, False, False, False, +False, False, False, False, False, False, False, False, False, False, False, False, False, +False)) + + ##Change Curve name to deform adds variable curv + curv = bpy.context.object + curv.name = "deform" + + ##Inserts Torus primitive + bpy.ops.mesh.primitive_torus_add(major_radius=1, minor_radius=0.25, +major_segments=12, minor_segments=4, use_abso=False, abso_major_rad=1, +abso_minor_rad=0.5) + + ##Positions Torus primitive to center of scene + bpy.context.active_object.location = [0, 0, 0] + + ##Changes Torus name to chain adds variable tor + tor = bpy.context.object + tor.name = "chain" + + ##Adds Array Modifier to tor + bpy.ops.object.modifier_add(type='ARRAY') + + ##Adds subsurf modifier tor + bpy.ops.object.modifier_add(type='SUBSURF') + + ##Smooths tor + bpy.ops.object.shade_smooth() + + ##Select curv + sce = bpy.context.scene + sce.objects.active = curv + + ##Toggle into editmode + bpy.ops.object.editmode_toggle() + + ##Translate curve object + bpy.ops.transform.translate(value=(2, 0, 0), constraint_axis=(True, False, False), +constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', +proportional_editing_falloff='SMOOTH', proportional_size=1, snap=False, +snap_target='CLOSEST', snap_point=(0, 0, 0), snap_align=False, snap_normal=(0, 0, 0), +release_confirm=False) + + ##Toggle into objectmode + bpy.ops.object.editmode_toggle() + + ##Select tor or chain + sce.objects.active = tor + + ##Selects Array Modifier for editing + array = tor.modifiers['Array'] + + ##Change Array Modifier Parameters + array.fit_type = ('FIT_CURVE') + array.curve = curv + array.offset_object = emp + array.add_offset_object = True + array.relative_offset_displacement = [ 0.549, 0, 0 ] + + ##Add curve modifier + bpy.ops.object.modifier_add(type='CURVE') + + ##Selects Curve Modifier for editing + cur = tor.modifiers['Curve'] + + ##Change Curve Modifier Parameters + cur.object = curv + +#makes AddChain an operator +class AddChain(bpy.types.Operator): + '''Add a Chain.''' + bl_idname = "mesh.primitive_chain_add" + bl_label = "Add Chain" + bl_options = {'REGISTER', 'UNDO'} + + + def execute(self, context): + Add_Chain() + + return {'FINISHED'} + +# Register the operator +menu_func = (lambda self, + context: self.layout.operator(AddChain.bl_idname, + text="Chain", icon='PLUGIN')) + + +def register(): + bpy.types.register(AddChain) + + # Add "Chain" menu to the "Add Mesh" menu. + bpy.types.INFO_MT_mesh_add.append(menu_func) + + +def unregister(): + bpy.types.unregister(AddChain) + + # Remove "Chain" menu from the "Add Mesh" menu. + bpy.types.INFO_MT_mesh_add.remove(menu_func) + +if __name__ == "__main__": + register() diff --git a/object_cloud_gen.py b/object_cloud_gen.py new file mode 100644 index 00000000..8b9f0e1f --- /dev/null +++ b/object_cloud_gen.py @@ -0,0 +1,679 @@ +# ##### 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 ##### + +""" +Place this file in the .blender/scripts/addons dir +You have to activated the script in the "Add-Ons" tab (user preferences). +The functionality can then be accessed via the Tool shelf when objects +are selected + +Rev 0 initial release +Rev 0.1 added scene to create_mesh per python api change. +Rev 0.2 Added Point Density turbulence and fixed degenerate +Rev 0.3 Fixed bug in degenerate +Rev 0.4 updated for api change/changed to new apply modifier technique +Rev 0.5 made particle count equation with radius so radius increases with cloud volume +Rev 0.6 added poll function to operator, fixing crash with no selected objects +Rev 0.7 added particles option and Type of Cloud wanted selector +""" + + +bl_addon_info = { + 'name': 'Object: Cloud Generator', + 'author': 'Nick Keeline(nrk)', + 'version': '0.7', + 'blender': (2, 5, 3), + 'location': 'Tool Shelf ', + 'description': 'Creates Volumetric Clouds', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Object/Cloud_Gen', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=22015&group_id=153&atid=469', + 'category': 'Object'} + + +import bpy +import mathutils +from math import * +from bpy.props import * + + +# This routine takes an object and deletes all of the geometry in it +# and adds a bounding box to it. +# It will add or subtract the bound box size by the variable sizeDifference. +def makeObjectIntoBoundBox(object, sizeDifference): + # Deselect All + bpy.ops.object.select_all(action='DESELECT') + + # Select the object + object.select = True + + # Go into Edit Mode + bpy.ops.object.mode_set(mode='EDIT') + + mesh = object.data + verts = mesh.verts + + #Set the max and min verts to the first vertex on the list + maxVert = [verts[0].co[0], verts[0].co[1], verts[0].co[2]] + minVert = [verts[0].co[0], verts[0].co[1], verts[0].co[2]] + + #Create Max and Min Vertex array for the outer corners of the box + for vert in verts: + #Max vertex + if vert.co[0] > maxVert[0]: + maxVert[0] = vert.co[0] + if vert.co[1] > maxVert[1]: + maxVert[1] = vert.co[1] + if vert.co[2] > maxVert[2]: + maxVert[2] = vert.co[2] + + #Min Vertex + if vert.co[0] < minVert[0]: + minVert[0] = vert.co[0] + if vert.co[1] < minVert[1]: + minVert[1] = vert.co[1] + if vert.co[2] < minVert[2]: + minVert[2] = vert.co[2] + + #Add the size difference to the max size of the box + maxVert[0] = maxVert[0] + sizeDifference + maxVert[1] = maxVert[1] + sizeDifference + maxVert[2] = maxVert[2] + sizeDifference + + #subtract the size difference to the min size of the box + minVert[0] = minVert[0] - sizeDifference + minVert[1] = minVert[1] - sizeDifference + minVert[2] = minVert[2] - sizeDifference + + #Create arrays of verts and faces to be added to the mesh + addVerts = [] + + #X high loop + addVerts.append([maxVert[0], maxVert[1], maxVert[2]]) + addVerts.append([maxVert[0], maxVert[1], minVert[2]]) + addVerts.append([maxVert[0], minVert[1], minVert[2]]) + addVerts.append([maxVert[0], minVert[1], maxVert[2]]) + + #x low loop + addVerts.append([minVert[0], maxVert[1], maxVert[2]]) + addVerts.append([minVert[0], maxVert[1], minVert[2]]) + addVerts.append([minVert[0], minVert[1], minVert[2]]) + addVerts.append([minVert[0], minVert[1], maxVert[2]]) + + # Make the faces of the bounding box. + addFaces = [] + + # Draw a box on paper and number the vertices. + # Use right hand rule to come up with number orders for faces on + # the box (with normals pointing out). + addFaces.append([0, 3, 2, 1]) + addFaces.append([4, 5, 6, 7]) + addFaces.append([0, 1, 5, 4]) + addFaces.append([1, 2, 6, 5]) + addFaces.append([2, 3, 7, 6]) + addFaces.append([0, 4, 7, 3]) + + # Delete all geometry from the object. + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.delete(type='ALL') + + # Must be in object mode for from_pydata to work + bpy.ops.object.mode_set(mode='OBJECT') + + # Add the mesh data. + mesh.from_pydata(addVerts, [], addFaces) + + # Update the mesh + mesh.update() + + +def applyScaleRotLoc(scene, obj): + # Deselect All + bpy.ops.object.select_all(action='DESELECT') + + # Select the object + obj.select = True + scene.objects.active = obj + + #bpy.ops.object.rotation_apply() + bpy.ops.object.location_apply() + bpy.ops.object.scale_apply() + + +def totallyDeleteObject(scene, obj): + #To Do this section to be updated when + #Ability to completely delet objects added to blender + # Deselect All + bpy.ops.object.select_all(action='DESELECT') + + # Select the object and delete it. + obj.select = True + scene.objects.active = obj + + # Delete all material slots in obj + for i in range(len(obj.material_slots)): + #textureSlots = cloudMaterial.texture_slots + obj.active_material_index = i - 1 + bpy.ops.object.material_slot_remove() + + #bpy.ops.object.parent_clear(type='CLEAR') + + # Delete the Main Object + bpy.ops.object.delete() + #bpy.data.objects.remove(obj) + + +def makeParent(parentobj, childobj, scene): + + applyScaleRotLoc(scene, parentobj) + + applyScaleRotLoc(scene, childobj) + + childobj.parent = parentobj + + #childobj.location = childobj.location - parentobj.location + + +def addNewObject(scene, name, copyobj): + ''' + Add an object and do other silly stuff. + ''' + # Create new mesh + mesh = bpy.data.meshes.new(name) + + # Create a new object. + ob_new = bpy.data.objects.new(name, mesh) + tempme = copyobj.data + ob_new.data = tempme.copy() + ob_new.scale = copyobj.scale + ob_new.location = copyobj.location + + # Link new object to the given scene and select it. + scene.objects.link(ob_new) + ob_new.select = True + + return ob_new + + +def combineObjects(scene, combined, listobjs): + # scene is the current scene + # combined is the object we want to combine everything into + # listobjs is the list of objects to stick into combined + + # Deselect All + bpy.ops.object.select_all(action='DESELECT') + + # Select the new object. + combined.select = True + scene.objects.active = combined + + # Add data + if (len(listobjs) > 0): + for i in listobjs: + # Add a modifier + bpy.ops.object.modifier_add(type='BOOLEAN') + + union = combined.modifiers + union[0].name = "AddEmUp" + union[0].object = i + union[0].operation = 'UNION' + + # Apply modifier + bpy.ops.object.modifier_apply(apply_as='DATA', modifier=union[0].name) + + +# Returns True if we want to degenerate +# and False if we want to generate a new cloud. +def degenerateCloud(obj): + if not obj: + return False + + if "CloudMember" in obj: + if obj["CloudMember"] != None: + if obj.parent: + if "CloudMember" not in obj.parent: + return False + + else: + return True + + return False + +class View3DPanel(bpy.types.Panel): + bl_space_type = 'VIEW_3D' + bl_region_type = 'TOOLS' + + +class VIEW3D_PT_tools_cloud(View3DPanel): + bl_label = "Cloud Generator" + bl_context = "objectmode" + + def draw(self, context): + active_obj = context.active_object + layout = self.layout + col = layout.column(align=True) + + degenerate = degenerateCloud(active_obj) + + if active_obj and degenerate: + + col.operator("cloud.generate_cloud", text="DeGenerate") + + elif active_obj is None: + + col.label(text="Select one or more") + col.label(text="objects to generate") + col.label(text="a cloud.") + + elif "CloudMember" in active_obj: + + col.label(text="Must select") + col.label(text="bound box") + + elif active_obj and active_obj.type == 'MESH': + + col.operator("cloud.generate_cloud", text="Generate Cloud") + + col.prop(context.scene, "cloudparticles") + col.prop(context.scene, "cloud_type") + else: + col.label(text="Select one or more") + col.label(text="objects to generate") + col.label(text="a cloud.") + +cloudTypes = [] + +cloudTypes.append(("0","Stratus","Generate Stratus_foggy Cloud")) +cloudTypes.append(("1","Cumulous","Generate Cumulous_puffy Cloud")) +cloudTypes.append(("2","Cirrus","Generate Cirrus_wispy Cloud")) +#cloudTypes.append(("3","Nimbus","Generate Nimbus Cloud")) + + +bpy.types.Scene.BoolProperty( attr="cloudparticles", + name="Particles", + description="Generate Cloud as Particle System", + default=False) + +bpy.types.Scene.EnumProperty( attr="cloud_type", + name="Type", + description="Select the type of cloud to create with material settings", + items = cloudTypes, default = '0') + +class GenerateCloud(bpy.types.Operator): + bl_idname = "cloud.generate_cloud" + bl_label = "Generate Cloud" + bl_description = "Create a Cloud." + bl_register = True + bl_undo = True + + def poll(self, context): + if not context.active_object: + return False + else: + return (context.active_object.type=='MESH') + + def execute(self, context): + # Make variable that is the current .blend file main data blocks + main = context.main + + # Make variable that is the active object selected by user + active_object = context.active_object + + # Make variable scene that is current scene + scene = context.scene + + # Parameters the user may want to change: + # Number of points this number is multiplied by the volume to get + # the number of points the scripts will put in the volume. + numOfPoints = 1.0 + maxNumOfPoints = 100000 + scattering = 2.5 + pointDensityRadiusFactor = 1.0 + densityScale = 1.5 + + # Should we degnerate? + degenerate = degenerateCloud(active_object) + + if degenerate: + # Degenerate Cloud + mainObj = active_object + + cloudMembers = active_object.children + + createdObjects = [] + definitionObjects = [] + for member in cloudMembers: + applyScaleRotLoc(scene, member) + if (member["CloudMember"] == "CreatedObj"): + createdObjects.append(member) + else: + definitionObjects.append(member) + + for defObj in definitionObjects: + #Delete cloudmember data from objects + if "CloudMember" in defObj: + del(defObj["CloudMember"]) + + for createdObj in createdObjects: + totallyDeleteObject(scene, createdObj) + + # Delete the main object + totallyDeleteObject(scene, mainObj) + + # Select all of the left over boxes so people can immediately + # press generate again if they want. + for eachMember in definitionObjects: + eachMember.max_draw_type = 'SOLID' + eachMember.select = True + #scene.objects.active = eachMember + + #TODO Delete this when render bug caused by degenerate is fixed. + self.report({'WARNING'}, "Please save file exit and reenter blender before rendering to clean memory and prevent crash") + + else: + # Generate Cloud + + ###############Create Combined Object bounds################## + # Make a list of all Selected objects. + selectedObjects = bpy.context.selected_objects + if not selectedObjects: + selectedObjects = [bpy.context.active_object] + + # Create a new object bounds + bounds = addNewObject(scene, + "CloudBounds", + selectedObjects[0]) + + bounds.max_draw_type = 'BOUNDS' + bounds.hide_render = False + + # Just add a Definition Property designating this + # as the main object. + bounds["CloudMember"] = "MainObj" + + # Since we used iteration 0 to copy with object we + # delete it off the list. + firstObject = selectedObjects[0] + del selectedObjects[0] + + # Apply location Rotation and Scale to all objects involved. + applyScaleRotLoc(scene, bounds) + for each in selectedObjects: + applyScaleRotLoc(scene, each) + + # Let's combine all of them together. + combineObjects(scene, bounds, selectedObjects) + + # Let's add some property info to the objects. + for selObj in selectedObjects: + selObj["CloudMember"] = "DefinitioinObj" + selObj.name = "DefinitioinObj" + selObj.max_draw_type = 'WIRE' + selObj.hide_render = True + makeParent(bounds, selObj, scene) + + # Do the same to the 1. object since it is no longer in list. + firstObject["CloudMember"] = "DefinitioinObj" + firstObject.name = "DefinitioinObj" + firstObject.max_draw_type = 'WIRE' + firstObject.hide_render = True + makeParent(bounds, firstObject, scene) + + ###############Create Cloud for putting Cloud Mesh############ + # Create a new object cloud. + cloud = addNewObject(scene, "CloudMesh", bounds) + cloud["CloudMember"] = "CreatedObj" + cloud.max_draw_type = 'WIRE' + cloud.hide_render = True + + makeParent(bounds, cloud, scene) + + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.subdivide(number_cuts=2, fractal=0, smoothness=1) + bpy.ops.object.location_apply() + bpy.ops.mesh.vertices_smooth(repeat=20) + bpy.ops.mesh.tris_convert_to_quads() + bpy.ops.mesh.faces_shade_smooth() + bpy.ops.object.editmode_toggle() + + ###############Create Particles in cloud obj################## + # Turn off gravity. + scene.use_gravity = False + + # Set time to 0. + scene.frame_current = 0 + + # Add a new particle system. + bpy.ops.object.particle_system_add() + + #Particle settings setting it up! + cloudParticles = cloud.active_particle_system + cloudParticles.name = "CloudParticles" + cloudParticles.settings.frame_start = 0 + cloudParticles.settings.frame_end = 0 + cloudParticles.settings.emit_from = 'VOLUME' + cloudParticles.settings.draw_as = 'DOT' + cloudParticles.settings.ren_as = 'NONE' + cloudParticles.settings.normal_factor = 0 + cloudParticles.settings.distribution = 'RAND' + cloudParticles.settings.physics_type = 'NO' + + ####################Create Volume Material#################### + # Deselect All + bpy.ops.object.select_all(action='DESELECT') + + # Select the object. + bounds.select = True + scene.objects.active = bounds + + # Turn bounds object into a box. + makeObjectIntoBoundBox(bounds, .6) + + # Delete all material slots in bounds object. + for i in range(len(bounds.material_slots)): + bounds.active_material_index = i - 1 + bpy.ops.object.material_slot_remove() + + # Add a new material. + cloudMaterial = main.materials.new("CloudMaterial") + bpy.ops.object.material_slot_add() + bounds.material_slots[0].material = cloudMaterial + + # Set Up the Cloud Material + cloudMaterial.name = "CloudMaterial" + cloudMaterial.type = 'VOLUME' + mVolume = cloudMaterial.volume + mVolume.scattering = scattering + mVolume.density = 0 + mVolume.density_scale = densityScale + mVolume.transmission_color = [3, 3, 3] + mVolume.step_size = 0.1 + mVolume.light_cache = True + mVolume.cache_resolution = 75 + + # Add a texture + vMaterialTextureSlots = cloudMaterial.texture_slots + cloudtex = main.textures.new("CloudTex") + cloudMaterial.add_texture(cloudtex, 'ORCO') + cloudtex.type = 'CLOUDS' + cloudtex.noise_type = 'HARD_NOISE' + cloudtex.noise_size = 2 + + # Add a force field to the points. + cloudField = bounds.field + cloudField.type = 'TEXTURE' + cloudField.strength = 2 + cloudField.texture = cloudtex + + # Set time + #for i in range(12): + # scene.current_frame = i + # scene.update() + scene.frame_current = 1 + + #bpy.ops.ptcache.bake(bake=False) + + # Add a Point Density texture + cloudPointDensity = main.textures.new("CloudPointDensity") + cloudPointDensity.type = 'POINT_DENSITY' + cloudMaterial.add_texture(cloudPointDensity, 'ORCO') + pDensity = vMaterialTextureSlots[1].texture + vMaterialTextureSlots[1].map_density = True + vMaterialTextureSlots[1].rgb_to_intensity = True + vMaterialTextureSlots[1].texture_coordinates = 'GLOBAL' + pDensity.pointdensity.vertices_cache = 'WORLD_SPACE' + pDensity.pointdensity.turbulence = True + pDensity.pointdensity.noise_basis = 'VORONOI_F2' + pDensity.pointdensity.turbulence_depth = 3 + + pDensity.use_color_ramp = True + pRamp = pDensity.color_ramp + pRamp.interpolation = 'LINEAR' + pRampElements = pRamp.elements + #pRampElements[1].position = .9 + #pRampElements[1].color = [.18,.18,.18,.8] + bpy.ops.texture.slot_move(type='UP') + + + # Estimate the number of particles for the size of bounds. + volumeBoundBox = (bounds.dimensions[0] * bounds.dimensions[1]* bounds.dimensions[2]) + numParticles = int((2.4462 * volumeBoundBox + 430.4) * numOfPoints) + if numParticles > maxNumOfPoints: + numParticles = maxNumOfPoints + if numParticles < 10000: + numParticles = int(numParticles + 15 * volumeBoundBox) + print(numParticles) + + # Set the number of particles according to the volume + # of bounds. + cloudParticles.settings.amount = numParticles + + pDensity.pointdensity.radius = (.00013764 * volumeBoundBox + .3989) * pointDensityRadiusFactor + + # Set time to 1. + scene.frame_current = 1 + + if not scene.cloudparticles: + ###############Create CloudPnts for putting points in######### + # Create a new object cloudPnts + cloudPnts = addNewObject(scene, "CloudPoints", bounds) + cloudPnts["CloudMember"] = "CreatedObj" + cloudPnts.max_draw_type = 'WIRE' + cloudPnts.hide_render = True + + makeParent(bounds, cloudPnts, scene) + + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.select_all(action='SELECT') + + bpy.ops.mesh.delete(type='ALL') + + meshPnts = cloudPnts.data + + listCloudParticles = cloudParticles.particles + + listMeshPnts = [] + for pTicle in listCloudParticles: + listMeshPnts.append(pTicle.location) + + # Must be in object mode fro from_pydata to work. + bpy.ops.object.mode_set(mode='OBJECT') + + # Add in the mesh data. + meshPnts.from_pydata(listMeshPnts, [], []) + + # Update the mesh. + meshPnts.update() + + # Add a modifier. + bpy.ops.object.modifier_add(type='DISPLACE') + + cldPntsModifiers = cloudPnts.modifiers + cldPntsModifiers[0].name = "CloudPnts" + cldPntsModifiers[0].texture = cloudtex + cldPntsModifiers[0].texture_coordinates = 'OBJECT' + cldPntsModifiers[0].texture_coordinate_object = cloud + cldPntsModifiers[0].strength = -1.4 + + # Apply modifier + bpy.ops.object.modifier_apply(apply_as='DATA', modifier=cldPntsModifiers[0].name) + + pDensity.pointdensity.point_source = 'OBJECT' + pDensity.pointdensity.object = cloudPnts + + # Deselect All + bpy.ops.object.select_all(action='DESELECT') + + # Select the object. + cloud.select = True + scene.objects.active = cloud + + bpy.ops.object.particle_system_remove() + + # Deselect All + bpy.ops.object.select_all(action='DESELECT') + + else: + + pDensity.pointdensity.point_source = 'PARTICLE_SYSTEM' + pDensity.pointdensity.object = cloud + pDensity.pointdensity.particle_system = cloudParticles + + if scene.cloud_type == '1': # Cumulous + print("Cumulous") + mVolume.density_scale = 2.22 + pDensity.pointdensity.turbulence_depth = 10 + pDensity.pointdensity.turbulence_strength = 6.3 + pDensity.pointdensity.turbulence_size = 2.9 + pRampElements[1].position = .606 + pDensity.pointdensity.radius = pDensity.pointdensity.radius + .1 + + elif scene.cloud_type == '2': # Cirrus + print("Cirrus") + pDensity.pointdensity.turbulence_strength = 22 + mVolume.transmission_color = [3.5, 3.5, 3.5] + mVolume.scattering = .13 + + # Select the object. + bounds.select = True + scene.objects.active = bounds + + return {'FINISHED'} + + +classes = [VIEW3D_PT_tools_cloud, + GenerateCloud] + + +def register(): + register = bpy.types.register + for cls in classes: + register(cls) + + +def unregister(): + unregister = bpy.types.unregister + for cls in classes: + unregister(cls) + + +if __name__ == "__main__": + register() diff --git a/object_fracture/__init__.py b/object_fracture/__init__.py new file mode 100644 index 00000000..f1c87d6a --- /dev/null +++ b/object_fracture/__init__.py @@ -0,0 +1,86 @@ +# ##### 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 ##### + +bl_addon_info = { + 'name': 'Object: Fracture Tools', + 'author': 'pildanovak', + 'version': '2.0', + 'blender': (2, 5, 3), + 'location': 'Fracture tools (Search > Fracture Object & ,' \ + 'Add -> Fracture Helper Objects', + 'description': 'Fractured Object, Bomb, Projectile, Recorder', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Object/Fracture', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=21793&group_id=153&atid=469', + 'category': 'Object'} + + +import bpy +from object_fracture import fracture_ops, fracture_setup + + +class INFO_MT_add_fracture_objects(bpy.types.Menu): + bl_idname = "INFO_MT_add_fracture_objects" + bl_label = "Fracture Helper Objects" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.operator("object.import_fracture_bomb", + text="Bomb") + layout.operator("object.import_fracture_projectile", + text="Projectile") + layout.operator("object.import_fracture_recorder", + text="Rigidbody Recorder") + +import space_info +# Define the submenu +menu_func = (lambda self, + context: self.layout.menu("INFO_MT_add_fracture_objects", icon="PLUGIN")) + + +def register(): + bpy.types.register(fracture_ops.FractureSimple) + bpy.types.register(fracture_ops.FractureGroup) + bpy.types.register(fracture_ops.ImportFractureRecorder) + bpy.types.register(fracture_ops.ImportFractureBomb) + bpy.types.register(fracture_ops.ImportFractureProjectile) + bpy.types.register(fracture_setup.SetupFractureShards) + bpy.types.register(INFO_MT_add_fracture_objects) + + # Add the "add fracture objects" menu to the "Add" menu + space_info.INFO_MT_add.append(menu_func) + + +def unregister(): + bpy.types.unregister(fracture_ops.FractureSimple) + bpy.types.unregister(fracture_ops.FractureGroup) + bpy.types.unregister(fracture_ops.ImportFractureRecorder) + bpy.types.unregister(fracture_ops.ImportFractureBomb) + bpy.types.unregister(fracture_ops.ImportFractureProjectile) + bpy.types.unregister(fracture_setup.SetupFractureShards) + bpy.types.unregister(INFO_MT_add_fracture_objects) + + # Remove "add fracture objects" menu from the "Add" menu. + space_info.INFO_MT_add.remove(menu_func) + +if __name__ == "__main__": + register() diff --git a/object_fracture/data.blend b/object_fracture/data.blend new file mode 100644 index 00000000..e0e72dcf Binary files /dev/null and b/object_fracture/data.blend differ diff --git a/object_fracture/fracture_ops.py b/object_fracture/fracture_ops.py new file mode 100644 index 00000000..b2aa8442 --- /dev/null +++ b/object_fracture/fracture_ops.py @@ -0,0 +1,492 @@ +# ##### 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 ##### + +import bpy +from bpy.props import * +import os +import random +import mathutils +from mathutils import * + + +def create_cutter(context, crack_type, scale, roughness): + ncuts = 12 + if crack_type == 'FLAT' or crack_type == 'FLAT_ROUGH': + bpy.ops.mesh.primitive_cube_add( + view_align=False, + enter_editmode=False, + location=(0, 0, 0), + rotation=(0, 0, 0), + layer=(True, False, False, False, + False, False, False, False, + False, False, False, False, + False, False, False, False, + False, False, False, False)) + + for v in context.scene.objects.active.data.verts: + v.co[0] += 1 + v.co[0] *= scale + v.co[1] *= scale + v.co[2] *= scale + + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.faces_shade_smooth() + bpy.ops.uv.reset() + + if crack_type == 'FLAT_ROUGH': + bpy.ops.mesh.subdivide( + number_cuts=ncuts, + fractal=roughness * 7 * scale, + smoothness=0) + + bpy.ops.mesh.vertices_smooth(repeat=5) + + bpy.ops.object.editmode_toggle() + + if crack_type == 'SPHERE' or crack_type == 'SPHERE_ROUGH': + bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=4, + size=1, + view_align=False, + enter_editmode=False, + location=(0, 0, 0), + rotation=(0, 0, 0), + layer=(True, False, False, False, + False, False, False, False, + False, False, False, False, + False, False, False, False, + False, False, False, False, + False, False, False, False, + False, False, False, False, + False, False, False, False)) + + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.faces_shade_smooth() + bpy.ops.uv.smart_project(angle_limit=66, island_margin=0) + + bpy.ops.object.editmode_toggle() + for v in context.scene.objects.active.data.verts: + v.co[0] += 1 + v.co[0] *= scale + v.co[1] *= scale + v.co[2] *= scale + + if crack_type == 'SPHERE_ROUGH': + for v in context.scene.objects.active.data.verts: + v.co[0] += roughness * scale * 0.2 * (random.random() - 0.5) + v.co[1] += roughness * scale * 0.1 * (random.random() - 0.5) + v.co[2] += roughness * scale * 0.1 * (random.random() - 0.5) + + bpy.context.scene.objects.active.select = True + + ''' + # Adding fracture material + # @todo Doesn't work at all yet. + sce = bpy.context.scene + if bpy.data.materials.get('fracture')==None: + bpy.ops.material.new() + bpy.ops.object.material_slot_add() + sce.objects.active.material_slots[0].material.name = 'fracture' + else: + bpy.ops.object.material_slot_add() + sce.objects.active.material_slots[0].material + = bpy.data.materials['fracture'] + ''' + + +#UNWRAP +def getsizefrommesh(ob): + bb = ob.bound_box + return ( + bb[5][0] - bb[0][0], + bb[3][1] - bb[0][1], + bb[1][2] - bb[0][2]) + + +def getIslands(shard): + sm = shard.data + islands = [] + vgroups = [] + fgroups = [] + + vgi = [] + for v in sm.verts: + vgi.append(-1) + + gindex = 0 + for i in range(len(vgi)): + if vgi[i] == -1: + gproc = [i] + vgroups.append([i]) + fgroups.append([]) + + while len(gproc) > 0: + i = gproc.pop(0) + for f in sm.faces: + #if i in f.verts: + for v in f.verts: + if v == i: + for v1 in f.verts: + if vgi[v1] == -1: + vgi[v1] = gindex + vgroups[gindex].append(v1) + gproc.append(v1) + + fgroups[gindex].append(f.index) + + gindex += 1 + + #print( gindex) + + if gindex == 1: + shards = [shard] + + else: + shards = [] + for gi in range(0, gindex): + bpy.ops.object.select_all(action='DESELECT') + bpy.context.scene.objects.active = shard + shard.select = True + bpy.ops.object.duplicate(linked=False, mode=1) + a = bpy.context.scene.objects.active + sm = a.data + print (a.name) + + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.editmode_toggle() + + for x in range(len(sm.verts) - 1, -1, -1): + if vgi[x] != gi: + #print('getIslands: selecting') + #print('getIslands: ' + str(x)) + a.data.verts[x].select = True + + print(bpy.context.scene.objects.active.name) + + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.delete() + bpy.ops.object.editmode_toggle() + + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') + + shards.append(a) + + bpy.context.scene.objects.unlink(shard) + + return shards + + +def boolop(ob, cutter, op): + sce = bpy.context.scene + + fault = 0 + new_shards = [] + + sizex, sizey, sizez = getsizefrommesh(ob) + gsize = sizex + sizey + sizez + + bpy.ops.object.select_all() + ob.select = True + sce.objects.active = ob + cutter.select = False + + bpy.ops.object.modifier_add(type='BOOLEAN') + a = sce.objects.active + a.modifiers['Boolean'].object = cutter + a.modifiers['Boolean'].operation = op + + nmesh = a.create_mesh(sce, apply_modifiers=True, settings='PREVIEW') + + if len(nmesh.verts) > 0: + a.modifiers.remove(a.modifiers['Boolean']) + bpy.ops.object.duplicate(linked=False, mode=1) + + new_shard = sce.objects.active + new_shard.data = nmesh + #scene.objects.link(new_shard) + + new_shard.location = a.location + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') + + sizex, sizey, sizez = getsizefrommesh(new_shard) + gsize2 = sizex + sizey + sizez + + if gsize2 > gsize * 1.01: # Size check + print (gsize2, gsize, ob.name, cutter.name) + fault = 1 + #print ('boolop: sizeerror') + + elif min(nmesh.edge_face_count) < 2: # Manifold check + fault = 1 + + if not fault: + new_shards = getIslands(new_shard) + + else: + sce.objects.unlink(new_shard) + + else: + fault = 2 + + return fault, new_shards + + +def splitobject(context, ob, crack_type, roughness): + scene = context.scene + + size = getsizefrommesh(ob) + shards = [] + scale = max(size) * 1.3 + + create_cutter(context, crack_type, scale, roughness) + cutter = context.active_object + cutter.location = ob.location + + cutter.location[0] += random.random() * size[0] * 0.1 + cutter.location[1] += random.random() * size[1] * 0.1 + cutter.location[2] += random.random() * size[2] * 0.1 + cutter.rotation_euler = [ + random.random() * 5000.0, + random.random() * 5000.0, + random.random() * 5000.0] + + scene.objects.active = ob + operations = ['INTERSECT', 'DIFFERENCE'] + + for op in operations: + fault, newshards = boolop(ob, cutter, op) + + shards.extend(newshards) + if fault > 0: + # Delete all shards in case of fault from previous operation. + for s in shards: + scene.objects.unlink(s) + + scene.objects.unlink(cutter) + #print('splitobject: fault') + + return [ob] + + if shards[0] != ob: + bpy.context.scene.objects.unlink(ob) + + bpy.context.scene.objects.unlink(cutter) + + return shards + + +def fracture_basic(context, nshards, crack_type, roughness): + tobesplit = [] + shards = [] + + for ob in context.scene.objects: + if ob.select: + tobesplit.append(ob) + + i = 1 # I counts shards, starts with 1 - the original object + iter = 0 # counts iterations, to prevent eternal loops in case + # of boolean faults + + maxshards = nshards * len(tobesplit) + + while i < maxshards and len(tobesplit) > 0 and iter < maxshards * 10: + ob = tobesplit.pop(0) + newshards = splitobject(context, ob, crack_type, roughness) + + tobesplit.extend(newshards) + + if len(newshards) > 1: + shards.extend(newshards) + #shards.remove(ob) + + i += (len(newshards) - 1) + + #print('fracture_basic: ' + str(i)) + #print('fracture_basic: lenobs', len(context.scene.objects)) + + iter += 1 + + +def fracture_group(context, group): + tobesplit = [] + shards = [] + + for ob in context.scene.objects: + if (ob.select + and (len(ob.users_group) == 0 or ob.users_group[0].name != group)): + tobesplit.append(ob) + + cutters = bpy.data.groups[group].objects + + # @todo This can be optimized. + # Avoid booleans on obs where bbox doesn't intersect. + i = 0 + for ob in tobesplit: + for cutter in cutters: + fault, newshards = boolop(ob, cutter, 'INTERSECT') + shards.extend(newshards) + + if fault == 1: + # Delete all shards in case of fault from previous operation. + for s in shards: + bpy.context.scene.objects.unlink(s) + + #print('fracture_group: fault') + #print('fracture_group: ' + str(i)) + + return + + i += 1 + + +class FractureSimple(bpy.types.Operator): + '''Split object with boolean operations for simulation, uses an object.''' + bl_idname = "object.fracture_simple" + bl_label = "Fracture Object" + bl_options = {'REGISTER', 'UNDO'} + + exe = BoolProperty(name="Execute", + description="If it shall actually run, for optimal performance...", + default=False) + + hierarchy = BoolProperty(name="Generate hierarchy", + description="Hierarchy is usefull for simulation of objects" \ + " breaking in motion.", + default=False) + + nshards = IntProperty(name="Number of shards", + description="Number of shards the object should be split into.", + min=2, + default=5) + + crack_type = EnumProperty(name='Crack type', + items=( + ('FLAT', 'Flat', 'a'), + ('FLAT_ROUGH', 'Flat rough', 'a'), + ('SPHERE', 'Spherical', 'a'), + ('SPHERE_ROUGH', 'Spherical rough', 'a')), + description='Look of the fracture surface', + default='FLAT') + + roughness = FloatProperty(name="Roughness", + description="Roughness of the fracture surface", + min=0.0, + max=3.0, + default=0.5) + + def execute(self, context): + #getIslands(context.object) + props = self.properties + + if props.exe: + fracture_basic(context, + props.nshards, + props.crack_type, + props.roughness) + + return {'FINISHED'} + + +class FractureGroup(bpy.types.Operator): + '''Split object with boolean operations for simulation, uses a group.''' + bl_idname = "object.fracture_group" + bl_label = "Fracture Object (Group)" + bl_options = {'REGISTER', 'UNDO'} + + exe = BoolProperty(name="Execute", + description="If it shall actually run, for optimal performance...", + default=False) + + e = [] + for i, g in enumerate(bpy.data.groups): + e.append((g.name, g.name, '')) + + group = EnumProperty(name='Group (hit F8 to refresh list)', + items=e, + description='Specify the group used for fracturing') + + def execute(self, context): + #getIslands(context.object) + + if self.properties.exe: + fracture_group(context, self.properties.group) + + return {'FINISHED'} + + +##################################################################### +# Import Functions + +def import_object(obname): + opath = "//data.blend\\Object\\" + obname + s = os.sep + dpath = bpy.utils.script_paths()[0] + \ + '%saddons%sobject_fracture%sdata.blend\\Object\\' % (s, s, s) + + # DEBUG + #print('import_object: ' + opath) + + bpy.ops.wm.link_append( + filepath=opath, + filename=obname, + directory=dpath, + filemode=1, + link=False, + autoselect=True, + active_layer=True, + instance_groups=True, + relative_path=True) + + for ob in bpy.context.selected_objects: + ob.location = bpy.context.scene.cursor_location + + +class ImportFractureRecorder(bpy.types.Operator): + '''Imports a rigidbody recorder''' + bl_idname = "object.import_fracture_recorder" + bl_label = "Add Rigidbody Recorder (Fracture)" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + import_object("RECORDER") + + return {'FINISHED'} + + +class ImportFractureBomb(bpy.types.Operator): + '''Import a bomb''' + bl_idname = "object.import_fracture_bomb" + bl_label = "Add Bomb (Fracture)" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + import_object("BOMB") + + return {'FINISHED'} + + +class ImportFractureProjectile(bpy.types.Operator, ): + '''Imports a projectile''' + bl_idname = "object.import_fracture_projectile" + bl_label = "Add Projectile (Fracture)" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + import_object("PROJECTILE") + + return {'FINISHED'} diff --git a/object_fracture/fracture_setup.py b/object_fracture/fracture_setup.py new file mode 100644 index 00000000..235dd213 --- /dev/null +++ b/object_fracture/fracture_setup.py @@ -0,0 +1,74 @@ +# ##### 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 ##### + +import bpy +from bpy.props import * + + +def getsizefrommesh(ob): + bb = ob.bound_box + return( + bb[5][0] - bb[0][0], + bb[3][1] - bb[0][1], + bb[1][2] - bb[0][2]) + + +def setupshards(context): + sce = context.scene + #print(dir(context)) + #bpy.data.scenes[0].game_data.all_frames + + tobeprocessed = [] + for ob in sce.objects: + if ob.selected: + tobeprocessed.append(ob) + + for ob in tobeprocessed: + g = ob.game + + g.physics_type = 'RIGID_BODY' + g.use_collision_bounds = 1 + g.collision_bounds = 'CONVEX_HULL' + g.rotation_damping = 0.9 + + sizex, sizey, sizez = getsizefrommesh(ob) + approxvolume = sizex * sizey * sizez + g.mass = approxvolume + + sce.objects.active = ob + + bpy.ops.object.game_property_new() + g.properties['prop'].name = 'shard' + #sm=FloatProperty(name='shard',description='shardprop',default=0.0) + #print (sm) + #np=bpy.types.GameFloatProperty(sm) + #name='shard',type='BOOL', value=1 + #print(ob) + + +class SetupFractureShards(bpy.types.Operator): + '''''' + bl_idname = "object.setup_fracture_shards" + bl_label = "Setup Fracture Shards" + bl_options = {'REGISTER', 'UNDO'} + + #def poll(self, context): + + def execute(self, context): + setupshards(context) + return {'FINISHED'} diff --git a/render_renderfarmfi.py b/render_renderfarmfi.py new file mode 100644 index 00000000..9c2db3c7 --- /dev/null +++ b/render_renderfarmfi.py @@ -0,0 +1,799 @@ +# ##### 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 ##### +# +# Copyright 2009-2010 Laurea University of Applied Sciences +# Authors: Nathan Letwory, Jesse Kaukonen + +import bpy +import hashlib +import http.client +import xmlrpc.client +import math +from os.path import abspath, isabs, join, isfile + +bpy.CURRENT_VERSION = 2 +bpy.found_newer_version = False +bpy.up_to_date = False +bpy.download_location = 'http://www.renderfarm.fi/blender' + +bl_addon_info = { + 'name': 'Render: Renderfarm.fi', + 'author': 'Nathan Letwory , Jesse Kaukonen ', + 'version': str(bpy.CURRENT_VERSION), + 'blender': (2, 5, 3), + 'location': 'Render > Engine > Renderfarm.fi', + 'description': 'Send .blend as session to http://www.renderfarm.fi to render', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/Render/Renderfarm.fi', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=22927&group_id=153&atid=469', + 'category': 'Render'} + +bpy.errorMessages = { + 'missing_desc': 'You need to enter a title, short and long description', + 'missing_creds': 'You haven\'t entered your credentials yet' +} + +bpy.statusMessage = { + 'title': 'TRIA_RIGHT', + 'shortdesc': 'TRIA_RIGHT', + 'longdesc': 'TRIA_RIGHT', + 'username': 'TRIA_RIGHT', + 'password': 'TRIA_RIGHT' +} + +bpy.errors = [] +bpy.ore_sessions = [] +bpy.queue_selected = -1 + +def rnaType(rna_type): + bpy.types.register(rna_type) + return rna_type + +def renderEngine(render_engine): + bpy.types.register(render_engine) + return render_engine + +@rnaType +class ORESession(bpy.types.IDPropertyGroup): + pass + +@rnaType +class ORESettings(bpy.types.IDPropertyGroup): + pass + +# entry point for settings collection +bpy.types.Scene.PointerProperty(attr='ore_render', type=ORESettings, name='ORE Render', description='ORE Render Settings') + +# fill the new struct +ORESettings.StringProperty(attr='username', name='E-mail', description='E-mail for Renderfarm.fi', maxlen=256, default='') +ORESettings.StringProperty(attr='password', name='Password', description='Renderfarm.fi password', maxlen=256, default='') +ORESettings.StringProperty(attr='hash', name='Hash', description='hash calculated out of credentials', maxlen=33, default='') + +ORESettings.StringProperty(attr='shortdesc', name='Short description', description='A short description of the scene (100 characters)', maxlen=101, default='') +ORESettings.StringProperty(attr='longdesc', name='Long description', description='A more elaborate description of the scene (2k)', maxlen=2048, default='') +ORESettings.StringProperty(attr='title', name='Title', description='Title for this session (128 characters)', maxlen=128, default='') +ORESettings.StringProperty(attr='url', name='Project URL', description='Project URL. Leave empty if not applicable', maxlen=256, default='') + +ORESettings.IntProperty(attr='parts', name='Parts/Frame', description='', min=1, max=1000, soft_min=1, soft_max=64, default=1) +ORESettings.IntProperty(attr='resox', name='Resolution X', description='X of render', min=1, max=10000, soft_min=1, soft_max=10000, default=1920) +ORESettings.IntProperty(attr='resoy', name='Resolution Y', description='Y of render', min=1, max=10000, soft_min=1, soft_max=10000, default=1080) +ORESettings.IntProperty(attr='memusage', name='Memory Usage', description='Estimated maximum memory usage during rendering in MB', min=1, max=6*1024, soft_min=1, soft_max=3*1024, default=256) +ORESettings.IntProperty(attr='start', name='Start Frame', description='Start Frame', default=1) +ORESettings.IntProperty(attr='end', name='End Frame', description='End Frame', default=250) +ORESettings.IntProperty(attr='fps', name='FPS', description='FPS', min=1, max=256, default=25) + +ORESettings.BoolProperty(attr='prepared', name='Prepared', description='Set to True if preparation has been run', default=False) +ORESettings.BoolProperty(attr='debug', name='Debug', description='Verbose output in console', default=False) +ORESettings.IntProperty(attr='selected_session', name='Selected Session', description='The selected session', default=0) + +# session struct +ORESession.StringProperty(attr='name', name='Name', description='Name of the session', maxlen=128, default='[session]') + +licenses = ( + ('1', 'CC by-nc-nd', 'Creative Commons: Attribution Non-Commercial No Derivatives'), + ('2', 'CC by-nc-sa', 'Creative Commons: Attribution Non-Commercial Share Alike'), + ('3', 'CC by-nd', 'Creative Commons: Attribution No Derivatives'), + ('4', 'CC by-nc', 'Creative Commons: Attribution Non-Commercial'), + ('5', 'CC by-sa', 'Creative Commons: Attribution Share Alike'), + ('6', 'CC by', 'Creative Commons: Attribution'), + ('7', 'Copyright', 'Copyright, no license specified'), + ) +ORESettings.EnumProperty(attr='inlicense', items=licenses, name='source license', description='license speficied for the source files', default='1') +ORESettings.EnumProperty(attr='outlicense', items=licenses, name='output license', description='license speficied for the output files', default='1') + +ORESettings.CollectionProperty(attr='sessions', type=ORESession, name='Sessions', description='Sessions on Renderfarm.fi') + +# all panels, except render panel +# Example of wrapping every class 'as is' +import properties_scene +for member in dir(properties_scene): + subclass = getattr(properties_scene, member) + try: subclass.COMPAT_ENGINES.add('RENDERFARMFI_RENDER') + except: pass +del properties_scene + +import properties_world +for member in dir(properties_world): + subclass = getattr(properties_world, member) + try: subclass.COMPAT_ENGINES.add('RENDERFARMFI_RENDER') + except: pass +del properties_world + +import properties_material +for member in dir(properties_material): + subclass = getattr(properties_material, member) + try: subclass.COMPAT_ENGINES.add('RENDERFARMFI_RENDER') + except: pass +del properties_material + +import properties_object +for member in dir(properties_object): + subclass = getattr(properties_object, member) + try: subclass.COMPAT_ENGINES.add('RENDERFARMFI_RENDER') + except: pass +del properties_object + +class RenderButtonsPanel(bpy.types.Panel): + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "render" + # COMPAT_ENGINES must be defined in each subclass, external engines can add themselves here + + def poll(self, context): + rd = context.scene.render + return (rd.use_game_engine==False) and (rd.engine in self.COMPAT_ENGINES) + +class RENDERFARM_MT_Session(bpy.types.Menu): + bl_label = "Show Session" + + def draw(self, context): + layout = self.layout + + layout.operator('ore.completed_sessions') + layout.operator('ore.accept_sessions') + layout.operator('ore.active_sessions') + layout.separator() + layout.operator('ore.cancelled_sessions') + +class LOGIN_PT_RenderfarmFi(RenderButtonsPanel): + bl_label = 'Login to Renderfarm.fi' + COMPAT_ENGINES = set(['RENDERFARMFI_RENDER']) + def draw(self, context): + layout = self.layout + # XXX layout.operator('ore.check_update') + ore = context.scene.ore_render + updateSessionList(ore) + checkStatus(ore) + + if ore.hash=='': + col = layout.column() + if ore.hash=='': + col.prop(ore, 'username', icon=bpy.statusMessage['username']) + col.prop(ore, 'password', icon=bpy.statusMessage['password']) + layout.operator('ore.login') + else: + layout.label(text='E-mail and password entered.', icon='INFO') + layout.operator('ore.change_user') + +class CHECK_PT_RenderfarmFi(RenderButtonsPanel): + bl_label = 'Check for updates' + COMPAT_ENGINES = set(['RENDERFARMFI_RENDER']) + + def draw(self, context): + layout = self.layout + ore = context.scene.ore_render + + if bpy.found_newer_version == True: + layout.operator('ore.open_download_location') + else: + if bpy.up_to_date == True: + layout.label(text='You have the latest version') + layout.operator('ore.check_update') + +class SESSIONS_PT_RenderfarmFi(RenderButtonsPanel): + bl_label = 'Sessions' + COMPAT_ENGINES = set(['RENDERFARMFI_RENDER']) + + def draw(self, context): + layout = self.layout + ore = context.scene.ore_render + + layout.menu("RENDERFARM_MT_Session") + if bpy.queue_selected == 1: + layout.label(text='Completed Sessions') + elif bpy.queue_selected == 2: + layout.label(text='Rendering Sessions') + elif bpy.queue_selected == 3: + layout.label(text='Pending Sessions') + elif bpy.queue_selected == 4: + layout.label(text='Cancelled and Rejected Sessions') + layout.template_list(ore, 'sessions', ore, 'selected_session', rows=2) + if bpy.queue_selected == 3: + layout.operator('ore.cancel_session') + +class RENDER_PT_RenderfarmFi(RenderButtonsPanel): + bl_label = "Scene Settings" + COMPAT_ENGINES = set(['RENDERFARMFI_RENDER']) + + def draw(self, context): + layout = self.layout + sce = context.scene + ore = sce.ore_render + + if ore.prepared and ore.hash!='': + layout.prop(ore, 'memusage') + + layout.separator() + row = layout.row() + row.label(text='Resolution: '+str(ore.resox)+'x'+str(ore.resoy)) + + layout.separator() + row = layout.row() + row.prop(ore, 'inlicense') + row.prop(ore, 'outlicense') + + layout.separator() + row = layout.row() + row.operator('ore.upload') + row.operator('ore.reset', icon='FILE_REFRESH') + else: + layout.prop(ore, 'title', icon=bpy.statusMessage['title']) + layout.prop(ore, 'shortdesc', icon=bpy.statusMessage['shortdesc']) + layout.prop(ore, 'longdesc', icon=bpy.statusMessage['longdesc']) + layout.prop(ore, 'url') + layout.separator() + layout.operator('ore.use_scene_settings', icon='HAND') + row = layout.row() + row.prop(ore, 'resox') + row.prop(ore, 'resoy') + layout.separator() + layout.prop(ore, 'parts') + row = layout.row() + row.prop(ore, 'start') + row.prop(ore, 'end') + layout.prop(ore, 'fps') + + layout.separator() + layout.operator('ore.prepare', icon='INFO') + +def random_string(length): + import string + import random + return ''.join(random.choice(string.ascii_letters) for ii in range(length + 1)) + +def encode_multipart_data(data, files): + boundary = random_string(30) + + def get_content_type(filename): + return 'application/octet-stream' # default this + + def encode_field(field_name): + return ('--' + boundary, + 'Content-Disposition: form-data; name="%s"' % field_name, + '', str(data[field_name])) + + def encode_file(field_name): + import codecs + filename = files [field_name] + return ('--' + boundary, + 'Content-Disposition: form-data; name="%s"; filename="%s"' % (field_name, filename), + 'Content-Type: %s' % get_content_type(filename), + '', str(open(filename, 'rb').read(), encoding='iso-8859-1')) + + lines = [] + for name in data: + lines.extend(encode_field(name)) + for name in files: + lines.extend(encode_file(name)) + lines.extend(('--%s--' % boundary, '')) + body = '\r\n'.join(lines) + + headers = {'content-type': 'multipart/form-data; boundary=' + boundary, + 'content-length': str(len(body))} + + return body, headers + +def send_post(url, data, files): + connection = http.client.HTTPConnection('xmlrpc.renderfarm.fi') + connection.request('POST', '/file', *encode_multipart_data(data, files)) + response = connection.getresponse() + res = response.read() + return res + +def md5_for_file(filepath): + md5hash = hashlib.md5() + blocksize = 0x10000 + f = open(filepath, "rb") + while True: + data = f.read(blocksize) + if not data: + break + md5hash.update(data) + return md5hash.hexdigest() + +def upload_file(key, userid, sessionid, server, path): + assert isabs(path) + assert isfile(path) + + data = { + 'userId': str(userid), + 'sessionKey': key, + 'sessionId': sessionid, + 'md5sum': md5_for_file(path) + } + files = { + 'blenderfile': path + } + + r = send_post(server, data, files) + + #print 'Uploaded %r' % (path) + + return r + +def run_upload(key, userid, sessionid, path): + #print('Upload', path) + r = upload_file(key, userid, sessionid, r'http://xmlrpc.renderfarm.fi/file', path) + o = xmlrpc.client.loads(r) + #print('Done!') + + return o[0][0] + +def ore_upload(op, context): + sce = context.scene + ore = sce.ore_render + if not ore.prepared: + op.report(set(['ERROR']), 'Your user or scene information is not complete') + return {'CANCELLED'} + try: + authproxy = xmlrpc.client.ServerProxy(r'https://xmlrpc.renderfarm.fi/auth') + res = authproxy.auth.getSessionKey(ore.username, ore.hash) + key = res['key'] + userid = res['userId'] + + proxy = xmlrpc.client.ServerProxy(r'http://xmlrpc.renderfarm.fi/session') + proxy._ServerProxy__transport.user_agent = 'Renderfarm.fi Uploader/%s' % (bpy.CURRENT_VERSION) + res = proxy.session.createSession(userid, key) + sessionid = res['sessionId'] + key = res['key'] + res = run_upload(key, userid, sessionid, bpy.data.filepath) + fileid = int(res['fileId']) + + res = proxy.session.setTitle(userid, res['key'], sessionid, ore.title) + res = proxy.session.setLongDescription(userid, res['key'], sessionid, ore.longdesc) + res = proxy.session.setShortDescription(userid, res['key'], sessionid, ore.shortdesc) + if len(ore.url)>0: + res = proxy.session.setExternalURLs(userid, res['key'], sessionid, ore.url) + res = proxy.session.setStartFrame(userid, res['key'], sessionid, ore.start) + res = proxy.session.setEndFrame(userid, res['key'], sessionid, ore.end) + res = proxy.session.setSplit(userid, res['key'], sessionid, ore.parts) + res = proxy.session.setMemoryLimit(userid, res['key'], sessionid, ore.memusage) + res = proxy.session.setXSize(userid, res['key'], sessionid, ore.resox) + res = proxy.session.setYSize(userid, res['key'], sessionid, ore.resoy) + res = proxy.session.setFrameRate(userid, res['key'], sessionid, ore.fps) + res = proxy.session.setOutputLicense(userid, res['key'], sessionid, int(ore.outlicense)) + res = proxy.session.setInputLicense(userid, res['key'], sessionid, int(ore.inlicense)) + res = proxy.session.setPrimaryInputFile(userid, res['key'], sessionid, fileid) + res = proxy.session.submit(userid, res['key'], sessionid) + op.report(set(['INFO']), 'Submission sent to Renderfarm.fi') + except xmlrpc.client.Error as v: + print('ERROR:', v) + op.report(set(['ERROR']), 'An error occurred while sending submission to Renderfarm.fi') + except Exception as e: + print('Unhandled error:', e) + op.report(set(['ERROR']), 'An error occurred while sending submission to Renderfarm.fi') + + return {'FINISHED'} + +def setStatus(property, status): + if status: + bpy.statusMessage[property] = 'ERROR' + else: + bpy.statusMessage[property] = 'TRIA_RIGHT' + +def showStatus(layoutform, property, message): + if bpy.statusMessage[property] == 'ERROR': + layoutform.label(text='', icon='ERROR') + +def checkStatus(ore): + bpy.errors = [] + + if ore.hash=='' and (ore.username=='' or ore.password==''): + bpy.errors.append('missing_creds') + + if '' in (ore.title, ore.longdesc, ore.shortdesc): + bpy.errors.append('missing_desc') + + setStatus('username', ore.hash=='' and ore.username=='') + setStatus('password', ore.hash=='' and ore.password=='') + + setStatus('title', ore.title=='') + setStatus('longdesc', ore.longdesc=='') + setStatus('shortdesc', ore.shortdesc=='') + +class OreSession: + + def __init__(self, id, title): + self.id = id + self.title = title + self.frames = 0 + self.startframe = 0 + self.endframe = 0 + self.rendertime = 0 + self.percentage = 0 + + def percentageComplete(self): + totFrames = self.endframe - self.startframe + done = math.floor((self.frames / totFrames)*100) + if done > 100: done = 100 + return done + +def xmlSessionsToOreSessions(sessions, queue): + bpy.ore_sessions = [] + completed = sessions[queue] + for sid in completed: + s = completed[sid]['title'] + t = completed[sid]['timestamps'] + sinfo = OreSession(sid, s) + if queue in ('completed', 'active'): + sinfo.frames = completed[sid]['framesRendered'] + sinfo.startframe = completed[sid]['startFrame'] + sinfo.endframe = completed[sid]['endFrame'] + bpy.ore_sessions.append(sinfo) + +def updateSessionList(ore): + while(len(ore.sessions) > 0): + ore.sessions.remove(0) + + for s in bpy.ore_sessions: + ore.sessions.add() + session = ore.sessions[-1] + session.name = s.title + ' [' + str(s.percentageComplete()) + '% complete]' + +class ORE_OpenDownloadLocation(bpy.types.Operator): + bl_idname = 'ore.open_download_location' + bl_label = 'Download new version for your platform' + + def execute(self, context): + import webbrowser + webbrowser.open(bpy.download_location) + return {'FINISHED'} + +class ORE_CancelSession(bpy.types.Operator): + bl_idname = 'ore.cancel_session' + bl_label = 'Cancel Session' + + def execute(self, context): + sce = context.scene + ore = sce.ore_render + userproxy = xmlrpc.client.ServerProxy(r'https://xmlrpc.renderfarm.fi/user') + if len(bpy.ore_sessions)>0: + s = bpy.ore_sessions[ore.selected_session] + try: + res = userproxy.user.cancelSession(ore.username, ore.hash, int(s.id)) + self.report(set(['INFO']), 'Session ' + s.title + ' with id ' + s.id + ' cancelled') + except: + self.report(set(['ERROR']), 'Could not cancel session ' + s.title + ' with id ' + s.id) + + return {'FINISHED'} + +class ORE_GetCompletedSessions(bpy.types.Operator): + bl_idname = 'ore.completed_sessions' + bl_label = 'Complete' + + def execute(self, context): + sce = context.scene + ore = sce.ore_render + bpy.queue_selected = 1 + userproxy = xmlrpc.client.ServerProxy(r'https://xmlrpc.renderfarm.fi/user') + + sessions = userproxy.user.getAllSessions(ore.username, ore.hash, 'completed') + + xmlSessionsToOreSessions(sessions, 'completed') + + updateSessionList(ore) + + return {'FINISHED'} + +class ORE_GetCancelledSessions(bpy.types.Operator): + bl_idname = 'ore.cancelled_sessions' + bl_label = 'Cancelled' + + def execute(self, context): + sce = context.scene + ore = sce.ore_render + bpy.queue_selected = 4 + userproxy = xmlrpc.client.ServerProxy(r'https://xmlrpc.renderfarm.fi/user') + + sessions = userproxy.user.getAllSessions(ore.username, ore.hash, 'completed') + + xmlSessionsToOreSessions(sessions, 'canceled') + + updateSessionList(ore) + + return {'FINISHED'} + +class ORE_GetActiveSessions(bpy.types.Operator): + bl_idname = 'ore.active_sessions' + bl_label = 'Rendering' + + def execute(self, context): + sce = context.scene + ore = sce.ore_render + bpy.queue_selected = 2 + userproxy = xmlrpc.client.ServerProxy(r'https://xmlrpc.renderfarm.fi/user') + + sessions = userproxy.user.getAllSessions(ore.username, ore.hash, 'active') + + xmlSessionsToOreSessions(sessions, 'active') + + updateSessionList(ore) + + return {'FINISHED'} + +class ORE_GetPendingSessions(bpy.types.Operator): + bl_idname = 'ore.accept_sessions' # using ORE lingo in API. acceptQueue is session waiting for admin approval + bl_label = 'Pending' + + def execute(self, context): + sce = context.scene + ore = sce.ore_render + bpy.queue_selected = 3 + userproxy = xmlrpc.client.ServerProxy(r'https://xmlrpc.renderfarm.fi/user') + + sessions = userproxy.user.getAllSessions(ore.username, ore.hash, 'accept') + + xmlSessionsToOreSessions(sessions, 'accept') + + updateSessionList(ore) + + return {'FINISHED'} + +class ORE_CheckUpdate(bpy.types.Operator): + bl_idname = 'ore.check_update' + bl_label = 'Check for new version' + + def execute(self, context): + sce = context.scene + ore = sce.ore_render + blenderproxy = xmlrpc.client.ServerProxy(r'http://xmlrpc.renderfarm.fi/blender') + try: + self.report(set(['INFO']), 'Checking for newer version on Renderfarm.fi') + dl_url = blenderproxy.blender.getCurrentVersion(bpy.CURRENT_VERSION) + if len(dl_url['url']) > 0: + self.report(set(['INFO']), 'Found a newer version on Renderfarm.fi ' + dl_url['url']) + bpy.download_location = dl_url['url'] + bpy.found_newer_version = True + else: + bpy.up_to_date = True + self.report(set(['INFO']), 'Done checking for newer version on Renderfarm.fi') + except xmlrpc.client.Fault as f: + print('ERROR:', f) + self.report(set(['ERROR']), 'An error occurred while checking for newer version on Renderfarm.fi') + + return {'FINISHED'} + +class ORE_LoginOp(bpy.types.Operator): + bl_idname = 'ore.login' + bl_label = 'Confirm credentials' + + def execute(self, context): + sce = context.scene + ore = sce.ore_render + + if ore.hash=='': + if ore.password != '' and ore.username != '': + ore.hash = hashlib.md5(ore.password.encode() + ore.username.encode()).hexdigest() + ore.password = '' + + checkStatus(ore) + + if len(bpy.errors) > 0: + ore.prepared = False + return {'CANCELLED'} + + return {'FINISHED'} + +class ORE_PrepareOp(bpy.types.Operator): + '''Checking the scene will also save to the current file when successful!''' + bl_idname = 'ore.prepare' + bl_label = 'Check scene' + + def execute(self, context): + def hasSSSMaterial(): + for m in bpy.data.materials: + if m.subsurface_scattering.enabled: + return True + return False + + def hasParticleSystem(): + if len(bpy.data.particles) > 0: + self.report({'WARNING'}, "Found particle system") + print("Found particle system") + return True + return False + + def hasSimulation(t): + for o in bpy.data.objects: + for m in o.modifiers: + if isinstance(m, t): + self.report({'WARNING'}, "Found simulation: " + str(t)) + print("Found simulation: " + str(t)) + return True + return False + + def hasFluidSimulation(): + return hasSimulation(bpy.types.FluidSimulationModifier) + + def hasSmokeSimulation(): + return hasSimulation(bpy.types.SmokeModifier) + + def hasClothSimulation(): + return hasSimulation(bpy.types.ClothModifier) + + def hasCollisionSimulation(): + return hasSimulation(bpy.types.CollisionModifier) + + def hasSoftbodySimulation(): + return hasSimulation(bpy.types.SoftBodyModifier) + + def hasUnsupportedSimulation(): + return hasSoftbodySimulation() or hasCollisionSimulation() or hasClothSimulation() or hasSmokeSimulation or hasFluidSimulation() or hasParticleSystem() + + sce = context.scene + ore = sce.ore_render + + errors = False + + checkStatus(ore) + + if len(bpy.errors) > 0: + ore.prepared = False + return {'CANCELLED'} + + rd = sce.render + print("=============================================") + rd.threads_mode = 'FIXED' + rd.threads = 1 + rd.resolution_x = ore.resox + rd.resolution_y = ore.resoy + if (rd.resolution_percentage != 100): + print("Resolution percentage is not 100. Changing to 100%") + self.report({'WARNING'}, "Resolution percentage is not 100. Changing to 100%") + errors = True + rd.resolution_percentage = 100 + if rd.file_format != 'PNG': + print("Renderfarm.fi always uses PNG for output. Changing to PNG.") + self.report({'WARNING'}, "Renderfarm.fi always uses PNG for output. Changing to PNG.") + errors = True + rd.file_format = 'PNG' + if rd.color_mode != 'RGBA': + print("Color mode must be set to RGBA. Changing to RGBA.") + self.report({'WARNING'}, "Color mode must be set to RGBA. Changing to RGBA.") + errors = True + rd.color_mode = 'RGBA' + if (rd.use_sss == True or hasSSSMaterial())and ore.parts > 1: + print("Subsurface Scattering is not supported when rendering with parts > 1. Disabling") + self.report({'WARNING'}, "Subsurface Scattering is not supported when rendering with parts > 1. Disabling") + errors = True + if hasUnsupportedSimulation() == True: + print("An unsupported simulation was detected. Please check your settings and remove them") + self.report({'WARNING'}, "An unsupported simulation was detected. Please check your settings and remove them") + errors = True + rd.use_sss = False + rd.save_buffers = False + rd.free_image_textures = True + print("Done checking the scene. Now do a test render") + self.report({'INFO'}, "Done checking the scene. Now do a test render") + print("=============================================") + + if (errors == True): + self.report({'WARNING'}, "Settings were changed or other issues found. Check console and do a test render to make sure everything works.") + + ore.prepared = True + rd.engine = 'BLENDER_RENDER' + bpy.ops.wm.save_mainfile() + rd.engine = 'RENDERFARMFI_RENDER' + + return {'FINISHED'} + +class ORE_ResetOp(bpy.types.Operator): + bl_idname = "ore.reset" + bl_label = "Reset Preparation" + + def execute(self, context): + sce = context.scene + sce.ore_render.prepared = False + return {'FINISHED'} + +class ORE_UploaderOp(bpy.types.Operator): + bl_idname = "ore.upload" + bl_label = "Render on Renderfarm.fi" + + def execute(self, context): + rd = context.scene.render + rd.engine = 'BLENDER_RENDER' + bpy.ops.wm.save_mainfile() + return ore_upload(self, context) + +class ORE_UseBlenderReso(bpy.types.Operator): + bl_idname = "ore.use_scene_settings" + bl_label = "Use Scene resolution" + + def execute(self, context): + sce = context.scene + ore = sce.ore_render + + ore.resox = sce.render.resolution_x + ore.resoy = sce.render.resolution_y + + return {'FINISHED'} + +class ORE_ChangeUser(bpy.types.Operator): + bl_idname = "ore.change_user" + bl_label = "Change user" + + def execute(self, context): + ore = context.scene.ore_render + ore.password = '' + ore.hash = '' + + return {'FINISHED'} + +class RenderfarmFi(bpy.types.RenderEngine): + bl_idname = 'RENDERFARMFI_RENDER' + bl_label = "Renderfarm.fi" + + def render(self, scene): + print('Do test renders with Blender Render') + +def menu_export(self, context): + import os + default_path = os.path.splitext(bpy.data.filepath)[0] + ".py" + self.layout.operator(RenderfarmFi.bl_idname, text=RenderfarmFi.bl_label) + +def register(): + bpy.types.register(RenderfarmFi) + bpy.types.register(ORE_OpenDownloadLocation) + bpy.types.register(ORE_CancelSession) + bpy.types.register(ORE_GetCompletedSessions) + bpy.types.register(ORE_GetCancelledSessions) + bpy.types.register(ORE_GetActiveSessions) + bpy.types.register(ORE_GetPendingSessions) + bpy.types.register(ORE_CheckUpdate) + bpy.types.register(ORE_LoginOp) + bpy.types.register(ORE_PrepareOp) + bpy.types.register(ORE_ResetOp) + bpy.types.register(ORE_UploaderOp) + bpy.types.register(ORE_UseBlenderReso) + bpy.types.register(ORE_ChangeUser) + bpy.types.register(RENDERFARM_MT_Session) + bpy.types.register(LOGIN_PT_RenderfarmFi) + bpy.types.register(CHECK_PT_RenderfarmFi) + bpy.types.register(SESSIONS_PT_RenderfarmFi) + bpy.types.register(RENDER_PT_RenderfarmFi) + bpy.types.INFO_MT_render.append(menu_export) + +def unregister(): + bpy.types.unregister(ORESession) + bpy.types.unregister(ORESettings) + +if __name__ == "__main__": + register() diff --git a/space_view3d_align_tools.py b/space_view3d_align_tools.py new file mode 100644 index 00000000..95f8843c --- /dev/null +++ b/space_view3d_align_tools.py @@ -0,0 +1,361 @@ +# AlingTools.py (c) 2009, 2010 Gabriel Beaudin (gabhead) +# +# ***** 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +"""Align Selected Objects""" + +bl_addon_info = { + 'name': '3D View: Align Tools', + 'author': 'Gabriel Beaudin (gabhead)', + 'version': '0.1', + 'blender': (2, 5, 3), + 'location': 'Tool Shelf', + 'description': 'Align selected objects to the active object', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/3D interaction/Align_Tools', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid==22389&group_id=153&atid=468', + 'category': '3D View'} + +import bpy + +##interface +###################### +class View3DPanel(bpy.types.Panel): + bl_space_type = 'VIEW_3D' + bl_region_type = 'TOOLS' + +class AlignUi(View3DPanel): + bl_label = "Align Tools" + bl_context = "objectmode" + + + def draw(self, context): + layout = self.layout + obj = context.object + + if obj != None: + row = layout.row() + row.label(text="Active object is: ", icon='OBJECT_DATA') + row = layout.row() + row.label(obj.name, icon='EDITMODE_HLT') + + box = layout.separator() + + col = layout.column() + col.label(text="Align Loc + Rot:", icon='MANIPUL') + + + col = layout.column(align=False) + col.operator("object.AlignObjects",text="XYZ") + + col = layout.column() + col.label(text="Align Location:", icon='MAN_TRANS') + + col = layout.column_flow(columns=5,align=True) + col.operator("object.AlignObjectsLocationX",text="X") + col.operator("object.AlignObjectsLocationY",text="Y") + col.operator("object.AlignObjectsLocationZ",text="Z") + col.operator("object.AlignObjectsLocationAll",text="All") + + col = layout.column() + col.label(text="Align Rotation:", icon='MAN_ROT') + + col = layout.column_flow(columns=5,align=True) + col.operator("object.AlignObjectsRotationX",text="X") + col.operator("object.AlignObjectsRotationY",text="Y") + col.operator("object.AlignObjectsRotationZ",text="Z") + col.operator("object.AlignObjectsRotationAll",text="All") + + col = layout.column() + col.label(text="Align Scale:", icon='MAN_SCALE') + + col = layout.column_flow(columns=5,align=True) + col.operator("object.AlignObjectsScaleX",text="X") + col.operator("object.AlignObjectsScaleY",text="Y") + col.operator("object.AlignObjectsScaleZ",text="Z") + col.operator("object.AlignObjectsScaleAll",text="All") + + + + +##Ops +################## + +## Def + +##Align all +def main(context): + for i in bpy.context.selected_objects: + i.location = bpy.context.active_object.location + i.rotation_euler = bpy.context.active_object.rotation_euler + +## Align Location + +def LocAll(context): + for i in bpy.context.selected_objects: + i.location = bpy.context.active_object.location + +def LocX(context): + for i in bpy.context.selected_objects: + i.location.x = bpy.context.active_object.location.x + +def LocY(context): + for i in bpy.context.selected_objects: + i.location.y = bpy.context.active_object.location.y + +def LocZ(context): + for i in bpy.context.selected_objects: + i.location.z = bpy.context.active_object.location.z + +## Aling Rotation +def RotAll(context): + for i in bpy.context.selected_objects: + i.rotation_euler = bpy.context.active_object.rotation_euler + +def RotX(context): + for i in bpy.context.selected_objects: + i.rotation_euler.x = bpy.context.active_object.rotation_euler.x + +def RotY(context): + for i in bpy.context.selected_objects: + i.rotation_euler.y = bpy.context.active_object.rotation_euler.y + +def RotZ(context): + for i in bpy.context.selected_objects: + i.rotation_euler.z = bpy.context.active_object.rotation_euler.z +## Aling Scale +def ScaleAll(context): + for i in bpy.context.selected_objects: + i.scale = bpy.context.active_object.scale + +def ScaleX(context): + for i in bpy.context.selected_objects: + i.scale.x = bpy.context.active_object.scale.x + +def ScaleY(context): + for i in bpy.context.selected_objects: + i.scale.y = bpy.context.active_object.scale.y + +def ScaleZ(context): + for i in bpy.context.selected_objects: + i.scale.z = bpy.context.active_object.scale.z + +## Classes + +## Align All Rotation And Location +class AlignOperator(bpy.types.Operator): + '''''' + bl_idname = "object.AlignObjects" + bl_label = "Align Selected To Active" + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + main(context) + return {'FINISHED'} + +#######################Align Location######################## +## Align LocationAll +class AlignLocationOperator(bpy.types.Operator): + '''''' + bl_idname = "object.AlignObjectsLocationAll" + bl_label = "Align Selected Location To Active" + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + LocAll(context) + return {'FINISHED'} +## Align LocationX +class AlignLocationXOperator(bpy.types.Operator): + '''''' + bl_idname = "object.AlignObjectsLocationX" + bl_label = "Align Selected Location X To Active" + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + LocX(context) + return {'FINISHED'} +## Align LocationY +class AlignLocationYOperator(bpy.types.Operator): + '''''' + bl_idname = "object.AlignObjectsLocationY" + bl_label = "Align Selected Location Y To Active" + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + LocY(context) + return {'FINISHED'} +## Align LocationZ +class AlignLocationZOperator(bpy.types.Operator): + '''''' + bl_idname = "object.AlignObjectsLocationZ" + bl_label = "Align Selected Location Z To Active" + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + LocZ(context) + return {'FINISHED'} + +#######################Align Rotation######################## +## Align RotationAll +class AlignRotationOperator(bpy.types.Operator): + '''''' + bl_idname = "object.AlignObjectsRotationAll" + bl_label = "Align Selected Rotation To Active" + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + RotAll(context) + return {'FINISHED'} +## Align RotationX +class AlignRotationXOperator(bpy.types.Operator): + '''''' + bl_idname = "object.AlignObjectsRotationX" + bl_label = "Align Selected Rotation X To Active" + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + RotX(context) + return {'FINISHED'} +## Align RotationY +class AlignRotationYOperator(bpy.types.Operator): + '''''' + bl_idname = "object.AlignObjectsRotationY" + bl_label = "Align Selected Rotation Y To Active" + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + RotY(context) + return {'FINISHED'} +## Align RotationZ +class AlignRotationZOperator(bpy.types.Operator): + '''''' + bl_idname = "object.AlignObjectsRotationZ" + bl_label = "Align Selected Rotation Z To Active" + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + RotZ(context) + return {'FINISHED'} +#######################Align Scale######################## +## Scale All +class AlignScaleOperator(bpy.types.Operator): + '''''' + bl_idname = "object.AlignObjectsScaleAll" + bl_label = "Align Selected Scale To Active" + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + ScaleAll(context) + return {'FINISHED'} +## Align ScaleX +class AlignScaleXOperator(bpy.types.Operator): + '''''' + bl_idname = "object.AlignObjectsScaleX" + bl_label = "Align Selected Scale X To Active" + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + ScaleX(context) + return {'FINISHED'} +## Align ScaleY +class AlignScaleYOperator(bpy.types.Operator): + '''''' + bl_idname = "object.AlignObjectsScaleY" + bl_label = "Align Selected Scale Y To Active" + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + ScaleY(context) + return {'FINISHED'} +## Align ScaleZ +class AlignScaleZOperator(bpy.types.Operator): + '''''' + bl_idname = "object.AlignObjectsScaleZ" + bl_label = "Align Selected Scale Z To Active" + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + ScaleZ(context) + return {'FINISHED'} + +## registring +def register(): + bpy.types.register(AlignUi) + bpy.types.register(AlignOperator) + bpy.types.register(AlignLocationOperator) + bpy.types.register(AlignLocationXOperator) + bpy.types.register(AlignLocationYOperator) + bpy.types.register(AlignLocationZOperator) + bpy.types.register(AlignRotationOperator) + bpy.types.register(AlignRotationXOperator) + bpy.types.register(AlignRotationYOperator) + bpy.types.register(AlignRotationZOperator) + bpy.types.register(AlignScaleOperator) + bpy.types.register(AlignScaleXOperator) + bpy.types.register(AlignScaleYOperator) + bpy.types.register(AlignScaleZOperator) + + +def unregister(): + bpy.types.unregister(AlignUi) + bpy.types.unregister(AlignOperator) + bpy.types.unregister(AlignLocationOperator) + bpy.types.unregister(AlignLocationXOperator) + bpy.types.unregister(AlignLocationYOperator) + bpy.types.unregister(AlignLocationZOperator) + bpy.types.unregister(AlignRotationOperator) + bpy.types.unregister(AlignRotationXOperator) + bpy.types.unregister(AlignRotationYOperator) + bpy.types.unregister(AlignRotationZOperator) + bpy.types.unregister(AlignScaleOperator) + bpy.types.unregister(AlignScaleXOperator) + bpy.types.unregister(AlignScaleYOperator) + bpy.types.unregister(AlignScaleZOperator) + +if __name__ == "__main__": + register() diff --git a/space_view3d_materials_utils.py b/space_view3d_materials_utils.py new file mode 100644 index 00000000..dca94e5c --- /dev/null +++ b/space_view3d_materials_utils.py @@ -0,0 +1,704 @@ +#(c) 2010 Michael Williamson (michaelw) +#ported from original by Michael Williamsn +# +#tested r28370 +# +# +# ##### 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 ##### + +""" +This script has several functions and operators... grouped for convenience +* assign material: + offers the user a list of ALL the materials in the blend file and an additional "new" entry + the chosen material will be assigned to all the selected objects in object mode. + + in edit mode the selected faces get the selected material applied. + + if the user chose "new" the new material can be renamed using the "last operator" section of the toolbox + After assigning the material "clean material slots" and "material to texface" are auto run to keep things tidy (see description bellow) + + +* select by material + in object mode this offers the user a menu of all materials in the blend file + any objects using the selected material will become selected, any objects without the material will be removed from selection. + + in edit mode: the menu offers only the materials attached to the current object. It will select the faces that use the material and deselect those that do not. + +* clean material slots + for all selected objects any empty material slots or material slots with materials that are not used by the mesh faces will be removed. + +* Any un-used materials and slots will be removed +""" + + +bl_addon_info = { + 'name': '3D View: Material Utils', + 'author': 'michaelw', + 'version': '1.3', + 'blender': (2, 5, 3), + 'location': 'View3D > Q key', + 'description': 'Menu of material tools (assign, select by etc) in the 3D View', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/3D interaction/Materials Utils', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=22140&group_id=153&atid=469', + 'category': '3D View'} + + +import bpy +from bpy.props import* + + +def replace_material(m1 , m2, all_objects = False): + #replace material named m1 with material named m2 + #m1 is the name of original material + #m2 is the name of the material to replace it with + #'all' will replace throughout the blend file + try: + matorg = bpy.data.materials[m1] + matrep = bpy.data.materials[m2] + + + #store active object + scn = bpy.context.scene + ob_active = bpy.context.active_object + + if all_objects: + objs = bpy.data.objects + + else: + objs = bpy.context.selected_editable_objects + + for ob in objs: + if ob.type == 'MESH': + scn.objects.active = ob + print(ob.name) + ms = ob.material_slots.values() + + for m in ms: + if m.material == matorg: + m.material = matrep + #don't break the loop as the material can be + # ref'd more than once + + #restore active object + scn.objects.active = ob_active + except: + print('no match to replace') + +def select_material_by_name(find_mat): + #in object mode selects all objects with material find_mat + #in edit mode selects all faces with material find_mat + + #check for editmode + editmode = False + + scn = bpy.context.scene + actob = bpy.context.active_object + if actob.mode == 'EDIT': + editmode =True + bpy.ops.object.mode_set() + + + if not editmode: + objs = bpy.data.objects + for ob in objs: + if ob.type == 'MESH': + ms = ob.material_slots.values() + for m in ms: + if m.material.name == find_mat: + ob.select = True + #the active object may not have the mat! + #set it to one that does! + scn.objects.active = ob + break + else: + ob.select = False + + #deselect non-meshes + else: + ob.select = False + + else: + #it's editmode, so select the faces + ob = actob + ms = ob.material_slots.values() + + #same material can be on multiple slots + slot_indeces =[] + i = 0 + found = False + for m in ms: + if m.material.name == find_mat: + slot_indeces.append(i) + found = True + me = ob.data + for f in me.faces: + if f.material_index in slot_indeces: + f.select = True + else: + f.select = False + me.update + if editmode: + bpy.ops.object.mode_set(mode = 'EDIT') + +def mat_to_texface(): + #assigns the first image in each material to the faces in the active uvlayer + #for all selected objects + + #check for editmode + editmode = False + + actob = bpy.context.active_object + if actob.mode == 'EDIT': + editmode =True + bpy.ops.object.mode_set() + + for ob in bpy.context.selected_editable_objects: + #get the materials from slots + ms = ob.material_slots.values() + + #build a list of images, one per material + images=[] + #get the textures from the mats + for m in ms: + gotimage = False + textures = m.material.texture_slots.values() + if len(textures) >= 1: + for t in textures: + if t != None: + tex = t.texture + if tex.type == 'IMAGE': + img = tex.image + images.append(img) + gotimage =True + break + + if not gotimage: + print('noimage on', m.name) + images.append(None) + + #now we have the images + #applythem to the uvlayer + + + me = ob.data + #got uvs? + if not me.uv_textures: + scn = bpy.context.scene + scn.objects.active = ob + bpy.ops.mesh.uv_texture_add() + scn.objects.active = actob + + #get active uvlayer + for t in me.uv_textures: + if t.active: + uvtex = t.data.values() + for f in me.faces: + #check that material had an image! + if images[f.material_index] != None: + uvtex[f.index].image = images[f.material_index] + uvtex[f.index].tex =True + else: + uvtex[f.index].tex =False + + me.update + + + if editmode: + bpy.ops.object.mode_set(mode = 'EDIT') + + + +def assignmatslots(ob, matlist): + #given an object and a list of material names + #removes all material slots form the object + #adds new ones for each material in matlist + #adds the materials to the slots as well. + + scn = bpy.context.scene + ob_active = bpy.context.active_object + scn.objects.active = ob + + for s in ob.material_slots: + bpy.ops.object.material_slot_remove() + + + #re-add them and assign material + i = 0 + for m in matlist: + mat = bpy.data.materials[m] + bpy.ops.object.material_slot_add() + ob.material_slots.values()[i].material = mat + i += 1 + + #restore active object: + scn.objects.active = ob_active + + +def cleanmatslots(): + #check for edit mode + editmode = False + actob = bpy.context.active_object + if actob.mode == 'EDIT': + editmode =True + bpy.ops.object.mode_set() + + + objs = bpy.context.selected_editable_objects + + for ob in objs: + print(ob.name) + mats = ob.material_slots.keys() + + #check the faces on the mesh to build a list of used materials + usedMatIndex =[] #we'll store used materials indices here + faceMats =[] + me = ob.data + for f in me.faces: + #get the material index for this face... + faceindex = f.material_index + + #indices will be lost: Store face mat use by name + currentfacemat = mats[faceindex] + faceMats.append(currentfacemat) + + + #check if index is already listed as used or not + found = 0 + for m in usedMatIndex: + if m == faceindex: + found = 1 + #break + + if found == 0: + #add this index to the list + usedMatIndex.append(faceindex) + + #re-assign the used mats to the mesh and leave out the unused + ml = [] + mnames = [] + for u in usedMatIndex: + ml.append( mats[u] ) + #we'll need a list of names to get the face indices... + mnames.append(mats[u]) + + assignmatslots(ob, ml) + + + #restore face indices: + i = 0 + for f in me.faces: + matindex = mnames.index(faceMats[i]) + f.material_index = matindex + i += 1 + print('Done') + if editmode: + bpy.ops.object.mode_set(mode = 'EDIT') + + + + + +def assign_mat(matname="Default"): + #get active object so we can restore it later + actob = bpy.context.active_object + + #check if material exists, if it doesn't then create it + mats =bpy.data.materials + found = False + for m in mats: + if m.name == matname: + target = m + found = True + break + if not found: + target = bpy.data.materials.new(matname) + + + #if objectmodeset all faces + editmode = False + allfaces = True + if actob.mode == 'EDIT': + editmode =True + allfaces = False + bpy.ops.object.mode_set() + + objs = bpy.context.selected_editable_objects + + for ob in objs: + #set the active object to our object + scn = bpy.context.scene + scn.objects.active = ob + + + #check if the material is on the object already + if ob.type =='MESH': + #check material slots for matname material + found=False + i = 0 + mats = ob.material_slots + for m in mats: + if m.name == matname: + found =True + index = i + #make slot active + ob.active_material_index = i + break + i += 1 + + if not found: + index=i + #the material is not attached to the object + #so attach it! + + #add a material slot + bpy.ops.object.material_slot_add() + + #make slot active + ob.active_material_index = i + + #and assign material to slot + ob.material_slots.values()[i].material = target + #now assign the material: + me =ob.data + if allfaces: + for f in me.faces: + f.material_index = index + elif allfaces == False: + for f in me.faces: + if f.select: + f.material_index = index + me.update + + #restore the active object + bpy.context.scene.objects.active = actob + if editmode: + bpy.ops.object.mode_set(mode = 'EDIT') + + + +def check_texture(img,mat): + #finds a texture from an image + #makes a texture if needed + #adds it to the material if it isn't there already + + try: + tex = bpy.data.textures[img.name] + except: + tex = bpy.data.textures.new(name=img.name) + finally: + tex.type = 'IMAGE' + tex = tex.recast_type() + tex.image = img + + #see if the material already uses this tex + #add it if needed + found = False + for m in mat.texture_slots: + if m and m.texture == tex: + found = True + break + if not found and mat: + mat.add_texture(tex, texture_coordinates='UV', map_to='COLOR') + +def texface_to_mat(): + # editmode check here! + editmode = False + ob = bpy.context.object + if ob.mode =='EDIT': + editmode = True + bpy.ops.object.mode_set() + + for ob in bpy.context.selected_editable_objects: + + faceindex = [] + unique_images = [] + + # get the texface images and store indices + if (ob.data.uv_textures): + for f in ob.data.active_uv_texture.data: + if f.image: + img = f.image + #build list of unique images + if img not in unique_images: + unique_images.append(img) + faceindex.append(unique_images.index(img)) + + else: + img = None + faceindex.append(None) + + + + #check materials for images exist; create if needed + matlist = [] + for i in unique_images: + if i: + print(i.name) + try: + m = bpy.data.materials[i.name] + + except: + m = bpy.data.materials.new(name = i.name) + continue + + finally: + matlist.append(m.name) + # add textures if needed + check_texture(i,m) + + #set up the object material slots + assignmatslots(ob, matlist) + + #set texface indices to material slot indices.. + me = ob.data + + i = 0 + for f in faceindex: + if f != None: + me.faces[i].material_index = f + i += 1 + if editmode: + bpy.ops.object.mode_set(mode = 'EDIT') + + +#operator classes: +#--------------------------------------------------------------------- + +class VIEW3D_OT_texface_to_material(bpy.types.Operator): + '''''' + bl_idname = "texface_to_material" + bl_label = "MW Texface Images to Material/Texture" + bl_options = {'REGISTER', 'UNDO'} + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + if context.selected_editable_objects: + texface_to_mat() + return {'FINISHED'} + else: + self.report({'WARNING'}, "No editable selected objects, could not finish") + return {'CANCELLED'} + +class VIEW3D_OT_assign_material(bpy.types.Operator): + '''assign a material to the selection''' + bl_idname = "assign_material" + bl_label = "MW Assign Material" + bl_options = {'REGISTER', 'UNDO'} + + matname = StringProperty(name = 'Material Name', + description = 'Name of Material to Assign', + default = "", maxlen = 21) + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + mn = self.properties.matname + print(mn) + assign_mat(mn) + cleanmatslots() + mat_to_texface() + return {'FINISHED'} + +class VIEW3D_OT_clean_material_slots(bpy.types.Operator): + '''removes any material slots from the + selected objects that are not used by the mesh''' + bl_idname = "clean_material_slots" + bl_label = "MW Clean Material Slots" + bl_options = {'REGISTER', 'UNDO'} + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + cleanmatslots() + return {'FINISHED'} + +class VIEW3D_OT_material_to_texface(bpy.types.Operator): + '''''' + bl_idname = "material_to_texface" + bl_label = "MW Material Images to Texface" + bl_options = {'REGISTER', 'UNDO'} + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + mat_to_texface() + return {'FINISHED'} + +class VIEW3D_OT_select_material_by_name(bpy.types.Operator): + '''''' + bl_idname = "select_material_by_name" + bl_label = "MW Select Material By Name" + bl_options = {'REGISTER', 'UNDO'} + matname = StringProperty(name = 'Material Name', + description = 'Name of Material to Select', + default = "", maxlen = 21) + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + mn = self.properties.matname + select_material_by_name(mn) + return {'FINISHED'} + + +class VIEW3D_OT_replace_material(bpy.types.Operator): + '''assign a material to the selection''' + bl_idname = "replace_material" + bl_label = "MW Replace Material" + bl_options = {'REGISTER', 'UNDO'} + + matorg = StringProperty(name = 'Material to Replace', + description = 'Name of Material to Assign', + default = "", maxlen = 21) + + matrep = StringProperty(name = 'Replacement material', + description = 'Name of Material to Assign', + default = "", maxlen = 21) + + all_objects = BoolProperty(name ='all_objects', + description="replace for all objects in this blend file", + default = True) + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + m1 = self.properties.matorg + m2 = self.properties.matrep + all = self.properties.all_objects + replace_material(m1,m2,all) + return {'FINISHED'} + +#menu classes +#------------------------------------------------------------------------------- +class VIEW3D_MT_master_material(bpy.types.Menu): + bl_label = "Master Material Menu" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.menu("VIEW3D_MT_assign_material", icon='ZOOMIN') + layout.menu("VIEW3D_MT_select_material", icon='HAND') + layout.separator() + layout.operator("clean_material_slots", + text = 'Clean Material Slots', icon='CANCEL') + layout.operator("material_to_texface", + text = 'Material to Texface',icon='FACESEL_HLT') + layout.operator("texface_to_material", + text = 'Texface to Material',icon='FACESEL_HLT') + + layout.separator() + layout.operator("replace_material", + text = 'Replace Material', icon='ARROW_LEFTRIGHT') + + + +class VIEW3D_MT_assign_material(bpy.types.Menu): + bl_label = "Assign Material" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + ob = context + layout.label + for i in range (len(bpy.data.materials)): + + layout.operator("assign_material", + text=bpy.data.materials[i].name, + icon='MATERIAL_DATA').matname = bpy.data.materials[i].name + + layout.operator("assign_material",text="Add New", + icon='ZOOMIN') + +class VIEW3D_MT_select_material(bpy.types.Menu): + bl_label = "Select by Material" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + ob = context.object + layout.label + if ob.mode == 'OBJECT': + #show all materials in entire blend file + for i in range (len(bpy.data.materials)): + + layout.operator("select_material_by_name", + text=bpy.data.materials[i].name, + icon='MATERIAL_DATA').matname = bpy.data.materials[i].name + + + elif ob.mode == 'EDIT': + #show only the materials on this object + mats = ob.material_slots.keys() + for m in mats: + layout.operator("select_material_by_name", + text=m, + icon='MATERIAL_DATA').matname = m + + + + + + +classes = [ +VIEW3D_OT_assign_material, +VIEW3D_OT_clean_material_slots, +VIEW3D_OT_material_to_texface, +VIEW3D_OT_select_material_by_name, +VIEW3D_OT_replace_material, +VIEW3D_OT_texface_to_material, +VIEW3D_MT_master_material, +VIEW3D_MT_assign_material, +VIEW3D_MT_select_material] + +def register(): + register = bpy.types.register + for cls in classes: + register(cls) + + km = bpy.context.manager.active_keyconfig.keymaps['3D View'] + kmi = km.items.add('wm.call_menu', 'Q', 'PRESS') + kmi.properties.name = "VIEW3D_MT_master_material" + +def unregister(): + unregister = bpy.types.unregister + for cls in classes: + unregister(cls) + + km = bpy.context.manager.active_keyconfig.keymaps['3D View'] + for kmi in km.items: + if kmi.idname == 'wm.call_menu': + if kmi.properties.name == "VIEW3D_MT_master_material": + km.remove_item(kmi) + break + +if __name__ == "__main__": + register() diff --git a/space_view3d_panel_measure.py b/space_view3d_panel_measure.py new file mode 100644 index 00000000..cbe68b09 --- /dev/null +++ b/space_view3d_panel_measure.py @@ -0,0 +1,989 @@ +# ##### 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 ##### + + +""" +Measure panel + +This script displays in OBJECT MODE: +* The distance of the 3D cursor to the origin of the + 3D space (if NOTHING is selected). +* The distance of the 3D cursor to the center of an object + (if exactly ONE object is selected). +* The distance between 2 object centers + (if exactly TWO objects are selected). +* The surface area of any selected mesh object. + +Display in EDIT MODE (Local and Global space supported): +* The distance of the 3D cursor to the origin + (in Local space it is the object center instead). +* The distance of the 3D cursor to a selected vertex. +* The distance between 2 selected vertices. + +Usage: + +This functionality can be accessed via the +"Properties" panel in 3D View ([N] key). + +It's very helpful to use one or two "Empty" objects with +"Snap during transform" enabled for fast measurement. + +Version history: +v0.7 - Initial support for drawing lines. + (Thanks to Algorith for applying my perspective_matrix patch.) + The distance value (in BUs) is also drawn in the 3D view now. + Also fixed some wrong calculations of global/local distances. + Now it's really "what you see is what is calculated". + Use bl_addon_info for Add-On information. + Use "3D View" in category & name + Renamed reenter_editmode to view3d.reenter_editmode. + Renamed panel_measure.py into space_view3d_panel_measure.py + Active object is only used for edit-mode now. Measurement + with exactly one sel. (but not neccessarily active) object + now gets the obj via the sel-object array. + API change Mathutils -> mathutils (r557) + Deselecting 1 of 2 objects now works correctly (active object is ignored). + Force a redraw of the area so disabling the "measure_panel_draw" + checkbox will clear the line/text. + Only calculate area (CPU heavy) if a "area" checkbox is enabled. +v0.6.4 - Fixed unneeded meshdata duplication (sometimes crashes Blender). + The script now correctly calculated the surface area (faceAreaGlobal) + of scaled meshes. + http://projects.blender.org/tracker/ + ?func=detail&atid=453&aid=21913&group_id=153 +v0.6.3 - Added register & unregister functions. +v0.6.2 - Fixed precision of second area property. + Reduced display precision to 5 (instead of 6). + Added (commented out code) for shortcut [F5] for + updating EditMode selection & calculation. + Changed the script so it can be managed from the "Add-Ons" tab + in the user preferences. + Corrected FSF address. +v0.6.1 - Updated reenter_editmode operator description. + Fixed search for selected mesh objects. + Added "BU^2" after values that are not yet translated via "unit". +v0.6 + *) Fix: Removed EditMode/ObjectMode toggle stuff. This causes all the + crashes and is generally not stable. + Instead I've added a manual "refresh" button. + I registered a new operator OBJECT_OT_reenter_editmode for this. + *) Use "unit" settings (i.e. none/metric/imperial) + *) Fix: Only display surface area (>=3 objects) if return value is >=0. + *) Minor: Renamed objectFaceArea to objectSurfaceArea + *) Updated Vector() and tuple() usage. + *) Fixed some comments. +v0.5 - Global surface area (object mode) is now calculated as well. + Support area calculation for face selection. + Also made measurement panel closed by default. (Area calculation + may use up a lot of CPU/RAM in extreme cases) +v0.4.1 - Various cleanups. + Using the shorter "scene" instead of "context.scene" + New functions measureGlobal() and measureLocal() for + user-friendly access to the "space" setting. +v0.4 - Calculate & display the surface area of mesh + objects (local space only right now). + Expanded global/local switch. + Made "local" option for 3Dcursor-only in edit mode actually work. + Fixed local/global calculation for 3Dcursor<->vertex in edit mode. +v0.3.2 - Fixed calculation & display of local/global coordinates. + The user can now select via dropdown which space is wanted/needed + Basically this is a bugfix and new feature at the same time :-) +v0.3.1 - Fixed bug where "measure_panel_dist" wasn't defined + before it was used. + Also added the distance calculation "origin -> 3D cursor" for edit mode. +v0.3 - Support for mesh edit mode (1 or 2 selected vertices) +v0.2.1 - Small fix (selecting nothing didn't calculate the distance + of the cursor from the origin anymore) +v0.2 - Distance value is now displayed via a FloatProperty widget (and + therefore saved to file too right now [according to ideasman42]. + The value is save inside the scene right now.) + Thanks goes to ideasman42 (Campbell Barton) for helping me out on this. +v0.1 - Initial revision. Seems to work fine for most purposes. + +TODO: + +There is a random segmentation fault when moving the 3D cursor in edit mode. +Mainly this happens when clicking inside the white circle of the translation +manipulator. There may be other cases though. + +See the other "todo" comments below. + +More links: +http://gitorious.org/blender-scripts/blender-measure-panel-script +http://blenderartists.org/forum/showthread.php?t=177800 + +""" + +bl_addon_info = { + 'name': '3D View: Measure panel', + 'author': 'Buerbaum Martin (Pontiac)', + 'version': '0.7', + 'blender': (2, 5, 3), + 'location': 'View3D > Properties > Measure', + 'description': 'Measure distances between objects', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/3D_interaction/Panel_Measure', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=21445&group_id=153&atid=469', + 'category': '3D View'} + + +import bpy +from bpy.props import * +from mathutils import Vector, Matrix +import bgl +import blf + + +# Precicion for display of float values. +PRECISION = 4 + +# Name of the custom properties as stored in the scene. +COLOR_LOCAL = (1.0, 0.0, 0.0, 0.8) +COLOR_GLOBAL = (0.0, 0.0, 1.0, 0.8) + + +# Returns a single selected object. +# Returns None if more than one (or nothing) is selected. +# Note: Ignores the active object. +def getSingleObject(context): + if len(context.selected_objects) == 1: + return context.selected_objects[0] + + return None + + +# Returns a list with 2 3D points (Vector) and a color (RGBA) +# depending on the current view mode and the selection. +def getMeasurePoints(context): + sce = context.scene + + # Get a single selected object (or nothing). + obj = getSingleObject(context) + + if (context.mode == 'EDIT_MESH'): + obj = context.active_object + + if (obj and obj.type == 'MESH' and obj.data): + # Get mesh data from Object. + mesh = obj.data + + # Get transformation matrix from object. + ob_mat = obj.matrix_world + # Also make an inversed copy! of the matrix. + ob_mat_inv = ob_mat.copy().invert() + + # Get the selected vertices. + # @todo: Better (more efficient) way to do this? + verts_selected = [v for v in mesh.verts if v.select == 1] + + if len(verts_selected) == 0: + # Nothing selected. + # We measure the distance from... + # local ... the object center to the 3D cursor. + # global ... the origin to the 3D cursor. + cur_loc = sce.cursor_location + obj_loc = obj.location.copy() + + # Convert to local space, if needed. + if measureLocal(sce): + p1 = cur_loc + p2 = obj_loc + return (p1, p2, COLOR_GLOBAL) + + else: + p1 = Vector((0.0, 0.0, 0.0)) + p2 = cur_loc + return (p1, p2, COLOR_GLOBAL) + + elif len(verts_selected) == 1: + # One vertex selected. + # We measure the distance from the + # selected vertex object to the 3D cursor. + cur_loc = sce.cursor_location + vert_loc = verts_selected[0].co.copy() + obj_loc = obj.location.copy() + + # Convert to local or global space. + if measureLocal(sce): + p1 = obj_loc + vert_loc + p2 = cur_loc + return (p1, p2, COLOR_LOCAL) + + else: + p1 = vert_loc * ob_mat + obj_loc + p2 = cur_loc + return (p1, p2, COLOR_GLOBAL) + + elif len(verts_selected) == 2: + # Two vertices selected. + # We measure the distance between the + # two selected vertices. + obj_loc = obj.location.copy() + vert1_loc = verts_selected[0].co.copy() + vert2_loc = verts_selected[1].co.copy() + + # Convert to local or global space. + if measureLocal(sce): + p1 = obj_loc + vert1_loc + p2 = obj_loc + vert2_loc + return (p1, p2, COLOR_LOCAL) + + else: + p1 = obj_loc + vert1_loc * ob_mat + p2 = obj_loc + vert2_loc * ob_mat + return (p1, p2, COLOR_GLOBAL) + + else: + return None + + elif (context.mode == 'OBJECT'): + # We are working on object mode. + + if len(context.selected_objects) > 2: + return None + elif len(context.selected_objects) == 2: + # 2 objects selected. + # We measure the distance between the 2 selected objects. + obj1, obj2 = context.selected_objects + obj1_loc = obj1.location.copy() + obj2_loc = obj2.location.copy() + return (obj1_loc, obj2_loc, COLOR_GLOBAL) + + elif (obj): + # One object selected. + # We measure the distance from the object to the 3D cursor. + cur_loc = sce.cursor_location + obj_loc = obj.location.copy() + return (obj_loc, cur_loc, COLOR_GLOBAL) + + elif not context.selected_objects: + # Nothing selected. + # We measure the distance from the origin to the 3D cursor. + p1 = Vector((0.0, 0.0, 0.0)) + p2 = sce.cursor_location + return (p1, p2, COLOR_GLOBAL) + + else: + return None + + +# Return the area of a face (in global space). +# @note Copies the functionality of the following functions, +# but also respects the scaling (via the "obj.matrix_world" parameter): +# @sa: rna_mesh.c:rna_MeshFace_area_get +# @sa: math_geom.c:area_quad_v3 +# @sa: math_geom.c:area_tri_v3 +def faceAreaGlobal(face, obj): + area = 0.0 + + mat = obj.matrix_world + + if len(face.verts) == 4: + # Quad + + # Get vertex indices + v1, v2, v3, v4 = face.verts + + # Get vertex data + v1 = obj.data.verts[v1] + v2 = obj.data.verts[v2] + v3 = obj.data.verts[v3] + v4 = obj.data.verts[v4] + + # Apply transform matrix to vertex coordinates. + v1 = v1.co * mat + v2 = v2.co * mat + v3 = v3.co * mat + v4 = v4.co * mat + + vec1 = v2 - v1 + vec2 = v4 - v1 + + n = vec1.cross(vec2) + + area = n.length / 2.0 + + vec1 = v4 - v3 + vec2 = v2 - v3 + + n = vec1.cross(vec2) + + area += n.length / 2.0 + + elif len(face.verts) == 3: + # Triangle + + # Get vertex indices + v1, v2, v3 = face.verts + + # Get vertex data + v1 = obj.data.verts[v1] + v2 = obj.data.verts[v2] + v3 = obj.data.verts[v3] + + # Apply transform matrix to vertex coordinates. + v1 = v1.co * mat + v2 = v2.co * mat + v3 = v3.co * mat + + vec1 = v3 - v2 + vec2 = v1 - v2 + + n = vec1.cross(vec2) + + area = n.length / 2.0 + + return area + + +# Calculate the surface area of a mesh object. +# *) Set selectedOnly=1 if you only want to count selected faces. +# *) Set globalSpace=1 if you want to calculate +# the global surface area (object mode). +# Note: Be sure you have updated the mesh data before +# running this with selectedOnly=1! +# @todo Support other object types (surfaces, etc...)? +def objectSurfaceArea(obj, selectedOnly, globalSpace): + if (obj and obj.type == 'MESH' and obj.data): + areaTotal = 0 + + mesh = obj.data + + # Count the area of all the faces. + for face in mesh.faces: + if not selectedOnly or face.select: + if globalSpace: + areaTotal += faceAreaGlobal(face, obj) + else: + areaTotal += face.area + + return areaTotal + + # We can not calculate an area for this object. + return -1 + + +# User friendly access to the "space" setting. +def measureGlobal(sce): + return (sce.measure_panel_transform == "measure_global") + + +# User friendly access to the "space" setting. +def measureLocal(sce): + return (sce.measure_panel_transform == "measure_local") + + +# Converts 3D coordinates in a 3DRegion +# into 2D screen coordinates for that region. +def region3d_get_2d_coordinates(context, loc_3d): + # Get screen information + mid_x = context.region.width / 2.0 + mid_y = context.region.height / 2.0 + width = context.region.width + height = context.region.height + + # Get matrices + view_mat = context.space_data.region_3d.perspective_matrix + total_mat = view_mat + + # order is important + vec = total_mat * Vector((loc_3d[0], loc_3d[1], loc_3d[2], 1.0)) + + # dehomogenise + vec = Vector(( + vec[0] / vec[3], + vec[1] / vec[3], + vec[2] / vec[3])) + + x = int(mid_x + vec[0] * width / 2.0) + y = int(mid_y + vec[1] * height / 2.0) + + return Vector((x, y, 0)) + + +def draw_measurements_callback(self, context): + sce = context.scene + + draw = 0 + if hasattr(sce, "measure_panel_draw"): + draw = sce.measure_panel_draw + + # 2D drawing code example + #bgl.glBegin(bgl.GL_LINE_STRIP) + #bgl.glVertex2i(0, 0) + #bgl.glVertex2i(80, 100) + #bgl.glEnd() + + # Get measured 3D points and colors. + line = getMeasurePoints(context) + if (line and draw): + p1, p2, color = line + + # Get and convert the Perspective Matrix of the current view/region. + view3d = bpy.context.space_data + region = view3d.region_3d + perspMatrix = region.perspective_matrix + tempMat = [perspMatrix[i][j] for i in range(4) for j in range(4)] + perspBuff = bgl.Buffer(bgl.GL_FLOAT, 16, tempMat) + + # --- + # Store previous OpenGL settings. + # Store MatrixMode + MatrixMode_prev = bgl.Buffer(bgl.GL_INT, [1]) + bgl.glGetIntegerv(bgl.GL_MATRIX_MODE, MatrixMode_prev) + MatrixMode_prev = MatrixMode_prev[0] + + # Store projection matrix + ProjMatrix_prev = bgl.Buffer(bgl.GL_DOUBLE, [16]) + bgl.glGetFloatv(bgl.GL_PROJECTION_MATRIX, ProjMatrix_prev) + + # Store Line width + lineWidth_prev = bgl.Buffer(bgl.GL_FLOAT, [1]) + bgl.glGetFloatv(bgl.GL_LINE_WIDTH, lineWidth_prev) + lineWidth_prev = lineWidth_prev[0] + + # Store GL_BLEND + blend_prev = bgl.Buffer(bgl.GL_BYTE, [1]) + bgl.glGetFloatv(bgl.GL_BLEND, blend_prev) + blend_prev = blend_prev[0] + + # Store glColor4f + color_prev = bgl.Buffer(bgl.GL_FLOAT, [4]) + bgl.glGetFloatv(bgl.GL_COLOR, color_prev) + + # --- + # Prepare for 3D drawing + bgl.glLoadIdentity() + bgl.glMatrixMode(bgl.GL_PROJECTION) + bgl.glLoadMatrixf(perspBuff) + + bgl.glColor4f(color[0], color[1], color[2], color[3]) + bgl.glEnable(bgl.GL_BLEND) + + # --- + # Draw 3D stuff. + width = 2 + bgl.glLineWidth(width) + bgl.glBegin(bgl.GL_LINE_STRIP) + bgl.glVertex3f(p1[0], p1[1], p1[2]) + bgl.glVertex3f(p2[0], p2[1], p2[2]) + bgl.glEnd() + + # --- + # Restore previous OpenGL settings + bgl.glLoadIdentity() + bgl.glMatrixMode(MatrixMode_prev) + bgl.glLoadMatrixf(ProjMatrix_prev) + bgl.glLineWidth(lineWidth_prev) + if not blend_prev: + bgl.glDisable(bgl.GL_BLEND) + bgl.glColor4f(color_prev[0], + color_prev[1], + color_prev[2], + color_prev[3]) + + # --- + # Draw (2D) text + # We do this after drawing the lines so + # we can draw it OVER the line. + coord_2d = region3d_get_2d_coordinates(context, p2 + (p1 - p2) * 0.5) + offset = 10 # Offset the text a bit to the right. + blf.position(0, coord_2d[0] + offset, coord_2d[1], 0) + + dist = (p1 - p2).length + text = "Distance: " + str(round(dist, PRECISION)) + " BU" + # @todo Get user pref for text color in 3D View + bgl.glColor4f(1.0, 1.0, 1.0, 1.0) + blf.size(0, 12, 72) # Prevent font size to randomly change. + blf.draw(0, text) + + +class VIEW3D_OT_display_measurements(bpy.types.Operator): + '''Display the measurements made in the 'Measure' panel''' + bl_idname = "view3d.display_measurements" + bl_label = "Display the measurements made in the" \ + " 'Measure' panel in the 3D View." + bl_options = {'REGISTER'} + + def modal(self, context, event): + context.area.tag_redraw() + + return {'FINISHED'} + + def execute(self, context): + if context.area.type == 'VIEW_3D': + if not self.bl_label in context.manager.operators.keys(): + # Add the region OpenGL drawing callback + for WINregion in context.area.regions: + if WINregion.type == 'WINDOW': + break + + context.manager.add_modal_handler(self) + self._handle = WINregion.callback_add( + draw_measurements_callback, + (self, context), + 'POST_PIXEL') + + print("Measure panel display callback added") + + return {'RUNNING_MODAL'} + + else: + self.report({'WARNING'}, "View3D not found, cannot run operator") + return {'CANCELLED'} + + +class VIEW3D_OT_reenter_editmode(bpy.types.Operator): + bl_label = "Re-enter EditMode" + bl_idname = "view3d.reenter_editmode" + bl_description = "Update mesh data of an active mesh object." \ + " This is done by exiting and re-entering mesh edit mode." + bl_options = {'REGISTER'} + + def invoke(self, context, event): + + # Get the active object. + obj = context.active_object + + if (obj and obj.type == 'MESH' and context.mode == 'EDIT_MESH'): + # Exit and re-enter mesh EditMode. + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') + return {'FINISHED'} + + return {'CANCELLED'} + + +class VIEW3D_PT_measure(bpy.types.Panel): + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_label = "Measure" + bl_default_closed = True + + def poll(self, context): + # Only display this panel in the object and edit mode 3D view. + if (context.area.type == 'VIEW_3D' and + (context.mode == 'EDIT_MESH' + or context.mode == 'OBJECT')): + return 1 + + return 0 + + def draw_header(self, context): + layout = self.layout + sce = context.scene + + # Execute operator (this adds the callback) + # if it wasn't done yet. + bpy.ops.view3d.display_measurements() + + # Define property for the draw setting. + sce.BoolProperty( + attr="measure_panel_draw", + description="Draw distances in 3D View", + default=1) + + # Define property for the calc-area setting. + # @todo prevent double calculations for each refresh automatically? + sce.BoolProperty( + attr="measure_panel_calc_area", + description="Calculate mesh surface area (heavy CPU" \ + " usage on bigger meshes)", + default=0) + + layout.prop(sce, "measure_panel_draw") + + def draw(self, context): + layout = self.layout + sce = context.scene + + # Force a redraw. + # This prevents the lines still be drawn after + # disabling the "measure_panel_draw" checkbox. + # @todo Better solution? + context.area.tag_redraw() + + # Get a single selected object (or nothing). + obj = getSingleObject(context) + + # Define a temporary attribute for the distance value + sce.FloatProperty( + name="Distance", + attr="measure_panel_dist", + precision=PRECISION, + unit="LENGTH") + sce.FloatProperty( + attr="measure_panel_area1", + precision=PRECISION, + unit="AREA") + sce.FloatProperty( + attr="measure_panel_area2", + precision=PRECISION, + unit="AREA") + + TRANSFORM = [ + ("measure_global", "Global", + "Calculate values in global space."), + ("measure_local", "Local", + "Calculate values inside the local object space.")] + + # Define dropdown for the global/local setting + bpy.types.Scene.EnumProperty( + attr="measure_panel_transform", + name="Space", + description="Choose in which space you want to measure.", + items=TRANSFORM, + default='measure_global') + + if (context.mode == 'EDIT_MESH'): + obj = context.active_object + + if (obj and obj.type == 'MESH' and obj.data): + # "Note: a Mesh will return the selection state of the mesh + # when EditMode was last exited. A Python script operating + # in EditMode must exit EditMode before getting the current + # selection state of the mesh." + # http://www.blender.org/documentation/249PythonDoc/ + # /Mesh.MVert-class.html#sel + # We can only provide this by existing & re-entering EditMode. + # @todo: Better way to do this? + + # Get mesh data from Object. + mesh = obj.data + + # Get transformation matrix from object. + ob_mat = obj.matrix_world + # Also make an inversed copy! of the matrix. + ob_mat_inv = ob_mat.copy() + Matrix.invert(ob_mat_inv) + + # Get the selected vertices. + # @todo: Better (more efficient) way to do this? + verts_selected = [v for v in mesh.verts if v.select == 1] + + if len(verts_selected) == 0: + # Nothing selected. + # We measure the distance from... + # local ... the object center to the 3D cursor. + # global ... the origin to the 3D cursor. + + # Get the 2 measure points + line = getMeasurePoints(context) + if line != 0: + dist_vec = line[0] - line[1] + + sce.measure_panel_dist = dist_vec.length + + row = layout.row() + row.prop(sce, "measure_panel_dist") + + row = layout.row() + row.label(text="", icon='CURSOR') + row.label(text="", icon='ARROW_LEFTRIGHT') + if measureLocal(sce): + row.label(text="Obj. Center") + else: + row.label(text="Origin [0,0,0]") + + row = layout.row() + row.operator("view3d.reenter_editmode", + text="Update selection & distance") +# @todo +# description="The surface area value can" \ +# " not be updated in mesh edit mode" \ +# " automatically. Press this button" \ +# " to do this manually, after you changed" \ +# " the selection.") + + row = layout.row() + row.prop(sce, + "measure_panel_transform", + expand=True) + + elif len(verts_selected) == 1: + # One vertex selected. + # We measure the distance from the + # selected vertex object to the 3D cursor. + + # Get the 2 measure points + line = getMeasurePoints(context) + if line != 0: + dist_vec = line[0] - line[1] + + sce.measure_panel_dist = dist_vec.length + + row = layout.row() + row.prop(sce, "measure_panel_dist") + + row = layout.row() + row.label(text="", icon='CURSOR') + row.label(text="", icon='ARROW_LEFTRIGHT') + row.label(text="", icon='VERTEXSEL') + + row = layout.row() + row.operator("view3d.reenter_editmode", + text="Update selection & distance") + + row = layout.row() + row.prop(sce, + "measure_panel_transform", + expand=True) + + elif len(verts_selected) == 2: + # Two vertices selected. + # We measure the distance between the + # two selected vertices. + + # Get the 2 measure points + line = getMeasurePoints(context) + if line != 0: + dist_vec = line[0] - line[1] + + sce.measure_panel_dist = dist_vec.length + + row = layout.row() + row.prop(sce, "measure_panel_dist") + + row = layout.row() + row.label(text="", icon='VERTEXSEL') + row.label(text="", icon='ARROW_LEFTRIGHT') + row.label(text="", icon='VERTEXSEL') + + row = layout.row() + row.operator("view3d.reenter_editmode", + text="Update selection & distance") + + row = layout.row() + row.prop(sce, + "measure_panel_transform", + expand=True) + + else: + row = layout.row() + row.prop(sce, "measure_panel_calc_area", + text="Surface area (selected faces):") + + if (sce.measure_panel_calc_area): + # Get selected faces + # @todo: Better (more efficient) way to do this? + faces_selected = [f for f in mesh.faces + if f.select == 1] + + if len(faces_selected) > 0: + area = objectSurfaceArea(obj, True, + measureGlobal(sce)) + if (area >= 0): + row = layout.row() + row.label( + text=str(len(faces_selected)), + icon='FACESEL') + sce.measure_panel_area1 = area + row.prop(sce, "measure_panel_area1") + + row = layout.row() + row.operator("view3d.reenter_editmode", + text="Update selection & area") + + row = layout.row() + row.prop(sce, + "measure_panel_transform", + expand=True) + + else: + row = layout.row() + row.label(text="Selection not supported.", + icon='INFO') + + row = layout.row() + row.operator("view3d.reenter_editmode", + text="Update selection") + + else: + row = layout.row() + row.operator("view3d.reenter_editmode", + text="Update selection") + + elif (context.mode == 'OBJECT'): + # We are working on object mode. + + if len(context.selected_objects) > 2: + # We have more that 2 objects selected... + + row = layout.row() + row.prop(sce, "measure_panel_calc_area", + text="Surface area (selected faces):") + + if (sce.measure_panel_calc_area): + + mesh_objects = [o for o in context.selected_objects + if (o.type == 'MESH')] + + if (len(mesh_objects) > 0): + # ... and at least one of them is a mesh. + + # Calculate and display surface area of the objects. + # @todo: Convert to scene units! We do not have a + # FloatProperty field here for automatic conversion. + + row = layout.row() + for o in mesh_objects: + area = objectSurfaceArea(o, False, + measureGlobal(sce)) + if (area >= 0): + row = layout.row() + row.label(text=o.name, icon='OBJECT_DATA') + row.label(text=str(round(area, PRECISION)) + + " BU^2") + + row = layout.row() + row.prop(sce, + "measure_panel_transform", + expand=True) + + elif len(context.selected_objects) == 2: + # 2 objects selected. + # We measure the distance between the 2 selected objects. + + obj1, obj2 = context.selected_objects + + # Get the 2 measure points + line = getMeasurePoints(context) + if line != 0: + dist_vec = line[0] - line[1] + + sce.measure_panel_dist = dist_vec.length + + row = layout.row() + row.prop(sce, "measure_panel_dist") + + row = layout.row() + row.label(text="", icon='OBJECT_DATA') + row.prop(obj1, "name", text="") + + row.label(text="", icon='ARROW_LEFTRIGHT') + + row.label(text="", icon='OBJECT_DATA') + row.prop(obj2, "name", text="") + + row = layout.row() + row.prop(sce, "measure_panel_calc_area", + text="Surface area:") + + if (sce.measure_panel_calc_area): + # Calculate and display surface area of the objects. + area1 = objectSurfaceArea(obj1, False, measureGlobal(sce)) + area2 = objectSurfaceArea(obj2, False, measureGlobal(sce)) + if (area1 >= 0 or area2 >= 0): + if (area1 >= 0): + row = layout.row() + row.label(text=obj1.name, icon='OBJECT_DATA') + sce.measure_panel_area1 = area1 + row.prop(sce, "measure_panel_area1") + + if (area2 >= 0): + row = layout.row() + row.label(text=obj2.name, icon='OBJECT_DATA') + sce.measure_panel_area2 = area2 + row.prop(sce, "measure_panel_area2") + + row = layout.row() + row.prop(sce, + "measure_panel_transform", + expand=True) + + elif (obj): + # One object selected. + # We measure the distance from the object to the 3D cursor. + + # Get the 2 measure points + line = getMeasurePoints(context) + if line != 0: + dist_vec = line[0] - line[1] + + sce.measure_panel_dist = dist_vec.length + + row = layout.row() + #row.label(text=str(dist_vec.length)) + row.prop(sce, "measure_panel_dist") + + row = layout.row() + row.label(text="", icon='CURSOR') + + row.label(text="", icon='ARROW_LEFTRIGHT') + + row.label(text="", icon='OBJECT_DATA') + row.prop(obj, "name", text="") + + row = layout.row() + row.prop(sce, "measure_panel_calc_area", + text="Surface area:") + + if (sce.measure_panel_calc_area): + # Calculate and display surface area of the object. + area = objectSurfaceArea(obj, False, measureGlobal(sce)) + if (area >= 0): + row = layout.row() + row.label(text=obj.name, icon='OBJECT_DATA') + sce.measure_panel_area1 = area + row.prop(sce, "measure_panel_area1") + + row = layout.row() + row.prop(sce, + "measure_panel_transform", + expand=True) + + elif not context.selected_objects: + # Nothing selected. + # We measure the distance from the origin to the 3D cursor. + + # Get the 2 measure points + line = getMeasurePoints(context) + if line != 0: + dist_vec = line[0] - line[1] + + sce.measure_panel_dist = dist_vec.length + + row = layout.row() + row.prop(sce, "measure_panel_dist") + + row = layout.row() + row.label(text="", icon='CURSOR') + row.label(text="", icon='ARROW_LEFTRIGHT') + row.label(text="Origin [0,0,0]") + + else: + row = layout.row() + row.label(text="Selection not supported.", + icon='INFO') + + +def register(): + bpy.types.register(VIEW3D_PT_measure) + bpy.types.register(VIEW3D_OT_reenter_editmode) + bpy.types.register(VIEW3D_OT_display_measurements) + + +def unregister(): + bpy.types.unregister(VIEW3D_PT_measure) + bpy.types.unregister(VIEW3D_OT_reenter_editmode) + bpy.types.unregister(VIEW3D_OT_display_measurements) + +if __name__ == "__main__": + register() diff --git a/space_view3d_property_chart.py b/space_view3d_property_chart.py new file mode 100644 index 00000000..bbbc90b3 --- /dev/null +++ b/space_view3d_property_chart.py @@ -0,0 +1,232 @@ +# +# ***** 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +"""List properties of selected objects""" + +bl_addon_info = { + 'name': '3D View: Object Property Chart', + 'author': 'Campbell Barton (ideasman42)', + 'version': '0.1', + 'blender': (2, 5, 3), + 'location': 'Tool Shelf', + 'description': 'Edit arbitrary selected properties for objects of the same type', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/3D interaction/Object Property Chart', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?' \ + 'func=detail&aid=22701&group_id=153&atid=469', + 'category': '3D View'} + +import bpy + +def _property_chart_data_get(self, context): + # eg. context.active_object + obj = eval("context.%s" % self.context_data_path_active) + + if obj is None: + return None, None + + # eg. context.selected_objects[:] + selected_objects = eval("context.%s" % self.context_data_path_selected)[:] + + if not selected_objects: + return None, None + + return obj, selected_objects + + +def _property_chart_draw(self, context): + ''' + This function can run for different types. + ''' + obj, selected_objects = _property_chart_data_get(self, context) + + if not obj: + return + + # active first + try: + active_index = selected_objects.index(obj) + except ValueError: + active_index = -1 + + if active_index > 0: # not the first alredy + selected_objects[0], selected_objects[active_index] = selected_objects[active_index], selected_objects[0] + + id_storage = context.scene + + strings = id_storage.get(self._PROP_STORAGE_ID) + + if strings is None: + strings = id_storage[self._PROP_STORAGE_ID] = "data data.name" + + if strings: + + def obj_prop_get(obj, attr_string): + """return a pair (rna_base, "rna_property") to give to the rna UI property function""" + attrs = attr_string.split(".") + val_new = obj + for i, attr in enumerate(attrs): + val_old = val_new + val_new = getattr(val_old, attr, Ellipsis) + + if val_new == Ellipsis: + return None, None + return val_old, attrs[-1] + + strings = strings.split() + + prop_all = [] + + for obj in selected_objects: + prop_pairs = [] + prop_found = False + for attr_string in strings: + prop_pairs.append(obj_prop_get(obj, attr_string)) + if prop_found == False and prop_pairs[-1] != (None, None): + prop_found = True + + if prop_found: + prop_all.append((obj, prop_pairs)) + + + # Collected all props, now display them all + layout = self.layout + + row = layout.row(align=True) + + col = row.column() + col.label(text="name") + for obj, prop_pairs in prop_all: + col.prop(obj, "name", text="") + + for i in range(len(strings)): + col = row.column() + + # name and copy button + rowsub = col.row(align=False) + rowsub.label(text=strings[i].rsplit(".", 1)[-1]) + props = rowsub.operator("wm.chart_copy", text="", icon='PASTEDOWN', emboss=False) + props.data_path_active = self.context_data_path_active + props.data_path_selected = self.context_data_path_selected + props.data_path = strings[i] + + for obj, prop_pairs in prop_all: + data, attr = prop_pairs[i] + if data: + col.prop(data, attr, text="")# , emboss=obj==active_object + else: + col.label(text="") + + # edit the display props + col = layout.column() + col.label(text="Object Properties") + col.prop(id_storage, '["%s"]' % self._PROP_STORAGE_ID, text="") + + +class View3DEditProps(bpy.types.Panel): + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + + bl_label = "Property Chart" + bl_context = "objectmode" + + _PROP_STORAGE_ID = "view3d_edit_props" + + # _property_chart_draw needs these + context_data_path_active = "active_object" + context_data_path_selected = "selected_objects" + + draw = _property_chart_draw + + +class SequencerEditProps(bpy.types.Panel): + bl_space_type = 'SEQUENCE_EDITOR' + bl_region_type = 'UI' + + bl_label = "Property Chart" + + _PROP_STORAGE_ID = "sequencer_edit_props" + + # _property_chart_draw needs these + context_data_path_active = "scene.sequence_editor.active_strip" + context_data_path_selected = "selected_sequences" + + draw = _property_chart_draw + + def poll(self, context): + return context.scene.sequence_editor is not None + +# Operator to copy properties + + +def _property_chart_copy(self, context): + obj, selected_objects = _property_chart_data_get(self, context) + + if not obj: + return + + data_path = self.properties.data_path + + # quick & nasty method! + for obj_iter in selected_objects: + if obj != obj_iter: + try: + exec("obj_iter.%s = obj.%s" % (data_path, data_path)) + except: + # just incase we need to know what went wrong! + import traceback + traceback.print_exc() + +from bpy.props import StringProperty + + +class CopyPropertyChart(bpy.types.Operator): + "Open a path in a file browser" + bl_idname = "wm.chart_copy" + bl_label = "Copy properties from active to selected" + + data_path_active = StringProperty() + data_path_selected = StringProperty() + data_path = StringProperty() + + def execute(self, context): + # so attributes are found for '_property_chart_data_get()' + self.context_data_path_active = self.properties.data_path_active + self.context_data_path_selected = self.properties.data_path_selected + + _property_chart_copy(self, context) + + return {'FINISHED'} + + +def register(): + bpy.types.register(View3DEditProps) + bpy.types.register(SequencerEditProps) + bpy.types.register(CopyPropertyChart) + + +def unregister(): + bpy.types.unregister(View3DEditProps) + bpy.types.unregister(SequencerEditProps) + bpy.types.unregister(CopyPropertyChart) + +if __name__ == "__main__": + register() diff --git a/space_view3d_spacebar_menu.py b/space_view3d_spacebar_menu.py new file mode 100644 index 00000000..afd8c814 --- /dev/null +++ b/space_view3d_spacebar_menu.py @@ -0,0 +1,1501 @@ +#3d_cursor_menu.py (c) 2010 Jonathan Smith (JayDez) +#Original Script by: Mariano Hidalgo (uselessdreamer) +#contributed to by: Crouch, sim88, sam, meta-androcto +# +#Tested with r28146 +# +# ##### 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 ##### + +""" +Dynamic Menu +This adds a the Dynamic Spacebar Menu in the View3D. + +Usage: +* This script gives a basic menu with common simple tools for easy access. +* Very similar to the Spacebar menu in 2.49 +* Context sensitive for Object. Edit, Sculpt, Pose, Weight/Texture/Vertex Paint. +* Object sensitive based on object selected in edit mode. + +Version history: +v1.5 - (meta-androcto) - adding context sensitive menus. +v1.3 - (JayDez) - Changed toggle editmode to an if statement, so that + if you are in editmode it will show change to object mode but + otherwise it shows change to edit mode. Also added separate icons + for change to edit mode and to object mode. +v1.2 - (JayDez) - Editing docs, changing 3D cursor to dynamic menu, + reorganizing menu. +v1.1 - (meta-androcto) - added editmode menu +v1.0 - (meta-androcto) - initial final revision (commited to contrib) +v0.1 through 0.9 - various tests/contributions by various people and scripts + Devs: JayDez, Crouch, sim88, meta-androcto, Sam + Scripts: 3D Cursor Menu, Original Dynamic Menu +""" + + +bl_addon_info = { + 'name': '3D View: Dynamic Spacebar Menu', + 'author': 'JayDez, sim88, meta-androcto', 'sam' + 'version': '1.5', + 'blender': (2, 5, 3), + 'location': 'View3D > Spacebar', + 'description': 'Context sensitive spacebar menu', + 'warning': '', # used for warning icon and text in addons panel + 'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \ + 'Scripts/3D_interaction/Dynamic_Spacebar_Menu', + 'tracker_url': 'https://projects.blender.org/tracker/index.php?'\ + 'func=detail&aid=22060&group_id=153&atid=469', + 'category': '3D View'} + +import bpy +from bpy import * +from mathutils import Vector, Matrix +import math + +# Dynamic Menu +class VIEW3D_MT_Space_Dynamic_Menu(bpy.types.Menu): + bl_label = "Dynamic Spacebar Menu" + + def draw(self, context): + layout = self.layout + settings = context.tool_settings + layout.operator_context = 'INVOKE_REGION_WIN' + + ob = context + if ob.mode == 'OBJECT': + # Object mode + + # Search Menu + layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM') + layout.separator() + + # Add Menu block + layout.menu("VIEW3D_MT_AddMenu", icon='OBJECT_DATAMODE') + layout.separator() + + # Transform block + layout.menu("VIEW3D_MT_TransformMenu", icon='MANIPUL') + + # Mirror block + layout.menu("VIEW3D_MT_MirrorMenu", icon='MOD_MIRROR') + layout.separator() + + # Parent block + layout.menu("VIEW3D_MT_ParentMenu", icon='ROTACTIVE') + + # Group block + layout.menu("VIEW3D_MT_GroupMenu", icon='GROUP') + layout.separator() + + # Modifier block + layout.operator_menu_enum("object.modifier_add", "type" , icon='MODIFIER') + layout.separator() + + # Cursor Block + layout.menu("VIEW3D_MT_CursorMenu", icon='CURSOR') + layout.separator() + + # Align block + layout.menu("VIEW3D_MT_AlignMenu", icon='ALIGN') + layout.separator() + + # Select block + layout.menu("VIEW3D_MT_SelectMenu", icon='RESTRICT_SELECT_OFF') + + # Toolshelf block + layout.operator("view3d.toolshelf", icon='MENU_PANEL') + layout.separator() + + # Properties block + layout.operator("view3d.properties", icon='MENU_PANEL') + layout.separator() + + # Toggle Editmode + layout.operator("object.editmode_toggle", text="Enter Edit Mode", + icon='EDITMODE_HLT') + + # Delete block + layout.operator("object.delete", text="Delete Object", + icon='CANCEL') + + + elif ob.mode == 'EDIT_MESH': + # Edit mode + + # Search Menu + layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM') + + # Add block + bl_label = "Create" + layout.menu("INFO_MT_mesh_add", text="Add Mesh", + icon='EDITMODE_HLT') + layout.separator() + + # Transform block + layout.menu("VIEW3D_MT_TransformMenu", icon='MANIPUL') + layout.separator() + + # Mirror block + layout.menu("VIEW3D_MT_MirrorMenu", icon='MOD_MIRROR') + layout.separator() + + # Cursor block + layout.menu("VIEW3D_MT_EditCursorMenu", icon='CURSOR') + layout.separator() + + # Edit block + layout.menu("VIEW3D_MT_edit_TK", icon='OUTLINER_OB_MESH') + layout.separator() + + # Extrude block + layout.menu("VIEW3D_MT_edit_mesh_extrude", icon='EDITMODE_HLT') + layout.separator() + + # Tools block + layout.menu("VIEW3D_MT_edit_mesh_specials", icon='MODIFIER') + layout.menu("VIEW3D_MT_uv_map", icon='MOD_UVPROJECT') + + # Select block + layout.menu("VIEW3D_MT_SelectMenu", icon='RESTRICT_SELECT_OFF') + + # Toolshelf block + layout.operator("view3d.toolshelf", icon='MENU_PANEL') + layout.separator() + + # Properties block + layout.operator("view3d.properties", icon='MENU_PANEL') + layout.separator() + + # Toggle Object Mode + layout.operator("object.editmode_toggle", text="Enter Object Mode", + icon='OBJECT_DATAMODE') + + # Delete Block + layout.operator("mesh.delete", icon='CANCEL') + + if ob.mode == 'EDIT_CURVE': + # Curve menu + + # Search Menu + layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM') + + # Transform block + layout.menu("VIEW3D_MT_TransformMenu", icon='MANIPUL') + layout.separator() + + # Mirror block + layout.menu("VIEW3D_MT_MirrorMenu", icon='MOD_MIRROR') + layout.separator() + + # Proportional block + layout.prop_menu_enum(settings, "proportional_editing", icon= "PROP_CON") + layout.prop_menu_enum(settings, "proportional_editing_falloff", icon= "SMOOTHCURVE") + layout.separator() + + # Edit Control Points + layout.menu("VIEW3D_MT_EditCurveCtrlpoints", + icon='CURVE_BEZCURVE') + layout.separator() + + # Edit Curve Specials + layout.menu("VIEW3D_MT_EditCurveSpecials", + icon='MODIFIER') + layout.separator() + + # Cursor block + layout.menu("VIEW3D_MT_CursorMenu", icon='CURSOR') + layout.separator() + + # Select Curve Block + layout.menu("VIEW3D_MT_SelectCurveMenu", icon='RESTRICT_SELECT_OFF') + layout.separator() + + # Toolshelf block + layout.operator("view3d.toolshelf", icon='MENU_PANEL') + layout.separator() + + # Properties block + layout.operator("view3d.properties", icon='MENU_PANEL') + layout.separator() + + # Toggle Objectmode + layout.operator("object.editmode_toggle", text="Enter Object Mode", + icon='OBJECT_DATA') + + # Delete block + layout.operator("object.delete", text="Delete Object", + icon='CANCEL') + + if ob.mode == 'EDIT_SURFACE': + # Surface menu + + # Search Menu + layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM') + + # Transform block + layout.menu("VIEW3D_MT_TransformMenu", icon='MANIPUL') + layout.separator() + + # Mirror block + layout.menu("VIEW3D_MT_MirrorMenu", icon='MOD_MIRROR') + layout.separator() + + # Proportional block + layout.prop_menu_enum(settings, "proportional_editing", icon= "PROP_CON") + layout.prop_menu_enum(settings, "proportional_editing_falloff", icon= "SMOOTHCURVE") + + # Edit Curve Specials + layout.menu("VIEW3D_MT_EditCurveSpecials", + icon='MODIFIER') + layout.separator() + + # Cursor block + layout.menu("VIEW3D_MT_CursorMenu", icon='CURSOR') + layout.separator() + + # Select Surface + layout.menu("VIEW3D_MT_SelectSurface", icon='RESTRICT_SELECT_OFF') + layout.separator() + + # Toolshelf block + layout.operator("view3d.toolshelf", icon='MENU_PANEL') + layout.separator() + + # Properties block + layout.operator("view3d.properties", icon='MENU_PANEL') + layout.separator() + + # Toggle Objectmode + layout.operator("object.editmode_toggle", text="Enter Object Mode", + icon='OBJECT_DATA') + + # Delete block + layout.operator("object.delete", text="Delete Object", + icon='CANCEL') + + if ob.mode == 'EDIT_METABALL': + # Metaball menu + + #Search Menu + layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM') + + # Transform block + layout.menu("VIEW3D_MT_TransformMenu", icon='MANIPUL') + layout.separator() + + # Mirror block + layout.menu("VIEW3D_MT_MirrorMenu", icon='MOD_MIRROR') + layout.separator() + + # Proportional block + layout.prop_menu_enum(settings, "proportional_editing", icon= "PROP_CON") + layout.prop_menu_enum(settings, "proportional_editing_falloff", icon= "SMOOTHCURVE") + + # Cursor block + layout.menu("VIEW3D_MT_CursorMenu", icon='CURSOR') + layout.separator() + + #Select Metaball + layout.menu("VIEW3D_MT_SelectMetaball", icon='RESTRICT_SELECT_OFF') + layout.separator() + + # Toolshelf block + layout.operator("view3d.toolshelf", icon='MENU_PANEL') + layout.separator() + + # Properties block + layout.operator("view3d.properties", icon='MENU_PANEL') + layout.separator() + + # Toggle Objectmode + layout.operator("object.editmode_toggle", text="Enter Object Mode", + icon='OBJECT_DATA') + + # Delete block + layout.operator("object.delete", text="Delete Object", + icon='CANCEL') + + elif ob.mode == 'EDIT_LATTICE': + # Lattice menu + + #Search Menu + layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM') + + # Transform block + layout.menu("VIEW3D_MT_TransformMenu", icon='MANIPUL') + layout.separator() + + # Mirror block + layout.menu("VIEW3D_MT_MirrorMenu", icon='MOD_MIRROR') + layout.separator() + + # Proportional block + layout.prop_menu_enum(settings, "proportional_editing", icon= "PROP_CON") + layout.prop_menu_enum(settings, "proportional_editing_falloff", icon= "SMOOTHCURVE") + + # Cursor block + layout.menu("VIEW3D_MT_CursorMenu", icon='CURSOR') + layout.separator() + + #Select Lattice + layout.menu("VIEW3D_MT_select_edit_lattice", icon='RESTRICT_SELECT_OFF') + layout.separator() + + layout.operator("lattice.make_regular") + layout.separator() + + # Toolshelf block + layout.operator("view3d.toolshelf", icon='MENU_PANEL') + layout.separator() + + # Properties block + layout.operator("view3d.properties", icon='MENU_PANEL') + layout.separator() + + # Toggle Objectmode + layout.operator("object.editmode_toggle", text="Enter Object Mode", + icon='OBJECT_DATA') + + # Delete block + layout.operator("object.delete", text="Delete Object", + icon='CANCEL') + + + if context.mode == 'PARTICLE': + # Particle menu + + #Search Menu + layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM') + + # Transform block + layout.menu("VIEW3D_MT_TransformMenu", icon='MANIPUL') + layout.separator() + + # Mirror block + layout.menu("VIEW3D_MT_MirrorMenu", icon='MOD_MIRROR') + layout.separator() + + # Proportional block + layout.prop_menu_enum(settings, "proportional_editing", icon= "PROP_CON") + layout.prop_menu_enum(settings, "proportional_editing_falloff", icon= "SMOOTHCURVE") + + # Cursor block + layout.menu("VIEW3D_MT_CursorMenu", icon='CURSOR') + layout.separator() + + # Toolshelf block + layout.operator("view3d.toolshelf", icon='MENU_PANEL') + layout.separator() + + # Properties block + layout.operator("view3d.properties", icon='MENU_PANEL') + layout.separator() + + # Toggle Objectmode + layout.operator("object.editmode_toggle", text="Enter Object Mode", + icon='OBJECT_DATA') + + # Delete block + layout.operator("object.delete", text="Delete Object", + icon='CANCEL') + + ob = context + if ob.mode == 'PAINT_WEIGHT': + # Weight paint menu + + # Search Menu + layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM') + + # Transform block + layout.menu("VIEW3D_MT_TransformMenu", icon='MANIPUL') + layout.separator() + + # Cursor block + layout.menu("VIEW3D_MT_CursorMenu", icon='CURSOR') + layout.separator() + + # Weight Paint block + layout.menu("VIEW3D_MT_paint_weight", icon='WPAINT_HLT') + layout.separator() + + # History/Cursor Block + layout.menu("VIEW3D_MT_undoS", icon='ARROW_LEFTRIGHT') + + # Toolshelf block + layout.operator("view3d.toolshelf", icon='MENU_PANEL') + layout.separator() + + # Properties block + layout.operator("view3d.properties", icon='MENU_PANEL') + layout.separator() + + # Toggle Objectmode + layout.operator("object.mode_set", text="Enter Object Mode", + icon='OBJECT_DATA') + + + + elif ob.mode == 'PAINT_VERTEX': + # Vertex paint menu + + # Search Menu + layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM') + + # Transform block + layout.menu("VIEW3D_MT_TransformMenu", icon='MANIPUL') + layout.separator() + + # Cursor block + layout.menu("VIEW3D_MT_CursorMenu", icon='CURSOR') + layout.separator() + + # Vertex Paint block + layout.operator("paint.vertex_color_set", icon='VPAINT_HLT') + layout.separator() + + # History/Cursor Block + layout.menu("VIEW3D_MT_undoS", icon='ARROW_LEFTRIGHT') + + # Toolshelf block + layout.operator("view3d.toolshelf", icon='MENU_PANEL') + layout.separator() + + # Properties block + layout.operator("view3d.properties", icon='MENU_PANEL') + layout.separator() + + # Toggle Objectmode + layout.operator("object.mode_set", text="Enter Object Mode", + icon='OBJECT_DATA') + + elif ob.mode == 'PAINT_TEXTURE': + # Texture paint menu + + # Search Menu + layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM') + + # Transform block + layout.menu("VIEW3D_MT_TransformMenu", icon='MANIPUL') + layout.separator() + + # Cursor block + layout.menu("VIEW3D_MT_CursorMenu", icon='CURSOR') + layout.separator() + + # History/Cursor Block + layout.menu("VIEW3D_MT_undoS", icon='ARROW_LEFTRIGHT') + + # Toolshelf block + layout.operator("view3d.toolshelf", icon='MENU_PANEL') + layout.separator() + + # Properties block + layout.operator("view3d.properties", icon='MENU_PANEL') + layout.separator() + + # Toggle Objectmode + layout.operator("object.mode_set", text="Enter Object Mode", + icon='OBJECT_DATA') + + elif ob.mode == 'SCULPT': + # Sculpt menu + + # Search Menu + layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM') + + # Transform block + layout.menu("VIEW3D_MT_TransformMenu", icon='MANIPUL') + layout.separator() + + # Mirror block + layout.menu("VIEW3D_MT_MirrorMenu", icon='MOD_MIRROR') + layout.separator() + + # Sculpt block + layout.menu("VIEW3D_MT_sculpt", icon='SCULPTMODE_HLT') + layout.separator() + + # Cursor block + layout.menu("VIEW3D_MT_CursorMenu", icon='CURSOR') + layout.separator() + + # History/Cursor Block + layout.menu("VIEW3D_MT_undoS", icon='ARROW_LEFTRIGHT') + + # Toolshelf block + layout.operator("view3d.toolshelf", icon='MENU_PANEL') + layout.separator() + + # Properties block + layout.operator("view3d.properties", icon='MENU_PANEL') + layout.separator() + + # Toggle Editmode + layout.operator("object.editmode_toggle", text="Enter Edit Mode", + icon='EDITMODE_HLT') + + elif ob.mode == 'EDIT_ARMATURE': + # Armature menu + + # Search Menu + layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM') + + # Transform block + layout.menu("VIEW3D_MT_TransformMenu", icon='MANIPUL') + layout.separator() + + # Mirror block + layout.menu("VIEW3D_MT_MirrorMenu", icon='MOD_MIRROR') + layout.separator() + + # Cursor block + layout.menu("VIEW3D_MT_CursorMenu", icon='CURSOR') + layout.separator() + + # Edit Armature roll + layout.menu("VIEW3D_MT_edit_armature_roll", + icon='BONE_DATA') + layout.separator() + + # Edit Armature Toolkit + layout.menu("VIEW3D_MT_EditArmatureTK", + icon='ARMATURE_DATA') + layout.separator() + + # Edit Armature Name + layout.menu("VIEW3D_MT_ArmatureName", + icon='NEW') + layout.separator() + + # Parent block + layout.menu("VIEW3D_MT_ParentMenu", icon='ROTACTIVE') + + layout.separator() + layout.operator_menu_enum("armature.flags_set", "mode", text="Bone Settings") + + # Edit Armature Select + layout.menu("VIEW3D_MT_SelectArmatureMenu", + icon='RESTRICT_SELECT_OFF') + layout.separator() + + # Edit Armature Specials + layout.menu("VIEW3D_MT_armature_specials", icon='MODIFIER') + layout.separator() + + # Toggle Posemode + layout.operator("object.posemode_toggle", text="Enter Pose Mode", + icon='EDITMODE_HLT') + + # Toggle Posemode + layout.operator("object.editmode_toggle", text="Enter Object Mode", + icon='OBJECT_DATA') + + # Delete block + layout.operator("object.delete", text="Delete Object", + icon='CANCEL') + + + if context.mode == 'POSE': + # Pose mode menu + arm = context.active_object.data + + # Search Menu + layout.operator("wm.search_menu", text="Search", icon='VIEWZOOM') + + # Transform Menu + layout.menu("VIEW3D_MT_TransformMenu", icon='MANIPUL') + + # Clear Transform + layout.menu("VIEW3D_MT_pose_transform") + + # Cursor Menu + layout.menu("VIEW3D_MT_CursorMenu", icon='CURSOR') + layout.separator() + + # Select Pose Block + layout.menu("VIEW3D_MT_SelectPoseMenu", icon='RESTRICT_SELECT_OFF') + layout.separator() + + # Pose Copy Block + layout.menu("VIEW3D_MT_PoseCopy", icon='FILE') + layout.separator() + + + if arm.drawtype in ('BBONE', 'ENVELOPE'): + layout.operator("transform.transform", + text="Scale Envelope Distance").mode = 'BONESIZE' + + layout.menu("VIEW3D_MT_pose_apply") + layout.separator() + + layout.operator("pose.relax") + layout.separator() + + layout.menu("VIEW3D_MT_KeyframeMenu") + layout.separator() + + layout.menu("VIEW3D_MT_pose_pose") + layout.menu("VIEW3D_MT_pose_motion") + layout.menu("VIEW3D_MT_pose_group") + layout.separator() + + layout.menu("VIEW3D_MT_pose_ik") + layout.menu("VIEW3D_MT_PoseNames") + layout.separator() + + layout.menu("VIEW3D_MT_pose_constraints") + layout.separator() + + + + layout.operator("pose.quaternions_flip") + layout.separator() + + layout.operator_context = 'INVOKE_AREA' + layout.operator("pose.armature_layers", + text="Change Armature Layers...") + layout.operator("pose.bone_layers", text="Change Bone Layers...") + layout.separator() + + layout.menu("VIEW3D_MT_pose_showhide") + layout.operator_menu_enum("pose.flags_set", 'mode', + text="Bone Settings") + + # Toggle Editmode + layout.operator("object.editmode_toggle", text="Enter Edit Mode", + icon='EDITMODE_HLT') + + +class VIEW3D_MT_AddMenu(bpy.types.Menu): + bl_label = "Add Object Menu" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.menu("INFO_MT_mesh_add", text="Add Mesh", + icon='OUTLINER_OB_MESH') + layout.menu("INFO_MT_curve_add", text="Add Curve", + icon='OUTLINER_OB_CURVE') + layout.menu("INFO_MT_surface_add", text="Add Surface", + icon='OUTLINER_OB_SURFACE') + layout.operator_menu_enum("object.metaball_add", "type", + icon='OUTLINER_OB_META') + layout.operator("object.text_add", text="Add Text", + icon='OUTLINER_OB_FONT') + layout.separator() + layout.menu("INFO_MT_armature_add", text="Add Armature", + icon='OUTLINER_OB_ARMATURE') + layout.operator("object.add", text="Lattice", + icon='OUTLINER_OB_LATTICE').type = 'LATTICE' + layout.separator() + layout.operator("object.add", text="Add Empty", + icon='OUTLINER_OB_EMPTY') + layout.separator() + + layout.operator("object.camera_add", text="Camera", + icon='OUTLINER_OB_CAMERA') + layout.operator_menu_enum("object.lamp_add", "type", + icon="OUTLINER_OB_LAMP") + layout.separator() + + layout.operator_menu_enum("object.effector_add", "type", + text="Force Field", + icon='OUTLINER_OB_EMPTY') + layout.operator_menu_enum("object.group_instance_add", "group", + text="Group Instance", + icon='OUTLINER_OB_EMPTY') + + +class VIEW3D_MT_TransformMenu(bpy.types.Menu): + bl_label = "Transform Menu" + + # TODO: get rid of the custom text strings? + def draw(self, context): + layout = self.layout + + layout.operator("transform.translate", text="Grab/Move") + # TODO: sub-menu for grab per axis + layout.operator("transform.rotate", text="Rotate") + # TODO: sub-menu for rot per axis + layout.operator("transform.resize", text="Scale") + # TODO: sub-menu for scale per axis + layout.separator() + + layout.operator("transform.tosphere", text="To Sphere") + layout.operator("transform.shear", text="Shear") + layout.operator("transform.warp", text="Warp") + layout.operator("transform.push_pull", text="Push/Pull") + if context.edit_object and context.edit_object.type == 'ARMATURE': + layout.operator("armature.align") + else: + layout.operator_context = 'EXEC_REGION_WIN' + # @todo vvv See alignmenu() in edit.c of b2.4x to get this working. + layout.operator("transform.transform", + text="Align to Transform Orientation").mode = 'ALIGN' + layout.separator() + + layout.operator_context = 'EXEC_AREA' + + layout.operator("object.origin_set", + text="Geometry to Origin").type = 'GEOMETRY_ORIGIN' + layout.operator("object.origin_set", + text="Origin to Geometry").type = 'ORIGIN_GEOMETRY' + layout.operator("object.origin_set", + text="Origin to 3D Cursor").type = 'ORIGIN_CURSOR' + + +class VIEW3D_MT_MirrorMenu(bpy.types.Menu): + bl_label = "Mirror Menu" + + def draw(self, context): + layout = self.layout + + layout.operator("transform.mirror", text="Interactive Mirror") + layout.separator() + + layout.operator_context = 'INVOKE_REGION_WIN' + + props = layout.operator("transform.mirror", text="X Global") + props.constraint_axis = (True, False, False) + props.constraint_orientation = 'GLOBAL' + props = layout.operator("transform.mirror", text="Y Global") + props.constraint_axis = (False, True, False) + props.constraint_orientation = 'GLOBAL' + props = layout.operator("transform.mirror", text="Z Global") + props.constraint_axis = (False, False, True) + props.constraint_orientation = 'GLOBAL' + + if context.edit_object: + layout.separator() + + props = layout.operator("transform.mirror", text="X Local") + props.constraint_axis = (True, False, False) + props.constraint_orientation = 'LOCAL' + props = layout.operator("transform.mirror", text="Y Local") + props.constraint_axis = (False, True, False) + props.constraint_orientation = 'LOCAL' + props = layout.operator("transform.mirror", text="Z Local") + props.constraint_axis = (False, False, True) + props.constraint_orientation = 'LOCAL' + + layout.operator("object.vertex_group_mirror") + +class VIEW3D_MT_ParentMenu(bpy.types.Menu): + bl_label = "Parent Menu" + + def draw(self, context): + layout = self.layout + + layout.operator("object.parent_set", text="Set") + layout.operator("object.parent_clear", text="Clear") + +class VIEW3D_MT_GroupMenu(bpy.types.Menu): + bl_label = "Group Menu" + + def draw(self, context): + layout = self.layout + + layout.operator("group.create") + layout.operator("group.objects_remove") + + layout.separator() + + layout.operator("group.objects_add_active") + layout.operator("group.objects_remove_active") + +class VIEW3D_MT_AlignMenu(bpy.types.Menu): + bl_label = "Align Menu" + + def draw(self, context): + layout = self.layout + + layout.menu("VIEW3D_MT_view_align_selected") + + layout.separator() + + layout.operator("view3d.view_all", text="Center Cursor and View All").center = True + layout.operator("view3d.camera_to_view", text="Align Active Camera to View") + layout.operator("view3d.view_selected") + layout.operator("view3d.view_center_cursor") + +class VIEW3D_MT_SelectMenu(bpy.types.Menu): + bl_label = "Select Menu" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.operator("view3d.select_border") + layout.operator("view3d.select_circle") + layout.separator() + layout.operator("object.select_all", text="Select/Deselect All") + layout.operator("object.select_inverse", text="Inverse") + layout.operator("object.select_random", text="Random") + layout.operator("object.select_mirror", text="Mirror") + layout.operator("object.select_by_layer", text="Select All by Layer") + layout.operator_menu_enum("object.select_by_type", "type", text="Select All by Type...") + layout.operator("object.select_camera", text="Select Camera") + layout.separator() + layout.operator_menu_enum("object.select_grouped", "type", text="Grouped") + layout.operator_menu_enum("object.select_linked", "type", text="Linked") + layout.operator("object.select_pattern", text="Select Pattern...") + +class VIEW3D_MT_SelectEditMenu(bpy.types.Menu): + bl_label = "Select Menu" + + def draw(self, context): + layout = self.layout + + layout.operator("view3d.select_border") + layout.operator("view3d.select_circle") + + layout.separator() + + layout.operator("mesh.select_all", text="Select/Deselect All") + layout.operator("mesh.select_inverse", text="Inverse") + + layout.separator() + + layout.operator("mesh.select_random", text="Random") + layout.operator("mesh.select_nth", text="Every N Number of Verts") + layout.operator("mesh.edges_select_sharp", text="Sharp Edges") + layout.operator("mesh.faces_select_linked_flat", text="Linked Flat Faces") + layout.operator("mesh.faces_select_interior", text="Interior Faces") + layout.operator("mesh.select_axis", text="Side of Active") + + layout.separator() + + layout.operator("mesh.select_by_number_vertices", text="Triangles").type = 'TRIANGLES' + layout.operator("mesh.select_by_number_vertices", text="Quads").type = 'QUADS' + if context.scene.tool_settings.mesh_selection_mode[2] == False: + layout.operator("mesh.select_non_manifold", text="Non Manifold") + layout.operator("mesh.select_by_number_vertices", text="Loose Verts/Edges").type = 'OTHER' + layout.operator("mesh.select_similar", text="Similar") + + layout.separator() + + layout.operator("mesh.select_less", text="Less") + layout.operator("mesh.select_more", text="More") + + layout.separator() + + layout.operator("mesh.select_mirror", text="Mirror") + + layout.operator("mesh.select_linked", text="Linked") + layout.operator("mesh.select_vertex_path", text="Vertex Path") + layout.operator("mesh.loop_multi_select", text="Edge Loop") + layout.operator("mesh.loop_multi_select", text="Edge Ring").ring = True + + layout.separator() + + layout.operator("mesh.loop_to_region") + layout.operator("mesh.region_to_loop") + +class VIEW3D_MT_SelectCurveMenu(bpy.types.Menu): + bl_label = "Select Menu" + + def draw(self, context): + layout = self.layout + + layout.operator("view3d.select_border") + layout.operator("view3d.select_circle") + + layout.separator() + + layout.operator("curve.select_all", text="Select/Deselect All") + layout.operator("curve.select_inverse") + layout.operator("curve.select_random") + layout.operator("curve.select_every_nth") + + layout.separator() + + layout.operator("curve.de_select_first") + layout.operator("curve.de_select_last") + layout.operator("curve.select_next") + layout.operator("curve.select_previous") + + layout.separator() + + layout.operator("curve.select_more") + layout.operator("curve.select_less") + +class VIEW3D_MT_SelectArmatureMenu(bpy.types.Menu): + bl_label = "Select Menu" + + def draw(self, context): + layout = self.layout + + layout.operator("view3d.select_border") + + + layout.separator() + + layout.operator("armature.select_all", text="Select/Deselect All") + layout.operator("armature.select_inverse", text="Inverse") + + layout.separator() + + layout.operator("armature.select_hierarchy", text="Parent").direction = 'PARENT' + layout.operator("armature.select_hierarchy", text="Child").direction = 'CHILD' + + layout.separator() + + props = layout.operator("armature.select_hierarchy", text="Extend Parent") + props.extend = True + props.direction = 'PARENT' + + props = layout.operator("armature.select_hierarchy", text="Extend Child") + props.extend = True + props.direction = 'CHILD' + + layout.operator("object.select_pattern", text="Select Pattern...") + + +class VIEW3D_MT_SelectPoseMenu(bpy.types.Menu): + bl_label = "Select Menu" + + def draw(self, context): + layout = self.layout + + layout.operator("view3d.select_border") + + layout.separator() + + layout.operator("pose.select_all", text="Select/Deselect All") + layout.operator("pose.select_inverse", text="Inverse") + layout.operator("pose.select_constraint_target", text="Constraint Target") + layout.operator("pose.select_linked", text="Linked") + + layout.separator() + + layout.operator("pose.select_hierarchy", text="Parent").direction = 'PARENT' + layout.operator("pose.select_hierarchy", text="Child").direction = 'CHILD' + + layout.separator() + + props = layout.operator("pose.select_hierarchy", text="Extend Parent") + props.extend = True + props.direction = 'PARENT' + + props = layout.operator("pose.select_hierarchy", text="Extend Child") + props.extend = True + props.direction = 'CHILD' + + layout.separator() + + layout.operator_menu_enum("pose.select_grouped", "type", text="Grouped") + layout.operator("object.select_pattern", text="Select Pattern...") + +class VIEW3D_MT_PoseCopy(bpy.types.Menu): + bl_label = "Pose Copy" + + def draw(self, context): + layout = self.layout + + layout.operator("pose.copy") + layout.operator("pose.paste") + layout.operator("pose.paste", + text="Paste X-Flipped Pose").flipped = True + layout.separator() + +class VIEW3D_MT_PoseNames(bpy.types.Menu): + bl_label = "Pose Copy" + + def draw(self, context): + layout = self.layout + + layout.operator_context = 'EXEC_AREA' + layout.operator("pose.autoside_names", + text="AutoName Left/Right").axis = 'XAXIS' + layout.operator("pose.autoside_names", + text="AutoName Front/Back").axis = 'YAXIS' + layout.operator("pose.autoside_names", + text="AutoName Top/Bottom").axis = 'ZAXIS' + + layout.operator("pose.flip_names") + + +class VIEW3D_MT_SelectSurface(bpy.types.Menu): + bl_label = "Select Menu" + + def draw(self, context): + layout = self.layout + + layout.operator("view3d.select_border") + layout.operator("view3d.select_circle") + + layout.separator() + + layout.operator("curve.select_all", text="Select/Deselect All") + layout.operator("curve.select_inverse") + layout.operator("curve.select_random") + layout.operator("curve.select_every_nth") + + layout.separator() + + layout.operator("curve.select_row") + + layout.separator() + + layout.operator("curve.select_more") + layout.operator("curve.select_less") + +class VIEW3D_MT_SelectMetaball(bpy.types.Menu): + bl_label = "Select Menu" + + def draw(self, context): + layout = self.layout + + layout.operator("view3d.select_border") + + layout.separator() + + #layout.operator("mball.select_deselect_all_metaelems") + layout.operator("mball.select_inverse_metaelems") + + layout.separator() + + layout.operator("mball.select_random_metaelems") + +class VIEW3D_MT_edit_TK(bpy.types.Menu): + bl_label = "Edit Mesh" + + def draw(self, context): + layout = self.layout + row = layout.row() + + layout.operator_context = 'INVOKE_REGION_WIN' + + prop = layout.operator("wm.context_set_value", + text="Select By Vertex", icon='VERTEXSEL') + prop.value = "(True, False, False)" + prop.data_path = "tool_settings.mesh_selection_mode" + layout.menu("VIEW3D_MT_edit_mesh_vertices", icon='VERTEXSEL') + + prop = layout.operator("wm.context_set_value", + text="Select By Edge", icon='EDGESEL') + prop.value = "(False, True, False)" + prop.data_path = "tool_settings.mesh_selection_mode" + layout.menu("VIEW3D_MT_edit_mesh_edges", icon='EDGESEL') + + prop = layout.operator("wm.context_set_value", + text="Select By Face", icon='FACESEL') + prop.value = "(False, False, True)" + prop.data_path = "tool_settings.mesh_selection_mode" + layout.menu("VIEW3D_MT_edit_mesh_faces", icon='FACESEL') + layout.separator() + + layout.menu("VIEW3D_MT_edit_mesh_normals", icon='META_DATA') + layout.operator("mesh.loopcut_slide", + text="Loopcut", icon='EDIT_VEC') + +class VIEW3D_MT_editM_Edge(bpy.types.Menu): + bl_label = "Edges" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.operator("mesh.mark_seam") + layout.operator("mesh.mark_seam", text="Clear Seam").clear = True + layout.separator() + + layout.operator("mesh.mark_sharp") + layout.operator("mesh.mark_sharp", text="Clear Sharp").clear = True + layout.operator("mesh.extrude_move_along_normals", text="Extrude") + layout.separator() + + layout.operator("mesh.edge_rotate", + text="Rotate Edge CW").direction = 'CW' + layout.operator("mesh.edge_rotate", + text="Rotate Edge CCW").direction = 'CCW' + layout.separator() + + layout.operator("TFM_OT_edge_slide", text="Edge Slide") + layout.operator("mesh.loop_multi_select", text="Edge Loop") + layout.operator("mesh.loop_multi_select", text="Edge Ring").ring = True + layout.operator("mesh.loop_to_region") + layout.operator("mesh.region_to_loop") + + +class VIEW3D_MT_EditCurveCtrlpoints(bpy.types.Menu): + bl_label = "Control Points" + + def draw(self, context): + layout = self.layout + + edit_object = context.edit_object + + if edit_object.type == 'CURVE': + layout.operator("transform.transform").mode = 'TILT' + layout.operator("curve.tilt_clear") + layout.operator("curve.separate") + + layout.separator() + + layout.operator_menu_enum("curve.handle_type_set", "type") + + layout.separator() + + layout.menu("VIEW3D_MT_hook") + + +class VIEW3D_MT_EditCurveSegments(bpy.types.Menu): + bl_label = "Curve Segments" + + def draw(self, context): + layout = self.layout + + layout.operator("curve.subdivide") + layout.operator("curve.switch_direction") + +class VIEW3D_MT_EditCurveSpecials(bpy.types.Menu): + bl_label = "Specials" + + def draw(self, context): + layout = self.layout + + layout.operator("curve.subdivide") + layout.operator("curve.switch_direction") + layout.operator("curve.spline_weight_set") + layout.operator("curve.radius_set") + layout.operator("curve.smooth") + layout.operator("curve.smooth_radius") + +class VIEW3D_MT_EditArmatureTK(bpy.types.Menu): + bl_label = "Armature Tools" + + def draw(self, context): + layout = self.layout + + # Edit Armature + + layout.operator("transform.transform", text="Scale Envelope Distance").mode = 'BONESIZE' + + layout.operator("transform.transform", text="Scale B-Bone Width").mode = 'BONESIZE' + + layout.separator() + + layout.operator("armature.extrude_move") + + layout.operator("armature.extrude_forked") + + layout.operator("armature.duplicate_move") + layout.operator("armature.merge") + layout.operator("armature.fill") + layout.operator("armature.delete") + layout.operator("armature.separate") + + layout.separator() + + layout.operator("armature.subdivide_multi", text="Subdivide") + layout.operator("armature.switch_direction", text="Switch Direction") + +class VIEW3D_MT_ArmatureName(bpy.types.Menu): + bl_label = "Armature Name" + + def draw(self, context): + layout = self.layout + + layout.operator_context = 'EXEC_AREA' + layout.operator("armature.autoside_names", text="AutoName Left/Right").type = 'XAXIS' + layout.operator("armature.autoside_names", text="AutoName Front/Back").type = 'YAXIS' + layout.operator("armature.autoside_names", text="AutoName Top/Bottom").type = 'ZAXIS' + layout.operator("armature.flip_names") + layout.separator() + +class VIEW3D_MT_KeyframeMenu(bpy.types.Menu): + bl_label = "Keyframe Menu" + + def draw(self, context): + layout = self.layout + + # Keyframe Bleck + layout.operator("anim.keyframe_insert_menu", + text="Insert Keyframe...") + layout.operator("anim.keyframe_delete_v3d", + text="Delete Keyframe...") + layout.operator("anim.keying_set_active_set", + text="Change Keying Set...") + layout.separator() + +# Classes for VIEW3D_MT_CursorMenu() +class VIEW3D_OT_pivot_cursor(bpy.types.Operator): + "Cursor as Pivot Point" + bl_idname = "view3d.pivot_cursor" + bl_label = "Cursor as Pivot Point" + + def poll(self, context): + return bpy.context.space_data.pivot_point != 'CURSOR' + + def execute(self, context): + bpy.context.space_data.pivot_point = 'CURSOR' + return {'FINISHED'} + +class VIEW3D_OT_revert_pivot(bpy.types.Operator): + "Revert Pivot Point" + bl_idname = "view3d.revert_pivot" + bl_label = "Reverts Pivot Point to median" + + def poll(self, context): + return bpy.context.space_data.pivot_point != 'MEDIAN_POINT' + + def execute(self, context): + bpy.context.space_data.pivot_point = 'MEDIAN_POINT' + # @todo Change this to 'BOUDNING_BOX_CENTER' if needed... + return{'FINISHED'} + +class VIEW3D_MT_CursorMenu(bpy.types.Menu): + bl_label = "Snap Cursor Menu" + + def draw(self, context): + + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + layout.operator("view3d.snap_cursor_to_selected", + text="Cursor to Selected") + layout.operator("view3d.snap_cursor_to_center", + text="Cursor to Center") + layout.operator("view3d.snap_cursor_to_grid", + text="Cursor to Grid") + layout.operator("view3d.snap_cursor_to_active", + text="Cursor to Active") + layout.separator() + layout.operator("view3d.snap_selected_to_cursor", + text="Selection to Cursor") + layout.operator("view3d.snap_selected_to_grid", + text="Selection to Grid") + layout.separator() + layout.operator("view3d.pivot_cursor", + text="Set Cursor as Pivot Point") + layout.operator("view3d.revert_pivot", + text="Revert Pivot Point") + +class VIEW3D_MT_EditCursorMenu(bpy.types.Menu): + bl_label = "Snap Cursor Menu" + + def draw(self, context): + + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + layout.operator("view3d.snap_cursor_to_selected", + text="Cursor to Selected") + layout.operator("view3d.snap_cursor_to_center", + text="Cursor to Center") + layout.operator("view3d.snap_cursor_to_grid", + text="Cursor to Grid") + layout.operator("view3d.snap_cursor_to_active", + text="Cursor to Active") + layout.separator() + layout.operator("view3d.snap_selected_to_cursor", + text="Selection to Cursor") + layout.operator("view3d.snap_selected_to_grid", + text="Selection to Grid") + layout.separator() + layout.operator("view3d.pivot_cursor", + text="Set Cursor as Pivot Point") + layout.operator("view3d.revert_pivot", + text="Revert Pivot Point") + layout.operator("view3d.snap_cursor_to_edge_intersection", + text="Cursor to Edge Intersection") + layout.operator("transform.snap_type", text="Snap Tools", + icon='SNAP_ON') + +def abs(val): + if val > 0: + return val + return -val + +def LineLineIntersect (p1, p2, p3, p4): + # based on Paul Bourke's Shortest Line Between 2 lines + + min = 0.0000001 + + v1 = Vector((p1.x - p3.x, p1.y - p3.y, p1.z - p3.z)) + v2 = Vector((p4.x - p3.x, p4.y - p3.y, p4.z - p3.z)) + + if abs(v2.x) < min and abs(v2.y) < min and abs(v2.z) < min: + return None + + v3 = Vector((p2.x - p1.x, p2.y - p1.y, p2.z - p1.z)) + + if abs(v3.x) < min and abs(v3.y) < min and abs(v3.z) < min: + return None + + d1 = v1.dot(v2) + d2 = v2.dot(v3) + d3 = v1.dot(v3) + d4 = v2.dot(v2) + d5 = v3.dot(v3) + + d = d5 * d4 - d2 * d2 + + if abs(d) < min: + return None + + n = d1 * d2 - d3 * d4 + + mua = n / d + mub = (d1 + d2 * (mua)) / d4 + + return [Vector((p1.x + mua * v3.x, p1.y + mua * v3.y, p1.z + mua * v3.z)), + Vector((p3.x + mub * v2.x, p3.y + mub * v2.y, p3.z + mub * v2.z))] + +def edgeIntersect(context, operator): + + obj = context.active_object + + if (obj.type != "MESH"): + operator.report({'ERROR'}, "Object must be a mesh") + return None + + edges = []; + mesh = obj.data + verts = mesh.verts + + is_editmode = (obj.mode == 'EDIT') + if is_editmode: + bpy.ops.object.mode_set(mode='OBJECT') + + for e in mesh.edges: + if e.select: + edges.append(e) + + if is_editmode: + bpy.ops.object.mode_set(mode='EDIT') + + if len(edges) != 2: + operator.report({'ERROR'}, "Operator requires exactly 2 edges to be selected.") + return + + line = LineLineIntersect(verts[edges[0].verts[0]].co, verts[edges[0].verts[1]].co, verts[edges[1].verts[0]].co, verts[edges[1].verts[1]].co) + + if (line == None): + operator.report({'ERROR'}, "Selected edges are parallel.") + return + + tm = obj.matrix_world.copy() + point = ((line[0] + line[1]) / 2) + point = tm * point + + context.scene.cursor_location = point + + return point + +class VIEW3D_OT_CursorToEdgeIntersection(bpy.types.Operator): + "Finds the mid-point of the shortest distance between two edges" + + bl_idname = "view3d.snap_cursor_to_edge_intersection" + bl_label = "Cursor to Edge Intersection" + + def poll(self, context): + obj = context.active_object + return obj != None and obj.type == 'MESH' + + def execute(self, context): + edgeIntersect(context, self) + return {'FINISHED'} + +class VIEW3D_MT_undoS(bpy.types.Menu): + bl_label = "Undo/Redo" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + layout.operator("ed.undo", icon='TRIA_LEFT') + layout.operator("ed.redo", icon='TRIA_RIGHT') + +def register(): + bpy.types.register(VIEW3D_MT_Space_Dynamic_Menu) + bpy.types.register(VIEW3D_MT_AddMenu) + bpy.types.register(VIEW3D_MT_TransformMenu) + bpy.types.register(VIEW3D_MT_MirrorMenu) + bpy.types.register(VIEW3D_MT_ParentMenu) + bpy.types.register(VIEW3D_MT_GroupMenu) + bpy.types.register(VIEW3D_MT_AlignMenu) + bpy.types.register(VIEW3D_MT_SelectMenu) + bpy.types.register(VIEW3D_MT_SelectEditMenu) + bpy.types.register(VIEW3D_MT_SelectCurveMenu) + bpy.types.register(VIEW3D_MT_SelectPoseMenu) + bpy.types.register(VIEW3D_MT_PoseCopy) + bpy.types.register(VIEW3D_MT_PoseNames) + bpy.types.register(VIEW3D_MT_KeyframeMenu) + bpy.types.register(VIEW3D_MT_SelectArmatureMenu) + bpy.types.register(VIEW3D_MT_ArmatureName) + bpy.types.register(VIEW3D_MT_SelectMetaball) + bpy.types.register(VIEW3D_MT_SelectSurface) + bpy.types.register(VIEW3D_MT_edit_TK) + bpy.types.register(VIEW3D_MT_EditArmatureTK) + bpy.types.register(VIEW3D_MT_editM_Edge) + bpy.types.register(VIEW3D_MT_EditCurveCtrlpoints) + bpy.types.register(VIEW3D_MT_EditCurveSegments) + bpy.types.register(VIEW3D_MT_EditCurveSpecials) + bpy.types.register(VIEW3D_OT_pivot_cursor) + bpy.types.register(VIEW3D_OT_revert_pivot) + bpy.types.register(VIEW3D_MT_CursorMenu) + bpy.types.register(VIEW3D_MT_EditCursorMenu) + bpy.types.register(VIEW3D_OT_CursorToEdgeIntersection) + bpy.types.register(VIEW3D_MT_undoS) + + km = bpy.context.manager.active_keyconfig.keymaps['3D View'] + kmi = km.items.add('wm.call_menu', 'SPACE', 'PRESS') + kmi.properties.name = "VIEW3D_MT_Space_Dynamic_Menu" + + +def unregister(): + bpy.types.unregister(VIEW3D_MT_Space_Dynamic_Menu) + bpy.types.unregister(VIEW3D_MT_AddMenu) + bpy.types.unregister(VIEW3D_MT_TransformMenu) + bpy.types.unregister(VIEW3D_MT_MirrorMenu) + bpy.types.unregister(VIEW3D_MT_ParentMenu) + bpy.types.unregister(VIEW3D_MT_GroupMenu) + bpy.types.unregister(VIEW3D_MT_AlignMenu) + bpy.types.unregister(VIEW3D_MT_SelectMenu) + bpy.types.unregister(VIEW3D_MT_SelectEditMenu) + bpy.types.unregister(VIEW3D_MT_SelectCurveMenu) + bpy.types.unregister(VIEW3D_MT_SelectPoseMenu) + bpy.types.unregister(VIEW3D_MT_PoseCopy) + bpy.types.unregister(VIEW3D_MT_PoseNames) + bpy.types.unregister(VIEW3D_MT_KeyframeMenu) + bpy.types.unregister(VIEW3D_MT_SelectArmatureMenu) + bpy.types.unregister(VIEW3D_MT_ArmatureName) + bpy.types.unregister(VIEW3D_MT_SelectMetaball) + bpy.types.unregister(VIEW3D_MT_SelectSurface) + bpy.types.unregister(VIEW3D_MT_edit_TK) + bpy.types.unregister(VIEW3D_MT_EditArmatureTK) + bpy.types.unregister(VIEW3D_MT_editM_Edge) + bpy.types.unregister(VIEW3D_MT_EditCurveCtrlpoints) + bpy.types.unregister(VIEW3D_MT_EditCurveSegments) + bpy.types.unregister(VIEW3D_MT_EditCurveSpecials) + bpy.types.unregister(VIEW3D_OT_pivot_cursor) + bpy.types.unregister(VIEW3D_OT_revert_pivot) + bpy.types.unregister(VIEW3D_MT_CursorMenu) + bpy.types.unregister(VIEW3D_MT_EditCursorMenu) + bpy.types.unregister(VIEW3D_OT_CursorToEdgeIntersection) + bpy.types.unregister(VIEW3D_MT_undoS) + + km = bpy.context.manager.active_keyconfig.keymaps['3D View'] + for kmi in km.items: + if kmi.idname == 'wm.call_menu': + if kmi.properties.name == "VIEW3D_MT_Space_Dynamic_Menu": + km.remove_item(kmi) + break + +if __name__ == "__main__": + register() -- cgit v1.2.3