@geoblocks/cesium-iso 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +50 -2
  3. package/cesium-iso.js +554 -0
  4. package/package.json +23 -4
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2020-present, camptocamp SA
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -1,3 +1,51 @@
1
- # Temporary Holding Version
1
+ # Cesium Iso
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An isometric view for CesiumJS, as in a game: an orthographic camera at a fixed pitch, looking from a corner.
4
+
5
+ - a drag turns the view; a right drag, or Ctrl or Shift with the left, slides it across the ground
6
+ - the wheel zooms where it points
7
+ - on a touch screen, one finger slides, two pinch to zoom, slide, and twist to turn
8
+ - Q and E turn it a quarter, to the next corner; WASD or the arrows slide it while held
9
+ - a double click glides to the spot
10
+ - let go while moving, it drifts on and slows down
11
+
12
+ Cesium's own camera inputs are off while it is active.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm i --save @geoblocks/cesium-iso
18
+ ```
19
+
20
+ ## Demo
21
+
22
+ [Demo](https://geoblocks.github.io/cesium-helpers/cesium-iso.html), with `Toy` (lit and shaded edges, warmer colors,
23
+ a gradient for the sky), `TiltShift` and `LensDistortion`'s vignette from [cesium-post-process](../cesium-post-process)
24
+ for a game's look.
25
+
26
+ ## Usage
27
+
28
+ ```javascript
29
+ import CesiumIso from '@geoblocks/cesium-iso';
30
+
31
+ const iso = new CesiumIso(viewer, {pitch: 30, heading: 45, size: 600, minSize: 20, maxSize: 8000});
32
+ iso.active = true;
33
+
34
+ iso.turn(1); // a quarter turn, as E (-1 as Q)
35
+ iso.turnTo(200); // to a heading, in degrees, the shorter way
36
+ iso.zoom(0.8); // in, toward the middle of the screen
37
+ iso.glideTo(position);
38
+ iso.target = position; // at once
39
+ ```
40
+
41
+ ## Options
42
+
43
+ | Name | Default | Description
44
+ | --------- | ------- | -----------
45
+ | `pitch` | `30` | Degrees down from the horizon
46
+ | `heading` | `45` | A corner's heading, in degrees, the others a quarter turn apart: square to a place's streets
47
+ | `size` | `400` | Meters across the screen's shorter side
48
+ | `minSize` | `20` | The closest zoom, in meters
49
+ | `maxSize` | `8000` | The farthest zoom, in meters
50
+ | `limit` | | A function moving a target back where the view may stand: it stops sliding there
51
+ | `orbit` | `5000` | Meters from the target to the camera, beyond the scene; closer for small scenes, where faces a fraction of a millimeter apart would fight at 5 km
package/cesium-iso.js ADDED
@@ -0,0 +1,554 @@
1
+ import {Cartesian2, Cartesian3, Cartographic, FixedFrameTransforms, IntersectionTests, Math as CesiumMath, Matrix4, Plane} from '@cesium/core';
2
+
3
+ // in meters, how far the camera stays from its target by default, out of the scene's way: an orthographic view's
4
+ // size does not depend on it, its depth's precision does (32-bit floats: half a millimeter at 5 km)
5
+ const ORBIT = 5000;
6
+ // the keys, as in the Sims: Q and E turn, WASD or the arrows slide across the ground while held, [x, y] on the screen
7
+ /** @type {Record<string, [number, number]>} */
8
+ const PAN = {w: [0, 1], arrowup: [0, 1], s: [0, -1], arrowdown: [0, -1], a: [-1, 0], arrowleft: [-1, 0], d: [1, 0], arrowright: [1, 0]};
9
+ // how fast the keys slide the view, a share of the screen's width a second; how long a turn and a glide take, in ms;
10
+ // how far two fingers turn before the view turns with them, so that a pinch does not turn it
11
+ const PAN_SPEED = 0.4;
12
+ const TURN_TIME = 450;
13
+ const GLIDE_TIME = 400;
14
+ const TWIST = CesiumMath.toRadians(15);
15
+ // after a drag, the view drifts on, slowing down by this share of its speed a frame (as three.js's OrbitControls' damping
16
+ // of 0.05), if let go within LET_GO ms of the last move
17
+ const DAMPING = 0.95;
18
+ const LET_GO = 60;
19
+
20
+ const easeInOut = (/** @type {number} */ t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
21
+ const positionScratch = new Cartesian2();
22
+ const planeScratch = new Plane(Cartesian3.UNIT_Z, 0);
23
+ const normalScratch = new Cartesian3();
24
+ const deltaScratch = new Cartesian3();
25
+ const frameScratch = new Matrix4();
26
+ const directionScratch = new Cartesian3();
27
+ const upScratch = new Cartesian3();
28
+ const orbitScratch = new Cartesian3();
29
+
30
+ /**
31
+ * An isometric view, as in a game: an orthographic camera at a fixed pitch, looking from a corner. A drag turns the
32
+ * view, a right drag (or Ctrl or Shift with the left) and one finger slide it across the ground, the wheel and a pinch
33
+ * zoom where they point, two fingers twist it; Q and E turn it a quarter, WASD and the arrows slide it, a double click
34
+ * glides to the spot.
35
+ */
36
+ export default class CesiumIso {
37
+ /**
38
+ * @param {import('@cesium/engine').CesiumWidget} viewer
39
+ * @param {{pitch?: number, heading?: number, size?: number, minSize?: number, maxSize?: number,
40
+ * limit?: (target: Cartesian3) => Cartesian3, orbit?: number}} [options] The pitch, in degrees down from the horizon; a
41
+ * corner's heading, in degrees, the others a quarter turn apart; the size, in meters across the screen's shorter side,
42
+ * and its bounds; a limit, a target moved back where the view may stand: it stops sliding there; the orbit, in meters
43
+ * from the target to the camera, beyond the scene: closer, faces a fraction of a millimeter apart do not fight.
44
+ */
45
+ constructor(viewer, options = {}) {
46
+ this.viewer = viewer;
47
+ this.pitch_ = CesiumMath.toRadians(options.pitch ?? 30);
48
+ this.corner_ = CesiumMath.toRadians(options.heading ?? 45);
49
+ this.size_ = options.size ?? 400;
50
+ this.minSize_ = options.minSize ?? 20;
51
+ this.maxSize_ = options.maxSize ?? 8000;
52
+ this.limit_ = options.limit;
53
+ this.orbit_ = options.orbit ?? ORBIT;
54
+ this.active_ = false;
55
+ /** @type {Cartesian3} */
56
+ this.target_ = new Cartesian3();
57
+ this.heading_ = 0;
58
+ /** @type {Map<number, {x: number, y: number}>} */
59
+ this.pointers_ = new Map();
60
+ /** @type {{grab?: Cartesian3 | undefined, turn?: boolean, last?: {x: number, y: number}, distance?: number, angle?: number, twist?: number} | undefined} */
61
+ this.drag_ = undefined;
62
+ /** @type {Set<string>} */
63
+ this.held_ = new Set();
64
+ /** @type {{from: number, to: number, start: number} | undefined} */
65
+ this.turning_ = undefined;
66
+ /** @type {{from: Cartesian3, to: Cartesian3, start: number} | undefined} */
67
+ this.gliding_ = undefined;
68
+ /** @type {number | undefined} */
69
+ this.frame_ = undefined;
70
+ /** @type {number | undefined} */
71
+ this.slid_ = undefined;
72
+ /** @type {(() => void) | undefined} */
73
+ this.settling_ = undefined;
74
+ // the view's last move during a drag, and how fast it moved then: turning in radians, sliding in meters, a ms
75
+ /** @type {{time: number, heading: number, target: Cartesian3, turn: number, slide: Cartesian3} | undefined} */
76
+ this.moved_ = undefined;
77
+ /** @type {{turn: number, slide: Cartesian3} | undefined} */
78
+ this.drift_ = undefined;
79
+ /** @type {number | undefined} */
80
+ this.drifted_ = undefined;
81
+ // the camera's frustum before, given back on deactivation
82
+ /** @type {any} */
83
+ this.frustum_ = undefined;
84
+
85
+ this.listeners_ = /** @type {[EventTarget, string, (e: any) => void][]} */ ([
86
+ [viewer.scene.canvas, 'pointerdown', e => this.down_(e)],
87
+ [viewer.scene.canvas, 'pointermove', e => this.move_(e)],
88
+ [viewer.scene.canvas, 'pointerup', e => this.up_(e)],
89
+ [viewer.scene.canvas, 'pointercancel', e => this.up_(e)],
90
+ [viewer.scene.canvas, 'wheel', e => this.wheel_(e)],
91
+ [viewer.scene.canvas, 'dblclick', e => this.glideTo(this.pick_(this.at_(e)))],
92
+ [viewer.scene.canvas, 'contextmenu', e => e.preventDefault()],
93
+ [window, 'keydown', e => this.keydown_(e)],
94
+ [window, 'keyup', e => this.held_.delete(e.key.toLowerCase())],
95
+ [window, 'blur', () => this.held_.clear()],
96
+ [window, 'resize', () => this.place_()],
97
+ ]);
98
+ }
99
+
100
+ get active() {
101
+ return this.active_;
102
+ }
103
+
104
+ set active(active) {
105
+ if (active === this.active_) {
106
+ return;
107
+ }
108
+ this.active_ = active;
109
+ const {scene} = this.viewer;
110
+ scene.screenSpaceCameraController.enableInputs = !active;
111
+ for (const [target, type, listener] of this.listeners_) {
112
+ if (active) {
113
+ target.addEventListener(type, listener, type === 'wheel' ? {passive: false} : undefined);
114
+ } else {
115
+ target.removeEventListener(type, listener);
116
+ }
117
+ }
118
+ if (active) {
119
+ // about the ground in the middle of the screen, from the nearest corner; before the scene has loaded, the
120
+ // ellipsoid there, raised to the terrain once its tiles are in, if the globe is shown
121
+ const {canvas, camera, globe} = scene;
122
+ const middle = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2);
123
+ const picked = this.pick_(middle);
124
+ this.target_ = picked ?? camera.pickEllipsoid(middle, scene.ellipsoid) ?? Cartesian3.clone(camera.positionWC);
125
+ this.heading_ = this.cornerAfter_(camera.heading, 0);
126
+ this.frustum_ = camera.frustum.clone();
127
+ // off any transform the camera was fixed to (once: Cesium reads the depth back here)
128
+ camera.lookAtTransform(Matrix4.IDENTITY);
129
+ camera.switchToOrthographicFrustum();
130
+ this.place_();
131
+ if (!picked && globe?.show) {
132
+ this.settling_ = globe.tileLoadProgressEvent.addEventListener(queued => {
133
+ const spot = Cartographic.fromCartesian(this.target_, scene.ellipsoid);
134
+ const height = queued ? undefined : globe.getHeight(spot);
135
+ if (height !== undefined) {
136
+ this.settle_();
137
+ this.target_ = Cartesian3.fromRadians(spot.longitude, spot.latitude, height, scene.ellipsoid);
138
+ this.place_();
139
+ }
140
+ });
141
+ }
142
+ } else {
143
+ this.settle_();
144
+ cancelAnimationFrame(this.frame_ ?? 0);
145
+ this.frame_ = this.turning_ = this.gliding_ = this.drag_ = this.moved_ = this.drift_ = this.drifted_ = undefined;
146
+ this.pointers_.clear();
147
+ this.held_.clear();
148
+ const {canvas} = scene;
149
+ if (this.frustum_.aspectRatio !== undefined) {
150
+ this.frustum_.aspectRatio = canvas.clientWidth / canvas.clientHeight;
151
+ }
152
+ scene.camera.frustum = this.frustum_;
153
+ scene.requestRender();
154
+ }
155
+ }
156
+
157
+ /**
158
+ * What the view turns about, in the middle of the screen, a copy; set once active, the view moves there at once.
159
+ */
160
+ get target() {
161
+ return Cartesian3.clone(this.target_);
162
+ }
163
+
164
+ set target(position) {
165
+ this.settle_();
166
+ this.target_ = Cartesian3.clone(position);
167
+ if (this.active_) {
168
+ this.place_();
169
+ }
170
+ }
171
+
172
+ /**
173
+ * No more waiting for the terrain under the first target.
174
+ */
175
+ settle_() {
176
+ this.settling_?.();
177
+ this.settling_ = undefined;
178
+ }
179
+
180
+ /**
181
+ * The view's heading, in degrees, clockwise from the north, where the camera looks.
182
+ */
183
+ get heading() {
184
+ return CesiumMath.toDegrees(this.heading_);
185
+ }
186
+
187
+ /**
188
+ * A turn, eased, to a heading, in degrees, the shorter way round: the view toward a side a place is seen from.
189
+ * @param {number} heading
190
+ */
191
+ turnTo(heading) {
192
+ this.drift_ = this.drifted_ = undefined;
193
+ this.anchor_();
194
+ const to = CesiumMath.toRadians(heading);
195
+ const delta = Math.atan2(Math.sin(to - this.heading_), Math.cos(to - this.heading_));
196
+ this.turning_ = {from: this.heading_, to: this.heading_ + delta, start: performance.now()};
197
+ this.animate_();
198
+ }
199
+
200
+ /**
201
+ * A quarter turn, eased, to the next corner: the camera clockwise about the middle, seen from above (-1, as Q), or
202
+ * the other way (1, as E); 0 to the nearest.
203
+ * @param {number} sign
204
+ */
205
+ turn(sign) {
206
+ this.drift_ = this.drifted_ = undefined;
207
+ this.anchor_();
208
+ this.turning_ = {from: this.heading_, to: this.cornerAfter_(this.heading_, -sign), start: performance.now()};
209
+ this.animate_();
210
+ }
211
+
212
+ /**
213
+ * Zooms in (a factor below 1) or out, toward a point on the screen, by default its middle.
214
+ * @param {number} factor
215
+ * @param {Cartesian2} [at]
216
+ */
217
+ zoom(factor, at) {
218
+ const {canvas} = this.viewer.scene;
219
+ const point = at ?? new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2);
220
+ const before = this.ground_(point, this.target_);
221
+ this.size_ = CesiumMath.clamp(this.size_ * factor, this.minSize_, this.maxSize_);
222
+ this.place_();
223
+ if (before) {
224
+ this.keep_(before, point);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Glides the view's middle to a position, eased.
230
+ * @param {Cartesian3 | undefined} position
231
+ */
232
+ glideTo(position) {
233
+ if (!position) {
234
+ return;
235
+ }
236
+ this.drift_ = this.drifted_ = undefined;
237
+ this.gliding_ = {from: Cartesian3.clone(this.target_), to: Cartesian3.clone(position), start: performance.now()};
238
+ this.animate_();
239
+ }
240
+
241
+ /**
242
+ * The camera on its orbit about the target, the frustum as wide as the size on the shorter side. Set straight: in
243
+ * the target's east-north-up frame, looking along the heading, down by the pitch; Cesium's lookAt and setView would
244
+ * read the depth under the middle back from the GPU to size an orthographic frustum, a stall at each move.
245
+ */
246
+ place_() {
247
+ if (!this.active_) {
248
+ return;
249
+ }
250
+ if (this.limit_) {
251
+ this.target_ = Cartesian3.clone(this.limit_(this.target_), this.target_);
252
+ }
253
+ const {camera, ellipsoid} = this.viewer.scene;
254
+ const [sh, ch, sp, cp] = [Math.sin(this.heading_), Math.cos(this.heading_), Math.sin(this.pitch_), Math.cos(this.pitch_)];
255
+ const frame = FixedFrameTransforms.eastNorthUpToFixedFrame(this.target_, ellipsoid, frameScratch);
256
+ const direction = Matrix4.multiplyByPointAsVector(frame, Cartesian3.fromElements(sh * cp, ch * cp, -sp, directionScratch), directionScratch);
257
+ const up = Matrix4.multiplyByPointAsVector(frame, Cartesian3.fromElements(sh * sp, ch * sp, cp, upScratch), upScratch);
258
+ Matrix4.multiplyByPoint(frame, Cartesian3.fromElements(-sh * cp * this.orbit_, -ch * cp * this.orbit_, sp * this.orbit_, orbitScratch), camera.position);
259
+ Cartesian3.clone(direction, camera.direction);
260
+ Cartesian3.clone(up, camera.up);
261
+ Cartesian3.cross(direction, up, camera.right);
262
+ // the orthographic frustum's width
263
+ Object.assign(camera.frustum, {width: this.width_()});
264
+ this.viewer.scene.requestRender();
265
+ }
266
+
267
+ /**
268
+ * The view's width, in meters: the size on the screen's shorter side.
269
+ */
270
+ width_() {
271
+ const {canvas} = this.viewer.scene;
272
+ return this.size_ * Math.max(1, canvas.clientWidth / canvas.clientHeight);
273
+ }
274
+
275
+ /**
276
+ * The heading of the corner after the nearest, by a sign.
277
+ * @param {number} heading
278
+ * @param {number} sign
279
+ */
280
+ cornerAfter_(heading, sign) {
281
+ const quarter = Math.PI / 2;
282
+ return this.corner_ + (Math.round((heading - this.corner_) / quarter) + sign) * quarter;
283
+ }
284
+
285
+ /**
286
+ * A point of the canvas, from a pointer event.
287
+ * @param {MouseEvent} e
288
+ */
289
+ at_(e) {
290
+ const rect = this.viewer.scene.canvas.getBoundingClientRect();
291
+ return new Cartesian2(e.clientX - rect.left, e.clientY - rect.top);
292
+ }
293
+
294
+ /**
295
+ * What is seen at a point of the canvas: the scene, else the globe, if shown.
296
+ * @param {Cartesian2} point
297
+ * @returns {Cartesian3 | undefined}
298
+ */
299
+ pick_(point) {
300
+ const {scene} = this.viewer;
301
+ const position = scene.pickPositionSupported ? scene.pickPosition(point) : undefined;
302
+ const ray = position || !scene.globe?.show ? undefined : scene.camera.getPickRay(point);
303
+ return position ?? (ray && scene.globe.pick(ray, scene));
304
+ }
305
+
306
+ /**
307
+ * Where a point of the canvas meets the ground's plane through a position.
308
+ * @param {Cartesian2} point
309
+ * @param {Cartesian3} through
310
+ * @returns {Cartesian3 | undefined}
311
+ */
312
+ ground_(point, through) {
313
+ const {scene} = this.viewer;
314
+ const normal = scene.ellipsoid.geodeticSurfaceNormal(through, normalScratch);
315
+ const plane = Plane.fromPointNormal(through, normal, planeScratch);
316
+ const ray = scene.camera.getPickRay(Cartesian2.clone(point, positionScratch));
317
+ return ray && IntersectionTests.rayPlane(ray, plane, new Cartesian3());
318
+ }
319
+
320
+ /**
321
+ * Slides the view so that a position of the ground is under a point of the canvas.
322
+ * @param {Cartesian3} position
323
+ * @param {Cartesian2} point
324
+ */
325
+ keep_(position, point) {
326
+ const now = this.ground_(point, position);
327
+ if (now) {
328
+ Cartesian3.add(this.target_, Cartesian3.subtract(position, now, deltaScratch), this.target_);
329
+ this.place_();
330
+ }
331
+ }
332
+
333
+ /**
334
+ * The target moved onto what is seen in the middle of the screen: the view does not change, being orthographic, but
335
+ * it turns about the ground there.
336
+ */
337
+ anchor_() {
338
+ const {canvas} = this.viewer.scene;
339
+ const ground = this.pick_(new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2));
340
+ if (ground) {
341
+ this.target_ = ground;
342
+ this.place_();
343
+ }
344
+ }
345
+
346
+ /**
347
+ * @param {PointerEvent} e
348
+ */
349
+ down_(e) {
350
+ const canvas = this.viewer.scene.canvas;
351
+ canvas.setPointerCapture(e.pointerId);
352
+ this.settle_();
353
+ this.pointers_.set(e.pointerId, this.at_(e));
354
+ this.gliding_ = this.drift_ = this.drifted_ = undefined;
355
+ this.snapshot_(e.timeStamp);
356
+ this.drag_ = this.start_(e);
357
+ }
358
+
359
+ /**
360
+ * What the pointers down do: one finger, the right button, or the left with Ctrl or Shift slide the view; the left
361
+ * turns it; two fingers pinch, slide and twist it.
362
+ * @param {PointerEvent} e
363
+ */
364
+ start_(e) {
365
+ const points = [...this.pointers_.values()];
366
+ if (points.length === 2) {
367
+ const [a, b] = points;
368
+ const middle = new Cartesian2((a.x + b.x) / 2, (a.y + b.y) / 2);
369
+ return {grab: this.ground_(middle, this.target_), distance: Math.hypot(b.x - a.x, b.y - a.y), angle: Math.atan2(b.y - a.y, b.x - a.x), twist: 0};
370
+ }
371
+ const slide = e.pointerType === 'touch' || e.button === 2 || e.ctrlKey || e.shiftKey;
372
+ const point = /** @type {{x: number, y: number}} */ (points[0]);
373
+ return slide ? {grab: this.ground_(new Cartesian2(point.x, point.y), this.target_)} : {turn: true, last: point};
374
+ }
375
+
376
+ /**
377
+ * @param {PointerEvent} e
378
+ */
379
+ move_(e) {
380
+ if (!this.pointers_.has(e.pointerId) || !this.drag_) {
381
+ return;
382
+ }
383
+ this.pointers_.set(e.pointerId, this.at_(e));
384
+ const drag = this.drag_;
385
+ const points = [...this.pointers_.values()];
386
+ if (points.length === 2 && drag.distance !== undefined) {
387
+ const [a, b] = /** @type {[{x: number, y: number}, {x: number, y: number}]} */ (points);
388
+ const middle = new Cartesian2((a.x + b.x) / 2, (a.y + b.y) / 2);
389
+ const [distance, angle] = [Math.hypot(b.x - a.x, b.y - a.y), Math.atan2(b.y - a.y, b.x - a.x)];
390
+ this.size_ = CesiumMath.clamp(this.size_ * drag.distance / distance, this.minSize_, this.maxSize_);
391
+ this.place_();
392
+ if (drag.grab) {
393
+ this.keep_(drag.grab, middle);
394
+ }
395
+ const turned = Math.atan2(Math.sin(angle - /** @type {number} */ (drag.angle)), Math.cos(angle - /** @type {number} */ (drag.angle)));
396
+ drag.twist = /** @type {number} */ (drag.twist) + turned;
397
+ if (Math.abs(drag.twist) >= TWIST) {
398
+ // the screen's y down: the fingers turning clockwise on it turn the ground clockwise under them
399
+ this.heading_ -= turned;
400
+ this.place_();
401
+ }
402
+ Object.assign(drag, {distance, angle});
403
+ // no drift after a pinch
404
+ this.snapshot_(e.timeStamp);
405
+ return;
406
+ } else if (drag.turn && drag.last) {
407
+ const point = /** @type {{x: number, y: number}} */ (points[0]);
408
+ this.heading_ -= 2 * Math.PI * (point.x - drag.last.x) / this.viewer.scene.canvas.clientHeight;
409
+ drag.last = point;
410
+ this.place_();
411
+ } else if (drag.grab) {
412
+ const point = /** @type {{x: number, y: number}} */ (points[0]);
413
+ this.keep_(drag.grab, new Cartesian2(point.x, point.y));
414
+ }
415
+ this.track_(e.timeStamp);
416
+ }
417
+
418
+ /**
419
+ * Where the view is, to measure its next move from.
420
+ * @param {number} time
421
+ */
422
+ snapshot_(time) {
423
+ this.moved_ = {time, heading: this.heading_, target: Cartesian3.clone(this.target_), turn: 0, slide: new Cartesian3()};
424
+ }
425
+
426
+ /**
427
+ * How fast the view moves under the drag, smoothed over the last moves.
428
+ * @param {number} time
429
+ */
430
+ track_(time) {
431
+ const moved = this.moved_;
432
+ const dt = moved ? time - moved.time : 0;
433
+ if (!moved || dt <= 0) {
434
+ return;
435
+ }
436
+ const slide = Cartesian3.divideByScalar(Cartesian3.subtract(this.target_, moved.target, deltaScratch), dt, deltaScratch);
437
+ moved.turn = (moved.turn + (this.heading_ - moved.heading) / dt) / 2;
438
+ Cartesian3.divideByScalar(Cartesian3.add(moved.slide, slide, moved.slide), 2, moved.slide);
439
+ Object.assign(moved, {time, heading: this.heading_, target: Cartesian3.clone(this.target_, moved.target)});
440
+ }
441
+
442
+ /**
443
+ * @param {PointerEvent} e
444
+ */
445
+ up_(e) {
446
+ this.pointers_.delete(e.pointerId);
447
+ // a finger lifted: the other slides on, from here
448
+ this.drag_ = this.pointers_.size ? this.start_(e) : undefined;
449
+ if (this.drag_) {
450
+ this.snapshot_(e.timeStamp);
451
+ return;
452
+ }
453
+ // let go while moving: the view drifts on, slowing down; else it turns about the ground in the middle
454
+ const moved = this.moved_;
455
+ this.moved_ = undefined;
456
+ if (moved && e.timeStamp - moved.time < LET_GO && (moved.turn || Cartesian3.magnitude(moved.slide))) {
457
+ this.drift_ = {turn: moved.turn, slide: moved.slide};
458
+ this.animate_();
459
+ } else {
460
+ this.anchor_();
461
+ }
462
+ }
463
+
464
+ /**
465
+ * @param {WheelEvent} e
466
+ */
467
+ wheel_(e) {
468
+ e.preventDefault();
469
+ this.zoom(Math.pow(1.0015, e.deltaY * (e.deltaMode === 1 ? 33 : 1)), this.at_(e));
470
+ }
471
+
472
+ /**
473
+ * @param {KeyboardEvent} e
474
+ */
475
+ keydown_(e) {
476
+ const target = /** @type {Element | null} */ (e.target);
477
+ if (target?.closest?.('input, select, textarea') || e.ctrlKey || e.metaKey || e.altKey) {
478
+ return;
479
+ }
480
+ const key = e.key.toLowerCase();
481
+ if (key === 'q' || key === 'e') {
482
+ this.turn(key === 'q' ? -1 : 1);
483
+ } else if (key in PAN) {
484
+ this.held_.add(key);
485
+ e.preventDefault();
486
+ this.animate_();
487
+ }
488
+ }
489
+
490
+ /**
491
+ * The keys held, the drift, the turn and the glide, a frame at a time, until they are done.
492
+ */
493
+ animate_() {
494
+ if (this.frame_ !== undefined) {
495
+ return;
496
+ }
497
+ const step = (/** @type {number} */ now) => {
498
+ this.frame_ = undefined;
499
+ if (!this.active_) {
500
+ return;
501
+ }
502
+ if (this.held_.size) {
503
+ const [x, y] = [...this.held_].reduce(([x, y], key) => [x + (PAN[key]?.[0] ?? 0), y + (PAN[key]?.[1] ?? 0)], [0, 0]);
504
+ const width = this.width_();
505
+ const meters = PAN_SPEED * width * Math.min(0.05, (now - (this.slid_ ?? now)) / 1000);
506
+ const {canvas} = this.viewer.scene;
507
+ const middle = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2);
508
+ const length = Math.hypot(x, y);
509
+ if (length) {
510
+ // the ground in the middle kept under a point of the screen as far from it as the keys slide, the other way;
511
+ // up the screen, the ground foreshortened by the pitch
512
+ const pixels = meters / width * canvas.clientWidth;
513
+ this.keep_(this.target_, new Cartesian2(middle.x - x / length * pixels, middle.y + y / length * pixels * Math.sin(this.pitch_)));
514
+ }
515
+ }
516
+ this.slid_ = this.held_.size ? now : undefined;
517
+ if (this.drift_) {
518
+ const dt = Math.min(50, now - (this.drifted_ ?? now));
519
+ this.heading_ += this.drift_.turn * dt;
520
+ Cartesian3.add(this.target_, Cartesian3.multiplyByScalar(this.drift_.slide, dt, deltaScratch), this.target_);
521
+ this.place_();
522
+ const keep = DAMPING ** (dt * 60 / 1000);
523
+ this.drift_.turn *= keep;
524
+ Cartesian3.multiplyByScalar(this.drift_.slide, keep, this.drift_.slide);
525
+ this.drifted_ = now;
526
+ // stopped: under a pixel a second
527
+ if (Math.abs(this.drift_.turn) < 1e-5 && Cartesian3.magnitude(this.drift_.slide) < this.width_() * 1e-6) {
528
+ this.drift_ = this.drifted_ = undefined;
529
+ this.anchor_();
530
+ }
531
+ }
532
+ if (this.gliding_) {
533
+ const t = Math.min(1, (now - this.gliding_.start) / GLIDE_TIME);
534
+ Cartesian3.lerp(this.gliding_.from, this.gliding_.to, easeInOut(t), this.target_);
535
+ this.place_();
536
+ if (t === 1) {
537
+ this.gliding_ = undefined;
538
+ }
539
+ }
540
+ if (this.turning_) {
541
+ const t = Math.min(1, (now - this.turning_.start) / TURN_TIME);
542
+ this.heading_ = this.turning_.from + (this.turning_.to - this.turning_.from) * easeInOut(t);
543
+ this.place_();
544
+ if (t === 1) {
545
+ this.turning_ = undefined;
546
+ }
547
+ }
548
+ if (this.held_.size || this.drift_ || this.gliding_ || this.turning_) {
549
+ this.frame_ = requestAnimationFrame(step);
550
+ }
551
+ };
552
+ this.frame_ = requestAnimationFrame(step);
553
+ }
554
+ }
package/package.json CHANGED
@@ -1,6 +1,25 @@
1
1
  {
2
2
  "name": "@geoblocks/cesium-iso",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "license": "BSD-3-Clause",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/geoblocks/cesium-helpers/",
9
+ "directory": "packages/cesium-iso"
10
+ },
11
+ "main": "cesium-iso.js",
12
+ "module": "cesium-iso.js",
13
+ "sideEffects": false,
14
+ "files": [
15
+ "cesium-iso.js"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "peerDependencies": {
21
+ "@cesium/core": ">=0.1.0",
22
+ "@cesium/engine": ">=10"
23
+ },
24
+ "gitHead": "430bbbb63bf3d60221d670f65d5e51fdb6d5b944"
25
+ }