package com.vitco.layout.content.sideview;
import com.jidesoft.action.CommandMenuBar;
import com.jidesoft.swing.JideButton;
import com.threed.jpct.Config;
import com.threed.jpct.SimpleVector;
import com.vitco.core.EngineInteractionPrototype;
import com.vitco.core.data.container.Voxel;
import com.vitco.low.hull.HullManager;
import com.vitco.manager.async.AsyncAction;
import com.vitco.manager.pref.PrefChangeListener;
import com.vitco.settings.VitcoSettings;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
/**
* Creates one side view instance (one perspective) and the specific user interaction.
*/
public class SideView extends EngineInteractionPrototype implements SideViewInterface {
private final int side;
// resets the view for this side
private void resetView() {
switch (side) {
case 0:
camera.setView(VitcoSettings.SIDE_VIEW1_CAMERA_POSITION);
break;
case 1:
camera.setView(VitcoSettings.SIDE_VIEW2_CAMERA_POSITION);
break;
case 2:
camera.setView(VitcoSettings.SIDE_VIEW3_CAMERA_POSITION);
break;
default: break;
}
camera.setFOVLimits(VitcoSettings.SIDE_VIEW_ZOOM_FOV, VitcoSettings.SIDE_VIEW_ZOOM_FOV);
camera.setFOV(VitcoSettings.SIDE_VIEW_ZOOM_FOV);
}
// constructor
public SideView(Integer side) {
super(side);
this.side = side;
resetView();
}
// the current depth of the plane that is shown
private int currentplane = -1;
private int prevcurrentplane = -1; // the previous current plane
// --------------
// define ghost overlay that we draw
private boolean voxelOutlineOutdated = false;
SimpleVector[][] voxelOutlineData = new SimpleVector[0][];
private HullManager<Voxel> voxelOutlineManager = new HullManager<Voxel>();
@Override
protected SimpleVector[][] getGhostOverlay() {
if (voxelOutlineOutdated) {
voxelOutlineOutdated = false;
voxelOutlineData = voxelOutlineManager.getOutline(side);
}
return voxelOutlineData;
}
@Override
protected boolean updateGhostOverlay() {
boolean result = false;
Voxel[][] changedVoxel = data.getNewSideVoxel("side" + side, side, prevcurrentplane);
if (changedVoxel[0] == null) {
voxelOutlineManager.clear();
voxelOutlineOutdated = true;
result = true;
} else {
// remove voxels
for (Voxel remove : changedVoxel[0]) {
voxelOutlineManager.clearPosition(remove.posId);
}
// update has changed
if (changedVoxel[0].length > 0) {
voxelOutlineOutdated = true;
result = true;
}
}
// add new voxels
for (Voxel add : changedVoxel[1]) {
voxelOutlineManager.update(add.posId, add);
}
// update has changed
if (changedVoxel[1].length > 0) {
voxelOutlineOutdated = true;
result = true;
}
return result;
}
// --------------
// get the voxels to render
@Override
protected Voxel[] getVoxels() {
// get the current voxels
Voxel[] voxels = null;
switch (side) {
case 0:
voxels = data.getVoxelsXY(currentplane);
break;
case 1:
voxels = data.getVoxelsXZ(currentplane);
break;
case 2:
voxels = data.getVoxelsYZ(currentplane);
break;
default: break;
}
return voxels;
}
@Override
protected Voxel[][] getChangedVoxels() {
return data.getNewSideVoxel("side" + side, side, currentplane);
}
// index to keep it to "one voxel per position"
private final HashMap<String, Voxel> selectedAtPos = new HashMap<String, Voxel>();
private final HashMap<String, Integer> selectedCountAtPos = new HashMap<String, Integer>();
// list of known voxel
// Note: Not really necessary, but might save some trouble in the future
// when selection and deselection of a voxel occurs withing the same frame
private final HashSet<String> liveVoxel = new HashSet<String>();
@Override
protected Voxel[][] getChangedSelectedVoxels() {
// logic to keep it to "one voxel per position" (e.g. there is no need
// to add many voxel "in depth" since the view can't rotate)
Voxel[][] changed = data.getNewSelectedVoxel("side" + side);
ArrayList<Voxel> toRemove = new ArrayList<Voxel>();
ArrayList<Voxel> toAdd = new ArrayList<Voxel>();
if (changed[0] == null) { // rebuild
selectedAtPos.clear();
selectedCountAtPos.clear();
liveVoxel.clear();
toRemove = null;
} else {
// remove individual voxel
for (Voxel remove : changed[0]) {
String strId = null;
switch (side) {
case 0:
strId = remove.x + "_" + remove.y;
break;
case 1:
strId = remove.x + "_" + remove.z;
break;
case 2:
strId = remove.y + "_" + remove.z;
break;
default: break;
}
Integer count = selectedCountAtPos.get(strId);
if (count != null) {
count -= 1;
if (liveVoxel.remove(remove.getPosAsString())) {
if (count == 0) {
selectedCountAtPos.remove(strId);
toRemove.add(selectedAtPos.remove(strId));
} else {
selectedCountAtPos.put(strId, count);
}
}
}
}
}
for (Voxel added : changed[1]) {
String strId = null;
int[] pos = new int[3];
switch (side) {
case 0:
strId = added.x + "_" + added.y;
pos[0] = added.x;
pos[1] = added.y;
break;
case 1:
strId = added.x + "_" + added.z;
pos[0] = added.x;
pos[2] = added.z;
break;
case 2:
strId = added.y + "_" + added.z;
pos[1] = added.y;
pos[2] = added.z;
break;
default: break;
}
Integer count = selectedCountAtPos.get(strId);
if (liveVoxel.add(added.getPosAsString())) {
if (count == null) {
selectedCountAtPos.put(strId, 1);
Voxel voxel = new Voxel(-1, pos, added.getColor(), false, null, 0);
selectedAtPos.put(strId, voxel);
toAdd.add(voxel);
} else {
selectedCountAtPos.put(strId, count+1);
}
}
}
Voxel[][] result = new Voxel[][]{
toRemove == null ? null : new Voxel[toRemove.size()],
new Voxel[toAdd.size()]
};
if (toRemove != null) {
toRemove.toArray(result[0]);
}
toAdd.toArray(result[1]);
return result;
}
@Override
public final JPanel build() {
// draw the ghost voxels (outline)
container.setDrawGhostOverlay(true);
// make sure we can see into the distance
world.setClippingPlanes(Config.nearPlane,VitcoSettings.SIDE_VIEW_MAX_ZOOM*2);
selectedVoxelsWorld.setClippingPlanes(Config.nearPlane,VitcoSettings.SIDE_VIEW_MAX_ZOOM*2);
// set initial current plane
preferences.storeObject("currentplane_sideview" + (side + 1), 0);
// register change of current plane of this sideview
preferences.addPrefChangeListener("currentplane_sideview" + (side + 1), new PrefChangeListener() {
@Override
public void onPrefChange(Object o) {
prevcurrentplane = currentplane;
currentplane = (Integer)o;
// update the container information
container.setPlane(currentplane);
// invalidate this buffers (as the plane has changed)
data.invalidateSideViewBuffer("side" + side, side, currentplane);
data.invalidateSideViewBuffer("side" + side, side, prevcurrentplane);
container.doNotSkipNextWorldRender();
invalidateVoxels();
forceRepaint();
}
});
// register clip buttons
actionManager.registerAction("sideview_move_plane_in" + (side + 1), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
preferences.storeObject("currentplane_sideview" + (side + 1), currentplane-1);
voxelAdapter.replayHover();
}
});
actionManager.registerAction("sideview_move_plane_out" + (side + 1), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
preferences.storeObject("currentplane_sideview" + (side + 1), currentplane+1);
voxelAdapter.replayHover();
}
});
actionManager.registerAction("sideview_set_plane_to_zero" + (side + 1), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
preferences.storeObject("currentplane_sideview" + (side + 1), 0);
voxelAdapter.replayHover();
}
});
// complex action for repainting the icon with number
complexActionManager.registerActionIsUsed("sideview_set_plane_to_zero_button" + (side + 1));
complexActionManager.performWhenActionIsReady("sideview_set_plane_to_zero_button" + (side + 1), new Runnable() {
@Override
public void run() {
preferences.addPrefChangeListener("currentplane_sideview" + (side + 1), new PrefChangeListener() {
private JideButton button = null;
private BufferedImage originalBG = null;
@Override
public void onPrefChange(Object o) {
if (button == null || originalBG == null) { // remember the original icon
button = ((JideButton) complexActionManager.getAction("sideview_set_plane_to_zero_button" + (side + 1)));
Icon icon = button.getIcon();
originalBG = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
icon.paintIcon(null, originalBG.getGraphics(), 0, 0);
}
// print the number
BufferedImage image = new BufferedImage(originalBG.getWidth(), originalBG.getHeight(), BufferedImage.TYPE_INT_ARGB);
image.getGraphics().drawImage(originalBG,0,0, null);
Graphics2D ig = (Graphics2D)image.getGraphics();
// Anti-alias
ig.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
ig.setColor(Color.GRAY);
ig.setFont(new Font(ig.getFont().getName(), Font.PLAIN, 9));
ig.drawString(String.valueOf(o), 4, 16);
ig.dispose();
ImageIcon icon = new ImageIcon();
icon.setImage(image);
button.setIcon(icon);
}
});
}
});
// register zoom buttons
actionManager.registerAction("sideview_zoom_in_tb" + (side + 1), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
camera.zoomIn(VitcoSettings.SIDE_VIEW_COARSE_ZOOM_SPEED);
forceRepaint();
}
});
actionManager.registerAction("sideview_zoom_out_tb" + (side + 1), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
camera.zoomOut(VitcoSettings.SIDE_VIEW_COARSE_ZOOM_SPEED);
forceRepaint();
}
});
// register the reset view action
actionManager.registerAction("sideview_reset_view" + (side + 1), new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
resetView();
forceRepaint();
}
});
// register zoom (mouse wheel)
camera.setZoomLimits(VitcoSettings.SIDE_VIEW_MIN_ZOOM, VitcoSettings.SIDE_VIEW_MAX_ZOOM);
container.addMouseWheelListener(new MouseAdapter() {
@Override
public void mouseWheelMoved(final MouseWheelEvent e) {
asyncActionManager.addAsyncAction(new AsyncAction() {
@Override
public void performAction() {
int rotation = e.getWheelRotation();
if (rotation < 0) {
camera.zoomIn(Math.abs(rotation) * VitcoSettings.SIDE_VIEW_FINE_ZOOM_SPEED);
} else {
camera.zoomOut(rotation * VitcoSettings.SIDE_VIEW_FINE_ZOOM_SPEED);
}
voxelAdapter.replayHover();
animationAdapter.mouseMoved(e); // keep selection refreshed (zoom ~ mouse move)
container.doNotSkipNextWorldRender();
forceRepaint();
}
});
}
});
// make sure there is no plane selected
preferences.storeObject("engine_view_voxel_preview_plane", -1);
// register shifting and preview plane
MouseAdapter shiftingMouseAdapter = new MouseAdapter() {
// preview plane
// =======================
@Override
public void mouseEntered(MouseEvent e) {
asyncActionManager.addAsyncAction(new AsyncAction() {
@Override
public void performAction() {
preferences.storeObject("engine_view_voxel_preview_plane", side*2);
}
});
}
@Override
public void mouseExited(MouseEvent e) {
asyncActionManager.addAsyncAction(new AsyncAction() {
@Override
public void performAction() {
preferences.storeObject("engine_view_voxel_preview_plane", -1);
}
});
}
// shifting
// =======================
private Point mouse_down_point = null;
@Override
public void mousePressed(final MouseEvent e) {
asyncActionManager.addAsyncAction(new AsyncAction() {
@Override
public void performAction() {
if ((e.getModifiers() & (MouseEvent.BUTTON1_MASK | MouseEvent.BUTTON2_MASK)) != 0) {
mouse_down_point = e.getPoint();
} else {
mouse_down_point = null;
}
}
});
}
@Override
public void mouseReleased(MouseEvent e) {
asyncActionManager.addAsyncAction(new AsyncAction() {
@Override
public void performAction() {
mouse_down_point = null;
}
});
}
@Override
public void mouseDragged(final MouseEvent e) {
asyncActionManager.addAsyncAction(new AsyncAction() {
@Override
public void performAction() {
if (mouse_down_point != null) {
if (camera.isEnabled()) {
// keep speed the same for different container sizes (uses shift2D!)
camera.shift2D(150 * (float) (e.getX() - mouse_down_point.getX()) / container.getWidth(),
150 * (float) (e.getY() - mouse_down_point.getY()) / container.getHeight(),
VitcoSettings.SIDE_VIEW_SIDE_MOVE_FACTOR);
mouse_down_point = e.getPoint();
voxelAdapter.replayHover();
container.doNotSkipNextWorldRender();
forceRepaint();
}
}
}
});
}
};
container.addMouseMotionListener(shiftingMouseAdapter);
container.addMouseListener(shiftingMouseAdapter);
// holds the menu and the draw panel (container)
final JPanel wrapper = new JPanel();
wrapper.setLayout(new BorderLayout());
// prevent "flickering" when swapping windows
preferences.addPrefChangeListener("engine_view_bg_color", new PrefChangeListener() {
@Override
public void onPrefChange(Object o) {
wrapper.setBackground((Color) o);
}
});
// draw bounding box (to display the 2D outline)
preferences.addPrefChangeListener("use_bounding_box", new PrefChangeListener() {
@Override
public void onPrefChange(Object o) {
boolean useBoundingBox = (Boolean)o;
// overlay part
container.setDrawBoundingBox(useBoundingBox);
// redraw container
container.doNotSkipNextWorldRender();
forceRepaint();
}
});
// create menu
CommandMenuBar menuPanel = new CommandMenuBar();
menuGenerator.buildMenuFromXML(menuPanel, "com/vitco/layout/content/sideview/toolbar" + (side + 1) + ".xml");
menuPanel.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
// so the background doesn't show
menuPanel.setOpaque(true);
// add menu and container
wrapper.add(menuPanel, BorderLayout.SOUTH);
wrapper.add(container, BorderLayout.CENTER);
return wrapper;
}
@Override
public int getSide() {
return side;
}
}