cesium-xs-sdk 1.0.13 → 1.0.14

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.
@@ -9198,17 +9198,38 @@ class ControlModule {
9198
9198
  }
9199
9199
  }
9200
9200
 
9201
- const toDataUri = (svg) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
9202
-
9203
- /**
9204
- * 军标内置默认图标。
9205
- * SDK 内置常见军标类型的默认图标,业务层可通过 imgUrl / useDefaultIcon 参数覆盖或追加。
9206
- */
9207
- const DEFAULT_SYMBOL_IMAGES = {
9208
- 'small-radio': toDataUri(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" width="80" height="80"><g id="图层_2" data-name="图层 2"><g id="图层_1-2" data-name="图层 1"><circle cx="36.93" cy="48.5" r="22" style="fill:none;stroke:red;stroke-miterlimit:10;stroke-width:4.535433070866142px"/><polyline points="42.56 25.87 53.06 9.51 54.06 27.5 65.07 10.37" style="fill:none;stroke:red;stroke-linejoin:bevel;stroke-width:4.535433070866142px"/><rect width="80" height="80" style="fill:red;opacity:0"/></g></g></svg>`),
9209
- 'large-radio': toDataUri(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" width="80" height="80"><g id="图层_2" data-name="图层 2"><g id="图层_1-2" data-name="图层 1"><polygon points="39.96 28.87 51.18 48.3 62.4 67.73 39.96 67.73 17.53 67.73 28.75 48.3 39.96 28.87" style="fill:none;stroke:red;stroke-miterlimit:10;stroke-width:4.535433070866142px"/><polyline points="39.96 28.64 50.46 12.27 51.46 30.27 62.47 13.14" style="fill:none;stroke:red;stroke-linejoin:bevel;stroke-width:4.535433070866142px"/><rect width="80" height="80" style="fill:red;opacity:0"/></g></g></svg>`),
9201
+ /**
9202
+ * 军标内置默认图标。
9203
+ * SDK 内置常见军标类型的默认图标,业务层可通过 imgUrl / useDefaultIcon 参数覆盖或追加。
9204
+ */
9205
+ const DEFAULT_SYMBOL_SVGS = {
9206
+ 'small-radio': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" width="80" height="80"><g id="图层_2" data-name="图层 2"><g id="图层_1-2" data-name="图层 1"><circle cx="36.93" cy="48.5" r="22" style="fill:none;stroke:red;stroke-miterlimit:10;stroke-width:4.535433070866142px"/><polyline points="42.56 25.87 53.06 9.51 54.06 27.5 65.07 10.37" style="fill:none;stroke:red;stroke-linejoin:bevel;stroke-width:4.535433070866142px"/><rect width="80" height="80" style="fill:red;opacity:0"/></g></g></svg>`,
9207
+ 'large-radio': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" width="80" height="80"><g id="图层_2" data-name="图层 2"><g id="图层_1-2" data-name="图层 1"><polygon points="39.96 28.87 51.18 48.3 62.4 67.73 39.96 67.73 17.53 67.73 28.75 48.3 39.96 28.87" style="fill:none;stroke:red;stroke-miterlimit:10;stroke-width:4.535433070866142px"/><polyline points="39.96 28.64 50.46 12.27 51.46 30.27 62.47 13.14" style="fill:none;stroke:red;stroke-linejoin:bevel;stroke-width:4.535433070866142px"/><rect width="80" height="80" style="fill:red;opacity:0"/></g></g></svg>`,
9210
9208
  };
9211
9209
 
9210
+ /** Blob URL 缓存,避免重复创建 */
9211
+ const blobUrlCache = new Map();
9212
+
9213
+ /**
9214
+ * 获取内置军标图标的 Blob URL(比 data URI 更利于 Cesium 材质加载)
9215
+ * @param {string} type 军标类型
9216
+ * @returns {string | null}
9217
+ */
9218
+ function getDefaultSymbolImage(type) {
9219
+ const svg = DEFAULT_SYMBOL_SVGS[type];
9220
+ if (!svg) return null;
9221
+ if (blobUrlCache.has(type)) return blobUrlCache.get(type);
9222
+ try {
9223
+ const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
9224
+ const url = URL.createObjectURL(blob);
9225
+ blobUrlCache.set(type, url);
9226
+ return url;
9227
+ } catch (e) {
9228
+ console.warn('生成军标图标 Blob URL 失败', e);
9229
+ return null;
9230
+ }
9231
+ }
9232
+
9212
9233
  const Cesium$2 = getCesium();
9213
9234
 
9214
9235
  /**
@@ -9227,10 +9248,21 @@ class MilitarySymbolModule {
9227
9248
  this.eventModule = null;
9228
9249
 
9229
9250
  this._editMode = false;
9251
+ this._editSymbolId = null;
9252
+ /** @type {Map<string, {move: Cesium.Entity, scale: Cesium.Entity, rotate: Cesium.Entity}>} */
9253
+ this._editHandles = new Map();
9230
9254
  this._draggingId = null;
9255
+ this._draggingHandle = null;
9256
+ this._dragStartScreen = null;
9257
+ this._dragStartScale = 1;
9258
+ this._dragStartRotation = 0;
9259
+ this._dragStartDist = 0;
9260
+ this._dragStartAngle = 0;
9231
9261
  this._dragObserverIds = [];
9232
9262
  /** @type {Set<Function>} */
9233
9263
  this._positionChangedCallbacks = new Set();
9264
+ /** @type {Set<Function>} */
9265
+ this._transformChangedCallbacks = new Set();
9234
9266
  }
9235
9267
 
9236
9268
  /**
@@ -9287,7 +9319,7 @@ class MilitarySymbolModule {
9287
9319
  _resolveImage(id, options) {
9288
9320
  const globalMode = this._defaultIconMode || this.config.militarySymbol?.defaultIconMode;
9289
9321
  const mode = options.iconMode || globalMode || 'default';
9290
- const defaultImage = DEFAULT_SYMBOL_IMAGES[options.type];
9322
+ const defaultImage = getDefaultSymbolImage(options.type);
9291
9323
  const registeredImage = this.defaultImageMap.get(options.type);
9292
9324
  const imgUrl = options.imgUrl || registeredImage || null;
9293
9325
 
@@ -9330,7 +9362,10 @@ class MilitarySymbolModule {
9330
9362
  _loadImage(url) {
9331
9363
  return new Promise((resolve, reject) => {
9332
9364
  const img = new Image();
9333
- img.crossOrigin = 'anonymous';
9365
+ // data URI / blob URL 等同源资源不需要 crossOrigin;跨域 http 资源需要
9366
+ if (url && /^https?:\/\//.test(url)) {
9367
+ img.crossOrigin = 'anonymous';
9368
+ }
9334
9369
  img.onload = () => resolve(img);
9335
9370
  img.onerror = (e) => reject(e);
9336
9371
  img.src = url;
@@ -9384,6 +9419,131 @@ class MilitarySymbolModule {
9384
9419
  return this.symbolMap.has(id) ? id : null;
9385
9420
  }
9386
9421
 
9422
+ /**
9423
+ * 根据鼠标拾取结果判断是否为编辑控制点
9424
+ * @private
9425
+ */
9426
+ _getHandleType(pickedEntity) {
9427
+ if (!pickedEntity) return null;
9428
+ const id = typeof pickedEntity.id === 'string' ? pickedEntity.id : pickedEntity.id?.id;
9429
+ if (!id) return null;
9430
+ if (id.endsWith('_move')) return 'move';
9431
+ if (id.endsWith('_scale')) return 'scale';
9432
+ if (id.endsWith('_rotate')) return 'rotate';
9433
+ return null;
9434
+ }
9435
+
9436
+ /**
9437
+ * 计算编辑控制点的三维坐标
9438
+ * @private
9439
+ */
9440
+ _getHandlePositions(position) {
9441
+ const center = Cesium$2.Cartesian3.fromDegrees(position[0], position[1], position[2] || 0);
9442
+ const matrix = Cesium$2.Transforms.eastNorthUpToFixedFrame(center);
9443
+ const offsetMeters = 120;
9444
+ const scaleOffset = Cesium$2.Matrix4.multiplyByPoint(
9445
+ matrix,
9446
+ new Cesium$2.Cartesian3(offsetMeters, 0, 0),
9447
+ new Cesium$2.Cartesian3()
9448
+ );
9449
+ const rotateOffset = Cesium$2.Matrix4.multiplyByPoint(
9450
+ matrix,
9451
+ new Cesium$2.Cartesian3(0, offsetMeters, 0),
9452
+ new Cesium$2.Cartesian3()
9453
+ );
9454
+ return { center, scale: scaleOffset, rotate: rotateOffset };
9455
+ }
9456
+
9457
+ /**
9458
+ * 为指定军标创建编辑控制点
9459
+ * @private
9460
+ */
9461
+ _createEditHandles(symbolId) {
9462
+ this._removeEditHandles(symbolId);
9463
+ const symbol = this.symbolMap.get(symbolId);
9464
+ if (!symbol) return;
9465
+
9466
+ const positions = this._getHandlePositions(symbol.metadata.position);
9467
+ const commonPoint = {
9468
+ pixelSize: 14,
9469
+ outlineColor: Cesium$2.Color.WHITE,
9470
+ outlineWidth: 2,
9471
+ heightReference: Cesium$2.HeightReference.CLAMP_TO_GROUND,
9472
+ };
9473
+ const commonLabel = {
9474
+ font: '12px Microsoft YaHei',
9475
+ fillColor: Cesium$2.Color.WHITE,
9476
+ outlineColor: Cesium$2.Color.BLACK,
9477
+ outlineWidth: 2,
9478
+ style: Cesium$2.LabelStyle.FILL_AND_OUTLINE,
9479
+ verticalOrigin: Cesium$2.VerticalOrigin.BOTTOM,
9480
+ horizontalOrigin: Cesium$2.HorizontalOrigin.CENTER,
9481
+ pixelOffset: new Cesium$2.Cartesian2(0, -10),
9482
+ heightReference: Cesium$2.HeightReference.CLAMP_TO_GROUND,
9483
+ };
9484
+
9485
+ const handles = {};
9486
+ handles.move = this.viewer.entities.add({
9487
+ id: `${symbolId}_move`,
9488
+ position: positions.center,
9489
+ point: { ...commonPoint, color: Cesium$2.Color.LIME },
9490
+ label: { ...commonLabel, text: '移动' },
9491
+ });
9492
+ handles.scale = this.viewer.entities.add({
9493
+ id: `${symbolId}_scale`,
9494
+ position: positions.scale,
9495
+ point: { ...commonPoint, color: Cesium$2.Color.DEEPSKYBLUE },
9496
+ label: { ...commonLabel, text: '缩放' },
9497
+ });
9498
+ handles.rotate = this.viewer.entities.add({
9499
+ id: `${symbolId}_rotate`,
9500
+ position: positions.rotate,
9501
+ point: { ...commonPoint, color: Cesium$2.Color.RED },
9502
+ label: { ...commonLabel, text: '旋转' },
9503
+ });
9504
+
9505
+ this._editHandles.set(symbolId, handles);
9506
+ }
9507
+
9508
+ /**
9509
+ * 移除指定军标的编辑控制点
9510
+ * @private
9511
+ */
9512
+ _removeEditHandles(symbolId) {
9513
+ const handles = this._editHandles.get(symbolId);
9514
+ if (!handles) return;
9515
+ Object.values(handles).forEach((entity) => {
9516
+ if (entity) this.viewer.entities.remove(entity);
9517
+ });
9518
+ this._editHandles.delete(symbolId);
9519
+ }
9520
+
9521
+ /**
9522
+ * 更新控制点位置以跟随军标
9523
+ * @private
9524
+ */
9525
+ _updateHandlePositions(symbolId) {
9526
+ const symbol = this.symbolMap.get(symbolId);
9527
+ const handles = this._editHandles.get(symbolId);
9528
+ if (!symbol || !handles) return;
9529
+ const positions = this._getHandlePositions(symbol.metadata.position);
9530
+ handles.move.position = positions.center;
9531
+ handles.scale.position = positions.scale;
9532
+ handles.rotate.position = positions.rotate;
9533
+ }
9534
+
9535
+ /**
9536
+ * 将 WGS84 坐标转换为屏幕坐标
9537
+ * @private
9538
+ */
9539
+ _cartesianToScreen(cartesian) {
9540
+ if (!cartesian) return null;
9541
+ try {
9542
+ return Cesium$2.SceneTransforms.wgs84ToWindowCoordinates(this.viewer.scene, cartesian);
9543
+ } catch (e) {
9544
+ return null;
9545
+ }
9546
+ }
9387
9547
  /**
9388
9548
  * 创建军标图元
9389
9549
  * @param {string} id 军标唯一ID
@@ -9422,7 +9582,6 @@ class MilitarySymbolModule {
9422
9582
  verticalOrigin: Cesium$2.VerticalOrigin.BOTTOM,
9423
9583
  horizontalOrigin: Cesium$2.HorizontalOrigin.CENTER,
9424
9584
  heightReference: Cesium$2.HeightReference.CLAMP_TO_GROUND,
9425
- disableDepthTestDistance: 100000,
9426
9585
  },
9427
9586
  label: {
9428
9587
  text: name,
@@ -9435,7 +9594,6 @@ class MilitarySymbolModule {
9435
9594
  horizontalOrigin: Cesium$2.HorizontalOrigin.CENTER,
9436
9595
  pixelOffset: new Cesium$2.Cartesian2(0, 4),
9437
9596
  heightReference: Cesium$2.HeightReference.CLAMP_TO_GROUND,
9438
- disableDepthTestDistance: 100000,
9439
9597
  show: true,
9440
9598
  },
9441
9599
  show: visible,
@@ -9469,6 +9627,10 @@ class MilitarySymbolModule {
9469
9627
  return false;
9470
9628
  }
9471
9629
  this.viewer.entities.remove(symbol.entity);
9630
+ this._removeEditHandles(id);
9631
+ if (this._editSymbolId === id) {
9632
+ this.disableEditMode();
9633
+ }
9472
9634
  this.symbolMap.delete(id);
9473
9635
  if (this._draggingId === id) {
9474
9636
  this._draggingId = null;
@@ -9481,6 +9643,11 @@ class MilitarySymbolModule {
9481
9643
  * 删除所有军标
9482
9644
  */
9483
9645
  removeAll() {
9646
+ this._editHandles.forEach((_, id) => this._removeEditHandles(id));
9647
+ this._editHandles.clear();
9648
+ if (this._editMode) {
9649
+ this.disableEditMode();
9650
+ }
9484
9651
  this.symbolMap.forEach((symbol) => {
9485
9652
  this.viewer.entities.remove(symbol.entity);
9486
9653
  });
@@ -9604,13 +9771,14 @@ class MilitarySymbolModule {
9604
9771
  const pitch = options.pitch ?? -Cesium$2.Math.PI_OVER_TWO;
9605
9772
  const range = options.range ?? 5000;
9606
9773
 
9607
- return this.viewer.camera.flyTo({
9608
- destination: Cesium$2.Cartesian3.fromDegrees(position[0], position[1], (position[2] || 0) + range),
9609
- orientation: { heading: 0, pitch, roll: 0 },
9610
- duration,
9611
- }).then(() => true).catch((err) => {
9612
- console.error('军标定位失败', err);
9613
- return false;
9774
+ return new Promise((resolve) => {
9775
+ this.viewer.camera.flyTo({
9776
+ destination: Cesium$2.Cartesian3.fromDegrees(position[0], position[1], (position[2] || 0) + range),
9777
+ orientation: { heading: 0, pitch, roll: 0 },
9778
+ duration,
9779
+ complete: () => resolve(true),
9780
+ cancel: () => resolve(false),
9781
+ });
9614
9782
  });
9615
9783
  }
9616
9784
 
@@ -9636,49 +9804,115 @@ class MilitarySymbolModule {
9636
9804
  }
9637
9805
 
9638
9806
  /**
9639
- * 启用拖拽编辑模式
9640
- * 鼠标在军标上按下即可拖拽,拖拽结束后触发 onPositionChanged 回调。
9807
+ * 启用军标编辑模式(生成移动/缩放/旋转控制点)
9808
+ * @param {string} id 要编辑的军标ID
9641
9809
  */
9642
- enableEditMode() {
9643
- if (this._editMode) return;
9810
+ enableEditMode(id) {
9811
+ if (this._editMode) {
9812
+ if (this._editSymbolId === id) return;
9813
+ this.disableEditMode();
9814
+ }
9815
+ if (!this.symbolMap.has(id)) {
9816
+ console.warn(`军标 ${id} 不存在,无法启用编辑`);
9817
+ return;
9818
+ }
9644
9819
  if (!this.eventModule) {
9645
- console.warn('MilitarySymbolModule 未注入 EventModule,无法启用拖拽编辑');
9820
+ console.warn('MilitarySymbolModule 未注入 EventModule,无法启用编辑');
9646
9821
  return;
9647
9822
  }
9648
9823
 
9649
9824
  this._editMode = true;
9825
+ this._editSymbolId = id;
9826
+ this._createEditHandles(id);
9827
+
9828
+ const downId = this.eventModule.registerObserver('mouse:down', (lngLat, pickedEntity, movement) => {
9829
+ const handleType = this._getHandleType(pickedEntity);
9830
+ let symbolId = null;
9831
+ if (handleType) {
9832
+ symbolId = this._editSymbolId;
9833
+ } else {
9834
+ symbolId = this._getSymbolIdFromPick(pickedEntity);
9835
+ }
9836
+ if (!symbolId || symbolId !== this._editSymbolId) return;
9837
+
9838
+ const symbol = this.symbolMap.get(symbolId);
9839
+ const centerScreen = this._cartesianToScreen(symbol.entity.position.getValue(Cesium$2.JulianDate.now()));
9840
+ const mouseScreen = movement.position;
9841
+ if (centerScreen && mouseScreen) {
9842
+ const dx = mouseScreen.x - centerScreen.x;
9843
+ const dy = mouseScreen.y - centerScreen.y;
9844
+ this._dragStartDist = Math.hypot(dx, dy);
9845
+ this._dragStartAngle = Math.atan2(dx, -dy);
9846
+ } else {
9847
+ this._dragStartDist = 0;
9848
+ this._dragStartAngle = 0;
9849
+ }
9650
9850
 
9651
- const downId = this.eventModule.registerObserver('mouse:down', (lngLat, pickedEntity) => {
9652
- const symbolId = this._getSymbolIdFromPick(pickedEntity);
9653
- if (!symbolId) return;
9654
9851
  this._draggingId = symbolId;
9852
+ this._draggingHandle = handleType || 'move';
9853
+ this._dragStartScale = symbol.metadata.scale;
9854
+ this._dragStartRotation = symbol.metadata.rotation || 0;
9655
9855
  this.viewer.scene.screenSpaceCameraController.enableInputs = false;
9656
9856
  });
9657
9857
 
9658
9858
  const moveId = this.eventModule.registerObserver('mouse:move', (lngLat, pickedEntity, movement) => {
9659
9859
  if (!this._draggingId || !movement?.endPosition) return;
9660
9860
 
9661
- const cartesian = this.viewer.scene.pickPosition(movement.endPosition)
9662
- || this.viewer.scene.camera.pickEllipsoid(movement.endPosition, this.viewer.scene.globe.ellipsoid);
9663
- if (!cartesian) return;
9664
-
9665
- const carto = Cesium$2.Cartographic.fromCartesian(cartesian);
9666
- const position = [
9667
- Cesium$2.Math.toDegrees(carto.longitude),
9668
- Cesium$2.Math.toDegrees(carto.latitude),
9669
- carto.height,
9670
- ];
9671
- this.update(this._draggingId, { position });
9861
+ const symbol = this.symbolMap.get(this._draggingId);
9862
+ if (!symbol) return;
9863
+
9864
+ if (this._draggingHandle === 'move') {
9865
+ const cartesian = this.viewer.scene.pickPosition(movement.endPosition)
9866
+ || this.viewer.scene.camera.pickEllipsoid(movement.endPosition, this.viewer.scene.globe.ellipsoid);
9867
+ if (!cartesian) return;
9868
+
9869
+ const carto = Cesium$2.Cartographic.fromCartesian(cartesian);
9870
+ const position = [
9871
+ Cesium$2.Math.toDegrees(carto.longitude),
9872
+ Cesium$2.Math.toDegrees(carto.latitude),
9873
+ carto.height,
9874
+ ];
9875
+ this.update(this._draggingId, { position });
9876
+ this._updateHandlePositions(this._draggingId);
9877
+ } else if (this._draggingHandle === 'scale') {
9878
+ const centerScreen = this._cartesianToScreen(symbol.entity.position.getValue(Cesium$2.JulianDate.now()));
9879
+ if (!centerScreen) return;
9880
+ const dx = movement.endPosition.x - centerScreen.x;
9881
+ const dy = movement.endPosition.y - centerScreen.y;
9882
+ const dist = Math.hypot(dx, dy);
9883
+ const ratio = this._dragStartDist > 0 ? dist / this._dragStartDist : 1;
9884
+ const newScale = Math.max(0.1, this._dragStartScale * ratio);
9885
+ this.update(this._draggingId, { scale: newScale });
9886
+ } else if (this._draggingHandle === 'rotate') {
9887
+ const centerScreen = this._cartesianToScreen(symbol.entity.position.getValue(Cesium$2.JulianDate.now()));
9888
+ if (!centerScreen) return;
9889
+ const dx = movement.endPosition.x - centerScreen.x;
9890
+ const dy = movement.endPosition.y - centerScreen.y;
9891
+ const angle = Math.atan2(dx, -dy);
9892
+ const deltaDeg = Cesium$2.Math.toDegrees(angle - this._dragStartAngle);
9893
+ let newRotation = this._dragStartRotation + deltaDeg;
9894
+ newRotation = ((newRotation % 360) + 360) % 360;
9895
+ this.update(this._draggingId, { rotation: newRotation });
9896
+ }
9672
9897
  });
9673
9898
 
9674
9899
  const upId = this.eventModule.registerObserver('mouse:up', () => {
9675
9900
  if (!this._draggingId) return;
9676
9901
  const symbol = this.symbolMap.get(this._draggingId);
9677
- const payload = symbol ? { id: this._draggingId, position: [...symbol.metadata.position] } : null;
9902
+ const payload = symbol
9903
+ ? {
9904
+ id: this._draggingId,
9905
+ position: [...symbol.metadata.position],
9906
+ scale: symbol.metadata.scale,
9907
+ rotation: symbol.metadata.rotation,
9908
+ }
9909
+ : null;
9678
9910
  this._draggingId = null;
9911
+ this._draggingHandle = null;
9679
9912
  this.viewer.scene.screenSpaceCameraController.enableInputs = true;
9680
9913
  if (payload) {
9681
- this._positionChangedCallbacks.forEach((callback) => callback(payload));
9914
+ this._positionChangedCallbacks.forEach((callback) => callback({ id: payload.id, position: payload.position }));
9915
+ this._transformChangedCallbacks.forEach((callback) => callback(payload));
9682
9916
  }
9683
9917
  });
9684
9918
 
@@ -9686,13 +9920,12 @@ class MilitarySymbolModule {
9686
9920
  }
9687
9921
 
9688
9922
  /**
9689
- * 禁用拖拽编辑模式
9923
+ * 禁用军标编辑模式
9690
9924
  */
9691
9925
  disableEditMode() {
9692
9926
  if (!this._editMode) return;
9693
9927
  if (this.eventModule) {
9694
9928
  this._dragObserverIds.forEach((observerId) => {
9695
- // 每个观察者注册时的事件类型固定,分别注销
9696
9929
  ['mouse:down', 'mouse:move', 'mouse:up'].forEach((eventType) => {
9697
9930
  try {
9698
9931
  this.eventModule.unregisterObserver(eventType, observerId);
@@ -9703,13 +9936,18 @@ class MilitarySymbolModule {
9703
9936
  });
9704
9937
  }
9705
9938
  this._dragObserverIds = [];
9939
+ if (this._editSymbolId) {
9940
+ this._removeEditHandles(this._editSymbolId);
9941
+ }
9942
+ this._editSymbolId = null;
9706
9943
  this._draggingId = null;
9944
+ this._draggingHandle = null;
9707
9945
  this.viewer.scene.screenSpaceCameraController.enableInputs = true;
9708
9946
  this._editMode = false;
9709
9947
  }
9710
9948
 
9711
9949
  /**
9712
- * 注册位置变化回调
9950
+ * 注册位置变化回调(仅位置)
9713
9951
  * @param {Function} callback 回调参数 { id, position }
9714
9952
  * @returns {Function} 注销函数
9715
9953
  */
@@ -9717,6 +9955,16 @@ class MilitarySymbolModule {
9717
9955
  this._positionChangedCallbacks.add(callback);
9718
9956
  return () => this._positionChangedCallbacks.delete(callback);
9719
9957
  }
9958
+
9959
+ /**
9960
+ * 注册变换变化回调(位置、缩放、旋转)
9961
+ * @param {Function} callback 回调参数 { id, position, scale, rotation }
9962
+ * @returns {Function} 注销函数
9963
+ */
9964
+ onTransformChanged(callback) {
9965
+ this._transformChangedCallbacks.add(callback);
9966
+ return () => this._transformChangedCallbacks.delete(callback);
9967
+ }
9720
9968
  }
9721
9969
 
9722
9970
  /**