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) {