@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.
- package/LICENSE +29 -0
- package/README.md +50 -2
- package/cesium-iso.js +554 -0
- 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
|
-
#
|
|
1
|
+
# Cesium Iso
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|