@accelint/map-toolkit 1.5.0 → 2.0.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 (197) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/catalog-info.yaml +4 -4
  3. package/dist/camera/events.js +1 -1
  4. package/dist/camera/index.d.ts +1 -1
  5. package/dist/camera/index.js +1 -1
  6. package/dist/camera/store.d.ts +1 -1
  7. package/dist/camera/store.js +3 -5
  8. package/dist/camera/store.js.map +1 -1
  9. package/dist/camera/types.d.ts +1 -1
  10. package/dist/camera/types.js +1 -1
  11. package/dist/cursor-coordinates/constants.js +1 -1
  12. package/dist/cursor-coordinates/index.d.ts +1 -1
  13. package/dist/cursor-coordinates/index.js +1 -1
  14. package/dist/cursor-coordinates/store.d.ts +1 -1
  15. package/dist/cursor-coordinates/store.js +1 -1
  16. package/dist/cursor-coordinates/types.d.ts +1 -1
  17. package/dist/cursor-coordinates/types.js +1 -1
  18. package/dist/cursor-coordinates/use-cursor-coordinates.d.ts +1 -1
  19. package/dist/cursor-coordinates/use-cursor-coordinates.js +4 -9
  20. package/dist/cursor-coordinates/use-cursor-coordinates.js.map +1 -1
  21. package/dist/deckgl/base-map/constants.js +1 -1
  22. package/dist/deckgl/base-map/controls.d.ts +1 -1
  23. package/dist/deckgl/base-map/controls.js +1 -1
  24. package/dist/deckgl/base-map/events.js +1 -1
  25. package/dist/deckgl/base-map/index.d.ts +1 -1
  26. package/dist/deckgl/base-map/index.js +1 -1
  27. package/dist/deckgl/base-map/provider.d.ts +3 -3
  28. package/dist/deckgl/base-map/provider.js +1 -1
  29. package/dist/deckgl/base-map/types.d.ts +1 -1
  30. package/dist/deckgl/base-map/types.js +1 -1
  31. package/dist/deckgl/index.d.ts +4 -4
  32. package/dist/deckgl/index.js +1 -1
  33. package/dist/deckgl/saved-viewports/index.d.ts +1 -1
  34. package/dist/deckgl/saved-viewports/index.js +1 -1
  35. package/dist/deckgl/saved-viewports/storage.d.ts +1 -1
  36. package/dist/deckgl/saved-viewports/storage.js +5 -10
  37. package/dist/deckgl/saved-viewports/storage.js.map +1 -1
  38. package/dist/deckgl/shapes/display-shape-layer/constants.js +66 -13
  39. package/dist/deckgl/shapes/display-shape-layer/constants.js.map +1 -1
  40. package/dist/deckgl/shapes/display-shape-layer/fiber.d.ts +1 -1
  41. package/dist/deckgl/shapes/display-shape-layer/fiber.js +1 -1
  42. package/dist/deckgl/shapes/display-shape-layer/index.d.ts +74 -35
  43. package/dist/deckgl/shapes/display-shape-layer/index.js +381 -154
  44. package/dist/deckgl/shapes/display-shape-layer/index.js.map +1 -1
  45. package/dist/deckgl/shapes/display-shape-layer/shape-label-layer.js +1 -1
  46. package/dist/deckgl/shapes/display-shape-layer/store.js +1 -1
  47. package/dist/deckgl/shapes/display-shape-layer/types.d.ts +108 -19
  48. package/dist/deckgl/shapes/display-shape-layer/types.js +1 -1
  49. package/dist/deckgl/shapes/display-shape-layer/use-select-shape.d.ts +1 -1
  50. package/dist/deckgl/shapes/display-shape-layer/use-select-shape.js +1 -1
  51. package/dist/deckgl/shapes/display-shape-layer/utils/display-style.js +66 -36
  52. package/dist/deckgl/shapes/display-shape-layer/utils/display-style.js.map +1 -1
  53. package/dist/deckgl/shapes/display-shape-layer/utils/elevation.js +407 -0
  54. package/dist/deckgl/shapes/display-shape-layer/utils/elevation.js.map +1 -0
  55. package/dist/deckgl/shapes/display-shape-layer/utils/icon-config.js +151 -0
  56. package/dist/deckgl/shapes/display-shape-layer/utils/icon-config.js.map +1 -0
  57. package/dist/deckgl/shapes/display-shape-layer/utils/interaction.js +50 -0
  58. package/dist/deckgl/shapes/display-shape-layer/utils/interaction.js.map +1 -0
  59. package/dist/deckgl/shapes/display-shape-layer/utils/labels.d.ts +1 -1
  60. package/dist/deckgl/shapes/display-shape-layer/utils/labels.js +28 -39
  61. package/dist/deckgl/shapes/display-shape-layer/utils/labels.js.map +1 -1
  62. package/dist/deckgl/shapes/draw-shape-layer/constants.js +1 -1
  63. package/dist/deckgl/shapes/draw-shape-layer/events.d.ts +1 -1
  64. package/dist/deckgl/shapes/draw-shape-layer/events.js +1 -1
  65. package/dist/deckgl/shapes/draw-shape-layer/fiber.js +1 -1
  66. package/dist/deckgl/shapes/draw-shape-layer/index.d.ts +1 -1
  67. package/dist/deckgl/shapes/draw-shape-layer/index.js +4 -14
  68. package/dist/deckgl/shapes/draw-shape-layer/index.js.map +1 -1
  69. package/dist/deckgl/shapes/draw-shape-layer/modes/draw-circle-mode-with-tooltip.js +2 -3
  70. package/dist/deckgl/shapes/draw-shape-layer/modes/draw-circle-mode-with-tooltip.js.map +1 -1
  71. package/dist/deckgl/shapes/draw-shape-layer/modes/draw-ellipse-mode-with-tooltip.js +1 -1
  72. package/dist/deckgl/shapes/draw-shape-layer/modes/draw-line-string-mode-with-tooltip.js +2 -19
  73. package/dist/deckgl/shapes/draw-shape-layer/modes/draw-line-string-mode-with-tooltip.js.map +1 -1
  74. package/dist/deckgl/shapes/draw-shape-layer/modes/draw-polygon-mode-with-tooltip.js +2 -32
  75. package/dist/deckgl/shapes/draw-shape-layer/modes/draw-polygon-mode-with-tooltip.js.map +1 -1
  76. package/dist/deckgl/shapes/draw-shape-layer/modes/draw-rectangle-mode-with-tooltip.js +9 -10
  77. package/dist/deckgl/shapes/draw-shape-layer/modes/draw-rectangle-mode-with-tooltip.js.map +1 -1
  78. package/dist/deckgl/shapes/draw-shape-layer/modes/index.js +2 -32
  79. package/dist/deckgl/shapes/draw-shape-layer/modes/index.js.map +1 -1
  80. package/dist/deckgl/shapes/draw-shape-layer/store.js +1 -1
  81. package/dist/deckgl/shapes/draw-shape-layer/types.d.ts +1 -1
  82. package/dist/deckgl/shapes/draw-shape-layer/types.js +1 -1
  83. package/dist/deckgl/shapes/draw-shape-layer/use-draw-shape.d.ts +1 -1
  84. package/dist/deckgl/shapes/draw-shape-layer/use-draw-shape.js +1 -1
  85. package/dist/deckgl/shapes/draw-shape-layer/utils/feature-conversion.js +3 -8
  86. package/dist/deckgl/shapes/draw-shape-layer/utils/feature-conversion.js.map +1 -1
  87. package/dist/deckgl/shapes/edit-shape-layer/constants.js +17 -2
  88. package/dist/deckgl/shapes/edit-shape-layer/constants.js.map +1 -1
  89. package/dist/deckgl/shapes/edit-shape-layer/events.d.ts +1 -1
  90. package/dist/deckgl/shapes/edit-shape-layer/events.js +1 -1
  91. package/dist/deckgl/shapes/edit-shape-layer/fiber.d.ts +1 -1
  92. package/dist/deckgl/shapes/edit-shape-layer/fiber.js +1 -1
  93. package/dist/deckgl/shapes/edit-shape-layer/index.d.ts +5 -2
  94. package/dist/deckgl/shapes/edit-shape-layer/index.js +51 -20
  95. package/dist/deckgl/shapes/edit-shape-layer/index.js.map +1 -1
  96. package/dist/deckgl/shapes/edit-shape-layer/modes/base-transform-mode.js +4 -1
  97. package/dist/deckgl/shapes/edit-shape-layer/modes/base-transform-mode.js.map +1 -1
  98. package/dist/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.js +1 -1
  99. package/dist/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.js.map +1 -1
  100. package/dist/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode.js +1 -1
  101. package/dist/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode.js.map +1 -1
  102. package/dist/deckgl/shapes/edit-shape-layer/modes/index.js +1 -1
  103. package/dist/deckgl/shapes/edit-shape-layer/modes/point-translate-mode.js +1 -1
  104. package/dist/deckgl/shapes/edit-shape-layer/modes/point-translate-mode.js.map +1 -1
  105. package/dist/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap.js +1 -1
  106. package/dist/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap.js.map +1 -1
  107. package/dist/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform.js +1 -1
  108. package/dist/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform.js.map +1 -1
  109. package/dist/deckgl/shapes/edit-shape-layer/modes/vertex-transform-mode.js +1 -1
  110. package/dist/deckgl/shapes/edit-shape-layer/modes/vertex-transform-mode.js.map +1 -1
  111. package/dist/deckgl/shapes/edit-shape-layer/store.js +70 -12
  112. package/dist/deckgl/shapes/edit-shape-layer/store.js.map +1 -1
  113. package/dist/deckgl/shapes/edit-shape-layer/types.d.ts +14 -2
  114. package/dist/deckgl/shapes/edit-shape-layer/types.js +1 -1
  115. package/dist/deckgl/shapes/edit-shape-layer/use-edit-shape.d.ts +1 -1
  116. package/dist/deckgl/shapes/edit-shape-layer/use-edit-shape.js +1 -1
  117. package/dist/deckgl/shapes/index.d.ts +4 -4
  118. package/dist/deckgl/shapes/index.js +1 -1
  119. package/dist/deckgl/shapes/shared/constants.d.ts +4 -3
  120. package/dist/deckgl/shapes/shared/constants.js +50 -10
  121. package/dist/deckgl/shapes/shared/constants.js.map +1 -1
  122. package/dist/deckgl/shapes/shared/events.d.ts +5 -1
  123. package/dist/deckgl/shapes/shared/events.js +1 -1
  124. package/dist/deckgl/shapes/shared/events.js.map +1 -1
  125. package/dist/deckgl/shapes/shared/hooks/use-shift-zoom-disable.js +19 -16
  126. package/dist/deckgl/shapes/shared/hooks/use-shift-zoom-disable.js.map +1 -1
  127. package/dist/deckgl/shapes/shared/types.d.ts +174 -53
  128. package/dist/deckgl/shapes/shared/types.js +155 -2
  129. package/dist/deckgl/shapes/shared/types.js.map +1 -1
  130. package/dist/deckgl/shapes/shared/utils/geometry-measurements.js +29 -24
  131. package/dist/deckgl/shapes/shared/utils/geometry-measurements.js.map +1 -1
  132. package/dist/deckgl/shapes/shared/utils/layer-config.js +8 -5
  133. package/dist/deckgl/shapes/shared/utils/layer-config.js.map +1 -1
  134. package/dist/deckgl/shapes/shared/utils/mode-utils.js +50 -20
  135. package/dist/deckgl/shapes/shared/utils/mode-utils.js.map +1 -1
  136. package/dist/deckgl/shapes/shared/utils/pick-filtering.js +22 -15
  137. package/dist/deckgl/shapes/shared/utils/pick-filtering.js.map +1 -1
  138. package/dist/deckgl/shapes/shared/utils/style-utils.d.ts +38 -14
  139. package/dist/deckgl/shapes/shared/utils/style-utils.js +43 -32
  140. package/dist/deckgl/shapes/shared/utils/style-utils.js.map +1 -1
  141. package/dist/deckgl/symbol-layer/fiber.d.ts +1 -1
  142. package/dist/deckgl/symbol-layer/fiber.js +1 -1
  143. package/dist/deckgl/symbol-layer/index.d.ts +1 -1
  144. package/dist/deckgl/symbol-layer/index.js +1 -1
  145. package/dist/deckgl/text-layer/character-sets.js +1 -1
  146. package/dist/deckgl/text-layer/default-settings.d.ts +1 -1
  147. package/dist/deckgl/text-layer/default-settings.js +1 -1
  148. package/dist/deckgl/text-layer/fiber.d.ts +1 -1
  149. package/dist/deckgl/text-layer/fiber.js +1 -1
  150. package/dist/deckgl/text-layer/index.d.ts +1 -1
  151. package/dist/deckgl/text-layer/index.js +1 -1
  152. package/dist/deckgl/text-settings.d.ts +3 -3
  153. package/dist/deckgl/text-settings.js +1 -1
  154. package/dist/map-cursor/events.js +1 -1
  155. package/dist/map-cursor/index.d.ts +1 -1
  156. package/dist/map-cursor/index.js +1 -1
  157. package/dist/map-cursor/store.d.ts +1 -1
  158. package/dist/map-cursor/store.js +1 -1
  159. package/dist/map-cursor/types.d.ts +1 -1
  160. package/dist/map-cursor/types.js +1 -1
  161. package/dist/map-cursor/use-map-cursor.d.ts +1 -1
  162. package/dist/map-cursor/use-map-cursor.js +1 -1
  163. package/dist/map-mode/events.js +1 -1
  164. package/dist/map-mode/index.d.ts +1 -1
  165. package/dist/map-mode/index.js +1 -1
  166. package/dist/map-mode/store.d.ts +1 -1
  167. package/dist/map-mode/store.js +3 -8
  168. package/dist/map-mode/store.js.map +1 -1
  169. package/dist/map-mode/types.d.ts +1 -1
  170. package/dist/map-mode/types.js +1 -1
  171. package/dist/map-mode/use-map-mode.d.ts +1 -1
  172. package/dist/map-mode/use-map-mode.js +1 -1
  173. package/dist/maplibre/hooks/use-maplibre.d.ts +1 -1
  174. package/dist/maplibre/hooks/use-maplibre.js +1 -1
  175. package/dist/maplibre/index.d.ts +1 -1
  176. package/dist/maplibre/index.js +1 -1
  177. package/dist/shared/cleanup.d.ts +1 -1
  178. package/dist/shared/cleanup.js +1 -1
  179. package/dist/shared/constants.js +1 -1
  180. package/dist/shared/create-map-store.d.ts +1 -1
  181. package/dist/shared/create-map-store.js +1 -1
  182. package/dist/shared/logger.js +31 -0
  183. package/dist/shared/logger.js.map +1 -0
  184. package/dist/shared/units.js +1 -1
  185. package/dist/viewport/index.d.ts +1 -1
  186. package/dist/viewport/index.js +1 -1
  187. package/dist/viewport/store.d.ts +1 -1
  188. package/dist/viewport/store.js +1 -1
  189. package/dist/viewport/types.d.ts +1 -1
  190. package/dist/viewport/types.js +1 -1
  191. package/dist/viewport/utils.d.ts +1 -1
  192. package/dist/viewport/utils.js +1 -1
  193. package/dist/viewport/viewport-size.d.ts +3 -3
  194. package/dist/viewport/viewport-size.js +1 -1
  195. package/package.json +22 -20
  196. package/dist/hotkey-manager/dist/react/use-hotkey.js +0 -39
  197. package/dist/hotkey-manager/dist/react/use-hotkey.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
2
+ * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.
3
3
  * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
4
  * you may not use this file except in compliance with the License. You may obtain a copy
5
5
  * of the License at https://www.apache.org/licenses/LICENSE-2.0
@@ -13,19 +13,17 @@
13
13
 
14
14
  'use client';
15
15
 
16
- import { ShapeFeatureType } from "../shared/types.js";
17
16
  import { COMPLETION_EDIT_TYPES, CONTINUOUS_EDIT_TYPES } from "../shared/constants.js";
18
- import { EDIT_SHAPE_LAYER_ID } from "./constants.js";
19
- import { cancelEditingFromLayer, editStore, saveEditingFromLayer, updateFeatureFromLayer } from "./store.js";
17
+ import { DEFAULT_HOTKEY_CONFIG, EDIT_SHAPE_LAYER_ID, SHAPE_PROPERTY_MAP } from "./constants.js";
18
+ import { cancelEditingFromLayer, disableEditPanning, editStore, enableEditPanning, saveEditingFromLayer, updateFeatureFromLayer } from "./store.js";
20
19
  import { MapContext } from "../../base-map/provider.js";
21
20
  import { getFillColor, getLineColor } from "../shared/utils/style-utils.js";
22
21
  import { useShiftZoomDisable } from "../shared/hooks/use-shift-zoom-disable.js";
23
22
  import { getDefaultEditableLayerProps } from "../shared/utils/layer-config.js";
24
- import { useHotkey } from "../../../hotkey-manager/dist/react/use-hotkey.js";
25
23
  import { getEditModeInstance } from "./modes/index.js";
26
- import { useCallback, useContext, useEffect, useMemo, useRef } from "react";
24
+ import { useCallback, useContext, useEffect, useRef } from "react";
25
+ import { Keycode, globalBind, registerHotkey, unregisterHotkey } from "@accelint/hotkey-manager";
27
26
  import { jsx } from "react/jsx-runtime";
28
- import { Keycode, globalBind, registerHotkey } from "@accelint/hotkey-manager";
29
27
 
30
28
  //#region src/deckgl/shapes/edit-shape-layer/index.tsx
31
29
  /**
@@ -58,11 +56,13 @@ function isCompletionEditType(editType) {
58
56
  *
59
57
  * For rectangles, adds the `shape: 'Rectangle'` property required by ModifyMode's
60
58
  * lockRectangles feature. ModifyMode checks `properties.shape === 'Rectangle'`.
59
+ *
60
+ * @param feature - The GeoJSON Feature to wrap.
61
+ * @param shape - The shape type, used to determine if mode-specific properties are needed.
62
+ * @returns A GeoJSON FeatureCollection containing the single feature.
61
63
  */
62
64
  function toFeatureCollection(feature, shape) {
63
- let shapeProperty;
64
- if (shape === ShapeFeatureType.Circle) shapeProperty = "Circle";
65
- else if (shape === ShapeFeatureType.Rectangle) shapeProperty = "Rectangle";
65
+ const shapeProperty = SHAPE_PROPERTY_MAP[shape];
66
66
  return {
67
67
  type: "FeatureCollection",
68
68
  features: [shapeProperty ? {
@@ -111,8 +111,10 @@ function toFeatureCollection(feature, shape) {
111
111
  * );
112
112
  * }
113
113
  * ```
114
+ *
115
+ * @throws {Error} Throws if neither `mapId` prop nor `MapProvider` context is available.
114
116
  */
115
- function EditShapeLayer({ id = EDIT_SHAPE_LAYER_ID, mapId, unit }) {
117
+ function EditShapeLayer({ id = EDIT_SHAPE_LAYER_ID, mapId, unit, hotkeyConfig = DEFAULT_HOTKEY_CONFIG }) {
116
118
  const contextId = useContext(MapContext);
117
119
  const actualMapId = mapId ?? contextId;
118
120
  if (!actualMapId) throw new Error("EditShapeLayer requires either a mapId prop or to be used within a MapProvider");
@@ -130,16 +132,45 @@ function EditShapeLayer({ id = EDIT_SHAPE_LAYER_ID, mapId, unit }) {
130
132
  pendingUpdateRef.current = null;
131
133
  }
132
134
  }, []);
133
- useHotkey(useMemo(() => registerHotkey({
134
- id: `saveEditHotkey-${actualMapId}`,
135
- key: { code: Keycode.Enter },
136
- onKeyUp: () => {
137
- if (editingStateRef.current?.editingShape) {
138
- cancelPendingUpdate();
139
- saveEditingFromLayer(actualMapId);
135
+ useEffect(() => {
136
+ const manager = registerHotkey({
137
+ id: `saveEditHotkey-${actualMapId}`,
138
+ key: { code: Keycode.Enter },
139
+ onKeyUp: () => {
140
+ if (editingStateRef.current?.editingShape) {
141
+ cancelPendingUpdate();
142
+ saveEditingFromLayer(actualMapId);
143
+ }
140
144
  }
141
- }
142
- }), [actualMapId, cancelPendingUpdate]));
145
+ });
146
+ const unbind = manager.bind();
147
+ return () => {
148
+ unbind();
149
+ unregisterHotkey(manager);
150
+ };
151
+ }, [actualMapId, cancelPendingUpdate]);
152
+ useEffect(() => {
153
+ const manager = registerHotkey({
154
+ id: `editPanningHotkey-${actualMapId}`,
155
+ key: hotkeyConfig.panning,
156
+ onKeyDown: (e) => {
157
+ if (editStore.get(actualMapId)?.editingShape) e.preventDefault();
158
+ },
159
+ onKeyHeld: () => {
160
+ enableEditPanning(actualMapId);
161
+ },
162
+ onKeyUp: () => {
163
+ disableEditPanning(actualMapId);
164
+ },
165
+ heldThresholdMs: 50,
166
+ alwaysTriggerKeyUp: true
167
+ });
168
+ const unbind = manager.bind();
169
+ return () => {
170
+ unbind();
171
+ unregisterHotkey(manager);
172
+ };
173
+ }, [actualMapId, hotkeyConfig]);
143
174
  useShiftZoomDisable(actualMapId, isEditing);
144
175
  useEffect(() => {
145
176
  return () => {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["shapeProperty: string | undefined"],"sources":["../../../../src/deckgl/shapes/edit-shape-layer/index.tsx"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n'use client';\n\nimport { globalBind, Keycode, registerHotkey } from '@accelint/hotkey-manager';\nimport { useHotkey } from '@accelint/hotkey-manager/react';\nimport { useCallback, useContext, useEffect, useMemo, useRef } from 'react';\nimport { MapContext } from '../../base-map/provider';\nimport { useShiftZoomDisable } from '../shared/hooks/use-shift-zoom-disable';\nimport { ShapeFeatureType, type ShapeFeatureTypeValues } from '../shared/types';\nimport { getDefaultEditableLayerProps } from '../shared/utils/layer-config';\nimport { getFillColor, getLineColor } from '../shared/utils/style-utils';\nimport {\n COMPLETION_EDIT_TYPES,\n CONTINUOUS_EDIT_TYPES,\n EDIT_SHAPE_LAYER_ID,\n} from './constants';\nimport { getEditModeInstance } from './modes';\nimport {\n cancelEditingFromLayer,\n editStore,\n saveEditingFromLayer,\n updateFeatureFromLayer,\n} from './store';\nimport type {\n EditAction,\n FeatureCollection,\n} from '@deck.gl-community/editable-layers';\nimport type { Feature } from 'geojson';\nimport type { EditShapeLayerProps } from './types';\n\n/**\n * Check if an edit type is a continuous event (fires during drag).\n * Continuous events are batched with RAF for smooth performance.\n *\n * @param editType - The edit type string from EditableGeoJsonLayer\n * @returns True if the edit type is continuous (e.g., 'translating', 'scaling', 'rotating')\n */\nfunction isContinuousEditType(editType: string): boolean {\n return CONTINUOUS_EDIT_TYPES.has(editType);\n}\n\n/**\n * Check if an edit type is a completion event (fires at drag end).\n * Completion events update state immediately without RAF batching.\n *\n * @param editType - The edit type string from EditableGeoJsonLayer\n * @returns True if the edit type is a completion event (e.g., 'translated', 'scaled', 'rotated')\n */\nfunction isCompletionEditType(editType: string): boolean {\n return COMPLETION_EDIT_TYPES.has(editType);\n}\n\n/**\n * Convert a GeoJSON Feature to a FeatureCollection for EditableGeoJsonLayer.\n * The editable-layers library accepts standard GeoJSON FeatureCollection at runtime,\n * but has stricter internal types. We use the geojson types which are compatible.\n *\n * For circles, adds the `shape: 'Circle'` property required by ResizeCircleMode.\n * ResizeCircleMode checks `properties.shape.includes('Circle')` to identify circles.\n *\n * For rectangles, adds the `shape: 'Rectangle'` property required by ModifyMode's\n * lockRectangles feature. ModifyMode checks `properties.shape === 'Rectangle'`.\n */\nfunction toFeatureCollection(\n feature: Feature,\n shape: ShapeFeatureTypeValues,\n): import('geojson').FeatureCollection {\n // Add shape property for modes that require it\n // - ResizeCircleMode requires shape: 'Circle'\n // - ModifyMode lockRectangles requires shape: 'Rectangle'\n let shapeProperty: string | undefined;\n if (shape === ShapeFeatureType.Circle) {\n shapeProperty = 'Circle';\n } else if (shape === ShapeFeatureType.Rectangle) {\n shapeProperty = 'Rectangle';\n }\n\n const featureWithShape = shapeProperty\n ? {\n ...feature,\n properties: {\n ...feature.properties,\n shape: shapeProperty,\n },\n }\n : feature;\n\n return {\n type: 'FeatureCollection',\n features: [featureWithShape],\n };\n}\n\n/**\n * EditShapeLayer - A React component for editing existing shapes on the map.\n *\n * This component wraps the EditableGeoJsonLayer from @deck.gl-community/editable-layers\n * and integrates with the map-mode and map-cursor systems for proper coordination.\n *\n * Key features:\n * - Renders only when actively editing (returns null otherwise)\n * - Uses cached mode instances to prevent deck.gl assertion errors\n * - Integrates with the editing store for state management\n * - Neutral mode authorization (lets UI decide how to handle mode conflicts)\n * - Circles use ResizeCircleMode with tooltip, other shapes use ModifyMode\n * - Fill colors rendered at 20% opacity, edit handles are white\n * - Live dimension/area tooltips during editing\n * - requestAnimationFrame() batching for smooth drag performance\n *\n * @example\n * ```tsx\n * // Import the fiber registration for JSX support\n * import '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/fiber';\n * import '@accelint/map-toolkit/deckgl/shapes/display-shape-layer/fiber';\n *\n * function Map({ mapId }) {\n * const { editingShape } = useEditShape(mapId);\n *\n * return (\n * <BaseMap id={mapId}>\n * <displayShapeLayer\n * data={shapes}\n * mapId={mapId}\n * selectedShapeId={editingShape?.id}\n * />\n * <EditShapeLayer mapId={mapId} />\n * </BaseMap>\n * );\n * }\n * ```\n */\nexport function EditShapeLayer({\n id = EDIT_SHAPE_LAYER_ID,\n mapId,\n unit,\n}: EditShapeLayerProps) {\n // Get mapId from context if not provided\n const contextId = useContext(MapContext);\n const actualMapId = mapId ?? contextId;\n\n if (!actualMapId) {\n throw new Error(\n 'EditShapeLayer requires either a mapId prop or to be used within a MapProvider',\n );\n }\n\n // Subscribe to editing state using the v2 store API\n const { state: editingState } = editStore.use(actualMapId);\n\n const isEditing = editingState?.editingShape != null;\n\n // RAF batching for movePosition events to reduce React updates during drag\n const pendingUpdateRef = useRef<{\n feature: Feature;\n rafId: number;\n } | null>(null);\n\n // Keep a ref to the latest editing state so the hotkey handler can access it\n const editingStateRef = useRef(editingState);\n editingStateRef.current = editingState;\n\n // Ensure global hotkey listeners are initialized\n // Safe to call multiple times - globalBind() checks if already bound\n useEffect(() => {\n globalBind();\n }, []);\n\n // Helper to cancel any pending RAF update (stable reference with useCallback)\n const cancelPendingUpdate = useCallback(() => {\n if (pendingUpdateRef.current) {\n cancelAnimationFrame(pendingUpdateRef.current.rafId);\n pendingUpdateRef.current = null;\n }\n }, []);\n\n // Register Enter key hotkey scoped to this component and map instance\n // Handler checks if actively editing to prevent conflicts with other Enter key uses\n const saveEditHotkey = useMemo(\n () =>\n registerHotkey({\n id: `saveEditHotkey-${actualMapId}`,\n key: { code: Keycode.Enter },\n onKeyUp: () => {\n // Use ref to get current editing state, avoiding stale closure\n if (editingStateRef.current?.editingShape) {\n cancelPendingUpdate();\n saveEditingFromLayer(actualMapId);\n }\n },\n }),\n [actualMapId, cancelPendingUpdate],\n );\n\n useHotkey(saveEditHotkey);\n\n // Disable zoom while Shift is held during editing\n // This prevents boxZoom (Shift+drag) from interfering with Shift modifier constraints\n // (e.g., Shift for uniform scaling, Shift for rotation snap)\n useShiftZoomDisable(actualMapId, isEditing);\n\n // Cleanup RAF on unmount\n useEffect(() => {\n return () => {\n if (pendingUpdateRef.current) {\n cancelAnimationFrame(pendingUpdateRef.current.rafId);\n }\n };\n }, []);\n\n // If not editing, return null (don't render the editable layer)\n if (!editingState?.editingShape) {\n return null;\n }\n\n const { editingShape, editMode, featureBeingEdited } = editingState;\n\n // Get the cached mode instance\n const mode = getEditModeInstance(editMode);\n\n // Use the live feature being edited, or fall back to original shape\n const featureToRender = featureBeingEdited ?? editingShape.feature;\n const data = toFeatureCollection(featureToRender, editingShape.shape);\n\n // Handle edit events from EditableGeoJsonLayer\n const handleEdit = ({\n updatedData,\n editType,\n }: EditAction<FeatureCollection>) => {\n const feature = updatedData.features[0];\n\n // Continuous events (during drag): batch with RAF for smooth performance\n if (isContinuousEditType(editType) && feature) {\n cancelPendingUpdate();\n const rafId = requestAnimationFrame(() => {\n updateFeatureFromLayer(actualMapId, feature as Feature);\n pendingUpdateRef.current = null;\n });\n pendingUpdateRef.current = { feature: feature as Feature, rafId };\n return;\n }\n\n // Completion events (drag end): update immediately\n if (isCompletionEditType(editType)) {\n cancelPendingUpdate();\n if (feature) {\n updateFeatureFromLayer(actualMapId, feature as Feature);\n }\n return;\n }\n\n // ESC key cancellation\n if (editType === 'cancelFeature') {\n cancelPendingUpdate();\n cancelEditingFromLayer(actualMapId);\n }\n };\n\n // Get colors from the shape's existing style properties with base opacity applied\n const fillColor = getFillColor(editingShape.feature, true);\n const lineColor = getLineColor(editingShape.feature);\n\n return (\n <editableGeoJsonLayer\n id={id}\n data={data}\n mode={mode}\n selectedFeatureIndexes={[0]}\n onEdit={handleEdit}\n getFillColor={fillColor}\n getLineColor={lineColor}\n getTentativeFillColor={fillColor}\n getTentativeLineColor={lineColor}\n {...getDefaultEditableLayerProps(unit)}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAS,qBAAqB,UAA2B;AACvD,QAAO,sBAAsB,IAAI,SAAS;;;;;;;;;AAU5C,SAAS,qBAAqB,UAA2B;AACvD,QAAO,sBAAsB,IAAI,SAAS;;;;;;;;;;;;;AAc5C,SAAS,oBACP,SACA,OACqC;CAIrC,IAAIA;AACJ,KAAI,UAAU,iBAAiB,OAC7B,iBAAgB;UACP,UAAU,iBAAiB,UACpC,iBAAgB;AAalB,QAAO;EACL,MAAM;EACN,UAAU,CAZa,gBACrB;GACE,GAAG;GACH,YAAY;IACV,GAAG,QAAQ;IACX,OAAO;IACR;GACF,GACD,QAI0B;EAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCH,SAAgB,eAAe,EAC7B,KAAK,qBACL,OACA,QACsB;CAEtB,MAAM,YAAY,WAAW,WAAW;CACxC,MAAM,cAAc,SAAS;AAE7B,KAAI,CAAC,YACH,OAAM,IAAI,MACR,iFACD;CAIH,MAAM,EAAE,OAAO,iBAAiB,UAAU,IAAI,YAAY;CAE1D,MAAM,YAAY,cAAc,gBAAgB;CAGhD,MAAM,mBAAmB,OAGf,KAAK;CAGf,MAAM,kBAAkB,OAAO,aAAa;AAC5C,iBAAgB,UAAU;AAI1B,iBAAgB;AACd,cAAY;IACX,EAAE,CAAC;CAGN,MAAM,sBAAsB,kBAAkB;AAC5C,MAAI,iBAAiB,SAAS;AAC5B,wBAAqB,iBAAiB,QAAQ,MAAM;AACpD,oBAAiB,UAAU;;IAE5B,EAAE,CAAC;AAoBN,WAhBuB,cAEnB,eAAe;EACb,IAAI,kBAAkB;EACtB,KAAK,EAAE,MAAM,QAAQ,OAAO;EAC5B,eAAe;AAEb,OAAI,gBAAgB,SAAS,cAAc;AACzC,yBAAqB;AACrB,yBAAqB,YAAY;;;EAGtC,CAAC,EACJ,CAAC,aAAa,oBAAoB,CACnC,CAEwB;AAKzB,qBAAoB,aAAa,UAAU;AAG3C,iBAAgB;AACd,eAAa;AACX,OAAI,iBAAiB,QACnB,sBAAqB,iBAAiB,QAAQ,MAAM;;IAGvD,EAAE,CAAC;AAGN,KAAI,CAAC,cAAc,aACjB,QAAO;CAGT,MAAM,EAAE,cAAc,UAAU,uBAAuB;CAGvD,MAAM,OAAO,oBAAoB,SAAS;CAI1C,MAAM,OAAO,oBADW,sBAAsB,aAAa,SACT,aAAa,MAAM;CAGrE,MAAM,cAAc,EAClB,aACA,eACmC;EACnC,MAAM,UAAU,YAAY,SAAS;AAGrC,MAAI,qBAAqB,SAAS,IAAI,SAAS;AAC7C,wBAAqB;AAKrB,oBAAiB,UAAU;IAAW;IAAoB,OAJ5C,4BAA4B;AACxC,4BAAuB,aAAa,QAAmB;AACvD,sBAAiB,UAAU;MAC3B;IAC+D;AACjE;;AAIF,MAAI,qBAAqB,SAAS,EAAE;AAClC,wBAAqB;AACrB,OAAI,QACF,wBAAuB,aAAa,QAAmB;AAEzD;;AAIF,MAAI,aAAa,iBAAiB;AAChC,wBAAqB;AACrB,0BAAuB,YAAY;;;CAKvC,MAAM,YAAY,aAAa,aAAa,SAAS,KAAK;CAC1D,MAAM,YAAY,aAAa,aAAa,QAAQ;AAEpD,QACE,oBAAC;EACK;EACE;EACA;EACN,wBAAwB,CAAC,EAAE;EAC3B,QAAQ;EACR,cAAc;EACd,cAAc;EACd,uBAAuB;EACvB,uBAAuB;EACvB,GAAI,6BAA6B,KAAK;GACtC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/deckgl/shapes/edit-shape-layer/index.tsx"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n'use client';\n\nimport {\n globalBind,\n Keycode,\n registerHotkey,\n unregisterHotkey,\n} from '@accelint/hotkey-manager';\nimport { useCallback, useContext, useEffect, useRef } from 'react';\nimport { MapContext } from '../../base-map/provider';\nimport { useShiftZoomDisable } from '../shared/hooks/use-shift-zoom-disable';\nimport { getDefaultEditableLayerProps } from '../shared/utils/layer-config';\nimport { getFillColor, getLineColor } from '../shared/utils/style-utils';\nimport {\n COMPLETION_EDIT_TYPES,\n CONTINUOUS_EDIT_TYPES,\n DEFAULT_HOTKEY_CONFIG,\n EDIT_SHAPE_LAYER_ID,\n SHAPE_PROPERTY_MAP,\n} from './constants';\nimport { getEditModeInstance } from './modes';\nimport {\n cancelEditingFromLayer,\n disableEditPanning,\n editStore,\n enableEditPanning,\n saveEditingFromLayer,\n updateFeatureFromLayer,\n} from './store';\nimport type {\n EditAction,\n FeatureCollection,\n} from '@deck.gl-community/editable-layers';\nimport type { Feature } from 'geojson';\nimport type { ShapeFeatureType } from '../shared/types';\nimport type { EditShapeLayerProps } from './types';\n\n/**\n * Check if an edit type is a continuous event (fires during drag).\n * Continuous events are batched with RAF for smooth performance.\n *\n * @param editType - The edit type string from EditableGeoJsonLayer\n * @returns True if the edit type is continuous (e.g., 'translating', 'scaling', 'rotating')\n */\nfunction isContinuousEditType(editType: string): boolean {\n return CONTINUOUS_EDIT_TYPES.has(editType);\n}\n\n/**\n * Check if an edit type is a completion event (fires at drag end).\n * Completion events update state immediately without RAF batching.\n *\n * @param editType - The edit type string from EditableGeoJsonLayer\n * @returns True if the edit type is a completion event (e.g., 'translated', 'scaled', 'rotated')\n */\nfunction isCompletionEditType(editType: string): boolean {\n return COMPLETION_EDIT_TYPES.has(editType);\n}\n\n/**\n * Convert a GeoJSON Feature to a FeatureCollection for EditableGeoJsonLayer.\n * The editable-layers library accepts standard GeoJSON FeatureCollection at runtime,\n * but has stricter internal types. We use the geojson types which are compatible.\n *\n * For circles, adds the `shape: 'Circle'` property required by ResizeCircleMode.\n * ResizeCircleMode checks `properties.shape.includes('Circle')` to identify circles.\n *\n * For rectangles, adds the `shape: 'Rectangle'` property required by ModifyMode's\n * lockRectangles feature. ModifyMode checks `properties.shape === 'Rectangle'`.\n *\n * @param feature - The GeoJSON Feature to wrap.\n * @param shape - The shape type, used to determine if mode-specific properties are needed.\n * @returns A GeoJSON FeatureCollection containing the single feature.\n */\nfunction toFeatureCollection(\n feature: Feature,\n shape: ShapeFeatureType,\n): import('geojson').FeatureCollection {\n // Add shape property for modes that require it\n // - ResizeCircleMode requires shape: 'Circle'\n // - ModifyMode lockRectangles requires shape: 'Rectangle'\n const shapeProperty = SHAPE_PROPERTY_MAP[shape];\n\n const featureWithShape = shapeProperty\n ? {\n ...feature,\n properties: {\n ...feature.properties,\n shape: shapeProperty,\n },\n }\n : feature;\n\n return {\n type: 'FeatureCollection',\n features: [featureWithShape],\n };\n}\n\n/**\n * EditShapeLayer - A React component for editing existing shapes on the map.\n *\n * This component wraps the EditableGeoJsonLayer from @deck.gl-community/editable-layers\n * and integrates with the map-mode and map-cursor systems for proper coordination.\n *\n * Key features:\n * - Renders only when actively editing (returns null otherwise)\n * - Uses cached mode instances to prevent deck.gl assertion errors\n * - Integrates with the editing store for state management\n * - Neutral mode authorization (lets UI decide how to handle mode conflicts)\n * - Circles use ResizeCircleMode with tooltip, other shapes use ModifyMode\n * - Fill colors rendered at 20% opacity, edit handles are white\n * - Live dimension/area tooltips during editing\n * - requestAnimationFrame() batching for smooth drag performance\n *\n * @example\n * ```tsx\n * // Import the fiber registration for JSX support\n * import '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/fiber';\n * import '@accelint/map-toolkit/deckgl/shapes/display-shape-layer/fiber';\n *\n * function Map({ mapId }) {\n * const { editingShape } = useEditShape(mapId);\n *\n * return (\n * <BaseMap id={mapId}>\n * <displayShapeLayer\n * data={shapes}\n * mapId={mapId}\n * selectedShapeId={editingShape?.id}\n * />\n * <EditShapeLayer mapId={mapId} />\n * </BaseMap>\n * );\n * }\n * ```\n *\n * @throws {Error} Throws if neither `mapId` prop nor `MapProvider` context is available.\n */\nexport function EditShapeLayer({\n id = EDIT_SHAPE_LAYER_ID,\n mapId,\n unit,\n hotkeyConfig = DEFAULT_HOTKEY_CONFIG,\n}: EditShapeLayerProps) {\n // Get mapId from context if not provided\n const contextId = useContext(MapContext);\n const actualMapId = mapId ?? contextId;\n\n if (!actualMapId) {\n throw new Error(\n 'EditShapeLayer requires either a mapId prop or to be used within a MapProvider',\n );\n }\n\n // Subscribe to editing state using the v2 store API\n const { state: editingState } = editStore.use(actualMapId);\n\n const isEditing = editingState?.editingShape != null;\n\n // RAF batching for movePosition events to reduce React updates during drag\n const pendingUpdateRef = useRef<{\n feature: Feature;\n rafId: number;\n } | null>(null);\n\n // Keep a ref to the latest editing state so the hotkey handler can access it\n const editingStateRef = useRef(editingState);\n editingStateRef.current = editingState;\n\n // Ensure global hotkey listeners are initialized\n // Safe to call multiple times - globalBind() checks if already bound\n useEffect(() => {\n globalBind();\n }, []);\n\n // Helper to cancel any pending RAF update (stable reference with useCallback)\n const cancelPendingUpdate = useCallback(() => {\n if (pendingUpdateRef.current) {\n cancelAnimationFrame(pendingUpdateRef.current.rafId);\n pendingUpdateRef.current = null;\n }\n }, []);\n\n // Register Enter key hotkey scoped to this component and map instance.\n // Handles the full lifecycle: register → bind → unbind → unregister.\n // Without unregistering on cleanup, remounting throws a duplicate-id error.\n useEffect(() => {\n const manager = registerHotkey({\n id: `saveEditHotkey-${actualMapId}`,\n key: { code: Keycode.Enter },\n onKeyUp: () => {\n if (editingStateRef.current?.editingShape) {\n cancelPendingUpdate();\n saveEditingFromLayer(actualMapId);\n }\n },\n });\n\n const unbind = manager.bind();\n\n return () => {\n unbind();\n unregisterHotkey(manager);\n };\n }, [actualMapId, cancelPendingUpdate]);\n\n // Register Space key for enabling panning while editing shape.\n // NOTE: Low threshold (50ms) enables near-instant panning response on hold.\n // alwaysTriggerKeyUp ensures panning ends even if onKeyHeld was triggered.\n useEffect(() => {\n const manager = registerHotkey({\n id: `editPanningHotkey-${actualMapId}`,\n key: hotkeyConfig.panning,\n onKeyDown: (e: KeyboardEvent) => {\n if (editStore.get(actualMapId)?.editingShape) {\n e.preventDefault();\n }\n },\n onKeyHeld: () => {\n enableEditPanning(actualMapId);\n },\n onKeyUp: () => {\n disableEditPanning(actualMapId);\n },\n heldThresholdMs: 50,\n alwaysTriggerKeyUp: true,\n });\n\n const unbind = manager.bind();\n\n return () => {\n unbind();\n unregisterHotkey(manager);\n };\n }, [actualMapId, hotkeyConfig]);\n\n // Disable zoom while Shift is held during editing\n // This prevents boxZoom (Shift+drag) from interfering with Shift modifier constraints\n // (e.g., Shift for uniform scaling, Shift for rotation snap)\n useShiftZoomDisable(actualMapId, isEditing);\n\n // Cleanup RAF on unmount\n useEffect(() => {\n return () => {\n if (pendingUpdateRef.current) {\n cancelAnimationFrame(pendingUpdateRef.current.rafId);\n }\n };\n }, []);\n\n // If not editing, return null (don't render the editable layer)\n if (!editingState?.editingShape) {\n return null;\n }\n\n const { editingShape, editMode, featureBeingEdited } = editingState;\n\n // Get the cached mode instance\n const mode = getEditModeInstance(editMode);\n\n // Use the live feature being edited, or fall back to original shape\n const featureToRender = featureBeingEdited ?? editingShape.feature;\n const data = toFeatureCollection(featureToRender, editingShape.shape);\n\n // Handle edit events from EditableGeoJsonLayer\n const handleEdit = ({\n updatedData,\n editType,\n }: EditAction<FeatureCollection>) => {\n const feature = updatedData.features[0];\n\n // Continuous events (during drag): batch with RAF for smooth performance\n if (isContinuousEditType(editType) && feature) {\n cancelPendingUpdate();\n const rafId = requestAnimationFrame(() => {\n updateFeatureFromLayer(actualMapId, feature as Feature);\n pendingUpdateRef.current = null;\n });\n pendingUpdateRef.current = { feature: feature as Feature, rafId };\n return;\n }\n\n // Completion events (drag end): update immediately\n if (isCompletionEditType(editType)) {\n cancelPendingUpdate();\n if (feature) {\n updateFeatureFromLayer(actualMapId, feature as Feature);\n }\n return;\n }\n\n // ESC key cancellation\n if (editType === 'cancelFeature') {\n cancelPendingUpdate();\n cancelEditingFromLayer(actualMapId);\n }\n };\n\n // Get colors from the shape's existing style properties with base opacity applied\n const fillColor = getFillColor(editingShape.feature, true);\n const lineColor = getLineColor(editingShape.feature);\n\n return (\n <editableGeoJsonLayer\n id={id}\n data={data}\n mode={mode}\n selectedFeatureIndexes={[0]}\n onEdit={handleEdit}\n getFillColor={fillColor}\n getLineColor={lineColor}\n getTentativeFillColor={fillColor}\n getTentativeLineColor={lineColor}\n {...getDefaultEditableLayerProps(unit)}\n />\n );\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
2
+ * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.
3
3
  * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
4
  * you may not use this file except in compliance with the License. You may obtain a copy
5
5
  * of the License at https://www.apache.org/licenses/LICENSE-2.0
@@ -80,6 +80,9 @@ import { CompositeMode } from "@deck.gl-community/editable-layers";
80
80
  * ```
81
81
  */
82
82
  var BaseTransformMode = class extends CompositeMode {
83
+ constructor(modes) {
84
+ super(modes);
85
+ }
83
86
  /** Track which mode is currently handling the drag operation */
84
87
  activeDragMode = null;
85
88
  /** Track current Shift state for dynamic modifier behavior */
@@ -1 +1 @@
1
- {"version":3,"file":"base-transform-mode.js","names":["updatedCursor: string | null | undefined","propsWithConfig: ModeProps<FeatureCollection>","filteredProps: ModeProps<FeatureCollection>"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/base-transform-mode.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n CompositeMode,\n type DraggingEvent,\n type FeatureCollection,\n type GeoJsonEditMode,\n type GuideFeatureCollection,\n type ModeProps,\n type PointerMoveEvent,\n type StartDraggingEvent,\n type StopDraggingEvent,\n type Tooltip,\n} from '@deck.gl-community/editable-layers';\nimport { filterGeometryAwarePicks } from '../../shared/utils/pick-filtering';\n\n/**\n * Configuration for how a mode handles the Shift key modifier.\n */\nexport type ShiftKeyConfig = {\n /** The modeConfig property name to set (e.g., 'lockScaling', 'snapRotation') */\n configKey: string;\n /** The value to set when Shift is held (defaults to true) */\n value?: boolean;\n};\n\n/**\n * Definition for how to detect and handle a specific edit handle type.\n */\nexport type HandleMatcher = {\n /** Function to determine if a pick matches this handle type */\n match: (pick: {\n isGuide?: boolean;\n object?: {\n properties?: {\n guideType?: string;\n editHandleType?: string;\n mode?: string;\n };\n };\n }) => boolean;\n /** The mode instance to delegate to when this handle is matched */\n mode: GeoJsonEditMode;\n /** Optional Shift key configuration for this mode */\n shiftConfig?: ShiftKeyConfig;\n};\n\n/**\n * Abstract base class for composite transform modes.\n *\n * This class extracts the common patterns shared by CircleTransformMode,\n * BoundingTransformMode, and VertexTransformMode:\n *\n * ## Core Responsibilities\n * - **Active mode tracking**: Tracks which child mode is handling the drag operation\n * - **Cursor aggregation**: Combines cursor updates from all child modes\n * - **Pick-based delegation**: Selects mode at drag start based on picked handles\n * - **Shift key modifiers**: Dynamic Shift key handling for scale/rotate operations\n * - **State management**: Clean state reset on drag stop\n * - **Pick filtering**: Prevents TypeError from sublayer elements without geometry\n *\n * ## Implementation Pattern\n * Subclasses define their specific child modes and handle matchers, while this\n * base class handles the delegation logic. The pattern is:\n *\n * 1. Define child mode instances in constructor\n * 2. Implement `getHandleMatchers()` to map picks to modes\n * 3. Implement `getDefaultMode()` for fallback behavior\n * 4. Optionally override `onDragging()` for tooltips or side effects\n *\n * ## How Mode Selection Works\n * At drag start, the base class evaluates handle matchers in order:\n * - First matcher that matches any pick wins\n * - If no matcher matches, uses the default mode\n * - Selected mode remains active for the entire drag operation\n *\n * @example Implementing a custom transform mode\n * ```typescript\n * import { BaseTransformMode, type HandleMatcher } from './base-transform-mode';\n * import { TranslateMode, RotateMode } from '@deck.gl-community/editable-layers';\n *\n * class MyTransformMode extends BaseTransformMode {\n * private translateMode: TranslateMode;\n * private rotateMode: RotateMode;\n *\n * constructor() {\n * const translateMode = new TranslateMode();\n * const rotateMode = new RotateMode();\n * super([rotateMode, translateMode]); // Order matters for cursor handling\n *\n * this.translateMode = translateMode;\n * this.rotateMode = rotateMode;\n * }\n *\n * protected getHandleMatchers(): HandleMatcher[] {\n * return [\n * {\n * match: (pick) =>\n * Boolean(pick.isGuide && pick.object?.properties?.editHandleType === 'rotate'),\n * mode: this.rotateMode,\n * shiftConfig: { configKey: 'snapRotation' },\n * },\n * ];\n * }\n *\n * protected getDefaultMode() {\n * return this.translateMode; // Dragging body translates by default\n * }\n * }\n * ```\n */\nexport abstract class BaseTransformMode extends CompositeMode {\n /** Track which mode is currently handling the drag operation */\n protected activeDragMode: GeoJsonEditMode | null = null;\n\n /** Track current Shift state for dynamic modifier behavior */\n protected isShiftHeld = false;\n\n /** Tooltip for operations that show live measurements */\n protected tooltip: Tooltip | null = null;\n\n /**\n * Get the handle matchers that define how picks map to modes.\n * Matchers are evaluated in order; first match wins.\n * The last matcher should typically be a catch-all for the default mode.\n */\n protected abstract getHandleMatchers(): HandleMatcher[];\n\n /**\n * Get the default mode to use when no handle matchers match.\n * This is typically TranslateMode for dragging the shape body.\n */\n protected abstract getDefaultMode(): GeoJsonEditMode;\n\n /**\n * Optional hook called during drag when the active mode is set.\n * Subclasses can override to update tooltips or perform other side effects.\n */\n protected onDragging?(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void;\n\n /**\n * Aggregates cursor updates from all child modes.\n * The first non-null cursor from any mode is used.\n */\n override handlePointerMove(\n event: PointerMoveEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n let updatedCursor: string | null | undefined = null;\n\n super.handlePointerMove(event, {\n ...props,\n onUpdateCursor: (cursor: string | null | undefined) => {\n updatedCursor = cursor || updatedCursor;\n },\n });\n\n props.onUpdateCursor(updatedCursor);\n }\n\n /**\n * Determines which mode should handle the drag based on picked handles.\n * Cancels map panning and delegates to the matched mode.\n */\n override handleStartDragging(\n event: StartDraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n if (event.picks.length) {\n event.cancelPan();\n }\n\n const picks = event.picks ?? [];\n const matchers = this.getHandleMatchers();\n\n // Find the first matcher that matches any pick\n for (const matcher of matchers) {\n if (picks.some(matcher.match)) {\n this.activeDragMode = matcher.mode;\n break;\n }\n }\n\n // Fall back to default mode if no matcher matched\n if (!this.activeDragMode) {\n this.activeDragMode = this.getDefaultMode();\n }\n\n this.activeDragMode.handleStartDragging(event, props);\n }\n\n /**\n * Delegates dragging to the active mode with Shift key handling.\n * Reads Shift state from the source event and applies configured modifiers.\n */\n override handleDragging(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n if (!this.activeDragMode) {\n return;\n }\n\n const sourceEvent = event.sourceEvent as KeyboardEvent | undefined;\n this.isShiftHeld = sourceEvent?.shiftKey ?? false;\n\n // Find the matcher for the active mode to get shift config\n const matchers = this.getHandleMatchers();\n const activeMatcher = matchers.find((m) => m.mode === this.activeDragMode);\n const shiftConfig = activeMatcher?.shiftConfig;\n\n // Apply shift key modifier if configured\n if (shiftConfig && this.isShiftHeld) {\n const propsWithConfig: ModeProps<FeatureCollection> = {\n ...props,\n modeConfig: {\n ...props.modeConfig,\n [shiftConfig.configKey]: shiftConfig.value ?? true,\n },\n };\n this.activeDragMode.handleDragging(event, propsWithConfig);\n } else {\n this.activeDragMode.handleDragging(event, props);\n }\n\n // Call subclass hook for tooltips or other side effects\n this.onDragging?.(event, props);\n }\n\n /**\n * Delegates stop dragging to the active mode with final Shift state.\n * Resets all drag-related state.\n */\n override handleStopDragging(\n event: StopDraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n if (!this.activeDragMode) {\n return;\n }\n\n // Find the matcher for the active mode to get shift config\n const matchers = this.getHandleMatchers();\n const activeMatcher = matchers.find((m) => m.mode === this.activeDragMode);\n const shiftConfig = activeMatcher?.shiftConfig;\n\n // Apply shift key modifier if configured (use last known state)\n if (shiftConfig && this.isShiftHeld) {\n const propsWithConfig: ModeProps<FeatureCollection> = {\n ...props,\n modeConfig: {\n ...props.modeConfig,\n [shiftConfig.configKey]: shiftConfig.value ?? true,\n },\n };\n this.activeDragMode.handleStopDragging(event, propsWithConfig);\n } else {\n this.activeDragMode.handleStopDragging(event, props);\n }\n\n this.resetDragState();\n }\n\n /**\n * Returns tooltips for display during drag operations.\n * Subclasses update `this.tooltip` in their `onDragging` hook.\n */\n override getTooltips(): Tooltip[] {\n return this.tooltip ? [this.tooltip] : [];\n }\n\n /**\n * Filters picks to prevent TypeError from sublayer elements without geometry.\n *\n * Some child modes (like ModifyMode, ResizeCircleMode) access\n * `pick.object.geometry.type` which throws if the pick doesn't have\n * a geometry property. This happens when picks include sublayer elements\n * like tooltip text that aren't GeoJSON features.\n */\n override getGuides(\n props: ModeProps<FeatureCollection>,\n ): GuideFeatureCollection {\n const picks = props.lastPointerMoveEvent?.picks;\n\n if (picks && picks.length > 0) {\n const { filteredPicks, didFilter } = filterGeometryAwarePicks(picks);\n\n if (didFilter) {\n const filteredProps: ModeProps<FeatureCollection> = {\n ...props,\n lastPointerMoveEvent: {\n ...props.lastPointerMoveEvent,\n picks: filteredPicks,\n },\n };\n return super.getGuides(filteredProps);\n }\n }\n\n return super.getGuides(props);\n }\n\n /**\n * Resets drag-related state. Called after drag stops.\n * Subclasses can override to reset additional state but should call super.\n */\n protected resetDragState(): void {\n this.activeDragMode = null;\n this.isShiftHeld = false;\n this.tooltip = null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyHA,IAAsB,oBAAtB,cAAgD,cAAc;;CAE5D,AAAU,iBAAyC;;CAGnD,AAAU,cAAc;;CAGxB,AAAU,UAA0B;;;;;CA4BpC,AAAS,kBACP,OACA,OACA;EACA,IAAIA,gBAA2C;AAE/C,QAAM,kBAAkB,OAAO;GAC7B,GAAG;GACH,iBAAiB,WAAsC;AACrD,oBAAgB,UAAU;;GAE7B,CAAC;AAEF,QAAM,eAAe,cAAc;;;;;;CAOrC,AAAS,oBACP,OACA,OACA;AACA,MAAI,MAAM,MAAM,OACd,OAAM,WAAW;EAGnB,MAAM,QAAQ,MAAM,SAAS,EAAE;EAC/B,MAAM,WAAW,KAAK,mBAAmB;AAGzC,OAAK,MAAM,WAAW,SACpB,KAAI,MAAM,KAAK,QAAQ,MAAM,EAAE;AAC7B,QAAK,iBAAiB,QAAQ;AAC9B;;AAKJ,MAAI,CAAC,KAAK,eACR,MAAK,iBAAiB,KAAK,gBAAgB;AAG7C,OAAK,eAAe,oBAAoB,OAAO,MAAM;;;;;;CAOvD,AAAS,eACP,OACA,OACA;AACA,MAAI,CAAC,KAAK,eACR;AAIF,OAAK,cADe,MAAM,aACM,YAAY;EAK5C,MAAM,cAFW,KAAK,mBAAmB,CACV,MAAM,MAAM,EAAE,SAAS,KAAK,eAAe,EACvC;AAGnC,MAAI,eAAe,KAAK,aAAa;GACnC,MAAMC,kBAAgD;IACpD,GAAG;IACH,YAAY;KACV,GAAG,MAAM;MACR,YAAY,YAAY,YAAY,SAAS;KAC/C;IACF;AACD,QAAK,eAAe,eAAe,OAAO,gBAAgB;QAE1D,MAAK,eAAe,eAAe,OAAO,MAAM;AAIlD,OAAK,aAAa,OAAO,MAAM;;;;;;CAOjC,AAAS,mBACP,OACA,OACA;AACA,MAAI,CAAC,KAAK,eACR;EAMF,MAAM,cAFW,KAAK,mBAAmB,CACV,MAAM,MAAM,EAAE,SAAS,KAAK,eAAe,EACvC;AAGnC,MAAI,eAAe,KAAK,aAAa;GACnC,MAAMA,kBAAgD;IACpD,GAAG;IACH,YAAY;KACV,GAAG,MAAM;MACR,YAAY,YAAY,YAAY,SAAS;KAC/C;IACF;AACD,QAAK,eAAe,mBAAmB,OAAO,gBAAgB;QAE9D,MAAK,eAAe,mBAAmB,OAAO,MAAM;AAGtD,OAAK,gBAAgB;;;;;;CAOvB,AAAS,cAAyB;AAChC,SAAO,KAAK,UAAU,CAAC,KAAK,QAAQ,GAAG,EAAE;;;;;;;;;;CAW3C,AAAS,UACP,OACwB;EACxB,MAAM,QAAQ,MAAM,sBAAsB;AAE1C,MAAI,SAAS,MAAM,SAAS,GAAG;GAC7B,MAAM,EAAE,eAAe,cAAc,yBAAyB,MAAM;AAEpE,OAAI,WAAW;IACb,MAAMC,gBAA8C;KAClD,GAAG;KACH,sBAAsB;MACpB,GAAG,MAAM;MACT,OAAO;MACR;KACF;AACD,WAAO,MAAM,UAAU,cAAc;;;AAIzC,SAAO,MAAM,UAAU,MAAM;;;;;;CAO/B,AAAU,iBAAuB;AAC/B,OAAK,iBAAiB;AACtB,OAAK,cAAc;AACnB,OAAK,UAAU"}
1
+ {"version":3,"file":"base-transform-mode.js","names":["updatedCursor: string | null | undefined","propsWithConfig: ModeProps<FeatureCollection>","filteredProps: ModeProps<FeatureCollection>"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/base-transform-mode.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n CompositeMode,\n type DraggingEvent,\n type FeatureCollection,\n type GeoJsonEditMode,\n type GuideFeatureCollection,\n type ModeProps,\n type PointerMoveEvent,\n type StartDraggingEvent,\n type StopDraggingEvent,\n type Tooltip,\n} from '@deck.gl-community/editable-layers';\nimport { filterGeometryAwarePicks } from '../../shared/utils/pick-filtering';\n\n/**\n * Configuration for how a mode handles the Shift key modifier.\n */\nexport type ShiftKeyConfig = {\n /** The modeConfig property name to set (e.g., 'lockScaling', 'snapRotation') */\n configKey: string;\n /** The value to set when Shift is held (defaults to true) */\n value?: boolean;\n};\n\n/**\n * Definition for how to detect and handle a specific edit handle type.\n *\n * Note: `mode` is typed as `any` because library modes (TranslateMode, RotateMode,\n * ScaleMode, etc.) narrow ModeProps to SimpleFeatureCollection while GeoJsonEditMode\n * uses FeatureCollection. This makes the concrete mode classes structurally incompatible\n * with their own parent type — a known type inconsistency in @deck.gl-community/editable-layers.\n * At runtime all modes are GeoJsonEditMode subclasses, so the widened type is safe.\n */\nexport type HandleMatcher = {\n /** Function to determine if a pick matches this handle type */\n match: (pick: {\n isGuide?: boolean;\n object?: {\n properties?: {\n guideType?: string;\n editHandleType?: string;\n mode?: string;\n };\n };\n }) => boolean;\n /** The mode instance to delegate to when this handle is matched */\n // biome-ignore lint/suspicious/noExplicitAny: Library type inconsistency — see HandleMatcher JSDoc\n mode: any;\n /** Optional Shift key configuration for this mode */\n shiftConfig?: ShiftKeyConfig;\n};\n\n/**\n * Abstract base class for composite transform modes.\n *\n * This class extracts the common patterns shared by CircleTransformMode,\n * BoundingTransformMode, and VertexTransformMode:\n *\n * ## Core Responsibilities\n * - **Active mode tracking**: Tracks which child mode is handling the drag operation\n * - **Cursor aggregation**: Combines cursor updates from all child modes\n * - **Pick-based delegation**: Selects mode at drag start based on picked handles\n * - **Shift key modifiers**: Dynamic Shift key handling for scale/rotate operations\n * - **State management**: Clean state reset on drag stop\n * - **Pick filtering**: Prevents TypeError from sublayer elements without geometry\n *\n * ## Implementation Pattern\n * Subclasses define their specific child modes and handle matchers, while this\n * base class handles the delegation logic. The pattern is:\n *\n * 1. Define child mode instances in constructor\n * 2. Implement `getHandleMatchers()` to map picks to modes\n * 3. Implement `getDefaultMode()` for fallback behavior\n * 4. Optionally override `onDragging()` for tooltips or side effects\n *\n * ## How Mode Selection Works\n * At drag start, the base class evaluates handle matchers in order:\n * - First matcher that matches any pick wins\n * - If no matcher matches, uses the default mode\n * - Selected mode remains active for the entire drag operation\n *\n * @example Implementing a custom transform mode\n * ```typescript\n * import { BaseTransformMode, type HandleMatcher } from './base-transform-mode';\n * import { TranslateMode, RotateMode } from '@deck.gl-community/editable-layers';\n *\n * class MyTransformMode extends BaseTransformMode {\n * private translateMode: TranslateMode;\n * private rotateMode: RotateMode;\n *\n * constructor() {\n * const translateMode = new TranslateMode();\n * const rotateMode = new RotateMode();\n * super([rotateMode, translateMode]); // Order matters for cursor handling\n *\n * this.translateMode = translateMode;\n * this.rotateMode = rotateMode;\n * }\n *\n * protected getHandleMatchers(): HandleMatcher[] {\n * return [\n * {\n * match: (pick) =>\n * Boolean(pick.isGuide && pick.object?.properties?.editHandleType === 'rotate'),\n * mode: this.rotateMode,\n * shiftConfig: { configKey: 'snapRotation' },\n * },\n * ];\n * }\n *\n * protected getDefaultMode() {\n * return this.translateMode; // Dragging body translates by default\n * }\n * }\n * ```\n */\nexport abstract class BaseTransformMode extends CompositeMode {\n // biome-ignore lint/complexity/noUselessConstructor: Widens parameter type from GeoJsonEditMode[] to any[] — see HandleMatcher JSDoc\n // biome-ignore lint/suspicious/noExplicitAny: Library type inconsistency — see HandleMatcher JSDoc\n constructor(modes: any[]) {\n super(modes);\n }\n\n /** Track which mode is currently handling the drag operation */\n protected activeDragMode: GeoJsonEditMode | null = null;\n\n /** Track current Shift state for dynamic modifier behavior */\n protected isShiftHeld = false;\n\n /** Tooltip for operations that show live measurements */\n protected tooltip: Tooltip | null = null;\n\n /**\n * Get the handle matchers that define how picks map to modes.\n * Matchers are evaluated in order; first match wins.\n * The last matcher should typically be a catch-all for the default mode.\n */\n protected abstract getHandleMatchers(): HandleMatcher[];\n\n /**\n * Get the default mode to use when no handle matchers match.\n * This is typically TranslateMode for dragging the shape body.\n */\n protected abstract getDefaultMode(): GeoJsonEditMode;\n\n /**\n * Optional hook called during drag when the active mode is set.\n * Subclasses can override to update tooltips or perform other side effects.\n */\n protected onDragging?(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void;\n\n /**\n * Aggregates cursor updates from all child modes.\n * The first non-null cursor from any mode is used.\n */\n override handlePointerMove(\n event: PointerMoveEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n let updatedCursor: string | null | undefined = null;\n\n super.handlePointerMove(event, {\n ...props,\n onUpdateCursor: (cursor: string | null | undefined) => {\n updatedCursor = cursor || updatedCursor;\n },\n });\n\n props.onUpdateCursor(updatedCursor);\n }\n\n /**\n * Determines which mode should handle the drag based on picked handles.\n * Cancels map panning and delegates to the matched mode.\n */\n override handleStartDragging(\n event: StartDraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n if (event.picks.length) {\n event.cancelPan();\n }\n\n const picks = event.picks ?? [];\n const matchers = this.getHandleMatchers();\n\n // Find the first matcher that matches any pick\n for (const matcher of matchers) {\n if (picks.some(matcher.match)) {\n this.activeDragMode = matcher.mode;\n break;\n }\n }\n\n // Fall back to default mode if no matcher matched\n if (!this.activeDragMode) {\n this.activeDragMode = this.getDefaultMode();\n }\n\n this.activeDragMode.handleStartDragging(event, props);\n }\n\n /**\n * Delegates dragging to the active mode with Shift key handling.\n * Reads Shift state from the source event and applies configured modifiers.\n */\n override handleDragging(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n if (!this.activeDragMode) {\n return;\n }\n\n const sourceEvent = event.sourceEvent as KeyboardEvent | undefined;\n this.isShiftHeld = sourceEvent?.shiftKey ?? false;\n\n // Find the matcher for the active mode to get shift config\n const matchers = this.getHandleMatchers();\n const activeMatcher = matchers.find((m) => m.mode === this.activeDragMode);\n const shiftConfig = activeMatcher?.shiftConfig;\n\n // Apply shift key modifier if configured\n if (shiftConfig && this.isShiftHeld) {\n const propsWithConfig: ModeProps<FeatureCollection> = {\n ...props,\n modeConfig: {\n ...props.modeConfig,\n [shiftConfig.configKey]: shiftConfig.value ?? true,\n },\n };\n this.activeDragMode.handleDragging(event, propsWithConfig);\n } else {\n this.activeDragMode.handleDragging(event, props);\n }\n\n // Call subclass hook for tooltips or other side effects\n this.onDragging?.(event, props);\n }\n\n /**\n * Delegates stop dragging to the active mode with final Shift state.\n * Resets all drag-related state.\n */\n override handleStopDragging(\n event: StopDraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n if (!this.activeDragMode) {\n return;\n }\n\n // Find the matcher for the active mode to get shift config\n const matchers = this.getHandleMatchers();\n const activeMatcher = matchers.find((m) => m.mode === this.activeDragMode);\n const shiftConfig = activeMatcher?.shiftConfig;\n\n // Apply shift key modifier if configured (use last known state)\n if (shiftConfig && this.isShiftHeld) {\n const propsWithConfig: ModeProps<FeatureCollection> = {\n ...props,\n modeConfig: {\n ...props.modeConfig,\n [shiftConfig.configKey]: shiftConfig.value ?? true,\n },\n };\n this.activeDragMode.handleStopDragging(event, propsWithConfig);\n } else {\n this.activeDragMode.handleStopDragging(event, props);\n }\n\n this.resetDragState();\n }\n\n /**\n * Returns tooltips for display during drag operations.\n * Subclasses update `this.tooltip` in their `onDragging` hook.\n */\n override getTooltips(): Tooltip[] {\n return this.tooltip ? [this.tooltip] : [];\n }\n\n /**\n * Filters picks to prevent TypeError from sublayer elements without geometry.\n *\n * Some child modes (like ModifyMode, ResizeCircleMode) access\n * `pick.object.geometry.type` which throws if the pick doesn't have\n * a geometry property. This happens when picks include sublayer elements\n * like tooltip text that aren't GeoJSON features.\n */\n override getGuides(\n props: ModeProps<FeatureCollection>,\n ): GuideFeatureCollection {\n const picks = props.lastPointerMoveEvent?.picks;\n\n if (picks && picks.length > 0) {\n const { filteredPicks, didFilter } = filterGeometryAwarePicks(picks);\n\n if (didFilter) {\n const filteredProps: ModeProps<FeatureCollection> = {\n ...props,\n lastPointerMoveEvent: {\n ...props.lastPointerMoveEvent,\n picks: filteredPicks,\n },\n };\n return super.getGuides(filteredProps);\n }\n }\n\n return super.getGuides(props);\n }\n\n /**\n * Resets drag-related state. Called after drag stops.\n * Subclasses can override to reset additional state but should call super.\n */\n protected resetDragState(): void {\n this.activeDragMode = null;\n this.isShiftHeld = false;\n this.tooltip = null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgIA,IAAsB,oBAAtB,cAAgD,cAAc;CAG5D,YAAY,OAAc;AACxB,QAAM,MAAM;;;CAId,AAAU,iBAAyC;;CAGnD,AAAU,cAAc;;CAGxB,AAAU,UAA0B;;;;;CA4BpC,AAAS,kBACP,OACA,OACA;EACA,IAAIA,gBAA2C;AAE/C,QAAM,kBAAkB,OAAO;GAC7B,GAAG;GACH,iBAAiB,WAAsC;AACrD,oBAAgB,UAAU;;GAE7B,CAAC;AAEF,QAAM,eAAe,cAAc;;;;;;CAOrC,AAAS,oBACP,OACA,OACA;AACA,MAAI,MAAM,MAAM,OACd,OAAM,WAAW;EAGnB,MAAM,QAAQ,MAAM,SAAS,EAAE;EAC/B,MAAM,WAAW,KAAK,mBAAmB;AAGzC,OAAK,MAAM,WAAW,SACpB,KAAI,MAAM,KAAK,QAAQ,MAAM,EAAE;AAC7B,QAAK,iBAAiB,QAAQ;AAC9B;;AAKJ,MAAI,CAAC,KAAK,eACR,MAAK,iBAAiB,KAAK,gBAAgB;AAG7C,OAAK,eAAe,oBAAoB,OAAO,MAAM;;;;;;CAOvD,AAAS,eACP,OACA,OACA;AACA,MAAI,CAAC,KAAK,eACR;AAIF,OAAK,cADe,MAAM,aACM,YAAY;EAK5C,MAAM,cAFW,KAAK,mBAAmB,CACV,MAAM,MAAM,EAAE,SAAS,KAAK,eAAe,EACvC;AAGnC,MAAI,eAAe,KAAK,aAAa;GACnC,MAAMC,kBAAgD;IACpD,GAAG;IACH,YAAY;KACV,GAAG,MAAM;MACR,YAAY,YAAY,YAAY,SAAS;KAC/C;IACF;AACD,QAAK,eAAe,eAAe,OAAO,gBAAgB;QAE1D,MAAK,eAAe,eAAe,OAAO,MAAM;AAIlD,OAAK,aAAa,OAAO,MAAM;;;;;;CAOjC,AAAS,mBACP,OACA,OACA;AACA,MAAI,CAAC,KAAK,eACR;EAMF,MAAM,cAFW,KAAK,mBAAmB,CACV,MAAM,MAAM,EAAE,SAAS,KAAK,eAAe,EACvC;AAGnC,MAAI,eAAe,KAAK,aAAa;GACnC,MAAMA,kBAAgD;IACpD,GAAG;IACH,YAAY;KACV,GAAG,MAAM;MACR,YAAY,YAAY,YAAY,SAAS;KAC/C;IACF;AACD,QAAK,eAAe,mBAAmB,OAAO,gBAAgB;QAE9D,MAAK,eAAe,mBAAmB,OAAO,MAAM;AAGtD,OAAK,gBAAgB;;;;;;CAOvB,AAAS,cAAyB;AAChC,SAAO,KAAK,UAAU,CAAC,KAAK,QAAQ,GAAG,EAAE;;;;;;;;;;CAW3C,AAAS,UACP,OACwB;EACxB,MAAM,QAAQ,MAAM,sBAAsB;AAE1C,MAAI,SAAS,MAAM,SAAS,GAAG;GAC7B,MAAM,EAAE,eAAe,cAAc,yBAAyB,MAAM;AAEpE,OAAI,WAAW;IACb,MAAMC,gBAA8C;KAClD,GAAG;KACH,sBAAsB;MACpB,GAAG,MAAM;MACT,OAAO;MACR;KACF;AACD,WAAO,MAAM,UAAU,cAAc;;;AAIzC,SAAO,MAAM,UAAU,MAAM;;;;;;CAO/B,AAAU,iBAAuB;AAC/B,OAAK,iBAAiB;AACtB,OAAK,cAAc;AACnB,OAAK,UAAU"}
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
2
+ * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.
3
3
  * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
4
  * you may not use this file except in compliance with the License. You may obtain a copy
5
5
  * of the License at https://www.apache.org/licenses/LICENSE-2.0
@@ -1 +1 @@
1
- {"version":3,"file":"bounding-transform-mode.js","names":["guidesToFilterOut: string[]","text: string"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n type DraggingEvent,\n type FeatureCollection,\n type GeoJsonEditMode,\n type GuideFeatureCollection,\n type ModeProps,\n TranslateMode,\n} from '@deck.gl-community/editable-layers';\nimport { featureCollection } from '@turf/helpers';\nimport {\n DEFAULT_DISTANCE_UNITS,\n getDistanceUnitAbbreviation,\n} from '@/shared/units';\nimport {\n formatEllipseTooltip,\n formatRectangleTooltip,\n} from '../../shared/constants';\nimport {\n computeEllipseMeasurementsFromPolygon,\n computeRectangleMeasurementsFromCorners,\n} from '../../shared/utils/geometry-measurements';\nimport { BaseTransformMode, type HandleMatcher } from './base-transform-mode';\nimport { RotateModeWithSnap } from './rotate-mode-with-snap';\nimport { ScaleModeWithFreeTransform } from './scale-mode-with-free-transform';\nimport type { Feature, Polygon } from 'geojson';\n\n/**\n * Transform mode for shapes that use bounding box manipulation (no vertex editing).\n *\n * Use this mode for shapes like ellipses and rectangles where individual vertex\n * editing is not meaningful or desired. Instead, shapes are manipulated via their\n * bounding box handles.\n *\n * ## Capabilities\n * This composite mode provides:\n * - **Translation** (TranslateMode): Drag the shape body to move it\n * - **Scaling** (ScaleModeWithFreeTransform): Drag corner handles to resize\n * - Default: Non-uniform scaling (can stretch/squish)\n * - With Shift: Uniform scaling (maintains aspect ratio)\n * - **Rotation** (RotateModeWithSnap): Drag top handle to rotate\n * - Default: Free rotation\n * - With Shift: Snap to 45° intervals\n * - **Live tooltip**: Shows dimensions and area during scaling\n *\n * ## Differences from VertexTransformMode\n * Unlike VertexTransformMode, this mode does NOT include vertex editing handles.\n * This prevents accidental distortion of shapes that have specific geometric constraints\n * (e.g., ellipses must maintain their elliptical shape, rectangles must maintain right angles).\n *\n * ## Handle Priority Logic\n * When drag starts, modes are evaluated in this priority order:\n * 1. If hovering over a scale handle → scaling takes priority\n * 2. If hovering over the rotate handle → rotation takes priority\n * 3. Otherwise → dragging the shape body translates it\n *\n * @example\n * ```typescript\n * import { BoundingTransformMode } from '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode';\n * import { EditableGeoJsonLayer } from '@deck.gl-community/editable-layers';\n *\n * // Used internally by EditShapeLayer for rectangles and ellipses\n * const mode = new BoundingTransformMode();\n *\n * const layer = new EditableGeoJsonLayer({\n * mode,\n * data: rectangleFeatureCollection,\n * selectedFeatureIndexes: [0],\n * onEdit: handleEdit,\n * // ... other props\n * });\n * ```\n */\nexport class BoundingTransformMode extends BaseTransformMode {\n private translateMode: TranslateMode;\n private scaleMode: ScaleModeWithFreeTransform;\n private rotateMode: RotateModeWithSnap;\n\n constructor() {\n const translateMode = new TranslateMode();\n const scaleMode = new ScaleModeWithFreeTransform();\n const rotateMode = new RotateModeWithSnap();\n\n // Order: scale and rotate first so their handles take priority over translate\n super([scaleMode, rotateMode, translateMode]);\n\n this.translateMode = translateMode;\n this.scaleMode = scaleMode;\n this.rotateMode = rotateMode;\n }\n\n protected override getHandleMatchers(): HandleMatcher[] {\n return [\n {\n // Scale handle: corner handles on bounding box\n match: (pick) =>\n Boolean(\n pick.isGuide && pick.object?.properties?.editHandleType === 'scale',\n ),\n mode: this.scaleMode,\n shiftConfig: { configKey: 'lockScaling' },\n },\n {\n // Rotate handle: top handle on bounding box\n match: (pick) =>\n Boolean(\n pick.isGuide &&\n pick.object?.properties?.editHandleType === 'rotate',\n ),\n mode: this.rotateMode,\n shiftConfig: { configKey: 'snapRotation' },\n },\n ];\n }\n\n protected override getDefaultMode(): GeoJsonEditMode {\n return this.translateMode;\n }\n\n /**\n * Update tooltip with shape dimensions during scaling.\n */\n protected override onDragging(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n // Only show tooltip when scaling\n if (this.activeDragMode !== this.scaleMode) {\n return;\n }\n\n this.updateShapeTooltip(event, props);\n }\n\n /**\n * Override getGuides to filter duplicate envelope guides.\n *\n * Both ScaleMode and RotateMode render the same bounding box envelope.\n * We keep scale's envelope and filter rotate's duplicate.\n * We also hide scale handles while rotating to avoid visual clutter.\n */\n override getGuides(\n props: ModeProps<FeatureCollection>,\n ): GuideFeatureCollection {\n const allGuides = super.getGuides(props);\n\n // biome-ignore lint/suspicious/noExplicitAny: Guide properties vary by mode, safely accessing with optional chaining\n const filteredGuides = allGuides.features.filter((guide: any) => {\n const properties = guide.properties || {};\n const editHandleType = properties.editHandleType;\n const mode = properties.mode;\n\n // Both scale and rotate modes have the same enveloping box as a guide - only need one\n const guidesToFilterOut: string[] = [mode as string];\n\n // Do not render scaling edit handles if rotating\n if (this.rotateMode.getIsRotating()) {\n guidesToFilterOut.push(editHandleType as string);\n }\n\n return !guidesToFilterOut.includes('scale');\n });\n\n // biome-ignore lint/suspicious/noExplicitAny: turf types mismatch with editable-layers GeoJSON types\n return featureCollection(filteredGuides as any) as any;\n }\n\n /**\n * Update the tooltip with shape dimensions and area.\n * Called during scaling to show live measurements.\n * Handles both rectangles (5 points) and ellipses (many points).\n */\n private updateShapeTooltip(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n const { mapCoords } = event;\n const distanceUnits =\n props.modeConfig?.distanceUnits ?? DEFAULT_DISTANCE_UNITS;\n\n // Get the selected feature\n const selectedIndexes = props.selectedIndexes;\n const selectedIndex = selectedIndexes?.[0];\n if (selectedIndex === undefined) {\n this.tooltip = null;\n return;\n }\n\n const feature = props.data.features[selectedIndex] as\n | Feature<Polygon>\n | undefined;\n if (!feature || feature.geometry.type !== 'Polygon') {\n this.tooltip = null;\n return;\n }\n\n const coordinates = feature.geometry.coordinates[0];\n if (!coordinates || coordinates.length < 4) {\n this.tooltip = null;\n return;\n }\n\n // Check if this is a rectangle (has shape: 'Rectangle' property)\n const isRectangle = feature.properties?.shape === 'Rectangle';\n\n let text: string;\n const unitAbbrev = getDistanceUnitAbbreviation(distanceUnits);\n\n if (isRectangle) {\n // Rectangle: calculate width and height from corners\n const corner0 = coordinates[0] as [number, number];\n const corner1 = coordinates[1] as [number, number];\n const corner2 = coordinates[2] as [number, number];\n\n const { width, height, area } = computeRectangleMeasurementsFromCorners(\n corner0,\n corner1,\n corner2,\n distanceUnits,\n );\n\n text = formatRectangleTooltip(width, height, area, unitAbbrev);\n } else {\n // Ellipse: calculate major/minor axes using consolidated utility\n const {\n majorAxis,\n minorAxis,\n area: ellipseArea,\n } = computeEllipseMeasurementsFromPolygon(\n coordinates as [number, number][],\n distanceUnits,\n );\n\n text = formatEllipseTooltip(\n majorAxis,\n minorAxis,\n ellipseArea,\n unitAbbrev,\n );\n }\n\n // Position tooltip at cursor - offset is applied via getPixelOffset in sublayer props\n this.tooltip = {\n position: mapCoords,\n text,\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,IAAa,wBAAb,cAA2C,kBAAkB;CAC3D,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,cAAc;EACZ,MAAM,gBAAgB,IAAI,eAAe;EACzC,MAAM,YAAY,IAAI,4BAA4B;EAClD,MAAM,aAAa,IAAI,oBAAoB;AAG3C,QAAM;GAAC;GAAW;GAAY;GAAc,CAAC;AAE7C,OAAK,gBAAgB;AACrB,OAAK,YAAY;AACjB,OAAK,aAAa;;CAGpB,AAAmB,oBAAqC;AACtD,SAAO,CACL;GAEE,QAAQ,SACN,QACE,KAAK,WAAW,KAAK,QAAQ,YAAY,mBAAmB,QAC7D;GACH,MAAM,KAAK;GACX,aAAa,EAAE,WAAW,eAAe;GAC1C,EACD;GAEE,QAAQ,SACN,QACE,KAAK,WACH,KAAK,QAAQ,YAAY,mBAAmB,SAC/C;GACH,MAAM,KAAK;GACX,aAAa,EAAE,WAAW,gBAAgB;GAC3C,CACF;;CAGH,AAAmB,iBAAkC;AACnD,SAAO,KAAK;;;;;CAMd,AAAmB,WACjB,OACA,OACM;AAEN,MAAI,KAAK,mBAAmB,KAAK,UAC/B;AAGF,OAAK,mBAAmB,OAAO,MAAM;;;;;;;;;CAUvC,AAAS,UACP,OACwB;AAqBxB,SAAO,kBApBW,MAAM,UAAU,MAAM,CAGP,SAAS,QAAQ,UAAe;GAC/D,MAAM,aAAa,MAAM,cAAc,EAAE;GACzC,MAAM,iBAAiB,WAAW;GAIlC,MAAMA,oBAA8B,CAHvB,WAAW,KAG4B;AAGpD,OAAI,KAAK,WAAW,eAAe,CACjC,mBAAkB,KAAK,eAAyB;AAGlD,UAAO,CAAC,kBAAkB,SAAS,QAAQ;IAC3C,CAG6C;;;;;;;CAQjD,AAAQ,mBACN,OACA,OACA;EACA,MAAM,EAAE,cAAc;EACtB,MAAM,gBACJ,MAAM,YAAY,iBAAiB;EAIrC,MAAM,gBADkB,MAAM,kBACU;AACxC,MAAI,kBAAkB,QAAW;AAC/B,QAAK,UAAU;AACf;;EAGF,MAAM,UAAU,MAAM,KAAK,SAAS;AAGpC,MAAI,CAAC,WAAW,QAAQ,SAAS,SAAS,WAAW;AACnD,QAAK,UAAU;AACf;;EAGF,MAAM,cAAc,QAAQ,SAAS,YAAY;AACjD,MAAI,CAAC,eAAe,YAAY,SAAS,GAAG;AAC1C,QAAK,UAAU;AACf;;EAIF,MAAM,cAAc,QAAQ,YAAY,UAAU;EAElD,IAAIC;EACJ,MAAM,aAAa,4BAA4B,cAAc;AAE7D,MAAI,aAAa;GAEf,MAAM,UAAU,YAAY;GAC5B,MAAM,UAAU,YAAY;GAC5B,MAAM,UAAU,YAAY;GAE5B,MAAM,EAAE,OAAO,QAAQ,SAAS,wCAC9B,SACA,SACA,SACA,cACD;AAED,UAAO,uBAAuB,OAAO,QAAQ,MAAM,WAAW;SACzD;GAEL,MAAM,EACJ,WACA,WACA,MAAM,gBACJ,sCACF,aACA,cACD;AAED,UAAO,qBACL,WACA,WACA,aACA,WACD;;AAIH,OAAK,UAAU;GACb,UAAU;GACV;GACD"}
1
+ {"version":3,"file":"bounding-transform-mode.js","names":["guidesToFilterOut: string[]","text: string"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n type DraggingEvent,\n type FeatureCollection,\n type GeoJsonEditMode,\n type GuideFeatureCollection,\n type ModeProps,\n TranslateMode,\n} from '@deck.gl-community/editable-layers';\nimport { featureCollection } from '@turf/helpers';\nimport {\n DEFAULT_DISTANCE_UNITS,\n getDistanceUnitAbbreviation,\n} from '@/shared/units';\nimport {\n formatEllipseTooltip,\n formatRectangleTooltip,\n} from '../../shared/constants';\nimport {\n computeEllipseMeasurementsFromPolygon,\n computeRectangleMeasurementsFromCorners,\n} from '../../shared/utils/geometry-measurements';\nimport { BaseTransformMode, type HandleMatcher } from './base-transform-mode';\nimport { RotateModeWithSnap } from './rotate-mode-with-snap';\nimport { ScaleModeWithFreeTransform } from './scale-mode-with-free-transform';\nimport type { Feature, Polygon } from 'geojson';\n\n/**\n * Transform mode for shapes that use bounding box manipulation (no vertex editing).\n *\n * Use this mode for shapes like ellipses and rectangles where individual vertex\n * editing is not meaningful or desired. Instead, shapes are manipulated via their\n * bounding box handles.\n *\n * ## Capabilities\n * This composite mode provides:\n * - **Translation** (TranslateMode): Drag the shape body to move it\n * - **Scaling** (ScaleModeWithFreeTransform): Drag corner handles to resize\n * - Default: Non-uniform scaling (can stretch/squish)\n * - With Shift: Uniform scaling (maintains aspect ratio)\n * - **Rotation** (RotateModeWithSnap): Drag top handle to rotate\n * - Default: Free rotation\n * - With Shift: Snap to 45° intervals\n * - **Live tooltip**: Shows dimensions and area during scaling\n *\n * ## Differences from VertexTransformMode\n * Unlike VertexTransformMode, this mode does NOT include vertex editing handles.\n * This prevents accidental distortion of shapes that have specific geometric constraints\n * (e.g., ellipses must maintain their elliptical shape, rectangles must maintain right angles).\n *\n * ## Handle Priority Logic\n * When drag starts, modes are evaluated in this priority order:\n * 1. If hovering over a scale handle → scaling takes priority\n * 2. If hovering over the rotate handle → rotation takes priority\n * 3. Otherwise → dragging the shape body translates it\n *\n * @example\n * ```typescript\n * import { BoundingTransformMode } from '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode';\n * import { EditableGeoJsonLayer } from '@deck.gl-community/editable-layers';\n *\n * // Used internally by EditShapeLayer for rectangles and ellipses\n * const mode = new BoundingTransformMode();\n *\n * const layer = new EditableGeoJsonLayer({\n * mode,\n * data: rectangleFeatureCollection,\n * selectedFeatureIndexes: [0],\n * onEdit: handleEdit,\n * // ... other props\n * });\n * ```\n */\nexport class BoundingTransformMode extends BaseTransformMode {\n private translateMode: TranslateMode;\n private scaleMode: ScaleModeWithFreeTransform;\n private rotateMode: RotateModeWithSnap;\n\n constructor() {\n const translateMode = new TranslateMode();\n const scaleMode = new ScaleModeWithFreeTransform();\n const rotateMode = new RotateModeWithSnap();\n\n // Order: scale and rotate first so their handles take priority over translate\n super([scaleMode, rotateMode, translateMode]);\n\n this.translateMode = translateMode;\n this.scaleMode = scaleMode;\n this.rotateMode = rotateMode;\n }\n\n protected override getHandleMatchers(): HandleMatcher[] {\n return [\n {\n // Scale handle: corner handles on bounding box\n match: (pick) =>\n Boolean(\n pick.isGuide && pick.object?.properties?.editHandleType === 'scale',\n ),\n mode: this.scaleMode,\n shiftConfig: { configKey: 'lockScaling' },\n },\n {\n // Rotate handle: top handle on bounding box\n match: (pick) =>\n Boolean(\n pick.isGuide &&\n pick.object?.properties?.editHandleType === 'rotate',\n ),\n mode: this.rotateMode,\n shiftConfig: { configKey: 'snapRotation' },\n },\n ];\n }\n\n protected override getDefaultMode(): GeoJsonEditMode {\n // biome-ignore lint/suspicious/noExplicitAny: Library type inconsistency — see HandleMatcher JSDoc in base-transform-mode\n return this.translateMode as any;\n }\n\n /**\n * Update tooltip with shape dimensions during scaling.\n */\n protected override onDragging(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n // Only show tooltip when scaling\n if (this.activeDragMode !== this.scaleMode) {\n return;\n }\n\n this.updateShapeTooltip(event, props);\n }\n\n /**\n * Override getGuides to filter duplicate envelope guides.\n *\n * Both ScaleMode and RotateMode render the same bounding box envelope.\n * We keep scale's envelope and filter rotate's duplicate.\n * We also hide scale handles while rotating to avoid visual clutter.\n */\n override getGuides(\n props: ModeProps<FeatureCollection>,\n ): GuideFeatureCollection {\n const allGuides = super.getGuides(props);\n\n // biome-ignore lint/suspicious/noExplicitAny: Guide properties vary by mode, safely accessing with optional chaining\n const filteredGuides = allGuides.features.filter((guide: any) => {\n const properties = guide.properties || {};\n const editHandleType = properties.editHandleType;\n const mode = properties.mode;\n\n // Both scale and rotate modes have the same enveloping box as a guide - only need one\n const guidesToFilterOut: string[] = [mode as string];\n\n // Do not render scaling edit handles if rotating\n if (this.rotateMode.getIsRotating()) {\n guidesToFilterOut.push(editHandleType as string);\n }\n\n return !guidesToFilterOut.includes('scale');\n });\n\n // biome-ignore lint/suspicious/noExplicitAny: turf types mismatch with editable-layers GeoJSON types\n return featureCollection(filteredGuides as any) as any;\n }\n\n /**\n * Update the tooltip with shape dimensions and area.\n * Called during scaling to show live measurements.\n * Handles both rectangles (5 points) and ellipses (many points).\n */\n private updateShapeTooltip(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n const { mapCoords } = event;\n const distanceUnits =\n props.modeConfig?.distanceUnits ?? DEFAULT_DISTANCE_UNITS;\n\n // Get the selected feature\n const selectedIndexes = props.selectedIndexes;\n const selectedIndex = selectedIndexes?.[0];\n if (selectedIndex === undefined) {\n this.tooltip = null;\n return;\n }\n\n const feature = props.data.features[selectedIndex] as\n | Feature<Polygon>\n | undefined;\n if (!feature || feature.geometry.type !== 'Polygon') {\n this.tooltip = null;\n return;\n }\n\n const coordinates = feature.geometry.coordinates[0];\n if (!coordinates || coordinates.length < 4) {\n this.tooltip = null;\n return;\n }\n\n // Check if this is a rectangle (has shape: 'Rectangle' property)\n const isRectangle = feature.properties?.shape === 'Rectangle';\n\n let text: string;\n const unitAbbrev = getDistanceUnitAbbreviation(distanceUnits);\n\n if (isRectangle) {\n // Rectangle: calculate width and height from corners\n const corner0 = coordinates[0] as [number, number];\n const corner1 = coordinates[1] as [number, number];\n const corner2 = coordinates[2] as [number, number];\n\n const { width, height, area } = computeRectangleMeasurementsFromCorners(\n corner0,\n corner1,\n corner2,\n distanceUnits,\n );\n\n text = formatRectangleTooltip(width, height, area, unitAbbrev);\n } else {\n // Ellipse: calculate major/minor axes using consolidated utility\n const {\n majorAxis,\n minorAxis,\n area: ellipseArea,\n } = computeEllipseMeasurementsFromPolygon(\n coordinates as [number, number][],\n distanceUnits,\n );\n\n text = formatEllipseTooltip(\n majorAxis,\n minorAxis,\n ellipseArea,\n unitAbbrev,\n );\n }\n\n // Position tooltip at cursor - offset is applied via getPixelOffset in sublayer props\n this.tooltip = {\n position: mapCoords,\n text,\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,IAAa,wBAAb,cAA2C,kBAAkB;CAC3D,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,cAAc;EACZ,MAAM,gBAAgB,IAAI,eAAe;EACzC,MAAM,YAAY,IAAI,4BAA4B;EAClD,MAAM,aAAa,IAAI,oBAAoB;AAG3C,QAAM;GAAC;GAAW;GAAY;GAAc,CAAC;AAE7C,OAAK,gBAAgB;AACrB,OAAK,YAAY;AACjB,OAAK,aAAa;;CAGpB,AAAmB,oBAAqC;AACtD,SAAO,CACL;GAEE,QAAQ,SACN,QACE,KAAK,WAAW,KAAK,QAAQ,YAAY,mBAAmB,QAC7D;GACH,MAAM,KAAK;GACX,aAAa,EAAE,WAAW,eAAe;GAC1C,EACD;GAEE,QAAQ,SACN,QACE,KAAK,WACH,KAAK,QAAQ,YAAY,mBAAmB,SAC/C;GACH,MAAM,KAAK;GACX,aAAa,EAAE,WAAW,gBAAgB;GAC3C,CACF;;CAGH,AAAmB,iBAAkC;AAEnD,SAAO,KAAK;;;;;CAMd,AAAmB,WACjB,OACA,OACM;AAEN,MAAI,KAAK,mBAAmB,KAAK,UAC/B;AAGF,OAAK,mBAAmB,OAAO,MAAM;;;;;;;;;CAUvC,AAAS,UACP,OACwB;AAqBxB,SAAO,kBApBW,MAAM,UAAU,MAAM,CAGP,SAAS,QAAQ,UAAe;GAC/D,MAAM,aAAa,MAAM,cAAc,EAAE;GACzC,MAAM,iBAAiB,WAAW;GAIlC,MAAMA,oBAA8B,CAHvB,WAAW,KAG4B;AAGpD,OAAI,KAAK,WAAW,eAAe,CACjC,mBAAkB,KAAK,eAAyB;AAGlD,UAAO,CAAC,kBAAkB,SAAS,QAAQ;IAC3C,CAG6C;;;;;;;CAQjD,AAAQ,mBACN,OACA,OACA;EACA,MAAM,EAAE,cAAc;EACtB,MAAM,gBACJ,MAAM,YAAY,iBAAiB;EAIrC,MAAM,gBADkB,MAAM,kBACU;AACxC,MAAI,kBAAkB,QAAW;AAC/B,QAAK,UAAU;AACf;;EAGF,MAAM,UAAU,MAAM,KAAK,SAAS;AAGpC,MAAI,CAAC,WAAW,QAAQ,SAAS,SAAS,WAAW;AACnD,QAAK,UAAU;AACf;;EAGF,MAAM,cAAc,QAAQ,SAAS,YAAY;AACjD,MAAI,CAAC,eAAe,YAAY,SAAS,GAAG;AAC1C,QAAK,UAAU;AACf;;EAIF,MAAM,cAAc,QAAQ,YAAY,UAAU;EAElD,IAAIC;EACJ,MAAM,aAAa,4BAA4B,cAAc;AAE7D,MAAI,aAAa;GAEf,MAAM,UAAU,YAAY;GAC5B,MAAM,UAAU,YAAY;GAC5B,MAAM,UAAU,YAAY;GAE5B,MAAM,EAAE,OAAO,QAAQ,SAAS,wCAC9B,SACA,SACA,SACA,cACD;AAED,UAAO,uBAAuB,OAAO,QAAQ,MAAM,WAAW;SACzD;GAEL,MAAM,EACJ,WACA,WACA,MAAM,gBACJ,sCACF,aACA,cACD;AAED,UAAO,qBACL,WACA,WACA,aACA,WACD;;AAIH,OAAK,UAAU;GACb,UAAU;GACV;GACD"}
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
2
+ * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.
3
3
  * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
4
  * you may not use this file except in compliance with the License. You may obtain a copy
5
5
  * of the License at https://www.apache.org/licenses/LICENSE-2.0
@@ -1 +1 @@
1
- {"version":3,"file":"circle-transform-mode.js","names":["area"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n type DraggingEvent,\n type FeatureCollection,\n type GeoJsonEditMode,\n type ModeProps,\n ResizeCircleMode,\n TranslateMode,\n} from '@deck.gl-community/editable-layers';\nimport { centroid } from '@turf/turf';\nimport {\n DEFAULT_DISTANCE_UNITS,\n getDistanceUnitAbbreviation,\n} from '@/shared/units';\nimport { formatCircleTooltip } from '../../shared/constants';\nimport { computeCircleMeasurements } from '../../shared/utils/geometry-measurements';\nimport { BaseTransformMode, type HandleMatcher } from './base-transform-mode';\nimport type { Feature, Polygon } from 'geojson';\n\n/**\n * Transform mode for circles combining resize and translate.\n *\n * ## Capabilities\n * This composite mode provides:\n * - **Resize** (ResizeCircleMode): Drag edge to resize from center\n * - **Translation** (TranslateMode): Drag the circle body to move it\n * - **Live tooltip**: Shows radius and area during resize\n *\n * ## Handle Priority Logic\n * When drag starts, modes are evaluated in this priority order:\n * 1. If dragging on the edge/handle → resize takes priority\n * 2. Otherwise → dragging the circle body translates it\n *\n * ## Resize Behavior\n * Unlike scale operations in BoundingTransformMode, circle resize maintains\n * the shape's circular geometry by resizing from the center point. The center\n * remains fixed while the radius changes based on cursor distance.\n *\n * @example\n * ```typescript\n * import { CircleTransformMode } from '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode';\n * import { EditableGeoJsonLayer } from '@deck.gl-community/editable-layers';\n *\n * // Used internally by EditShapeLayer for circles\n * const mode = new CircleTransformMode();\n *\n * const layer = new EditableGeoJsonLayer({\n * mode,\n * data: circleFeatureCollection,\n * selectedFeatureIndexes: [0],\n * onEdit: handleEdit,\n * modeConfig: { distanceUnits: 'kilometers' },\n * // ... other props\n * });\n * ```\n */\nexport class CircleTransformMode extends BaseTransformMode {\n private resizeMode: ResizeCircleMode;\n private translateMode: TranslateMode;\n\n constructor() {\n const resizeMode = new ResizeCircleMode();\n const translateMode = new TranslateMode();\n\n // Order matters: resize first so edge handles take priority\n super([resizeMode, translateMode]);\n\n this.resizeMode = resizeMode;\n this.translateMode = translateMode;\n }\n\n protected override getHandleMatchers(): HandleMatcher[] {\n return [\n {\n // Resize handle: intermediate point on circle edge\n match: (pick) =>\n Boolean(\n pick.isGuide &&\n pick.object?.properties?.guideType === 'editHandle' &&\n pick.object?.properties?.editHandleType === 'intermediate',\n ),\n mode: this.resizeMode,\n // No shift config - resize doesn't have modifiers\n },\n ];\n }\n\n protected override getDefaultMode(): GeoJsonEditMode {\n return this.translateMode;\n }\n\n /**\n * Update the tooltip with circle radius and area during resize.\n */\n protected override onDragging(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n // Only show tooltip when resizing\n if (this.activeDragMode !== this.resizeMode) {\n return;\n }\n\n const { mapCoords } = event;\n const distanceUnits =\n props.modeConfig?.distanceUnits ?? DEFAULT_DISTANCE_UNITS;\n\n // Get the selected feature to calculate radius from its geometry\n const selectedIndexes = props.selectedIndexes;\n const selectedIndex = selectedIndexes?.[0];\n if (selectedIndex === undefined) {\n this.tooltip = null;\n return;\n }\n\n const feature = props.data.features[selectedIndex] as\n | Feature<Polygon>\n | undefined;\n if (!feature || feature.geometry.type !== 'Polygon') {\n this.tooltip = null;\n return;\n }\n\n // Calculate center and radius from the polygon geometry\n const coordinates = feature.geometry.coordinates[0];\n if (!coordinates || coordinates.length < 3) {\n this.tooltip = null;\n return;\n }\n\n const centerFeature = centroid(feature);\n const center = centerFeature.geometry.coordinates as [number, number];\n const firstPoint = coordinates[0] as [number, number];\n const { radius, area } = computeCircleMeasurements(\n center,\n firstPoint,\n distanceUnits,\n );\n const unitAbbrev = getDistanceUnitAbbreviation(distanceUnits);\n\n // Position tooltip at cursor - offset is applied via getPixelOffset in sublayer props\n this.tooltip = {\n position: mapCoords,\n text: formatCircleTooltip(radius, area, unitAbbrev),\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEA,IAAa,sBAAb,cAAyC,kBAAkB;CACzD,AAAQ;CACR,AAAQ;CAER,cAAc;EACZ,MAAM,aAAa,IAAI,kBAAkB;EACzC,MAAM,gBAAgB,IAAI,eAAe;AAGzC,QAAM,CAAC,YAAY,cAAc,CAAC;AAElC,OAAK,aAAa;AAClB,OAAK,gBAAgB;;CAGvB,AAAmB,oBAAqC;AACtD,SAAO,CACL;GAEE,QAAQ,SACN,QACE,KAAK,WACH,KAAK,QAAQ,YAAY,cAAc,gBACvC,KAAK,QAAQ,YAAY,mBAAmB,eAC/C;GACH,MAAM,KAAK;GAEZ,CACF;;CAGH,AAAmB,iBAAkC;AACnD,SAAO,KAAK;;;;;CAMd,AAAmB,WACjB,OACA,OACM;AAEN,MAAI,KAAK,mBAAmB,KAAK,WAC/B;EAGF,MAAM,EAAE,cAAc;EACtB,MAAM,gBACJ,MAAM,YAAY,iBAAiB;EAIrC,MAAM,gBADkB,MAAM,kBACU;AACxC,MAAI,kBAAkB,QAAW;AAC/B,QAAK,UAAU;AACf;;EAGF,MAAM,UAAU,MAAM,KAAK,SAAS;AAGpC,MAAI,CAAC,WAAW,QAAQ,SAAS,SAAS,WAAW;AACnD,QAAK,UAAU;AACf;;EAIF,MAAM,cAAc,QAAQ,SAAS,YAAY;AACjD,MAAI,CAAC,eAAe,YAAY,SAAS,GAAG;AAC1C,QAAK,UAAU;AACf;;EAIF,MAAM,SADgB,SAAS,QAAQ,CACV,SAAS;EACtC,MAAM,aAAa,YAAY;EAC/B,MAAM,EAAE,QAAQ,iBAAS,0BACvB,QACA,YACA,cACD;AAID,OAAK,UAAU;GACb,UAAU;GACV,MAAM,oBAAoB,QAAQA,QALjB,4BAA4B,cAAc,CAKR;GACpD"}
1
+ {"version":3,"file":"circle-transform-mode.js","names":["area"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n type DraggingEvent,\n type FeatureCollection,\n type GeoJsonEditMode,\n type ModeProps,\n ResizeCircleMode,\n TranslateMode,\n} from '@deck.gl-community/editable-layers';\nimport { centroid } from '@turf/turf';\nimport {\n DEFAULT_DISTANCE_UNITS,\n getDistanceUnitAbbreviation,\n} from '@/shared/units';\nimport { formatCircleTooltip } from '../../shared/constants';\nimport { computeCircleMeasurements } from '../../shared/utils/geometry-measurements';\nimport { BaseTransformMode, type HandleMatcher } from './base-transform-mode';\nimport type { Feature, Polygon } from 'geojson';\n\n/**\n * Transform mode for circles combining resize and translate.\n *\n * ## Capabilities\n * This composite mode provides:\n * - **Resize** (ResizeCircleMode): Drag edge to resize from center\n * - **Translation** (TranslateMode): Drag the circle body to move it\n * - **Live tooltip**: Shows radius and area during resize\n *\n * ## Handle Priority Logic\n * When drag starts, modes are evaluated in this priority order:\n * 1. If dragging on the edge/handle → resize takes priority\n * 2. Otherwise → dragging the circle body translates it\n *\n * ## Resize Behavior\n * Unlike scale operations in BoundingTransformMode, circle resize maintains\n * the shape's circular geometry by resizing from the center point. The center\n * remains fixed while the radius changes based on cursor distance.\n *\n * @example\n * ```typescript\n * import { CircleTransformMode } from '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode';\n * import { EditableGeoJsonLayer } from '@deck.gl-community/editable-layers';\n *\n * // Used internally by EditShapeLayer for circles\n * const mode = new CircleTransformMode();\n *\n * const layer = new EditableGeoJsonLayer({\n * mode,\n * data: circleFeatureCollection,\n * selectedFeatureIndexes: [0],\n * onEdit: handleEdit,\n * modeConfig: { distanceUnits: 'kilometers' },\n * // ... other props\n * });\n * ```\n */\nexport class CircleTransformMode extends BaseTransformMode {\n private resizeMode: ResizeCircleMode;\n private translateMode: TranslateMode;\n\n constructor() {\n const resizeMode = new ResizeCircleMode();\n const translateMode = new TranslateMode();\n\n // Order matters: resize first so edge handles take priority\n super([resizeMode, translateMode]);\n\n this.resizeMode = resizeMode;\n this.translateMode = translateMode;\n }\n\n protected override getHandleMatchers(): HandleMatcher[] {\n return [\n {\n // Resize handle: intermediate point on circle edge\n match: (pick) =>\n Boolean(\n pick.isGuide &&\n pick.object?.properties?.guideType === 'editHandle' &&\n pick.object?.properties?.editHandleType === 'intermediate',\n ),\n mode: this.resizeMode,\n // No shift config - resize doesn't have modifiers\n },\n ];\n }\n\n protected override getDefaultMode(): GeoJsonEditMode {\n // biome-ignore lint/suspicious/noExplicitAny: Library type inconsistency — see HandleMatcher JSDoc in base-transform-mode\n return this.translateMode as any;\n }\n\n /**\n * Update the tooltip with circle radius and area during resize.\n */\n protected override onDragging(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n // Only show tooltip when resizing\n if (this.activeDragMode !== this.resizeMode) {\n return;\n }\n\n const { mapCoords } = event;\n const distanceUnits =\n props.modeConfig?.distanceUnits ?? DEFAULT_DISTANCE_UNITS;\n\n // Get the selected feature to calculate radius from its geometry\n const selectedIndexes = props.selectedIndexes;\n const selectedIndex = selectedIndexes?.[0];\n if (selectedIndex === undefined) {\n this.tooltip = null;\n return;\n }\n\n const feature = props.data.features[selectedIndex] as\n | Feature<Polygon>\n | undefined;\n if (!feature || feature.geometry.type !== 'Polygon') {\n this.tooltip = null;\n return;\n }\n\n // Calculate center and radius from the polygon geometry\n const coordinates = feature.geometry.coordinates[0];\n if (!coordinates || coordinates.length < 3) {\n this.tooltip = null;\n return;\n }\n\n const centerFeature = centroid(feature);\n const center = centerFeature.geometry.coordinates as [number, number];\n const firstPoint = coordinates[0] as [number, number];\n const { radius, area } = computeCircleMeasurements(\n center,\n firstPoint,\n distanceUnits,\n );\n const unitAbbrev = getDistanceUnitAbbreviation(distanceUnits);\n\n // Position tooltip at cursor - offset is applied via getPixelOffset in sublayer props\n this.tooltip = {\n position: mapCoords,\n text: formatCircleTooltip(radius, area, unitAbbrev),\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEA,IAAa,sBAAb,cAAyC,kBAAkB;CACzD,AAAQ;CACR,AAAQ;CAER,cAAc;EACZ,MAAM,aAAa,IAAI,kBAAkB;EACzC,MAAM,gBAAgB,IAAI,eAAe;AAGzC,QAAM,CAAC,YAAY,cAAc,CAAC;AAElC,OAAK,aAAa;AAClB,OAAK,gBAAgB;;CAGvB,AAAmB,oBAAqC;AACtD,SAAO,CACL;GAEE,QAAQ,SACN,QACE,KAAK,WACH,KAAK,QAAQ,YAAY,cAAc,gBACvC,KAAK,QAAQ,YAAY,mBAAmB,eAC/C;GACH,MAAM,KAAK;GAEZ,CACF;;CAGH,AAAmB,iBAAkC;AAEnD,SAAO,KAAK;;;;;CAMd,AAAmB,WACjB,OACA,OACM;AAEN,MAAI,KAAK,mBAAmB,KAAK,WAC/B;EAGF,MAAM,EAAE,cAAc;EACtB,MAAM,gBACJ,MAAM,YAAY,iBAAiB;EAIrC,MAAM,gBADkB,MAAM,kBACU;AACxC,MAAI,kBAAkB,QAAW;AAC/B,QAAK,UAAU;AACf;;EAGF,MAAM,UAAU,MAAM,KAAK,SAAS;AAGpC,MAAI,CAAC,WAAW,QAAQ,SAAS,SAAS,WAAW;AACnD,QAAK,UAAU;AACf;;EAIF,MAAM,cAAc,QAAQ,SAAS,YAAY;AACjD,MAAI,CAAC,eAAe,YAAY,SAAS,GAAG;AAC1C,QAAK,UAAU;AACf;;EAIF,MAAM,SADgB,SAAS,QAAQ,CACV,SAAS;EACtC,MAAM,aAAa,YAAY;EAC/B,MAAM,EAAE,QAAQ,iBAAS,0BACvB,QACA,YACA,cACD;AAID,OAAK,UAAU;GACb,UAAU;GACV,MAAM,oBAAoB,QAAQA,QALjB,4BAA4B,cAAc,CAKR;GACpD"}
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
2
+ * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.
3
3
  * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
4
  * you may not use this file except in compliance with the License. You may obtain a copy
5
5
  * of the License at https://www.apache.org/licenses/LICENSE-2.0
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
2
+ * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.
3
3
  * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
4
  * you may not use this file except in compliance with the License. You may obtain a copy
5
5
  * of the License at https://www.apache.org/licenses/LICENSE-2.0
@@ -1 +1 @@
1
- {"version":3,"file":"point-translate-mode.js","names":[],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/point-translate-mode.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n type ClickEvent,\n type DraggingEvent,\n type FeatureCollection,\n GeoJsonEditMode,\n type GuideFeatureCollection,\n type ModeProps,\n type PointerMoveEvent,\n type StartDraggingEvent,\n type StopDraggingEvent,\n TranslateMode,\n} from '@deck.gl-community/editable-layers';\n\n/**\n * Edit mode for Point shapes that supports both click-to-place and drag behaviors.\n *\n * ## Capabilities\n * This mode provides two ways to reposition a point:\n * - **Click on empty space**: Instantly moves the point to the clicked location\n * - **Drag the point**: Traditional click-and-drag behavior (via TranslateMode)\n *\n * ## Behavior Details\n * - Clicking anywhere on the map (that isn't the point itself) repositions the point\n * - Clicking directly on the point and dragging works as traditional translation\n * - Both behaviors emit the 'translated' edit type for consistency with existing event handling\n *\n * @example\n * ```typescript\n * import { PointTranslateMode } from '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/modes/point-translate-mode';\n * import { EditableGeoJsonLayer } from '@deck.gl-community/editable-layers';\n *\n * // Used internally by EditShapeLayer for points\n * const mode = new PointTranslateMode();\n *\n * const layer = new EditableGeoJsonLayer({\n * mode,\n * data: pointFeatureCollection,\n * selectedFeatureIndexes: [0],\n * onEdit: handleEdit,\n * });\n * ```\n */\nexport class PointTranslateMode extends GeoJsonEditMode {\n private translateMode = new TranslateMode();\n\n /**\n * Handle click events to reposition the point.\n *\n * If the click is on empty map space (not on the point itself),\n * moves the point to the clicked location immediately.\n *\n * @param event - Click event containing map coordinates and pick information\n * @param props - Mode props containing data, selected indexes, and edit callback\n */\n override handleClick(\n event: ClickEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n // If clicked on the point itself or a guide, let drag handle it\n const clickedOnFeature = event.picks?.some(\n (pick) => pick.isGuide || pick.featureIndex !== undefined,\n );\n\n if (clickedOnFeature) {\n return;\n }\n\n const { mapCoords } = event;\n const selectedIndex = props.selectedIndexes?.[0];\n\n if (selectedIndex === undefined) {\n return;\n }\n\n const feature = props.data.features[selectedIndex];\n\n if (!feature) {\n return;\n }\n\n // Create updated feature with new coordinates\n const updatedFeature = {\n ...feature,\n geometry: {\n type: 'Point' as const,\n coordinates: mapCoords,\n },\n };\n\n // Emit edit action with 'translated' type to work with existing completion handlers\n props.onEdit({\n updatedData: {\n ...props.data,\n features: [updatedFeature],\n },\n editType: 'translated',\n editContext: {\n featureIndexes: [selectedIndex],\n },\n });\n }\n\n /**\n * Delegate pointer move events to TranslateMode for cursor updates.\n *\n * @param event - Pointer move event with current cursor position\n * @param props - Mode props containing state and configuration\n */\n override handlePointerMove(\n event: PointerMoveEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n this.translateMode.handlePointerMove(event, props);\n }\n\n /**\n * Delegate start dragging to TranslateMode for traditional drag behavior.\n *\n * @param event - Drag start event with pointer down coordinates\n * @param props - Mode props containing data and edit callback\n */\n override handleStartDragging(\n event: StartDraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n this.translateMode.handleStartDragging(event, props);\n }\n\n /**\n * Delegate dragging to TranslateMode for traditional drag behavior.\n *\n * @param event - Dragging event with current and previous pointer positions\n * @param props - Mode props containing data and edit callback\n */\n override handleDragging(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n this.translateMode.handleDragging(event, props);\n }\n\n /**\n * Delegate stop dragging to TranslateMode for traditional drag behavior.\n *\n * @param event - Drag stop event with final pointer position\n * @param props - Mode props containing data and edit callback\n */\n override handleStopDragging(\n event: StopDraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n this.translateMode.handleStopDragging(event, props);\n }\n\n /**\n * Delegate guide rendering to TranslateMode.\n *\n * @param props - Mode props containing data and selected indexes\n * @returns Guide feature collection for rendering edit handles\n */\n override getGuides(\n props: ModeProps<FeatureCollection>,\n ): GuideFeatureCollection {\n return this.translateMode.getGuides(props);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,IAAa,qBAAb,cAAwC,gBAAgB;CACtD,AAAQ,gBAAgB,IAAI,eAAe;;;;;;;;;;CAW3C,AAAS,YACP,OACA,OACM;AAMN,MAJyB,MAAM,OAAO,MACnC,SAAS,KAAK,WAAW,KAAK,iBAAiB,OACjD,CAGC;EAGF,MAAM,EAAE,cAAc;EACtB,MAAM,gBAAgB,MAAM,kBAAkB;AAE9C,MAAI,kBAAkB,OACpB;EAGF,MAAM,UAAU,MAAM,KAAK,SAAS;AAEpC,MAAI,CAAC,QACH;EAIF,MAAM,iBAAiB;GACrB,GAAG;GACH,UAAU;IACR,MAAM;IACN,aAAa;IACd;GACF;AAGD,QAAM,OAAO;GACX,aAAa;IACX,GAAG,MAAM;IACT,UAAU,CAAC,eAAe;IAC3B;GACD,UAAU;GACV,aAAa,EACX,gBAAgB,CAAC,cAAc,EAChC;GACF,CAAC;;;;;;;;CASJ,AAAS,kBACP,OACA,OACM;AACN,OAAK,cAAc,kBAAkB,OAAO,MAAM;;;;;;;;CASpD,AAAS,oBACP,OACA,OACM;AACN,OAAK,cAAc,oBAAoB,OAAO,MAAM;;;;;;;;CAStD,AAAS,eACP,OACA,OACM;AACN,OAAK,cAAc,eAAe,OAAO,MAAM;;;;;;;;CASjD,AAAS,mBACP,OACA,OACM;AACN,OAAK,cAAc,mBAAmB,OAAO,MAAM;;;;;;;;CASrD,AAAS,UACP,OACwB;AACxB,SAAO,KAAK,cAAc,UAAU,MAAM"}
1
+ {"version":3,"file":"point-translate-mode.js","names":[],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/point-translate-mode.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n type ClickEvent,\n type DraggingEvent,\n type FeatureCollection,\n GeoJsonEditMode,\n type GuideFeatureCollection,\n type ModeProps,\n type PointerMoveEvent,\n type StartDraggingEvent,\n type StopDraggingEvent,\n TranslateMode,\n} from '@deck.gl-community/editable-layers';\n\n/**\n * Edit mode for Point shapes that supports both click-to-place and drag behaviors.\n *\n * ## Capabilities\n * This mode provides two ways to reposition a point:\n * - **Click on empty space**: Instantly moves the point to the clicked location\n * - **Drag the point**: Traditional click-and-drag behavior (via TranslateMode)\n *\n * ## Behavior Details\n * - Clicking anywhere on the map (that isn't the point itself) repositions the point\n * - Clicking directly on the point and dragging works as traditional translation\n * - Both behaviors emit the 'translated' edit type for consistency with existing event handling\n *\n * @example\n * ```typescript\n * import { PointTranslateMode } from '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/modes/point-translate-mode';\n * import { EditableGeoJsonLayer } from '@deck.gl-community/editable-layers';\n *\n * // Used internally by EditShapeLayer for points\n * const mode = new PointTranslateMode();\n *\n * const layer = new EditableGeoJsonLayer({\n * mode,\n * data: pointFeatureCollection,\n * selectedFeatureIndexes: [0],\n * onEdit: handleEdit,\n * });\n * ```\n */\nexport class PointTranslateMode extends GeoJsonEditMode {\n // biome-ignore lint/suspicious/noExplicitAny: TranslateMode narrows ModeProps to SimpleFeatureCollection but GeoJsonEditMode uses FeatureCollection — library type inconsistency\n private translateMode: GeoJsonEditMode = new TranslateMode() as any;\n\n /**\n * Handle click events to reposition the point.\n *\n * If the click is on empty map space (not on the point itself),\n * moves the point to the clicked location immediately.\n *\n * @param event - Click event containing map coordinates and pick information\n * @param props - Mode props containing data, selected indexes, and edit callback\n */\n override handleClick(\n event: ClickEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n // If clicked on the point itself or a guide, let drag handle it\n const clickedOnFeature = event.picks?.some(\n (pick) => pick.isGuide || pick.featureIndex !== undefined,\n );\n\n if (clickedOnFeature) {\n return;\n }\n\n const { mapCoords } = event;\n const selectedIndex = props.selectedIndexes?.[0];\n\n if (selectedIndex === undefined) {\n return;\n }\n\n const feature = props.data.features[selectedIndex];\n\n if (!feature) {\n return;\n }\n\n // Create updated feature with new coordinates\n const updatedFeature = {\n ...feature,\n geometry: {\n type: 'Point' as const,\n coordinates: mapCoords,\n },\n };\n\n // Emit edit action with 'translated' type to work with existing completion handlers\n props.onEdit({\n updatedData: {\n ...props.data,\n features: [updatedFeature],\n },\n editType: 'translated',\n editContext: {\n featureIndexes: [selectedIndex],\n },\n });\n }\n\n /**\n * Delegate pointer move events to TranslateMode for cursor updates.\n *\n * @param event - Pointer move event with current cursor position\n * @param props - Mode props containing state and configuration\n */\n override handlePointerMove(\n event: PointerMoveEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n this.translateMode.handlePointerMove(event, props);\n }\n\n /**\n * Delegate start dragging to TranslateMode for traditional drag behavior.\n *\n * @param event - Drag start event with pointer down coordinates\n * @param props - Mode props containing data and edit callback\n */\n override handleStartDragging(\n event: StartDraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n this.translateMode.handleStartDragging(event, props);\n }\n\n /**\n * Delegate dragging to TranslateMode for traditional drag behavior.\n *\n * @param event - Dragging event with current and previous pointer positions\n * @param props - Mode props containing data and edit callback\n */\n override handleDragging(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n this.translateMode.handleDragging(event, props);\n }\n\n /**\n * Delegate stop dragging to TranslateMode for traditional drag behavior.\n *\n * @param event - Drag stop event with final pointer position\n * @param props - Mode props containing data and edit callback\n */\n override handleStopDragging(\n event: StopDraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n this.translateMode.handleStopDragging(event, props);\n }\n\n /**\n * Delegate guide rendering to TranslateMode.\n *\n * @param props - Mode props containing data and selected indexes\n * @returns Guide feature collection for rendering edit handles\n */\n override getGuides(\n props: ModeProps<FeatureCollection>,\n ): GuideFeatureCollection {\n return this.translateMode.getGuides(props);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,IAAa,qBAAb,cAAwC,gBAAgB;CAEtD,AAAQ,gBAAiC,IAAI,eAAe;;;;;;;;;;CAW5D,AAAS,YACP,OACA,OACM;AAMN,MAJyB,MAAM,OAAO,MACnC,SAAS,KAAK,WAAW,KAAK,iBAAiB,OACjD,CAGC;EAGF,MAAM,EAAE,cAAc;EACtB,MAAM,gBAAgB,MAAM,kBAAkB;AAE9C,MAAI,kBAAkB,OACpB;EAGF,MAAM,UAAU,MAAM,KAAK,SAAS;AAEpC,MAAI,CAAC,QACH;EAIF,MAAM,iBAAiB;GACrB,GAAG;GACH,UAAU;IACR,MAAM;IACN,aAAa;IACd;GACF;AAGD,QAAM,OAAO;GACX,aAAa;IACX,GAAG,MAAM;IACT,UAAU,CAAC,eAAe;IAC3B;GACD,UAAU;GACV,aAAa,EACX,gBAAgB,CAAC,cAAc,EAChC;GACF,CAAC;;;;;;;;CASJ,AAAS,kBACP,OACA,OACM;AACN,OAAK,cAAc,kBAAkB,OAAO,MAAM;;;;;;;;CASpD,AAAS,oBACP,OACA,OACM;AACN,OAAK,cAAc,oBAAoB,OAAO,MAAM;;;;;;;;CAStD,AAAS,eACP,OACA,OACM;AACN,OAAK,cAAc,eAAe,OAAO,MAAM;;;;;;;;CASjD,AAAS,mBACP,OACA,OACM;AACN,OAAK,cAAc,mBAAmB,OAAO,MAAM;;;;;;;;CASrD,AAAS,UACP,OACwB;AACxB,SAAO,KAAK,cAAc,UAAU,MAAM"}
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
2
+ * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.
3
3
  * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
4
  * you may not use this file except in compliance with the License. You may obtain a copy
5
5
  * of the License at https://www.apache.org/licenses/LICENSE-2.0
@@ -1 +1 @@
1
- {"version":3,"file":"rotate-mode-with-snap.js","names":["rotatedFeatures: FeatureCollection"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n type DraggingEvent,\n type FeatureCollection,\n ImmutableFeatureCollection,\n type ModeProps,\n RotateMode,\n type StopDraggingEvent,\n} from '@deck.gl-community/editable-layers';\nimport { bearing, centroid, transformRotate } from '@turf/turf';\nimport type { Position } from 'geojson';\n\n/** Snap interval in degrees (45° = 8 positions around the circle) */\nconst SNAP_INTERVAL_DEGREES = 45;\n\n/**\n * Calculate the angle between two points relative to a centroid.\n * Uses turfBearing for geographic bearing convention (matches parent RotateMode).\n * Returns angle in degrees.\n *\n * Note: centroid must be a turf Point Feature (not just coordinates) to match\n * the parent RotateMode's behavior exactly.\n */\nfunction getRotationAngle(\n centroidFeature: ReturnType<typeof centroid>,\n startPoint: Position,\n endPoint: Position,\n): number {\n const bearing1 = bearing(centroidFeature, startPoint);\n const bearing2 = bearing(centroidFeature, endPoint);\n return bearing2 - bearing1;\n}\n\n/**\n * Snap an angle to the nearest interval.\n */\nfunction snapAngle(angle: number, interval: number): number {\n return Math.round(angle / interval) * interval;\n}\n\n/**\n * Extends RotateMode to support snapping rotation to 45° intervals.\n *\n * ## Features\n * - **Default**: Free rotation (smooth, continuous angles)\n * - **With Shift**: Snap to 45° intervals (0°, 45°, 90°, 135°, 180°, etc.)\n *\n * This allows precise alignment of shapes to common angles, making it easy to\n * create axis-aligned or diagonally-aligned shapes.\n *\n * ## Implementation\n * The snap behavior is controlled by the `modeConfig.snapRotation` property,\n * which is set by BaseTransformMode when the Shift key is held. This class\n * calculates the rotation angle and rounds it to the nearest 45° interval\n * when snapping is enabled.\n *\n * ## Snap Interval\n * The snap interval is fixed at 45° (8 positions around the circle), providing\n * these angles: 0°, 45°, 90°, 135°, 180°, 225°, 270°, 315°.\n *\n * @example\n * ```typescript\n * import { RotateModeWithSnap } from '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap';\n * import { EditableGeoJsonLayer } from '@deck.gl-community/editable-layers';\n *\n * const mode = new RotateModeWithSnap();\n *\n * const layer = new EditableGeoJsonLayer({\n * mode,\n * data: featureCollection,\n * selectedFeatureIndexes: [0],\n * onEdit: handleEdit,\n * modeConfig: {\n * snapRotation: true, // Enable 45° snapping\n * },\n * });\n * ```\n */\nexport class RotateModeWithSnap extends RotateMode {\n /**\n * Override handleDragging to support snapped rotation.\n * When snapRotation is true, rotates to nearest 45° interval.\n * When snapRotation is false, delegates to parent for standard rotation.\n */\n override handleDragging(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n const snapRotation = props.modeConfig?.snapRotation ?? false;\n\n // If not snapping, use parent's rotation logic\n if (!snapRotation) {\n super.handleDragging(event, props);\n return;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n const self = this as any;\n\n if (!self._isRotating) {\n return;\n }\n\n const rotateAction = this.getRotateActionWithSnap(\n event.pointerDownMapCoords,\n event.mapCoords,\n 'rotating',\n props,\n );\n\n if (rotateAction) {\n props.onEdit(rotateAction);\n }\n\n event.cancelPan();\n }\n\n /**\n * Override handleStopDragging to emit the final rotated geometry with snap.\n * When snapRotation is false, delegates to parent for standard rotation.\n */\n override handleStopDragging(\n event: StopDraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n const snapRotation = props.modeConfig?.snapRotation ?? false;\n\n // If not snapping, use parent's rotation logic\n if (!snapRotation) {\n super.handleStopDragging(event, props);\n return;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n const self = this as any;\n\n if (self._isRotating) {\n const rotateAction = this.getRotateActionWithSnap(\n event.pointerDownMapCoords,\n event.mapCoords,\n 'rotated',\n props,\n );\n\n if (rotateAction) {\n props.onEdit(rotateAction);\n }\n\n // Reset state\n self._geometryBeingRotated = null;\n self._selectedEditHandle = null;\n self._isRotating = false;\n }\n }\n\n /**\n * Get a rotate action, optionally snapping to 45° intervals.\n */\n private getRotateActionWithSnap(\n startDragPoint: Position,\n currentPoint: Position,\n editType: string,\n props: ModeProps<FeatureCollection>,\n ) {\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n const self = this as any;\n\n if (!self._geometryBeingRotated) {\n return null;\n }\n\n const geometry = self._geometryBeingRotated as FeatureCollection;\n // @ts-expect-error turf types differ from editable-layers types\n const centerFeature = centroid(geometry);\n\n // Calculate the rotation angle (pass centroid Feature to match parent RotateMode)\n let angle = getRotationAngle(centerFeature, startDragPoint, currentPoint);\n\n // Snap to 45° intervals if enabled\n const snapRotation = props.modeConfig?.snapRotation ?? false;\n if (snapRotation) {\n angle = snapAngle(angle, SNAP_INTERVAL_DEGREES);\n }\n\n // Apply the rotation using turf (use centroid Feature as pivot to match parent)\n // @ts-expect-error turf types differ from editable-layers types\n const rotatedFeatures: FeatureCollection = transformRotate(\n // @ts-expect-error turf types differ from editable-layers types\n geometry,\n angle,\n {\n pivot: centerFeature,\n },\n );\n\n // Build the updated data using ImmutableFeatureCollection (matches parent RotateMode)\n const selectedIndexes = props.selectedIndexes;\n let updatedData = new ImmutableFeatureCollection(props.data);\n\n for (let i = 0; i < selectedIndexes.length; i++) {\n const selectedIndex = selectedIndexes[i];\n const movedFeature = rotatedFeatures.features[i];\n if (selectedIndex !== undefined && movedFeature) {\n updatedData = updatedData.replaceGeometry(\n selectedIndex,\n movedFeature.geometry,\n );\n }\n }\n\n return {\n updatedData: updatedData.getObject(),\n editType,\n editContext: {\n featureIndexes: selectedIndexes,\n },\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAwBA,MAAM,wBAAwB;;;;;;;;;AAU9B,SAAS,iBACP,iBACA,YACA,UACQ;CACR,MAAM,WAAW,QAAQ,iBAAiB,WAAW;AAErD,QADiB,QAAQ,iBAAiB,SAAS,GACjC;;;;;AAMpB,SAAS,UAAU,OAAe,UAA0B;AAC1D,QAAO,KAAK,MAAM,QAAQ,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCxC,IAAa,qBAAb,cAAwC,WAAW;;;;;;CAMjD,AAAS,eACP,OACA,OACA;AAIA,MAAI,EAHiB,MAAM,YAAY,gBAAgB,QAGpC;AACjB,SAAM,eAAe,OAAO,MAAM;AAClC;;AAMF,MAAI,CAFS,KAEH,YACR;EAGF,MAAM,eAAe,KAAK,wBACxB,MAAM,sBACN,MAAM,WACN,YACA,MACD;AAED,MAAI,aACF,OAAM,OAAO,aAAa;AAG5B,QAAM,WAAW;;;;;;CAOnB,AAAS,mBACP,OACA,OACA;AAIA,MAAI,EAHiB,MAAM,YAAY,gBAAgB,QAGpC;AACjB,SAAM,mBAAmB,OAAO,MAAM;AACtC;;EAIF,MAAM,OAAO;AAEb,MAAI,KAAK,aAAa;GACpB,MAAM,eAAe,KAAK,wBACxB,MAAM,sBACN,MAAM,WACN,WACA,MACD;AAED,OAAI,aACF,OAAM,OAAO,aAAa;AAI5B,QAAK,wBAAwB;AAC7B,QAAK,sBAAsB;AAC3B,QAAK,cAAc;;;;;;CAOvB,AAAQ,wBACN,gBACA,cACA,UACA,OACA;EAEA,MAAM,OAAO;AAEb,MAAI,CAAC,KAAK,sBACR,QAAO;EAGT,MAAM,WAAW,KAAK;EAEtB,MAAM,gBAAgB,SAAS,SAAS;EAGxC,IAAI,QAAQ,iBAAiB,eAAe,gBAAgB,aAAa;AAIzE,MADqB,MAAM,YAAY,gBAAgB,MAErD,SAAQ,UAAU,OAAO,sBAAsB;EAKjD,MAAMA,kBAAqC,gBAEzC,UACA,OACA,EACE,OAAO,eACR,CACF;EAGD,MAAM,kBAAkB,MAAM;EAC9B,IAAI,cAAc,IAAI,2BAA2B,MAAM,KAAK;AAE5D,OAAK,IAAI,IAAI,GAAG,IAAI,gBAAgB,QAAQ,KAAK;GAC/C,MAAM,gBAAgB,gBAAgB;GACtC,MAAM,eAAe,gBAAgB,SAAS;AAC9C,OAAI,kBAAkB,UAAa,aACjC,eAAc,YAAY,gBACxB,eACA,aAAa,SACd;;AAIL,SAAO;GACL,aAAa,YAAY,WAAW;GACpC;GACA,aAAa,EACX,gBAAgB,iBACjB;GACF"}
1
+ {"version":3,"file":"rotate-mode-with-snap.js","names":["rotatedFeatures: SimpleFeatureCollection"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n type DraggingEvent,\n ImmutableFeatureCollection,\n type ModeProps,\n RotateMode,\n type SimpleFeatureCollection,\n type StopDraggingEvent,\n} from '@deck.gl-community/editable-layers';\nimport { bearing, centroid, transformRotate } from '@turf/turf';\nimport type { Position } from 'geojson';\n\n/** Snap interval in degrees (45° = 8 positions around the circle) */\nconst SNAP_INTERVAL_DEGREES = 45;\n\n/**\n * Calculate the angle between two points relative to a centroid.\n * Uses turfBearing for geographic bearing convention (matches parent RotateMode).\n * Returns angle in degrees.\n *\n * Note: centroid must be a turf Point Feature (not just coordinates) to match\n * the parent RotateMode's behavior exactly.\n */\nfunction getRotationAngle(\n centroidFeature: ReturnType<typeof centroid>,\n startPoint: Position,\n endPoint: Position,\n): number {\n const bearing1 = bearing(centroidFeature, startPoint);\n const bearing2 = bearing(centroidFeature, endPoint);\n return bearing2 - bearing1;\n}\n\n/**\n * Snap an angle to the nearest interval.\n */\nfunction snapAngle(angle: number, interval: number): number {\n return Math.round(angle / interval) * interval;\n}\n\n/**\n * Extends RotateMode to support snapping rotation to 45° intervals.\n *\n * ## Features\n * - **Default**: Free rotation (smooth, continuous angles)\n * - **With Shift**: Snap to 45° intervals (0°, 45°, 90°, 135°, 180°, etc.)\n *\n * This allows precise alignment of shapes to common angles, making it easy to\n * create axis-aligned or diagonally-aligned shapes.\n *\n * ## Implementation\n * The snap behavior is controlled by the `modeConfig.snapRotation` property,\n * which is set by BaseTransformMode when the Shift key is held. This class\n * calculates the rotation angle and rounds it to the nearest 45° interval\n * when snapping is enabled.\n *\n * ## Snap Interval\n * The snap interval is fixed at 45° (8 positions around the circle), providing\n * these angles: 0°, 45°, 90°, 135°, 180°, 225°, 270°, 315°.\n *\n * @example\n * ```typescript\n * import { RotateModeWithSnap } from '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap';\n * import { EditableGeoJsonLayer } from '@deck.gl-community/editable-layers';\n *\n * const mode = new RotateModeWithSnap();\n *\n * const layer = new EditableGeoJsonLayer({\n * mode,\n * data: featureCollection,\n * selectedFeatureIndexes: [0],\n * onEdit: handleEdit,\n * modeConfig: {\n * snapRotation: true, // Enable 45° snapping\n * },\n * });\n * ```\n */\nexport class RotateModeWithSnap extends RotateMode {\n /**\n * Override handleDragging to support snapped rotation.\n * When snapRotation is true, rotates to nearest 45° interval.\n * When snapRotation is false, delegates to parent for standard rotation.\n */\n override handleDragging(\n event: DraggingEvent,\n props: ModeProps<SimpleFeatureCollection>,\n ) {\n const snapRotation = props.modeConfig?.snapRotation ?? false;\n\n // If not snapping, use parent's rotation logic\n if (!snapRotation) {\n super.handleDragging(event, props);\n return;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n const self = this as any;\n\n if (!self._isRotating) {\n return;\n }\n\n const rotateAction = this.getRotateActionWithSnap(\n event.pointerDownMapCoords,\n event.mapCoords,\n 'rotating',\n props,\n );\n\n if (rotateAction) {\n props.onEdit(rotateAction);\n }\n\n event.cancelPan();\n }\n\n /**\n * Override handleStopDragging to emit the final rotated geometry with snap.\n * When snapRotation is false, delegates to parent for standard rotation.\n */\n override handleStopDragging(\n event: StopDraggingEvent,\n props: ModeProps<SimpleFeatureCollection>,\n ) {\n const snapRotation = props.modeConfig?.snapRotation ?? false;\n\n // If not snapping, use parent's rotation logic\n if (!snapRotation) {\n super.handleStopDragging(event, props);\n return;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n const self = this as any;\n\n if (self._isRotating) {\n const rotateAction = this.getRotateActionWithSnap(\n event.pointerDownMapCoords,\n event.mapCoords,\n 'rotated',\n props,\n );\n\n if (rotateAction) {\n props.onEdit(rotateAction);\n }\n\n // Reset state\n self._geometryBeingRotated = null;\n self._selectedEditHandle = null;\n self._isRotating = false;\n }\n }\n\n /**\n * Get a rotate action, optionally snapping to 45° intervals.\n */\n private getRotateActionWithSnap(\n startDragPoint: Position,\n currentPoint: Position,\n editType: string,\n props: ModeProps<SimpleFeatureCollection>,\n ) {\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n const self = this as any;\n\n if (!self._geometryBeingRotated) {\n return null;\n }\n\n const geometry = self._geometryBeingRotated as SimpleFeatureCollection;\n const centerFeature = centroid(geometry);\n\n // Calculate the rotation angle (pass centroid Feature to match parent RotateMode)\n let angle = getRotationAngle(centerFeature, startDragPoint, currentPoint);\n\n // Snap to 45° intervals if enabled\n const snapRotation = props.modeConfig?.snapRotation ?? false;\n if (snapRotation) {\n angle = snapAngle(angle, SNAP_INTERVAL_DEGREES);\n }\n\n // Apply the rotation using turf (use centroid Feature as pivot to match parent)\n const rotatedFeatures: SimpleFeatureCollection = transformRotate(\n geometry,\n angle,\n {\n pivot: centerFeature,\n },\n );\n\n // Build the updated data using ImmutableFeatureCollection (matches parent RotateMode)\n const selectedIndexes = props.selectedIndexes;\n let updatedData = new ImmutableFeatureCollection(props.data);\n\n for (let i = 0; i < selectedIndexes.length; i++) {\n const selectedIndex = selectedIndexes[i];\n const movedFeature = rotatedFeatures.features[i];\n if (selectedIndex !== undefined && movedFeature) {\n updatedData = updatedData.replaceGeometry(\n selectedIndex,\n movedFeature.geometry,\n );\n }\n }\n\n return {\n updatedData: updatedData.getObject(),\n editType,\n editContext: {\n featureIndexes: selectedIndexes,\n },\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAwBA,MAAM,wBAAwB;;;;;;;;;AAU9B,SAAS,iBACP,iBACA,YACA,UACQ;CACR,MAAM,WAAW,QAAQ,iBAAiB,WAAW;AAErD,QADiB,QAAQ,iBAAiB,SAAS,GACjC;;;;;AAMpB,SAAS,UAAU,OAAe,UAA0B;AAC1D,QAAO,KAAK,MAAM,QAAQ,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCxC,IAAa,qBAAb,cAAwC,WAAW;;;;;;CAMjD,AAAS,eACP,OACA,OACA;AAIA,MAAI,EAHiB,MAAM,YAAY,gBAAgB,QAGpC;AACjB,SAAM,eAAe,OAAO,MAAM;AAClC;;AAMF,MAAI,CAFS,KAEH,YACR;EAGF,MAAM,eAAe,KAAK,wBACxB,MAAM,sBACN,MAAM,WACN,YACA,MACD;AAED,MAAI,aACF,OAAM,OAAO,aAAa;AAG5B,QAAM,WAAW;;;;;;CAOnB,AAAS,mBACP,OACA,OACA;AAIA,MAAI,EAHiB,MAAM,YAAY,gBAAgB,QAGpC;AACjB,SAAM,mBAAmB,OAAO,MAAM;AACtC;;EAIF,MAAM,OAAO;AAEb,MAAI,KAAK,aAAa;GACpB,MAAM,eAAe,KAAK,wBACxB,MAAM,sBACN,MAAM,WACN,WACA,MACD;AAED,OAAI,aACF,OAAM,OAAO,aAAa;AAI5B,QAAK,wBAAwB;AAC7B,QAAK,sBAAsB;AAC3B,QAAK,cAAc;;;;;;CAOvB,AAAQ,wBACN,gBACA,cACA,UACA,OACA;EAEA,MAAM,OAAO;AAEb,MAAI,CAAC,KAAK,sBACR,QAAO;EAGT,MAAM,WAAW,KAAK;EACtB,MAAM,gBAAgB,SAAS,SAAS;EAGxC,IAAI,QAAQ,iBAAiB,eAAe,gBAAgB,aAAa;AAIzE,MADqB,MAAM,YAAY,gBAAgB,MAErD,SAAQ,UAAU,OAAO,sBAAsB;EAIjD,MAAMA,kBAA2C,gBAC/C,UACA,OACA,EACE,OAAO,eACR,CACF;EAGD,MAAM,kBAAkB,MAAM;EAC9B,IAAI,cAAc,IAAI,2BAA2B,MAAM,KAAK;AAE5D,OAAK,IAAI,IAAI,GAAG,IAAI,gBAAgB,QAAQ,KAAK;GAC/C,MAAM,gBAAgB,gBAAgB;GACtC,MAAM,eAAe,gBAAgB,SAAS;AAC9C,OAAI,kBAAkB,UAAa,aACjC,eAAc,YAAY,gBACxB,eACA,aAAa,SACd;;AAIL,SAAO;GACL,aAAa,YAAY,WAAW;GACpC;GACA,aAAa,EACX,gBAAgB,iBACjB;GACF"}
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
2
+ * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.
3
3
  * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
4
  * you may not use this file except in compliance with the License. You may obtain a copy
5
5
  * of the License at https://www.apache.org/licenses/LICENSE-2.0
@@ -1 +1 @@
1
- {"version":3,"file":"scale-mode-with-free-transform.js","names":["origin: Position"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n type DraggingEvent,\n type FeatureCollection,\n type ModeProps,\n ScaleMode,\n type StopDraggingEvent,\n} from '@deck.gl-community/editable-layers';\nimport type { Position } from 'geojson';\n\ntype ScaleFactors = {\n scaleX: number;\n scaleY: number;\n};\n\ntype ScaleContext = {\n origin: Position;\n scaleFactors: ScaleFactors;\n geometry: FeatureCollection;\n};\n\n/**\n * Extends ScaleMode to support non-uniform (free) scaling.\n *\n * ## Features\n * - **Default**: Free scaling - can stretch/squish in any direction\n * - **With Shift**: Uniform scaling (maintains aspect ratio)\n *\n * ## How Non-Uniform Scaling Works\n *\n * Non-uniform scaling applies separate X and Y scale factors based on cursor\n * movement relative to the opposite corner (the \"origin\" or anchor point).\n *\n * ```\n * Origin (opposite corner) Drag Handle (start)\n * ●───────────────────────────────────●\n * │ │\n * │ startDelta = start - origin │\n * │ currentDelta = current - origin\n * │ │\n * │ scaleX = currentDeltaX / startDeltaX\n * │ scaleY = currentDeltaY / startDeltaY\n * │ │\n * ●───────────────────────────────────● Cursor (current)\n * ```\n *\n * Each coordinate is transformed: `newCoord = origin + (oldCoord - origin) × scale`\n *\n * ## Why We Override Parent's Uniform Scaling\n *\n * The parent ScaleMode calculates uniform scale factors in screen coordinates,\n * which can distort rotated shapes. We calculate our own uniform factor using\n * vector projection to ensure the corner follows the cursor's projection onto\n * the original drag line, preserving shape orientation.\n *\n * ## Minimum Scale Clamping\n *\n * All scale factors are clamped to a minimum of 0.01 to prevent:\n * - Shape inversion (negative scale flipping the shape inside-out)\n * - Shape collapse (scale of 0 making the shape a point/line)\n *\n * @example\n * ```typescript\n * import { ScaleModeWithFreeTransform } from '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform';\n * import { EditableGeoJsonLayer } from '@deck.gl-community/editable-layers';\n *\n * const mode = new ScaleModeWithFreeTransform();\n *\n * const layer = new EditableGeoJsonLayer({\n * mode,\n * data: featureCollection,\n * selectedFeatureIndexes: [0],\n * onEdit: handleEdit,\n * modeConfig: {\n * lockScaling: false, // Default: free scaling (stretch/squish)\n * // lockScaling: true, // Hold Shift: uniform scaling (maintain aspect ratio)\n * },\n * });\n * ```\n */\nexport class ScaleModeWithFreeTransform extends ScaleMode {\n /**\n * Override handleDragging to support non-uniform scaling.\n * When lockScaling is false (default), applies separate X/Y scale factors.\n * When lockScaling is true, applies uniform scaling (same factor for X and Y).\n *\n * Note: We don't use parent's handleDragging for uniform scaling because it\n * calculates scale factors in screen coordinates which distorts rotated shapes.\n * Instead, we calculate our own uniform scale factor based on distance ratios.\n */\n override handleDragging(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n const self = this as any;\n\n if (!self._isScaling) {\n return;\n }\n\n props.onUpdateCursor(self._cursor);\n\n const scaleContext = this.getScaleContext(event, self);\n if (!scaleContext) {\n return;\n }\n\n const lockScaling = props.modeConfig?.lockScaling ?? false;\n const { origin, scaleFactors, geometry } = scaleContext;\n\n // For uniform scaling, use a single scale factor for both axes\n // Calculate based on distance from origin to preserve aspect ratio\n const finalScaleX = lockScaling\n ? this.calculateUniformScaleFactor(event, origin)\n : scaleFactors.scaleX;\n const finalScaleY = lockScaling\n ? this.calculateUniformScaleFactor(event, origin)\n : scaleFactors.scaleY;\n\n const scaledFeatures = this.applyNonUniformScale(\n geometry,\n finalScaleX,\n finalScaleY,\n origin,\n );\n\n const updatedData = self._getUpdatedData(props, scaledFeatures);\n\n props.onEdit({\n updatedData,\n editType: 'scaling',\n editContext: {\n featureIndexes: props.selectedIndexes,\n },\n });\n\n event.cancelPan();\n }\n\n /**\n * Override handleStopDragging to emit the final scaled geometry.\n * Uses the same uniform/non-uniform scaling logic as handleDragging.\n */\n override handleStopDragging(\n event: StopDraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n const self = this as any;\n\n if (self._isScaling) {\n this.emitFinalScaledGeometry(event, props, self);\n this.resetScaleState(props, self);\n }\n }\n\n /**\n * Get the scale context (origin, scale factors, geometry) for the current drag.\n * Returns null if required data is not available.\n */\n private getScaleContext(\n event: DraggingEvent | StopDraggingEvent,\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n self: any,\n ): ScaleContext | null {\n if (!self._selectedEditHandle) {\n return null;\n }\n\n const oppositeHandle = self._getOppositeScaleHandle(\n self._selectedEditHandle,\n );\n if (!oppositeHandle) {\n return null;\n }\n\n const geometry = self._geometryBeingScaled;\n if (!geometry) {\n return null;\n }\n\n const origin: Position = oppositeHandle.geometry.coordinates;\n const scaleFactors = this.calculateScaleFactors(event, origin);\n\n return { origin, scaleFactors, geometry };\n }\n\n /**\n * Calculate separate X and Y scale factors based on cursor movement.\n *\n * ## Math Explanation\n *\n * For each axis, we compute: `scale = currentDelta / startDelta`\n *\n * Where:\n * - `startDelta` = distance from origin to where drag started (the handle position)\n * - `currentDelta` = distance from origin to current cursor position\n *\n * Example: If the handle started 100px from origin and cursor is now 150px from origin,\n * scale = 150/100 = 1.5 (shape grows to 150% of original size along that axis).\n *\n * ## Edge Cases\n * - If `startDelta` is near zero (handle very close to origin), we return scale=1\n * to avoid division by zero and prevent erratic behavior\n * - Negative scale values are clamped to 0.01 minimum to prevent shape inversion\n */\n private calculateScaleFactors(\n event: DraggingEvent | StopDraggingEvent,\n origin: Position,\n ): ScaleFactors {\n const startDragPoint = event.pointerDownMapCoords;\n const currentPoint = event.mapCoords;\n\n // Calculate deltas from the anchor point (origin) to drag positions\n const startDeltaX = (startDragPoint[0] ?? 0) - (origin[0] ?? 0);\n const startDeltaY = (startDragPoint[1] ?? 0) - (origin[1] ?? 0);\n const currentDeltaX = (currentPoint[0] ?? 0) - (origin[0] ?? 0);\n const currentDeltaY = (currentPoint[1] ?? 0) - (origin[1] ?? 0);\n\n // Epsilon for near-zero checks to avoid division by zero\n const epsilon = 0.0000001;\n // Minimum scale to prevent shape from collapsing or inverting\n const minScale = 0.01;\n\n // Scale = ratio of (current distance from origin) / (original distance from origin)\n // If original distance is near-zero, default to scale=1 (no change)\n const rawScaleX =\n Math.abs(startDeltaX) > epsilon ? currentDeltaX / startDeltaX : 1;\n const rawScaleY =\n Math.abs(startDeltaY) > epsilon ? currentDeltaY / startDeltaY : 1;\n\n // Clamp to prevent negative values (which would invert/flip the shape)\n const scaleX = Math.max(rawScaleX, minScale);\n const scaleY = Math.max(rawScaleY, minScale);\n\n return { scaleX, scaleY };\n }\n\n /**\n * Calculate a uniform scale factor for aspect-ratio-preserving scaling.\n *\n * ## Why Use Projection Instead of Simple Distance Ratio?\n *\n * A naive approach would be: `scale = distance(origin, cursor) / distance(origin, start)`\n *\n * But this fails for diagonal movements - if you drag a corner handle and move\n * perpendicular to the diagonal, the simple distance changes even though you\n * don't want the shape to scale.\n *\n * ## Vector Projection Math\n *\n * Instead, we project the cursor position onto the line defined by\n * (origin → start drag point). This way, only movement along the original\n * drag direction affects the scale.\n *\n * ```\n * Origin Start (drag handle)\n * ●──────────────────●\n * \\\n * \\ Cursor moved diagonally\n * ●\n * /\n * / Projected point (what we measure from)\n * ●──────────────────●\n * Origin Projection\n * ```\n *\n * The projection formula uses the dot product:\n * ```\n * projectedDist = (current · start) / |start|\n * scale = projectedDist / |start|\n * ```\n *\n * This equals: `scale = (current · start) / |start|²`\n */\n private calculateUniformScaleFactor(\n event: DraggingEvent | StopDraggingEvent,\n origin: Position,\n ): number {\n const startDragPoint = event.pointerDownMapCoords;\n const currentPoint = event.mapCoords;\n\n // Vector from origin to start drag point (defines the scaling direction)\n const startDeltaX = (startDragPoint[0] ?? 0) - (origin[0] ?? 0);\n const startDeltaY = (startDragPoint[1] ?? 0) - (origin[1] ?? 0);\n\n // Vector from origin to current cursor position\n const currentDeltaX = (currentPoint[0] ?? 0) - (origin[0] ?? 0);\n const currentDeltaY = (currentPoint[1] ?? 0) - (origin[1] ?? 0);\n\n // Distance from origin to start point (|start|)\n const startDist = Math.sqrt(startDeltaX ** 2 + startDeltaY ** 2);\n\n if (startDist < 0.0000001) {\n return 1;\n }\n\n // Dot product: current · start = |current| × |start| × cos(θ)\n // This gives us the component of 'current' that lies along 'start'\n const dotProduct =\n currentDeltaX * startDeltaX + currentDeltaY * startDeltaY;\n\n // Project current point onto start vector: projectedDist = (current · start) / |start|\n const projectedDist = dotProduct / startDist;\n\n // Scale factor = projectedDist / |start| = (current · start) / |start|²\n // Clamp to minScale to prevent negative values (shape inversion)\n const minScale = 0.01;\n const rawScale = projectedDist / startDist;\n return Math.max(rawScale, minScale);\n }\n\n /**\n * Emit the final scaled geometry when dragging stops.\n */\n private emitFinalScaledGeometry(\n event: StopDraggingEvent,\n props: ModeProps<FeatureCollection>,\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n self: any,\n ) {\n const scaleContext = this.getScaleContext(event, self);\n if (!scaleContext) {\n return;\n }\n\n const lockScaling = props.modeConfig?.lockScaling ?? false;\n const { origin, scaleFactors, geometry } = scaleContext;\n\n // For uniform scaling, use a single scale factor for both axes\n const finalScaleX = lockScaling\n ? this.calculateUniformScaleFactor(event, origin)\n : scaleFactors.scaleX;\n const finalScaleY = lockScaling\n ? this.calculateUniformScaleFactor(event, origin)\n : scaleFactors.scaleY;\n\n const scaledFeatures = this.applyNonUniformScale(\n geometry,\n finalScaleX,\n finalScaleY,\n origin,\n );\n\n const updatedData = self._getUpdatedData(props, scaledFeatures);\n\n props.onEdit({\n updatedData,\n editType: 'scaled',\n editContext: {\n featureIndexes: props.selectedIndexes,\n },\n });\n }\n\n /**\n * Reset the scale state after dragging stops.\n */\n private resetScaleState(\n props: ModeProps<FeatureCollection>,\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n self: any,\n ) {\n props.onUpdateCursor(null);\n self._geometryBeingScaled = null;\n self._selectedEditHandle = null;\n self._cursor = null;\n self._isScaling = false;\n }\n\n /**\n * Apply non-uniform scaling to geometry.\n * Transforms each coordinate by scaling X and Y independently around the origin.\n */\n private applyNonUniformScale(\n geometry: FeatureCollection,\n scaleX: number,\n scaleY: number,\n origin: Position,\n ): FeatureCollection {\n const scaledFeatures = geometry.features.map((feature) => {\n const scaledGeometry = this.scaleGeometry(\n feature.geometry,\n scaleX,\n scaleY,\n origin,\n );\n return {\n ...feature,\n geometry: scaledGeometry,\n };\n });\n\n return {\n type: 'FeatureCollection',\n // biome-ignore lint/suspicious/noExplicitAny: GeoJSON feature type variance\n features: scaledFeatures as any,\n };\n }\n\n /**\n * Scale a geometry's coordinates non-uniformly around an origin point.\n *\n * ## Coordinate Transformation\n *\n * Each coordinate is transformed using: `new = origin + (old - origin) × scale`\n *\n * This is equivalent to:\n * 1. Translate so origin is at (0,0): `temp = old - origin`\n * 2. Scale: `temp = temp × scale`\n * 3. Translate back: `new = temp + origin`\n *\n * The origin (opposite corner from the drag handle) stays fixed while\n * all other points move proportionally.\n */\n private scaleGeometry(\n // biome-ignore lint/suspicious/noExplicitAny: GeoJSON geometry types are complex - includes Point, LineString, Polygon, Multi* variants\n geometry: any,\n scaleX: number,\n scaleY: number,\n origin: Position,\n // biome-ignore lint/suspicious/noExplicitAny: GeoJSON geometry types are complex - return type varies by input\n ): any {\n // Transform a single coordinate around the origin point\n const scaleCoord = (coord: Position): Position => {\n return [\n (origin[0] ?? 0) + ((coord[0] ?? 0) - (origin[0] ?? 0)) * scaleX,\n (origin[1] ?? 0) + ((coord[1] ?? 0) - (origin[1] ?? 0)) * scaleY,\n ];\n };\n\n switch (geometry.type) {\n case 'Point':\n return {\n ...geometry,\n coordinates: scaleCoord(geometry.coordinates),\n };\n case 'LineString':\n case 'MultiPoint':\n return {\n ...geometry,\n coordinates: geometry.coordinates.map(scaleCoord),\n };\n case 'Polygon':\n case 'MultiLineString':\n return {\n ...geometry,\n coordinates: geometry.coordinates.map((ring: Position[]) =>\n ring.map(scaleCoord),\n ),\n };\n case 'MultiPolygon':\n return {\n ...geometry,\n coordinates: geometry.coordinates.map((polygon: Position[][]) =>\n polygon.map((ring: Position[]) => ring.map(scaleCoord)),\n ),\n };\n default:\n return geometry;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scale-mode-with-free-transform.js","names":["origin: Position"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n type DraggingEvent,\n type ModeProps,\n ScaleMode,\n type SimpleFeatureCollection,\n type StopDraggingEvent,\n} from '@deck.gl-community/editable-layers';\nimport type { Position } from 'geojson';\n\ntype ScaleFactors = {\n scaleX: number;\n scaleY: number;\n};\n\ntype ScaleContext = {\n origin: Position;\n scaleFactors: ScaleFactors;\n geometry: SimpleFeatureCollection;\n};\n\n/**\n * Extends ScaleMode to support non-uniform (free) scaling.\n *\n * ## Features\n * - **Default**: Free scaling - can stretch/squish in any direction\n * - **With Shift**: Uniform scaling (maintains aspect ratio)\n *\n * ## How Non-Uniform Scaling Works\n *\n * Non-uniform scaling applies separate X and Y scale factors based on cursor\n * movement relative to the opposite corner (the \"origin\" or anchor point).\n *\n * ```\n * Origin (opposite corner) Drag Handle (start)\n * ●───────────────────────────────────●\n * │ │\n * │ startDelta = start - origin │\n * │ currentDelta = current - origin\n * │ │\n * │ scaleX = currentDeltaX / startDeltaX\n * │ scaleY = currentDeltaY / startDeltaY\n * │ │\n * ●───────────────────────────────────● Cursor (current)\n * ```\n *\n * Each coordinate is transformed: `newCoord = origin + (oldCoord - origin) × scale`\n *\n * ## Why We Override Parent's Uniform Scaling\n *\n * The parent ScaleMode calculates uniform scale factors in screen coordinates,\n * which can distort rotated shapes. We calculate our own uniform factor using\n * vector projection to ensure the corner follows the cursor's projection onto\n * the original drag line, preserving shape orientation.\n *\n * ## Minimum Scale Clamping\n *\n * All scale factors are clamped to a minimum of 0.01 to prevent:\n * - Shape inversion (negative scale flipping the shape inside-out)\n * - Shape collapse (scale of 0 making the shape a point/line)\n *\n * @example\n * ```typescript\n * import { ScaleModeWithFreeTransform } from '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform';\n * import { EditableGeoJsonLayer } from '@deck.gl-community/editable-layers';\n *\n * const mode = new ScaleModeWithFreeTransform();\n *\n * const layer = new EditableGeoJsonLayer({\n * mode,\n * data: featureCollection,\n * selectedFeatureIndexes: [0],\n * onEdit: handleEdit,\n * modeConfig: {\n * lockScaling: false, // Default: free scaling (stretch/squish)\n * // lockScaling: true, // Hold Shift: uniform scaling (maintain aspect ratio)\n * },\n * });\n * ```\n */\nexport class ScaleModeWithFreeTransform extends ScaleMode {\n /**\n * Override handleDragging to support non-uniform scaling.\n * When lockScaling is false (default), applies separate X/Y scale factors.\n * When lockScaling is true, applies uniform scaling (same factor for X and Y).\n *\n * Note: We don't use parent's handleDragging for uniform scaling because it\n * calculates scale factors in screen coordinates which distorts rotated shapes.\n * Instead, we calculate our own uniform scale factor based on distance ratios.\n */\n override handleDragging(\n event: DraggingEvent,\n props: ModeProps<SimpleFeatureCollection>,\n ) {\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n const self = this as any;\n\n if (!self._isScaling) {\n return;\n }\n\n props.onUpdateCursor(self._cursor);\n\n const scaleContext = this.getScaleContext(event, self);\n if (!scaleContext) {\n return;\n }\n\n const lockScaling = props.modeConfig?.lockScaling ?? false;\n const { origin, scaleFactors, geometry } = scaleContext;\n\n // For uniform scaling, use a single scale factor for both axes\n // Calculate based on distance from origin to preserve aspect ratio\n const finalScaleX = lockScaling\n ? this.calculateUniformScaleFactor(event, origin)\n : scaleFactors.scaleX;\n const finalScaleY = lockScaling\n ? this.calculateUniformScaleFactor(event, origin)\n : scaleFactors.scaleY;\n\n const scaledFeatures = this.applyNonUniformScale(\n geometry,\n finalScaleX,\n finalScaleY,\n origin,\n );\n\n const updatedData = self._getUpdatedData(props, scaledFeatures);\n\n props.onEdit({\n updatedData,\n editType: 'scaling',\n editContext: {\n featureIndexes: props.selectedIndexes,\n },\n });\n\n event.cancelPan();\n }\n\n /**\n * Override handleStopDragging to emit the final scaled geometry.\n * Uses the same uniform/non-uniform scaling logic as handleDragging.\n */\n override handleStopDragging(\n event: StopDraggingEvent,\n props: ModeProps<SimpleFeatureCollection>,\n ) {\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n const self = this as any;\n\n if (self._isScaling) {\n this.emitFinalScaledGeometry(event, props, self);\n this.resetScaleState(props, self);\n }\n }\n\n /**\n * Get the scale context (origin, scale factors, geometry) for the current drag.\n * Returns null if required data is not available.\n */\n private getScaleContext(\n event: DraggingEvent | StopDraggingEvent,\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n self: any,\n ): ScaleContext | null {\n if (!self._selectedEditHandle) {\n return null;\n }\n\n const oppositeHandle = self._getOppositeScaleHandle(\n self._selectedEditHandle,\n );\n if (!oppositeHandle) {\n return null;\n }\n\n const geometry = self._geometryBeingScaled;\n if (!geometry) {\n return null;\n }\n\n const origin: Position = oppositeHandle.geometry.coordinates;\n const scaleFactors = this.calculateScaleFactors(event, origin);\n\n return { origin, scaleFactors, geometry };\n }\n\n /**\n * Calculate separate X and Y scale factors based on cursor movement.\n *\n * ## Math Explanation\n *\n * For each axis, we compute: `scale = currentDelta / startDelta`\n *\n * Where:\n * - `startDelta` = distance from origin to where drag started (the handle position)\n * - `currentDelta` = distance from origin to current cursor position\n *\n * Example: If the handle started 100px from origin and cursor is now 150px from origin,\n * scale = 150/100 = 1.5 (shape grows to 150% of original size along that axis).\n *\n * ## Edge Cases\n * - If `startDelta` is near zero (handle very close to origin), we return scale=1\n * to avoid division by zero and prevent erratic behavior\n * - Negative scale values are clamped to 0.01 minimum to prevent shape inversion\n */\n private calculateScaleFactors(\n event: DraggingEvent | StopDraggingEvent,\n origin: Position,\n ): ScaleFactors {\n const startDragPoint = event.pointerDownMapCoords;\n const currentPoint = event.mapCoords;\n\n // Calculate deltas from the anchor point (origin) to drag positions\n const startDeltaX = (startDragPoint[0] ?? 0) - (origin[0] ?? 0);\n const startDeltaY = (startDragPoint[1] ?? 0) - (origin[1] ?? 0);\n const currentDeltaX = (currentPoint[0] ?? 0) - (origin[0] ?? 0);\n const currentDeltaY = (currentPoint[1] ?? 0) - (origin[1] ?? 0);\n\n // Epsilon for near-zero checks to avoid division by zero\n const epsilon = 0.0000001;\n // Minimum scale to prevent shape from collapsing or inverting\n const minScale = 0.01;\n\n // Scale = ratio of (current distance from origin) / (original distance from origin)\n // If original distance is near-zero, default to scale=1 (no change)\n const rawScaleX =\n Math.abs(startDeltaX) > epsilon ? currentDeltaX / startDeltaX : 1;\n const rawScaleY =\n Math.abs(startDeltaY) > epsilon ? currentDeltaY / startDeltaY : 1;\n\n // Clamp to prevent negative values (which would invert/flip the shape)\n const scaleX = Math.max(rawScaleX, minScale);\n const scaleY = Math.max(rawScaleY, minScale);\n\n return { scaleX, scaleY };\n }\n\n /**\n * Calculate a uniform scale factor for aspect-ratio-preserving scaling.\n *\n * ## Why Use Projection Instead of Simple Distance Ratio?\n *\n * A naive approach would be: `scale = distance(origin, cursor) / distance(origin, start)`\n *\n * But this fails for diagonal movements - if you drag a corner handle and move\n * perpendicular to the diagonal, the simple distance changes even though you\n * don't want the shape to scale.\n *\n * ## Vector Projection Math\n *\n * Instead, we project the cursor position onto the line defined by\n * (origin → start drag point). This way, only movement along the original\n * drag direction affects the scale.\n *\n * ```\n * Origin Start (drag handle)\n * ●──────────────────●\n * \\\n * \\ Cursor moved diagonally\n * ●\n * /\n * / Projected point (what we measure from)\n * ●──────────────────●\n * Origin Projection\n * ```\n *\n * The projection formula uses the dot product:\n * ```\n * projectedDist = (current · start) / |start|\n * scale = projectedDist / |start|\n * ```\n *\n * This equals: `scale = (current · start) / |start|²`\n */\n private calculateUniformScaleFactor(\n event: DraggingEvent | StopDraggingEvent,\n origin: Position,\n ): number {\n const startDragPoint = event.pointerDownMapCoords;\n const currentPoint = event.mapCoords;\n\n // Vector from origin to start drag point (defines the scaling direction)\n const startDeltaX = (startDragPoint[0] ?? 0) - (origin[0] ?? 0);\n const startDeltaY = (startDragPoint[1] ?? 0) - (origin[1] ?? 0);\n\n // Vector from origin to current cursor position\n const currentDeltaX = (currentPoint[0] ?? 0) - (origin[0] ?? 0);\n const currentDeltaY = (currentPoint[1] ?? 0) - (origin[1] ?? 0);\n\n // Distance from origin to start point (|start|)\n const startDist = Math.sqrt(startDeltaX ** 2 + startDeltaY ** 2);\n\n if (startDist < 0.0000001) {\n return 1;\n }\n\n // Dot product: current · start = |current| × |start| × cos(θ)\n // This gives us the component of 'current' that lies along 'start'\n const dotProduct =\n currentDeltaX * startDeltaX + currentDeltaY * startDeltaY;\n\n // Project current point onto start vector: projectedDist = (current · start) / |start|\n const projectedDist = dotProduct / startDist;\n\n // Scale factor = projectedDist / |start| = (current · start) / |start|²\n // Clamp to minScale to prevent negative values (shape inversion)\n const minScale = 0.01;\n const rawScale = projectedDist / startDist;\n return Math.max(rawScale, minScale);\n }\n\n /**\n * Emit the final scaled geometry when dragging stops.\n */\n private emitFinalScaledGeometry(\n event: StopDraggingEvent,\n props: ModeProps<SimpleFeatureCollection>,\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n self: any,\n ) {\n const scaleContext = this.getScaleContext(event, self);\n if (!scaleContext) {\n return;\n }\n\n const lockScaling = props.modeConfig?.lockScaling ?? false;\n const { origin, scaleFactors, geometry } = scaleContext;\n\n // For uniform scaling, use a single scale factor for both axes\n const finalScaleX = lockScaling\n ? this.calculateUniformScaleFactor(event, origin)\n : scaleFactors.scaleX;\n const finalScaleY = lockScaling\n ? this.calculateUniformScaleFactor(event, origin)\n : scaleFactors.scaleY;\n\n const scaledFeatures = this.applyNonUniformScale(\n geometry,\n finalScaleX,\n finalScaleY,\n origin,\n );\n\n const updatedData = self._getUpdatedData(props, scaledFeatures);\n\n props.onEdit({\n updatedData,\n editType: 'scaled',\n editContext: {\n featureIndexes: props.selectedIndexes,\n },\n });\n }\n\n /**\n * Reset the scale state after dragging stops.\n */\n private resetScaleState(\n props: ModeProps<SimpleFeatureCollection>,\n // biome-ignore lint/suspicious/noExplicitAny: Accessing private properties from parent class\n self: any,\n ) {\n props.onUpdateCursor(null);\n self._geometryBeingScaled = null;\n self._selectedEditHandle = null;\n self._cursor = null;\n self._isScaling = false;\n }\n\n /**\n * Apply non-uniform scaling to geometry.\n * Transforms each coordinate by scaling X and Y independently around the origin.\n */\n private applyNonUniformScale(\n geometry: SimpleFeatureCollection,\n scaleX: number,\n scaleY: number,\n origin: Position,\n ): SimpleFeatureCollection {\n const scaledFeatures = geometry.features.map((feature) => {\n const scaledGeometry = this.scaleGeometry(\n feature.geometry,\n scaleX,\n scaleY,\n origin,\n );\n return {\n ...feature,\n geometry: scaledGeometry,\n };\n });\n\n return {\n type: 'FeatureCollection',\n // biome-ignore lint/suspicious/noExplicitAny: GeoJSON feature type variance\n features: scaledFeatures as any,\n };\n }\n\n /**\n * Scale a geometry's coordinates non-uniformly around an origin point.\n *\n * ## Coordinate Transformation\n *\n * Each coordinate is transformed using: `new = origin + (old - origin) × scale`\n *\n * This is equivalent to:\n * 1. Translate so origin is at (0,0): `temp = old - origin`\n * 2. Scale: `temp = temp × scale`\n * 3. Translate back: `new = temp + origin`\n *\n * The origin (opposite corner from the drag handle) stays fixed while\n * all other points move proportionally.\n */\n private scaleGeometry(\n // biome-ignore lint/suspicious/noExplicitAny: GeoJSON geometry types are complex - includes Point, LineString, Polygon, Multi* variants\n geometry: any,\n scaleX: number,\n scaleY: number,\n origin: Position,\n // biome-ignore lint/suspicious/noExplicitAny: GeoJSON geometry types are complex - return type varies by input\n ): any {\n // Transform a single coordinate around the origin point\n const scaleCoord = (coord: Position): Position => {\n return [\n (origin[0] ?? 0) + ((coord[0] ?? 0) - (origin[0] ?? 0)) * scaleX,\n (origin[1] ?? 0) + ((coord[1] ?? 0) - (origin[1] ?? 0)) * scaleY,\n ];\n };\n\n switch (geometry.type) {\n case 'Point':\n return {\n ...geometry,\n coordinates: scaleCoord(geometry.coordinates),\n };\n case 'LineString':\n case 'MultiPoint':\n return {\n ...geometry,\n coordinates: geometry.coordinates.map(scaleCoord),\n };\n case 'Polygon':\n case 'MultiLineString':\n return {\n ...geometry,\n coordinates: geometry.coordinates.map((ring: Position[]) =>\n ring.map(scaleCoord),\n ),\n };\n case 'MultiPolygon':\n return {\n ...geometry,\n coordinates: geometry.coordinates.map((polygon: Position[][]) =>\n polygon.map((ring: Position[]) => ring.map(scaleCoord)),\n ),\n };\n default:\n return geometry;\n }\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
2
+ * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.
3
3
  * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
4
  * you may not use this file except in compliance with the License. You may obtain a copy
5
5
  * of the License at https://www.apache.org/licenses/LICENSE-2.0