@ztgkzhaohao/geo-effect-kit 0.3.1 → 0.4.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/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { BoundingSphere, BillboardGraphics, CallbackPositionProperty, CallbackProperty, Cartesian2, Cartesian3, Cartesian4, CircleGeometry, Color, ColorMaterialProperty, ConstantPositionProperty, ConstantProperty, CustomDataSource, EllipsoidSurfaceAppearance, Event, GeometryInstance, GroundPrimitive, HeadingPitchRoll, HeadingPitchRange, HeightReference, HorizontalOrigin, Material, Math as CesiumMath, PointGraphics, PolylineArrowMaterialProperty, PolylineDashMaterialProperty, PolylineGlowMaterialProperty, PolylineOutlineMaterialProperty, PolygonGeometry, PolygonHierarchy, PostProcessStage, Primitive, Rectangle, Transforms, VerticalOrigin, } from 'cesium';
1
+ import { BoundingSphere, BillboardGraphics, CallbackPositionProperty, CallbackProperty, Cartesian2, Cartesian3, Cartesian4, CircleGeometry, Color, ColorMaterialProperty, ConstantPositionProperty, ConstantProperty, CustomDataSource, CylinderGeometry, EllipsoidSurfaceAppearance, Event, GeometryInstance, GroundPrimitive, HeadingPitchRoll, HeadingPitchRange, HeightReference, HorizontalOrigin, Intersect, Material, MaterialAppearance, Math as CesiumMath, Matrix4, PointGraphics, PolylineArrowMaterialProperty, PolylineDashMaterialProperty, PolylineGlowMaterialProperty, PolylineOutlineMaterialProperty, PolygonGeometry, PolygonHierarchy, PostProcessStage, Primitive, Rectangle, Transforms, VerticalOrigin, } from 'cesium';
2
2
  import { decompressFrames, parseGIF } from 'gifuct-js';
3
+ import { normalizeScanConeExpansionOptions, sampleScanConeExpansionFrame } from './scan-cone-expansion.js';
3
4
  export const GEO_RADAR_SCAN_MATERIAL_TYPE = 'GeoRadarScanMaterial';
4
5
  export const GEO_RIPPLE_SPREAD_MATERIAL_TYPE = 'GeoRippleSpreadMaterial';
5
6
  export const GEO_LIGHT_WALL_MATERIAL_TYPE = 'GeoLightWallMaterial';
@@ -233,6 +234,7 @@ export function normalizeScanConeOptions(options) {
233
234
  pitch: finiteOr(options.pitch ?? 0, 0),
234
235
  showOrigin: options.showOrigin ?? true,
235
236
  visible: options.visible ?? true,
237
+ ...(options.expansion ? { expansion: normalizeScanConeExpansionOptions(options.expansion) } : {}),
236
238
  };
237
239
  }
238
240
  export function normalizeShieldDomeOptions(options) {
@@ -3120,17 +3122,42 @@ export class ScanConeEffect {
3120
3122
  dataSource;
3121
3123
  options;
3122
3124
  coneEntity = null;
3125
+ conePrimitive = null;
3123
3126
  originEntity = null;
3124
3127
  material = null;
3128
+ primitiveMaterial = null;
3129
+ primitiveOriginScratch = new Cartesian3();
3130
+ primitiveHeadingPitchRollScratch = new HeadingPitchRoll();
3131
+ primitiveModelMatrixScratch = new Matrix4();
3132
+ primitiveScaleScratch = new Cartesian3();
3133
+ primitiveTranslationScratch = new Cartesian3();
3134
+ expansionState = createInitialScanConeExpansionState('idle');
3135
+ expansionPreviousTimestamp = null;
3136
+ pausedByVisibility = false;
3137
+ completionNotified = false;
3138
+ cameraFollowPlanned = false;
3139
+ cameraFollowCancelledByUser = false;
3140
+ cameraFollowListenersAttached = false;
3141
+ cameraFollowGeneration = 0;
3142
+ activeCameraFollowGeneration = null;
3143
+ internallyCancellingCameraFollowGeneration = null;
3125
3144
  renderFrame = 0;
3126
3145
  destroyed = false;
3146
+ handleCameraFollowUserInput = () => {
3147
+ if (this.activeCameraFollowGeneration === null)
3148
+ return;
3149
+ this.cameraFollowCancelledByUser = true;
3150
+ this.stopExpansionCameraFollow(true);
3151
+ };
3127
3152
  constructor(viewer, options) {
3128
3153
  this.viewer = viewer;
3129
3154
  this.options = normalizeScanConeOptions(options);
3130
3155
  this.dataSource = new CustomDataSource('geo-effect-kit-scan-cone');
3131
3156
  this.dataSource.show = this.options.visible;
3132
3157
  this.viewer.dataSources.add(this.dataSource);
3133
- this.renderEntities();
3158
+ this.resetExpansionState();
3159
+ this.renderCurrentPath();
3160
+ this.startExpansionCameraFollow(this.getExpansionRemainingDurationMs());
3134
3161
  this.startRenderLoop();
3135
3162
  this.viewer.scene.requestRender();
3136
3163
  }
@@ -3144,27 +3171,107 @@ export class ScanConeEffect {
3144
3171
  showOrigin: options.showOrigin ?? this.options.showOrigin,
3145
3172
  visible: options.visible ?? this.options.visible,
3146
3173
  });
3147
- const rebuildEntities = shouldRebuildScanCone(this.options, next);
3174
+ const previous = this.options;
3175
+ const modeChanged = Boolean(previous.expansion) !== Boolean(next.expansion);
3176
+ const restartExpansion = Boolean(previous.expansion && next.expansion) && (previous.lengthMeters !== next.lengthMeters ||
3177
+ previous.expansion?.maxRadiusMeters !== next.expansion?.maxRadiusMeters ||
3178
+ previous.expansion?.durationMs !== next.expansion?.durationMs);
3179
+ const cameraFollowChanged = previous.expansion?.cameraFollow !== next.expansion?.cameraFollow;
3180
+ const cameraTargetChanged = Boolean(previous.expansion && next.expansion) && (!positionEqual(previous.center, next.center) ||
3181
+ previous.heading !== next.heading ||
3182
+ previous.pitch !== next.pitch ||
3183
+ previous.speed !== next.speed);
3184
+ const rebuildEntities = !next.expansion && shouldRebuildScanCone(previous, next);
3185
+ if (modeChanged)
3186
+ this.stopExpansionCameraFollow(true);
3148
3187
  this.options = next;
3149
- if (rebuildEntities) {
3188
+ if (modeChanged) {
3189
+ this.cameraFollowPlanned = false;
3190
+ this.cameraFollowCancelledByUser = false;
3191
+ this.resetExpansionState();
3192
+ this.renderCurrentPath();
3193
+ this.startExpansionCameraFollow(this.getExpansionRemainingDurationMs());
3194
+ }
3195
+ else if (rebuildEntities) {
3150
3196
  this.renderEntities();
3151
3197
  }
3152
3198
  else {
3153
3199
  this.applyMaterialOptions();
3154
3200
  this.syncOriginEntity();
3201
+ if (restartExpansion)
3202
+ this.restartExpansion();
3203
+ else if (this.options.expansion)
3204
+ this.updatePrimitiveModelMatrix(getAnimationSeconds());
3205
+ }
3206
+ if (!modeChanged && !restartExpansion && cameraFollowChanged) {
3207
+ this.stopExpansionCameraFollow(true);
3208
+ this.cameraFollowPlanned = false;
3209
+ this.startExpansionCameraFollow(this.getExpansionRemainingDurationMs());
3210
+ }
3211
+ if (!modeChanged &&
3212
+ !restartExpansion &&
3213
+ !cameraFollowChanged &&
3214
+ cameraTargetChanged &&
3215
+ !this.cameraFollowCancelledByUser) {
3216
+ this.stopExpansionCameraFollow(true);
3217
+ this.cameraFollowPlanned = false;
3218
+ this.startExpansionCameraFollow(this.getExpansionRemainingDurationMs());
3155
3219
  }
3156
3220
  this.dataSource.show = this.options.visible;
3221
+ if (this.conePrimitive)
3222
+ this.conePrimitive.show = this.options.visible;
3223
+ if (!modeChanged && !restartExpansion && previous.visible !== this.options.visible) {
3224
+ if (this.options.visible)
3225
+ this.resumeExpansionAfterVisibilityPause();
3226
+ else
3227
+ this.pauseExpansionForVisibility();
3228
+ }
3157
3229
  if (this.options.visible)
3158
3230
  this.startRenderLoop();
3159
3231
  else
3160
3232
  this.stopRenderLoop();
3161
3233
  this.viewer.scene.requestRender();
3162
3234
  }
3235
+ restartExpansion() {
3236
+ if (this.destroyed || !this.options.expansion)
3237
+ return;
3238
+ this.stopExpansionCameraFollow(true);
3239
+ this.cameraFollowPlanned = false;
3240
+ this.cameraFollowCancelledByUser = false;
3241
+ this.expansionState = createInitialScanConeExpansionState(this.options.visible ? 'running' : 'paused');
3242
+ this.expansionPreviousTimestamp = null;
3243
+ this.pausedByVisibility = !this.options.visible;
3244
+ this.completionNotified = false;
3245
+ this.updatePrimitiveModelMatrix(getAnimationSeconds());
3246
+ this.startExpansionCameraFollow(this.getExpansionRemainingDurationMs());
3247
+ if (this.options.visible)
3248
+ this.startRenderLoop();
3249
+ this.viewer.scene.requestRender();
3250
+ }
3251
+ cancelExpansion() {
3252
+ if (this.destroyed || !this.options.expansion)
3253
+ return;
3254
+ if (this.expansionState.status === 'completed' || this.expansionState.status === 'cancelled')
3255
+ return;
3256
+ this.expansionState = { ...this.expansionState, status: 'cancelled' };
3257
+ this.expansionPreviousTimestamp = null;
3258
+ this.pausedByVisibility = false;
3259
+ this.stopExpansionCameraFollow(true);
3260
+ }
3261
+ isExpanding() {
3262
+ return !this.destroyed && this.expansionState.status === 'running';
3263
+ }
3264
+ getExpansionState() {
3265
+ return { ...this.expansionState };
3266
+ }
3163
3267
  show() {
3164
3268
  if (this.destroyed)
3165
3269
  return;
3166
3270
  this.options = { ...this.options, visible: true };
3167
3271
  this.dataSource.show = true;
3272
+ if (this.conePrimitive)
3273
+ this.conePrimitive.show = true;
3274
+ this.resumeExpansionAfterVisibilityPause();
3168
3275
  this.startRenderLoop();
3169
3276
  this.viewer.scene.requestRender();
3170
3277
  }
@@ -3173,12 +3280,19 @@ export class ScanConeEffect {
3173
3280
  return;
3174
3281
  this.options = { ...this.options, visible: false };
3175
3282
  this.dataSource.show = false;
3283
+ if (this.conePrimitive)
3284
+ this.conePrimitive.show = false;
3285
+ this.pauseExpansionForVisibility();
3176
3286
  this.stopRenderLoop();
3177
3287
  this.viewer.scene.requestRender();
3178
3288
  }
3179
3289
  flyTo(options = {}) {
3180
3290
  if (this.destroyed)
3181
3291
  return;
3292
+ if (this.activeCameraFollowGeneration !== null) {
3293
+ this.cameraFollowCancelledByUser = true;
3294
+ this.stopExpansionCameraFollow(true);
3295
+ }
3182
3296
  const center = this.getOriginCartesian();
3183
3297
  const radius = Math.max(this.options.radiusMeters, this.options.lengthMeters);
3184
3298
  this.viewer.camera.flyToBoundingSphere(new BoundingSphere(center, radius), {
@@ -3189,12 +3303,15 @@ export class ScanConeEffect {
3189
3303
  destroy() {
3190
3304
  if (this.destroyed)
3191
3305
  return;
3306
+ this.stopExpansionCameraFollow(true);
3192
3307
  this.destroyed = true;
3193
3308
  this.stopRenderLoop();
3309
+ this.removeConePrimitive();
3194
3310
  this.dataSource.entities.removeAll();
3195
3311
  this.coneEntity = null;
3196
3312
  this.originEntity = null;
3197
3313
  this.material = null;
3314
+ this.primitiveMaterial = null;
3198
3315
  this.viewer.dataSources.remove(this.dataSource, true);
3199
3316
  this.viewer.scene.requestRender();
3200
3317
  }
@@ -3208,9 +3325,11 @@ export class ScanConeEffect {
3208
3325
  return {
3209
3326
  ...this.options,
3210
3327
  center: { ...this.options.center },
3328
+ ...(this.options.expansion ? { expansion: { ...this.options.expansion } } : {}),
3211
3329
  };
3212
3330
  }
3213
3331
  renderEntities() {
3332
+ this.removeConePrimitive();
3214
3333
  clearEntities(this.dataSource);
3215
3334
  this.originEntity = null;
3216
3335
  this.material = createScanConeMaterialProperty(this.options);
@@ -3230,14 +3349,48 @@ export class ScanConeEffect {
3230
3349
  });
3231
3350
  this.syncOriginEntity();
3232
3351
  }
3352
+ renderCurrentPath() {
3353
+ if (this.options.expansion)
3354
+ this.renderPrimitive();
3355
+ else
3356
+ this.renderEntities();
3357
+ }
3358
+ renderPrimitive() {
3359
+ clearEntities(this.dataSource);
3360
+ this.coneEntity = null;
3361
+ this.originEntity = null;
3362
+ this.material = null;
3363
+ this.removeConePrimitive();
3364
+ registerScanConeMaterial();
3365
+ this.primitiveMaterial = Material.fromType(GEO_SCAN_CONE_MATERIAL_TYPE, createScanConeMaterialUniforms(this.options));
3366
+ this.conePrimitive = this.viewer.scene.primitives.add(new Primitive({
3367
+ geometryInstances: new GeometryInstance({
3368
+ id: 'geo-effect-kit-scan-cone-volume',
3369
+ geometry: new CylinderGeometry({
3370
+ length: 1,
3371
+ topRadius: 0,
3372
+ bottomRadius: 1,
3373
+ slices: 128,
3374
+ vertexFormat: MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat,
3375
+ }),
3376
+ }),
3377
+ appearance: new MaterialAppearance({
3378
+ material: this.primitiveMaterial,
3379
+ translucent: true,
3380
+ closed: false,
3381
+ faceForward: true,
3382
+ }),
3383
+ asynchronous: false,
3384
+ modelMatrix: this.createPrimitiveModelMatrix(getAnimationSeconds()),
3385
+ show: this.options.visible,
3386
+ }));
3387
+ this.syncOriginEntity();
3388
+ }
3233
3389
  applyMaterialOptions() {
3234
- if (!this.material)
3235
- return;
3236
- this.material.uniforms.color = Color.fromCssColorString(this.options.color).withAlpha(1);
3237
- this.material.uniforms.opacity = this.options.opacity;
3238
- this.material.uniforms.speed = this.options.speed;
3239
- this.material.uniforms.coneType = getScanConeTypeUniform(this.options.type);
3240
- this.material.uniforms.aperture = this.options.aperture;
3390
+ if (this.material)
3391
+ applyScanConeMaterialUniforms(this.material.uniforms, this.options);
3392
+ if (this.primitiveMaterial)
3393
+ applyScanConeMaterialUniforms(this.primitiveMaterial.uniforms, this.options);
3241
3394
  }
3242
3395
  syncOriginEntity() {
3243
3396
  if (!this.options.showOrigin) {
@@ -3267,8 +3420,8 @@ export class ScanConeEffect {
3267
3420
  if (this.originEntity.point)
3268
3421
  this.originEntity.point.color = new ConstantProperty(color);
3269
3422
  }
3270
- getOriginCartesian() {
3271
- return Cartesian3.fromDegrees(this.options.center.longitude, this.options.center.latitude, this.options.center.height ?? 0);
3423
+ getOriginCartesian(result) {
3424
+ return Cartesian3.fromDegrees(this.options.center.longitude, this.options.center.latitude, this.options.center.height ?? 0, undefined, result);
3272
3425
  }
3273
3426
  getConeCenterCartesian() {
3274
3427
  return Cartesian3.fromDegrees(this.options.center.longitude, this.options.center.latitude, (this.options.center.height ?? 0) + this.options.lengthMeters / 2);
@@ -3281,13 +3434,32 @@ export class ScanConeEffect {
3281
3434
  startRenderLoop() {
3282
3435
  if (this.renderFrame || !this.options.visible || typeof window === 'undefined')
3283
3436
  return;
3284
- const tick = () => {
3437
+ const tick = (timestamp) => {
3438
+ this.renderFrame = 0;
3285
3439
  if (this.destroyed || !this.options.visible) {
3286
- this.renderFrame = 0;
3287
3440
  return;
3288
3441
  }
3289
- this.viewer.scene.requestRender();
3290
- this.renderFrame = window.requestAnimationFrame(tick);
3442
+ try {
3443
+ const animationSeconds = Number.isFinite(timestamp) ? timestamp / 1000 : 0;
3444
+ if (this.primitiveMaterial)
3445
+ this.primitiveMaterial.uniforms.timeSeconds = animationSeconds;
3446
+ const modelMatrixUpdated = this.advanceExpansion(timestamp);
3447
+ if (this.destroyed || !this.options.visible)
3448
+ return;
3449
+ if (this.conePrimitive && !modelMatrixUpdated)
3450
+ this.updatePrimitiveModelMatrix(animationSeconds);
3451
+ }
3452
+ finally {
3453
+ if (!this.destroyed && this.options.visible) {
3454
+ try {
3455
+ this.viewer.scene.requestRender();
3456
+ }
3457
+ finally {
3458
+ if (!this.renderFrame)
3459
+ this.renderFrame = window.requestAnimationFrame(tick);
3460
+ }
3461
+ }
3462
+ }
3291
3463
  };
3292
3464
  this.renderFrame = window.requestAnimationFrame(tick);
3293
3465
  }
@@ -3299,6 +3471,210 @@ export class ScanConeEffect {
3299
3471
  window.cancelAnimationFrame(this.renderFrame);
3300
3472
  this.renderFrame = 0;
3301
3473
  }
3474
+ resetExpansionState() {
3475
+ if (!this.options.expansion) {
3476
+ this.expansionState = createInitialScanConeExpansionState('idle');
3477
+ this.expansionPreviousTimestamp = null;
3478
+ this.pausedByVisibility = false;
3479
+ this.completionNotified = false;
3480
+ return;
3481
+ }
3482
+ const shouldRun = this.options.expansion.autoStart;
3483
+ this.expansionState = createInitialScanConeExpansionState(shouldRun ? (this.options.visible ? 'running' : 'paused') : 'idle');
3484
+ this.expansionPreviousTimestamp = null;
3485
+ this.pausedByVisibility = shouldRun && !this.options.visible;
3486
+ this.completionNotified = false;
3487
+ }
3488
+ advanceExpansion(timestamp) {
3489
+ const expansion = this.options.expansion;
3490
+ if (!expansion || this.expansionState.status !== 'running')
3491
+ return false;
3492
+ const safeTimestamp = Number.isFinite(timestamp) ? timestamp : 0;
3493
+ const previousTimestamp = this.expansionPreviousTimestamp;
3494
+ const elapsedDelta = previousTimestamp === null ? 0 : Math.max(0, safeTimestamp - previousTimestamp);
3495
+ this.expansionPreviousTimestamp = previousTimestamp === null
3496
+ ? safeTimestamp
3497
+ : Math.max(previousTimestamp, safeTimestamp);
3498
+ const frame = sampleScanConeExpansionFrame(expansion, this.options.lengthMeters, this.expansionState.elapsedMs + elapsedDelta);
3499
+ const completed = frame.elapsedMs >= expansion.durationMs;
3500
+ this.expansionState = {
3501
+ ...frame,
3502
+ status: completed ? 'completed' : 'running',
3503
+ };
3504
+ this.updatePrimitiveModelMatrix(safeTimestamp / 1000);
3505
+ const callbackState = { ...this.expansionState };
3506
+ const notifyCompletion = completed && !this.completionNotified;
3507
+ if (notifyCompletion)
3508
+ this.completionNotified = true;
3509
+ if (completed)
3510
+ this.detachCameraFollowListeners();
3511
+ try {
3512
+ expansion.onFrame?.(callbackState);
3513
+ }
3514
+ finally {
3515
+ if (!this.destroyed && notifyCompletion)
3516
+ expansion.onComplete?.({ ...callbackState });
3517
+ }
3518
+ return true;
3519
+ }
3520
+ pauseExpansionForVisibility() {
3521
+ if (!this.options.expansion || this.expansionState.status !== 'running')
3522
+ return;
3523
+ this.expansionState = { ...this.expansionState, status: 'paused' };
3524
+ this.expansionPreviousTimestamp = null;
3525
+ this.pausedByVisibility = true;
3526
+ this.stopExpansionCameraFollow(true);
3527
+ this.cameraFollowPlanned = false;
3528
+ }
3529
+ resumeExpansionAfterVisibilityPause() {
3530
+ if (!this.options.expansion || this.expansionState.status !== 'paused' || !this.pausedByVisibility)
3531
+ return;
3532
+ this.expansionState = { ...this.expansionState, status: 'running' };
3533
+ this.expansionPreviousTimestamp = null;
3534
+ this.pausedByVisibility = false;
3535
+ this.startExpansionCameraFollow(this.getExpansionRemainingDurationMs());
3536
+ }
3537
+ getExpansionRemainingDurationMs() {
3538
+ if (!this.options.expansion)
3539
+ return 0;
3540
+ return Math.max(0, this.options.expansion.durationMs - this.expansionState.elapsedMs);
3541
+ }
3542
+ startExpansionCameraFollow(durationMs) {
3543
+ const expansion = this.options.expansion;
3544
+ if (!expansion?.cameraFollow ||
3545
+ !this.options.visible ||
3546
+ this.expansionState.status !== 'running' ||
3547
+ this.cameraFollowPlanned ||
3548
+ this.cameraFollowCancelledByUser ||
3549
+ durationMs <= 0)
3550
+ return;
3551
+ this.cameraFollowPlanned = true;
3552
+ const camera = this.viewer.camera;
3553
+ let sphere;
3554
+ let visibility;
3555
+ try {
3556
+ sphere = this.createFinalExpansionBoundingSphere(durationMs);
3557
+ const position = camera.positionWC ?? camera.position;
3558
+ const direction = camera.directionWC ?? camera.direction;
3559
+ const up = camera.upWC ?? camera.up;
3560
+ visibility = camera.frustum
3561
+ .computeCullingVolume(position, direction, up)
3562
+ .computeVisibility(sphere);
3563
+ }
3564
+ catch {
3565
+ return;
3566
+ }
3567
+ if (visibility === Intersect.INSIDE)
3568
+ return;
3569
+ const heading = Number.isFinite(camera.heading) ? camera.heading : 0;
3570
+ const pitch = Number.isFinite(camera.pitch)
3571
+ ? clamp(camera.pitch, -CesiumMath.PI_OVER_TWO + 0.01, CesiumMath.PI_OVER_TWO - 0.01)
3572
+ : -CesiumMath.PI_OVER_FOUR;
3573
+ const framedSphere = new BoundingSphere(sphere.center, sphere.radius * 1.18);
3574
+ const generation = ++this.cameraFollowGeneration;
3575
+ this.activeCameraFollowGeneration = generation;
3576
+ this.attachCameraFollowListeners();
3577
+ try {
3578
+ camera.flyToBoundingSphere(framedSphere, {
3579
+ duration: durationMs / 1000,
3580
+ offset: new HeadingPitchRange(heading, pitch, 0),
3581
+ complete: () => this.releaseExpansionCameraFollow(generation),
3582
+ cancel: () => this.handleExpansionCameraFollowCancelled(generation),
3583
+ });
3584
+ }
3585
+ catch {
3586
+ this.releaseExpansionCameraFollow(generation);
3587
+ }
3588
+ }
3589
+ createFinalExpansionBoundingSphere(durationMs) {
3590
+ const expansion = this.options.expansion;
3591
+ const radius = expansion?.maxRadiusMeters ?? this.options.radiusMeters;
3592
+ const length = this.options.lengthMeters;
3593
+ const finalHeading = this.options.heading +
3594
+ (getAnimationSeconds() + durationMs / 1000) * this.options.speed * 36;
3595
+ const headingPitchRoll = HeadingPitchRoll.fromDegrees(finalHeading, this.options.pitch, 0);
3596
+ const transform = Transforms.headingPitchRollToFixedFrame(this.getOriginCartesian(), headingPitchRoll);
3597
+ const localCenter = new Cartesian3(0, 0, length / 2);
3598
+ const center = Matrix4.multiplyByPoint(transform, localCenter, new Cartesian3());
3599
+ return new BoundingSphere(center, Math.hypot(radius, length / 2));
3600
+ }
3601
+ attachCameraFollowListeners() {
3602
+ if (this.cameraFollowListenersAttached)
3603
+ return;
3604
+ this.cameraFollowListenersAttached = true;
3605
+ this.viewer.canvas.addEventListener('pointerdown', this.handleCameraFollowUserInput);
3606
+ this.viewer.canvas.addEventListener('wheel', this.handleCameraFollowUserInput);
3607
+ this.viewer.canvas.addEventListener('touchstart', this.handleCameraFollowUserInput);
3608
+ }
3609
+ stopExpansionCameraFollow(cancelFlight) {
3610
+ const generation = this.activeCameraFollowGeneration;
3611
+ if (generation === null) {
3612
+ this.detachCameraFollowListeners();
3613
+ return;
3614
+ }
3615
+ if (!cancelFlight) {
3616
+ this.releaseExpansionCameraFollow(generation);
3617
+ return;
3618
+ }
3619
+ this.internallyCancellingCameraFollowGeneration = generation;
3620
+ try {
3621
+ this.viewer.camera.cancelFlight();
3622
+ }
3623
+ catch {
3624
+ // Camera follow is optional; camera failures must not stop the effect lifecycle.
3625
+ }
3626
+ finally {
3627
+ this.releaseExpansionCameraFollow(generation);
3628
+ if (this.internallyCancellingCameraFollowGeneration === generation) {
3629
+ this.internallyCancellingCameraFollowGeneration = null;
3630
+ }
3631
+ }
3632
+ }
3633
+ handleExpansionCameraFollowCancelled(generation) {
3634
+ if (this.activeCameraFollowGeneration !== generation)
3635
+ return;
3636
+ if (this.internallyCancellingCameraFollowGeneration !== generation) {
3637
+ this.cameraFollowCancelledByUser = true;
3638
+ }
3639
+ this.releaseExpansionCameraFollow(generation);
3640
+ }
3641
+ releaseExpansionCameraFollow(generation) {
3642
+ if (this.activeCameraFollowGeneration !== generation)
3643
+ return;
3644
+ this.activeCameraFollowGeneration = null;
3645
+ this.detachCameraFollowListeners();
3646
+ }
3647
+ detachCameraFollowListeners() {
3648
+ if (!this.cameraFollowListenersAttached)
3649
+ return;
3650
+ this.viewer.canvas.removeEventListener('pointerdown', this.handleCameraFollowUserInput);
3651
+ this.viewer.canvas.removeEventListener('wheel', this.handleCameraFollowUserInput);
3652
+ this.viewer.canvas.removeEventListener('touchstart', this.handleCameraFollowUserInput);
3653
+ this.cameraFollowListenersAttached = false;
3654
+ }
3655
+ createPrimitiveModelMatrix(animationSeconds, result = this.primitiveModelMatrixScratch) {
3656
+ const heading = this.options.heading + animationSeconds * this.options.speed * 36;
3657
+ HeadingPitchRoll.fromDegrees(heading, this.options.pitch, 0, this.primitiveHeadingPitchRollScratch);
3658
+ Transforms.headingPitchRollToFixedFrame(this.getOriginCartesian(this.primitiveOriginScratch), this.primitiveHeadingPitchRollScratch, undefined, undefined, result);
3659
+ const radius = Math.max(0.000001, this.expansionState.radiusMeters);
3660
+ const length = Math.max(0.000001, this.expansionState.lengthMeters);
3661
+ Cartesian3.fromElements(0, 0, length / 2, this.primitiveTranslationScratch);
3662
+ Cartesian3.fromElements(radius, radius, length, this.primitiveScaleScratch);
3663
+ Matrix4.multiplyByTranslation(result, this.primitiveTranslationScratch, result);
3664
+ return Matrix4.multiplyByScale(result, this.primitiveScaleScratch, result);
3665
+ }
3666
+ updatePrimitiveModelMatrix(animationSeconds) {
3667
+ if (!this.conePrimitive)
3668
+ return;
3669
+ this.createPrimitiveModelMatrix(animationSeconds, this.conePrimitive.modelMatrix);
3670
+ }
3671
+ removeConePrimitive() {
3672
+ if (!this.conePrimitive)
3673
+ return;
3674
+ this.viewer.scene.primitives.remove(this.conePrimitive);
3675
+ this.conePrimitive = null;
3676
+ this.primitiveMaterial = null;
3677
+ }
3302
3678
  }
3303
3679
  export class ShieldDomeEffect {
3304
3680
  viewer;
@@ -3635,14 +4011,21 @@ function createLightWallMaterialProperty(options) {
3635
4011
  export function createScanConeMaterialProperty(options) {
3636
4012
  const normalized = normalizeScanConeOptions(options);
3637
4013
  registerScanConeMaterial();
3638
- return new DynamicCesiumMaterialProperty(GEO_SCAN_CONE_MATERIAL_TYPE, {
3639
- color: Color.fromCssColorString(normalized.color).withAlpha(1),
3640
- opacity: normalized.opacity,
3641
- speed: normalized.speed,
3642
- timeSeconds: -1,
3643
- coneType: getScanConeTypeUniform(normalized.type),
3644
- aperture: normalized.aperture,
3645
- });
4014
+ return new DynamicCesiumMaterialProperty(GEO_SCAN_CONE_MATERIAL_TYPE, createScanConeMaterialUniforms(normalized));
4015
+ }
4016
+ function createScanConeMaterialUniforms(options, timeSeconds = -1) {
4017
+ return {
4018
+ color: Color.fromCssColorString(options.color).withAlpha(1),
4019
+ opacity: options.opacity,
4020
+ speed: options.speed,
4021
+ timeSeconds,
4022
+ coneType: getScanConeTypeUniform(options.type),
4023
+ aperture: options.aperture,
4024
+ };
4025
+ }
4026
+ function applyScanConeMaterialUniforms(uniforms, options) {
4027
+ const timeSeconds = typeof uniforms.timeSeconds === 'number' ? uniforms.timeSeconds : -1;
4028
+ Object.assign(uniforms, createScanConeMaterialUniforms(options, timeSeconds));
3646
4029
  }
3647
4030
  function createShieldDomeMaterialProperty(options) {
3648
4031
  registerShieldDomeMaterial();
@@ -3997,6 +4380,15 @@ function getConeBottomRadius(options) {
3997
4380
  const apertureRadius = Math.tan(CesiumMath.toRadians(options.aperture) / 2) * options.lengthMeters;
3998
4381
  return Math.max(options.radiusMeters, apertureRadius);
3999
4382
  }
4383
+ function createInitialScanConeExpansionState(status) {
4384
+ return {
4385
+ status,
4386
+ progress: 0,
4387
+ radiusMeters: 0,
4388
+ lengthMeters: 0,
4389
+ elapsedMs: 0,
4390
+ };
4391
+ }
4000
4392
  function normalizePositions(positions, close) {
4001
4393
  const normalized = (positions.length ? positions : [{ longitude: 0, latitude: 0 }]).map(normalizePosition);
4002
4394
  const onlyPosition = normalized[0];