Source code
/* Copyright (C) 1999-2009 by Peter Eastman
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. */
package artofillusion.animation;
import artofillusion.*;
import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.ui.*;
import buoy.event.*;
import buoy.widget.*;
import java.awt.*;
import java.text.*;
/** SkeletonTool is an EditingTool used for manipulating the skeletons of objects. */
public class SkeletonTool extends EditingTool
{
private static final int CLICK_TOL = 6;
private static final int HANDLE_SIZE = 4;
private static final int NO_HANDLES = 0;
private static final int UNLOCKED_HANDLES = 1;
private static final int ALL_HANDLES = 2;
private static int whichHandles = UNLOCKED_HANDLES;
private Point clickPoint;
private Vec3 clickPos, handlePos[];
private boolean hideHandle[], invertDragDir;
private int clickedHandle;
private IKSolver ik;
private Mesh oldMesh, mesh;
private UndoRecord undo;
private boolean allowCreating;
private String helpText;
public SkeletonTool(MeshEditorWindow fr, boolean allowCreating)
{
super(fr);
this.allowCreating = allowCreating;
initButton("skeleton");
clickedHandle = -1;
helpText = Translate.text(allowCreating ? "skeletonTool.helpText" : "skeletonTool.helpTextNoCreate");
}
public void activate()
{
super.activate();
theWindow.setHelpText(helpText);
}
public int whichClicks()
{
return ALL_CLICKS;
}
public String getToolTipText()
{
return Translate.text("skeletonTool.tipText");
}
/** Find the positions of all the degree-of-freedom handles. */
private void findHandlePositions(Mat4 objToScreen, Joint j, ViewerCanvas view)
{
if (j.parent == null || whichHandles == NO_HANDLES)
hideHandle = new boolean [] {true, true, true, true};
else if (whichHandles == UNLOCKED_HANDLES)
hideHandle = new boolean [] {j.angle1.fixed, j.angle2.fixed, j.length.fixed, j.twist.fixed};
else
hideHandle = new boolean [4];
double scale = 70.0/view.getScale();
handlePos = new Vec3 [] {new Vec3(0.0, scale, 0.0), new Vec3(scale, 0.0, 0.0),
new Vec3(0.0, 0.0, scale), new Vec3(-scale*0.6, -scale*0.6, scale*0.4)};
Vec3 jointPos = new Vec3(j.coords.getOrigin());
objToScreen.transform(jointPos);
int jointX = (int) jointPos.x;
int jointY = (int) jointPos.y;
for (int i = 0; i < hideHandle.length; i++)
{
if (hideHandle[i])
continue;
j.coords.fromLocal().transformDirection(handlePos[i]);
handlePos[i].add(j.coords.getOrigin());
objToScreen.transform(handlePos[i]);
int x = (int) handlePos[i].x;
int y = (int) handlePos[i].y;
if (x == jointX && y == jointY)
hideHandle[i] = true;
}
}
public void drawOverlay(ViewerCanvas view)
{
if (ik != null)
return;
MeshViewer mv = (MeshViewer) view;
int selected = mv.getSelectedJoint();
mesh = (oldMesh == null ? (Mesh) mv.getController().getObject().getObject() : oldMesh);
Skeleton s = mesh.getSkeleton();
Joint j = s.getJoint(selected);
if (j == null)
return;
Camera cam = mv.getCamera();
CoordinateSystem objCoords = mv.getDisplayCoordinates();
Mat4 objToScreen = cam.getWorldToScreen().times(objCoords.fromLocal());
if (clickedHandle == -1)
{
// Draw the handles for each degree of freedom.
findHandlePositions(objToScreen, j, view);
Vec3 jointPos = new Vec3(j.coords.getOrigin());
objToScreen.transform(jointPos);
int jointX = (int) jointPos.x;
int jointY = (int) jointPos.y;
for (int i = 0; i < hideHandle.length; i++)
{
if (hideHandle[i])
continue;
int x = (int) handlePos[i].x;
int y = (int) handlePos[i].y;
view.drawLine(new Point(jointX, jointY), new Point(x, y), Color.darkGray);
view.drawBox(x-HANDLE_SIZE/2, y-HANDLE_SIZE/2, HANDLE_SIZE, HANDLE_SIZE, ViewerCanvas.handleColor);
}
}
else if (clickedHandle < 2)
{
// Draw a circle to show the DOF being moved.
double len = j.length.pos;
Mat4 m1, m2;
if (clickedHandle == 0)
{
m1 = Mat4.zrotation(j.twist.pos*Math.PI/180.0);
m2 = j.parent.coords.fromLocal().times(Mat4.yrotation(j.angle2.pos*Math.PI/180.0));
}
else
{
m1 = Mat4.xrotation(j.angle1.pos*Math.PI/180.0).times(Mat4.zrotation(j.twist.pos*Math.PI/180.0));
m2 = j.parent.coords.fromLocal();
}
m2 = objToScreen.times(m2);
Point pos[] = new Point [64];
for (int i = 0; i < pos.length; i++)
{
double angle = i*2.0*Math.PI/pos.length;
Vec3 p = new Vec3(0.0, 0.0, len);
p = m1.timesDirection(p);
if (clickedHandle == 0)
p = m2.times(Mat4.xrotation(angle).timesDirection(p));
else
p = m2.times(Mat4.yrotation(angle).timesDirection(p));
pos[i] = new Point((int) p.x, (int) p.y);
}
for (int i = 1; i < pos.length; i++)
view.drawLine(new Point(pos[i-1].x, pos[i-1].y), new Point(pos[i].x, pos[i].y), Color.darkGray);
view.drawLine(new Point(pos[0].x, pos[0].y), new Point(pos[pos.length-1].x, pos[pos.length-1].y), Color.darkGray);
}
}
public void mousePressed(WidgetMouseEvent e, ViewerCanvas view)
{
MeshViewer mv = (MeshViewer) view;
ViewerCanvas allViews[] = ((MeshEditorWindow) theWindow).getAllViews();
mesh = (Mesh) mv.getController().getObject().getObject();
Skeleton s = mesh.getSkeleton();
Joint selectedJoint = s.getJoint(mv.getSelectedJoint());
Camera cam = mv.getCamera();
CoordinateSystem objCoords = mv.getDisplayCoordinates();
Mat4 objToScreen = cam.getWorldToScreen().times(objCoords.fromLocal());
clickPoint = e.getPoint();
if (e.isControlDown() && allowCreating)
{
// Create a new joint.
undo = new UndoRecord(theWindow, false, UndoRecord.COPY_OBJECT, new Object [] {mesh, mesh.duplicate()});
Joint j, parent = s.getJoint(mv.getSelectedJoint());
if (parent == null)
{
double distToJoint = cam.getDistToScreen();
clickPos = cam.convertScreenToWorld(clickPoint, distToJoint);
objCoords.toLocal().transform(clickPos);
j = new Joint(new CoordinateSystem(clickPos, Vec3.vz(), Vec3.vy()), null, "Root "+s.getNextJointID());
j.angle1.fixed = j.angle2.fixed = true;
s.addJoint(j, -1);
}
else
{
double distToJoint = cam.getWorldToView().timesZ(parent.coords.getOrigin());
clickPos = cam.convertScreenToWorld(clickPoint, distToJoint);
objCoords.toLocal().transform(clickPos);
Vec3 zdir = clickPos.minus(parent.coords.getOrigin());
zdir.normalize();
Vec3 ydir = cam.getCameraCoordinates().getZDirection().cross(zdir);
ydir.normalize();
j = new Joint(new CoordinateSystem(clickPos, zdir, ydir), parent, "Bone "+s.getNextJointID());
s.addJoint(j, parent.id);
}
for (int k = 0; k < allViews.length; k++)
((MeshViewer) allViews[k]).setSelectedJoint(j.id);
boolean moving[] = new boolean [s.getNumJoints()];
moving[s.findJointIndex(j.id)] = true;
ik = new IKSolver(s, mv.getLockedJoints(), moving);
theWindow.updateImage();
theWindow.updateMenus();
oldMesh = (Mesh) mesh.duplicate();
return;
}
// See whether a handle was clicked.
if (selectedJoint != null)
{
findHandlePositions(objToScreen, selectedJoint, view);
for (int i = 0; i < hideHandle.length; i++)
{
if (!hideHandle[i] && Math.abs(handlePos[i].x-clickPoint.x) <= 0.5*CLICK_TOL && Math.abs(handlePos[i].y-clickPoint.y) <= 0.5*CLICK_TOL)
{
clickedHandle = i;
oldMesh = (Mesh) mesh.duplicate();
Vec2 jointPos = objToScreen.timesXY(selectedJoint.coords.getOrigin());
invertDragDir = (handlePos[i].x < jointPos.x);
theWindow.updateImage();
return;
}
}
}
// Determine which joint was clicked on.
Joint joint[] = s.getJoints();
int i;
for (i = 0; i < joint.length; i++)
{
Vec2 pos = objToScreen.timesXY(joint[i].coords.getOrigin());
if ((clickPoint.x > pos.x-CLICK_TOL) &&
(clickPoint.x < pos.x+CLICK_TOL) &&
(clickPoint.y > pos.y-CLICK_TOL) &&
(clickPoint.y < pos.y+CLICK_TOL))
break;
}
if (i == joint.length)
{
// No joint was clicked on, so deselect the selected joint.
for (int j = 0; j < allViews.length; j++)
((MeshViewer) allViews[j]).setSelectedJoint(-1);
theWindow.updateImage();
theWindow.updateMenus();
return;
}
if (e.isShiftDown())
{
// Toggle whether this joint is locked.
for (int j = 0; j < allViews.length; j++)
{
MeshViewer v = (MeshViewer) allViews[j];
if (v.isJointLocked(joint[i].id))
v.unlockJoint(joint[i].id);
else
v.lockJoint(joint[i].id);
}
theWindow.updateImage();
return;
}
// Make it the selected joint, and prepare to drag it.
for (int j = 0; j < allViews.length; j++)
((MeshViewer) allViews[j]).setSelectedJoint(joint[i].id);
clickPos = joint[i].coords.getOrigin();
theWindow.updateImage();
theWindow.updateMenus();
boolean moving[] = new boolean [s.getNumJoints()];
moving[s.findJointIndex(joint[i].id)] = true;
ik = new IKSolver(s, mv.getLockedJoints(), moving);
oldMesh = (Mesh) mesh.duplicate();
}
public void mouseDragged(final WidgetMouseEvent e, ViewerCanvas view)
{
final MeshViewer mv = (MeshViewer) view;
final CoordinateSystem objCoords = mv.getDisplayCoordinates();
final Camera cam = mv.getCamera();
final Mesh mesh = (Mesh) mv.getController().getObject().getObject();
final Skeleton s = mesh.getSkeleton();
final Joint selectedJoint = s.getJoint(mv.getSelectedJoint());
if (clickedHandle > -1)
{
// Adjust a single degree of freedom.
if (undo == null)
undo = new UndoRecord(getWindow(), false, UndoRecord.COPY_OBJECT, new Object [] {mesh, mesh.duplicate()});
double dist = clickPoint.x-e.getPoint().x;
if (invertDragDir)
dist = -dist;
Joint origJoint = oldMesh.getSkeleton().getJoint(mv.getSelectedJoint());
Joint.DOF dof = null;
Joint.DOF origDof = null;
String name = null;
switch (clickedHandle)
{
case 0:
dof = selectedJoint.angle1;
origDof = origJoint.angle1;
dist *= 1.8/Math.PI;
name = "X Bend";
break;
case 1:
dof = selectedJoint.angle2;
origDof = origJoint.angle2;
dist *= -1.8/Math.PI;
name = "Y Bend";
break;
case 2:
dof = selectedJoint.length;
origDof = origJoint.length;
dist /= -view.getScale();
name = "Length";
break;
case 3:
dof = selectedJoint.twist;
origDof = origJoint.twist;
dist *= 1.8/Math.PI;
name = "Twist";
break;
}
dof.set(origDof.pos+dist);
selectedJoint.recalcCoords(true);
if (!mv.getSkeletonDetached())
adjustMesh(mesh);
mv.getController().objectChanged();
theWindow.updateImage();
NumberFormat nf = NumberFormat.getNumberInstance();
nf.setMaximumFractionDigits(3);
theWindow.setHelpText(name+": "+nf.format(dof.pos));
return;
}
if (ik == null)
return;
boolean converged = false;
Vec3 target[] = new Vec3 [s.getNumJoints()];
int jointIndex = s.findJointIndex(selectedJoint.id);
ActionProcessor process = view.getActionProcessor();
do
{
Vec3 jointPos = objCoords.fromLocal().times(selectedJoint.coords.getOrigin());
double distToJoint = cam.getWorldToView().timesZ(jointPos);
Vec3 goal = cam.convertScreenToWorld(e.getPoint(), distToJoint);
objCoords.toLocal().transform(goal);
if (undo == null)
undo = new UndoRecord(SkeletonTool.this.getWindow(), false, UndoRecord.COPY_OBJECT, new Object [] {mesh, mesh.duplicate()});
target[jointIndex] = goal;
converged = ik.solve(target, 100);
if (!mv.getSkeletonDetached())
adjustMesh(mesh);
((ObjectViewer) view).getController().objectChanged();
theWindow.updateImage();
} while (!converged && (process == null || (!process.hasEvent() && !process.hasBeenStopped())));
}
public void mouseReleased(WidgetMouseEvent e, ViewerCanvas view)
{
if (undo == null && e.getClickCount() == 2 && !e.isShiftDown() && !e.isControlDown())
((MeshEditorWindow) theWindow).editJointCommand(); // They double-clicked without dragging.
ik = null;
mesh = oldMesh = null;
clickedHandle = -1;
if (undo != null)
theWindow.setUndoRecord(undo);
undo = null;
theWindow.setHelpText(helpText);
}
public void iconDoubleClicked()
{
BComboBox dofChoice = new BComboBox(new String [] {
Translate.text("noDOF"),
Translate.text("unlockedDOF"),
Translate.text("allDOF"),
});
dofChoice.setSelectedIndex(whichHandles);
ComponentsDialog dlg = new ComponentsDialog(theFrame, Translate.text("skeletonToolTitle"),
new Widget [] {dofChoice}, new String [] {null});
if (!dlg.clickedOk())
return;
whichHandles = dofChoice.getSelectedIndex();
theWindow.updateImage();
}
/** Adjust the mesh after the skeleton has changed. */
protected void adjustMesh(Mesh newMesh)
{
Skeleton.adjustMesh(oldMesh, newMesh);
}
}
Run 1
The code has some positive aspects: clear method names, reasonable use of static final constants, and logical class organization. However, several significant issues hurt readability. The mousePressed method is excessively long, mixing joint creation, handle detection, joint selection, and locking logic. Nesting is deep in places, especially within conditional blocks for joint creation and dragging. Some methods (drawOverlay, mousePressed) contain multiple responsibilities, and complex mathematical transformations are written inline without explanatory helper methods. Variable names are mostly descriptive, but some terse names like 'j', 's', 'mv' persist. The reading flow is disjointed—comment lines like '// Draw a circle...' help, but the dense coordinate math obscures clarity. Overall, while not unreadable, the code would benefit significantly from decomposition into smaller helper methods, reduced nesting, and extraction of complex logic into named operations.
Run 2
The code is generally structured and consistent in naming, but it has several readability issues. Methods like `mousePressed` and `drawOverlay` are overly long, violating the single-responsibility principle and mixing multiple actions. Deep nesting is evident in `mousePressed`, especially in joint creation, hit testing, and toggling locked joints, making the logic hard to follow. The `switch` block in `mouseDragged` handles branching but is still embedded in a long method. Many lines perform multiple operations (e.g., constructor calls with multiple arguments, chained transformations), reducing clarity. The flow is natural in some places but disrupted by heavy inline comments explaining complex branches rather than extracting helper methods. Overall, the code is functional and comments help, but it lacks decomposition and would be difficult to maintain or extend without careful study.
Run 3
The code has a clear purpose and follows a logical class structure, but readability is heavily compromised by excessive nesting (especially in mousePressed, with multiple if-else blocks inside control flow), inconsistent indentation, and long methods that mix multiple responsibilities. For example, mousePressed handles joint creation, handle detection, selection, locking, and drag preparation all in one method, making intent hard to follow. Action granularity is mixed: some lines perform multiple operations, while others are properly separated. The repeated manual transformation of points and duplicated view update logic add noise. Naming is mostly descriptive, but magic numbers (e.g., 1.8/Math.PI, 70.0, 0.6 scale factors) obscure intent. The findHandlePositions and drawOverlay methods are moderately readable but still contain deeply nested loops and inline coordinate math. Overall, the code functions but requires significant effort to parse; refactoring into smaller methods with clearer separation of concerns and reduced nesting would improve readability.
Run 1
Code Structure: Methods logically separate rendering, mouse events, configuration, and mesh adjustment, but mousePressed and mouseDragged combine several responsibilities and rely on shared mutable state. Nesting: Mostly moderate, with early returns reducing depth; creation and handle-selection branches still require tracking nested conditions. Clarity of intent: Descriptive method names and section comments explain the workflow, while abbreviated variables, numeric handle indices, and unexplained mathematical constants obscure details. Code length: Reasonable for the functionality, but long event handlers and repeated selection, undo, and refresh operations could be extracted into helpers. Action granularity: Generally one statement per line, although chained assignments, dense expressions, and grouped declarations reduce precision. Reading flow: Event methods follow a natural interaction sequence, but inconsistent indentation, long lines, and state dependencies across callbacks make local reasoning harder.
Run 2
Code Structure: Clearly separated event handlers and a few focused helpers provide logical organization, but mousePressed and mouseDragged combine several responsibilities and depend heavily on shared mutable state. Nesting: Mostly moderate, with early returns limiting depth; nested handle selection and joint-creation branches require extra attention. Clarity of intent: Descriptive method names, constants, and explanatory comments clarify behavior, while abbreviated variables, numeric handle indices, and unexplained mathematical factors obscure details. Code length: Reasonable for the functionality, but repeated coordinate transformations, selection updates, and undo initialization could be extracted into helpers. Action granularity: Most lines perform one action, although chained assignments and long constructor or transformation expressions reduce scanability. Reading flow: The interaction sequence is natural and comments mark major phases, but inconsistent indentation, omitted braces, dense expressions, and state dependencies across callbacks interrupt smooth reading.
Run 3
Code Structure: Event handlers and helper methods provide logical organization, but large handlers combine selection, creation, undo management, and geometry updates. Nesting: Mostly moderate, with early returns reducing depth; mousePressed still contains several nested branches and loops. Clarity of intent: Method names and comments explain the main operations, but abbreviated variables, numeric handle indices, magic constants, and shared mutable state obscure details. Code length: Reasonable for the functionality, though repeated view updates, coordinate transformations, and selection loops could be extracted into helpers. Action granularity: Most statements perform one action per line, but chained assignments and dense expressions occasionally combine operations. Reading flow: The event lifecycle is easy to follow, while inconsistent brace indentation, long conditions, and state dependencies between handlers interrupt otherwise natural progression.