/* * JaamSim Discrete Event Simulation * Copyright (C) 2012 Ausenco Engineering Canada Inc. * Copyright (C) 2016 JaamSim Software Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jaamsim.controllers; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Frame; import java.awt.Image; import java.awt.dnd.DragSourceDragEvent; import java.awt.dnd.DragSourceDropEvent; import java.awt.dnd.DragSourceEvent; import java.awt.dnd.DragSourceListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import com.jaamsim.DisplayModels.DisplayModel; import com.jaamsim.Graphics.DisplayEntity; import com.jaamsim.Graphics.LinkDisplayable; import com.jaamsim.basicsim.Entity; import com.jaamsim.basicsim.ObjectType; import com.jaamsim.basicsim.Simulation; import com.jaamsim.datatypes.IntegerVector; import com.jaamsim.events.EventManager; import com.jaamsim.input.ColourInput; import com.jaamsim.input.Input; import com.jaamsim.input.InputAgent; import com.jaamsim.input.InputErrorException; import com.jaamsim.input.KeywordIndex; import com.jaamsim.math.AABB; import com.jaamsim.math.Mat4d; import com.jaamsim.math.MathUtils; import com.jaamsim.math.Plane; import com.jaamsim.math.Quaternion; import com.jaamsim.math.Ray; import com.jaamsim.math.Transform; import com.jaamsim.math.Vec2d; import com.jaamsim.math.Vec3d; import com.jaamsim.math.Vec4d; import com.jaamsim.render.Action; import com.jaamsim.render.CameraInfo; import com.jaamsim.render.DisplayModelBinding; import com.jaamsim.render.Future; import com.jaamsim.render.LineProxy; import com.jaamsim.render.MeshDataCache; import com.jaamsim.render.MeshProtoKey; import com.jaamsim.render.OffscreenTarget; import com.jaamsim.render.PreviewCache; import com.jaamsim.render.RenderProxy; import com.jaamsim.render.RenderUtils; import com.jaamsim.render.Renderer; import com.jaamsim.render.TessFontKey; import com.jaamsim.render.TexCache; import com.jaamsim.render.WindowInteractionListener; import com.jaamsim.render.util.ExceptionLogger; import com.jaamsim.ui.ContextMenu; import com.jaamsim.ui.FrameBox; import com.jaamsim.ui.GUIFrame; import com.jaamsim.ui.LogBox; import com.jaamsim.ui.View; /** * Top level owner of the JaamSim renderer. This class both owns and drives the Renderer object, but is also * responsible for gathering rendering data every frame. * @author Matt Chudleigh * */ public class RenderManager implements DragSourceListener { private final static int EXCEPTION_STACK_THRESHOLD = 10; // The number of recoverable exceptions until a stack trace is output private final static int EXCEPTION_PRINT_RATE = 30; // The number of total exceptions until the overall log is printed /** * Default plane used for Mouse click intersections. */ static final Plane XY_PLANE = new Plane(); private int numberOfExceptions = 0; private static RenderManager s_instance = null; /** * Basic singleton pattern */ public static void initialize(boolean safeGraphics) { s_instance = new RenderManager(safeGraphics); } public static RenderManager inst() { return s_instance; } private final Thread managerThread; private final Renderer renderer; private final AtomicBoolean finished = new AtomicBoolean(false); private final AtomicBoolean fatalError = new AtomicBoolean(false); private final AtomicBoolean redraw = new AtomicBoolean(false); private final AtomicBoolean screenshot = new AtomicBoolean(false); private final AtomicBoolean showLinks = new AtomicBoolean(false); private final AtomicBoolean createLinks = new AtomicBoolean(false); private final double linkArrowSize = 0.2; private final ExceptionLogger exceptionLogger; private final HashMap<Integer, CameraControl> windowControls = new HashMap<>(); private final HashMap<Integer, View> windowToViewMap= new HashMap<>(); private int activeWindowID = -1; private final Object popupLock; private JPopupMenu lastPopup; /** * The last scene rendered */ private ArrayList<RenderProxy> cachedScene; private DisplayEntity selectedEntity = null; private long simTick = 0; private long dragHandleID = 0; private Vec3d dragCollisionPoint; private Vec3d dragEntityPosition; private ArrayList<Vec3d> dragEntityPoints; // The object type for drag-and-drop operation, if this is null, the user is not dragging private ObjectType dndObjectType; private long dndDropTime = 0; // The video recorder to sample private VideoRecorder recorder; // FIXME: the preview cache will cause a GUIFrame to be created, needs fixing for fully headless // operation private final PreviewCache previewCache = new PreviewCache(); // Below are special PickingIDs for resizing and dragging handles public static final long MOVE_PICK_ID = -1; // For now this order is implicitly the same as the handle order in RenderObserver, don't re arrange it without touching // the handle list public static final long RESIZE_POSX_PICK_ID = -2; public static final long RESIZE_NEGX_PICK_ID = -3; public static final long RESIZE_POSY_PICK_ID = -4; public static final long RESIZE_NEGY_PICK_ID = -5; public static final long RESIZE_PXPY_PICK_ID = -6; public static final long RESIZE_PXNY_PICK_ID = -7; public static final long RESIZE_NXPY_PICK_ID = -8; public static final long RESIZE_NXNY_PICK_ID = -9; public static final long ROTATE_PICK_ID = -10; public static final long LINEDRAG_PICK_ID = -11; // Line nodes start at this constant and proceed into the negative range, therefore this should be the lowest defined constant public static final long LINENODE_PICK_ID = -12; private RenderManager(boolean safeGraphics) { renderer = new Renderer(safeGraphics); exceptionLogger = new ExceptionLogger(EXCEPTION_STACK_THRESHOLD); managerThread = new Thread(new Runnable() { @Override public void run() { renderManagerLoop(); } }, "RenderManagerThread"); managerThread.start(); GUIFrame.registerCallback(new Runnable() { @Override public void run() { synchronized(redraw) { if (windowControls.size() == 0 && !screenshot.get()) { return; // Do not queue a redraw if there are no open windows } redraw.set(true); redraw.notifyAll(); } } }); popupLock = new Object(); } public static final void updateTime(long simTick) { if (!RenderManager.isGood()) return; RenderManager.inst().simTick = simTick; GUIFrame.updateUI(); } public static final void redraw() { if (!isGood()) return; GUIFrame.updateUI(); } public void createWindow(View view) { // First see if this window has already been opened for (Map.Entry<Integer, CameraControl> entry : windowControls.entrySet()) { if (entry.getValue().getView() == view) { // This view has a window, just reshow that one focusWindow(entry.getKey()); return; } } IntegerVector windSize = view.getWindowSize(); IntegerVector windPos = view.getWindowPos(); Image icon = GUIFrame.getWindowIcon(); CameraControl control = new CameraControl(renderer, view); int windowID = renderer.createWindow(windPos.get(0), windPos.get(1), windSize.get(0), windSize.get(1), view.getID(), view.getTitle(), view.getName(), icon, control); control.setWindowID(windowID); windowControls.put(windowID, control); windowToViewMap.put(windowID, view); GUIFrame.updateUI(); } public static final void clear() { if (!isGood()) return; RenderManager.inst().closeAllWindows(); } private void closeAllWindows() { ArrayList<Integer> windIDs = renderer.getOpenWindowIDs(); for (int id : windIDs) { renderer.closeWindow(id); } } public void windowClosed(int windowID) { // Update the state of the window in the input file View v = windowToViewMap.get(windowID); if (!v.getKeepWindowOpen()) InputAgent.applyArgs(v, "ShowWindow", "FALSE"); v.setKeepWindowOpen(false); windowControls.remove(windowID); windowToViewMap.remove(windowID); } public void setActiveWindow(int windowID) { activeWindowID = windowID; final View activeView = windowToViewMap.get(windowID); if (activeView != null) { EventQueue.invokeLater(new Runnable() { @Override public void run() { GUIFrame.setActiveView(activeView); } }); } } public static boolean isGood() { return (s_instance != null && !s_instance.finished.get() && !s_instance.fatalError.get()); } /** * Ideally, this states that it is safe to call initialize() (assuming isGood() returned false) * @return */ public static boolean canInitialize() { return s_instance == null; } private void renderManagerLoop() { while (!finished.get() && !fatalError.get()) { try { if (renderer.hasFatalError()) { // Well, something went horribly wrong fatalError.set(true); LogBox.formatRenderLog("Renderer failed with error: %s\n", renderer.getErrorString()); EventQueue.invokeAndWait(new Runnable() { @Override public void run() { LogBox.getInstance().setVisible(true); } }); // Do some basic cleanup windowControls.clear(); previewCache.clear(); break; } if (!renderer.isInitialized()) { // Give the renderer a chance to initialize try { Thread.sleep(100); } catch(InterruptedException e) {} continue; } double renderTime = EventManager.ticksToSecs(simTick); redraw.set(false); for (int i = 0; i < View.getAll().size(); i++) { View v = View.getAll().get(i); v.update(renderTime); } for (CameraControl cc : windowControls.values()) { cc.checkForUpdate(); } cachedScene = new ArrayList<>(); DisplayModelBinding.clearCacheCounters(); DisplayModelBinding.clearCacheMissData(); boolean screenShotThisFrame = screenshot.get(); long startNanos = System.nanoTime(); ArrayList<DisplayModelBinding> selectedBindings = new ArrayList<>(); // Update all graphical entities in the simulation final ArrayList<? extends Entity> allEnts = Entity.getAll(); for (int i = 0; i < allEnts.size(); i++) { DisplayEntity de; try { Entity e = allEnts.get(i); if (e instanceof DisplayEntity) de = (DisplayEntity)e; else continue; } catch (IndexOutOfBoundsException e) { break; } try { de.updateGraphics(renderTime); } // Catch everything so we don't screw up the behavior handling catch (Throwable e) { logException(e); } } long updateNanos = System.nanoTime(); int totalBindings = 0; for (int i = 0; i < allEnts.size(); i++) { DisplayEntity de; try { Entity e = allEnts.get(i); if (e instanceof DisplayEntity) de = (DisplayEntity)e; else continue; } catch (IndexOutOfBoundsException e) { break; } for (DisplayModelBinding binding : de.getDisplayBindings()) { try { totalBindings++; binding.collectProxies(renderTime, cachedScene); if (binding.isBoundTo(selectedEntity)) { selectedBindings.add(binding); } } catch (Throwable t) { // Log the exception in the exception list logException(t); } } } // Collect selection proxies second so they always appear on top for (DisplayModelBinding binding : selectedBindings) { try { binding.collectSelectionProxies(renderTime, cachedScene); } catch (Throwable t) { // Log the exception in the exception list logException(t); } } // Finally include the displayable links for linked entities if (showLinks.get()) { addLinkDisplays(cachedScene); } long endNanos = System.nanoTime(); renderer.setScene(cachedScene); String cacheString = " Hits: " + DisplayModelBinding.getCacheHits() + " Misses: " + DisplayModelBinding.getCacheMisses() + " Total: " + totalBindings; double gatherMS = (endNanos - updateNanos) / 1000000.0; double updateMS = (updateNanos - startNanos) / 1000000.0; String timeString = "Gather time (ms): " + gatherMS + " Update time (ms): " + updateMS; // Do some picking debug ArrayList<Integer> windowIDs = renderer.getOpenWindowIDs(); for (int id : windowIDs) { Renderer.WindowMouseInfo mouseInfo = renderer.getMouseInfo(id); if (mouseInfo == null || !mouseInfo.mouseInWindow) { // Not currently picking for this window renderer.setWindowDebugInfo(id, cacheString + " Not picking. " + timeString, new ArrayList<Long>()); continue; } List<PickData> picks = pickForMouse(id, false); ArrayList<Long> debugIDs = new ArrayList<>(picks.size()); StringBuilder dbgMsg = new StringBuilder(cacheString); dbgMsg.append(" Picked ").append(picks.size()); dbgMsg.append(" entities at (").append(mouseInfo.x); dbgMsg.append(", ").append(mouseInfo.y).append("): "); for (PickData pd : picks) { Entity ent = Entity.idToEntity(pd.id); if (ent != null) dbgMsg.append(ent.getName()); dbgMsg.append(", "); debugIDs.add(pd.id); } dbgMsg.append(timeString); renderer.setWindowDebugInfo(id, dbgMsg.toString(), debugIDs); } if (GUIFrame.getShuttingDownFlag()) { shutdown(); } renderer.queueRedraw(); if (screenShotThisFrame) { takeScreenShot(); } } catch (Throwable t) { // Make a note of it, but try to keep going logException(t); } // Wait for a redraw request synchronized(redraw) { while (!redraw.get()) { try { redraw.wait(); } catch (InterruptedException e) {} } } } exceptionLogger.printExceptionLog(); } public void popupMenu(final int windowID) { try { // Transfer control from the NEWT-EDT to the AWT-EDT EventQueue.invokeAndWait(new Runnable() { @Override public void run() { popupMenuImp(windowID); } }); } catch (InvocationTargetException ex) { assert(false); } catch (InterruptedException ex) { assert(false); } } // Temporary dumping ground until I find a better place for this // Note: this is intentionally package private to be called by an inner class void popupMenuImp(int windowID) { synchronized (popupLock) { Renderer.WindowMouseInfo mouseInfo = renderer.getMouseInfo(windowID); if (mouseInfo == null) { // Somehow this window was closed along the way, just ignore this click return; } final Frame awtFrame = renderer.getAWTFrame(windowID); if (awtFrame == null) { return; } List<PickData> picks = pickForMouse(windowID, false); ArrayList<DisplayEntity> ents = new ArrayList<>(); for (PickData pd : picks) { if (!pd.isEntity) { continue; } Entity ent = Entity.idToEntity(pd.id); if (ent instanceof DisplayEntity) { DisplayEntity de = (DisplayEntity)ent; if (de.isMovable()) // only a movable DisplayEntity responds to a right-click ents.add(de); } } if (!mouseInfo.mouseInWindow) { // Somehow this window does not currently have the mouse over it.... ignore? return; } final JPopupMenu menu = new JPopupMenu(); lastPopup = menu; menu.setLightWeightPopupEnabled(false); final int menuX = mouseInfo.x + awtFrame.getInsets().left; final int menuY = mouseInfo.y + awtFrame.getInsets().top; if (ents.size() == 0) { return; } // Nothing to show if (ents.size() == 1) { ContextMenu.populateMenu(menu, ents.get(0), menuX, menuY); } else { // Several entities, let the user pick the interesting entity first Collections.sort(ents, Input.uiSortOrder); for (final DisplayEntity de : ents) { JMenuItem thisItem = new JMenuItem(de.getName()); thisItem.addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent event ) { menu.removeAll(); ContextMenu.populateMenu(menu, de, menuX, menuY); menu.show(awtFrame, menuX, menuY); } } ); menu.add( thisItem ); } } menu.show(awtFrame, menuX, menuY); menu.repaint(); } // synchronized (_popupLock) } public void handleMouseClicked(int windowID, int x, int y, short count) { List<PickData> picks = pickForMouse(windowID, false); Collections.sort(picks, new SelectionSorter()); for (PickData pd : picks) { // Select the first entity after sorting if (pd.isEntity) { DisplayEntity ent = (DisplayEntity)Entity.idToEntity(pd.id); if (!ent.isMovable()) { continue; } FrameBox.setSelectedEntity(ent, true); Vec3d globalCoord = getGlobalPositionForMouseData(windowID, x, y, ent); ent.handleMouseClicked(count, globalCoord); GUIFrame.updateUI(); return; } } // If no entity is found, set the selected entity to the view window FrameBox.setSelectedEntity(windowToViewMap.get(windowID), false); GUIFrame.updateUI(); } /** * Utility, convert a window and mouse coordinate into a list of picking IDs for that pixel * @param windowID * @param mouseX * @param mouseY * @return */ private List<PickData> pickForMouse(int windowID, boolean precise) { Renderer.WindowMouseInfo mouseInfo = renderer.getMouseInfo(windowID); View view = windowToViewMap.get(windowID); if (mouseInfo == null || view == null || !mouseInfo.mouseInWindow) { // The mouse is not actually in the window, or the window was closed along the way return new ArrayList<>(); // empty set } Ray pickRay = RenderUtils.getPickRay(mouseInfo); return pickForRay(pickRay, view.getID(), precise); } /** * PickData represents enough information to sort a list of picks based on a picking preference * metric. For now it holds the object size and distance from pick point to object center * */ private static class PickData { public long id; public double size; public double dist; boolean isEntity; /** * This pick does not correspond to an entity, and is a handle or other UI element * @param id */ public PickData(long id, double d) { this.id = id; size = 0; dist = d; isEntity = false; } /** * This pick was an entity * @param id - the id * @param ent - the entity */ public PickData(long id, double d, DisplayEntity ent) { this.id = id; size = ent.getSize().mag3(); dist = d; isEntity = true; } } /** * This Comparator sorts based on entity selection preference */ private static class SelectionSorter implements Comparator<PickData> { @Override public int compare(PickData p0, PickData p1) { if (p0.isEntity && !p1.isEntity) { return -1; } if (!p0.isEntity && p1.isEntity) { return 1; } return Double.compare(p0.size, p1.size); } } /** * This Comparator sorts based on interaction handle priority */ private static class HandleSorter implements Comparator<PickData> { @Override public int compare(PickData p0, PickData p1) { int p0priority = getHandlePriority(p0.id); int p1priority = getHandlePriority(p1.id); if (p0priority == p1priority) return 0; return (p0priority < p1priority) ? 1 : -1; } } /** * This determines the priority for interaction handles if several are selectable at drag time * @param handleID * @return */ private static int getHandlePriority(long handleID) { if (handleID == MOVE_PICK_ID) return 1; if (handleID == LINEDRAG_PICK_ID) return 1; if (handleID <= LINENODE_PICK_ID) return 2; if (handleID == ROTATE_PICK_ID) return 3; if (handleID == RESIZE_POSX_PICK_ID) return 4; if (handleID == RESIZE_NEGX_PICK_ID) return 4; if (handleID == RESIZE_POSY_PICK_ID) return 4; if (handleID == RESIZE_NEGY_PICK_ID) return 4; if (handleID == RESIZE_PXPY_PICK_ID) return 5; if (handleID == RESIZE_PXNY_PICK_ID) return 5; if (handleID == RESIZE_NXPY_PICK_ID) return 5; if (handleID == RESIZE_NXNY_PICK_ID) return 5; return 0; } public Vec3d getNearestPick(int windowID) { Renderer.WindowMouseInfo mouseInfo = renderer.getMouseInfo(windowID); View view = windowToViewMap.get(windowID); if (mouseInfo == null || view == null || !mouseInfo.mouseInWindow) { // The mouse is not actually in the window, or the window was closed along the way return null; } Ray pickRay = RenderUtils.getPickRay(mouseInfo); List<Renderer.PickResult> picks = renderer.pick(pickRay, view.getID(), true); if (picks.size() == 0) { return null; } double pickDist = Double.POSITIVE_INFINITY; for (Renderer.PickResult pick : picks) { if (pick.dist < pickDist && pick.pickingID >= 0) { // Negative pickingIDs are reserved for interaction handles and are therefore not // part of the content pickDist = pick.dist; } } if (pickDist == Double.POSITIVE_INFINITY) { return null; } return pickRay.getPointAtDist(pickDist); } /** * Perform a pick from this world space ray * @param pickRay - the ray * @return */ private List<PickData> pickForRay(Ray pickRay, int viewID, boolean precise) { List<Renderer.PickResult> picks = renderer.pick(pickRay, viewID, precise); List<PickData> uniquePicks = new ArrayList<>(); // IDs that have already been added Set<Long> knownIDs = new HashSet<>(); for (Renderer.PickResult pick : picks) { if (knownIDs.contains(pick.pickingID)) { continue; } knownIDs.add(pick.pickingID); DisplayEntity ent = (DisplayEntity)Entity.idToEntity(pick.pickingID); if (ent == null) { // This object is not an entity, but may be a picking handle uniquePicks.add(new PickData(pick.pickingID, pick.dist)); } else { uniquePicks.add(new PickData(pick.pickingID, pick.dist, ent)); } } return uniquePicks; } /** * Pick on a window at a position other than the current mouse position * @param windowID * @param x * @param y * @return */ private Ray getRayForMouse(int windowID, int x, int y) { Renderer.WindowMouseInfo mouseInfo = renderer.getMouseInfo(windowID); if (mouseInfo == null) { return new Ray(); } return RenderUtils.getPickRayForPosition(mouseInfo.cameraInfo, x, y, mouseInfo.width, mouseInfo.height); } /** * Returns the global coordinates for the given entity corresponding * to given screen coordinates. * @param windowID - view window that clicked * @param x - horizontal raster coordinate * @param y - vertical raster coordinate * @param ent - entity whose local coordinates are returned * @return local coordinate for the mouse click */ public Vec3d getGlobalPositionForMouseData(int windowID, int x, int y, DisplayEntity ent) { // Determine the plane in the global coordinate system that corresponds // to the entity's local X-Y plane Transform trans = ent.getGlobalTrans(); Plane entityPlane = new Plane(); // Defaults to XY entityPlane.transform(trans, entityPlane); // Return the global coordinates for the point on the local X-Y plane // that lines up with the screen coordinates Ray mouseRay = getRayForMouse(windowID, x, y); double mouseDist = entityPlane.collisionDist(mouseRay); return mouseRay.getPointAtDist(mouseDist); } public Vec3d getRenderedStringSize(TessFontKey fontKey, double textHeight, String string) { return renderer.getTessFont(fontKey).getStringSize(textHeight, string); } public double getRenderedStringLength(TessFontKey fontKey, double textHeight, String string) { return renderer.getTessFont(fontKey).getStringLength(textHeight, string); } public int getRenderedStringPosition(TessFontKey fontKey, double textHeight, String string, double x) { return renderer.getTessFont(fontKey).getStringPosition(textHeight, string, x); } /** * Returns the x-coordinate for a given insertion position in a string. * Insertion position i is the location prior to the i-th character in the string. * * @param i - insertion position * @return x coordinate of the insertion position relative to the beginning of the string. */ public double getOffsetForStringPosition(TessFontKey fontKey, double textHeight, String string, int i) { StringBuilder sb = new StringBuilder(string); return getRenderedStringLength(fontKey, textHeight, sb.substring(0, i).toString()); } private void logException(Throwable t) { exceptionLogger.logException(t); numberOfExceptions++; // Only print the exception log periodically (this can get a bit spammy) if (numberOfExceptions % EXCEPTION_PRINT_RATE == 0) { LogBox.renderLog("Recoverable Exceptions from RenderManager: "); exceptionLogger.printExceptionLog(); LogBox.renderLog(""); } } public static void setSelection(Entity ent, boolean canMakeLink) { if (!RenderManager.isGood()) return; RenderManager.inst().setSelectEntity(ent, canMakeLink); } private void setSelectEntity(Entity ent, boolean canMakeLink) { if (ent instanceof DisplayEntity) { DisplayEntity oldEnt = selectedEntity; selectedEntity = (DisplayEntity)ent; if (createLinks.get() && canMakeLink) { if (selectedEntity != null && oldEnt != null && oldEnt != selectedEntity) { try { oldEnt.linkTo(selectedEntity); } catch (InputErrorException e) {} } } } else { selectedEntity = null; } GUIFrame.updateUI(); } public boolean isEntitySelected() { return (selectedEntity != null); } /** * This method gives the RenderManager a chance to handle mouse drags before the CameraControl * gets to handle it (note: this may need to be refactored into a proper event handling heirarchy) * @param dragInfo * @return true if the drag action was handled successfully. */ public boolean handleDrag(WindowInteractionListener.DragInfo dragInfo) { // If there is no object to move and the control is pressed then do nothing (return true) // If control is not pressed then move the camera (return false) if (selectedEntity == null || !selectedEntity.isMovable()) return dragInfo.controlDown(); // Find the start and current world space positions Ray firstRay = getRayForMouse(dragInfo.windowID, dragInfo.startX, dragInfo.startY); Ray currentRay = getRayForMouse(dragInfo.windowID, dragInfo.x, dragInfo.y); Ray lastRay = getRayForMouse(dragInfo.windowID, dragInfo.x - dragInfo.dx, dragInfo.y - dragInfo.dy); Transform trans = selectedEntity.getGlobalTrans(); Plane entityPlane = new Plane(); // Defaults to XY entityPlane.transform(trans, entityPlane); // Transform the plane to world space double firstDist = entityPlane.collisionDist(firstRay); double currentDist = entityPlane.collisionDist(currentRay); double lastDist = entityPlane.collisionDist(lastRay); // If the Control key is not pressed, then the selected entity handles the drag action if (!dragInfo.controlDown()) { Vec3d firstPt = firstRay.getPointAtDist(firstDist); Vec3d currentPt = currentRay.getPointAtDist(currentDist); boolean ret = selectedEntity.handleDrag(currentPt, firstPt); return ret; } // Handle each handle by type... // Missed the selected entity and its handles if (dragHandleID == 0) return true; // MOVE if (dragHandleID == MOVE_PICK_ID) return handleMove(currentRay, firstRay, currentDist, firstDist, dragInfo.shiftDown()); // RESIZE if (dragHandleID <= RESIZE_POSX_PICK_ID && dragHandleID >= RESIZE_NXNY_PICK_ID) return handleResize(currentRay, lastRay, currentDist, lastDist); // ROTATE if (dragHandleID == ROTATE_PICK_ID) return handleRotate(currentRay, lastRay, currentDist, lastDist); // LINE MOVE if (dragHandleID == LINEDRAG_PICK_ID) return handleLineMove(currentRay, lastRay, currentDist, lastDist, dragInfo.shiftDown()); // LINE NODE MOVE if (dragHandleID <= LINENODE_PICK_ID) return handleLineNodeMove(currentRay, firstRay, currentDist, firstDist, dragInfo.shiftDown()); return false; } //Moves the selected entity to a new position in space private boolean handleMove(Ray currentRay, Ray firstRay, double currentDist, double firstDist, boolean shift) { // Trap degenerate cases if (currentDist < 0 || currentDist == Double.POSITIVE_INFINITY || firstDist < 0 || firstDist == Double.POSITIVE_INFINITY) return true; // Vertical move if (shift) { Vec3d entPos = new Vec3d(dragEntityPosition); double zDiff = RenderUtils.getZDiff(dragCollisionPoint, currentRay, firstRay); entPos.z += zDiff; if (Simulation.isSnapToGrid()) entPos = Simulation.getSnapGridPosition(entPos, selectedEntity.getGlobalPosition()); selectedEntity.setInputForGlobalPosition(entPos); return true; } // Horizontal move Plane dragPlane = new Plane(new Vec3d(0, 0, 1), dragCollisionPoint.z); // XY plane at collision point double cDist = dragPlane.collisionDist(currentRay); double lDist = dragPlane.collisionDist(firstRay); if (cDist < 0 || cDist == Double.POSITIVE_INFINITY || lDist < 0 || lDist == Double.POSITIVE_INFINITY) return true; Vec3d cPoint = currentRay.getPointAtDist(cDist); Vec3d lPoint = firstRay.getPointAtDist(lDist); Vec3d del = new Vec3d(); del.sub3(cPoint, lPoint); Vec3d pos = new Vec3d(dragEntityPosition); pos.add3(del); if (Simulation.isSnapToGrid()) pos = Simulation.getSnapGridPosition(pos, selectedEntity.getGlobalPosition()); selectedEntity.setInputForGlobalPosition(pos); return true; } private boolean handleResize(Ray currentRay, Ray lastRay, double currentDist, double lastDist) { Vec3d currentPoint = currentRay.getPointAtDist(currentDist); Vec3d lastPoint = lastRay.getPointAtDist(lastDist); Vec3d size = selectedEntity.getSize(); Mat4d transMat = selectedEntity.getTransMatrix(); Mat4d invTransMat = selectedEntity.getInvTransMatrix(); Vec3d entSpaceCurrent = new Vec3d(); // entSpacePoint is the current point in model space entSpaceCurrent.multAndTrans3(invTransMat, currentPoint); Vec3d entSpaceLast = new Vec3d(); // entSpaceLast is the last point in model space entSpaceLast.multAndTrans3(invTransMat, lastPoint); Vec3d entSpaceDelta = new Vec3d(); entSpaceDelta.sub3(entSpaceCurrent, entSpaceLast); Vec3d pos = selectedEntity.getGlobalPosition(); Vec3d scale = selectedEntity.getSize(); Vec4d fixedPoint = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d); if (dragHandleID == RESIZE_POSX_PICK_ID) { //scale.x = 2*entSpaceCurrent.x() * size.x(); scale.x += entSpaceDelta.x * size.x; fixedPoint = new Vec4d(-0.5, 0.0, 0.0, 1.0d); } if (dragHandleID == RESIZE_POSY_PICK_ID) { scale.y += entSpaceDelta.y * size.y; fixedPoint = new Vec4d( 0.0, -0.5, 0.0, 1.0d); } if (dragHandleID == RESIZE_NEGX_PICK_ID) { scale.x -= entSpaceDelta.x * size.x; fixedPoint = new Vec4d( 0.5, 0.0, 0.0, 1.0d); } if (dragHandleID == RESIZE_NEGY_PICK_ID) { scale.y -= entSpaceDelta.y * size.y; fixedPoint = new Vec4d( 0.0, 0.5, 0.0, 1.0d); } if (dragHandleID == RESIZE_PXPY_PICK_ID) { scale.x += entSpaceDelta.x * size.x; scale.y += entSpaceDelta.y * size.y; fixedPoint = new Vec4d(-0.5, -0.5, 0.0, 1.0d); } if (dragHandleID == RESIZE_PXNY_PICK_ID) { scale.x += entSpaceDelta.x * size.x; scale.y -= entSpaceDelta.y * size.y; fixedPoint = new Vec4d(-0.5, 0.5, 0.0, 1.0d); } if (dragHandleID == RESIZE_NXPY_PICK_ID) { scale.x -= entSpaceDelta.x * size.x; scale.y += entSpaceDelta.y * size.y; fixedPoint = new Vec4d( 0.5, -0.5, 0.0, 1.0d); } if (dragHandleID == RESIZE_NXNY_PICK_ID) { scale.x -= entSpaceDelta.x * size.x; scale.y -= entSpaceDelta.y * size.y; fixedPoint = new Vec4d( 0.5, 0.5, 0.0, 1.0d); } // Handle the case where the scale is pulled through itself. Fix the scale, // and swap the currently selected handle if (scale.x <= 0.00005) { scale.x = 0.0001; if (dragHandleID == RESIZE_POSX_PICK_ID) { dragHandleID = RESIZE_NEGX_PICK_ID; } else if (dragHandleID == RESIZE_NEGX_PICK_ID) { dragHandleID = RESIZE_POSX_PICK_ID; } else if (dragHandleID == RESIZE_PXPY_PICK_ID) { dragHandleID = RESIZE_NXPY_PICK_ID; } else if (dragHandleID == RESIZE_PXNY_PICK_ID) { dragHandleID = RESIZE_NXNY_PICK_ID; } else if (dragHandleID == RESIZE_NXPY_PICK_ID) { dragHandleID = RESIZE_PXPY_PICK_ID; } else if (dragHandleID == RESIZE_NXNY_PICK_ID) { dragHandleID = RESIZE_PXNY_PICK_ID; } } if (scale.y <= 0.00005) { scale.y = 0.0001; if (dragHandleID == RESIZE_POSY_PICK_ID) { dragHandleID = RESIZE_NEGY_PICK_ID; } else if (dragHandleID == RESIZE_NEGY_PICK_ID) { dragHandleID = RESIZE_POSY_PICK_ID; } else if (dragHandleID == RESIZE_PXPY_PICK_ID) { dragHandleID = RESIZE_PXNY_PICK_ID; } else if (dragHandleID == RESIZE_PXNY_PICK_ID) { dragHandleID = RESIZE_PXPY_PICK_ID; } else if (dragHandleID == RESIZE_NXPY_PICK_ID) { dragHandleID = RESIZE_NXNY_PICK_ID; } else if (dragHandleID == RESIZE_NXNY_PICK_ID) { dragHandleID = RESIZE_NXPY_PICK_ID; } } Vec4d oldFixed = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d); oldFixed.mult4(transMat, fixedPoint); selectedEntity.setSize(scale); transMat = selectedEntity.getTransMatrix(); // Get the new matrix Vec4d newFixed = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d); newFixed.mult4(transMat, fixedPoint); Vec4d posAdjust = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d); posAdjust.sub3(oldFixed, newFixed); pos.add3(posAdjust); selectedEntity.setInputForGlobalPosition(pos); KeywordIndex kw = InputAgent.formatPointInputs("Size", selectedEntity.getSize(), "m"); InputAgent.apply(selectedEntity, kw); return true; } private boolean handleRotate(Ray currentRay, Ray lastRay, double currentDist, double lastDist) { Mat4d transMat = selectedEntity.getTransMatrix(); // The points where the previous pick ended and current position. Collision is with the entity's XY plane Vec3d currentPoint = currentRay.getPointAtDist(currentDist); Vec3d lastPoint = lastRay.getPointAtDist(lastDist); Vec3d align = selectedEntity.getAlignment(); Vec4d rotateCenter = new Vec4d(align.x, align.y, align.z, 1.0d); rotateCenter.mult4(transMat, rotateCenter); Vec4d a = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d); a.sub3(lastPoint, rotateCenter); Vec4d b = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d); b.sub3(currentPoint, rotateCenter); Vec4d aCrossB = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d); aCrossB.cross3(a, b); double sinTheta = aCrossB.z / a.mag3() / b.mag3(); double theta = Math.asin(sinTheta); Vec3d orient = selectedEntity.getOrientation(); orient.z += theta; KeywordIndex kw = InputAgent.formatPointInputs("Orientation", orient, "rad"); InputAgent.apply(selectedEntity, kw); return true; } private boolean handleLineMove(Ray currentRay, Ray lastRay, double currentDist, double lastDist, boolean shift) { // The points where the previous pick ended and current position. Collision is with the entity's XY plane Vec3d currentPoint = currentRay.getPointAtDist(currentDist); Vec3d lastPoint = lastRay.getPointAtDist(lastDist); ArrayList<Vec3d> screenPoints = selectedEntity.getPoints(); if (screenPoints == null || screenPoints.isEmpty()) return true; Vec3d delta = new Vec3d(); if (shift) { Vec4d medPoint = RenderUtils.getGeometricMedian(screenPoints); delta.z = RenderUtils.getZDiff(medPoint, currentRay, lastRay); } else { delta.sub3(currentPoint, lastPoint); if (selectedEntity.getCurrentRegion() != null) { Transform invTrans = selectedEntity.getCurrentRegion().getInverseRegionTransForVectors(); invTrans.multAndTrans(delta, delta); } } // Set the new position for the line InputAgent.apply(selectedEntity, InputAgent.formatPointsInputs("Points", screenPoints, delta)); // Set the position of the entity to the coordinates of the first node InputAgent.apply(selectedEntity, InputAgent.formatPointInputs("Position", screenPoints.get(0), "m")); return true; } private boolean handleLineNodeMove(Ray currentRay, Ray firstRay, double currentDist, double firstDist, boolean shift) { int nodeIndex = (int)(-1*(dragHandleID - LINENODE_PICK_ID)); ArrayList<Vec3d> screenPoints = selectedEntity.getPoints(); if (screenPoints == null || nodeIndex >= screenPoints.size()) return false; // Global node position at the start of the move Vec3d point = selectedEntity.getGlobalPosition(dragEntityPoints.get(nodeIndex)); // Global node position at the end of the move Vec3d diff = new Vec3d(); if (shift) { diff.z = RenderUtils.getZDiff(point, currentRay, firstRay); } else { Plane pointPlane = new Plane(null, point.z); diff = RenderUtils.getPlaneCollisionDiff(pointPlane, currentRay, firstRay); diff.z = 0.0d; } point.add3(diff); // Align the node to snap grid if (Simulation.isSnapToGrid()) point = Simulation.getSnapGridPosition(point, selectedEntity.getGlobalPosition(screenPoints.get(nodeIndex))); // Set the new position for the node screenPoints.get(nodeIndex).set3(selectedEntity.getLocalPosition(point)); InputAgent.apply(selectedEntity, InputAgent.formatPointsInputs("Points", screenPoints, new Vec3d())); // Set the position of the entity to the coordinates of the first node if (nodeIndex == 0) InputAgent.apply(selectedEntity, InputAgent.formatPointInputs("Position", screenPoints.get(0), "m")); return true; } private void splitLineEntity(int windowID, int x, int y) { Ray currentRay = getRayForMouse(windowID, x, y); Mat4d rayMatrix = MathUtils.RaySpace(currentRay); ArrayList<Vec3d> points = selectedEntity.getPoints(); if (points == null || points.isEmpty()) return; Transform trans = null; if (selectedEntity.getCurrentRegion() != null || selectedEntity.getRelativeEntity() != null) trans = selectedEntity.getGlobalPositionTransform(); ArrayList<Vec3d> globalPoints = new ArrayList<>(points); if (trans != null) globalPoints = (ArrayList<Vec3d>) RenderUtils.transformPointsWithTrans(trans.getMat4dRef(), globalPoints); int splitInd = 0; Vec4d nearPoint = null; // Find a line segment we are near for (;splitInd < points.size() - 1; ++splitInd) { Vec4d a = new Vec4d(globalPoints.get(splitInd ).x, globalPoints.get(splitInd ).y, globalPoints.get(splitInd ).z, 1.0d); Vec4d b = new Vec4d(globalPoints.get(splitInd+1).x, globalPoints.get(splitInd+1).y, globalPoints.get(splitInd+1).z, 1.0d); nearPoint = RenderUtils.rayClosePoint(rayMatrix, a, b); double rayAngle = RenderUtils.angleToRay(rayMatrix, nearPoint); if (rayAngle > 0 && rayAngle < 0.01309) { // 0.75 degrees in radians break; } } if (splitInd == points.size() - 1) { // No appropriate point was found return; } if (trans != null) { Transform invTrans = new Transform(); trans.inverse(invTrans); invTrans.multAndTrans(nearPoint, nearPoint); } // If we are here, we have a segment to split, at index i ArrayList<Vec3d> splitPoints = new ArrayList<>(); for(int i = 0; i <= splitInd; ++i) { splitPoints.add(points.get(i)); } splitPoints.add(nearPoint); for (int i = splitInd+1; i < points.size(); ++i) { splitPoints.add(points.get(i)); } KeywordIndex kw = InputAgent.formatPointsInputs("Points", splitPoints, new Vec3d()); InputAgent.apply(selectedEntity, kw); } private void removeLineNode(int windowID, int x, int y) { Ray currentRay = getRayForMouse(windowID, x, y); Mat4d rayMatrix = MathUtils.RaySpace(currentRay); ArrayList<Vec3d> points = selectedEntity.getPoints(); if (points == null || points.size() <= 2) return; ArrayList<Vec3d> globalPoints = new ArrayList<>(points); Transform trans = null; if (selectedEntity.getCurrentRegion() != null || selectedEntity.getRelativeEntity() != null) { trans = selectedEntity.getGlobalPositionTransform(); globalPoints = (ArrayList<Vec3d>) RenderUtils.transformPointsWithTrans(trans.getMat4dRef(), globalPoints); } int removeInd = 0; // Find a line segment we are near for ( ;removeInd < points.size(); ++removeInd) { Vec4d p = new Vec4d(globalPoints.get(removeInd).x, globalPoints.get(removeInd).y, globalPoints.get(removeInd).z, 1.0d); double rayAngle = RenderUtils.angleToRay(rayMatrix, p); if (rayAngle > 0 && rayAngle < 0.01309) { // 0.75 degrees in radians break; } if (removeInd == points.size()) { // No appropriate point was found return; } } ArrayList<Vec3d> splitPoints = new ArrayList<>(); for(int i = 0; i < points.size(); ++i) { if (i == removeInd) continue; splitPoints.add(points.get(i)); } KeywordIndex kw = InputAgent.formatPointsInputs("Points", splitPoints, new Vec3d()); InputAgent.apply(selectedEntity, kw); } private boolean isMouseHandleID(long id) { return (id < 0); // For now all negative IDs are mouse handles, this may change } public boolean handleMouseButton(int windowID, int x, int y, int button, boolean isDown, int modifiers) { if (button != 1) { return false; } if (!isDown) { // Click released dragHandleID = 0; return true; // handled } boolean controlDown = (modifiers & WindowInteractionListener.MOD_CTRL) != 0; boolean altDown = (modifiers & WindowInteractionListener.MOD_ALT) != 0; if (controlDown && altDown) { // Check if we can split a line segment if (selectedEntity != null) { if ((modifiers & WindowInteractionListener.MOD_SHIFT) != 0) { removeLineNode(windowID, x, y); } else { splitLineEntity(windowID, x, y); } return true; } } if (!controlDown) { return false; } Ray pickRay = getRayForMouse(windowID, x, y); View view = windowToViewMap.get(windowID); if (view == null) { return false; } List<PickData> picks = pickForRay(pickRay, view.getID(), true); Collections.sort(picks, new HandleSorter()); if (picks.size() == 0) { return false; } double mouseHandleDist = Double.POSITIVE_INFINITY; double entityDist = Double.POSITIVE_INFINITY; // See if we are hovering over any interaction handles for (PickData pd : picks) { if (isMouseHandleID(pd.id) && mouseHandleDist == Double.POSITIVE_INFINITY) { // this is a mouse handle, remember the handle for future drag events dragHandleID = pd.id; mouseHandleDist = pd.dist; } if (selectedEntity != null && pd.id == selectedEntity.getEntityNumber()) { // We clicked on the selected entity entityDist = pd.dist; } } // The following logical condition effectively checks if we hit the entity first, and did not select // any mouse handle other than the move handle if (entityDist != Double.POSITIVE_INFINITY && entityDist < mouseHandleDist && (dragHandleID == 0 || dragHandleID == MOVE_PICK_ID)) { // Use the entity collision point for dragging instead of the handle collision point dragEntityPosition = selectedEntity.getGlobalPosition(); dragEntityPoints = selectedEntity.getPoints(); dragCollisionPoint = pickRay.getPointAtDist(entityDist); dragHandleID = MOVE_PICK_ID; return true; } if (mouseHandleDist != Double.POSITIVE_INFINITY) { // We hit a mouse handle dragEntityPosition = selectedEntity.getGlobalPosition(); dragEntityPoints = selectedEntity.getPoints(); dragCollisionPoint = pickRay.getPointAtDist(mouseHandleDist); return true; } return false; } public void clearSelection() { selectedEntity = null; } public void hideExistingPopups() { synchronized (popupLock) { if (lastPopup == null) { return; } lastPopup.setVisible(false); lastPopup = null; } } public boolean isDragAndDropping() { // This is such a brutal hack to work around newt's lack of drag and drop support // Claim we are still dragging for up to 10ms after the last drop failed... long currTime = System.nanoTime(); return dndObjectType != null && ((currTime - dndDropTime) < 100000000); // Did the last 'drop' happen less than 100 ms ago? } public void startDragAndDrop(ObjectType ot) { dndObjectType = ot; } public void mouseMoved(int windowID, int x, int y) { Ray currentRay = getRayForMouse(windowID, x, y); double dist = RenderManager.XY_PLANE.collisionDist(currentRay); if (dist == Double.POSITIVE_INFINITY) { // I dunno... return; } Vec3d xyPlanePoint = currentRay.getPointAtDist(dist); GUIFrame.showLocatorPosition(xyPlanePoint); } public void createDNDObject(int windowID, int x, int y) { Ray currentRay = getRayForMouse(windowID, x, y); double dist = RenderManager.XY_PLANE.collisionDist(currentRay); if (dist == Double.POSITIVE_INFINITY) { // Unfortunate... return; } Vec3d creationPoint = currentRay.getPointAtDist(dist); // Create a new instance Class<? extends Entity> proto = dndObjectType.getJavaClass(); String name = proto.getSimpleName(); Entity ent = InputAgent.defineEntityWithUniqueName(proto, name, "", true); // Set input values for a dragged and dropped entity ent.setInputsForDragAndDrop(); // We are no longer drag-and-dropping dndObjectType = null; FrameBox.setSelectedEntity(ent, false); if (ent instanceof DisplayEntity) { try { ((DisplayEntity)ent).dragged(creationPoint); } catch (InputErrorException e) {} this.focusWindow(windowID); } } @Override public void dragDropEnd(DragSourceDropEvent arg0) { // Clear the dragging flag dndDropTime = System.nanoTime(); } @Override public void dragEnter(DragSourceDragEvent arg0) {} @Override public void dragExit(DragSourceEvent arg0) {} @Override public void dragOver(DragSourceDragEvent arg0) {} @Override public void dropActionChanged(DragSourceDragEvent arg0) {} public AABB getMeshBounds(MeshProtoKey key, boolean block) { if (block || MeshDataCache.isMeshLoaded(key)) { return MeshDataCache.getMeshData(key).getDefaultBounds(); } // The mesh is not loaded and we are non-blocking, so trigger a mesh load and return MeshDataCache.loadMesh(key); return null; } public ArrayList<Action.Description> getMeshActions(MeshProtoKey key, boolean block) { if (block || MeshDataCache.isMeshLoaded(key)) { return MeshDataCache.getMeshData(key).getActionDescriptions(); } // The mesh is not loaded and we are non-blocking, so trigger a mesh load and return MeshDataCache.loadMesh(key); return null; } public Vec2d getImageDims(URI imageURI) { if (imageURI == null) return null; Dimension dim = TexCache.getImageDimension(imageURI); if (dim == null) return null; return new Vec2d(dim.getWidth(), dim.getHeight()); } /** * Set the current windows camera to an isometric view */ public void setIsometricView() { CameraControl control = windowControls.get(activeWindowID); if (control == null) return; // The constant is acos(1/sqrt(3)) control.setRotationAngles(0.955316, Math.PI/4); } /** * Set the current windows camera to an XY plane view */ public void setXYPlaneView() { CameraControl control = windowControls.get(activeWindowID); if (control == null) return; // Do not look straight down the Z axis as that is actually a degenerate state control.setRotationAngles(0.0000001, 0.0); } public View getActiveView() { return windowToViewMap.get(activeWindowID); } public ArrayList<Integer> getOpenWindowIDs() { return renderer.getOpenWindowIDs(); } public String getWindowName(int windowID) { return renderer.getWindowName(windowID); } public void focusWindow(int windowID) { renderer.focusWindow(windowID); } public static Frame getOpenWindowForView(View view) { if (!isGood()) return null; RenderManager rman = RenderManager.inst(); for (Map.Entry<Integer, View> entry : rman.windowToViewMap.entrySet()) { if (entry.getValue() == view) return rman.renderer.getAWTFrame(entry.getKey()); } return null; } /** * Can this hardware perform off screen rendering. Note: this method returning true is necessary, but not sufficient to * support off screen rendering. * @return */ public static boolean canRenderOffscreen() { if (!isGood()) return false; RenderManager rman = RenderManager.inst(); return rman.renderer.isGL3Supported(); } /** * Queue up an off screen rendering, this simply passes the call directly to the renderer * @param scene * @param camInfo * @param width * @param height * @return */ public Future<BufferedImage> renderOffscreen(ArrayList<RenderProxy> scene, CameraInfo camInfo, int viewID, int width, int height, Runnable runWhenDone) { return renderer.renderOffscreen(scene, viewID, camInfo, width, height, runWhenDone, null); } /** * Return a FutureImage of the equivalent screen renderer from the given position looking at the given center * @param cameraPos * @param viewCenter * @param width - width of returned image * @param height - height of returned image * @param target - optional target to prevent re-allocating GPU resources * @return */ public Future<BufferedImage> renderScreenShot(View view, int width, int height, OffscreenTarget target) { Vec3d cameraPos = view.getGlobalPosition(); Vec3d cameraCenter = view.getGlobalCenter(); Vec3d viewDiff = new Vec3d(); viewDiff.sub3(cameraPos, cameraCenter); double rotZ = Math.atan2(viewDiff.x, -viewDiff.y); double xyDist = Math.hypot(viewDiff.x, viewDiff.y); double rotX = Math.atan2(xyDist, viewDiff.z); if (Math.abs(rotX) < 0.005) { rotZ = 0; // Don't rotate if we are looking straight up or down } Quaternion rot = new Quaternion(); rot.setRotZAxis(rotZ); Quaternion tmp = new Quaternion(); tmp.setRotXAxis(rotX); rot.mult(rot, tmp); Transform trans = new Transform(cameraPos, rot, 1); CameraInfo camInfo = new CameraInfo(Math.PI/3, trans, view.getSkyboxTexture()); return renderer.renderOffscreen(null, view.getID(), camInfo, width, height, null, target); } public Future<BufferedImage> getPreviewForDisplayModel(DisplayModel dm, Runnable notifier) { return previewCache.getPreview(dm, notifier); } public OffscreenTarget createOffscreenTarget(int width, int height) { return renderer.createOffscreenTarget(width, height); } public void freeOffscreenTarget(OffscreenTarget target) { renderer.freeOffscreenTarget(target); } private void takeScreenShot() { if (recorder != null) recorder.sample(); synchronized(screenshot) { screenshot.set(false); recorder = null; screenshot.notifyAll(); } } public void blockOnScreenShot(VideoRecorder recorder) { assert(!screenshot.get()); synchronized (screenshot) { screenshot.set(true); this.recorder = recorder; GUIFrame.updateUI(); while (screenshot.get()) { try { screenshot.wait(); } catch (InterruptedException ex) {} } } } public void shutdown() { finished.set(true); if (renderer != null) { renderer.shutdown(); } } public void handleKeyPressed(int keyCode, char keyChar, boolean shift, boolean control, boolean alt) { selectedEntity.handleKeyPressed(keyCode, keyChar, shift, control, alt); } public void handleKeyReleased(int keyCode, char keyChar, boolean shift, boolean control, boolean alt) { selectedEntity.handleKeyReleased(keyCode, keyChar, shift, control, alt); } public void setShowLinks(boolean bShow) { showLinks.set(bShow); } public void setCreateLinks(boolean bCreate) { createLinks.set(bCreate); } private void addLink(LinkDisplayable sourceLD, LinkDisplayable destLD, ArrayList<RenderProxy> scene) { Vec3d source = sourceLD.getSourcePoint(); Vec3d sink = destLD.getSinkPoint(); double sourceRadius = sourceLD.getRadius(); double sinkRadius = destLD.getRadius(); Vec3d arrowDir = new Vec3d(); arrowDir.sub3(sink, source); if (arrowDir.mag3() < (sourceRadius + sinkRadius)) { // The two objects are too close return; } // Scale back the arrows to the 'radius' provided double linkSize = arrowDir.mag3() - sourceRadius - sinkRadius; arrowDir.normalize3(); Vec3d temp = new Vec3d(); temp.scale3(sourceRadius, arrowDir); source.add3(temp); temp.scale3(sinkRadius, arrowDir); sink.sub3(temp); double arrowHeadSize = Math.min(linkSize*0.3, linkArrowSize); temp.scale3(arrowHeadSize, arrowDir); Vec3d arrowMidPoint = new Vec3d(); arrowMidPoint.sub3(sink, temp); Vec3d arrowHeadDir = new Vec3d(); arrowHeadDir.cross3(arrowDir, new Vec3d(0,0,1)); if (arrowHeadDir.mag3() == 0.0) { arrowHeadDir.set3(1, 0, 0); } else { arrowHeadDir.normalize3(); } arrowHeadDir.scale3(arrowHeadSize*0.5); Vec3d arrowPoint0 = new Vec3d(); Vec3d arrowPoint1 = new Vec3d(); arrowPoint0.sub3(arrowMidPoint, arrowHeadDir); arrowPoint1.add3(arrowMidPoint, arrowHeadDir); Vec4d source4 = new Vec4d(source.x, source.y, source.z, 1); Vec4d sink4 = new Vec4d(sink.x, sink.y, sink.z, 1); Vec4d ap0 = new Vec4d(arrowPoint0.x, arrowPoint0.y, arrowPoint0.z, 1); Vec4d ap1 = new Vec4d(arrowPoint1.x, arrowPoint1.y, arrowPoint1.z, 1); ArrayList<Vec4d> segments = new ArrayList<>(6); segments.add(source4); segments.add(sink4); // Now add the 'head' of the arrow segments.add(sink4); segments.add(ap0); segments.add(sink4); segments.add(ap1); scene.add(new LineProxy(segments, ColourInput.BLACK, 1, DisplayModel.ALWAYS, 0)); } private void addLinkDisplays(ArrayList<RenderProxy> scene) { ArrayList<? extends Entity> allEnts = Entity.getAll(); for (int i = 0; i < allEnts.size(); i++) { try { Entity e = allEnts.get(i); if (!(e instanceof LinkDisplayable)) continue; LinkDisplayable ld = (LinkDisplayable)e; ArrayList<Entity> dests = ld.getDestinationEntities(); // Now scan the destinations for (Entity dest : dests) { if (!(dest instanceof LinkDisplayable)) continue; LinkDisplayable destLD = (LinkDisplayable)dest; addLink(ld, destLD, scene); } ArrayList<Entity> sources = ld.getSourceEntities(); // Now scan the destinations for (Entity source : sources) { if (!(source instanceof LinkDisplayable)) continue; LinkDisplayable sourceLD = (LinkDisplayable)source; addLink(sourceLD, ld, scene); } } catch (Throwable t) { // Log the exception in the exception list logException(t); } } } public static void setDebugInfo(boolean showDebug) { if (!isGood()) { return; } s_instance.renderer.setDebugInfo(showDebug); GUIFrame.updateUI(); } }