Skip to content

Commit

Permalink
Gizmo Helper: show numeric values for some gizmos
Browse files Browse the repository at this point in the history
  • Loading branch information
nkallen committed Mar 5, 2022
1 parent e4d26b7 commit 71a2d62
Show file tree
Hide file tree
Showing 21 changed files with 302 additions and 119 deletions.
43 changes: 43 additions & 0 deletions __tests__/command/KeyboardInterpreter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* @jest-environment jsdom
*/
import { KeyboardInterpreter } from "../../src/command/KeyboardInterpreter"

let interpreter: KeyboardInterpreter;

beforeEach(() => {
interpreter = new KeyboardInterpreter();
})

test('0-9', () => {
expect(interpreter.state).toEqual('');
interpreter.interpret(new KeyboardEvent('keydown', { key: '0' }));
expect(interpreter.state).toEqual('0');
interpreter.interpret(new KeyboardEvent('keydown', { key: '1' }));
expect(interpreter.state).toEqual('01');
interpreter.interpret(new KeyboardEvent('keydown', { key: '2' }));
expect(interpreter.state).toEqual('012');
interpreter.interpret(new KeyboardEvent('keydown', { key: '3' }));
expect(interpreter.state).toEqual('0123');
})

test('dot', () => {
expect(interpreter.state).toEqual('');
interpreter.interpret(new KeyboardEvent('keydown', { key: '0' }));
interpreter.interpret(new KeyboardEvent('keydown', { key: '.' }));
interpreter.interpret(new KeyboardEvent('keydown', { key: '1' }));
expect(interpreter.state).toEqual('0.1');
})

test('backspace', () => {
expect(interpreter.state).toEqual('');
interpreter.interpret(new KeyboardEvent('keydown', { key: '1' }));
interpreter.interpret(new KeyboardEvent('keydown', { key: '.' }));
interpreter.interpret(new KeyboardEvent('keydown', { key: '1' }));
interpreter.interpret(new KeyboardEvent('keydown', { key: 'Backspace' }));
expect(interpreter.state).toEqual('1.');
interpreter.interpret(new KeyboardEvent('keydown', { key: 'Backspace' }));
expect(interpreter.state).toEqual('1');
interpreter.interpret(new KeyboardEvent('keydown', { key: '2' }));
expect(interpreter.state).toEqual('12');
})
24 changes: 24 additions & 0 deletions __tests__/commands/MiniGizmo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ describe(LengthGizmo, () => {
const intersector = { raycast: jest.fn(), snap: jest.fn() };
const cb = jest.fn();
let info = {} as MovementInfo;
const moveEvent = new MouseEvent('move', { ctrlKey: false });

gizmo.onPointerEnter(intersector);
intersector.raycast.mockReturnValueOnce({ point: new THREE.Vector3() })
Expand All @@ -185,6 +186,29 @@ describe(LengthGizmo, () => {
gizmo.onPointerUp(cb, intersector, info)
gizmo.onPointerLeave(intersector);
})

test("ctrl key uses snaps", () => {
const snap = jest.fn();
const intersector = { raycast: jest.fn(), snap };
const cb = jest.fn();
let info = {} as MovementInfo;
const moveEvent = new MouseEvent('move', { ctrlKey: true });

snap.mockImplementation(() => [{ position: new THREE.Vector3(1, 1, 1) }]);
gizmo.update(viewport.camera);

gizmo.onPointerEnter(intersector);

intersector.raycast.mockReturnValueOnce({ point: new THREE.Vector3() })
gizmo.onPointerDown(cb, intersector, {} as MovementInfo);

gizmo.onPointerMove(cb, intersector, { viewport, event: moveEvent } as MovementInfo);
expect(gizmo.value).toBe(1);

gizmo.onPointerUp(cb, intersector, info)

gizmo.onPointerLeave(intersector);
})
})

describe(DistanceGizmo, () => {
Expand Down
8 changes: 7 additions & 1 deletion __tests__/viewport/Viewport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ beforeEach(() => {
editor = new Editor();
viewport = MakeViewport(editor);
editor.viewports.push(viewport);
db = editor.db;
db = editor._db;
materials = editor.materials;
signals = editor.signals;
selection = editor.selection;
Expand Down Expand Up @@ -96,6 +96,12 @@ test("navigation start & end restores selector state correctly", () => {
expect(viewport.multiplexer.enabled).toBe(false);
});

test("denormalizeScreenPosition", () => {
expect(viewport.denormalizeScreenPosition(new THREE.Vector2(-1, -1))).toEqual(new THREE.Vector2(0, 100));
expect(viewport.denormalizeScreenPosition(new THREE.Vector2(0, 0))).toEqual(new THREE.Vector2(50, 50));
expect(viewport.denormalizeScreenPosition(new THREE.Vector2(1, 1))).toEqual(new THREE.Vector2(100, 0));
})

const X = new THREE.Vector3(1, 0, 0);
const Y = new THREE.Vector3(0, 1, 0);
const Z = new THREE.Vector3(0, 0, 1);
Expand Down
19 changes: 17 additions & 2 deletions __tests__/viewport/ViewportPointControl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ describe(ViewportPointControl, () => {
expect(enqueue).toBeCalledTimes(1);
})

// NOTE: the next couple methods use complicated mocking to test. With profound regret!
// Starting a click enqueues a command. The command then registers a callback that is invoked
// on further mouse events. Here, we just avoid enqueing the command; so we register a custom
// callback for the purpose of testing, and assert on that.

test('startClick & startDrag enqueues move command', async () => {
expect(points.startClick([{ object: item.underlying.points.get(0), point: new THREE.Vector3() }], downEvent)).toBe(true);
let command: any;
Expand Down Expand Up @@ -92,14 +97,24 @@ describe(ViewportPointControl, () => {

test('dragging on a free construction plane', async () => {
expect(points.startClick([{ object: item.underlying.points.get(0), point: new THREE.Vector3() }], downEvent)).toBe(true);
const enqueue = jest.spyOn(editor, 'enqueue').mockImplementation((c, _) => {
return Promise.resolve();
})
points.startDrag(new MouseEvent('move'), new THREE.Vector2());
expect(enqueue).toBeCalledTimes(1);

let result;
const cb = jest.fn().mockImplementation(value => result = value);
const promise = points.execute(cb);

expect(cb).toBeCalledTimes(0);
points.continueDrag(new MouseEvent('move'), new THREE.Vector2(1, 1));
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toBeCalledTimes(1);

expect(result).toApproximatelyEqual(new THREE.Vector3(-4, 3, 0));
promise.finish();

points.endDrag(new THREE.Vector2());

await promise;
})
})
Expand Down
15 changes: 1 addition & 14 deletions __tests__/visual_model/VisualModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,29 @@ import * as THREE from "three";
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial';
import c3d from '../../build/Release/c3d.node';
import { CenterCircleFactory } from "../../src/commands/circle/CircleFactory";
import LineFactory from "../../src/commands/line/LineFactory";
import { RegionFactory } from "../../src/commands/region/RegionFactory";
import SphereFactory from "../../src/commands/sphere/SphereFactory";
import { EditorSignals } from "../../src/editor/EditorSignals";
import { GeometryDatabase } from "../../src/editor/GeometryDatabase";
import MaterialDatabase from '../../src/editor/MaterialDatabase';
import { ParallelMeshCreator } from '../../src/editor/MeshCreator';
import { SolidCopier } from "../../src/editor/SolidCopier";
import { SelectionDatabase } from "../../src/selection/SelectionDatabase";
import theme from '../../src/startup/default-theme';
import { RenderedSceneBuilder } from "../../src/visual_model/RenderedSceneBuilder";
import { ControlPointGroup, Curve3D, CurveEdge, CurveGroup, GeometryGroupUtils, SpaceInstance } from '../../src/visual_model/VisualModel';
import { CurveEdgeGroupBuilder, CurveSegmentGroupBuilder, mergeBufferAttributes, mergeBufferGeometries } from '../../src/visual_model/VisualModelBuilder';
import { BetterRaycastingPoints } from "../../src/visual_model/VisualModelRaycasting";
import { FakeMaterials } from "../../__mocks__/FakeMaterials";

let materials: MaterialDatabase;
let makeSphere: SphereFactory;
let makeLine: LineFactory;
let makeCircle: CenterCircleFactory;
let db: GeometryDatabase;
let signals: EditorSignals;
let makeRegion: RegionFactory;
let highlighter: RenderedSceneBuilder;
let selection: SelectionDatabase;

beforeEach(() => {
materials = new FakeMaterials();
signals = new EditorSignals();
db = new GeometryDatabase(new ParallelMeshCreator(), new SolidCopier(), materials, signals);
makeSphere = new SphereFactory(db, materials, signals);
makeLine = new LineFactory(db, materials, signals);
makeCircle = new CenterCircleFactory(db, materials, signals);
makeRegion = new RegionFactory(db, materials, signals);
selection = new SelectionDatabase(db, materials, signals);
highlighter = new RenderedSceneBuilder(db, materials, selection, theme, signals);
});


Expand Down Expand Up @@ -171,7 +158,7 @@ describe(mergeBufferAttributes, () => {
describe(mergeBufferGeometries, () => {
test('it works', () => {
const grids: c3d.MeshBuffer[] = [
{ index: new Uint32Array([1, 2]), position: new Float32Array([1, 2, 3]), normal: new Float32Array([10, 11, 12]) },
{ index: new Uint32Array([1, 2]), position: new Float32Array([1, 2, 3]), normal: new Float32Array([10, 11, 12]) } as any,
{ index: new Uint32Array([3, 4]), position: new Float32Array([4, 5, 6]), normal: new Float32Array([13, 14, 15]) },
]
const result = mergeBufferGeometries(grids);
Expand Down
56 changes: 34 additions & 22 deletions src/command/AbstractGizmo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SnapResult } from "../editor/snaps/SnapPicker";
import { CancellablePromise } from "../util/CancellablePromise";
import { Helper, Helpers } from "../util/Helpers";
import { GizmoMaterialDatabase } from "./GizmoMaterials";
import { KeyboardInterpreter } from "./KeyboardInterpreter";
import { Executable } from "./Quasimode";
import { SnapPresentation, SnapPresenter } from "./SnapPresenter";

Expand All @@ -30,10 +31,10 @@ import { SnapPresentation, SnapPresenter } from "./SnapPresenter";
* when a user types "x" with the move gizmo active, it starts moving along the x axis.
*/

export interface GizmoView {
export interface GizmoView<I> {
handle: THREE.Object3D;
picker: THREE.Object3D;
helper?: GizmoHelper;
helper?: GizmoHelper<I>;
}

export interface EditorLike {
Expand Down Expand Up @@ -65,7 +66,7 @@ export abstract class AbstractGizmo<I> extends Helper implements Executable<I, v

protected handle = new THREE.Group();
readonly picker = new THREE.Group();
readonly helper?: GizmoHelper;
readonly helper?: GizmoHelper<I>;

constructor(readonly title: string, protected readonly editor: EditorLike) {
super();
Expand All @@ -76,8 +77,8 @@ export abstract class AbstractGizmo<I> extends Helper implements Executable<I, v

onPointerEnter(intersector: Intersector) { }
onPointerLeave(intersector: Intersector) { }
onKeyPress(cb: (i: I) => void, text: string) { }
abstract onPointerMove(cb: (i: I) => void, intersector: Intersector, info: MovementInfo): void;
onKeyPress(cb: (i: I) => void, text: string): I | undefined { return }
abstract onPointerMove(cb: (i: I) => void, intersector: Intersector, info: MovementInfo): I | undefined;
abstract onPointerDown(cb: (i: I) => void, intersect: Intersector, info: MovementInfo): void;
abstract onPointerUp(cb: (i: I) => void, intersect: Intersector, info: MovementInfo): void;
abstract onInterrupt(cb: (i: I) => void): void;
Expand Down Expand Up @@ -108,7 +109,7 @@ export abstract class AbstractGizmo<I> extends Helper implements Executable<I, v
const reenableControls = viewport.disableControls();
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerUp);
domElement.ownerDocument.addEventListener('keypress', onKeyPress);
domElement.ownerDocument.addEventListener('keydown', onKeyPress);
const disp = this.editor.registry.addOne(domElement, "gizmo:finish", () => {
const lastEvent = new PointerEvent("pointerup");
onPointerUp(lastEvent);
Expand All @@ -117,7 +118,7 @@ export abstract class AbstractGizmo<I> extends Helper implements Executable<I, v
reenableControls.dispose();
document.removeEventListener('pointerup', onPointerUp);
document.removeEventListener('pointermove', onPointerMove);
domElement.ownerDocument.removeEventListener('keypress', onKeyPress);
domElement.ownerDocument.removeEventListener('keydown', onKeyPress);
disp.dispose();
});
}
Expand All @@ -139,7 +140,7 @@ export abstract class AbstractGizmo<I> extends Helper implements Executable<I, v
}

const onKeyPress = (event: KeyboardEvent) => {
stateMachine.keyPress(event);
stateMachine.keyPress(event);
}

const trigger = this.trigger.register(this, viewport, addEventHandlers);
Expand Down Expand Up @@ -270,7 +271,10 @@ export interface MovementInfo {
// This class handles computing some useful data (like click start and click end) of the
// gizmo user interaction. It deals with the hover->click->drag->unclick case (the traditional
// gizmo interactions) as well as the keyboardCommand->move->click->unclick case (blender modal-style).
type State = { tag: 'none' } | { tag: 'hover' } | { tag: 'dragging', clearEventHandlers: Disposable, clearPresenter: Disposable } | { tag: 'command', clearEventHandlers: Disposable, clearPresenter: Disposable, text: string }
type State = { tag: 'none' }
| { tag: 'hover' }
| { tag: 'dragging', clearEventHandlers: Disposable, clearPresenter: Disposable, text: KeyboardInterpreter }
| { tag: 'command', clearEventHandlers: Disposable, clearPresenter: Disposable, text: KeyboardInterpreter }

export class GizmoStateMachine<I, O> implements MovementInfo {
// NOTE: isActive and isEnabled differ only slightly. When !isEnabled, the gizmo is COMPLETELY disabled.
Expand Down Expand Up @@ -341,12 +345,13 @@ export class GizmoStateMachine<I, O> implements MovementInfo {
case 'none':
case 'hover':
const { worldPosition } = this;
const center3d = this.gizmo.getWorldPosition(worldPosition).project(this.camera);
this.gizmo.getWorldPosition(worldPosition)
const center3d = worldPosition.clone().project(this.camera);
this.center2d.set(center3d.x, center3d.y);
this.pointStart3d.copy(intersection.point);
this.pointStart2d.copy(this.currentMousePosition);
this.gizmo.onPointerDown(this.cb, this.intersector, this);
this.gizmo.helper?.onStart(this.viewport.domElement, this.center2d);
this.gizmo.helper?.onStart(this.viewport, this.center2d);
break;
case 'command':
this.pointerMove();
Expand All @@ -367,7 +372,7 @@ export class GizmoStateMachine<I, O> implements MovementInfo {
this.gizmo.update(this.camera);
this.begin();
const clearPresenter = this.presenter.execute();
this.state = { tag: 'command', clearEventHandlers, clearPresenter, text: "" };
this.state = { tag: 'command', clearEventHandlers, clearPresenter, text: new KeyboardInterpreter() };
this.gizmo.dispatchEvent({ type: 'start' });
} else {
clearEventHandlers.dispose();
Expand All @@ -387,7 +392,7 @@ export class GizmoStateMachine<I, O> implements MovementInfo {
this.begin();
const clearEventHandlers = start();
const clearPresenter = this.presenter.execute();
this.state = { tag: 'dragging', clearEventHandlers, clearPresenter };
this.state = { tag: 'dragging', clearEventHandlers, clearPresenter, text: new KeyboardInterpreter() };
this.gizmo.dispatchEvent({ type: 'start' });
break;
default: break;
Expand All @@ -411,8 +416,10 @@ export class GizmoStateMachine<I, O> implements MovementInfo {
this.angle = Math.atan2(this.endRadius.y, this.endRadius.x) - Math.atan2(startRadius.y, startRadius.x);

this.presenter.clear();
this.gizmo.helper?.onMove(this.pointEnd2d);
this.gizmo.onPointerMove(this.cb, this.intersector, this);
const value = this.gizmo.onPointerMove(this.cb, this.intersector, this);
if (value !== undefined) {
this.gizmo.helper?.onMove(this.pointEnd2d, value);
}

this.editor.signals.gizmoChanged.dispatch();
break;
Expand Down Expand Up @@ -472,14 +479,16 @@ export class GizmoStateMachine<I, O> implements MovementInfo {
}
}

keyPress(event: KeyboardEvent): void {
keyPress(event: KeyboardEvent) {
if (!this.isActive) return;
if (!this.isEnabled) return;

switch (this.state.tag) {
case 'dragging':
case 'command':
this.state.text += event.key;
this.gizmo.onKeyPress(this.cb, this.state.text);
this.state.text.interpret(event);
const value = this.gizmo.onKeyPress(this.cb, this.state.text.state);
if (value !== undefined) this.gizmo.helper?.onKeyPress(value);
this.editor.signals.gizmoChanged.dispatch();
break;
default: break;
Expand All @@ -494,7 +503,7 @@ export class GizmoStateMachine<I, O> implements MovementInfo {
this.state.clearPresenter.dispose();
this.gizmo.dispatchEvent({ type: 'interrupt' });
this.gizmo.onInterrupt(this.cb);
this.gizmo.helper?.onEnd();
this.gizmo.helper?.onInterrupt();
case 'hover':
this.gizmo.onPointerLeave(this.intersector);
this.state = { tag: 'none' };
Expand All @@ -519,8 +528,11 @@ export class GizmoStateMachine<I, O> implements MovementInfo {
}
}

export interface GizmoHelper {
onStart(parentElement: HTMLElement, position: THREE.Vector2): void;
onMove(position: THREE.Vector2): void;
export interface GizmoHelper<I> {
onStart(viewport: Viewport, positionSS: THREE.Vector2): void;
onMove(positionSS: THREE.Vector2, info: I): void;
onMove(positionSS: THREE.Vector2, info: I): void;
onKeyPress(info: I): void;
onEnd(): void;
onInterrupt(): void;
}
6 changes: 4 additions & 2 deletions src/command/CommandExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,10 @@ export class CommandExecutor {
db.clearTemporaryObjects();
snaps.xor = false;
PlaneDatabase.ScreenSpace.reset();
// TODO: remove when more data is gathered
if (helpers.scene.children.length > 0) console.error("Helpers scene is not empty");
if (helpers.scene.children.length > 0) {
console.error("Helpers scene is not empty");
console.error([...helpers.scene.children]);
}
helpers.clear();
}

Expand Down
18 changes: 18 additions & 0 deletions src/command/KeyboardInterpreter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

export class KeyboardInterpreter {
private _state = "";
private caret = 0;
get state() { return this._state; }

interpret(event: KeyboardEvent) {
if (/^[0-9.\-]$/.test(event.key)) {
this._state += event.key;
this.caret++;
} else {
switch (event.key) {
case 'Backspace':
this._state = this._state.slice(0, this._state.length - 1);
}
}
}
}
Loading

0 comments on commit 71a2d62

Please sign in to comment.