Source code
/* Copyright (C) 2006-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;
import artofillusion.ui.*;
import artofillusion.ui.Compound3DManipulator.*;
import artofillusion.math.*;
import artofillusion.object.*;
import java.awt.event.*;
import java.awt.*;
import java.util.*;
import buoy.event.*;
import buoy.widget.*;
/**
* This editing tool presents a compound interface for moving, scaling, and rotating objects.
*/
public class MoveScaleRotateObjectTool extends EditingTool
{
private boolean dragInProgress, draggingObjects;
private ArrayList<ObjectInfo> objects;
private CoordinateSystem originalCoords[];
private Object3D originalObjects[];
private ObjectInfo clickedObject;
private Point clickPoint;
private Vec3 rotationCenter[];
private UndoRecord undo;
private final Compound3DManipulator manipulator;
private boolean tooltipsEnabled, tooltipsAdded, applyToChildren = true;
private int scaleAround = POSITIONS_FIXED, rotateAround = PARENT_CENTER;
private static BToolTip ROTATE_TIP = new BToolTip(Translate.text("moveScaleRotateObjectTool.rotateTipText"));
private static BToolTip MOVE_TIP = new BToolTip(Translate.text("moveScaleRotateObjectTool.moveTipText"));
private static BToolTip SCALE_TIP = new BToolTip(Translate.text("moveScaleRotateObjectTool.scaleTipText"));
private static final int POSITIONS_FIXED = 0;
private static final int POSITIONS_SCALE = 1;
private static final int OBJECT_CENTER = 0;
private static final int PARENT_CENTER = 1;
private static final int SELECTION_CENTER = 2;
public MoveScaleRotateObjectTool(LayoutWindow fr)
{
super(fr);
initButton("moveScaleRotate");
manipulator = new Compound3DManipulator();
manipulator.addEventLink(HandlePressedEvent.class, this, "handlePressed");
manipulator.addEventLink(HandleDraggedEvent.class, this, "handleDragged");
manipulator.addEventLink(HandleReleasedEvent.class, this, "handleReleased");
}
public int whichClicks()
{
return OBJECT_CLICKS+ALL_CLICKS;
}
public boolean allowSelectionChanges()
{
return !dragInProgress;
}
public String getToolTipText()
{
return Translate.text("moveScaleRotateObjectTool.tipText");
}
/** Get the LayoutWindow to which this tool belongs. */
public LayoutWindow getWindow()
{
return (LayoutWindow) theWindow;
}
public void drawOverlay(ViewerCanvas view)
{
BoundingBox selectionBounds = findSelectionBounds(view.getCamera());
if (!dragInProgress && manipulator.getViewMode() == Compound3DManipulator.NPQ_MODE && selectionBounds != null)
{
// Calculate the axis directions.
ObjectInfo firstObj = getWindow().getSelectedObjects().iterator().next();
CoordinateSystem coords = firstObj.getCoords();
manipulator.setNPQAxes(coords.getUpDirection().cross(coords.getZDirection()), coords.getUpDirection(), coords.getZDirection());
}
manipulator.draw(view, selectionBounds);
if (!dragInProgress)
{
if (selectionBounds != null)
theWindow.setHelpText(Translate.text("moveScaleRotateObjectTool.helpText"));
else
theWindow.setHelpText(Translate.text("moveScaleRotateObjectTool.errorText"));
}
}
public void mousePressed(WidgetMouseEvent e, ViewerCanvas view)
{
BoundingBox selectionBounds = findSelectionBounds(view.getCamera());
dragInProgress = false;
draggingObjects = false;
if (selectionBounds != null)
dragInProgress = manipulator.mousePressed(e, view, selectionBounds);
}
public void mousePressedOnObject(WidgetMouseEvent e, ViewerCanvas view, int obj)
{
draggingObjects = true;
BoundingBox selectionBounds = findSelectionBounds(view.getCamera());
Rectangle screenBounds = manipulator.findScreenBounds(selectionBounds, view.getCamera());
handlePressed(manipulator.new HandlePressedEvent(view, Compound3DManipulator.MOVE, Compound3DManipulator.ALL, screenBounds, selectionBounds, e));
clickedObject = view.getScene().getObject(obj);
clickPoint = e.getPoint();
}
public void mouseDragged(WidgetMouseEvent e, ViewerCanvas view)
{
if (draggingObjects)
{
BoundingBox selectionBounds = findSelectionBounds(view.getCamera());
Rectangle screenBounds = manipulator.findScreenBounds(selectionBounds, view.getCamera());
Point dragPoint = e.getPoint();
int dx = dragPoint.x - clickPoint.x;
int dy = dragPoint.y - clickPoint.y;
if (e.isShiftDown() && !e.isControlDown())
{
if (Math.abs(dx) > Math.abs(dy))
dy = 0;
else
dx = 0;
}
Vec3 v;
if (e.isControlDown())
v = view.getCamera().getCameraCoordinates().getZDirection().times(-dy*0.01);
else
v = view.getCamera().findDragVector(clickedObject.getCoords().getOrigin(), dx, dy);
handleDragged(manipulator.new HandleDraggedEvent(view, Compound3DManipulator.MOVE, Compound3DManipulator.ALL, screenBounds, selectionBounds, e, Mat4.translation(v.x, v.y, v.z)));
}
else
manipulator.mouseDragged(e, view);
}
public void mouseReleased(WidgetMouseEvent e, ViewerCanvas view)
{
if (draggingObjects)
handleReleased(null);
else
manipulator.mouseReleased(e, view);
}
protected void handlePressed(HandlePressedEvent ev)
{
objects = new ArrayList<ObjectInfo>();
int sel[];
if (applyToChildren)
sel = getWindow().getSelectionWithChildren();
else
sel = getWindow().getSelectedIndices();
for (int i = 0; i < sel.length; i++)
objects.add(theWindow.getScene().getObject(sel[i]));
originalCoords = new CoordinateSystem[objects.size()];
for (int i = 0; i < originalCoords.length; i++)
originalCoords[i] = objects.get(i).getCoords();
if (ev.getHandleType() == Compound3DManipulator.SCALE)
{
originalObjects = new Object3D[objects.size()];
for (int i = 0; i < originalObjects.length; i++)
originalObjects[i] = objects.get(i).getObject();
}
if (ev.getHandleType() == Compound3DManipulator.ROTATE)
{
manipulator.setRotateAroundSelectionCenter(rotateAround == SELECTION_CENTER);
rotationCenter = new Vec3[objects.size()];
for (int i = 0; i < rotationCenter.length; i++)
{
ObjectInfo parent = objects.get(i);
if (rotateAround == SELECTION_CENTER)
rotationCenter[i] = new Vec3();
else
rotationCenter[i] = originalCoords[i].getOrigin();
if (rotateAround == PARENT_CENTER)
while (parent.getParent() != null)
{
parent = parent.getParent();
if (parent.selected)
rotationCenter[i] = parent.getCoords().getOrigin();
}
}
}
}
protected void handleDragged(HandleDraggedEvent ev)
{
if (undo == null)
{
undo = new UndoRecord(theWindow, false);
for (int i = 0; i < originalCoords.length; i++)
{
originalCoords[i] = originalCoords[i].duplicate();
undo.addCommand(UndoRecord.COPY_COORDS, new Object[] {objects.get(i).getCoords(), originalCoords[i]});
if (ev.getHandleType() == Compound3DManipulator.SCALE)
{
originalObjects[i] = originalObjects[i].duplicate();
undo.addCommand(UndoRecord.COPY_OBJECT, new Object[] {objects.get(i).getObject(), originalObjects[i]});
}
}
}
transformObjects(ev);
theWindow.setModified();
theWindow.updateImage();
if (ev.getHandleType() == Compound3DManipulator.MOVE)
{
Vec3 drag = ev.getTransform().times(new Vec3());
theWindow.setHelpText(Translate.text("reshapeMeshTool.dragText",
Math.round(drag.x*1e5)/1e5+", "+Math.round(drag.y*1e5)/1e5+", "+Math.round(drag.z*1e5)/1e5));
}
else if (ev.getHandleType() == Compound3DManipulator.ROTATE)
theWindow.setHelpText(Translate.text("rotateMeshTool.dragText", Double.toString(Math.round(ev.getRotationAngle()*1e5*180.0/Math.PI)/1e5)));
else if (ev.getAxis() == Compound3DManipulator.UV && !ev.getMouseEvent().isShiftDown())
theWindow.setHelpText(Translate.text("scaleMeshTool.dragText", Math.round(ev.getPrimaryScale()*1e5)/1e5+", "+Math.round(ev.getSecondaryScale()*1e5)/1e5));
else
theWindow.setHelpText(Translate.text("scaleMeshTool.dragText", Double.toString(Math.round(ev.getPrimaryScale()*1e5)/1e5)));
}
protected void handleReleased(HandleReleasedEvent ev)
{
if (undo != null)
theWindow.setUndoRecord(undo);
theWindow.updateImage();
undo = null;
rotationCenter = null;
dragInProgress = false;
}
/**
* Apply a transformation to the objects.
*/
private void transformObjects(HandleDraggedEvent ev)
{
for (int i = 0; i < objects.size(); i++)
{
Mat4 transform = ev.getTransform();
if (ev.getHandleType() == Compound3DManipulator.SCALE)
{
if (scaleAround == POSITIONS_SCALE)
{
// Scale the positions.
CoordinateSystem coords = originalCoords[i].duplicate();
coords.transformOrigin(transform);
objects.get(i).getCoords().copyCoords(coords);
}
// Work out the scale factors for the size.
double xscale = 1.0, yscale = 1.0, zscale = 1.0;
Vec3 xdir = originalCoords[i].fromLocal().timesDirection(transform.timesDirection(new Vec3(1.0, 0.0, 0.0)));
Vec3 ydir = originalCoords[i].fromLocal().timesDirection(transform.timesDirection(new Vec3(0.0, 1.0, 0.0)));
Vec3 zdir = originalCoords[i].fromLocal().timesDirection(transform.timesDirection(new Vec3(0.0, 0.0, 1.0)));
xdir.normalize();
ydir.normalize();
zdir.normalize();
if (ev.getMouseEvent().isShiftDown())
{
xscale = yscale = zscale = ev.getPrimaryScale();
}
else if (ev.getAxis() == Compound3DManipulator.UV)
{
Vec3 uDir = manipulator.getAxisDirection(Compound3DManipulator.U, ev.getView());
Vec3 vDir = manipulator.getAxisDirection(Compound3DManipulator.V, ev.getView());
double xudot = Math.abs(xdir.dot(uDir));
double yudot = Math.abs(ydir.dot(uDir));
double zudot = Math.abs(zdir.dot(uDir));
double xvdot = Math.abs(xdir.dot(vDir));
double yvdot = Math.abs(ydir.dot(vDir));
double zvdot = Math.abs(zdir.dot(vDir));
if (xudot >= yudot && xudot >= zudot)
{
xscale = ev.getPrimaryScale();
if (yvdot >= zvdot)
yscale = ev.getSecondaryScale();
else
zscale = ev.getSecondaryScale();
}
else if (yudot >= xudot && yudot >= zudot)
{
yscale = ev.getPrimaryScale();
if (xvdot >= zvdot)
xscale = ev.getSecondaryScale();
else
zscale = ev.getSecondaryScale();
}
else
{
zscale = ev.getPrimaryScale();
if (xvdot >= yvdot)
xscale = ev.getSecondaryScale();
else
yscale = ev.getSecondaryScale();
}
}
else
{
Vec3 scaleDir = manipulator.getAxisDirection(ev.getAxis(), ev.getView());
double xdot = Math.abs(xdir.dot(scaleDir));
double ydot = Math.abs(ydir.dot(scaleDir));
double zdot = Math.abs(zdir.dot(scaleDir));
if (xdot >= ydot && xdot >= zdot)
xscale = ev.getPrimaryScale();
else if (ydot >= xdot && ydot >= zdot)
yscale = ev.getPrimaryScale();
else
zscale = ev.getPrimaryScale();
}
Vec3 size = originalObjects[i].getBounds().getSize();
size.x *= xscale;
size.y *= yscale;
size.z *= zscale;
objects.get(i).getObject().setSize(size.x, size.y, size.z);
getWindow().getScene().objectModified(objects.get(i).getObject());
}
else
{
if (ev.getHandleType() == Compound3DManipulator.ROTATE)
{
Vec3 center = rotationCenter[i];
transform = Mat4.translation(center.x, center.y, center.z).times(transform.times(Mat4.translation(-center.x, -center.y, -center.z)));
}
CoordinateSystem coords = originalCoords[i].duplicate();
coords.transformCoordinates(transform);
objects.get(i).getCoords().copyCoords(coords);
}
}
}
public void keyPressed(KeyPressedEvent e, ViewerCanvas view)
{
if (e.getKeyCode() == KeyEvent.VK_W && e.getModifiersEx() == 0)
{
// Change the axis mode.
ViewMode mode = manipulator.getViewMode();
if (mode == Compound3DManipulator.XYZ_MODE)
manipulator.setViewMode(Compound3DManipulator.UV_MODE);
else if (mode == Compound3DManipulator.UV_MODE)
manipulator.setViewMode(Compound3DManipulator.NPQ_MODE);
else
manipulator.setViewMode(Compound3DManipulator.XYZ_MODE);
theWindow.updateImage();
return;
}
if (e.getKeyCode() == KeyEvent.VK_F1)
{
// Enable tooltips.
tooltipsEnabled = !tooltipsEnabled;
if (tooltipsEnabled && !tooltipsAdded)
{
ViewerCanvas allViews[] = ((MeshEditorWindow) theWindow).getAllViews();
for (int i = 0; i < allViews.length; i++)
allViews[i].addEventLink(ToolTipEvent.class, this, "showToolTip");
tooltipsAdded = true;
}
if (!tooltipsEnabled)
BToolTip.hide();
return;
}
// Handle arrow keys. This is equivalent to dragging the first selected object by one pixel.
double dx, dy;
int key = e.getKeyCode();
if (key == KeyPressedEvent.VK_UP)
{
dx = 0;
dy = -1;
}
else if (key == KeyPressedEvent.VK_DOWN)
{
dx = 0;
dy = 1;
}
else if (key == KeyPressedEvent.VK_LEFT)
{
dx = -1;
dy = 0;
}
else if (key == KeyPressedEvent.VK_RIGHT)
{
dx = 1;
dy = 0;
}
else
return;
e.consume();
int sel[];
if (applyToChildren)
sel = getWindow().getSelectionWithChildren();
else
sel = getWindow().getSelectedIndices();
if (sel.length == 0)
return; // No objects are selected.
if (view.getSnapToGrid())
{
double scale = view.getGridSpacing()*view.getScale();
if (!e.isAltDown())
scale /= view.getSnapToSubdivisions();
dx *= scale;
dy *= scale;
}
else if (e.isAltDown())
{
dx *= 10;
dy *= 10;
}
Camera cam = view.getCamera();
CoordinateSystem cameraCoords = cam.getCameraCoordinates();
Scene theScene = theWindow.getScene();
Vec3 v;
if (e.isControlDown())
v = cameraCoords.getZDirection().times(-dy*0.01);
else
{
Vec3 origin = theScene.getObject(sel[0]).getCoords().getOrigin();
if (Math.abs(origin.minus(cameraCoords.getOrigin()).dot(cameraCoords.getZDirection())) < 1e-10)
{
// The object being moved is in the plane of the camera, so use a slightly
// different point to avoid dividing by zero.
origin = origin.plus(cameraCoords.getZDirection().times(cam.getClipDistance()));
}
v = cam.findDragVector(origin, dx, dy);
}
theWindow.setUndoRecord(undo = new UndoRecord(getWindow(), false));
ArrayList<ObjectInfo> toMove = new ArrayList<ObjectInfo>();
for (int i = 0; i < sel.length; i++)
toMove.add(theScene.getObject(sel[i]));
for (int i = 0; i < toMove.size(); i++)
{
CoordinateSystem c = toMove.get(i).getCoords();
undo.addCommand(UndoRecord.COPY_COORDS, new Object [] {c, c.duplicate()});
c.setOrigin(c.getOrigin().plus(v));
}
theWindow.getScene().applyTracksAfterModification(toMove);
theWindow.updateImage();
undo = null;
}
private void showToolTip(ToolTipEvent ev)
{
if (!tooltipsEnabled)
return;
ViewerCanvas view = (ViewerCanvas) ev.getWidget();
HandleType type = manipulator.getHandleTypeAtLocation(ev.getPoint(), view, findSelectionBounds(view.getCamera()));
if (type == Compound3DManipulator.MOVE)
MOVE_TIP.processEvent(ev);
else if (type == Compound3DManipulator.ROTATE)
ROTATE_TIP.processEvent(ev);
else if (type == Compound3DManipulator.SCALE)
SCALE_TIP.processEvent(ev);
else
BToolTip.hide();
}
/**
* This method returns a bounding box for the selected objects in view coordinates,
* or null if nothing is selected.
*/
protected BoundingBox findSelectionBounds(Camera cam)
{
boolean anything = false;
BoundingBox bounds = null;
for (ObjectInfo info : getWindow().getSelectedObjects())
{
BoundingBox objBounds = info.getBounds().transformAndOutset(info.getCoords().fromLocal());
if (!anything)
bounds = objBounds;
else
bounds = bounds.merge(objBounds);
anything = true;
}
return (bounds == null ? null : bounds.transformAndOutset(cam.getWorldToView()));
}
public void iconDoubleClicked()
{
BCheckBox childrenBox = new BCheckBox(Translate.text("applyToUnselectedChildren"), applyToChildren);
BComboBox centerChoice = new BComboBox(new String [] {
Translate.text("individualObjectCenters"),
Translate.text("parentObject"),
Translate.text("centerOfSelection")
});
centerChoice.setSelectedIndex(rotateAround);
RowContainer rotateRow = new RowContainer();
rotateRow.add(Translate.label("rotateAround"));
rotateRow.add(centerChoice);
BComboBox scaleChoice = new BComboBox(new String [] {
Translate.text("remainFixed"),
Translate.text("scaleWithObjects")
});
scaleChoice.setSelectedIndex(scaleAround);
RowContainer scaleRow = new RowContainer();
scaleRow.add(Translate.label("objectPositions"));
scaleRow.add(scaleChoice);
ComponentsDialog dlg = new ComponentsDialog(theFrame, Translate.text("moveRotateResizeToolTitle"),
new Widget [] {childrenBox, rotateRow, scaleRow}, new String [] {null, null, null});
if (!dlg.clickedOk())
return;
applyToChildren = childrenBox.getState();
rotateAround = centerChoice.getSelectedIndex();
scaleAround = scaleChoice.getSelectedIndex();
}
}
Run 1
The code is structured logically with clear method separation and helpful comments, but readability is hindered by several factors. The 'transformObjects' method is particularly long and contains deeply nested conditional logic (up to 4 levels), making it difficult to follow. The 'handleDragged' method has complex, chained expressions like 'Math.round(drag.x*1e5)/1e5' that obscure intent. Multiple actions are packed into single lines, especially in event handlers, reducing clarity. Import statements are overly broad (using wildcards), which can make it harder to identify dependencies. The 'keyPressed' method is verbose with repetitive arrow-key handling code that could be simplified. While the code follows consistent naming conventions and includes some useful comments, the deep nesting and occasional lack of clear intent lower its readability. The class has a clear purpose and the undo/redo logic is reasonably organized, but complex transformation math and UI event handling create cognitive overhead.
Run 2
The code is moderately readable but has significant issues. The structure is logical with clear method separation, but many methods are overly long (transformObjects, keyPressed, handleDragged) and could be broken into smaller functions. Nesting is problematic in transformObjects and keyPressed, with multiple levels of conditionals making logic hard to follow. Intent is generally clear from method names and comments, though some variable names are ambiguous (e.g., 'sel', 'v'). Code length is excessive for what the tool does - many methods could be more concise through helper methods. Action granularity varies: some lines perform single operations while others (especially in transformObjects and keyPressed) pack multiple actions. Reading flow is disrupted by deeply nested conditionals and long methods, though the overall class organization follows a logical pattern from initialization to event handling to transformation logic.
Run 3
The code has clear class structure and well-named methods, but readability suffers from several issues. Long methods like handleDragged, transformObjects, and keyPressed exceed 100 lines with deeply nested conditionals, especially in the scaling logic where multiple if-else branches and dot product calculations create cognitive overhead. The nesting depth reaches 5-6 levels in some areas, making control flow difficult to trace. While the code is concise in places, the transformObjects method is unnecessarily long and would benefit from decomposition into helper methods for scale calculations. Action granularity is mixed—some lines perform single operations while others chain multiple method calls. Reading flow is disrupted by inconsistent brace style (both Allman and K&R) and long mathematical expressions. The code's intent is generally clear from comments and names, but the complexity of the 3D math operations obscures the main logic flow. Overall, functional but challenging to read without domain expertise.
Run 1
Code Structure: Event handlers and helper methods provide logical organization, but transformation calculations, UI updates, and interaction state remain concentrated in one large class. Nesting: Most handlers are shallow, while transformObjects contains several nested loops and conditionals that increase reading effort. Clarity of intent: Descriptive method names, named constants, and targeted comments explain the overall purpose; abbreviated mathematical variables and dense matrix expressions obscure details. Code length: Much of the length is justified by functionality, but repeated selection logic, axis comparisons, and help-text formatting could be extracted into helpers. Action granularity: Most statements perform one action per line, although chained assignments, combined declarations, and assignment within a method call reduce clarity. Reading flow: The press-drag-release lifecycle is natural and related functionality is generally grouped, but inconsistent indentation, long lines, and transitions between interaction handling and mathematical calculations interrupt the flow.
Run 2
Code Structure: Event handlers and helper methods provide logical organization, but transformation mathematics, UI behavior, and undo management remain coupled in one large class. Nesting: Most handlers are shallow, while scaling and rotation logic contain several nested loops and conditionals. Clarity of intent: Descriptive method names, constants, and comments explain the overall purpose; abbreviated mathematical variables, magic numbers, and shared mutable drag state require closer inspection. Code length: Several methods, particularly transformObjects and keyPressed, are lengthy and could be split into focused helpers; selection and movement logic are repeated. Action granularity: Most statements perform one action per line, although chained assignments, combined declarations, and dense expressions occasionally obscure intermediate steps. Reading flow: The press-drag-release sequence and spacing support natural reading, but inconsistent indentation, omitted braces, and long argument lists interrupt it. Overall, reasonably readable event-driven code with concentrated complexity.
Run 3
Code Structure: Event handlers and helper methods provide logical organization, but transformation calculations, UI updates, and interaction state remain concentrated in one large class. Nesting: Most handlers are shallow, while scaling logic and parent-center traversal introduce several nested levels. Clarity of intent: Descriptive method names, constants, and targeted comments explain the tool's purpose; abbreviated mathematical variables and implicit relationships between mutable fields require extra effort. Code length: Much of the length is justified by functionality, but repeated selection handling, axis comparisons, and long keyboard and transformation methods could be extracted into helpers. Action granularity: Statements generally perform one action per line, though chained assignments, combined declarations, and assignment inside a method call reduce clarity. Reading flow: The press-drag-release sequence is natural and whitespace separates tasks effectively, but inconsistent indentation, omitted braces, long expressions, and state dependencies interrupt smooth reading.