diff --git a/res/static/magnet.svg b/res/static/magnet.svg
new file mode 100644
index 0000000..5d1abd7
--- /dev/null
+++ b/res/static/magnet.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/res/static/orthographic.svg b/res/static/orthographic.svg
new file mode 100644
index 0000000..71cc3eb
--- /dev/null
+++ b/res/static/orthographic.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/res/static/perspective.svg b/res/static/perspective.svg
new file mode 100644
index 0000000..15c4509
--- /dev/null
+++ b/res/static/perspective.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/src/app_context.ts b/src/app_context.ts
index 0a571a9..d5ddac3 100644
--- a/src/app_context.ts
+++ b/src/app_context.ts
@@ -17,6 +17,7 @@ import { TVoxelisers, VoxeliseParams, VoxeliserFactory } from './voxelisers/voxe
import { ExporterFactory, TExporters } from './exporters/exporters';
import { Atlas } from './atlas';
import { Palette } from './palette';
+import { ArcballCamera } from './camera';
/* eslint-disable */
export enum EAction {
@@ -83,8 +84,9 @@ export class AppContext {
this._ui.disable(EAction.Simplify);
- Renderer.Get.toggleIsGridEnabled();
Renderer.Get.toggleIsAxesEnabled();
+ ArcballCamera.Get.setCameraMode('perspective');
+ ArcballCamera.Get.toggleAngleSnap();
}
public do(action: EAction) {
@@ -234,6 +236,7 @@ export class AppContext {
public draw() {
Renderer.Get.update();
+ this._ui.tick();
Renderer.Get.draw();
}
diff --git a/src/camera.ts b/src/camera.ts
index 3b99811..5e9704e 100644
--- a/src/camera.ts
+++ b/src/camera.ts
@@ -1,9 +1,10 @@
import { m4, v3 } from 'twgl.js';
import { MouseManager } from './mouse';
-import { degreesToRadians } from './math';
+import { AppMath, between, clamp, degreesToRadians, roundToNearest } from './math';
import { Renderer } from './renderer';
-import { SmoothVariable, SmoothVectorVariable } from './util';
+import { LOG, SmoothVariable, SmoothVectorVariable } from './util';
import { Vector3 } from './vector';
+import { AppConfig } from './config';
export class ArcballCamera {
public isUserRotating = false;
@@ -13,6 +14,8 @@ export class ArcballCamera {
private readonly zNear: number;
private readonly zFar: number;
public aspect: number;
+
+ private _isPerspective: boolean = true;
private readonly _defaultDistance = 18.0;
private readonly _defaultAzimuth = -1.0;
@@ -26,6 +29,10 @@ export class ArcballCamera {
private readonly up: v3.Vec3 = [0, 1, 0];
private eye: v3.Vec3 = [0, 0, 0];
+ private _azimuthRelief = 0.0;
+ private _elevationRelief = 0.0;
+ private _isAngleSnapped = false;
+
private mouseSensitivity = 0.005;
private scrollSensitivity = 0.005;
@@ -43,21 +50,66 @@ export class ArcballCamera {
this.gl = Renderer.Get._gl;
this.aspect = this.gl.canvas.width / this.gl.canvas.height;
- this._elevation.setClamp(0.01, Math.PI - 0.01);
+ this._elevation.setClamp(0.001, Math.PI - 0.001);
this._distance.setClamp(1.0, 100.0);
+
+ this.setCameraMode('perspective');
+ }
+
+ public isPerspective() {
+ return this._isPerspective;
+ }
+
+ public isOrthographic() {
+ return !this._isPerspective;
+ }
+
+ public isAlignedWithAxis(axis: 'x' | 'y' | 'z'): boolean {
+ const azimuth = Math.abs(this._azimuth.getTarget() % (Math.PI * 2));
+ const elevation = this._elevation.getTarget();
+
+ switch (axis) {
+ case 'x':
+ return AppMath.nearlyEqual(azimuth, AppMath.RADIANS_0) || AppMath.nearlyEqual(azimuth, AppMath.RADIANS_180);
+ case 'y':
+ return AppMath.nearlyEqual(elevation, AppMath.RADIANS_0, 0.002) || AppMath.nearlyEqual(elevation, AppMath.RADIANS_180, 0.002);
+ case 'z':
+ return AppMath.nearlyEqual(azimuth, AppMath.RADIANS_90) || AppMath.nearlyEqual(azimuth, AppMath.RADIANS_270);
+ }
+ }
+
+ public setCameraMode(mode: 'perspective' | 'orthographic') {
+ this._isPerspective = mode === 'perspective';
+ }
+
+ private _angleSnap = false;
+ public toggleAngleSnap() {
+ this._angleSnap = !this._angleSnap;
+
+ if (!this._angleSnap) {
+ this._isAngleSnapped = false;
+ this._azimuthRelief = 0.0;
+ this._elevationRelief = 0.0;
+ }
+ }
+ public isAngleSnapEnabled() {
+ return this._angleSnap;
}
public updateCamera() {
this.aspect = this.gl.canvas.width / this.gl.canvas.height;
const mouseDelta = MouseManager.Get.getMouseDelta();
+ mouseDelta.dx *= this.mouseSensitivity;
+ mouseDelta.dy *= this.mouseSensitivity;
+
if (this.isUserRotating) {
- this._azimuth.addToTarget(mouseDelta.dx * this.mouseSensitivity);
- this._elevation.addToTarget(mouseDelta.dy * this.mouseSensitivity);
+ this._azimuth.addToTarget(mouseDelta.dx);
+ this._elevation.addToTarget(mouseDelta.dy);
}
if (this.isUserTranslating) {
- const my = mouseDelta.dy * this.mouseSensitivity;
- const mx = mouseDelta.dx * this.mouseSensitivity;
+ const my = mouseDelta.dy;
+ const mx = mouseDelta.dx;
// Up-down
const dy = -Math.cos(this._elevation.getTarget() - Math.PI/2);
const df = Math.sin(this._elevation.getTarget() - Math.PI/2);
@@ -72,6 +124,89 @@ export class ArcballCamera {
this._target.addToTarget(new Vector3(dx * mx, 0.0, dz * mx));
}
+ const axisSnapRadius = clamp(AppConfig.ANGLE_SNAP_RADIUS_DEGREES, 0.0, 90.0) * degreesToRadians;
+
+ if (this._shouldSnapCameraAngle()) {
+ let shouldSnapToAzimuth = false;
+ let shouldSnapToElevation = false;
+ let snapAngleAzimuth = 0.0;
+ let snapAngleElevation = 0.0;
+
+ const azimuth = this._azimuth.getTarget();
+ const elevation = this._elevation.getTarget();
+
+ const modAzimuth = Math.abs(azimuth % (90 * degreesToRadians));
+
+ if (modAzimuth < axisSnapRadius || modAzimuth > (90*degreesToRadians - axisSnapRadius)) {
+ shouldSnapToAzimuth = true;
+ snapAngleAzimuth = roundToNearest(azimuth, 90 * degreesToRadians);
+ }
+
+ const elevationSnapPoints = [0, 90, 180].map((x) => x * degreesToRadians);
+ for (const elevationSnapPoint of elevationSnapPoints) {
+ if (elevationSnapPoint - axisSnapRadius <= elevation && elevation <= elevationSnapPoint + axisSnapRadius) {
+ shouldSnapToElevation = true;
+ snapAngleElevation = elevationSnapPoint;
+ break;
+ }
+ }
+
+ if (shouldSnapToAzimuth && shouldSnapToElevation) {
+ this._azimuth.setTarget(snapAngleAzimuth);
+ this._elevation.setTarget(snapAngleElevation);
+ this._isAngleSnapped = true;
+ }
+ }
+
+ /*
+ if (this.isOrthographic()) {
+ const azimuth0 = between(this._azimuth.getTarget(), 0.0 - axisSnapRadius, 0.0 + axisSnapRadius);
+ const azimuth90 = between(this._azimuth.getTarget(), Math.PI/2 - axisSnapRadius, Math.PI/2 + axisSnapRadius);
+ const azimuth180 = between(this._azimuth.getTarget(), Math.PI - axisSnapRadius, Math.PI + axisSnapRadius);
+ const azimuth270 = between(this._azimuth.getTarget(), 3*Math.PI/2 - axisSnapRadius, 3*Math.PI/2 + axisSnapRadius);
+
+ const elevationTop = between(this._elevation.getTarget(), 0.0 - axisSnapRadius, 0.0 + axisSnapRadius);
+ const elevationMiddle = between(this._elevation.getTarget(), Math.PI/2 - axisSnapRadius, Math.PI/2 + axisSnapRadius);
+ const elevationBottom = between(this._elevation.getTarget(), Math.PI - axisSnapRadius, Math.PI + axisSnapRadius);
+
+ if (elevationMiddle) {
+ if (azimuth0) {
+ this._azimuth.setTarget(0);
+ this._elevation.setTarget(Math.PI/2);
+ this._isAngleSnapped = true;
+ } else if (azimuth90) {
+ this._azimuth.setTarget(90);
+ this._elevation.setTarget(Math.PI/2);
+ this._isAngleSnapped = true;
+ } else if (azimuth180) {
+ this._azimuth.setTarget(180);
+ this._elevation.setTarget(Math.PI/2);
+ this._isAngleSnapped = true;
+ } else if (azimuth270) {
+ this._azimuth.setTarget(270);
+ this._elevation.setTarget(Math.PI/2);
+ this._isAngleSnapped = true;
+ }
+ }
+ }
+ */
+
+ if (this._isAngleSnapped && this.isUserRotating) {
+ this._azimuthRelief += mouseDelta.dx;
+ this._elevationRelief += mouseDelta.dy;
+
+ if (!between(this._azimuthRelief, -axisSnapRadius, axisSnapRadius) || !between(this._elevationRelief, -axisSnapRadius, axisSnapRadius)) {
+ this._azimuth.setTarget(this._azimuth.getTarget() + this._azimuthRelief * 2);
+ this._elevation.setTarget(this._elevation.getTarget() + this._elevationRelief * 2);
+ this._isAngleSnapped = false;
+ }
+ }
+
+ if (!this._isAngleSnapped) {
+ this._azimuthRelief = 0.0;
+ this._elevationRelief = 0.0;
+ }
+
// Move camera towards target location
this._distance.tick();
this._azimuth.tick();
@@ -86,6 +221,10 @@ export class ArcballCamera {
];
}
+ private _shouldSnapCameraAngle() {
+ return this.isOrthographic() && this._angleSnap;
+ }
+
getCameraPosition(azimuthOffset: number, elevationOffset: number) {
const azimuth = this._azimuth.getActual() + azimuthOffset;
const elevation = this._elevation.getActual() + elevationOffset;
@@ -114,7 +253,12 @@ export class ArcballCamera {
}
public getProjectionMatrix() {
- return m4.perspective(this.fov, this.aspect, this.zNear, this.zFar);
+ if (this._isPerspective) {
+ return m4.perspective(this.fov, this.aspect, this.zNear, this.zFar);
+ } else {
+ const zoom = this._distance.getActual() / 3.6;
+ return m4.ortho(-zoom * this.aspect, zoom * this.aspect, -zoom, zoom, -1000, 1000);
+ }
}
public getCameraMatrix() {
diff --git a/src/config.ts b/src/config.ts
index 7942d36..42c5b2d 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,27 +1,30 @@
// TODO: Replace with UI options
export namespace AppConfig {
- /** Darkens corner even if corner block does not exist, recommended */
+ /** Darkens corner even if corner block does not exist, recommended. */
export const AMBIENT_OCCLUSION_OVERRIDE_CORNER = true;
- /** Enable logging to the console */
+ /** Enable logging to the console. */
export const LOGGING_ENABLED = true;
- /** Enables runtime assertions, useful for debugging */
+ /** Enables runtime assertions, useful for debugging. */
export const ASSERTIONS_ENABLED = true;
- /** Optimises rendering by not rendering triangles facing away from camera's view direction */
+ /** Optimises rendering by not rendering triangles facing away from camera's view direction. */
export const FACE_CULLING = false;
- /** Enables extra runtimes checks that slow execution */
+ /** Enables extra runtimes checks that slow execution. */
export const DEBUG_ENABLED = true;
- /** The number of samples used when sampling a voxel's colour from a textured material */
+ /** The number of samples used when sampling a voxel's colour from a textured material. */
export const MULTISAMPLE_COUNT = 16;
- /** Max size of Node's old space in MBs */
+ /** Max size of Node's old space in MBs. */
export const OLD_SPACE_SIZE = 2048;
- /** This value determines how much more important it is to closely match a block's transparency value than it's colour */
+ /** This value determines how much more important it is to closely match a block's transparency value than its colour. */
export const ALPHA_BIAS = 1.0;
+
+ /** The angle radius (in degrees) around a snapping point the viewport camera must be within to snap. Must be between 0.0 and 90.0 */
+ export const ANGLE_SNAP_RADIUS_DEGREES = 10.0;
}
diff --git a/src/event.ts b/src/event.ts
index 0f1423b..6015619 100644
--- a/src/event.ts
+++ b/src/event.ts
@@ -2,13 +2,6 @@ import { ASSERT, LOG } from './util';
/* eslint-disable */
export enum EAppEvent {
- onModelActiveChanged,
- onModelAvailableChanged,
- onGridEnabledChanged,
- onAxesEnabledChanged,
- onWireframeEnabledChanged,
- onNormalsEnabledChanged,
- onDevViewEnabledChanged,
}
/* eslint-enable */
diff --git a/src/geometry.ts b/src/geometry.ts
index af7bd31..5f61f9c 100644
--- a/src/geometry.ts
+++ b/src/geometry.ts
@@ -219,36 +219,89 @@ export class DebugGeometryTemplates {
return MergeAttributeData(line, cone);
}
- public static grid(dimensions: Vector3, spacing?: number): RenderBuffer {
+ public static COLOUR_MINOR: RGBA = { r: 0.5, g: 0.5, b: 0.5, a: 0.3 };
+ public static COLOUR_MAJOR: RGBA = { r: 1.0, g: 1.0, b: 1.0, a: 0.3 };
+
+ public static gridX(dimensions: Vector3, spacing?: number): RenderBuffer {
+ const buffer = new RenderBuffer([
+ { name: 'position', numComponents: 3 },
+ { name: 'colour', numComponents: 4 },
+ ]);
+
+ buffer.add(DebugGeometryTemplates.line(
+ new Vector3(0, -dimensions.y / 2, -dimensions.z / 2),
+ new Vector3(0, -dimensions.y / 2, dimensions.z / 2),
+ DebugGeometryTemplates.COLOUR_MAJOR,
+ ));
+
+ buffer.add(DebugGeometryTemplates.line(
+ new Vector3(0, dimensions.y / 2, -dimensions.z / 2),
+ new Vector3(0, dimensions.y / 2, dimensions.z / 2),
+ DebugGeometryTemplates.COLOUR_MAJOR,
+ ));
+
+ buffer.add(DebugGeometryTemplates.line(
+ new Vector3(0, -dimensions.y / 2, -dimensions.z / 2),
+ new Vector3(0, dimensions.y / 2, -dimensions.z / 2),
+ DebugGeometryTemplates.COLOUR_MAJOR,
+ ));
+
+ buffer.add(DebugGeometryTemplates.line(
+ new Vector3(0, -dimensions.y / 2, dimensions.z / 2),
+ new Vector3(0, dimensions.y / 2, dimensions.z / 2),
+ DebugGeometryTemplates.COLOUR_MAJOR,
+ ));
+
+ if (spacing) {
+ ASSERT(spacing > 0.0);
+ for (let y = -dimensions.y / 2; y < dimensions.y / 2; y += spacing) {
+ buffer.add(DebugGeometryTemplates.line(
+ new Vector3(0, y, -dimensions.z / 2),
+ new Vector3(0, y, dimensions.z / 2),
+ DebugGeometryTemplates.COLOUR_MINOR,
+ ));
+ }
+
+ for (let z = -dimensions.z / 2; z < dimensions.z / 2; z += spacing) {
+ buffer.add(DebugGeometryTemplates.line(
+ new Vector3(0, -dimensions.y / 2, z),
+ new Vector3(0, dimensions.y / 2, z),
+ DebugGeometryTemplates.COLOUR_MINOR,
+ ));
+ }
+ }
+
+ return buffer;
+ }
+
+ public static gridY(dimensions: Vector3, spacing?: number): RenderBuffer {
const buffer = new RenderBuffer([
{ name: 'position', numComponents: 3 },
{ name: 'colour', numComponents: 4 },
]);
- const COLOUR_MINOR: RGBA = { r: 0.5, g: 0.5, b: 0.5, a: 0.3 };
- const COLOUR_MAJOR: RGBA = { r: 1.0, g: 1.0, b: 1.0, a: 0.3 };
buffer.add(DebugGeometryTemplates.line(
new Vector3(-dimensions.x / 2, 0, -dimensions.z / 2),
new Vector3(-dimensions.x / 2, 0, dimensions.z / 2),
- COLOUR_MAJOR,
+ DebugGeometryTemplates.COLOUR_MAJOR,
));
buffer.add(DebugGeometryTemplates.line(
new Vector3(dimensions.x / 2, 0, -dimensions.z / 2),
new Vector3(dimensions.x / 2, 0, dimensions.z / 2),
- COLOUR_MAJOR,
+ DebugGeometryTemplates.COLOUR_MAJOR,
));
buffer.add(DebugGeometryTemplates.line(
new Vector3(-dimensions.x / 2, 0, -dimensions.z / 2),
new Vector3(dimensions.x / 2, 0, -dimensions.z / 2),
- COLOUR_MAJOR,
+ DebugGeometryTemplates.COLOUR_MAJOR,
));
buffer.add(DebugGeometryTemplates.line(
new Vector3(-dimensions.x / 2, 0, dimensions.z / 2),
new Vector3(dimensions.x / 2, 0, dimensions.z / 2),
- COLOUR_MAJOR,
+ DebugGeometryTemplates.COLOUR_MAJOR,
));
if (spacing) {
@@ -257,7 +310,7 @@ export class DebugGeometryTemplates {
buffer.add(DebugGeometryTemplates.line(
new Vector3(x, 0, -dimensions.z / 2),
new Vector3(x, 0, dimensions.z / 2),
- COLOUR_MINOR,
+ DebugGeometryTemplates.COLOUR_MINOR,
));
}
@@ -265,7 +318,59 @@ export class DebugGeometryTemplates {
buffer.add(DebugGeometryTemplates.line(
new Vector3(-dimensions.x / 2, 0, z),
new Vector3(dimensions.x / 2, 0, z),
- COLOUR_MINOR,
+ DebugGeometryTemplates.COLOUR_MINOR,
+ ));
+ }
+ }
+
+ return buffer;
+ }
+
+ public static gridZ(dimensions: Vector3, spacing?: number): RenderBuffer {
+ const buffer = new RenderBuffer([
+ { name: 'position', numComponents: 3 },
+ { name: 'colour', numComponents: 4 },
+ ]);
+
+ buffer.add(DebugGeometryTemplates.line(
+ new Vector3(-dimensions.x / 2, -dimensions.y / 2, 0),
+ new Vector3(-dimensions.x / 2, dimensions.y / 2, 0),
+ DebugGeometryTemplates.COLOUR_MAJOR,
+ ));
+
+ buffer.add(DebugGeometryTemplates.line(
+ new Vector3(dimensions.x / 2, -dimensions.y / 2, 0),
+ new Vector3(dimensions.x / 2, dimensions.y / 2, 0),
+ DebugGeometryTemplates.COLOUR_MAJOR,
+ ));
+
+ buffer.add(DebugGeometryTemplates.line(
+ new Vector3(-dimensions.x / 2, -dimensions.y / 2, 0),
+ new Vector3(dimensions.x / 2, -dimensions.y / 2, 0),
+ DebugGeometryTemplates.COLOUR_MAJOR,
+ ));
+
+ buffer.add(DebugGeometryTemplates.line(
+ new Vector3(-dimensions.x / 2, dimensions.y / 2, 0),
+ new Vector3(dimensions.x / 2, dimensions.y / 2, 0),
+ DebugGeometryTemplates.COLOUR_MAJOR,
+ ));
+
+ if (spacing) {
+ ASSERT(spacing > 0.0);
+ for (let x = -dimensions.x / 2; x < dimensions.x / 2; x += spacing) {
+ buffer.add(DebugGeometryTemplates.line(
+ new Vector3(x, -dimensions.y / 2, 0),
+ new Vector3(x, dimensions.y / 2, 0),
+ DebugGeometryTemplates.COLOUR_MINOR,
+ ));
+ }
+
+ for (let y = -dimensions.y / 2; y < dimensions.y / 2; y += spacing) {
+ buffer.add(DebugGeometryTemplates.line(
+ new Vector3(-dimensions.x / 2, y, 0),
+ new Vector3(dimensions.x / 2, y, 0),
+ DebugGeometryTemplates.COLOUR_MINOR,
));
}
}
diff --git a/src/math.ts b/src/math.ts
index 4595b09..0c1f2ef 100644
--- a/src/math.ts
+++ b/src/math.ts
@@ -1,6 +1,20 @@
import { AppError, LOG_ERROR } from './util';
import { Vector3 } from './vector';
+export namespace AppMath {
+ export const RADIANS_0 = degreesToRadians(0.0);
+ export const RADIANS_90 = degreesToRadians(90.0);
+ export const RADIANS_180 = degreesToRadians(180.0);
+ export const RADIANS_270 = degreesToRadians(270.0);
+
+ export function nearlyEqual(a: number, b: number, tolerance: number = 0.0001) {
+ return Math.abs(a - b) < tolerance;
+ }
+
+ export function degreesToRadians(degrees: number) {
+ return degrees * (Math.PI / 180.0);
+ }
+}
export const argMax = (array: [number]) => {
return array.map((x, i) => [x, i]).reduce((r, a) => (a[0] > r[0] ? a : r))[1];
@@ -34,6 +48,10 @@ export const roundToNearest = (value: number, base: number) => {
return Math.round(value / base) * base;
};
+export const between = (value: number, min: number, max: number) => {
+ return min <= value && value <= max;
+};
+
export const mapRange = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
return (value - fromMin)/(fromMax - fromMin) * (toMax - toMin) + toMin;
};
diff --git a/src/renderer.ts b/src/renderer.ts
index af941aa..9a7e7aa 100644
--- a/src/renderer.ts
+++ b/src/renderer.ts
@@ -9,7 +9,6 @@ import { VoxelMesh } from './voxel_mesh';
import { BlockMesh } from './block_mesh';
import * as twgl from 'twgl.js';
-import { EAppEvent, EventManager } from './event';
import { RGBA, RGBAUtil } from './colour';
import { Texture } from './texture';
@@ -24,7 +23,6 @@ export enum MeshType {
/* eslint-disable */
enum EDebugBufferComponents {
- Grid,
Wireframe,
Normals,
Bounds,
@@ -59,6 +57,13 @@ export class Renderer {
private _isGridComponentEnabled: { [bufferComponent: string]: boolean };
private _axesEnabled: boolean;
+ private _gridBuffers: {
+ x: { [meshType: string]: RenderBuffer};
+ y: { [meshType: string]: RenderBuffer};
+ z: { [meshType: string]: RenderBuffer};
+ };
+ private _gridEnabled: boolean;
+
private static _instance: Renderer;
public static get Get() {
return this._instance || (this._instance = new this());
@@ -73,6 +78,9 @@ export class Renderer {
this._modelsAvailable = 0;
this._materialBuffers = [];
+ this._gridBuffers = { x: {}, y: {}, z: {} };
+ this._gridEnabled = true;
+
this._debugBuffers = {};
this._debugBuffers[MeshType.None] = {};
this._debugBuffers[MeshType.TriangleMesh] = {};
@@ -80,7 +88,6 @@ export class Renderer {
this._debugBuffers[MeshType.BlockMesh] = {};
this._isGridComponentEnabled = {};
- this._isGridComponentEnabled[EDebugBufferComponents.Grid] = false;
this._axesEnabled = false;
this._axisBuffer = new RenderBuffer([
@@ -117,50 +124,44 @@ export class Renderer {
// /////////////////////////////////////////////////////////////////////////
public toggleIsGridEnabled() {
- const isEnabled = !this._isGridComponentEnabled[EDebugBufferComponents.Grid];
- this._isGridComponentEnabled[EDebugBufferComponents.Grid] = isEnabled;
- EventManager.Get.broadcast(EAppEvent.onGridEnabledChanged, isEnabled);
+ this._gridEnabled = !this._gridEnabled;
+ }
+
+ public isGridEnabled() {
+ return this._gridEnabled;
+ }
+
+ public isAxesEnabled() {
+ return this._axesEnabled;
}
public toggleIsAxesEnabled() {
this._axesEnabled = !this._axesEnabled;
- EventManager.Get.broadcast(EAppEvent.onAxesEnabledChanged, this._axesEnabled);
}
public toggleIsWireframeEnabled() {
const isEnabled = !this._isGridComponentEnabled[EDebugBufferComponents.Wireframe];
this._isGridComponentEnabled[EDebugBufferComponents.Wireframe] = isEnabled;
- EventManager.Get.broadcast(EAppEvent.onWireframeEnabledChanged, isEnabled);
}
public toggleIsNormalsEnabled() {
const isEnabled = !this._isGridComponentEnabled[EDebugBufferComponents.Normals];
this._isGridComponentEnabled[EDebugBufferComponents.Normals] = isEnabled;
- EventManager.Get.broadcast(EAppEvent.onNormalsEnabledChanged, isEnabled);
}
public toggleIsDevDebugEnabled() {
const isEnabled = !this._isGridComponentEnabled[EDebugBufferComponents.Dev];
this._isGridComponentEnabled[EDebugBufferComponents.Dev] = isEnabled;
- EventManager.Get.broadcast(EAppEvent.onDevViewEnabledChanged, isEnabled);
}
public clearMesh() {
- EventManager.Get.broadcast(EAppEvent.onModelAvailableChanged, MeshType.TriangleMesh, false);
- EventManager.Get.broadcast(EAppEvent.onModelAvailableChanged, MeshType.VoxelMesh, false);
- EventManager.Get.broadcast(EAppEvent.onModelAvailableChanged, MeshType.BlockMesh, false);
-
this._materialBuffers = [];
this._modelsAvailable = 0;
this.setModelToUse(MeshType.None);
}
- public useMesh(mesh: Mesh) {
- EventManager.Get.broadcast(EAppEvent.onModelAvailableChanged, MeshType.TriangleMesh, false);
- EventManager.Get.broadcast(EAppEvent.onModelAvailableChanged, MeshType.VoxelMesh, false);
- EventManager.Get.broadcast(EAppEvent.onModelAvailableChanged, MeshType.BlockMesh, false);
-
+ public useMesh(mesh: Mesh) {
LOG('Using mesh');
this._materialBuffers = [];
@@ -255,21 +256,16 @@ export class Renderer {
});
const dimensions = mesh.getBounds().getDimensions();
- this._debugBuffers[MeshType.TriangleMesh][EDebugBufferComponents.Grid] = DebugGeometryTemplates.grid(dimensions);
- // this._debugBuffers[MeshType.TriangleMesh][EDebugBufferComponents.Wireframe] = DebugGeometryTemplates.meshWireframe(mesh, new RGB(0.18, 0.52, 0.89).toRGBA());
- // this._debugBuffers[MeshType.TriangleMesh][EDebugBufferComponents.Normals] = DebugGeometryTemplates.meshNormals(mesh, new RGB(0.89, 0.52, 0.18).toRGBA());
- // delete this._debugBuffers[MeshType.TriangleMesh][EDebugBufferComponents.Dev];
+
+ this._gridBuffers.x[MeshType.TriangleMesh] = DebugGeometryTemplates.gridX(dimensions);
+ this._gridBuffers.y[MeshType.TriangleMesh] = DebugGeometryTemplates.gridY(dimensions);
+ this._gridBuffers.z[MeshType.TriangleMesh] = DebugGeometryTemplates.gridZ(dimensions);
this._modelsAvailable = 1;
this.setModelToUse(MeshType.TriangleMesh);
-
- EventManager.Get.broadcast(EAppEvent.onModelAvailableChanged, MeshType.TriangleMesh, true);
}
public useVoxelMesh(voxelMesh: VoxelMesh, voxelSize: number, ambientOcclusionEnabled: boolean) {
- EventManager.Get.broadcast(EAppEvent.onModelAvailableChanged, MeshType.VoxelMesh, false);
- EventManager.Get.broadcast(EAppEvent.onModelAvailableChanged, MeshType.BlockMesh, false);
-
LOG('Using voxel mesh');
LOG(voxelMesh);
@@ -285,18 +281,15 @@ export class Renderer {
);
dimensions.add(1);
- this._debugBuffers[MeshType.VoxelMesh][EDebugBufferComponents.Grid] = DebugGeometryTemplates.grid(Vector3.mulScalar(dimensions, voxelSize), voxelSize);
- // this._debugBuffers[MeshType.VoxelMesh][EDebugBufferComponents.Wireframe] = DebugGeometryTemplates.voxelMeshWireframe(voxelMesh, new RGB(0.18, 0.52, 0.89).toRGBA(), this._voxelSize);
+ this._gridBuffers.x[MeshType.VoxelMesh] = DebugGeometryTemplates.gridX(Vector3.mulScalar(dimensions, voxelSize), voxelSize);
+ this._gridBuffers.y[MeshType.VoxelMesh] = DebugGeometryTemplates.gridY(Vector3.mulScalar(dimensions, voxelSize), voxelSize);
+ this._gridBuffers.z[MeshType.VoxelMesh] = DebugGeometryTemplates.gridZ(Vector3.mulScalar(dimensions, voxelSize), voxelSize);
this._modelsAvailable = 2;
this.setModelToUse(MeshType.VoxelMesh);
-
- EventManager.Get.broadcast(EAppEvent.onModelAvailableChanged, MeshType.VoxelMesh, true);
}
public useBlockMesh(blockMesh: BlockMesh) {
- EventManager.Get.broadcast(EAppEvent.onModelAvailableChanged, MeshType.BlockMesh, false);
-
LOG('Using block mesh');
LOG(blockMesh);
this._blockBuffer = twgl.createBufferInfoFromArrays(this._gl, blockMesh.createBuffer());
@@ -308,18 +301,17 @@ export class Renderer {
this._atlasSize = blockMesh.getAtlas().getAtlasSize();
- this._debugBuffers[MeshType.BlockMesh][EDebugBufferComponents.Grid] = this._debugBuffers[MeshType.VoxelMesh][EDebugBufferComponents.Grid];
+ this._gridBuffers.y[MeshType.BlockMesh] = this._gridBuffers.y[MeshType.VoxelMesh];
this._modelsAvailable = 3;
this.setModelToUse(MeshType.BlockMesh);
-
- EventManager.Get.broadcast(EAppEvent.onModelAvailableChanged, MeshType.BlockMesh, true);
}
// /////////////////////////////////////////////////////////////////////////
private _drawDebug() {
- const debugComponents = [EDebugBufferComponents.Grid];
+ /*
+ const debugComponents = [EDebugBufferComponents.GridY];
for (const debugComp of debugComponents) {
if (this._isGridComponentEnabled[debugComp]) {
ASSERT(this._debugBuffers[this._meshToUse]);
@@ -328,6 +320,9 @@ export class Renderer {
if (debugComp === EDebugBufferComponents.Dev) {
this._gl.disable(this._gl.DEPTH_TEST);
}
+ if (debugComp === EDebugBufferComponents.GridY && !ArcballCamera.Get.isAlignedWithAxis('y')) {
+ continue;
+ }
this._drawBuffer(this._gl.LINES, buffer.getWebGLBuffer(), ShaderManager.Get.debugProgram, {
u_worldViewProjection: ArcballCamera.Get.getWorldViewProjection(),
});
@@ -335,6 +330,33 @@ export class Renderer {
}
}
}
+ */
+ // Draw grid
+ if (this._gridEnabled) {
+ if (ArcballCamera.Get.isAlignedWithAxis('x') && !ArcballCamera.Get.isAlignedWithAxis('y') && !ArcballCamera.Get.isUserRotating) {
+ const gridBuffer = this._gridBuffers.x[this._meshToUse];
+ if (gridBuffer !== undefined) {
+ this._drawBuffer(this._gl.LINES, gridBuffer.getWebGLBuffer(), ShaderManager.Get.debugProgram, {
+ u_worldViewProjection: ArcballCamera.Get.getWorldViewProjection(),
+ });
+ }
+ } else if (ArcballCamera.Get.isAlignedWithAxis('z') && !ArcballCamera.Get.isAlignedWithAxis('y') && !ArcballCamera.Get.isUserRotating) {
+ const gridBuffer = this._gridBuffers.z[this._meshToUse];
+ if (gridBuffer !== undefined) {
+ this._drawBuffer(this._gl.LINES, gridBuffer.getWebGLBuffer(), ShaderManager.Get.debugProgram, {
+ u_worldViewProjection: ArcballCamera.Get.getWorldViewProjection(),
+ });
+ }
+ } else {
+ const gridBuffer = this._gridBuffers.y[this._meshToUse];
+ if (gridBuffer !== undefined) {
+ this._drawBuffer(this._gl.LINES, gridBuffer.getWebGLBuffer(), ShaderManager.Get.debugProgram, {
+ u_worldViewProjection: ArcballCamera.Get.getWorldViewProjection(),
+ });
+ }
+ }
+ }
+
// Draw axis
if (this._axesEnabled) {
this._gl.disable(this._gl.DEPTH_TEST);
@@ -413,7 +435,6 @@ export class Renderer {
const isModelAvailable = this._modelsAvailable >= meshType;
if (isModelAvailable) {
this._meshToUse = meshType;
- EventManager.Get.broadcast(EAppEvent.onModelActiveChanged, meshType);
}
}
diff --git a/src/ui/elements/toolbar_item.ts b/src/ui/elements/toolbar_item.ts
index 6d552ed..187f50b 100644
--- a/src/ui/elements/toolbar_item.ts
+++ b/src/ui/elements/toolbar_item.ts
@@ -2,7 +2,12 @@ import { ASSERT, getRandomID, STATIC_DIR } from '../../util';
import path from 'path';
import fs from 'fs';
-import { EAppEvent, EventManager } from '../../event';
+
+export type TToolbarBooleanProperty = 'enabled' | 'active';
+
+export type TToolbarItemParams = {
+ icon: string;
+}
export class ToolbarItemElement {
private _id: string;
@@ -10,36 +15,44 @@ export class ToolbarItemElement {
private _iconPath: string;
private _isEnabled: boolean;
private _isActive: boolean;
- private _onClick: () => void;
-
- public constructor(iconName: string, onClick: () => void,
- _activeChangedEvent?: EAppEvent, _activeChangedDelegate?: (...args: any[]) => boolean,
- _enableChangedEvent?: EAppEvent, _enableChangedDelegate?: (...args: any[]) => boolean,
- ) {
- this._id = getRandomID();
- this._iconName = iconName;
- this._iconPath = path.join(STATIC_DIR, iconName + '.svg');
- this._isEnabled = false;
- this._isActive = false;
- this._onClick = onClick;
+ private _onClick?: () => void;
- // Enabled/Disabled Event
- if (_enableChangedEvent !== undefined && _enableChangedDelegate) {
- EventManager.Get.add(_enableChangedEvent, (...args: any[]) => {
- const isEnabled = _enableChangedDelegate(args);
- this.setEnabled(isEnabled);
- });
- } else {
- this._isEnabled = true;
- }
+ public constructor(params: TToolbarItemParams) {
+ this._id = getRandomID();
- // Active/Inactive Event
- if (_activeChangedEvent !== undefined && _activeChangedDelegate) {
- EventManager.Get.add(_activeChangedEvent, (...args: any[]) => {
- const isActive = _activeChangedDelegate(args);
- this.setActive(isActive);
- });
+ this._iconName = params.icon;
+ this._iconPath = path.join(STATIC_DIR, params.icon + '.svg');
+
+ this._isEnabled = true;
+ this._isActive = false;
+ }
+
+ public tick() {
+ if (this._isEnabledDelegate !== undefined) {
+ this.setEnabled(this._isEnabledDelegate());
}
+
+ if (this._isActiveDelegate !== undefined) {
+ this.setActive(this._isActiveDelegate());
+ }
+ }
+
+ private _isActiveDelegate?: () => boolean;
+ public isActive(delegate: () => boolean) {
+ this._isActiveDelegate = delegate;
+ return this;
+ }
+
+ private _isEnabledDelegate?: () => boolean;
+ public isEnabled(delegate: () => boolean) {
+ this._isEnabledDelegate = delegate;
+ return this;
+ }
+
+ public onClick(delegate: () => void) {
+ this._onClick = delegate;
+
+ return this;
}
public generateHTML() {
@@ -56,7 +69,7 @@ export class ToolbarItemElement {
ASSERT(element !== null);
element.addEventListener('click', () => {
- if (this._isEnabled) {
+ if (this._isEnabled && this._onClick) {
this._onClick();
}
});
diff --git a/src/ui/layout.ts b/src/ui/layout.ts
index ead7bf7..9660ff6 100644
--- a/src/ui/layout.ts
+++ b/src/ui/layout.ts
@@ -9,7 +9,6 @@ import { ASSERT, ATLASES_DIR, LOG } from '../util';
import fs from 'fs';
import { ToolbarItemElement } from './elements/toolbar_item';
-import { EAppEvent } from '../event';
import { MeshType, Renderer } from '../renderer';
import { ArcballCamera } from '../camera';
import { TVoxelisers } from '../voxelisers/voxelisers';
@@ -189,119 +188,117 @@ export class UI {
groups: {
'viewmode': {
elements: {
- 'mesh': new ToolbarItemElement('mesh', () => {
- Renderer.Get.setModelToUse(MeshType.TriangleMesh);
- },
- EAppEvent.onModelActiveChanged, (...args: any[]) => {
- const modelUsed = args[0][0][0] as MeshType;
- return modelUsed === MeshType.TriangleMesh;
- },
- EAppEvent.onModelAvailableChanged, (...args: any[]) => {
- const modelType = args[0][0][0] as MeshType;
- const isCached = args[0][0][1] as boolean;
- return modelType >= MeshType.TriangleMesh && isCached;
- }),
-
- 'voxelMesh': new ToolbarItemElement('voxel', () => {
- Renderer.Get.setModelToUse(MeshType.VoxelMesh);
- }, EAppEvent.onModelActiveChanged, (...args: any[]) => {
- const modelUsed = args[0][0][0] as MeshType;
- return modelUsed === MeshType.VoxelMesh;
- }, EAppEvent.onModelAvailableChanged, (...args: any[]) => {
- const modelType = args[0][0][0] as MeshType;
- const isCached = args[0][0][1] as boolean;
- return modelType >= MeshType.VoxelMesh && isCached;
- }),
-
- 'blockMesh': new ToolbarItemElement('block', () => {
- Renderer.Get.setModelToUse(MeshType.BlockMesh);
- }, EAppEvent.onModelActiveChanged, (...args: any[]) => {
- const modelUsed = args[0][0][0] as MeshType;
- return modelUsed === MeshType.BlockMesh;
- }, EAppEvent.onModelAvailableChanged, (...args: any[]) => {
- const modelType = args[0][0][0] as MeshType;
- const isCached = args[0][0][1] as boolean;
- return modelType >= MeshType.BlockMesh && isCached;
- }),
+ 'mesh': new ToolbarItemElement({ icon: 'mesh' })
+ .onClick(() => {
+ Renderer.Get.setModelToUse(MeshType.TriangleMesh);
+ })
+ .isActive(() => {
+ return Renderer.Get.getActiveMeshType() === MeshType.TriangleMesh;
+ })
+ .isEnabled(() => {
+ return Renderer.Get.getModelsAvailable() >= MeshType.TriangleMesh;
+ }),
+ 'voxelMesh': new ToolbarItemElement({ icon: 'voxel' })
+ .onClick(() => {
+ Renderer.Get.setModelToUse(MeshType.VoxelMesh);
+ })
+ .isActive(() => {
+ return Renderer.Get.getActiveMeshType() === MeshType.VoxelMesh;
+ })
+ .isEnabled(() => {
+ return Renderer.Get.getModelsAvailable() >= MeshType.VoxelMesh;
+ }),
+ 'blockMesh': new ToolbarItemElement({ icon: 'block' })
+ .onClick(() => {
+ Renderer.Get.setModelToUse(MeshType.BlockMesh);
+ })
+ .isActive(() => {
+ return Renderer.Get.getActiveMeshType() === MeshType.BlockMesh;
+ })
+ .isEnabled(() => {
+ return Renderer.Get.getModelsAvailable() >= MeshType.BlockMesh;
+ }),
},
elementsOrder: ['mesh', 'voxelMesh', 'blockMesh'],
},
- 'zoom': {
- elements: {
- 'zoomOut': new ToolbarItemElement('minus', () => {
- ArcballCamera.Get.onZoomOut();
- }),
- 'zoomIn': new ToolbarItemElement('plus', () => {
- ArcballCamera.Get.onZoomIn();
- }),
- 'centre': new ToolbarItemElement('centre', () => {
- ArcballCamera.Get.reset();
- }),
- },
- elementsOrder: ['zoomOut', 'zoomIn', 'centre'],
- },
'debug': {
elements: {
- 'grid': new ToolbarItemElement('grid', () => {
- Renderer.Get.toggleIsGridEnabled();
- }, EAppEvent.onGridEnabledChanged, (...args: any[]) => {
- const isEnabled = args[0][0][0] as boolean;
- return isEnabled;
- }, EAppEvent.onModelActiveChanged, (...args: any[]) => {
- return Renderer.Get.getActiveMeshType() !== MeshType.None;
- }),
- 'axes': new ToolbarItemElement('axes', () => {
- Renderer.Get.toggleIsAxesEnabled();
- }, EAppEvent.onAxesEnabledChanged, (...args: any[]) => {
- const isEnabled = args[0][0][0] as boolean;
- return isEnabled;
- }),
+ 'grid': new ToolbarItemElement({ icon: 'grid' })
+ .onClick(() => {
+ Renderer.Get.toggleIsGridEnabled();
+ })
+ .isActive(() => {
+ return Renderer.Get.isGridEnabled();
+ })
+ .isEnabled(() => {
+ return Renderer.Get.getActiveMeshType() !== MeshType.None;
+ }),
+ 'axes': new ToolbarItemElement({ icon: 'axes' })
+ .onClick(() => {
+ Renderer.Get.toggleIsAxesEnabled();
+ })
+ .isActive(() => {
+ return Renderer.Get.isAxesEnabled();
+ }),
},
elementsOrder: ['grid', 'axes'],
},
+
},
- groupsOrder: ['viewmode', 'zoom', 'debug'],
+ groupsOrder: ['viewmode', 'debug'],
};
private _toolbarRight = {
groups: {
- 'debug': {
+ 'zoom': {
elements: {
- /*
- 'wireframe': new ToolbarItemElement('wireframe', () => {
- Renderer.Get.toggleIsWireframeEnabled();
- }, EAppEvent.onWireframeEnabledChanged, (...args: any[]) => {
- const isEnabled = args[0][0][0] as boolean;
- return isEnabled;
- }, EAppEvent.onModelActiveChanged, (...args: any[]) => {
- const modelUsed = args[0][0][0] as MeshType;
- return modelUsed === MeshType.TriangleMesh || modelUsed === MeshType.VoxelMesh;
- }),
- 'normals': new ToolbarItemElement('normal', () => {
- Renderer.Get.toggleIsNormalsEnabled();
- }, EAppEvent.onNormalsEnabledChanged, (...args: any[]) => {
- const isEnabled = args[0][0][0] as boolean;
- return isEnabled;
- }, EAppEvent.onModelActiveChanged, (...args: any[]) => {
- const modelUsed = args[0][0][0] as MeshType;
- return modelUsed === MeshType.TriangleMesh;
- }),
- 'dev': new ToolbarItemElement('debug', () => {
- Renderer.Get.toggleIsDevDebugEnabled();
- }, EAppEvent.onDevViewEnabledChanged, (...args: any[]) => {
- const isEnabled = args[0][0][0] as boolean;
- return isEnabled;
- }, EAppEvent.onModelActiveChanged, (...args: any[]) => {
- const modelUsed = args[0][0][0] as MeshType;
- const devBufferAvailable = Renderer.Get.getModelsAvailable() >= 2;
- return modelUsed === MeshType.TriangleMesh && devBufferAvailable;
- }),
- */
+ 'zoomOut': new ToolbarItemElement({ icon: 'minus' })
+ .onClick(() => {
+ ArcballCamera.Get.onZoomOut();
+ }),
+ 'zoomIn': new ToolbarItemElement({ icon: 'plus' })
+ .onClick(() => {
+ ArcballCamera.Get.onZoomIn();
+ }),
+ 'reset': new ToolbarItemElement({ icon: 'centre' })
+ .onClick(() => {
+ ArcballCamera.Get.reset();
+ }),
},
- elementsOrder: [], // ['wireframe', 'normals', 'dev'],
+ elementsOrder: ['zoomOut', 'zoomIn', 'reset'],
+ },
+ 'camera': {
+ elements: {
+ 'perspective': new ToolbarItemElement({ icon: 'perspective' })
+ .onClick(() => {
+ ArcballCamera.Get.setCameraMode('perspective');
+ })
+ .isActive(() => {
+ return ArcballCamera.Get.isPerspective();
+ }),
+ 'orthographic': new ToolbarItemElement({ icon: 'orthographic' })
+ .onClick(() => {
+ ArcballCamera.Get.setCameraMode('orthographic');
+ })
+ .isActive(() => {
+ return ArcballCamera.Get.isOrthographic();
+ }),
+ 'angleSnap': new ToolbarItemElement({ icon: 'magnet' })
+ .onClick(() => {
+ ArcballCamera.Get.toggleAngleSnap();
+ })
+ .isActive(() => {
+ return ArcballCamera.Get.isAngleSnapEnabled();
+ })
+ .isEnabled(() => {
+ return ArcballCamera.Get.isOrthographic();
+ }),
+
+ },
+ elementsOrder: ['perspective', 'orthographic', 'angleSnap'],
},
},
- groupsOrder: ['debug'],
+ groupsOrder: ['camera', 'zoom'],
};
private _uiDull: { [key: string]: Group } = this._ui;
@@ -317,6 +314,22 @@ export class UI {
this._ui.assign.elements.fallable.addDescription('Read tooltips for more info');
}
+ public tick() {
+ for (const groupName in this._toolbarLeftDull) {
+ const toolbarGroup = this._toolbarLeftDull[groupName];
+ for (const toolbarItem of toolbarGroup.elementsOrder) {
+ toolbarGroup.elements[toolbarItem].tick();
+ }
+ }
+
+ for (const groupName in this._toolbarRightDull) {
+ const toolbarGroup = this._toolbarRightDull[groupName];
+ for (const toolbarItem of toolbarGroup.elementsOrder) {
+ toolbarGroup.elements[toolbarItem].tick();
+ }
+ }
+ }
+
public build() {
const groupHTML: { [key: string]: string } = {};
for (const groupName in this._ui) {
diff --git a/src/util.ts b/src/util.ts
index 12c06ea..532e7ea 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -231,7 +231,7 @@ export class SmoothVariable {
}
public setTarget(target: number) {
- this._target = target;
+ this._target = clamp(target, this._min, this._max);
}
public setActual(actual: number) {