#!BPY
#"""
#Name: 'Handle Panel'
#Blender: 249
#Group: 'Misc'
#Tooltip: 'A GUI to control the handles of the model moveable parts'
#"""
####---Header------------------------------------------------------------------
__author__ = "Witold.Jaworski"
__url__ = ("http://www.blender.org","http://airplanes3d.net")
__email__ = ("me@wjaworski.pl")
__version__ = "1.20"
__bpydoc__ = """\
This script allows to change position of model's moveable parts through a GUI.
What is a moveable part?
When you build a detailed model of a car, an aeroplane, or other machine,
you can make some of its assemblies moveable. For example - aeroplane's
undercarriage, which can be extended for the landing or hidden for the flight.
Usually moving such assebly means a rather complicated movement of many
connected objects, cooperating together. There are many axis, bolts and struts
in the typical undercarriage. Some of them rotate, the others shift, when the
wheel is changing its position. Such assembly we call here a 'moveable part'.
Positioning of moveable part - the handles
It would be very difficult to put such assembly as aeroplane undercarriage in a
desired, realistic position, changing its elements one by one. To manage it, the
elements of moveable part are connected by parent-child realtions. Usage of
some constraints (mainly the Track To) are also required. At the end, you will
reach situation, when movement of whole assembly will depend on one object, which
controls the whole part. Such object is called here a 'moveable part handle',
or, shorter, a 'handle. For example, the steering wheel in a car can be a handle
that controls the twist of the forward wheels.
Which objects are the best for being handles?
In practice of digital models, the steering wheel is not, in fact, the best
candidate for the handle for twisting wheels. Sometimes it may be difficult to
reach it by mouse, because it is inside the car. In general, steering wheel
rotates, and this kind of movement in Blender is a little bit more difficult to
perform than a translation. (It is much more easier in Blender to grab and
move object, than to grab and rotate, because before the rotation sometimes you
have to switch the rotation axis mode). All this leads to conclusion, that it is
better to create an artifical, easily accesible handle for an moveable part, than
use the real-world counerpart for this role.
Such artifical handle, placed somewhere outside the car, can control both:
twisting of forward wheels, and correspoding steering wheel rotation. It may be
placed on a 'technical' layer, intended to be invisible during renderings.
Restricting the possible positions of the handle:
Every mechanical assembly has certain limitations, that restricts its possible
position. Car's forward wheels for example - they can be twisted by about +/-
45 degrees, no more. These limitations can modeled by a Limit Location
constraint, applied to the handle. Without them the user would be able to twist
the wheels by 90 degrees, making unrealistic collisions of some objects from
their assembly.
Why this script is needed:
There is a problem with the 'Limit Location' constraint: when you restrict handle
position to a single segment of, let's say, X axis, it is paralel to the world
X axis. Setting the [Local] toggle on Limit Location constraint changes only the
X origin to the center of handle's parent object. It does not change the axis
direction
There will probably no problem with it as long, as your model is in the default
position, in which it was built. The handle moves along axis set by 'Limit
Location' constraint, and for this movement the moveable pat is properly tuned.
But when you will try to rotate whole model - the orientation of the 'Limit
Location' axis remains unchanged in the world system. In effect, the handle
position can appear in improper place. Some of the moveable parts can "go away"
their intended placements.
To avoid this effect, this script was prepared. The influence factor of handle's
'Limit Location' constraints should be set to 0, making it inactive. The idea
is that this script will find the handles in the model, and use the information
from inactive 'Limit Location' constraints to set them in proper position.
Position of the handle will be restricted according the 'Limit Location'
settings, but aligned to handle's parent object orientation. Thus, every handle
should have a parent. Parent's origin is treated as default position of the
handle.
So, to use this script, create a 'Limit Location' constraint to every handle,
test everything, but at the end decrease influence factor of the constraint to
0.
This script will display on its screen a slider for every handle, found in the
model. Changing the slider position will move the handle between the set limits,
but in the LOCAL coordinates of its parent. This way you can still control the
moveable part, and it does not depended on orientation of the whole model!
You can create as many handles, as you like in your model. A an object is
recognized by this script as a handle, when:
- it has name, that ends with '.Handle' suffix;
- it has a Limit Location constraint, that restricts its movements,
with toggle [Local] set and influence factor = 0;
- it has a parent object. Its origin is a handle neutral position;
Script usage
Every handle is represented on the screen by a rectangle, which contains handle's
name and a tabular control. Subsequent tabs ('Pos','Label','Cockpit') contain
groups of controls.
Pos tab:
Contains a 'Position' slider, which allows you to move the handle. Moveable part
will follow. The effect will be visible when you release the slider handle (if
you are doing it with mouse) or press [ENTER] (if you are typing the value from
the keyboard). There is also a 'Reset' button. It places the handle in neutral
position. (i.e. at origin of the handle's parent object - it depends on your
model, where it is).
Label tab:
Contains a text input field 'Label', where you can change the name of this
handle, displayed on the GUI screen. (It does NOT change the name of the
Blender's handle object - it is just a 'decoration'). Initially, the displayed
name is the same as object's name, but it can be different. Additional 'Apply'
button has the same effect as pressing [Enter] on 'Label' field - it changes the
hande's name, displayed on the script GUI screen.
Cockpit tab:
Sometimes it would be too difficult to synchronize the movement of a model part
with proper movement of its model control - for example synchronize the movement
of the aeroplane's elevator with rotation of the stick in the cockpit.
Here this script comes in help. You can assign to a handle one additional object
(intentionally - from the cockpit, but this is only suggestion). On this tab you
can specify:
- connected object name ('OB:' text input field);
- which coordinate will be changed, proportionally to the handle ('Dim');
- the range of the change ('Min' and 'Max' number inputs);
Comments on usage:
1. Dragging slider handle with mouse, (on the Pos tab) try to release it over
the slider - otherwise the part may not move. In case of any doubt - you can
enter the % of position manually, treating slider as normal text input.
2. You can easily arrange the order of the controls, begining their labels with
an ordnal number (for example - '1. Rudder', '2. Eleveator', and so on).
Handles are sorted at the begining of the script, so they will be aranged to
new labels when you open the script again.
3. Sometimes buttons from the inactive tabs may appear on the screen. I was
not able to fix this bug - just click another tab, and then return to the
previous one to neglect this effect.
Update log:
2010-07-26: Adaptation to the absolute coordinates of 2.49's Limit Location
constrains. (Earlier Blender versions, like 2.43, used relative Limit Location
coordinates)
"""
####imports-----------------------------------------------------------------
import math
from Blender import *
####constants---------------------------------------------------------------
HANDLE_SUFFIX = '.Handle' #all object handle names should have this suffix
X = 0 #offset for the X coordinate in three-element lists (for code brevity)
Y = 1 #offset for the Y coordinate in three-element lists (for code brevity)
Z = 2 #offset for the Z coordinate in three-element lists (for code brevity)
#modules for GUI metrics:
GUI_W = Draw.GetStringWidth('U') #Letter width: based on the 'U'
#- a letter not too wide, not too thin...
GUI_H = 2 * GUI_W #Text's height
GUI_SEP = GUI_H / 2 #Interline separator
GUI_LN = GUI_H + GUI_SEP #Full line height: the text and the separator
#event ids:
EVT_BASE = 10 #number of possible events id per Handle object
EVT_EXIT = 1 #events 1..9 are reserved for the "framework"
#events reminders, used in Handle class:
EV_SLIDER = 0 #Pos tab: Slider has been changed
EV_RESET = 1 #Pos tab: [Reset] button has been clicked
EV_LABEL = 2 #Label tab: label field has been changed
EV_APPLY = 3 #Label tab: [apply] button has been clicked
EV_COBJECT = 4 #Cockpit tab: name of the object has been changed
EV_CSELECT = 5 #Cockpit tab: [...] button was clicked
EV_CDIM = 6 #Cockpit tab: dimension menu has been changed
EV_CMIN = 7 #Cockpit tab: min value has been changed
EV_CMAX = 8 #Cockpit tab: max value has been changed
#names of properties that Handle may create and use on the Blender object:
PROP_LABEL = "HANDLE:label" #Label of the handle, to be displayed
PROP_OBJECT = "HANDLE:object" #name of optional object from the cockpit,
#that also will be controlled by this handle
PROP_DIM = "HANDLE:dim" #dimension of the cockpit object that will
#be controlled by the handle
PROP_MIN = "HANDLE:min" #minimum value of controlled dimension
PROP_MAX = "HANDLE:max" #maximum value of controlled dimension
#id for dimensions (used in the handle GUI, on the menu button 'Dimension')
XPOS = 1
YPOS = 2
ZPOS = 3
XROT = 4
YROT = 5
ZROT = 6
####globals-----------------------------------------------------------------
handles = [] #list of the Handle objects, found in current scene
####tabular control classes----------------------------------------------------
class Rounded:
"""Base class for tabular control: can draw rectangles with rounded corners
"""
#class properties
SlicesPerRound = 3 #number of the pie-slices per round
Radius = 4.0 #size of the rounding
#properties that can be redefined in instance constructor
foreground = [0.0,0.0,0.0] #default contour color: black
background = [1.0,1.0,1.0] #default background color: white
#methods
def RoundCorner(self,x,y,r,start,end,foreground=None,background=None):
"""Draws a rounded corner, with circle center at x,y
Arguments:
x: x coordinate of the rounding center (float)
y: y coordinate of the rounding center (float)
r: radius of the rounding (float)
start: start angle of the rounding, in degrees (float)
end: end angle of the rounding, in degrees (float)
foreground: optional: non-default contour color (3 floats)
background: optional: non-default rounding inner color (3 floats)
"""
ang = [] #list of the vertice angles (radians, relative to the center)
angle = (end-start)*math.pi/180.0 #the corner angle (usually pi/2)
start = start*math.pi/180.0 #starting angle of the corner
for i in range(self.SlicesPerRound+1):
ang.append(start + angle*i/self.SlicesPerRound)
p = [] #list of the vertices (without center vertex)
for i in range(self.SlicesPerRound+1):
p.append([x+r*math.cos(ang[i]),y+r*math.sin(ang[i])])
#let's draw the background slices:
if not background : background = self.background
BGL.glColor3f(background[0],background[1],background[2])
BGL.glBegin(BGL.GL_TRIANGLE_FAN)
BGL.glVertex2f(x,y) #center point
for i in range(self.SlicesPerRound+1):
BGL.glVertex2f(p[i][X],p[i][Y])
BGL.glEnd()
#let's draw the contour:
if not foreground : foreground = self.foreground
BGL.glColor3f(foreground[0],foreground[1],foreground[2])
BGL.glBegin(BGL.GL_LINE_STRIP)
for i in range(self.SlicesPerRound+1):
BGL.glVertex2f(p[i][X],p[i][Y])
BGL.glEnd()
def DrawLine(self, x1, y1, x2, y2, color):
"""Draws a line with specified color
Arguments:
x1,y1: first point of the line
x2,y2: second point of the line
color: line color (3 floats)
"""
BGL.glColor3f(color[0],color[1],color[2])
BGL.glBegin(BGL.GL_LINES)
BGL.glVertex2f(x1,y1)
BGL.glVertex2f(x2,y2)
BGL.glEnd()
def DrawText(self, x, y, color, text, fontsize='normal'):
"""Draws a line with specified color
Arguments:
x,y: lower-left corner of the text
color: line color (3 floats)
text: text to draw
fontize: optional: as used in Blender.Draw.Text() function
"""
BGL.glColor3f(color[0],color[1],color[2])
BGL.glRasterPos2d(x, y)
Draw.Text(text, fontsize)
def DrawRect(self, x,y,width,height,lr=None,ur=None,rr=None,br=None, \
foreground = None, background = None):
"""Draws a rectangle with some corners rounded
Arguments:
x: x of lower left rectangle corner (without rounding)
y: y of lower left rectangle corner (without rounding)
width: width of the rectange
height: height of the rectangle
lr: optional: radius of rounds on the left side.
when = 0 - no contour is drawn on this side.
ur: optional: radius of rounds on the upper side.
when = 0 - no contour is drawn on this side.
rr: optional: radius of rounds on the right side.
when = 0 - no contour is drawn on this side.
br: optional: radius of rounds on the bottom side.
when = 0 - no contour is drawn on this side.
foreground: optional: non-default contour color (3 floats)
background: optional: non-default inner color (3 floats)
"""
if not foreground : foreground = self.foreground
if not background : background = self.background
if lr == None : lr = self.Radius
if ur == None : ur = self.Radius
if rr == None : rr = self.Radius
if br == None : br = self.Radius
BGL.glColor3f(background[0],background[1],background[2])
#main area:
BGL.glRectf(x+lr,y+br,x+width-rr,y+height-ur)
#left:
BGL.glRectf(x,y+br,x+lr,y+height-ur)
#upper:
BGL.glRectf(x+lr,y+height-ur,x+width-rr,y+height)
#right:
BGL.glRectf(x+width-rr,y+br,x+width,y+height-ur)
#bottom:
BGL.glRectf(x+lr,y,x+width-rr,y+br)
#contours:
BGL.glColor3f(foreground[0],foreground[1],foreground[2])
BGL.glBegin(BGL.GL_LINES)
if lr > 0:
BGL.glVertex2f(x,y+br)
BGL.glVertex2f(x,y+height-ur)
if ur > 0:
BGL.glVertex2f(x+lr,y+height)
BGL.glVertex2f(x+width-rr,y+height)
if rr > 0:
BGL.glVertex2f(x+width,y+height-ur)
BGL.glVertex2f(x+width,y+br)
if br > 0:
BGL.glVertex2f(x+lr,y)
BGL.glVertex2f(x+width-rr,y)
BGL.glEnd()
#corners:
self.RoundCorner(x+lr,y+br,min(lr,br),180.0,270.0,foreground,background)
self.RoundCorner(x+lr,y+height-ur,min(lr,ur),90.0,180.0,foreground,background)
self.RoundCorner(x+width-rr,y+height-ur,min(rr,ur),0.0,90.0,foreground,background)
self.RoundCorner(x+width-rr,y+br,min(rr,br),-90.0,0.0,foreground,background)
class Tab(Rounded):
"""Helper class - represents a single 'tab'
It is one tab, from possible many tabs presented by the TabControl.
All this class is providing are methods to draw a block with proper name,
to check the mouse clicks, and preserve the function for drawing
whole content of the main control area (below the tab). This function
will be retrieved and used by the TabControl object.
"""
#private fields
#__label => String: text - a label to be placed on the tab
#__base => list [x,y]: actual tab position (set by Draw(), used by IsInside())
#(it is lower - left corner)
#public fields with defaults:
drawContent = None #function used to draw the content of main area
#put here and used by the TabControl when this
#tab is the selected one
width = 2*GUI_SEP #width of this tab (it depends on the label text)
height = GUI_LN #height of this tab
#instance methods:
def __init__(self, name, drawfncn = None, height = None):
"""Initializes a tab instance
Arguments:
name: text, that will be displayed on the tab (its label)
drawfncn: optional - function, that will be placed as
public drawContent attribute. (a kind of 'payload' -
it is not used by Tab itself)
height: optional - height of the tab
(the width depends on the label)
"""
self.__label = name
self.__base = [0,0] #just to create a default value
self.drawContent = drawfncn
if height : self.height = height
self.width = GUI_SEP + Draw.GetStringWidth(self.__label) + GUI_SEP
def Draw(self, x, y, color):
"""Draws a tab at specified position
Arguments:
x: x coordinate of lower-left tab corner
y: y coordinate of lower-left tab corner
color: background color of the tab (3 floats)
"""
self.__base = [x,y]
self.DrawRect(x,y,self.width,self.height, br=0, background=color)
BGL.glColor3f(self.foreground[0],self.foreground[1],self.foreground[2])
BGL.glRasterPos2d(x+GUI_SEP, y+(GUI_SEP/2)+1)
Draw.Text(self.__label)
def IsInside(self, x,y):
"""returns True, when x,y are inside the tab area
Arguments:
x: x coordinate of test point
y: y coordinate of test point
"""
p = self.__base #just to make the expression shorter
return (p[X] < x < p[X]+self.width) and (p[Y] < y < p[Y]+self.height)
class TabControl(Rounded):
"""A control - container of the tabs
Every tab has assigned different control set, which is shown on
TabControl working area, when the tab was selected (by a mouse click)
This control is a container, that enables to use may different control
sets on the same area, this saving the screen space.
"""
#public fields, with defaults:
width = 40*GUI_W #width of the whole component on the screen
height = 3*GUI_LN #height of the whole component on the screen
selected = 0 #index of the selected tab
#private fields
#__base => list [x,y]: lower-left corner of the control
#__offset => int: offset from left control edge to the first tab
#__theight => int: height of the tabs
#__tabs => list of Tab: list of the control's Tab instances,
#__inactive => list(3 floats): background color for the inactive tabs
#instance methods:
def __init__(self, width=None, height=None, toffset=0, \
theight=GUI_H+2, inactive = [0.9, 0.85, 0.85]):
"""Initializes a tab instance
Arguments:
width: optional - width of the control
height: optional - height of the control
toffset: optional - distance from left tab control edge to
the first tab
theight: optional - non-default height of the tabs
inactive: optional - non-default color of non-selected tabs
(3 floats)
"""
if width : self.width= width
if height: self.height=height
self.selected = 0
self.__offset = toffset
self.__theight = theight
self.__inactive = inactive
self.__tabs = []
self.__base = [0,0]
def Append(self,name, drawfncn=None):
"""Adds another tab to the tab control
Arguments:
name: label (and id) of the tab
drawfncn: function(x,y,width,height), used to draw controls
of selected tab on specified area.
x,y: lower - left corner of the area
width: width of the area
height: height of the area
"""
self.__tabs.append(Tab(name, drawfncn, self.__theight))
def Draw(self, x, y):
"""Draws the tab control
Arguments:
x: x coordinate of lower-left corner of the working area
y: y coordinate of lower-left corner of the working area
"""
self.__base = [x,y]
p = self.__base #just to make the code text shorter
height = self.height-self.__theight #height of the working area...
offset = self.__offset
#drawing the working area:
self.DrawRect(p[X],p[Y],self.width,height)
for tab in self.__tabs:
if (offset + tab.width) > self.width : break #there is not enough space for all tabs!
t = [p[X]+offset,p[Y] + height] #current tab position
if self.__tabs.index(tab) == self.selected:
tab.Draw(t[X],t[Y],self.background)
self.DrawLine(t[X]+1,t[Y],t[X]+tab.width-1,t[Y],self.background)
#draw the active content:
drawfncn = tab.drawContent
if drawfncn : drawfncn(p[X],p[Y],self.width,height)
else:
tab.Draw(t[X],t[Y],self.__inactive)
self.DrawLine(t[X],t[Y],t[X]+tab.width,t[Y],self.foreground)
offset += GUI_SEP/2 + tab.width
def IsInside(self, x,y):
"""returns True, when x,y are inside the tab area
Arguments:
x: x coordinate of test point
y: y coordinate of test point
"""
p = self.__base #to make the code shorter:
ll = [p[X] + self.__offset, p[Y] + (self.height - self.__theight)] #lower - left corner
ur = [p[X]+self.width, p[Y]+self.height] #upper - right corner
return (ll[X] < x < ur[X]) and (ll[Y] < y < ur[Y])
def OnInput(self,event, pressed,p):
"""Reacts on a device event
Arguments:
event: any event from Blender.Draw module
pressed: 0, if the button has been released
p: mouse positon (X,Y), or None for non-mouse events
"""
if event == Draw.LEFTMOUSE and pressed :
#check if the user has clicked a tab:
if self.IsInside(p[X],p[Y]):#it is in the tab area, at least
#check, if another tab is clicked:
for tab in self.__tabs:
#when user has clicked non-selected tab:
if tab.IsInside(p[X],p[Y]) and \
self.__tabs.index(tab) != self.selected:
self.selected = self.__tabs.index(tab) #it is selected one, now!
#actual window can be a Script or Text
Window.WaitCursor(True)
Window.Redraw(Window.Types.SCRIPT) #force redraw
Window.WaitCursor(False)
break #quit of the loop
####other classes--------------------------------------------------------------
class Handle:
"""Class encapsulates details of handling a single handle object
"""
#class fields:
Width = 40*GUI_W #Width of the whole component on the screen
Height = 3*GUI_LN #Height of the whole component on the screen
Margin = GUI_SEP/2 + 1 #Offset from bounding box to controls inside
LblColor = [0.0,0.0,1.0]#color of the name label (blue)
EventBase = [EVT_BASE] #seed for the instance event bases.
#it is a list, beacuse this form will allow the
#handle constructor to increase it with every
#subsequent call (every instance has a reference
#to the same object - the list)
#private instance fields:
#__obj => Object: the Blender's handle object
#__name => String: the name of the handle, shown on the GUI screen
#__min => list(3 float): position of the handle at value == 0"
#__max => list(3 float): position of the handle at value == 100"
#__base => Vector: location of the handle's parent
# (this value is often used in functions)
#__eventbase => int: lowest event number, assigned to controls.
# events that have numbers from _eventbase
# to eventbase + EVT_BASE -1 belong to this
# instance.
#__tab => TabControl: it manages tab pages
# 'Pos' tab:
#__value => Slider button: determines handle position
# 'Label' tab
#__lbl => String button: label of this handle
# 'Cockpit' tab:
#__cname => String button: name of the cockpit object
#__cdim => Menu button: dimension of the cockpit object (default: Xrot)
#(Xpos,Ypos,Zpos,Xrot,Yrot,Zrot)
#__cmin => Number button: min. position value for cockpit object
#__cmax => Number button: max. position value for cockpit object
#__cupd => int: internal flag. When > 0, Update method
#should reset __cmin and __cmax values
#to current cockipt object coordinate
#value (to avoid unexpected "jumps" of
#newly assigned cockpit object).
#This flag is reset to 0 in Update()
#__descendants => list of Blender objects: all children of a cockpit object,
#including also the children of the children.
#list required to fix Blender's bug with
#setLocation: it does not alter the locations
#of changed object descendants, when the parent
#was rotated by setEuler()
#instance methods:
def __init__(self, object):
"""Initializes a handle instance
Arguments:
object: a Blender's object - a proper handle of a moveable
part
A ValueException will be thrown, when not proper object has been
passed
"""
limit = Handle.GetLimit(object)
if not limit :
raise ValueException("object '%s' has no Limit Location constraint" \
% object.name)
#filling the private fields:
self.__obj = object
self.__eventbase = self.EventBase[0]#lowest event number, assigned
#to controls of the handle
self.EventBase[0] += EVT_BASE #In every instance EventBase keeps the
#ref to the sam object - a list. It has
#one global value for the whole class
#The line above has incrased its value
self.__name = self.__obj.name
self.__min = [limit[Constraint.Settings.XMIN], \
limit[Constraint.Settings.YMIN], \
limit[Constraint.Settings.ZMIN]]
self.__max = [limit[Constraint.Settings.XMAX], \
limit[Constraint.Settings.YMAX], \
limit[Constraint.Settings.ZMAX]]
self.__cupd = 0 #this flag always defaults to 0 in constructor
#creating the slider object:
self.__value = Draw.Create(0.0)
#setting the starting values for the cockpit objects:
self.__cname = Draw.Create(self.GetProperty(PROP_OBJECT,""))
self.__descendants = None #this will force initial refresh on this
#during nearest Update() call.
self.__cdim = Draw.Create(self.GetProperty(PROP_DIM,XROT))
self.__cmin = Draw.Create(self.GetProperty(PROP_MIN,0))
self.__cmax = Draw.Create(self.GetProperty(PROP_MAX,0))
#setting the starting values for the label
self.__lbl = Draw.Create(self.GetProperty(PROP_LABEL,""))
if self.__lbl.val: self.__name = self.__lbl.val
#coordinates of the handle are NOT distance from its parent!
#That's why we have to use the 'base' - parent's location
#'localspace' obj coordinates are, in fact, the same figures
#like shown to the user in the 'Transfrom properties' dialog
self.__base = Mathutils.Vector(self.__obj.parent.getLocation('localspace'))
self.__tab = TabControl(width = self.Width - 2*self.Margin, \
height = self.Height - 2*self.Margin, \
toffset = 20*GUI_W-self.Margin)
self.__tab.Append("Pos", self.DrawPosTab)
self.__tab.Append("Cockpit", self.DrawCockpitTab)
self.__tab.Append("Label", self.DrawLabelTab)
self.Refresh() #setting the slider to the initial position
#this method is a module method, because it is used in Handle class and in the
#main() function:
@staticmethod
def GetLimit(obj):
"""Gets the 'Limit Location' constraint from an object - part handle
Arguments:
obj - Blender object to be checked
Returns: a Constraint object, None if not found
"""
#limits: a list of Limit Location constraints...
limits = filter(lambda c: c.type == Constraint.Type.LIMITLOC, obj.constraints)
if limits: #if it contains at least one such constraint:
c = limits[0] #we will analyze the first one (an object should have only one)
#if the constraint has a parent:
#(previously - before Blender 2.46 - I have checked if it jas local
#parent coordinates, but there is no such flag in the actual API:
#it has silently disappeared.
#original condtidion:
#if obj.parent and c[Constraint.Settings.LIMIT_LOCAL_NOPARENT]:
#actual (Blneder 2.46 and later):
if obj.parent:
return c
else: return None
else: return None
def GetName(self):
"""Returns object name (the label, presented on GUI)"""
return self.__name
def __cmp__(self,other):
"""Compares two handles.
Arguments:
other : other handle object
Returns result of string comparison their GetName()s
"""
return cmp(self.GetName(),other.GetName())
def GetProperty(self,name,default):
"""Retrieves an ID property 'name' from the Blender's handle object.
Arguments:
name : property name
default : default value, used when property does not exists
Returns a property value, or default, when it does not exists.
It is helper function, used in object constructor.
"""
if self.__obj.properties.has_key(name):
return self.__obj.properties[name]
else:
return default
def SaveProperties(self):
"""Updates the properties of Blender's object with actual values
To be called at the end of this instance
"""
prop = self.__obj.properties
prop[PROP_OBJECT] = self.__cname.val
prop[PROP_DIM] = self.__cdim.val
prop[PROP_MIN] = self.__cmin.val
prop[PROP_MAX] = self.__cmax.val
prop[PROP_LABEL] = self.__lbl.val
def Refresh(self):
"""Updates the slider value with actual handle position
To be called at constructor or when a sync between value and handle
is required
"""
#setting up the initial position:
v = 0
handle = Mathutils.Vector(self.__obj.getLocation('localspace'))
min = Mathutils.Vector(self.__min) #min is relative to __base
max = Mathutils.Vector(self.__max) #max is relative to __base
#2010-07-26: l = handle - (self.__base + min) #l: distance from handle to min
#commented code above worked well in 2.43, but not with 2.49, where min and max are absolute!
l = handle - min #l: distance from handle to neutral point
span = max - min #span: whole available distance
#the handle position may be not "normalized", i.e on the
#line from handle origin to its parent origin, so we have
#to check all three coordinates:
if span.length > 0.0: v = l.length / span.length
#the result should be clamped to 0.0 ... 1.0 range
if v < 0.0 : v = 0
if v > 1.0 : v = 1.0
self.__value.val = v*100 #value will always be a Button
def DrawPosTab(self,x,y,width,height):
"""Draws the conrols in 'Pos' tab
Arguments:
x,y : a lower-left corner of available area
width : width of available area
height : height of available area
"""
pos = [x,y]
#applying the margin:
pos[Y] += self.Margin
pos[X] += self.Margin
width -= 2*self.Margin
height -= 2*self.Margin
Text = self.__tab.DrawText #a shortuct to useful function
fg = self.__tab.foreground #a shortcut to actual color
h = GUI_H+self.Margin #Y offset to upper controls
#slider
self.__value = Draw.Slider('Position (%): ', self.__eventbase+EV_SLIDER, \
pos[X], pos[Y]+h-2, width, GUI_H, \
self.__value.val, 0, 100, 1, \
'0% means lowest position, 100% means upper position')
#button: Reset
resetWidth = 7*GUI_W
Draw.PushButton("Reset", self.__eventbase+EV_RESET, \
pos[X]+width-resetWidth+self.Margin,pos[Y],\
resetWidth-self.Margin,GUI_H, \
'Place this handle at default position')
def DrawLabelTab(self,x,y,width,height):
"""Draws the conrols in 'Label' tab
Arguments:
x,y : a lower-left corner of available area
width : width of available area
height : height of available area
"""
pos = [x,y]
#applying the margin:
pos[Y] += self.Margin
pos[X] += self.Margin
width -= 2*self.Margin
height -= 2*self.Margin
Text = self.__tab.DrawText #a shortuct to useful function
fg = self.__tab.foreground #a shortcut to actual color
h = GUI_H+self.Margin #Y offset to upper controls
#slider
self.__lbl = Draw.String('Label: ', self.__eventbase+EV_LABEL, \
pos[X], pos[Y]+h-2, width, GUI_H, \
self.__lbl.val, 20, \
'Label for this handle, that will be displayed on this screen')
#button: Reset
resetWidth = 7*GUI_W
Draw.PushButton("Apply", self.__eventbase+EV_APPLY, \
pos[X]+width-resetWidth+self.Margin,pos[Y],\
resetWidth-self.Margin,GUI_H, \
'Applies this name')
def DrawCockpitTab(self,x,y,width,height):
"""Draws the conrols in 'Cockpit' tab
Arguments:
x,y : a lower-left corner of available area
width : width of available area
height : height of available area
"""
pos = [x,y]
#applying the margin:
pos[Y] += self.Margin
pos[X] += self.Margin
width -= 2*self.Margin
height -= 2*self.Margin
Text = self.__tab.DrawText #a shortuct to useful function
fg = self.__tab.foreground #a shortcut to actual color
h = GUI_H+self.Margin #Y offset to upper controls
#field: Name
nameWidth = 25*GUI_W
self.__cname = Draw.String('OB: ', self.__eventbase+EV_COBJECT, \
pos[X], pos[Y]+h-2, nameWidth, GUI_H, \
self.__cname.val, 20, \
'Name of the controlled object in the cockpit')
selWidth = 2*GUI_W
Draw.PushButton("...", self.__eventbase+EV_CSELECT, \
pos[X]+nameWidth+2, \
pos[Y]+h-2, selWidth, GUI_H, \
'Put the active object name as the controlled object')
#field: Dimension
pos[X] = x + 3*GUI_W + 3
dimWidth = 10*GUI_W
dims = 'Dimension %t|'
dims += 'X rotation %x4|Y rotation %x5|Z rotation %x6|'
dims += 'X position %x1|Y position %x2|Z position %x3'
self.__cdim = Draw.Menu(dims, self.__eventbase+EV_CDIM, \
pos[X], pos[Y], dimWidth, GUI_H, \
self.__cdim.val, \
'Coordinate to be controlled by the handle')
#field Min:
pos[X] += dimWidth +2
numWidth = 12*GUI_W
self.__cmin = Draw.Number('Min: ', self.__eventbase+EV_CMIN, \
pos[X], pos[Y], numWidth, GUI_H, \
self.__cmin.val, -1000.0, 1000.0,\
'coordinate minimum value (angles in degrees)')
#field Max:
pos[X] += numWidth + 2
self.__cmax = Draw.Number('Max:', self.__eventbase+EV_CMAX, \
pos[X], pos[Y], numWidth, GUI_H, \
self.__cmax.val, -1000.0, 1000.0,\
'coordinate maximum value (angles in degrees)')
def Draw(self,x, y):
"""Draws a GUI with handle slider and other controls
Arguments:
x,y : a lower-left corner for the GUI
"""
pos = [x, y + self.Height] #upper - left corner
bkg = self.__tab.background
fg = self.__tab.foreground
BGL.glColor3f(bkg[0],bkg[1],bkg[2]) #background color: white
BGL.glRectf(pos[X], pos[Y] - self.Margin, pos[X]+self.Width, pos[Y]-self.Height)
pos[Y] -= (GUI_H + self.Margin) #first control line...
#label: name of the handle
self.__tab.DrawText(pos[X]+self.Margin, pos[Y],self.LblColor,self.__name)
#tab control:
if self.__tab: self.__tab.Draw(x + self.Margin, y + self.Margin-3)
def OnInput(self,event,pressed,p) :
"""Reacts on a device event
Arguments:
event: any event from Blender.Draw module
pressed: 0, if the button has been released
p: mouse positon -(X,Y), or None for non-mouse events
"""
if self.__tab : self.__tab.OnInput(event,pressed,p)
def OnEvent(self, event):
"""Reacts on signalized event
Arguments:
event: an event id - an integer, assigned to a specific GUI element
in the Draw method.
"""
if self.__eventbase <= event < self.__eventbase + EVT_BASE :
event %= EVT_BASE #from this moment event is a number from 0 to 9
if event == EV_SLIDER : #Pos tab: slider
self.Update()
elif event == EV_RESET : #Pos tab: reset button
self.Reset()
elif event in (EV_APPLY, EV_LABEL) : #Label tab: label field, apply button
if self.__lbl.val :
self.__name = self.__lbl.val
else:
self.__name = self.__obj.name
self.SaveProperties()
Draw.Redraw()
elif event == EV_CSELECT : #Cockpit tab: select button
if Object.GetSelected():
self.__cname.val = Object.GetSelected()[0].name
self.__cupd += 1 #request to refresh Min and Max values
#in nearest call to Update() function
self.Update()
elif event in (EV_COBJECT, EV_CDIM, EV_CMIN, EV_CMAX):
#Cockpit settings have been changed:
if event in (EV_COBJECT, EV_CDIM): self.__cupd += 1
self.Update()
def Update(self):
"""Updates handle position (in effect - moves the model moveable part)
"""
v = self.__value.val/100.0
#set position of the base object
#2010-07-26 x = self.__base.x + self.__min[X]*(1-v) + self.__max[X]*v
# y = self.__base.y + self.__min[Y]*(1-v) + self.__max[Y]*v
# z = self.__base.z + self.__min[Z]*(1-v) + self.__max[Z]*v
#code above worked well in 2.43, but in 2.49 __min and __max are absolute!
x = self.__min[X]*(1-v) + self.__max[X]*v
y = self.__min[Y]*(1-v) + self.__max[Y]*v
z = self.__min[Z]*(1-v) + self.__max[Z]*v
self.__obj.setLocation(x,y,z)
#optionally - set position of an accomapnying object in the cockpit:
if self.__cname.val :
try: #the cockpit object may not exists
obj = Object.Get(self.__cname.val)
dim = self.__cdim.val #selected dimension number (1..6)
#self.__descendants may be Noneat first entrance:
if self.__cupd or self.__descendants == None:
#this list is needed to repair the bug: setEuler does not
#change the position of object's descendants - we will have
#to refresh them manually, for every object from this list.
self.__descendants = filter(lambda o: isDescendant(o,obj),\
Scene.GetCurrent().objects)
if self.__cupd > 0 : #accept actual coordinates as min and max
pos = None #actual value of selected coordinate
if dim in (XPOS, YPOS, ZPOS):
pos = obj.getLocation('localspace')[dim-XPOS]
elif dim in (XROT, YROT, ZROT):
pos = obj.getEuler('localspace')[dim-XROT]*180.0/math.pi
if pos != None : #just to be sure, that dim was non-empty
self.__cupd = 0
self.__cmin.val = pos
self.__cmax.val = pos
elif dim in (XPOS,YPOS,ZPOS): #change position
min = self.__cmin.val
max = self.__cmax.val
loc = obj.getLocation('localspace')
loc[dim-XPOS] = min * (1-v) + max*v
obj.setLocation(loc.x,loc.y,loc.z)
elif dim in (XROT, YROT,ZROT): #change rotation
#rotation is specified in degrees,
#but the Euler constructor requires radians!
min = self.__cmin.val * math.pi / 180.0
max = self.__cmax.val * math.pi / 180.0
rot = obj.getEuler('localspace')
rot[dim-XROT] = min * (1-v) + max*v
obj.setEuler(rot)
#fixing the bug in setEuler: it does not alter the
#location of obj descendants - we have to set it manually!
for o in self.__descendants:
o.setLocation(o.getLocation('localspace'))
self.SaveProperties()
except: #usually - when the object with __cname was not found:
self.__cname.val = ""
Window.RedrawAll()
def Reset(self):
"""Resets the handle to the the rest (parent) position
"""
#(previously - before Blender 2.46 - setLocation has worked in world coordinates
#but now it works in the coordinates relative to the parent
#so I have just to calculate the parent position:
#self.__obj.setLocation(self.__base.x,self.__base.y,self.__base.z)
back = Mathutils.Vector(self.__obj.matrixLocal[3][:3])
loc = Mathutils.Vector(self.__obj.loc)
base = loc - back
self.__obj.setLocation(base.x,base.y,base.z)
self.Refresh()
self.Update()
####module functions-----------------------------------------------------------
def isDescendant(obj,ancestor):
"""Returns True when an obj is a descendant of the ancestor Blender object
Arguments:
obj - Blender object to be checked
ancestor - Blender object to be tested
Returns: True, when obj has ancestor among its parents (also non-direct)
"""
if obj.parent == None : return False
elif obj.parent == ancestor: return True
else: return isDescendant(obj.parent, ancestor)
def draw():
"""Draws a GUI
"""
global handles
scrHeight = Window.GetAreaSize()[Y]
scrWidth = Window.GetAreaSize()[X]
BGL.glClear(BGL.GL_COLOR_BUFFER_BIT)
#set up the initial position - lower left screen corner
top = scrHeight - GUI_LN
pos = [GUI_W, top] #1 text lines for the header
BGL.glColor3f(1,1,1) #text color: white
BGL.glRasterPos2d(pos[X],pos[Y])
Draw.Text("Moveable parts controls:")
QuitWidth = 10*GUI_W
Draw.PushButton("Quit", EVT_EXIT, \
pos[X]+Handle.Width-QuitWidth,pos[Y],QuitWidth,GUI_H,\
'Close this panel, terminate this script')
for handle in handles:
if pos[Y] < Handle.Height: #we have reached the end of this column
pos[Y] = top #so go back to the top...
pos[X] += Handle.Width+Handle.Margin #...but in the next column.
pos[Y] -= Handle.Height #we need LOWER left point for drawing...
if pos[X] < scrWidth: #"safety switch"
handle.Draw(pos[X],pos[Y])
def buttonEvent(event):
"""Callback on an event generated by GUI controls (buttons)
"""
global handles
index = (event / EVT_BASE) - 1
if index < 0: #"framework" events
if event == EVT_EXIT:
Draw.Exit()
else: #delegate it to the handles:
for handle in handles:
handle.OnEvent(event)
def deviceEvent(key, pressed):
"""Callback on an event generated by keyboard or mouse
Arguments:
key: a key (from the keyboard or mouse button)
pressed: 0, if key was released, 1 when pressed.
"""
if key in (Draw.QKEY, Draw.ESCKEY) and not pressed:
#[Q] on keyboard released:
Draw.Exit()
else:
#for mouse events: we have to get mouse position:
if key in (Draw.LEFTMOUSE,Draw.MIDDLEMOUSE,Draw.RIGHTMOUSE):
p = Window.GetMouseCoords()
#reading the lower left corner of contaning window - to "localize"
#mouse coordinates on eventual input.
id = Window.GetAreaID()
info = filter(lambda o: o['id'] == id, Window.GetScreenInfo())
if info : #there should be always non-empty list!
base = info[0]['vertices'][:2] #lower left corner of client area
#translate p to local window coordinates
p = (p[X] - base[X], p[Y] - base[Y])
else:
p = None
for handle in handles: #by the handle's TabControl
handle.OnInput(key,pressed,p)
def main():
"""Main procedure of this script: registers the GUI callbacks
"""
global handles
#fills in the list of the handle objects in current scene:
handles = [Handle(object) for object in \
filter(lambda o: o.name[-len(HANDLE_SUFFIX):] == HANDLE_SUFFIX \
and Handle.GetLimit(o), Scene.GetCurrent().objects)]
handles.sort() #sort it, using labels
Draw.Register(draw,deviceEvent,buttonEvent)
#let's run it:
main()