I've been using python and reportlab to auto-generate long documents and want to use the PDF outline tree for easy navigation through the document. According to the docs, canvas.bookmarkPage comes with multiple options to adjust the document view after jumping to the destination page. The standard one is a simple page Fit to the window. From a user perspective, I would prefer FitH (as wide as possible, with destination at the top of the screen) or XYZ (keep user zoom level with destination at the top of the screen). When using any fit option except the basic Fit, the function call must be provided with the coordinates to arrange the view accordingly.
However, I could not find any explanations, examples, code snippets, or anything on how to figure this out, and it took me a good while to come up with a solution. So, I want to share this solution here and ask if this is really the best way to do it or if I overlooked something basic.
The key thing here is SmartParagraph which remembers its position after it was drawn. First, I used flowable.canv.absolutePosition(0,0) in the afterFlowable() method because this is where I needed this information to pass it to bookmarkPage(). However, the position was always reported as 0, 0, so apparently the flowable and/or the canvas have forgotten everything about the position when afterFlowable() is reached. So I thought there has to be some point in time when a Flowable knows its position and after investigating the source code I found out that after draw(), it still knows where it is.
So: SmartParagraph is a subclass of Paragraph that stores its position after it is drawn, so that later in the document building process this can be used by any external element for whatever.
The example will create a dummy pdf with 2 headings that do a nice FitH zoom and two headings that do the basic Fit zoom.
Does anyone have a better idea on how to solve this?
import typing
from reportlab.lib.styles import ParagraphStyle as PS
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus.flowables import Flowable
from reportlab.platypus import PageBreak, Spacer
from reportlab.platypus.paragraph import Paragraph
from reportlab.platypus.doctemplate import SimpleDocTemplate
from reportlab.lib.units import cm
class SmartParagraph(Paragraph):
def __init__(self, text, *args, **kwds):
"""This paragraph remembers its position on the canvas"""
super(SmartParagraph, self).__init__(text, *args, **kwds)
self._pos: typing.Tuple[int, int] = None
def draw(self):
super(SmartParagraph, self).draw()
self._pos = self.canv.absolutePosition(0, 0)
def get_pos(self) -> typing.Tuple[int, int]:
return self._pos
class CustomDocTemplate(SimpleDocTemplate):
def __init__(self, filename, outline_levels: int = 4, **kwargs):
super(CustomDocTemplate, self).__init__(filename, **kwargs)
self._bookmark_keys = list()
if not isinstance(outline_levels, int) and outline_levels < 1:
raise ValueError("Outline levels must be integer and at least 1")
self._outline_levels = {f'Heading{level+1}': level for level in range(outline_levels)}
# Map of kind: Heading1 -> 0
# Heading 1 is level 0, I dont make the rules
def afterFlowable(self, flowable: Flowable):
"""Registers TOC entries."""
if isinstance(flowable, Paragraph):
flowable: Paragraph
text = flowable.getPlainText()
style = flowable.style.name
if style in self._outline_levels:
level = self._outline_levels[style]
else:
return
if text not in self._bookmark_keys:
key = text
self._bookmark_keys.append(key)
else:
# There might headings with identical text, yet they need a different key
# Keys are stored in a list and incremented if a duplicate is found
cnt = 1
while True:
key = text + str(cnt)
if key not in self._bookmark_keys:
self._bookmark_keys.append(key)
break
cnt += 1
if isinstance(flowable, SmartParagraph):
# Only smart paragraphs know their own position
x, y = flowable.get_pos()
y += flowable.style.fontSize + 15
self.canv.bookmarkPage(key, fit="FitH", top=y)
else:
# Dumb paragraphs need to show the whole page
self.canv.bookmarkPage(key)
self.canv.addOutlineEntry(title=text, key=key, level=level)
def _endBuild(self):
"""Override of parent function. Shows outline tree by default when opening PDF."""
super(CustomDocTemplate, self)._endBuild()
self.canv.showOutline()
story = list()
story.append(SmartParagraph('First Smart Heading', getSampleStyleSheet()['h1']))
story.append(Paragraph('Text in first heading'))
story.append(Spacer(1, 0.5 * cm))
story.append(SmartParagraph('First Sub Smart Heading', getSampleStyleSheet()['h2']))
story.append(Paragraph('Text in first sub heading'))
story.append(Spacer(1, 0.5 * cm))
story.append(Paragraph('Second Sub Dumb Heading', getSampleStyleSheet()['h2']))
story.append(Paragraph('Text in second sub heading'))
story.append(PageBreak())
story.append(Paragraph('Last Dumb Heading', getSampleStyleSheet()['h1']))
story.append(Paragraph('Text in last heading', PS('body')))
doc = CustomDocTemplate('mintoc.pdf')
doc.multiBuild(story)
Related
I'm writing a script in python using Open Cascade Technology (using the pyOCCT package for Anaconda) to import STEP files, defeature them procedurally and re-export them. I want to preserve the product hierarchy, names and colours as much as possible. Currently the script can import STEP files, simplify all of the geometry while roughly preserving the hierarchy and re-export the step file. The problem is no matter how I approach the problem, I can't manage to make it preserve the colours of the STEP file in a few particular cases.
Here's the model I pass in to the script:
And here's the result of the simplification:
In this case, the simplification has worked correctly but the colours of some of the bodies were not preserved. The common thread is that the bodies that loose their colours are children of products which only have other bodies as their children (ie: they don't contain sub-products).
This seems to be related to the way that Open Cascade imports STEP files which are translated as follows:
Alright, now for some code:
from OCCT.STEPControl import STEPControl_Reader, STEPControl_Writer, STEPControl_AsIs
from OCCT.BRepAlgoAPI import BRepAlgoAPI_Defeaturing
from OCCT.TopAbs import TopAbs_FACE, TopAbs_SHAPE, TopAbs_COMPOUND
from OCCT.TopExp import TopExp_Explorer
from OCCT.ShapeFix import ShapeFix_Shape
from OCCT.GProp import GProp_GProps
from OCCT.BRepGProp import BRepGProp
from OCCT.TopoDS import TopoDS
from OCCT.TopTools import TopTools_ListOfShape
from OCCT.BRep import BRep_Tool
from OCCT.Quantity import Quantity_ColorRGBA
from OCCT.ShapeBuild import ShapeBuild_ReShape
from OCCT.STEPCAFControl import STEPCAFControl_Reader, STEPCAFControl_Writer
from OCCT.XCAFApp import XCAFApp_Application
from OCCT.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ColorGen, XCAFDoc_ColorSurf
from OCCT.XmlXCAFDrivers import XmlXCAFDrivers
from OCCT.TCollection import TCollection_ExtendedString
from OCCT.TDF import TDF_LabelSequence
from OCCT.TDataStd import TDataStd_Name
from OCCT.TDocStd import TDocStd_Document
from OCCT.TNaming import TNaming_NamedShape
from OCCT.Interface import Interface_Static
# DBG
def export_step(shape, path):
writer = STEPControl_Writer()
writer.Transfer( shape, STEPControl_AsIs )
writer.Write(path)
# DBG
def print_shape_type(label, shapeTool):
if shapeTool.IsFree_(label):
print("Free")
if shapeTool.IsShape_(label):
print("Shape")
if shapeTool.IsSimpleShape_(label):
print("SimpleShape")
if shapeTool.IsReference_(label):
print("Reference")
if shapeTool.IsAssembly_(label):
print("Assembly")
if shapeTool.IsComponent_(label):
print("Component")
if shapeTool.IsCompound_(label):
print("Compound")
if shapeTool.IsSubShape_(label):
print("SubShape")
# Returns a ListOfShape containing the faces to be removed in the defeaturing
# NOTE: For concisness I've simplified this algorithm and as such it *MAY* not produce exactly
# the same output as shown in the screenshots but should still do SOME simplification
def select_faces(shape):
exp = TopExp_Explorer(shape, TopAbs_FACE)
selection = TopTools_ListOfShape()
nfaces = 0
while exp.More():
rgb = None
s = exp.Current()
exp.Next()
nfaces += 1
face = TopoDS.Face_(s)
gprops = GProp_GProps()
BRepGProp.SurfaceProperties_(face, gprops)
area = gprops.Mass()
surf = BRep_Tool.Surface_(face)
if area < 150:
selection.Append(face)
#log(f"\t\tRemoving face with area: {area}")
return selection, nfaces
# Performs the defeaturing
def simplify(shape):
defeaturer = BRepAlgoAPI_Defeaturing()
defeaturer.SetShape(shape)
sel = select_faces(shape)
if sel[0].Extent() == 0:
return shape
defeaturer.AddFacesToRemove(sel[0])
defeaturer.SetRunParallel(True)
defeaturer.SetToFillHistory(False)
defeaturer.Build()
if (not defeaturer.IsDone()):
return shape# TODO: Handle errors
return defeaturer.Shape()
# Given the label of an entity it finds it's displayed colour. If the entity has no defined colour the parents are searched for defined colours as well.
def find_color(label, colorTool):
col = Quantity_ColorRGBA()
status = False
while not status and label != None:
try:
status = colorTool.GetColor(label, XCAFDoc_ColorSurf, col)
except:
break
label = label.Father()
return (col.GetRGB().Red(), col.GetRGB().Green(), col.GetRGB().Blue(), col.Alpha(), status, col)
# Finds all child shapes and simplifies them recursively. Returns true if there were any subshapes.
# For now this assumes all shapes passed into this are translated as "SimpleShape".
# "Assembly" entities should be skipped as we don't need to touch them, "Compound" entities should work with this as well, though the behaviour is untested.
# Use the print_shape_type(shapeLabel, shapeTool) method to identify a shape.
def simplify_subshapes(shapeLabel, shapeTool, colorTool, set_colours=None):
labels = TDF_LabelSequence()
shapeTool.GetSubShapes_(shapeLabel, labels)
#print_shape_type(shapeLabel, shapeTool)
#print(f"{shapeTool.GetShape_(shapeLabel).ShapeType()}")
cols = {}
for i in range(1, labels.Length()+1):
label = labels.Value(i)
currShape = shapeTool.GetShape_(label)
print(f"\t{currShape.ShapeType()}")
if currShape.ShapeType() == TopAbs_COMPOUND:
# This code path should never be taken as far as I understand
simplify_subshapes(label, shapeTool, colorTool, set_colours)
else:
''' See the comment at the bottom of the main loop for an explanation of the function of this block
col = find_color(label, colorTool)
#print(f"{name} RGBA: {col[0]:.5f} {col[1]:.5f} {col[2]:.5f} {col[3]:.5f} defined={col[4]}")
cols[label.Tag()] = col
if set_colours != None:
colorTool.SetColor(label, set_colours[label.Tag()][5], XCAFDoc_ColorSurf)'''
# Doing both of these things seems to result in colours being reset but the geometry doesn't get replaced
nshape = simplify(currShape)
shapeTool.SetShape(label, nshape) # This doesn't work
return labels.Length() > 0, cols
# Set up XCaf Document
app = XCAFApp_Application.GetApplication_()
fmt = TCollection_ExtendedString('MDTV-XCAF')
doc = TDocStd_Document(fmt)
app.InitDocument(doc)
shapeTool = XCAFDoc_DocumentTool.ShapeTool_(doc.Main())
colorTool = XCAFDoc_DocumentTool.ColorTool_(doc.Main())
# Import the step file
reader = STEPCAFControl_Reader()
reader.SetNameMode(True)
reader.SetColorMode(True)
Interface_Static.SetIVal_("read.stepcaf.subshapes.name", 1) # Tells the importer to import subshape names
reader.ReadFile("testcolours.step")
reader.Transfer(doc)
labels = TDF_LabelSequence()
shapeTool.GetShapes(labels)
# Simplify each shape that was imported
for i in range(1, labels.Length()+1):
label = labels.Value(i)
shape = shapeTool.GetShape_(label)
# Assemblies are just made of other shapes, so we'll skip this and simplify them individually...
if shapeTool.IsAssembly_(label):
continue
# This function call here is meant to be the fix for the bug described.
# The idea was to check if the TopoDS_Shape we're looking at is a COMPOUND and if so we would simplify and call SetShape()
# on each of the sub-shapes instead in an attempt to preserve the colours stored in the sub-shape's labels.
#status, loadedCols = simplify_subshapes(label, shapeTool, colorTool)
#if status:
#continue
shape = simplify(shape)
shapeTool.SetShape(label, shape)
# The code gets a bit messy here because this was another attempt at fixing the problem by building a dictionary of colours
# before the shapes were simplified and then resetting the colours of each subshape after simplification.
# This didn't work either.
# But the idea was to call this function once to generate the dictionary, then simplify, then call it again passing in the dictionary so it could be re-applied.
#if status:
# simplify_subshapes(label, shapeTool, colorTool, loadedCols)
shapeTool.UpdateAssemblies()
# Re-export
writer = STEPCAFControl_Writer()
Interface_Static.SetIVal_("write.step.assembly", 2)
Interface_Static.SetIVal_("write.stepcaf.subshapes.name", 1)
writer.Transfer(doc, STEPControl_AsIs)
writer.Write("testcolours-simplified.step")
There's a lot of stuff here for a minimum reproducible example but the general flow of the program is that we import the step file:
reader.ReadFile("testcolours.step")
reader.Transfer(doc)
Then we iterate through each label in the file (essentially every node in the tree):
labels = TDF_LabelSequence()
shapeTool.GetShapes(labels)
# Simplify each shape that was imported
for i in range(1, labels.Length()+1):
label = labels.Value(i)
shape = shapeTool.GetShape_(label)
We skip any labels marked as assemblies since they contain children and we only want to simplify individual bodies. We then call simplify(shape) which performs the simplification and returns a new shape, we then call shapeTool.SetShape() to bind the new shape to the old label.
The thing that doesn't work here is that as explained, Component3 and Component4 don't get marked as Assemblies and are treated as SimpleShapes and when they are simplified as one shape, the colours are lost.
One solution I attempted was to call a method simplify_subshapes() which would iterate through each of the subshapes, and do the same thing as the main loop, simplifying them and then calling SetShape(). This ended up being even worse as it resulted in those bodies not being simplified at all but still loosing their colours.
I also attempted to use the simplify_subshapes() method to make a dictionary of all the colours of the subshapes, then simplify the COMPOUND shape and then call the same method again to this time re-apply the colours to the subshapes using the dictionary (the code for this is commented out with an explanation as to what it did).
col = find_color(label, colorTool)
#print(f"{name} RGBA: {col[0]:.5f} {col[1]:.5f} {col[2]:.5f} {col[3]:.5f} defined={col[4]}")
cols[label.Tag()] = col
if set_colours != None:
colorTool.SetColor(label, set_colours[label.Tag()][5], XCAFDoc_ColorSurf)
As far as I see it the issue could be resolved either by getting open cascade to import Component3 and Component4 as Assemblies OR by finding a way to make SetShape() work as intended on subshapes.
Here's a link to the test file:
testcolours.step
I am currently working on a little script that creates a crane-like rig automatically in Autodesk Maya, the user gets to choose the amount of joints by a UI.
My question is how do I take the integer input of the user and use it as the variable value for my "jointAmount"?
I am also wondering how I would be able to call my function(AutoCraneRig) to actually run the script from the UI. I have a "apply"-button but I am unsure how to connect it to my function.
I have seen similar posts like mine but I feel that the solutions shown are somewhat hard for me to understand and/or I can't really relate what is shown to my own problem.
If anything is unclear or more information is needed from me please don't hesitate to call me out.
Here is what my current UI look like
import maya.cmds as cmds
import pymel.core as pm
def jntctrl():
number = pm.intField(jnt, q=1, v=1)
print(number)
if pm.window("stuff", exists = True):
pm.deleteUI("stuff")
pm.window("stuff", t = "Crane Rig Generator", w=400, h=200)
pm.columnLayout(adj = True)
pm.text(label="Joint Amount:")
jnt = pm.intField(changeCommand = 'jntctrl()')
pm.button(label="Create Crane")
pm.showWindow()
#Defining how many joints the user want to have for their crane rig
jointAmmount = 5
#Defining how many controllers the user want to have to orient the crane.
#May not exceed the joint amount
controllerAmount = 5
def autoCraneRig():
#Creating the joints
for i in range(jointAmmount):
pm.joint()
pm.move(0, i, 0)
#Creating the controllers
for i in range(controllerAmount):
pm.circle()
pm.rotate (0,90,0)
pm.makeIdentity (apply= True)
#Creating the groups
for i in range(controllerAmount):
pm.group()
#Somehow one of the nurbs get parented to a group when running the script, here i select both the groups and then unparent them.
pm.select("group*", "nurbsCircle*")
pm.parent(world = True)
#Creating lists/dictionaries for the groups
#Since I wanted to parent my objects by their number I had to put all objects in lists/dictionries to get access.
groups = pm.ls('group*')
nbs = [int(n.split('group')[-1]) for n in groups]
groupDic = dict(zip(nbs, groups))
#Create a list/dictionary for the joints
joint = pm.ls('joint*', type='joint')
nbs = [int(n.split('joint')[-1]) for n in joint]
jointDic = dict(zip(nbs, joint))
common = list(set(groupDic.keys())&set(jointDic.keys()))
#Parenting the groups to the joints
for i in common:
pm.parent(groupDic[i], jointDic[i])
#Reseting the transformations of the groups and then unparenting them to still have the transformation data of the joints
pm.select("group*")
pm.makeIdentity()
pm.parent(world = True)
#Creating a list/dictionary for the nurbs aswell that will be parented to the groups in numeric order
nurbs_sh = pm.ls('nurbsCircle*', type='nurbsCurve')
#I had to get the transformation information from the nurbs before parenting them with anything would work(took a long time to get it right).
nurbs_tr = pm.listRelatives(nurbs_sh, p=1)
nbs = [int(n.split('nurbsCircle')[-1]) for n in nurbs_tr]
curveDic = dict(zip(nbs, nurbs_tr))
common = list(set(groupDic.keys())&set(curveDic.keys()))
#Parent the nurbs to the groups
for i in common:
pm.parent(curveDic[i], groupDic[i])
#Select the nurbs and reset transformations and then freeze transform
pm.select("nurbsCircle*")
pm.makeIdentity()
#Orient constrain the controllers/nurbs to the joints
for i in common:
pm.orientConstraint(curveDic[i], jointDic[i])
#Parent the 2nd group with the first controller. Do this for the whole hierarchy.
for i in common:
pm.parent(groupDic[i+1], curveDic[i])
#I'm getting keyError after I put the "+1" in my groupDic and I don't know why, although it still works, I guess.
autoCraneRig()
Here's an example for how to call a specific function/command when a button is clicked, and how to get the value of an int field. The key is in naming the fields, so you can reference the UI control later.
import pymel.core as pm
def ui():
if (pm.window("myWindow", exists=True)):
pm.deleteUI("myWindow")
window = pm.window("myWindow", t="My Window", w=400, h=200)
pm.columnLayout(adj=True)
pm.intField("myIntField")
pm.button("Button", aop=True, command="action()")
pm.showWindow(window)
def action():
print("Button clicked!")
value = pm.intField("myIntField", q=True, v=True)
print(value)
ui()
If you want to get more into making UI's, I would recommend you watch these two videos:
PySide UI Creation in Maya: Video One
PySide UI Creation in Maya: Video Two
I'm trying to write an automated script in Wand on Python that builds the frames for a text animation by writing a caption of an image one letter at a time.
The problem is that when I write one letter using the caption command (documentation here http://docs.wand-py.org/en/0.4.4/wand/image.html) it writes a giant letter, while when I write the whole text, it is fitted nicely in the image.
I thought of a possible solution: write the first letter colored and the rest transparent and cycle through that, however the caption command is not capable of doing multicolored text as far as I know.
If someone could suggest me another option I would be grateful. I could use draw.text, however that doesn't automatically calculate when to go on the next line as far as I know...
My code looks like this:
imgname = random.choice(os.listdir('/home/gionny/Downloads/HighResImg'))
text = 'Hello, world! This is a slightly longer sentence.'
fontname = random.choice(os.listdir('/home/gionny/Downloads/font'))
with Image(filename='HighResImg/'+imgname) as i:
font = Font(path = 'font/'+fontname, color = Color('#fff'))
textWidth = i.width*2/3
textHeight = i.height*2/3
offsetLeft = (i.width - textWidth)/2
offsetTop = (i.height - textHeight)/2
with Image(filename='logo.gif') as l:
l.resize(80,80)
l.transparentize(0.7)
with Drawing() as draw:
draw.composite(operator='atop', left=i.width-90, top=i.height-90, width=l.width, height=l.height, image=l)
for c in range(0, len(text)):
caption = i.caption(text = text[c], left = offsetLeft, top = offsetTop, width=textWidth, height=textHeight, font = font, gravity = 'center')
print(caption)
cl = i.clone()
cl.format = 'jpeg'
cl.save(filename='Text/text'+str(c)+'.jpg')
cl.destroy()
If someone could suggest me another option I would be grateful. I could use draw.text, however that doesn't automatically calculate when to go on the next line as far as I know...
There's no quick way around it, you are responsible for calculating the x,y coordinates with each iteration. Especially when using mixed fonts pulled at random.
The method wand.drawing.Drawing.get_font_metrics has be provided for this sort of thing. Simply keep an accumulator & update with each iteration.
from wand.image import Image
from wand.color import Color
from wand.drawing import Drawing
with Image(width=400, height=250, background=Color("skyblue")) as background:
leftOffset = 35 # <= Starting position.
topOffset = background.height/2;
for letter in "Hello World":
with Drawing() as ctx:
ctx.font = "TimesNewRoman"
ctx.font_size = 64.0
metrics = ctx.get_font_metrics(background, letter)
ctx.text(leftOffset, int(topOffset+metrics.text_height/4), letter)
with Image(width=background.width,
height=background.height,
background=Color("transparent")) as frame:
ctx.draw(frame)
background.sequence.append(frame)
leftOffset += int(metrics.text_width) # <= Adjust for next iteration.
background.save(filename="output.gif")
Now for repeating the next-line process, just increase topOffset by the font metrics text_height if the leftOffset is greater than canvas width.
I'm trying to create o hierarchy of joints for a skeleton in maya python. And I'm doing this
def makeSkelet(args):
helperSkelet('Root_Locator', 'root_Joint')
helperSkelet('Pelvis_Locator', 'pelvis_Joint')
helperSkelet('Spine_Locator', 'spine_Joint')
helperSkelet('Spine01_Locator', 'spine01_Joint')
helperSkelet('Spine02_Locator', 'spine02_Joint')
helperSkelet('Neck_Locator', 'neck_Joint')
helperSkelet('Head_Locator', 'head_Joint')
mc.select(cl=True)
helperSkelet('ArmL_Locator', 'armL_joint')
helperSkelet('ElbowL_Locator', 'elbowL_Joint')
helperSkelet('HandL_Locator', 'handL_Joint')
mc.select(cl=True)
helperSkelet('ArmR_Locator', 'armR_joint')
helperSkelet('ElbowR_Locator', 'elbowR_Joint')
helperSkelet('HandR_Locator', 'handR_Joint')
mc.select(cl=True)
helperSkelet('HipL_Locator', 'hipL_joint')
helperSkelet('KneeL_Locator', 'kneeL_Joint')
helperSkelet('AnkleL_Locator', 'ankleL_Joint')
helperSkelet('FootL_Locator', 'footL_Joint')
mc.select(cl=True)
helperSkelet('HipR_Locator', 'hipR_joint')
helperSkelet('KneeR_Locator', 'kneeR_Joint')
helperSkelet('AnkleR_Locator', 'ankleR_Joint')
helperSkelet('FootR_Locator', 'footR_Joint')
Now this works fine, because the joints must be created in this order. (the helper skelet is a function where i create the joint with the reference to a locator position)
I was wondering if there is a more optimized way to do this considering the order or creation must be kept .
Thank you
If by "optimize" you mean getting better performace, I agree with what #downshift said.
If what you meant was instead making your code "cleaner" (more general or scalable or simply more pythonic), here's another way you can do the same, which is a bit more compact (and separates the logic from your input):
def helperSkeletGroup(group, symmetric=False):
# quick workaround to capitalize a word, leaving the following letters unchanged
capitalize = lambda s: s[:1].upper() + s[1:]
symmetric_group = []
for elem in group:
if symmetric:
symmetric_group.append('{0}R'.format(elem))
elem = '{0}L'.format(elem)
# format locators and joints
loc, joint = '{0}_Locator'.format(capitalize(elem)), '{0}_Joint'.format(elem)
helperSkelet(loc, joint)
cmds.select(cl=True)
if symmetric_group:
helperSkeletGroup(symmetric_group)
helperSkeletGroup(['root', 'pelvis', 'spine', 'spine01', 'spine02', 'neck', 'head'])
helperSkeletGroup(['arm', 'elbow', 'hand'], True)
helperSkeletGroup(['hip', 'knee', 'ankle', 'foot'], True)
This comes with a few advantages:
it handles symmetry for you
the code doesn't grow too much, as the number of joints increases
if at some point you want to change the naming convention for locators and joints, you can do it by changing a single line
Alternatively, you could go with an OOP approach.
Here's an example:
class Skeleton:
def __init__(self):
self.joint_groups = []
def add_joint_group(self, group, symmetric=False):
# quick workaround to capitalize a word, leaving the following letters unchanged
capitalize = lambda s: s[:1].upper() + s[1:]
processed, processed_symmetric = [], []
for elem in group:
if symmetric:
processed_symmetric.append('{0}R'.format(elem))
elem = '{0}L'.format(elem)
processed.append(('{0}_Locator'.format(capitalize(elem)), '{0}_Joint'.format(elem)))
self.joint_groups.append(processed)
if processed_symmetric:
self.add_joint_group(processed_symmetric)
def helper_skelet(self, loc, joint):
# your helper logic goes here
print loc, joint
def build(self):
for group in self.joint_groups:
for loc, joint in group:
self.helper_skelet(loc, joint)
cmds.select(cl=True)
skeleton = Skeleton()
skeleton.add_joint_group(['root', 'pelvis', 'spine', 'spine01', 'spine02', 'neck', 'head'])
skeleton.add_joint_group(['arm', 'elbow', 'hand'], True)
skeleton.add_joint_group(['hip', 'knee', 'ankle', 'foot'], True)
from pprint import pformat
print pformat(skeleton.joint_groups)
skeleton.build()
Here the code is a bit longer but it is all contained in a single object, where you could store additional data, which you get only at construction time and which you might need later on.
EDIT (to answer #Giakaama's question in the comment):
If you save the class in a separate file skeleton_class.py, you can import the class in your main.py (or whatever you want to call it), as such:
from skeleton_class import Skeleton
where the lower-case skeleton_class refers to your module (read: file) and Skeleton is the class itself.
Once you've done that, you can do the same as above:
skeleton = Skeleton()
skeleton.add_joint_group(['root', 'pelvis', 'spine', 'spine01', 'spine02', 'neck', 'head'])
skeleton.add_joint_group(['arm', 'elbow', 'hand'], True)
skeleton.build()
I have been developing a GUI for reading continuous data from a serial port. After reading the data, some calculations are made and the results will be plotted and refreshed (aka dynamic plotting). I use the wx backend provided in the matplotlib for this purposes. To do this, I basically use an array to store my results, in which I keep appending it to, after each calculation, and replot the whole graph. To make it "dynamic", I just set the x-axis lower and upper limits for each iteration. Something like found in:
http://eli.thegreenplace.net/2008/08/01/matplotlib-with-wxpython-guis/
The problem, however, is that since the data is continuous, and if I keep plotting it, eventually the system memory will run out and system will crash. Is there any other way I can plot my result continuously?
To do this, I basically use an array
to store my results, in which I keep
appending it to
Try limiting the size of this array, either by deleting old data or by deleting every n-th entry (the screen resolution will prevent all entries to be displayed anyway). I assume you write all the data to disk so you won't lose anything.
Also, analise your code for memory leaks. Stuff you use and don't need anymore but that doesn't get garbage-collected because you still have a reference to it.
I have created such a component with pythons Tkinter. The source is here.
Basically, you have to keep the plotted data somewhere. You cannot keep an infinite amount of data points in memory, so you either have to save it to disk or you have to overwrite old data points.
Data and representation of data are two different things. You might want to store your data to disk if it's important data to be analyzed later, but only keep a fixed period of time or the last N points for display purposes. You could even let the user pick the time frame to be displayed.
I actually ran into this problem (more of a mental block, actually...).
First of all I copy-pasted some wx Plot code from wx Demo Code.
What I do is keep a live log of a value, and compare it to two markers (min and max, shown as red and green dotted lines) (but I will make these 2 markers optional - hence the optional parameters).
In order to implement the live log, I first wanted to use the deque class, but since the data is in tuple mode (x,y coordinates) I gave up and just tried to rewrite the entire parameter list of tuples: see _update_coordinates.
It works just fine for keeping track of the last 100-10,000 plots. Would have also included a printscreen, but I'm too much of a noob at stackoverflow to be allowed :))
My live parameter is updated every 0.25 seconds over a 115kbps UART.
The trick is at the end, in the custom refresh method!
Here is most of the code:
class DefaultPlotFrame(wx.Frame):
def __init__(self, ymin=0, ymax=MAXIMUM_PLOTS, minThreshold=None,
maxThreshold=None, plotColour='blue',
title="Default Plot Frame",
position=(10,10),
backgroundColour="yellow", frameSize=(400,300)):
self.minThreshold = minThreshold
self.maxThreshold = maxThreshold
self.frame1 = wx.Frame(None, title="wx.lib.plot", id=-1, size=(410, 340), pos=position)
self.panel1 = wx.Panel(self.frame1)
self.panel1.SetBackgroundColour(backgroundColour)
self.ymin = ymin
self.ymax = ymax
self.title = title
self.plotColour = plotColour
self.lines = [None, None, None]
# mild difference between wxPython26 and wxPython28
if wx.VERSION[1] < 7:
self.plotter = plot.PlotCanvas(self.panel1, size=frameSize)
else:
self.plotter = plot.PlotCanvas(self.panel1)
self.plotter.SetInitialSize(size=frameSize)
# enable the zoom feature (drag a box around area of interest)
self.plotter.SetEnableZoom(False)
# list of (x,y) data point tuples
self.coordinates = []
for x_item in range(MAXIMUM_PLOTS):
self.coordinates.append((x_item, (ymin+ymax)/2))
self.queue = deque(self.coordinates)
if self.maxThreshold!=None:
self._update_max_threshold()
#endif
if self.lockThreshold!=None:
self._update_min_threshold()
#endif
self.line = plot.PolyLine(self.coordinates, colour=plotColour, width=1)
self.lines[0] = (self.line)
self.gc = plot.PlotGraphics(self.lines, title, 'Time', 'Value')
self.plotter.Draw(self.gc, xAxis=(0, MAXIMUM_PLOTS), yAxis=(ymin, ymax))
self.frame1.Show(True)
def _update_max_threshold(self):
if self.maxThreshold!=None:
self.maxCoordinates = []
for x_item in range(MAXIMUM_PLOTS):
self.maxCoordinates.append((x_item, self.maxThreshold))
#endfor
self.maxLine = plot.PolyLine(self.maxCoordinates, colour="green", width=1)
self.maxMarker = plot.PolyMarker(self.maxCoordinates, colour="green", marker='dot')
self.lines[1] = self.maxMarker
#endif
def _update_live_param(self, liveParam, minParam, maxParam):
if minParam!=None:
self.minThreshold = int(minParam)
self._update_min_threshold()
#endif
if maxParam!=None:
self.maxThreshold = int(maxParam)
self._update_max_threshold()
#endif
if liveParam!=None:
self._update_coordinates(int(liveParam))
#endif
def _update_coordinates(self, newValue):
newList = []
for x,y in self.coordinates[1:]:
newList.append((x-1, y))
#endfor
newList.append((x, newValue))
print "New list", newList
self.line = (plot.PolyLine(newList, colour=self.plotColour, width=1))
self.lines[0] = self.line
self.coordinates = newList
def _MyLIVE_MAGIC_refresh__(self, liveParam=None, minParam=None, maxParam=None):
self._update_live_param(liveParam, minParam, maxParam)
self.gc = plot.PlotGraphics(self.lines, self.title, 'Time', 'Value')
self.plotter.Draw(self.gc, xAxis=(0, MAXIMUM_PLOTS), yAxis=(self.ymin, self.ymax))
self.plotter.Refresh()
self.frame1.Refresh()