From 3075e8750b5073f6bbd96cf40eb58dce581be78c Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 27 Jul 2018 11:37:16 -0400 Subject: [PATCH 1/2] Allow adjusting camera clipbounds. This was motivated by wanting to set the near and far clip values. This can be done by `map.camera().clipbounds = {near: , far: }`. This simplifies some of the camera code. Before, we were generating a display matrix but separately calculating the transform in a series of steps, and similarly we were doing the inverse via a set of steps instead of a matrix multiplication. This uses the `display` and `world` matrices explicitly in the transforms. The `css` transform property was ambiguous about the transform origin. We now explicitly expect a transform-origin of `0 0`, as we use that elsewhere in the project. This adjusts several tests. Some of the tests have be rewritten for clarity. Some of the camera helper functions have been eliminated as they were simple wrappers around matrix functions. There are now distinct clipbounds for perspective and parallel projections. The css transform format was rewritten to be more compact and not lose precision in some instances. Note that the webgl use of the perspective camera projection will need additional refactoring to handle any clipbounds. --- examples/tiles/index.pug | 2 +- examples/tiles/main.js | 2 +- src/camera.js | 348 ++++++++++++++---------------------- src/canvas/tileLayer.js | 2 +- src/d3/tileLayer.js | 2 +- src/gl/tileLayer.js | 7 +- src/gl/vglRenderer.js | 20 ++- src/tileLayer.js | 5 + tests/cases/camera.js | 184 +++++-------------- tests/cases/pointFeature.js | 4 +- 10 files changed, 209 insertions(+), 367 deletions(-) diff --git a/examples/tiles/index.pug b/examples/tiles/index.pug index a0de4d3d67..9bd60c95fc 100644 --- a/examples/tiles/index.pug +++ b/examples/tiles/index.pug @@ -88,7 +88,7 @@ block append mainContent .form-group(title="The camera can use a parallel or perspective projection. The difference is subtly unless the data has non-zero z-values.") label(for="camera-projection") Camera Projection - select#camera-projection.cameraparam(param-name="projection", placeholder="parallel") + select#camera-projection.cameraparam(param-name="projection", placeholder="parallel", reload="true") option(value="parallel") Parallel option(value="perspective") Perspective diff --git a/examples/tiles/main.js b/examples/tiles/main.js index f242505340..40169f4ad9 100644 --- a/examples/tiles/main.js +++ b/examples/tiles/main.js @@ -349,7 +349,7 @@ $(function () { } break; } - if (ctl.is('.layerparam') && ctl.attr('reload') === 'true') { + if (ctl.is('.layerparam,.cameraparam') && ctl.attr('reload') === 'true') { map.deleteLayer(osmLayer); osmLayer = map.createLayer('osm', layerParams); tileDebug.osmLayer = osmLayer; diff --git a/src/camera.js b/src/camera.js index 9de55a29c1..4a7a570c08 100644 --- a/src/camera.js +++ b/src/camera.js @@ -120,30 +120,10 @@ var camera = function (spec) { * @protected */ this._createProj = function () { - var s = this.constructor.bounds.near / this.constructor.bounds.far; - - // call mat4.frustum or mat4.ortho here - if (this._projection === 'perspective') { - mat4.frustum( - this._proj, - this.constructor.bounds.left * s, - this.constructor.bounds.right * s, - this.constructor.bounds.bottom * s, - this.constructor.bounds.top * s, - -this.constructor.bounds.near, - -this.constructor.bounds.far - ); - } else if (this._projection === 'parallel') { - mat4.ortho( - this._proj, - this.constructor.bounds.left, - this.constructor.bounds.right, - this.constructor.bounds.bottom, - this.constructor.bounds.top, - this.constructor.bounds.near, - this.constructor.bounds.far - ); - } + var func = this._projection === 'perspective' ? mat4.frustum : mat4.ortho, + clipbounds = this._clipbounds[this._projection]; + func(this._proj, clipbounds.left, clipbounds.right, clipbounds.bottom, + clipbounds.top, clipbounds.near, clipbounds.far); }; /** @@ -156,7 +136,7 @@ var camera = function (spec) { this._bounds = null; this._display = null; this._world = null; - this._transform = camera.combine(this._proj, this._view); + this._transform = mat4.multiply(util.mat4AsArray(), this._proj, this._view); mat4.invert(this._inverse, this._transform); this.geoTrigger(geo_event.camera.view, { camera: this @@ -203,40 +183,81 @@ var camera = function (spec) { }); /** - * Getter for the "display" matrix. This matrix converts from - * world coordinates into display coordinates. This matrix exists to - * generate matrix3d css transforms that can be used in layers that - * render on the DOM. Read only. + * Getter/setter for the render clipbounds. Opposite bounds must have + * different values. There are independant clipbounds for each projection + * (parallel and perspective); switching the projection will switch to the + * clipbounds. Individual values of the clipbounds can be set either via a + * command like `camera.clipbounds = {near: 3, far: 1}` or + * `camera.clipbounds.near = 3`. In the second example, no check is made to + * ensure a non-zero volume clipbounds. + * + * @property {object} clipbounds The clipbounds for the current projection. + * @name geo.camera#clipbounds + */ + Object.defineProperty(this, 'clipbounds', { + get: function () { + return this._clipbounds[this._projection]; + }, + set: function (bounds) { + var clipbounds = this._clipbounds[this._projection]; + bounds = { + left: bounds.left === undefined ? clipbounds.left : bounds.left, + right: bounds.right === undefined ? clipbounds.right : bounds.right, + top: bounds.top === undefined ? clipbounds.top : bounds.top, + bottom: bounds.bottom === undefined ? clipbounds.bottom : bounds.bottom, + near: bounds.near === undefined ? clipbounds.near : bounds.near, + far: bounds.far === undefined ? clipbounds.far : bounds.far + }; + if (bounds.left === bounds.right) { + throw new Error('Left and right values must be different'); + } + if (bounds.top === bounds.bottom) { + throw new Error('Top and bottom values must be different'); + } + if (bounds.near === bounds.far) { + throw new Error('Near and far values must be different'); + } + this._clipbounds[this._projection] = bounds; + this._createProj(); + this._update(); + } + }); + + /** + * Getter for the "display" matrix. This matrix converts from world + * coordinates into display coordinates. Read only. * * @property {mat4} display The display matrix. * @name geo.camera#display */ Object.defineProperty(this, 'display', { get: function () { - var mat; if (this._display === null) { - mat = camera.affine( - {x: 1, y: 1}, // translate to: [0, 2] x [0, 2] - { - x: this.viewport.width / 2, - y: this.viewport.height / -2 - } // scale to: [0, width] x [-height, 0] - ); - - // applies mat to the transform (world -> normalized) - this._display = camera.combine( - mat, - this._transform - ); + var b = this._clipbounds[this._projection]; + var mat = util.mat4AsArray(); + mat4.translate(mat, mat, [ + this.viewport.width / 2, + this.viewport.height / 2, + 0]); + mat4.scale(mat, mat, [ + this.viewport.width / (b.right - b.left), + this.viewport.height / (b.bottom - b.top), + 1]); + mat4.translate(mat, mat, [ + -(b.left + b.right) / 2, + -(b.top + b.bottom) / 2, + 0]); + mat4.multiply(mat, mat, this._transform); + this._display = mat; } return this._display; } }); /** - * Getter for the "world" matrix. This matrix converts from - * display coordinates into world coordinates. This is constructed - * by inverting the "display" matrix. Read only. + * Getter for the "world" matrix. This matrix converts from display + * coordinates into world coordinates. This is the inverse of the "display" + * matrix. Read only. * * @property {mat4} world The world matrix. * @name geo.camera#world @@ -422,7 +443,7 @@ var camera = function (spec) { * @returns {vec4} The point in clip space coordinates. */ this._worldToClip4 = function (point) { - return camera.applyTransform(this._transform, point); + return vec4.transformMat4(point, point, this._transform); }; /** @@ -432,7 +453,7 @@ var camera = function (spec) { * @returns {vec4} The point in world space coordinates. */ this._clipToWorld4 = function (point) { - return camera.applyTransform(this._inverse, point); + return vec4.transformMat4(point, point, this._inverse); }; /** @@ -474,92 +495,63 @@ var camera = function (spec) { }; /** - * Project a vec4 from world space into viewport space. - * @param {vec4} point The point in world coordinates (mutated). - * @returns {vec4} The point in display coordinates. + * Project a vector from world space into viewport (display) space. The + * resulting vector always has the last component (`w`) equal to 1. * - * @note For the moment, this computation assumes the following: - * * point[3] > 0 - * * depth range [0, 1] - * - * The clip space z and w coordinates are returned with the window - * x/y coordinates. + * @param {vec2|vec3|vec4} point The point in world coordinates. + * @returns {vec4} The point in display coordinates. */ this.worldToDisplay4 = function (point) { - // This is because z = 0 is the far plane exposed to the user, but - // internally the far plane is at -2. - point[2] -= 2; - - // convert to clip space - this._worldToClip4(point); - - // apply projection specific transformation - point = this.applyProjection(point); - - // convert to display space - point[0] = this._viewport.width * (1 + point[0]) / 2.0; - point[1] = this._viewport.height * (1 - point[1]) / 2.0; - point[2] = (1 + point[2]) / 2.0; + point = [point[0], point[1], point[2] || 0, point[3] || 1]; + point = vec4.transformMat4(point, point, this.display); + if (point[3] && point[3] !== 1) { + point = [point[0] / point[3], point[1] / point[3], point[2] / point[3], 1]; + } return point; }; /** - * Project a vec4 from display space into world space in place. - * @param {vec4} point The point in display coordinates (mutated). - * @returns {vec4} The point in world space coordinates. + * Project a vector from viewport (display) space into world space. The + * resulting vector always has the last component (`w`) equal to 1. * - * @note For the moment, this computation assumes the following: - * * point[3] > 0 - * * depth range [0, 1] + * @param {vec2|vec3|vec4} point The point in display coordinates. + * @returns {vec4} The point in world space coordinates. */ this.displayToWorld4 = function (point) { - // convert to clip space - point[0] = 2 * point[0] / this._viewport.width - 1; - point[1] = -2 * point[1] / this._viewport.height + 1; - point[2] = 2 * point[2] - 1; - - // invert projection transform - point = this.unapplyProjection(point); - - // convert to world coordinates - this._clipToWorld4(point); - - // move far surface to z = 0 - point[2] += 2; + point = [point[0], point[1], point[2] || 0, point[3] || 1]; + point = vec4.transformMat4(point, point, this.world); + if (point[3] && point[3] !== 1) { + point = [point[0] / point[3], point[1] / point[3], point[2] / point[3], 1]; + } return point; }; /** - * Project a point object from world space into viewport space. + * Project a 2D point object from world space into viewport space. `z` is + * set to `-this.clipbounds.near` to scale with the clip space. + * * @param {object} point The point in world coordinates. * @param {number} point.x * @param {number} point.y * @returns {object} The point in display coordinates. */ this.worldToDisplay = function (point) { - // define some magic numbers: - var z = 0, // z coordinate of the surface in world coordinates - w = 1; // enables perspective divide (i.e. for point conversion) - point = this.worldToDisplay4( - [point.x, point.y, z, w] - ); + var b = this._clipbounds[this._projection]; + point = this.worldToDisplay4([point.x, point.y, -b.near]); return {x: point[0], y: point[1]}; }; /** - * Project a point object from viewport space into world space. + * Project a 2D point object from viewport space into world space. `z` is + * set to -1 to scale with the clip space. + * * @param {object} point The point in display coordinates. * @param {number} point.x * @param {number} point.y * @returns {object} The point in world coordinates. */ this.displayToWorld = function (point) { - // define some magic numbers: - var z = 1, // the z coordinate of the surface - w = 2; // perspective divide at z = 1 - point = this.displayToWorld4( - [point.x, point.y, z, w] - ); + point = this.displayToWorld4([point.x, point.y, -1]); return {x: point[0], y: point[1]}; }; @@ -578,10 +570,7 @@ var camera = function (spec) { ul = this.displayToWorld({x: 0, y: 0}); ur = this.displayToWorld({x: this._viewport.width, y: 0}); ll = this.displayToWorld({x: 0, y: this._viewport.height}); - lr = this.displayToWorld({ - x: this._viewport.width, - y: this._viewport.height - }); + lr = this.displayToWorld({x: this._viewport.width, y: this._viewport.height}); bds.left = Math.min(ul.x, ur.x, ll.x, lr.x); bds.bottom = Math.min(ul.y, ur.y, ll.y, lr.y); @@ -768,19 +757,13 @@ var camera = function (spec) { /** * Returns a CSS transform that converts (by default) from world coordinates - * into display coordinates. This allows users of this module to - * position elements using world coordinates directly inside DOM - * elements. + * into display coordinates. This allows users of this module to position + * elements using world coordinates directly inside DOM elements. This + * expects that the transform-origin is 0 0. * - * @note This transform will not take into account projection specific - * transforms. For perspective projections, one can use the properties - * `perspective` and `perspective-origin` to apply the projection - * in css directly. - * - * @param {string} transform The transform to return - * * display - * * world - * @returns {string} The css transform string + * @param {string} [transform='display'] The transform to return. One of + * `display` or `world`. + * @returns {string} The css transform string. */ this.css = function (transform) { var m; @@ -855,6 +838,7 @@ var camera = function (spec) { return this._transform; }; + this._clipbounds = this.constructor.clipbounds; // initialize the view matrix this._resetView(); @@ -881,103 +865,49 @@ camera.projection = { }; /** - * Camera clipping bounds, probably shouldn't be modified. + * Default camera clipping bounds. Some features and renderers may rely on the + * far clip value being more positive than the near clip value. */ -camera.bounds = { - left: -1, - right: 1, - top: 1, - bottom: -1, - far: -2, - near: -1 +camera.clipbounds = { + perspective: { + left: -1, + right: 1, + top: 1, + bottom: -1, + far: 2000, + near: 0.01 + }, + parallel: { + left: -1, + right: 1, + top: 1, + bottom: -1, + far: -1, + near: 1 + } }; /** - * Output a mat4 as a css transform. + * Output a mat4 as a css transform. This expects that the transform-origin is + * 0 0. + * * @param {mat4} t A matrix transform. * @returns {string} A css transform string. */ camera.css = function (t) { - return ( - 'matrix3d(' + - [ - t[0].toFixed(20), - t[1].toFixed(20), - t[2].toFixed(20), - t[3].toFixed(20), - t[4].toFixed(20), - t[5].toFixed(20), - t[6].toFixed(20), - t[7].toFixed(20), - t[8].toFixed(20), - t[9].toFixed(20), - t[10].toFixed(20), - t[11].toFixed(20), - t[12].toFixed(20), - t[13].toFixed(20), - t[14].toFixed(20), - t[15].toFixed(20) - ].join(',') + - ')' - ); -}; - -/** - * Generate a mat4 representing an affine coordinate transformation. - * - * For the following affine transform: - * - * x |-> m * (x + a) + b - * - * applies the css transform: - * - * translate(b) scale(m) translate(a) . - * - * If a parameter is `null` or `undefined`, that component is skipped. - * - * @param {object?} pre Coordinate offset **before** scaling. - * @param {object?} scale Coordinate scaling. - * @param {object?} post Coordinate offset **after** scaling. - * @returns {mat4} The new transform matrix. - */ -camera.affine = function (pre, scale, post) { - var mat = util.mat4AsArray(); - - // Note: mat4 operations are applied to the right side of the current - // transform, so the first applied here is the last applied to the - // coordinate. - if (post) { - mat4.translate(mat, mat, [post.x || 0, post.y || 0, post.z || 0]); - } - if (scale) { - mat4.scale(mat, mat, [scale.x || 1, scale.y || 1, scale.z || 1]); - } - if (pre) { - mat4.translate(mat, mat, [pre.x || 0, pre.y || 0, pre.z || 0]); - } - return mat; -}; - -/** - * Apply the given transform matrix to a point in place. - * @param {mat4} t - * @param {vec4} pt - * @returns {vec4} - */ -camera.applyTransform = function (t, pt) { - return vec4.transformMat4(pt, pt, t); -}; - -/** - * Combine two transforms by multiplying their matrix representations. - * @note The second transform provided will be the first applied in the - * coordinate transform. - * @param {mat4} A - * @param {mat4} B - * @returns {mat4} A * B - */ -camera.combine = function (A, B) { - return mat4.multiply(util.mat4AsArray(), A, B); + return 'matrix3d(' + + t.map(function (val) { + /* Format each value with a certain precision, but don't use scientific + * notation or keep needless trailing zeroes. */ + val = (+val).toPrecision(15); + if (val.indexOf('e') >= 0) { + val = (+val).toString(); + } else if (val.indexOf('.') >= 0) { + val = val.replace(/(\.|)0+$/, ''); + } + return val; + }).join(',') + + ')'; }; inherit(camera, object); diff --git a/src/canvas/tileLayer.js b/src/canvas/tileLayer.js index f3dbbf91e0..9659063d37 100644 --- a/src/canvas/tileLayer.js +++ b/src/canvas/tileLayer.js @@ -34,7 +34,7 @@ var canvas_tileLayer = function () { quad.lr = this.fromLocal(this.fromLevel({ x: bounds.right - to.x, y: bounds.bottom - to.y }, level), 0); - quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * 1e-5; + quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * m_this._levelZIncrement; m_nextTileId += 1; quad.id = m_nextTileId; tile.quadId = quad.id; diff --git a/src/d3/tileLayer.js b/src/d3/tileLayer.js index f85518585d..697470fd30 100644 --- a/src/d3/tileLayer.js +++ b/src/d3/tileLayer.js @@ -29,7 +29,7 @@ var d3_tileLayer = function () { quad.lr = this.fromLocal(this.fromLevel({ x: bounds.right - to.x, y: bounds.bottom - to.y }, level), 0); - quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * 1e-5; + quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * m_this._levelZIncrement; m_nextTileId += 1; quad.id = m_nextTileId; tile.quadId = quad.id; diff --git a/src/gl/tileLayer.js b/src/gl/tileLayer.js index a5d9f83526..28fe4a76f3 100644 --- a/src/gl/tileLayer.js +++ b/src/gl/tileLayer.js @@ -37,7 +37,12 @@ var gl_tileLayer = function () { quad.lr = this.fromLocal(this.fromLevel({ x: bounds.right - to.x, y: bounds.bottom - to.y }, level), 0); - quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * 1e-5; + /* Make sure our level increments are within the clipbounds and ordered so + * that lower levels are farther away that higher levels. */ + var clipbounds = m_this.map().camera().clipbounds; + var z = level * m_this._levelZIncrement; + z = clipbounds.far + (clipbounds.near - clipbounds.far) * z; + quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = z; m_nextTileId += 1; quad.id = m_nextTileId; tile.quadId = quad.id; diff --git a/src/gl/vglRenderer.js b/src/gl/vglRenderer.js index c337e3289c..b7229b36b7 100644 --- a/src/gl/vglRenderer.js +++ b/src/gl/vglRenderer.js @@ -227,14 +227,20 @@ var vglRenderer = function (arg) { view = camera.view, proj = camera.projectionMatrix; if (proj[15]) { - /* we want positive z to be closer to the camera, but webGL does the - * converse, so reverse the z coordinates. */ - proj = mat4.scale(util.mat4AsArray(), proj, [1, 1, -1]); + /* In the parallel projection, we want the clipbounds [near, far] to map + * to [0, 1]. The ortho matrix scales to [-1, 1]. */ + proj = mat4.copy(util.mat4AsArray(), proj); + proj = mat4.scale(proj, proj, [1, 1, -0.5]); + proj = mat4.translate(proj, proj, [0, 0, camera.clipbounds.far]); + } else { + /* This rescales the perspective projection to work with most gl + * features. It doesn't work with all clipbounds, and will probably need + * to be refactored when we have tiltable maps. */ + var near = camera.clipbounds.near, + far = camera.clipbounds.far; + proj = mat4.copy(util.mat4AsArray(), proj); + proj = mat4.scale(proj, proj, [1 / near, 1 / near, -1 / far]); } - /* A similar kluge as in the base camera class worldToDisplay4. With this, - * we can show z values from 0 to 1. */ - proj = mat4.translate(util.mat4AsArray(), proj, - [0, 0, camera.constructor.bounds.far]); /* Check if the rotation is a multiple of 90 */ var basis = Math.PI / 2, angle = rotation % basis, // move to range (-pi/2, pi/2) diff --git a/src/tileLayer.js b/src/tileLayer.js index aeea6093a9..ea316b850e 100644 --- a/src/tileLayer.js +++ b/src/tileLayer.js @@ -196,6 +196,11 @@ var tileLayer = function (options) { m_maxBounds = [], m_exited; + // Space tile levels this far apart in z-buffer space. This value doesn't + // have any visual effect in parallel projections, but will slightly skew + // perspective projections. + this._levelZIncrement = 1e-5; + // copy the options into a private variable this._options = $.extend(true, {}, options); diff --git a/tests/cases/camera.js b/tests/cases/camera.js index f8bb28fee1..7f639ef2fc 100644 --- a/tests/cases/camera.js +++ b/tests/cases/camera.js @@ -4,10 +4,10 @@ var $ = require('jquery'); var mat4 = require('gl-mat4'); -var vec3 = require('gl-vec3'); var vec4 = require('gl-vec4'); var geo = require('../test-utils').geo; var closeToArray = require('../test-utils').closeToArray; +var closeToEqual = require('../test-utils').closeToEqual; describe('geo.camera', function () { 'use strict'; @@ -21,10 +21,10 @@ describe('geo.camera', function () { c.bounds = b; b1 = c.bounds; - expect(b.left).toBe(b1.left); - expect(b.right).toBe(b1.right); - expect(b.top).toBe(b1.top); - expect(b.bottom).toBe(b1.bottom); + expect(b1.left).toBeCloseTo(b.left); + expect(b1.right).toBeCloseTo(b.right); + expect(b1.top).toBeCloseTo(b.top); + expect(b1.bottom).toBeCloseTo(b.bottom); }; } function testcase(proj) { @@ -47,28 +47,13 @@ describe('geo.camera', function () { }); describe('resize viewport', function () { - function number_near(n1, n2, tol) { - return Math.abs(n1 - n2) < tol; - } - function bounds_near(b1, b2, tol) { - tol = tol || 1e-4; - var n = number_near(b1.left, b2.left, tol) && - number_near(b1.right, b2.right, tol) && - number_near(b1.bottom, b2.bottom, tol) && - number_near(b1.top, b2.top, tol); - if (!n) { - console.log(JSON.stringify(b1) + ' != ' + JSON.stringify(b2)); - } - return n; - } - it('100 x 100 -> 90 x 90', function () { var c = geo.camera({viewport: {width: 100, height: 100}}); c.bounds = {left: 0, right: 100, bottom: 0, top: 100}; c.viewport = {width: 90, height: 90}; - expect(bounds_near(c.bounds, {left: 5, right: 95, bottom: 5, top: 95})) + expect(closeToEqual(c.bounds, {left: 5, right: 95, bottom: 5, top: 95}, 4)) .toBe(true); }); it('100 x 100 -> 100 x 90', function () { @@ -77,7 +62,7 @@ describe('geo.camera', function () { c.bounds = {left: 0, right: 100, bottom: 0, top: 100}; c.viewport = {width: 100, height: 90}; - expect(bounds_near(c.bounds, {left: 0, right: 100, bottom: 5, top: 95})) + expect(closeToEqual(c.bounds, {left: 0, right: 100, bottom: 5, top: 95}, 4)) .toBe(true); }); it('100 x 100 -> 20 x 10', function () { @@ -86,7 +71,7 @@ describe('geo.camera', function () { c.bounds = {left: 0, right: 100, bottom: 0, top: 100}; c.viewport = {width: 20, height: 10}; - expect(bounds_near(c.bounds, {left: 40, right: 60, bottom: 45, top: 55})) + expect(closeToEqual(c.bounds, {left: 40, right: 60, bottom: 45, top: 55}, 4)) .toBe(true); }); it('100 x 100 -> 140 x 120', function () { @@ -95,7 +80,7 @@ describe('geo.camera', function () { c.bounds = {left: 0, right: 100, bottom: 0, top: 100}; c.viewport = {width: 140, height: 120}; - expect(bounds_near(c.bounds, {left: -20, right: 120, bottom: -10, top: 110})) + expect(closeToEqual(c.bounds, {left: -20, right: 120, bottom: -10, top: 110}, 4)) .toBe(true); }); it('50 x 100 -> 100 x 100', function () { @@ -104,7 +89,7 @@ describe('geo.camera', function () { c.bounds = {left: 0, right: 50, bottom: 0, top: 100}; c.viewport = {width: 100, height: 100}; - expect(bounds_near(c.bounds, {left: -25, right: 75, bottom: 0, top: 100})) + expect(closeToEqual(c.bounds, {left: -25, right: 75, bottom: 0, top: 100}, 4)) .toBe(true); }); it('50 x 50 -> 100 x 100', function () { @@ -113,7 +98,7 @@ describe('geo.camera', function () { c.bounds = {left: 0, right: 50, bottom: 0, top: 50}; c.viewport = {width: 100, height: 100}; - expect(bounds_near(c.bounds, {left: -25, right: 75, bottom: -25, top: 75})) + expect(closeToEqual(c.bounds, {left: -25, right: 75, bottom: -25, top: 75}, 4)) .toBe(true); }); }); @@ -364,6 +349,7 @@ describe('geo.camera', function () { position: 'absolute', width: '100px', height: '100px', + 'transform-origin': '0 0', transform: 'none' /*,border: '1px solid black'*/ }); @@ -380,21 +366,15 @@ describe('geo.camera', function () { bottom: node.position().top + box.height, top: node.position().top, left: node.position().left, - right: node.position().left + box.width, - height: box.height, - width: box.width + right: node.position().left + box.width }; } function assert_position(position) { - var _ = get_node_position(), k, actual = {}; - for (k in position) { - if (position.hasOwnProperty(k)) { - position[k] = position[k].toFixed(2); - actual[k] = _[k].toFixed(2); - } - } - expect(actual).toEqual(position); + if (!closeToEqual(get_node_position(), position, 2)) { //DWM:: + console.log('assert_position', get_node_position(), position); //DWM:: + } //DWM:: + expect(closeToEqual(get_node_position(), position, 2)).toBe(true); } it('Display and world parameters', function () { @@ -413,24 +393,19 @@ describe('geo.camera', function () { camera = geo.camera(); camera.viewport = {width: 100, height: 100}; - camera.bounds = geo.camera.bounds; - camera.pan({x: 10, y: 0}); + camera.bounds = geo.camera.clipbounds.parallel; + camera.pan({x: 10, y: 0}); node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(10); - expect(node.position().top).toBe(0); + assert_position({left: 10, top: 0, right: 110, bottom: 100}); camera.pan({x: 0, y: -5}); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(10); - expect(node.position().top).toBe(-5); + assert_position({left: 10, top: -5, right: 110, bottom: 95}); camera.pan({x: -10, y: 5}); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(0); - expect(node.position().top).toBe(0); + assert_position({left: 0, top: 0, right: 100, bottom: 100}); }); it('Simple zooming', function () { var node = make_node(), @@ -438,50 +413,40 @@ describe('geo.camera', function () { view; camera.viewport = {width: 100, height: 100}; - camera.bounds = geo.camera.bounds; + camera.bounds = geo.camera.clipbounds.parallel; view = camera.view; camera.zoom(1); expect(camera.view).toBe(view); camera.zoom(0.5); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(25); - expect(node.position().top).toBe(25); + assert_position({left: 0, top: 0, right: 50, bottom: 50}); camera.zoom(6); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(-100); - expect(node.position().top).toBe(-100); + assert_position({left: 0, top: 0, right: 300, bottom: 300}); camera.zoom(1 / 3); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(0); - expect(node.position().top).toBe(0); + assert_position({left: 0, top: 0, right: 100, bottom: 100}); }); it('Zooming + panning', function () { var node = make_node(), camera = geo.camera(); camera.viewport = {width: 100, height: 100}; - camera.bounds = geo.camera.bounds; + camera.bounds = geo.camera.clipbounds.parallel; camera.pan({x: -25, y: -25}); camera.zoom(0.5); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(0); - expect(node.position().top).toBe(0); + assert_position({left: -25, top: -25, right: 25, bottom: 25}); camera.pan({x: 50, y: 50}); camera.zoom(2); - node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBe(0); - expect(node.position().top).toBe(0); + assert_position({left: 0, top: 0, right: 100, bottom: 100}); }); it('Simple rotation', function () { var node = make_node(), @@ -489,7 +454,7 @@ describe('geo.camera', function () { view; camera.viewport = {width: 100, height: 100}; - camera.bounds = geo.camera.bounds; + camera.bounds = geo.camera.clipbounds.parallel; view = camera.view; camera._rotate(0); @@ -497,18 +462,15 @@ describe('geo.camera', function () { camera._rotate(30 * Math.PI / 180); node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBeLessThan(-18); - expect(node.position().top).toBeLessThan(-18); + assert_position({left: 0, top: -50, right: 136.60, bottom: 86.60}); camera._rotate(-30 * Math.PI / 180); node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBeCloseTo(0, 4); - expect(node.position().top).toBeCloseTo(0, 4); + assert_position({left: 0, top: 0, right: 100, bottom: 100}); camera._rotate(-30 * Math.PI / 180, {x: 50, y: 0}); node.css('transform', geo.camera.css(camera.view)); - expect(node.position().left).toBeLessThan(-10); - expect(node.position().top).toBeLessThan(-40); + assert_position({left: -43.30, top: -25, right: 93.30, bottom: 111.60}); }); describe('World to display', function () { @@ -520,12 +482,7 @@ describe('geo.camera', function () { camera.bounds = {left: 0, right: 100, bottom: 0, top: 100}; node.css('transform', camera.css()); - assert_position({ - left: 0, - right: 100, - top: 0, - bottom: 100 - }); + assert_position({left: 0, top: 0, right: 100, bottom: 100}); }); it('zoom', function () { @@ -537,21 +494,11 @@ describe('geo.camera', function () { camera.zoom(2); node.css('transform', camera.css()); - assert_position({ - left: -50, - top: -50, - right: 150, - bottom: 150 - }); + assert_position({left: 0, top: -100, right: 200, bottom: 100}); camera.zoom(1 / 4); node.css('transform', camera.css()); - assert_position({ - left: 25, - top: 25, - right: 75, - bottom: 75 - }); + assert_position({left: 0, top: 50, right: 50, bottom: 100}); }); it('pan', function () { @@ -563,21 +510,11 @@ describe('geo.camera', function () { camera.pan({x: 50, y: 0}); node.css('transform', camera.css()); - assert_position({ - left: 50, - top: 0, - right: 150, - bottom: 100 - }); + assert_position({left: 50, top: 0, right: 150, bottom: 100}); camera.pan({x: -25, y: 10}); node.css('transform', camera.css()); - assert_position({ - left: 25, - top: -10, - right: 125, - bottom: 90 - }); + assert_position({left: 25, top: -10, right: 125, bottom: 90}); }); it('pan + zoom', function () { var node = make_node(), @@ -589,21 +526,11 @@ describe('geo.camera', function () { camera.zoom(2); camera.pan({x: 10, y: -5}); node.css('transform', camera.css()); - assert_position({ - left: -30, - top: -40, - right: 170, - bottom: 160 - }); + assert_position({left: 20, top: -90, right: 220, bottom: 110}); camera.zoom(0.5); node.css('transform', camera.css()); - assert_position({ - left: 20, - top: 10, - right: 120, - bottom: 110 - }); + assert_position({left: 20, top: 10, right: 120, bottom: 110}); }); }); }); @@ -780,37 +707,6 @@ describe('geo.camera', function () { expect(c.toString()).toEqual(c.ppMatrix(c.transform)); }); - it('Affine transform generator', function () { - var t, v; - - t = geo.camera.affine( - {x: 10, y: 20, z: 30}, - {x: 2, y: 3, z: 4}, - {x: -10, y: -20, z: -30} - ); - - v = vec3.transformMat4( - vec3.create(), - vec3.fromValues(1, 0, 0), - t - ); - expect(v).toEqual(vec3.fromValues(12, 40, 90)); - - v = vec3.transformMat4( - vec3.create(), - vec3.fromValues(0, 1, 0), - t - ); - expect(v).toEqual(vec3.fromValues(10, 43, 90)); - - v = vec3.transformMat4( - vec3.create(), - vec3.fromValues(0, 0, 1), - t - ); - expect(v).toEqual(vec3.fromValues(10, 40, 94)); - }); - it('Unknown transform error', function () { var c = geo.camera(); expect(function () { c.css('not-valid'); }).toThrow(); diff --git a/tests/cases/pointFeature.js b/tests/cases/pointFeature.js index 90bde8141e..8050be2684 100644 --- a/tests/cases/pointFeature.js +++ b/tests/cases/pointFeature.js @@ -248,8 +248,8 @@ describe('geo.pointFeature', function () { stepAnimationFrame(); var circles = layer.node().find('circle'); expect(circles.length).toBe(19); - expect(circles.eq(0).attr('r')).toBe('5'); - expect(circles.eq(12).attr('r')).toBe('10'); + expect(circles.eq(0).attr('r')).toBeCloseTo('5'); + expect(circles.eq(12).attr('r')).toBeCloseTo('10'); unmockAnimationFrame(); }); }); From 91a0a1f5f4d1ef98247e2071aa5b4f5e3dbc3408 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 16 Aug 2018 15:54:41 -0400 Subject: [PATCH 2/2] Add more tests. --- tests/cases/camera.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/cases/camera.js b/tests/cases/camera.js index 7f639ef2fc..d2eadda4ff 100644 --- a/tests/cases/camera.js +++ b/tests/cases/camera.js @@ -717,4 +717,21 @@ describe('geo.camera', function () { expect(function () { c.viewport = {width: 0, height: 100}; }).toThrow(); expect(function () { c.viewport = {width: 100, height: -100}; }).toThrow(); }); + + it('clipbounds', function () { + var c = geo.camera(); + + expect(c.clipbounds.near).toBe(1); + c.clipbounds = {near: 2, far: -2}; + expect(c.clipbounds.near).toBe(2); + expect(function () { c.clipbounds = {near: 2, far: 2}; }).toThrow(); + expect(function () { c.clipbounds = {left: 2, right: 2}; }).toThrow(); + expect(function () { c.clipbounds = {top: 2, bottom: 2}; }).toThrow(); + c.projection = 'perspective'; + expect(c.clipbounds.near).toBe(0.01); + c.clipbounds = {near: 0.1, far: 1000}; + expect(c.clipbounds.near).toBe(0.1); + c.projection = 'parallel'; + expect(c.clipbounds.near).toBe(2); + }); });