useful for architectural modelling: move face loops by face normal

This is the final version of the script!!! It now moves the faces, skins/bridges the new and old edges as expected, and deletes the original selected faces. [I’ll leave the original script in here also, so you can see how the script evolved]
EDIT (2007-07-23): added a line to recalculate the newly created face normals. Thanks to Ubuntuist for pointing out this problem.


#!BPY

""" Registration info for Blender menus: <- these words are ignored
Name: 'Expand face loops along face normals (skin+delete)'
Blender: 244
Group: 'Object'
Tip: 'Expands the face loop selection based on the face normal directions (skins and deletes originals)'
"""

__author__ = "René Pihlak aka Daredemo"
__version__ = "1.1.2 2007/07"

__bpydoc__ = """\
"Expand face loops along face normals" moves vertices of selected faces based on the face normal direction.


Usage:

Select some faces and run it in Object mode
"""


import Blender as B
from Blender import Mesh as M
from Blender import Mathutils
from Blender.Mathutils import *
from Blender import *

def deform_norm(o):
	
	f = B.Draw.Create(0.2)
	block = []
	block.append(("Expansion value  ", f, -30.0, 30.0, "Enter extrusion value to move verts based on face normal"))
	retval = B.Draw.PupBlock("Expand by", block)
	
	FACT = f.val
	
	v_arr = []
	v_arr_s = []
	s_arr_f = []

	m_mesh = o.getData(mesh=True)
	
	vrt = m_mesh.verts
	tot_v = len(m_mesh.verts)
	
	for s_vrt in vrt:
		v_arr.append((s_vrt.co, s_vrt.no, (0, 0,)))
		
	s_mesh = [ff for ff in m_mesh.faces if ff.sel]
	
	ft_a = []
	f_v_tot = []
	f_e_tot = []

	#Count the index of the vert in the "old" array
	new_count = 0
	#Count the new vertices that will be made
	new_count2 = 0
	for ft, f_val in enumerate(s_mesh):
		fv_a = []
		fv_f = []
		f_v_tot.append(len(f_val.v))
		#Finding the vertice pairs that make edges of given faces and adding to an array
		for f_key in f_val.edge_keys:
			f_e_tot.append(f_key)
		for fg in f_val.v:
#Add the data to the "old" array
			v_arr_s.append(fg.index)
#Check if the this is the first occurence of this vertex
			if new_count == v_arr_s.index(fg.index):
#If it's first occurence, add a new count for it
				s_arr_f.append((tot_v+new_count2,fg.index))
				new_count2 += 1
			else:
#If it's already used vertex, refer to the previous occurence in the "new" array
				fbh = [fh[1] for fh in s_arr_f]
				s_arr_f.append((s_arr_f[fbh.index(fg.index)][0],fg.index))
			new_count += 1
	

	#If an edge is listed more than once, it's in between faces and we'll ignore it
	f_e_u = [f_sel for f_sel in f_e_tot if f_e_tot.count(f_sel) == 1]
	
	#list of currently selected faces to delete in the end
	F_DEL = m_mesh.faces.selected()
		

	for fc, val in enumerate(m_mesh.faces):
		c_mesh = m_mesh.faces[fc]
		c_m_no = Vector(c_mesh.no)
		if c_mesh.sel:
			for vt, v_val in enumerate(c_mesh.v):
				v_arr.insert(v_val.index, (v_arr[v_val.index][0], v_arr[v_val.index][1], (v_arr[v_val.index][2][0],1)))
				v_arr.pop(v_val.index+1)
				v_no = v_val.no
#Find the length of the projection of the vertex normal to the face normal
				vt_prj = ProjectVecs(v_val.no, c_m_no)
				prj_len = (vt_prj[0]**2 + vt_prj[1]**2 + vt_prj[2]**2)**0.5
				if prj_len <> 1:
					if v_arr[v_val.index][2][0] == 0:
#If the vertex normal is not identical to face normal, scale the vertice normal
						v_arr.insert(v_val.index, (v_arr[v_val.index][0], (v_arr[v_val.index][1][0]/prj_len, v_arr[v_val.index][1][1]/prj_len, v_arr[v_val.index][1][2]/prj_len), (1, v_arr[v_val.index][2][1])))
						v_arr.pop(v_val.index+1)
						
#Extend the vertices
	EXTEND = []
	face_count = 0
	vert_count = 0
	for fr in f_v_tot:
		for fx in range(vert_count,vert_count+fr):
			if fx == v_arr_s.index(v_arr_s[fx]):
				a,b,c = v_arr[v_arr_s[fx]][0]
				a += v_arr[v_arr_s[fx]][1][0]*FACT
				b += v_arr[v_arr_s[fx]][1][1]*FACT
				c += v_arr[v_arr_s[fx]][1][2]*FACT
				EXTEND.append((a,b,c))
		vert_count += fr
	m_mesh.verts.extend(EXTEND)

#Extend faces
	EXTEND = [fext[0] for fext in s_arr_f]
	for fz in f_v_tot:
		gg = []
		for gx in range(face_count,face_count+fz):
			gg.append(EXTEND[gx])
		face_count +=fz
		m_mesh.faces.extend(gg)
		
	#Extend the edges [filling the caps]
	for this_edge in f_e_u:
		EXTEND = []
		EXTEND.append(int(this_edge[0]))
		EXTEND.append(int(this_edge[1]))
		old_new = [fer[1] for fer in s_arr_f]
		old_v3 = old_new.index(this_edge[1])
		old_v4 = old_new.index(this_edge[0])
		EXTEND.append(s_arr_f[old_v3][0])
		EXTEND.append(s_arr_f[old_v4][0])
		#print this_edge[0], this_edge[1]
		m_mesh.faces.extend(EXTEND)
	
	#Delete the original selected /faces
	m_mesh.faces.delete(1,F_DEL)
	
	#Update the mesh
	m_mesh.update()
	B.Redraw(-1)
	#Recalculate normals
	m_mesh.recalcNormals()
	if FACT < 0:
		m_mesh.flipNormals()


obj = B.Scene.GetCurrent().objects.active
if obj.getData(mesh=1):
	deform_norm(obj)

Thought I’ll write a smiple script to move vertices based on the face normals [yes, yes, I know of Alt-S, but it doesn’t work on Mac OS X builds… well, not on PPC… or it works but gives completely ugly results].

Anyway, here’s the script. How do i make the verts move?

import Blender as B
from Blender import Mesh as M
from Blender import Mathutils
from Blender.Mathutils import *
from Blender import *

def deform_norm(o):
v_arr = []
m_mesh = o.getData()

vrt = m_mesh.verts

for vr, s_vrt in enumerate(vrt):
#making and array with vert coordinates [I want to add extrusion to the script in the end… so I though I might need to know the coordinates, so, never mind the coordinates part], the normals, and two booleans, one is if it’s selected, and the other is if the normal has the same normal as the face
v_arr.append((s_vrt.co, s_vrt.no, (0, 0)))

for fc, val in enumerate(m_mesh.faces):
c_mesh = m_mesh.faces[fc]
c_m_no = Vector(c_mesh.no)
if c_mesh.sel:
for vt, v_val in enumerate(c_mesh.v):
#Changing the “is selected” boolean flag
v_arr.insert(v_val.index, (v_arr[v_val.index][0], v_arr[v_val.index][1], (v_arr[v_val.index][2][0],1)))
#after inserting a new value, I delete the old
v_arr.pop(v_val.index+1)
v_no = v_val.no

  		vt_prj = ProjectVecs(v_val.no, c_m_no)

#Calculate the projection of the vert normal on the face normal
prj_len = (vt_prj[0]**2 + vt_prj[1]**2 + vt_prj[2]**2)**0.5
if prj_len <> 1:
if v_arr[v_val.index][2][0] == 0:

#Avoid writing the same vert data multiple times
v_arr.insert(v_val.index, (v_arr[v_val.index][0], (v_arr[v_val.index][1][0]/prj_len, v_arr[v_val.index][1][1]/prj_len, v_arr[v_val.index][1][2]/prj_len), (1, v_arr[v_val.index][2][1])))
v_arr.pop(v_val.index+1)

#Now this should change the coordinates by 0.3 Blender units along the face normal… but…
for ee, v_a in enumerate(v_arr):
if v_a[2][1] == 1:
m_mesh.verts[ee].co[0] = v_a[0][0] + 0.3 * v_a[1][0]
m_mesh.verts[ee].co[1] = v_a[0][1] + 0.3 * v_a[1][1]
m_mesh.verts[ee].co[2] = v_a[0][2] + 0.3 * v_a[1][2]

p = (v_a[0][0] + 0.2 * v_a[1][0], v_a[0][1] + 0.2 * v_a[1][1], v_a[0][2] + 0.2 * v_a[1][2])

  	B.Redraw(-1)

obj = B.Scene.GetCurrent().objects.active
if obj.getData(mesh=1):
deform_norm(obj)

import Blender as B
from Blender import Mesh as M
from Blender import Mathutils
from Blender.Mathutils import *

def deform_norm(o):
	v_arr = []
	m_mesh = o.getData()
	
	vrt = m_mesh.verts
	
	for vr, s_vrt in enumerate(vrt):
		v_arr.append(((s_vrt.co[0], s_vrt.co[1], s_vrt.co[2]), (s_vrt.no[0],s_vrt.no[1],s_vrt.no[2]), (0, 0)))

	for fc, val in enumerate(m_mesh.faces):
		c_mesh = m_mesh.faces[fc]
		c_m_no = Vector(c_mesh.no)
		if c_mesh.sel:
			for vt, v_val in enumerate(c_mesh.v):
				v_arr.insert(v_val.index, (v_arr[v_val.index][0], v_arr[v_val.index][1], (v_arr[v_val.index][2][0],1)))
				v_arr.pop(v_val.index+1)
				v_no = v_val.no
				vt_prj = ProjectVecs(v_val.no, c_m_no)
				prj_len = (vt_prj[0]**2 + vt_prj[1]**2 + vt_prj[2]**2)**0.5
				if prj_len &lt;&gt; 1:
					if v_arr[v_val.index][2][0] == 0:
						v_arr.insert(v_val.index, (v_arr[v_val.index][0], (v_arr[v_val.index][1][0]/prj_len, v_arr[v_val.index][1][1]/prj_len, v_arr[v_val.index][1][2]/prj_len), (1, v_arr[v_val.index][2][1])))
						v_arr.pop(v_val.index+1)

	for ee, v_a in enumerate(v_arr):
		if v_a[2][1] == 1:
			m_mesh.verts[ee][0] = v_a[0][0] + 0.3 * v_a[1][0]
			m_mesh.verts[ee][1] = v_a[0][1] + 0.3 * v_a[1][1]
			m_mesh.verts[ee][2] = v_a[0][2] + 0.3 * v_a[1][2]

	m_mesh.update()
	B.Redraw(-1)

obj = B.Scene.GetCurrent().objects.active
if obj.getData(mesh=1):
	deform_norm(obj)

thanks for Brandano for his patience.

I make lot of archtectural stuff and this is really useful, thanks!

It would be even MORE useful if it had an option to create faces to sides like extrusion does :slight_smile:

I’m on it. That’s why I have so much info stored on the arrays at the moment because I’m trying to find a way to make new verts and connect them with the original edges. WIP. I’ll keep you informed how this all works out… maybe tonight… as my modelling has taken gone on vacation having been programming RenderMan shaders all day.

I’ve already added a popup where you can enter the numeric value by which you can deform it.

Entirely new version. Instead of just moving the faces, it now creates a new “ring” of faces that are moved based on the original faces’ normals. Also new feature is the popup that asks for a numeric value for the displacement.

Comments, suggestions?
Next step would be to get the new faces and the body connect, and then remove the old faces.

#!BPY

""" Registration info for Blender menus: &lt;- these words are ignored
Name: 'Expand face loops along face normals'
Blender: 244
Group: 'Object'
Tip: 'Expands the face loop selection based on the face normal directions'
"""

__author__ = "René Pihlak aka Daredemo"
__version__ = "1.0 2007/07"

__bpydoc__ = """\
"Expand face loops along face normals" moves vertices of selected faces based on the face normal direction.

Usage:

Select some faces and run it in Object mode
"""


import Blender as B
from Blender import Mesh as M
from Blender import Mathutils
from Blender.Mathutils import *
from Blender import *

def deform_norm(o):
	
	f = B.Draw.Create(0.2)
	block = []
	block.append(("Expansion value  ", f, -30.0, 30.0, "Enter extrusion value to move verts based on face normal"))
	retval = B.Draw.PupBlock("Expand by", block)
	
	FACT = f.val
	
	v_arr = []
	v_arr_s = []
	s_arr_f = []

	m_mesh = o.getData(mesh=True)
	
	vrt = m_mesh.verts
	tot_v = len(m_mesh.verts)
	
	for s_vrt in vrt:
		v_arr.append((s_vrt.co, s_vrt.no, (0, 0, 0, 0)))
		
	s_mesh = [ff for ff in m_mesh.faces if ff.sel]
	
	ft_a = []
	f_v_tot = []

	#Count the index of the vert in the "old" array
	new_count = 0
	#Count the new vertices that will be made
	new_count2 = 0
	for ft, f_val in enumerate(s_mesh):
		fv_a = []
		fv_f = []
		f_v_tot.append(len(f_val.v))
		for fg in f_val.v:
#Add the data to the "old" array
			v_arr_s.append(fg.index)
#Check if the this is the first occurence of this vertex
			if new_count == v_arr_s.index(fg.index):
#If it's first occurence, add a new count for it
				s_arr_f.append((tot_v+new_count2,fg.index))
				new_count2 += 1
			else:
#If it's already used vertex, refer to the previous occurence in the "new" array
				fbh = [fh[1] for fh in s_arr_f]
				s_arr_f.append((s_arr_f[fbh.index(fg.index)][0],fg.index))
			new_count += 1	

	for fc, val in enumerate(m_mesh.faces):
		c_mesh = m_mesh.faces[fc]
		c_m_no = Vector(c_mesh.no)
		if c_mesh.sel:
			for vt, v_val in enumerate(c_mesh.v):
				v_arr.insert(v_val.index, (v_arr[v_val.index][0], v_arr[v_val.index][1], (v_arr[v_val.index][2][0],1)))
				v_arr.pop(v_val.index+1)
				v_no = v_val.no
#Find the length of the projection of the vertex normal to the face normal
				vt_prj = ProjectVecs(v_val.no, c_m_no)
				prj_len = (vt_prj[0]**2 + vt_prj[1]**2 + vt_prj[2]**2)**0.5
				if prj_len &lt;&gt; 1:
					if v_arr[v_val.index][2][0] == 0:
#If the vertex normal is not identical to face normal, scale the vertice normal
						v_arr.insert(v_val.index, (v_arr[v_val.index][0], (v_arr[v_val.index][1][0]/prj_len, v_arr[v_val.index][1][1]/prj_len, v_arr[v_val.index][1][2]/prj_len), (1, v_arr[v_val.index][2][1])))
						v_arr.pop(v_val.index+1)
						
#Extend the vertices
	EXTEND = []
	face_count = 0
	vert_count = 0
	for fr in f_v_tot:
		for fx in range(vert_count,vert_count+fr):
			if fx == v_arr_s.index(v_arr_s[fx]):
				a,b,c = v_arr[v_arr_s[fx]][0]
				a += v_arr[v_arr_s[fx]][1][0]*FACT
				b += v_arr[v_arr_s[fx]][1][1]*FACT
				c += v_arr[v_arr_s[fx]][1][2]*FACT
				EXTEND.append((a,b,c))
		vert_count += fr
	m_mesh.verts.extend(EXTEND)

#Extend faces
	EXTEND = [fext[0] for fext in s_arr_f]
	for fz in f_v_tot:
		gg = []
		for gx in range(face_count,face_count+fz):
			gg.append(EXTEND[gx])
		face_count +=fz
		m_mesh.faces.extend(gg)
	

	m_mesh.update()
	B.Redraw(-1)

obj = B.Scene.GetCurrent().objects.active
if obj.getData(mesh=1):
	deform_norm(obj)

just noticed that it completely hates tris :confused:
not yet sure why. too late tonight to check it.

EDIT: code updated and tris problem solved

Added the final version to the first post.
Now featuring:

  • makes new faces
  • moves the faces based on the original face normals
  • bridges/skins the cap between old and new faces
  • deletes the original selected faces

Since so far the ALT-S is not working on Mac OS X (10.4.10 PPC), I decided to rewrite the whole thing. On my Mac, ALT-S messes up the normals of vertices that are on the outer rim/edge and makes it impractical to use. Here’s a fix. Enjoy it.

This is great!

However, there seems to be some problems with normals. Side faces has some times incorrect normals.

Here is a test file:
http://www.opendimension.org/blender/images/extrude_normals.blend

Maybe some usability improvements? Check for edit mode and popup for non-active/non-valid object:

 
import BPyMessages

.....


# save Editmode state so we can restore it afterwards
editmode = Window.EditMode()
# if user is in editmode, we have to change to objectmode  
if editmode: Window.EditMode(0) 


obj = B.Scene.GetCurrent().objects.active
if obj == None or obj.type != 'Mesh':
    BPyMessages.Error_NoMeshActive()
else:    
    deform_norm(obj)
    
#if user was in edit mode, then we should't change that
if editmode: Window.EditMode(1)


I see no error in the supplied .blend file. The “Side faces” are exactly the way as I would have wanted them to be. Verts that make the faces are all moved along the vertex normal so that they are by some user defined number of blender units from the original face along the face normal.

If you are looking for some different functionality… then let me know.

Yes, I could, or maybe should make some tests to check the object selection, etc, but having been busy modelling this weekend, it will have to come at later time. I was pretty happy with the script, and the last two days of using it have revealed very little problems with it. Soon I’ll show the house I’m (re-)making.

One of the troubles I have currently is with vert deletition. When the factor is negative, it leaves a cloud of verts behind where the original faces where. it’s not must of a trouble to clean it manually, but it’s not very elegent at the moment. THAT will be my next “bug” to get rid off.

EDITED:
Oh, and the script doesn’t work in Edit Mode. And I’m not going to check if edit mode is on. :stuck_out_tongue:
Just install the script in the scripts folder and use it from the menu. It will ONLY appear in the Object mode.
In Object mode: Menu > Object > Scripts > myscript

I meant this:

The selected faces points downwards. I think they should point upwards?

Anyway, thanks for sharing this script. It is usefull allready.

Just re-calculate the normals.
I’m not sure if the script has to re-calculate the normals, maybe. I’ll think if it.
You have to understand that the script is not “extruding” the faces like you do manually. It makes some number of vertices and then starts to connect them one by one [well, 3 or 4 at a time]. It doesn’t really know, or care about the normals at the time of greating new faces.

Done. Added one line [and two lines of comments] to the first post script:

m_mesh.recalcNormals()

that should do it.

EDIT: upped version to v1.1.2 because there’s so many little changes :stuck_out_tongue:
OK, seems that recalc.Normals() is not very smart and if if the factor is negative [we pull the faces inside], then the normals don’t point outside after recalculating the normals, go figure. so I added this:


if FACT &lt; 0:
   m_mesh.flipNormals()

NOW it should work. let me know if you find more bugs

Thanks! Now works perfectly!

as a warning, in some complicated situations, some of the normals might still get reversed. so, sometimes you’d still need to flip the normals manually. I’m investigating the cause of it. although it’s not very likely scenario