Friday, May 22, 2020

Selection Algorithm, Unit Testing, Fatigue, Next Steps

Writing tools is deceptively difficult. A few mouse listeners, getting the right data structures down for the stuff the tools will edit, maybe some file IO utilities... no biggie... right? I can tell you the most difficult, tedious aspect so far has been getting the handling of mouse gestures solid.

What mouse gestures do I even want to support, and for what types of editing? What's being edited? These may seem obvious when thinking about any kind of geometry editor: Create meshes by drawing lines and triangles and then edit those meshes by dragging the individual vertices, edges, and entire triangles around. Shift+click should probably be a thing so multiple things can be edited at once, delete should probably eventually delete stuff, etc. There are definitely other actions that will need to be supported--especially so when taking into account all of the non-mesh things this system needs to support.

So, it's kind of clear what the editor should be like, but it's also very NOT clear. The truth is, a lot of stuff is still very fuzzy in my mind, including what functionality I even want for the first stages of actually building a game. A roadmap would help, but maintaining that is almost a full-time job on its own, and I'm wearing myself down a bit. I have my triangle though, so I'll start from there.

JavaScript gives you a number of very useful event handlers out of the box. Assuming a Chrome-based platform, they all even "just work." Well, kind of. The mouse drag events seem to only fire for working with HTML elements--not within elements like our WebGL canvas. That means drag gestures need to be handled manually, which is where a lot of the difficulty comes in. To edit my triangle, what structures and gestures exactly do I need to handle? Well, I've chosen the phrase Selection Handle to be the thing the mouse can interact with. (Selection) Handles are used to normalize what the mouse interacts with to simplify the input and mouse systems, leaving the specifics of how something is edited to the specific types of handles.

Given the dissemination of the correct list of handles from the active scene (only for the triangle so far), let's run through some scenarios:

  1. Handles exist of a user-customizable size. Handles on the screen should dynamically change in size as the user adjusts it.
  2. Non-selected or highlighted handles are dynamically rendered with a user-customizable color.
  3. Mouse hover over handles alters their render color to another user-customizable (highlight) color.
  4. Clicking a handle selects it.
  5. Shift-clicking a handle toggles it as selected or unselected.
  6. Clicking on no handle deselects all handles.
  7. Shift+clicking+dragging creates a selection area such that:
    1. The area is rendered in an intuitive way
    2. All handles within the area when the click is released become selected, regardless if they were previously selected or not.
  8. Click+dragging an unselected handle selects it and then drags it along with the mouse.
  9. Shift+clicking+dragging any handle selects and then drags all selected handles along with the mouse.
There's also another case for when there are multiple potentially-overlapping handles: Clicking in one location multiple times cycles through the handles that could be selected. Additionally, all selection and drag operations need to be undoable and redoable.

You can see that dissecting what seems like common, every-day mouse gestures yields a complex set of states and conditions that need to be dealt with. (I'm actually including the bulk of this driver code down below.) This functionality needs to be at least 99% defect-free, as it's essentially part of the kernel of any mouse-centric application. This makes it a great candidate for unit tests, and luckily mouse input is just data and there are tools for writing these unit tests (also included below).

The initial version of the selection algorithm was near-sighted and tangled in with the color-changing logic. It instantly fell apart when trying to implement the dragging functionality. It was necessary though, as it helped me map out the scenarios I listed above.

The second version is ultimately what I'm sticking with, though there are things about it I don't like. Both versions took me about a week to implement, and then the unit tests took me another week and a half or so. This isn't fun work either, and I feel like I have a type of PTSD from all of the system refactoring needed to get everything running smoothly. That being said, once all refactoring was done, the unit tests held up and so did the functionality. I started with vertex selection handles, and minus some minor selection bookkeeping adjustments, other types of selection handles fell right into place (triangle and edge).

All of this has left me exhausted. Exhausted and feeling a little hopeless. Is what I'm doing dumb or at all worth it? I think it will be in the end, but I think there's a lesson to be learned about balancing the "fun" part of a technical hobby with the "legitimate work" part; both parts are necessary, and neglecting one for too long for the other's sake is bound to intensify the eventual fatigue.

What's next? There are a lot of immediate next steps I could take:
  1. Grid
  2. Zooming
  3. Improving the save system and "catching up" the functionality that isn't currently persisted/restored
  4. Begin with brushes
  5. Texture support
Addressing technical debt is always there. I'm one person, so the code is far from perfect--even for how I want it to be. I have to choose something with more bang for the buck while I refresh though. I'd like to choose either texture support or brush functionality, but zooming is also more of a "kernelly" thing that I feel needs to be established before I implement more advanced functionality. However, while looking into zooming, I realized a grid would be helpful for debugging the zoom (math)--and thus I'm going with the grid. The grid will be good, since it's forcing me to address other areas of technical debt in healthier, less grindy ways.

SelectionManager.ts
import { Layer } from "../engine/world/Layer";
import { SelectionModel, SelectionHandle, TertiarySelectionCategory, PrimarySelectionCategory, SecondarySelectionCategory } from "../engine/model/select/Selectable";
import { Engine } from "../engine/Engine";
import { World } from "../engine/world/World";
import { vec4, vec2 } from "gl-matrix";
import { Model } from "../engine/model/Model";
import { SelectionUtilities } from "../engine/model/select/SelectionUtilities";
import { DEFAULT_SHADER_NAME } from "../TestScene";
import { Pair } from "../../util/Pair";
import { Camera } from "../engine/render/Camera";
import { BoundingShape } from "../engine/core/BoundingShape";
import { Mesh } from "../engine/model/Mesh";
import { WireframeRenderMode, ShadingRenderMode } from "../engine/render/RenderMode";
import { Vertex } from "../engine/model/Vertex";
import { IDMap } from "../../util/IDMap";
import { ID } from "../engine/core/ID";
export const SELECTION_HANDLE_LAYER_ORDER: number = 10000;
export interface ClickState
{
startMousePosition: vec2;
wasDragging: boolean;
}
export class SelectionResult
{
public readonly handlesSelected: SelectionHandle[] = [];
public readonly handlesDeselected: SelectionHandle[] = [];
public readonly handlesMoved: SelectionHandle[] = [];
public readonly netAmountMoved: vec2 = vec2.create();
public readonly dragInitialMouseDownPosition: vec2 = vec2.create();
}
export class SelectionManager
{
private readonly selectionHandleLayer: Layer = new Layer();
//private readonly layersForEditingByName: Map<string, Layer> = new Map();
private readonly selectionModel: SelectionModel = new SelectionModel();
private readonly world: World;
private selectionHandlesAndModels: Pair<SelectionHandle, Model>[] = [];
private selectedHandlePairIndices: Set<number> = new Set();
private clickAndDragState: ClickState = null;
private handleIndices: number[];
private indexCycleIndex: number;
private readonly selectionResult: SelectionResult = new SelectionResult();
private handleSize: number;
private normalHandleColor: vec4;
private selectedHandleColor: vec4;
private readonly selectingArea: Pair<boolean, vec2> = { first: false, second: [ 0, 0 ] };
private readonly areaBoundingShape: BoundingShape = new BoundingShape();
private readonly areaModel: Model = new Model(
DEFAULT_SHADER_NAME,
new Mesh(),
{
color: /* TODO */ [ 0.3, 0.7, 0.3, 0.3 ],
paralaxFactor: 1.0,
visible: false,
wireframeRenderMode: WireframeRenderMode.NEVER,
shadingRenderMode: ShadingRenderMode.FLAT_FULL_BRIGHT
});
public constructor(
private readonly engine: Engine,
private readonly selectionHandleShaderName: string)
{
this.world = this.engine.getWorld();
const firstVertex: Vertex = this.areaModel.mesh.addVertex([ 0, 0 ]);
const secondVertex: Vertex = this.areaModel.mesh.addVertex([ 0, 0 ]);
const thirdVertex: Vertex = this.areaModel.mesh.addVertex([ 0, 0 ]);
const fourthVertex: Vertex = this.areaModel.mesh.addVertex([ 0, 0 ]);
this.areaModel.mesh.addTriangle(firstVertex.getID(), secondVertex.getID(), thirdVertex.getID());
this.areaModel.mesh.addTriangle(thirdVertex.getID(), fourthVertex.getID(), firstVertex.getID());
}
public getSelectionHandleLayer(): Layer
{
return this.selectionHandleLayer;
}
public setHandleSize(handleSize: number): void
{
this.handleSize = handleSize;
// TODO: Just update handle models, not rebuild the entire thing. Oh well this.updateHandles(false);
}
public setNormalHandleColor(normalHandleColor: vec4): void
{
this.normalHandleColor = normalHandleColor;
this.updateHandles(false);
}
public setSelectedHandleColor(selectedHandleColor: vec4): void
{
this.selectedHandleColor = selectedHandleColor;
this.updateHandles(false);
}
public setPrimaryCategory(primaryCategory: PrimarySelectionCategory): void
{
this.selectionModel.setPrimaryCategory(primaryCategory);
this.updateHandles(true);
}
public setSecondaryCategory(secondaryCategory: SecondarySelectionCategory): void
{
this.selectionModel.setSecondaryCategory(secondaryCategory);
this.updateHandles(true);
}
public setTertiaryCategory(tertiaryCategory: TertiarySelectionCategory): void
{
this.selectionModel.setTertiaryCategory(tertiaryCategory);
this.updateHandles(true);
}
/*public addLayerForEditing(layer: Layer): void { this.layersForEditingByName.set(layer.getName(), layer);
this.updateHandles(); }*/
/*public removeLayerForEditing(layer: Layer): void { const layerName: string = layer.getName(); if (this.layersForEditingByName.has(layerName)) { this.layersForEditingByName.delete(layerName);
this.updateHandles(); } }*/
public handleSelectionAndDragging(
currentPosition: vec2,
primaryButtonDown: boolean,
shiftKeyPressed: boolean,
engineDebug: boolean): SelectionResult
{
this.selectionResult.handlesSelected.splice(0, this.selectionResult.handlesSelected.length);
this.selectionResult.handlesDeselected.splice(0, this.selectionResult.handlesDeselected.length);
this.selectionResult.handlesMoved.splice(0, this.selectionResult.handlesMoved.length);
if (this.clickAndDragState != null)
{
this.handleUpdateInClickOrDrag(currentPosition, primaryButtonDown, shiftKeyPressed, engineDebug);
}
else if (this.selectingArea.first)
{
this.handleSelectingArea(currentPosition, primaryButtonDown, shiftKeyPressed, engineDebug);
}
else {
this.handleNoState(currentPosition, primaryButtonDown, shiftKeyPressed, engineDebug);
}
this.updateHandles(false);
this.updateHandleColors();
return this.selectionResult;
}
private handleUpdateInClickOrDrag(
currentPosition: vec2,
primaryButtonDown: boolean,
shiftKeyPressed: boolean,
engineDebug: boolean): void
{
if (engineDebug)console.log(`clickAndDragState not null`);
// How far did the mouse move? const mouseDelta: vec2 = vec2.subtract(vec2.create(), currentPosition, this.clickAndDragState.startMousePosition);
const pixelsMoved: number = (Math.abs(mouseDelta[0]) + Math.abs(mouseDelta[1]));
// Not too far, it's part of a click or the end of dragging. if (pixelsMoved == 0)
{
this.handleMouseDidNotMoveInClickOrDrag(currentPosition, primaryButtonDown, shiftKeyPressed, engineDebug);
}
// Too far, it's the start of dragging. else {
this.handleMouseMovedInClickOrDrag(currentPosition, primaryButtonDown, shiftKeyPressed, mouseDelta, pixelsMoved, engineDebug);
}
}
private handleSelectingArea(
currentPosition: vec2,
primaryButtonDown: boolean,
shiftKeyPressed: boolean,
engineDebug: boolean): void
{
if (engineDebug)console.log(`selectingArea`);
const selectedHandles: SelectionHandle[] = this.handleAreaSelection(currentPosition, primaryButtonDown, shiftKeyPressed);
this.selectionResult.handlesSelected.push(...selectedHandles);
}
private handleNoState(
currentPosition: vec2,
primaryButtonDown: boolean,
shiftKeyPressed: boolean,
engineDebug: boolean): void
{
const handleIndices: number[] = this.findSelectionHandleIndices(currentPosition, primaryButtonDown);
if (engineDebug)console.log(`no other state`);
if (primaryButtonDown)
{
if (engineDebug)console.log(` primary button down`);
if (handleIndices.length > 0)
{
if (engineDebug)console.log(` handleIndices under mouse ${handleIndices.length}`);
this.clickAndDragState = {
startMousePosition: vec2.clone(currentPosition),
wasDragging: false };
this.handleIndices = handleIndices;
this.indexCycleIndex = 0;
vec2.copy(this.selectionResult.dragInitialMouseDownPosition, currentPosition);
}
else {
if (engineDebug)console.log(` handleIndices is 0`);
// No handles under the mouse, but the Shift key being down triggers selecting an area if (shiftKeyPressed)
{
if (engineDebug)console.log(` shift key pressed`);
this.selectingArea.first = true;
vec2.copy(this.selectingArea.second, currentPosition);
this.updateAreaSelectionModel(this.selectingArea.second);
this.areaModel.renderProperties.visible = true;
}
else {
if (engineDebug)console.log(` shift key not pressed`);
const deselectedHandles: SelectionHandle[] = this.clearSelection(true);
this.selectionResult.handlesDeselected.push(...deselectedHandles);
}
}
}
}
private updateHandles(handlesChanged: boolean): void
{
this.selectionHandleLayer.clearModels();
if (handlesChanged)
{
for (let ii = 0; ii < this.selectionHandlesAndModels.length; ii++)
{
const handle: Pair<SelectionHandle, Model> = this.selectionHandlesAndModels[ii];
handle.first.setSelected(false);
handle.first.setHighlighted(false);
}
this.selectedHandlePairIndices.clear();
this.clickAndDragState = null;
this.handleIndices = null;
this.indexCycleIndex = 0;
this.selectionResult.handlesSelected.splice(0, this.selectionResult.handlesSelected.length);
this.selectionResult.handlesDeselected.splice(0, this.selectionResult.handlesDeselected.length);
this.selectionResult.handlesMoved.splice(0, this.selectionResult.handlesMoved.length);
vec2.zero(this.selectionResult.netAmountMoved);
}
//if (handles.length > 0) const handles: SelectionHandle[] = this.world.getSelectionHandles(this.selectionModel);
// TODO: Switching the set of handles causes issues with the selection pipeline. // All of the bookkeeping gets out of sync. // This is why the SelectionManager should not maintain the list of handles at all. this.selectionHandlesAndModels = SelectionUtilities.createModelFromHandles(
handles,
DEFAULT_SHADER_NAME,
this.handleSize,
this.normalHandleColor,
this.selectedHandleColor);
for (let ii = 0; ii < this.selectionHandlesAndModels.length; ii++)
{
const selectionHandleAndModel: Pair<SelectionHandle, Model> = this.selectionHandlesAndModels[ii];
this.selectionHandleLayer.addModel(selectionHandleAndModel.second);
}
this.selectionHandleLayer.addModel(this.areaModel);
}
private handleMouseDidNotMoveInClickOrDrag(
currentPosition: vec2,
primaryButtonDown: boolean,
shiftKeyPressed: boolean,
engineDebug: boolean): void
{
if (engineDebug)console.log(` pixelsMoved == 0`);
// Mouse button was and is still down? Report back about the handles that were moved. if (primaryButtonDown)
{
if (engineDebug)console.log(` primary button down`);
}
// Mouse button no longer down, this is a click else if (!this.clickAndDragState.wasDragging)
{
if (engineDebug)console.log(` primary button not down and was not just dragging`);
const handleIndex: number = this.handleIndices[this.indexCycleIndex];
if (shiftKeyPressed)
{
if (engineDebug)console.log(` shift key pressed (toggling selection on ${handleIndex} and ending mouse click event)`);
if (this.selectionHandlesAndModels[handleIndex].first.getIsSelected())
{
this.removeSelection(handleIndex);
this.selectionResult.handlesDeselected.push(this.selectionHandlesAndModels[handleIndex].first);
}
else {
this.addSelection(handleIndex);
this.selectionResult.handlesSelected.push(this.selectionHandlesAndModels[handleIndex].first);
}
this.clickAndDragState = null;
}
else {
if (engineDebug)console.log(` shift key not pressed (clearing selection, then selecting ${handleIndex})`);
const deselectedHandles: SelectionHandle[] = this.clearSelection(false);
const handle: Pair<SelectionHandle, Model> = this.selectionHandlesAndModels[handleIndex];
this.addSelection(handleIndex);
this.selectionResult.handlesDeselected.push(...deselectedHandles);
this.selectionResult.handlesSelected.push(handle.first);
}
// Cycle to the handle, in case the user is clicking in the same spot (to cycle the handle) this.indexCycleIndex = ((this.indexCycleIndex + 1) % this.handleIndices.length);
}
else {
this.addMovedHandlesToResult(currentPosition);
this.clickAndDragState = null;
}
}
private handleMouseMovedInClickOrDrag(
currentPosition: vec2,
primaryButtonDown: boolean,
shiftKeyPressed: boolean,
mouseDelta: vec2,
pixelsMoved: number,
engineDebug: boolean): void
{
if (engineDebug)console.log(` pixelsMoved > 0 (${pixelsMoved})`);
// Mouse button still down: This is a drag
if (primaryButtonDown)
{
if (engineDebug)console.log(` primary button down`);
if (!this.clickAndDragState.wasDragging)
{
// We know this is the start of dragging because wasDragging is false vec2.copy(this.selectionResult.dragInitialMouseDownPosition, this.clickAndDragState.startMousePosition);
}
// This ensures that the "mouse button up" event doesn't single-select the vertex that was used to drag. this.clickAndDragState.wasDragging = true;
// It's possible for nothing to be selected yet, so grab the first handle index and select it. // The handle index is guaranteed to be there because we wouldn't be in the "clickAndDragState" if there weren't handles under the mouse when setting it to not null. const handleIndex: number = this.handleIndices[0];
const handle: Pair<SelectionHandle, Model> = this.selectionHandlesAndModels[handleIndex];
if (!handle.first.getIsSelected())
{
if (!shiftKeyPressed)
{
const deselectedHandles: SelectionHandle[] = this.clearSelection(false);
this.selectionResult.handlesDeselected.push(...deselectedHandles);
}
this.addSelection(handleIndex);
this.selectionResult.handlesSelected.push(handle.first);
}
this.selectedHandlePairIndices.forEach(index => {
const handle: Pair<SelectionHandle, Model> = this.selectionHandlesAndModels[index];
handle.first.translated(mouseDelta);
});
this.clickAndDragState.startMousePosition = currentPosition;
}
// Mouse button not down anymore else {
if (engineDebug)console.log(` primary button not down`);
// This event is over. this.clickAndDragState = null;
// Report back about the handles that were moved. this.addMovedHandlesToResult(currentPosition);
}
}
private findSelectionHandleIndices(currentPosition: vec2, primaryButtonDown: boolean): number[]
{
let indices: number[] = [];
for (let ii = 0; ii < this.selectionHandlesAndModels.length; ii++)
{
const selectionHandleAndModel: Pair<SelectionHandle, Model> = this.selectionHandlesAndModels[ii];
if (selectionHandleAndModel.first.boundingShape.pointIsIn(currentPosition))
{
selectionHandleAndModel.first.setHighlighted(true);
indices.push(ii);
}
else {
selectionHandleAndModel.first.setHighlighted(false);
}
}
return indices;
}
private addMovedHandlesToResult(currentPosition: vec2): void
{
vec2.subtract(this.selectionResult.netAmountMoved, currentPosition, this.selectionResult.dragInitialMouseDownPosition);
if (this.clickAndDragState &&
((Math.abs(this.selectionResult.netAmountMoved[0]) > 0) || (Math.abs(this.selectionResult.netAmountMoved[1]) > 0)))
{
this.selectedHandlePairIndices.forEach(index => {
const handle: Pair<SelectionHandle, Model> = this.selectionHandlesAndModels[index];
this.selectionResult.handlesMoved.push(handle.first);
});
}
}
private handleAreaSelection(currentPosition: vec2, primaryButtonDown: boolean, shiftKeyPressed: boolean): SelectionHandle[]
{
this.updateAreaSelectionModel(currentPosition);
const selectedHandles: SelectionHandle[] = [];
if (primaryButtonDown && shiftKeyPressed)
{
for (let ii = 0; ii < this.selectionHandlesAndModels.length; ii++)
{
const selectionHandleAndModel: Pair<SelectionHandle, Model> = this.selectionHandlesAndModels[ii];
if (selectionHandleAndModel.first.boundingShape.boxesCollide(this.areaBoundingShape))
{
selectionHandleAndModel.second.renderProperties.color = this.selectedHandleColor;
selectionHandleAndModel.first.setHighlighted(true);
}
else {
if (!selectionHandleAndModel.first.getIsSelected())
{
selectionHandleAndModel.second.renderProperties.color = this.normalHandleColor;
}
selectionHandleAndModel.first.setHighlighted(false);
}
}
}
else {
this.selectingArea.first = false;
this.areaModel.renderProperties.visible = false;
for (let ii = 0; ii < this.selectionHandlesAndModels.length; ii++)
{
const selectionHandleAndModel: Pair<SelectionHandle, Model> = this.selectionHandlesAndModels[ii];
selectionHandleAndModel.first.setHighlighted(false);
if (!selectionHandleAndModel.first.getIsSelected())
{
selectionHandleAndModel.second.renderProperties.color = this.normalHandleColor;
}
if (selectionHandleAndModel.first.boundingShape.boxesCollide(this.areaBoundingShape))
{
this.addSelection(ii);
selectedHandles.push(selectionHandleAndModel.first);
}
}
}
return selectedHandles;
}
private updateAreaSelectionModel(currentPosition: vec2): void
{
this.areaBoundingShape.calculateFrom([ this.selectingArea.second, currentPosition ]);
const areaBoundingShapeLowerLeft: vec2 = this.areaBoundingShape.getBoxLowerLeft();
const areaBoundingShapeUpperRight: vec2 = this.areaBoundingShape.getBoxUpperRight();
const _areaModelVertexPositions: IDMap<vec2> = this.areaModel.mesh.getVertexPositions();
const areaModelVertexPositions: vec2[] = _areaModelVertexPositions.valuesAsArray();
const firstVertexPosition: vec2 = areaModelVertexPositions[0];
const secondVertexPosition: vec2 = areaModelVertexPositions[1];
const thirdVertexPosition: vec2 = areaModelVertexPositions[2];
const fourthVertexPosition: vec2 = areaModelVertexPositions[3];
firstVertexPosition[0] = areaBoundingShapeLowerLeft[0];
firstVertexPosition[1] = areaBoundingShapeLowerLeft[1];
secondVertexPosition[0] = areaBoundingShapeLowerLeft[0];
secondVertexPosition[1] = areaBoundingShapeUpperRight[1];
thirdVertexPosition[0] = areaBoundingShapeUpperRight[0];
thirdVertexPosition[1] = areaBoundingShapeUpperRight[1];
fourthVertexPosition[0] = areaBoundingShapeUpperRight[0];
fourthVertexPosition[1] = areaBoundingShapeLowerLeft[1];
}
private clearSelection(clearHighlightedHandles): SelectionHandle[]
{
const deselectedHandles: SelectionHandle[] = [];
this.selectedHandlePairIndices.forEach(index => {
const selectionHandleAndModel: Pair<SelectionHandle, Model> = this.selectionHandlesAndModels[index];
if (selectionHandleAndModel.first.getIsSelected())
{
deselectedHandles.push(selectionHandleAndModel.first);
}
selectionHandleAndModel.second.renderProperties.color = this.normalHandleColor;
selectionHandleAndModel.first.setSelected(false);
if (clearHighlightedHandles)
{
selectionHandleAndModel.first.setHighlighted(false);
}
});
this.selectedHandlePairIndices.clear();
return deselectedHandles;
}
public select(handle: SelectionHandle): boolean
{
const handleIndex: number = this.findIndexOfSelectionHandle(handle);
if (handleIndex != -1)
{
this.addSelection(handleIndex);
return true;
}
return false;
}
public deselect(handle: SelectionHandle): boolean
{
const handleIndex: number = this.findIndexOfSelectionHandle(handle);
if (handleIndex != -1)
{
this.removeSelection(handleIndex);
return true;
}
return false;
}
private findIndexOfSelectionHandle(handle: SelectionHandle): number
{
for (let ii = 0; ii < this.selectionHandlesAndModels.length; ii++)
{
const handleAndModel: Pair<SelectionHandle, Model> = this.selectionHandlesAndModels[ii];
if (handleAndModel.first == handle)
{
return ii;
}
}
return -1;
}
private addSelection(handlePairIndex: number): void
{
this.selectedHandlePairIndices.add(handlePairIndex);
this.selectionHandlesAndModels[handlePairIndex].second.renderProperties.color = this.selectedHandleColor;
this.selectionHandlesAndModels[handlePairIndex].first.setSelected(true);
}
private removeSelection(handlePairIndex: number): void
{
this.selectedHandlePairIndices.delete(handlePairIndex);
this.selectionHandlesAndModels[handlePairIndex].second.renderProperties.color = this.normalHandleColor;
this.selectionHandlesAndModels[handlePairIndex].first.setSelected(false);
}
private updateHandleColors(): void
{
for (let ii = 0; ii < this.selectionHandlesAndModels.length; ii++)
{
const handle: Pair<SelectionHandle, Model> = this.selectionHandlesAndModels[ii];
if (handle.first.shouldHighlight())
{
handle.second.renderProperties.color = this.selectedHandleColor;
}
else {
handle.second.renderProperties.color = this.normalHandleColor;
}
}
}
}SelectionSystem.spec.ts
import { vec2, vec4 } from "gl-matrix";
import { WorldEditorApplication } from "../app/applications/world-editor/WorldEditorApplication";
import { Camera } from "../app/engine/render/Camera";
import { Command, UnifiedKeyEvent, KeyPressedState } from "../app/engine/input/Command";
import { MouseMoveCommand } from "../app/commands/MouseMoveCommand";
import { KeyEventBuilder } from "../app/engine/input/KeyEventBuilder";
import { Layer } from "../app/engine/world/Layer";
import { Model } from "../app/engine/model/Model";
import { Mesh } from "../app/engine/model/Mesh";
import { RenderProperties } from "../app/engine/render/RenderProperties";
import { WireframeRenderMode, ShadingRenderMode } from "../app/engine/render/RenderMode";
import { BehaviorSubject } from "rxjs";
import { Zone } from "../app/engine/world/Zone";
import { ManagementCallbacks } from "../app/engine/world/WorldSpace";
import { ZoneSelectionHandle } from "../app/engine/model/select/ZoneSelectionHandle";
import { IDProvider } from "../app/services/idprovider.service";
import { World } from "../app/engine/world/World";
import { SelectionHandle, SelectionModel, PrimarySelectionCategory, SecondarySelectionCategory, TertiarySelectionCategory } from "../app/engine/model/select/Selectable";
import { SelectHandlesCommand, SelectHandlesCommandInstance } from "../app/commands/SelectHandlesCommand";
import { DeselectHandlesCommand, DeselectHandlesCommandInstance } from "../app/commands/DeselectHandlesCommand";
import { HandlesMovedCommand, HandlesMovedCommandInstance } from "../app/commands/HandlesMovedCommand";
import { CommandService } from "../app/services/command.service";
import { UndoCommand } from "../app/commands/UndoCommand";
import { RedoCommand } from "../app/commands/RedoCommand";
import { Vertex } from "../app/engine/model/Vertex";
import { ID } from "../app/engine/core/ID";
import { IDMap } from "../util/IDMap";
describe("Selection System", () => {
const RENDERER_NAME: string = "test-renderer";
const KEY_EVENT_BUILDER: KeyEventBuilder = new KeyEventBuilder();
const MOUSE_MOVE_TO_100_100_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().pressedState(KeyPressedState.UP).withMousePosition([ 100, (960 - 100) ], [ 0, 0 ]).end();
const MOUSE_PRIMARY_BUTTON_DOWN_AT_100_100_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().mouseButton(0).pressedState(KeyPressedState.DOWN).withMousePosition([ 100, (960 - 100) ], [ 0, 0 ]).end();
const MOUSE_PRIMARY_BUTTON_UP_AT_100_100_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().mouseButton(0).pressedState(KeyPressedState.UP).withMousePosition([ 100, (960 - 100) ], [ 0, 0 ]).end();
const MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_100_100_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().mouseButton(0).shift(true).pressedState(KeyPressedState.DOWN).withMousePosition([ 100, (960 - 100) ], [ 0, 0 ]).end();
const MOUSE_PRIMARY_BUTTON_DOWN_AT_75_75_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().mouseButton(0).pressedState(KeyPressedState.DOWN).withMousePosition([ 75, (960 - 75) ], [ 0, 0 ]).end();
const MOUSE_PRIMARY_BUTTON_UP_AT_75_75_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().mouseButton(0).pressedState(KeyPressedState.UP).withMousePosition([ 75, (960 - 75) ], [ 0, 0 ]).end();
const MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_75_75_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().mouseButton(0).shift(true).pressedState(KeyPressedState.DOWN).withMousePosition([ 75, (960 - 75) ], [ 0, 0 ]).end();
const MOUSE_MOVE_TO_10_10_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().pressedState(KeyPressedState.UP).withMousePosition([ 10, (960 - 10) ], [ 0, 0 ]).end();
const MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().mouseButton(0).pressedState(KeyPressedState.DOWN).withMousePosition([ 10, (960 - 10) ], [ 0, 0 ]).end();
const MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_10_10_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().mouseButton(0).shift(true).pressedState(KeyPressedState.DOWN).withMousePosition([ 10, (960 - 10) ], [ 0, 0 ]).end();
const MOUSE_PRIMARY_BUTTON_UP_WITH_SHIFT_AT_10_10_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().mouseButton(0).shift(true).pressedState(KeyPressedState.UP).withMousePosition([ 10, (960 - 10) ], [ 0, 0 ]).end();
const MOUSE_MOVE_TO_30_30_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().pressedState(KeyPressedState.UP).withMousePosition([ 30, (960 - 30) ], [ 0, 0 ]).end();
const MOUSE_PRIMARY_BUTTON_DOWN_AT_30_30_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().mouseButton(0).pressedState(KeyPressedState.DOWN).withMousePosition([ 30, (960 - 30) ], [ 0, 0 ]).end();
const MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_30_30_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().mouseButton(0).shift(true).pressedState(KeyPressedState.DOWN).withMousePosition([ 30, (960 - 30) ], [ 0, 0 ]).end();
const MOUSE_PRIMARY_BUTTON_UP_WITH_SHIFT_AT_30_30_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().mouseButton(0).shift(true).pressedState(KeyPressedState.UP).withMousePosition([ 30, (960 - 30) ], [ 0, 0 ]).end();
const CTRL_Z_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().ctrl(true).keyboard("KeyZ").pressedState(KeyPressedState.UP).end();
const CTRL_SHIFT_Z_EVENT: UnifiedKeyEvent = KEY_EVENT_BUILDER.start().ctrl(true).shift(true).keyboard("KeyZ").pressedState(KeyPressedState.UP).end();
const NORMAL_HANDLE_COLOR: vec4 = [ 0.0, 0.0, 1.0, 1.0 ];
const HIGHLIGHTED_HANDLE_COLOR: vec4 = [ 0.0, 1.0, 0.0, 1.0 ];
const SELECTION_MODEL: SelectionModel = new SelectionModel();
SELECTION_MODEL.setPrimaryCategory(PrimarySelectionCategory.NONE);
SELECTION_MODEL.setSecondaryCategory(SecondarySelectionCategory.NONE);
SELECTION_MODEL.setTertiaryCategory(TertiarySelectionCategory.VERTICES);
let camera: Camera;
let glRenderer: any;
let webGLService: any;
let inputService: any;
let commandService: CommandService;
const undoCommand: UndoCommand = new UndoCommand("undo", CTRL_Z_EVENT);
const redoCommand: RedoCommand = new RedoCommand("redo", CTRL_SHIFT_Z_EVENT);
let worldEditorApplication: WorldEditorApplication;
let commands: Command<any, any>[];
let mouseMoveCommand: MouseMoveCommand;
let selectHandlesCommand: SelectHandlesCommand;
let deselectHandlesCommand: DeselectHandlesCommand;
let handlesMovedCommand: HandlesMovedCommand;
let mesh: Mesh;
let meshVertices: Vertex[];
let meshVertexIDs: ID[];
let renderProperties: RenderProperties;
let model: Model;
let layer: Layer;
let selectionHandles: SelectionHandle[];
let firstVertexHandle: SelectionHandle;
let secondVertexHandle: SelectionHandle;
let thirdVertexHandle: SelectionHandle;
let managementCallbacks: ManagementCallbacks<ZoneSelectionHandle, Zone>;
let idProvider: IDProvider;
let zone: Zone;
let world: World;
let configurationService: any;
let engine: any;
let engineService: any;
let hudLayer: Layer;
beforeEach((done) => {
camera = new Camera();
glRenderer = {};
glRenderer.camera = camera;
webGLService = jasmine.createSpyObj("webGLService", [ "getRendererFromName" ]);
webGLService.getRendererFromName.and.returnValue(glRenderer);
inputService = {};
commandService = new CommandService(null, null);
spyOn(commandService, "commandWasExecuted");
commandService.addSystemCommand(undoCommand);
commandService.addSystemCommand(redoCommand);
worldEditorApplication = new WorldEditorApplication(webGLService, inputService, commandService, RENDERER_NAME);
commands = worldEditorApplication.getOrCreateFocusedCommands();
for (let ii = 0; ii < commands.length; ii++)
{
const command: Command<any, any> = commands[ii];
if (command instanceof MouseMoveCommand)
{
mouseMoveCommand = command;
}
else if (command instanceof SelectHandlesCommand)
{
selectHandlesCommand = command;
}
else if (command instanceof DeselectHandlesCommand)
{
deselectHandlesCommand = command;
}
else if (command instanceof HandlesMovedCommand)
{
handlesMovedCommand = command;
}
}
spyOn(selectHandlesCommand, "setSelectedHandles");
spyOn(selectHandlesCommand, "execute");
spyOn(deselectHandlesCommand, "setDeselectedHandles");
spyOn(deselectHandlesCommand, "execute");
spyOn(handlesMovedCommand, "setMovement");
spyOn(handlesMovedCommand, "execute");
mesh = new Mesh();
meshVertices = [
mesh.addVertex([ 10, 10 ]),
mesh.addVertex([ 30, 30 ]),
mesh.addVertex([ 50, 10 ]),
];
meshVertexIDs = meshVertices.map(vertex => vertex.getID());
mesh.addTriangle(meshVertexIDs[0], meshVertexIDs[1], meshVertexIDs[2]);
renderProperties = { visible: true, color: [ 1, 1, 1, 1], wireframeRenderMode: WireframeRenderMode.ALL, shadingRenderMode: ShadingRenderMode.FLAT_FULL_BRIGHT, paralaxFactor: 1.0 };
model = new Model("shader", mesh, renderProperties);
layer = new Layer();
layer.addModel(model);
selectionHandles = model.getSelectionHandles(SELECTION_MODEL);
firstVertexHandle = selectionHandles[0];
secondVertexHandle = selectionHandles[1];
thirdVertexHandle = selectionHandles[2];
managementCallbacks = { loadFullInPlace: () => {}, dispose: () => {} };
idProvider = { provideID: (name: string) => { return "name"; } };
zone = new Zone(managementCallbacks, "zone", "test", "", idProvider);
zone.addBackgroundLayer(layer);
world = new World();
world.addZone(zone, true);
configurationService = {
selectTriangles$: new BehaviorSubject<boolean>(false),
selectEdges$: new BehaviorSubject<boolean>(false),
selectVertices$: new BehaviorSubject<boolean>(false),
selectionHandleSize$: new BehaviorSubject<number>(10),
normalSelectionHandleColor$: new BehaviorSubject<vec4>(NORMAL_HANDLE_COLOR),
selectedSelectionHandleColor$: new BehaviorSubject<vec4>(HIGHLIGHTED_HANDLE_COLOR)
};
engine = jasmine.createSpyObj("engine", [ "getWorld" ]);
engine.getWorld.and.returnValue(world);
engineService = jasmine.createSpyObj("engineService", [ "createOrGetEngine", "addHUDLayer" ]);
engineService.createOrGetEngine.and.returnValue(engine);
worldEditorApplication.configure(configurationService, engineService);
hudLayer = engineService.addHUDLayer.calls.allArgs()[0][0];
worldEditorApplication.addLayerForEditing(layer);
setTimeout(() => {
configurationService.selectionHandleSize$.next(10);
done();
}, 10);
});
it("handles do not highlight when not touched by the mouse", () => {
mouseMoveCommand.execute(MOUSE_MOVE_TO_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
expect(firstVertexHandle.shouldHighlight()).toBeFalsy();
expect(secondVertexHandle.shouldHighlight()).toBeFalsy();
expect(thirdVertexHandle.shouldHighlight()).toBeFalsy();
expect(commandService.commandWasExecuted).not.toHaveBeenCalled();
expect(selectHandlesCommand.setSelectedHandles).not.toHaveBeenCalled();
expect(selectHandlesCommand.execute).not.toHaveBeenCalled();
expect(deselectHandlesCommand.setDeselectedHandles).not.toHaveBeenCalled();
expect(deselectHandlesCommand.execute).not.toHaveBeenCalled();
expect(handlesMovedCommand.setMovement).not.toHaveBeenCalled();
expect(handlesMovedCommand.execute).not.toHaveBeenCalled();
});
it("handles highlight when touched by the mouse", () => {
mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
expect(firstVertexHandle.shouldHighlight()).toBeTruthy();
expect(secondVertexHandle.shouldHighlight()).toBeFalsy();
expect(thirdVertexHandle.shouldHighlight()).toBeFalsy();
expect(commandService.commandWasExecuted).not.toHaveBeenCalled();
expect(selectHandlesCommand.setSelectedHandles).not.toHaveBeenCalled();
expect(selectHandlesCommand.execute).not.toHaveBeenCalled();
expect(deselectHandlesCommand.setDeselectedHandles).not.toHaveBeenCalled();
expect(deselectHandlesCommand.execute).not.toHaveBeenCalled();
expect(handlesMovedCommand.setMovement).not.toHaveBeenCalled();
expect(handlesMovedCommand.execute).not.toHaveBeenCalled();
});
it("handles unhighlight when no longer touched by the mouse", () => {
mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
mouseMoveCommand.execute(MOUSE_MOVE_TO_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
expect(firstVertexHandle.shouldHighlight()).toBeFalsy();
expect(secondVertexHandle.shouldHighlight()).toBeFalsy();
expect(thirdVertexHandle.shouldHighlight()).toBeFalsy();
expect(commandService.commandWasExecuted).not.toHaveBeenCalled();
expect(selectHandlesCommand.setSelectedHandles).not.toHaveBeenCalled();
expect(selectHandlesCommand.execute).not.toHaveBeenCalled();
expect(deselectHandlesCommand.setDeselectedHandles).not.toHaveBeenCalled();
expect(deselectHandlesCommand.execute).not.toHaveBeenCalled();
expect(handlesMovedCommand.setMovement).not.toHaveBeenCalled();
expect(handlesMovedCommand.execute).not.toHaveBeenCalled();
});
it("clicking a handle selects it", () => {
// Move the mouse there mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button back up
mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
const selectHandlesCommandInstance: SelectHandlesCommandInstance = new SelectHandlesCommandInstance(selectHandlesCommand, [ firstVertexHandle ]);
(<any>selectHandlesCommand.execute).and.returnValue(selectHandlesCommandInstance);
worldEditorApplication.preRender(engine, 1);
expect(firstVertexHandle.shouldHighlight()).toBeTruthy();
expect(firstVertexHandle.getIsSelected()).toBeTruthy();
expect(selectHandlesCommand.setSelectedHandles).toHaveBeenCalledWith([ firstVertexHandle ]);
expect(commandService.commandWasExecuted).toHaveBeenCalledWith(selectHandlesCommand, selectHandlesCommandInstance);
});
it("clicking a handle also deselects other selected handles", () => {
// Move the mouse to the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down on the first point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button up on the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Move the mouse to the second point mouseMoveCommand.execute(MOUSE_MOVE_TO_30_30_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down on the second point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_30_30_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
expect(firstVertexHandle.getIsSelected()).toBeTruthy();
// Mouse button up on the second point mouseMoveCommand.execute(MOUSE_MOVE_TO_30_30_EVENT, null, null);
const selectHandlesCommandInstance: SelectHandlesCommandInstance = new SelectHandlesCommandInstance(selectHandlesCommand, [ secondVertexHandle ]);
(<any>selectHandlesCommand.execute).and.returnValue(selectHandlesCommandInstance);
const deselectHandlesCommandInstance: DeselectHandlesCommandInstance = new DeselectHandlesCommandInstance(deselectHandlesCommand, [ firstVertexHandle ]);
(<any>deselectHandlesCommand.execute).and.returnValue(deselectHandlesCommandInstance);
worldEditorApplication.preRender(engine, 1);
expect(firstVertexHandle.getIsSelected()).toBeFalsy();
expect(secondVertexHandle.getIsSelected()).toBeTruthy();
expect(selectHandlesCommand.setSelectedHandles).toHaveBeenCalledWith([ secondVertexHandle ]);
expect(commandService.commandWasExecuted).toHaveBeenCalledWith(selectHandlesCommand, selectHandlesCommandInstance);
expect(deselectHandlesCommand.setDeselectedHandles).toHaveBeenCalledWith([ firstVertexHandle ]);
expect(commandService.commandWasExecuted).toHaveBeenCalledWith(deselectHandlesCommand, deselectHandlesCommandInstance);
expect(handlesMovedCommand.setMovement).not.toHaveBeenCalled();
expect(handlesMovedCommand.execute).not.toHaveBeenCalled();
});
it("clicking on nothing deselects everything", () => {
// Move the mouse to the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down on the first point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button up on the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Move mouse off of everything mouseMoveCommand.execute(MOUSE_MOVE_TO_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse down out in the middle of nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_100_100_EVENT, null, null);
const deselectHandlesCommandInstance: DeselectHandlesCommandInstance = new DeselectHandlesCommandInstance(deselectHandlesCommand, [ firstVertexHandle ]);
(<any>deselectHandlesCommand.execute).and.returnValue(deselectHandlesCommandInstance);
worldEditorApplication.preRender(engine, 1);
expect(deselectHandlesCommand.setDeselectedHandles).toHaveBeenCalledWith([ firstVertexHandle ]);
expect(commandService.commandWasExecuted).toHaveBeenCalledWith(deselectHandlesCommand, deselectHandlesCommandInstance);
// Mouse back up in the same spot, deselecting everything mouseMoveCommand.execute(MOUSE_MOVE_TO_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
expect(firstVertexHandle.getIsSelected()).toBeFalsy();
expect(secondVertexHandle.getIsSelected()).toBeFalsy();
expect(thirdVertexHandle.getIsSelected()).toBeFalsy();
expect(handlesMovedCommand.setMovement).not.toHaveBeenCalled();
expect(handlesMovedCommand.execute).not.toHaveBeenCalled();
});
it("clicking on an unselected handle and then dragging the mouse selects that handle and drags it", () => {
// Move the mouse to the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down on the first point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse down out in the middle of nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_100_100_EVENT, null, null);
const selectHandlesCommandInstance: SelectHandlesCommandInstance = new SelectHandlesCommandInstance(selectHandlesCommand, [ firstVertexHandle ]);
(<any>selectHandlesCommand.execute).and.returnValue(selectHandlesCommandInstance);
worldEditorApplication.preRender(engine, 1);
expect(selectHandlesCommand.setSelectedHandles).toHaveBeenCalledWith([ firstVertexHandle ]);
expect(commandService.commandWasExecuted).toHaveBeenCalledWith(selectHandlesCommand, selectHandlesCommandInstance);
// Update the mouse at the same position again so it knows the drag event is over mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_UP_AT_100_100_EVENT, null, null);
const handlesMovedCommandInstance: HandlesMovedCommandInstance = new HandlesMovedCommandInstance(handlesMovedCommand, [ firstVertexHandle ], [ 90, 90 ]);
(<any>handlesMovedCommand.execute).and.returnValue(handlesMovedCommandInstance);
worldEditorApplication.preRender(engine, 1);
expect(handlesMovedCommand.setMovement).toHaveBeenCalledWith([ firstVertexHandle ], vec2.fromValues(90, 90));
expect(commandService.commandWasExecuted).toHaveBeenCalledWith(handlesMovedCommand, handlesMovedCommandInstance);
const firstHandlePosition: vec2 = firstVertexHandle.getPosition();
expect(firstVertexHandle.getIsSelected()).toBeTruthy();
expect(firstHandlePosition[0]).toEqual(100);
expect(firstHandlePosition[1]).toEqual(100);
});
it("clicking and dragging an already-selected handle does not change any selections but does drag the selected handle", () => {
// Move the mouse to the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down on the first point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button up on the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
expect(selectHandlesCommand.setSelectedHandles).toHaveBeenCalledTimes(1);
// Mouse button down on the first point again mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Move the mouse to a different point, button still down mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Update the mouse at the same position again so it knows the drag event is over mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_UP_AT_100_100_EVENT, null, null);
const handlesMovedCommandInstance: HandlesMovedCommandInstance = new HandlesMovedCommandInstance(handlesMovedCommand, [ firstVertexHandle ], [ 90, 90 ]);
(<any>handlesMovedCommand.execute).and.returnValue(handlesMovedCommandInstance);
worldEditorApplication.preRender(engine, 1);
expect(selectHandlesCommand.setSelectedHandles).toHaveBeenCalledTimes(1);
expect(handlesMovedCommand.setMovement).toHaveBeenCalledWith([ firstVertexHandle ], vec2.fromValues(90, 90));
expect(commandService.commandWasExecuted).toHaveBeenCalledWith(handlesMovedCommand, handlesMovedCommandInstance);
const firstHandlePosition: vec2 = firstVertexHandle.getPosition();
expect(firstVertexHandle.getIsSelected()).toBeTruthy();
expect(firstHandlePosition[0]).toEqual(100);
expect(firstHandlePosition[1]).toEqual(100);
});
it("shift-clicking on nothing does not deselect anything", () => {
// Move the mouse to the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Move the mouse to the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down on the first point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button up on the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Move the mouse to nowhere mouseMoveCommand.execute(MOUSE_MOVE_TO_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down with shift in nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down back up in nowhere mouseMoveCommand.execute(MOUSE_MOVE_TO_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
const firstHandlePosition: vec2 = firstVertexHandle.getPosition();
expect(firstVertexHandle.getIsSelected()).toBeTruthy();
expect(firstHandlePosition[0]).toEqual(10);
expect(firstHandlePosition[1]).toEqual(10);
expect(selectHandlesCommand.setSelectedHandles).toHaveBeenCalled();
expect(selectHandlesCommand.execute).toHaveBeenCalled();
expect(deselectHandlesCommand.setDeselectedHandles).not.toHaveBeenCalled();
expect(deselectHandlesCommand.execute).not.toHaveBeenCalled();
expect(handlesMovedCommand.setMovement).not.toHaveBeenCalled();
expect(handlesMovedCommand.execute).not.toHaveBeenCalled();
});
it("shift-clicking an additional handle selects it as well", () => {
// Move the mouse to the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down on the first point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button up on the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
const firstSelectHandlesCommandInstance: SelectHandlesCommandInstance = new SelectHandlesCommandInstance(selectHandlesCommand, [ firstVertexHandle ]);
(<any>selectHandlesCommand.execute).and.returnValue(firstSelectHandlesCommandInstance);
worldEditorApplication.preRender(engine, 1);
expect(selectHandlesCommand.setSelectedHandles).toHaveBeenCalledWith([ firstVertexHandle ]);
expect(commandService.commandWasExecuted).toHaveBeenCalledWith(selectHandlesCommand, firstSelectHandlesCommandInstance);
// Move the mouse to the second point mouseMoveCommand.execute(MOUSE_MOVE_TO_30_30_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down with shift on the second point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_30_30_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button up on the second point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_UP_WITH_SHIFT_AT_30_30_EVENT, null, null);
const secondSelectHandlesCommandInstance: SelectHandlesCommandInstance = new SelectHandlesCommandInstance(selectHandlesCommand, [ secondVertexHandle ]);
(<any>selectHandlesCommand.execute).and.returnValue(secondSelectHandlesCommandInstance);
worldEditorApplication.preRender(engine, 1);
expect(selectHandlesCommand.setSelectedHandles).toHaveBeenCalledWith([ secondVertexHandle ]);
expect(commandService.commandWasExecuted).toHaveBeenCalledWith(selectHandlesCommand, secondSelectHandlesCommandInstance);
expect(firstVertexHandle.getIsSelected()).toBeTruthy();
expect(secondVertexHandle.getIsSelected()).toBeTruthy();
});
it("shift-clicking a selected handle unselects it", () => {
// Move the mouse to the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down on the first point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button up on the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Move the mouse to the second point mouseMoveCommand.execute(MOUSE_MOVE_TO_30_30_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down with shift on the second point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_30_30_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button up on the second point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_UP_WITH_SHIFT_AT_30_30_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Move the mouse to the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down on the first point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button up on the first point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_UP_WITH_SHIFT_AT_10_10_EVENT, null, null);
const deselectHandlesCommandInstance: DeselectHandlesCommandInstance = new DeselectHandlesCommandInstance(deselectHandlesCommand, [ firstVertexHandle ]);
(<any>deselectHandlesCommand.execute).and.returnValue(deselectHandlesCommandInstance);
worldEditorApplication.preRender(engine, 1);
expect(deselectHandlesCommand.setDeselectedHandles).toHaveBeenCalledWith([ firstVertexHandle ]);
expect(commandService.commandWasExecuted).toHaveBeenCalledWith(deselectHandlesCommand, deselectHandlesCommandInstance);
expect(firstVertexHandle.getIsSelected()).toBeFalsy();
expect(secondVertexHandle.getIsSelected()).toBeTruthy();
});
it("shift-dragging not over handles draws selection area", () => {
// Move the mouse to nowhere mouseMoveCommand.execute(MOUSE_MOVE_TO_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down with shift in nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down--still with shift--in a different nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_75_75_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
const models: Model[] = hudLayer.getModels();
const selectionAreaModel: Model = models[3];
const _vertexPositions: IDMap<vec2> = selectionAreaModel.mesh.getVertexPositions();
const vertexPositions: vec2[] = _vertexPositions.valuesAsArray();
expect(vertexPositions[0][0]).toEqual(75);
expect(vertexPositions[0][1]).toEqual(75);
expect(vertexPositions[1][0]).toEqual(75);
expect(vertexPositions[1][1]).toEqual(100);
expect(vertexPositions[2][0]).toEqual(100);
expect(vertexPositions[2][1]).toEqual(100);
expect(vertexPositions[3][0]).toEqual(100);
expect(vertexPositions[3][1]).toEqual(75);
expect(commandService.commandWasExecuted).not.toHaveBeenCalled();
});
it("shift-dragging not over handles does not change any highlights or selections", () => {
// Move the mouse to nowhere mouseMoveCommand.execute(MOUSE_MOVE_TO_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down with shift in nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down--still with shift--in a different nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_75_75_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
expect(firstVertexHandle.shouldHighlight()).toBeFalsy();
expect(secondVertexHandle.shouldHighlight()).toBeFalsy();
expect(thirdVertexHandle.shouldHighlight()).toBeFalsy();
expect(commandService.commandWasExecuted).not.toHaveBeenCalled();
});
it("shift-dragging over handles highlights them but does not select them", () => {
// Move the mouse to nowhere mouseMoveCommand.execute(MOUSE_MOVE_TO_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down with shift in nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down--still with shift--at (10, 10) mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
expect(firstVertexHandle.shouldHighlight()).toBeTruthy();
expect(firstVertexHandle.getIsSelected()).toBeFalsy();
expect(commandService.commandWasExecuted).not.toHaveBeenCalled();
});
it("shift-clicking an unselected handle and then dragging selects that handle and then drags all selected handles", () => {
// Move the mouse to nowhere mouseMoveCommand.execute(MOUSE_MOVE_TO_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down with shift at (10, 10) mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button up with shift at (10, 10) mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_UP_WITH_SHIFT_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down with shift at (30, 30) mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_30_30_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down with shift in nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_WITH_SHIFT_AT_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
const firstVertexPosition: vec2 = firstVertexHandle.getPosition();
const secondVertexPosition: vec2 = secondVertexHandle.getPosition();
expect(firstVertexHandle.getIsSelected()).toBeTruthy();
expect(firstVertexPosition[0]).toEqual(80);
expect(firstVertexPosition[1]).toEqual(80);
expect(secondVertexHandle.getIsSelected()).toBeTruthy();
expect(secondVertexPosition[0]).toEqual(100);
expect(secondVertexPosition[1]).toEqual(100);
});
it("dragging handle actually moves its model", () => {
// Move the mouse to the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down on the first point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse down out in the middle of nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
const firstVertexPosition: vec2 = firstVertexHandle.getPosition();
expect(firstVertexHandle.getIsSelected()).toBeTruthy();
expect(firstVertexPosition[0]).toEqual(100);
expect(firstVertexPosition[1]).toEqual(100);
const models: Model[] = hudLayer.getModels();
const firstHandleModel: Model = models[0];
const vertexPositions: vec2[] = firstHandleModel.mesh.getVertexPositions().valuesAsArray();
expect(vertexPositions[0][0]).toEqual(90);
expect(vertexPositions[0][1]).toEqual(90);
expect(vertexPositions[1][0]).toEqual(90);
expect(vertexPositions[1][1]).toEqual(110);
expect(vertexPositions[2][0]).toEqual(110);
expect(vertexPositions[2][1]).toEqual(110);
expect(vertexPositions[3][0]).toEqual(110);
expect(vertexPositions[3][1]).toEqual(90);
});
it("dragging handle and undoing it also undoes model movement", () => {
// Move the mouse to the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down on the first point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse down out in the middle of nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse back up out in the middle of nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_UP_AT_100_100_EVENT, null, null);
(<any>commandService.commandWasExecuted).and.callThrough();
(<any>handlesMovedCommand.setMovement).and.callThrough();
(<any>handlesMovedCommand.execute).and.callThrough();
worldEditorApplication.preRender(engine, 1);
commandService.executeCommand(CTRL_Z_EVENT, null);
const firstVertexPosition: vec2 = firstVertexHandle.getPosition();
expect(firstVertexPosition).toEqual([ 10, 10 ]);
worldEditorApplication.preRender(engine, 1);
const models: Model[] = hudLayer.getModels();
const firstHandleModel: Model = models[0];
const vertexPositions: vec2[] = firstHandleModel.mesh.getVertexPositions().valuesAsArray();
expect(vertexPositions[0][0]).toEqual(0);
expect(vertexPositions[0][1]).toEqual(0);
expect(vertexPositions[1][0]).toEqual(0);
expect(vertexPositions[1][1]).toEqual(20);
expect(vertexPositions[2][0]).toEqual(20);
expect(vertexPositions[2][1]).toEqual(20);
expect(vertexPositions[3][0]).toEqual(20);
expect(vertexPositions[3][1]).toEqual(0);
});
it("undoing drag handle is not repeated", () => {
// Move the mouse to the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down on the first point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse down out in the middle of nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse back up out in the middle of nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_UP_AT_100_100_EVENT, null, null);
(<any>commandService.commandWasExecuted).and.callThrough();
(<any>handlesMovedCommand.setMovement).and.callThrough();
(<any>handlesMovedCommand.execute).and.callThrough();
worldEditorApplication.preRender(engine, 1);
commandService.executeCommand(CTRL_Z_EVENT, null);
worldEditorApplication.preRender(engine, 1);
commandService.executeCommand(CTRL_Z_EVENT, null);
const firstVertexPosition: vec2 = firstVertexHandle.getPosition();
expect(firstVertexPosition).toEqual([ 10, 10 ]);
worldEditorApplication.preRender(engine, 1);
const models: Model[] = hudLayer.getModels();
const firstHandleModel: Model = models[0];
const vertexPositions: vec2[] = firstHandleModel.mesh.getVertexPositions().valuesAsArray();
expect(vertexPositions[0][0]).toEqual(0);
expect(vertexPositions[0][1]).toEqual(0);
expect(vertexPositions[1][0]).toEqual(0);
expect(vertexPositions[1][1]).toEqual(20);
expect(vertexPositions[2][0]).toEqual(20);
expect(vertexPositions[2][1]).toEqual(20);
expect(vertexPositions[3][0]).toEqual(20);
expect(vertexPositions[3][1]).toEqual(0);
});
it("dragging handle and then dragging it again without deselecting it results in two command instances", () => {
// Move the mouse to the first point mouseMoveCommand.execute(MOUSE_MOVE_TO_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse button down on the first point mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_10_10_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse down out in the middle of nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse back up out in the middle of nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_UP_AT_100_100_EVENT, null, null);
(<any>commandService.commandWasExecuted).and.callThrough();
(<any>handlesMovedCommand.setMovement).and.callThrough();
(<any>handlesMovedCommand.execute).and.callThrough();
worldEditorApplication.preRender(engine, 1);
// Mouse down out in the middle of nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_100_100_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse down out in the middle of a different nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_DOWN_AT_75_75_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
// Mouse back up out in the middle of a different nowhere mouseMoveCommand.execute(MOUSE_PRIMARY_BUTTON_UP_AT_75_75_EVENT, null, null);
worldEditorApplication.preRender(engine, 1);
const firstVertexPosition: vec2 = firstVertexHandle.getPosition();
let models: Model[] = hudLayer.getModels();
let firstHandleModel: Model = models[0];
let vertexPositions: vec2[] = firstHandleModel.mesh.getVertexPositions().valuesAsArray();
expect(firstVertexPosition).toEqual([ 75, 75 ]);
expect(vertexPositions[0][0]).toEqual(65);
expect(vertexPositions[0][1]).toEqual(65);
expect(vertexPositions[1][0]).toEqual(65);
expect(vertexPositions[1][1]).toEqual(85);
expect(vertexPositions[2][0]).toEqual(85);
expect(vertexPositions[2][1]).toEqual(85);
expect(vertexPositions[3][0]).toEqual(85);
expect(vertexPositions[3][1]).toEqual(65);
commandService.executeCommand(CTRL_Z_EVENT, null);
worldEditorApplication.preRender(engine, 1);
models = hudLayer.getModels();
firstHandleModel = models[0];
vertexPositions = firstHandleModel.mesh.getVertexPositions().valuesAsArray();
expect(firstVertexPosition).toEqual([ 100, 100 ]);
expect(vertexPositions[0][0]).toEqual(90);
expect(vertexPositions[0][1]).toEqual(90);
expect(vertexPositions[1][0]).toEqual(90);
expect(vertexPositions[1][1]).toEqual(110);
expect(vertexPositions[2][0]).toEqual(110);
expect(vertexPositions[2][1]).toEqual(110);
expect(vertexPositions[3][0]).toEqual(110);
expect(vertexPositions[3][1]).toEqual(90);
commandService.executeCommand(CTRL_Z_EVENT, null);
worldEditorApplication.preRender(engine, 1);
models = hudLayer.getModels();
firstHandleModel = models[0];
vertexPositions = firstHandleModel.mesh.getVertexPositions().valuesAsArray();
expect(firstVertexPosition).toEqual([ 10, 10 ]);
expect(vertexPositions[0][0]).toEqual(0);
expect(vertexPositions[0][1]).toEqual(0);
expect(vertexPositions[1][0]).toEqual(0);
expect(vertexPositions[1][1]).toEqual(20);
expect(vertexPositions[2][0]).toEqual(20);
expect(vertexPositions[2][1]).toEqual(20);
expect(vertexPositions[3][0]).toEqual(20);
expect(vertexPositions[3][1]).toEqual(0);
});
});


No comments:

Post a Comment