@accelint/map-toolkit 1.0.0 → 1.2.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 (84) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/catalog-info.yaml +4 -4
  3. package/dist/camera/events.js.map +1 -1
  4. package/dist/camera/store.js.map +1 -1
  5. package/dist/cursor-coordinates/index.d.ts +4 -2
  6. package/dist/cursor-coordinates/index.js +3 -2
  7. package/dist/cursor-coordinates/store.d.ts +48 -0
  8. package/dist/cursor-coordinates/store.js +92 -0
  9. package/dist/cursor-coordinates/store.js.map +1 -0
  10. package/dist/cursor-coordinates/types.d.ts +87 -0
  11. package/dist/cursor-coordinates/types.js +12 -0
  12. package/dist/cursor-coordinates/use-cursor-coordinates.d.ts +41 -37
  13. package/dist/cursor-coordinates/use-cursor-coordinates.js +131 -202
  14. package/dist/cursor-coordinates/use-cursor-coordinates.js.map +1 -1
  15. package/dist/deckgl/base-map/controls.d.ts +1 -1
  16. package/dist/deckgl/base-map/controls.js.map +1 -1
  17. package/dist/deckgl/base-map/events.js.map +1 -1
  18. package/dist/deckgl/base-map/index.d.ts +2 -2
  19. package/dist/deckgl/base-map/index.js +3 -3
  20. package/dist/deckgl/base-map/provider.d.ts +2 -2
  21. package/dist/deckgl/base-map/provider.js.map +1 -1
  22. package/dist/deckgl/index.js +1 -1
  23. package/dist/deckgl/saved-viewports/index.js.map +1 -1
  24. package/dist/deckgl/saved-viewports/storage.js.map +1 -1
  25. package/dist/deckgl/shapes/display-shape-layer/constants.js.map +1 -1
  26. package/dist/deckgl/shapes/display-shape-layer/fiber.js.map +1 -1
  27. package/dist/deckgl/shapes/display-shape-layer/index.js.map +1 -1
  28. package/dist/deckgl/shapes/display-shape-layer/shape-label-layer.js.map +1 -1
  29. package/dist/deckgl/shapes/display-shape-layer/store.js.map +1 -1
  30. package/dist/deckgl/shapes/display-shape-layer/use-select-shape.js.map +1 -1
  31. package/dist/deckgl/shapes/display-shape-layer/utils/display-style.js.map +1 -1
  32. package/dist/deckgl/shapes/display-shape-layer/utils/labels.js.map +1 -1
  33. package/dist/deckgl/shapes/draw-shape-layer/constants.js.map +1 -1
  34. package/dist/deckgl/shapes/draw-shape-layer/events.js.map +1 -1
  35. package/dist/deckgl/shapes/draw-shape-layer/fiber.js.map +1 -1
  36. package/dist/deckgl/shapes/draw-shape-layer/index.js.map +1 -1
  37. package/dist/deckgl/shapes/draw-shape-layer/modes/draw-ellipse-mode-with-tooltip.js.map +1 -1
  38. package/dist/deckgl/shapes/draw-shape-layer/modes/index.js.map +1 -1
  39. package/dist/deckgl/shapes/draw-shape-layer/store.js.map +1 -1
  40. package/dist/deckgl/shapes/draw-shape-layer/use-draw-shape.js.map +1 -1
  41. package/dist/deckgl/shapes/draw-shape-layer/utils/feature-conversion.js.map +1 -1
  42. package/dist/deckgl/shapes/edit-shape-layer/constants.js.map +1 -1
  43. package/dist/deckgl/shapes/edit-shape-layer/events.js.map +1 -1
  44. package/dist/deckgl/shapes/edit-shape-layer/index.js.map +1 -1
  45. package/dist/deckgl/shapes/edit-shape-layer/modes/base-transform-mode.js.map +1 -1
  46. package/dist/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.js.map +1 -1
  47. package/dist/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode.js.map +1 -1
  48. package/dist/deckgl/shapes/edit-shape-layer/modes/index.js.map +1 -1
  49. package/dist/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap.js.map +1 -1
  50. package/dist/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform.js.map +1 -1
  51. package/dist/deckgl/shapes/edit-shape-layer/modes/vertex-transform-mode.js.map +1 -1
  52. package/dist/deckgl/shapes/edit-shape-layer/store.js.map +1 -1
  53. package/dist/deckgl/shapes/edit-shape-layer/use-edit-shape.js.map +1 -1
  54. package/dist/deckgl/shapes/index.d.ts +1 -1
  55. package/dist/deckgl/shapes/shared/constants.js.map +1 -1
  56. package/dist/deckgl/shapes/shared/events.js.map +1 -1
  57. package/dist/deckgl/shapes/shared/hooks/use-shift-zoom-disable.js.map +1 -1
  58. package/dist/deckgl/shapes/shared/types.js.map +1 -1
  59. package/dist/deckgl/shapes/shared/utils/geometry-measurements.js.map +1 -1
  60. package/dist/deckgl/shapes/shared/utils/layer-config.js.map +1 -1
  61. package/dist/deckgl/shapes/shared/utils/mode-utils.js.map +1 -1
  62. package/dist/deckgl/shapes/shared/utils/pick-filtering.js.map +1 -1
  63. package/dist/deckgl/shapes/shared/utils/style-utils.js.map +1 -1
  64. package/dist/deckgl/symbol-layer/fiber.js.map +1 -1
  65. package/dist/deckgl/symbol-layer/index.js.map +1 -1
  66. package/dist/deckgl/text-layer/character-sets.js.map +1 -1
  67. package/dist/deckgl/text-layer/default-settings.js.map +1 -1
  68. package/dist/deckgl/text-layer/fiber.js.map +1 -1
  69. package/dist/deckgl/text-layer/index.js.map +1 -1
  70. package/dist/deckgl/text-settings.js.map +1 -1
  71. package/dist/map-cursor/events.js.map +1 -1
  72. package/dist/map-cursor/store.js.map +1 -1
  73. package/dist/map-cursor/use-map-cursor.js.map +1 -1
  74. package/dist/map-mode/events.js.map +1 -1
  75. package/dist/map-mode/store.js.map +1 -1
  76. package/dist/map-mode/use-map-mode.js.map +1 -1
  77. package/dist/maplibre/hooks/use-maplibre.js.map +1 -1
  78. package/dist/shared/constants.js.map +1 -1
  79. package/dist/shared/create-map-store.js.map +1 -1
  80. package/dist/shared/units.js.map +1 -1
  81. package/dist/viewport/store.js.map +1 -1
  82. package/dist/viewport/utils.js.map +1 -1
  83. package/dist/viewport/viewport-size.js.map +1 -1
  84. package/package.json +12 -10
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","names":["DEFAULT_DRAWING_STATE: DrawingState"],"sources":["../../../../src/deckgl/shapes/draw-shape-layer/store.ts"],"sourcesContent":["/*\n * Copyright 2025 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\n/**\n * Draw Shape Store\n *\n * Manages drawing state for shape creation.\n *\n * @example\n * ```tsx\n * import { drawStore } from '@accelint/map-toolkit/deckgl/shapes';\n *\n * function DrawControls({ mapId }) {\n * const { state, draw, cancel } = drawStore.use(mapId);\n *\n * return (\n * <div>\n * <p>Drawing: {state.activeShapeType ?? 'none'}</p>\n * <button onClick={() => draw('Polygon')}>Draw Polygon</button>\n * <button onClick={cancel}>Cancel</button>\n * </div>\n * );\n * }\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { MapModeEvents } from '@/map-mode/events';\nimport { createMapStore } from '@/shared/create-map-store';\nimport {\n releaseModeAndCursor,\n requestModeAndCursor,\n} from '../shared/utils/mode-utils';\nimport {\n DRAW_CURSOR_MAP,\n DRAW_SHAPE_LAYER_ID,\n DRAW_SHAPE_MODE,\n} from './constants';\nimport { DrawShapeEvents } from './events';\nimport { convertFeatureToShape } from './utils/feature-conversion';\nimport type { UniqueId } from '@accelint/core';\nimport type { Feature } from 'geojson';\nimport type { MapModeEventType } from '@/map-mode/types';\nimport type { Shape, ShapeFeatureType } from '../shared/types';\nimport type { DrawShapeEvent, ShapeDrawnEvent } from './events';\nimport type { DrawFunction, DrawingState, DrawShapeOptions } from './types';\n\n/**\n * Typed event bus instances\n */\nconst drawShapeBus = Broadcast.getInstance<DrawShapeEvent>();\nconst mapModeBus = Broadcast.getInstance<MapModeEventType>();\n\n/**\n * Default drawing state\n */\nconst DEFAULT_DRAWING_STATE: DrawingState = {\n activeShapeType: null,\n tentativeFeature: null,\n styleDefaults: null,\n circleDefaults: null,\n};\n\n/**\n * Actions for draw shape store\n */\ntype DrawShapeActions = {\n /** Start drawing a shape of the specified type */\n draw: DrawFunction;\n /** Cancel the current drawing operation */\n cancel: () => void;\n};\n\n/**\n * Start drawing a shape\n */\nfunction startDrawing(\n mapId: UniqueId,\n state: DrawingState,\n shapeType: ShapeFeatureType,\n options: DrawShapeOptions | undefined,\n notify: () => void,\n setState: (updates: Partial<DrawingState>) => void,\n): void {\n // Already drawing - cancel first\n if (state.activeShapeType) {\n cancelDrawingInternal(mapId, state, notify, setState);\n }\n\n // Update state with new object reference\n setState({\n activeShapeType: shapeType,\n tentativeFeature: null,\n styleDefaults: options?.styleDefaults ?? null,\n circleDefaults: options?.circleDefaults ?? null,\n });\n\n // Request map mode and cursor using shared utilities\n const cursor = DRAW_CURSOR_MAP[shapeType];\n requestModeAndCursor(mapId, DRAW_SHAPE_MODE, cursor, DRAW_SHAPE_LAYER_ID);\n\n // Emit drawing started event\n drawShapeBus.emit(DrawShapeEvents.drawing, {\n shapeType,\n mapId,\n });\n\n notify();\n}\n\n/**\n * Complete drawing and create a shape\n */\nfunction completeDrawingInternal(\n mapId: UniqueId,\n state: DrawingState,\n feature: Feature,\n notify: () => void,\n setState: (updates: Partial<DrawingState>) => void,\n): Shape {\n if (!state.activeShapeType) {\n throw new Error('Cannot complete drawing - not currently drawing');\n }\n\n const shapeType = state.activeShapeType;\n const styleDefaults = state.styleDefaults;\n\n // Convert feature to Shape\n const shape = convertFeatureToShape(feature, shapeType, styleDefaults);\n\n // Reset state with new object reference\n setState({\n activeShapeType: null,\n tentativeFeature: null,\n styleDefaults: null,\n circleDefaults: null,\n });\n\n // Release mode and cursor using shared utilities\n releaseModeAndCursor(mapId, DRAW_SHAPE_LAYER_ID);\n\n // Emit shape drawn event\n drawShapeBus.emit(DrawShapeEvents.drawn, {\n shape,\n mapId,\n } as unknown as ShapeDrawnEvent['payload']);\n\n notify();\n\n return shape;\n}\n\n/**\n * Cancel the current drawing operation\n */\nfunction cancelDrawingInternal(\n mapId: UniqueId,\n state: DrawingState,\n notify: () => void,\n setState: (updates: Partial<DrawingState>) => void,\n): void {\n if (!state.activeShapeType) {\n return; // Nothing to cancel\n }\n\n const shapeType = state.activeShapeType;\n\n // Reset state with new object reference\n setState({\n activeShapeType: null,\n tentativeFeature: null,\n styleDefaults: null,\n circleDefaults: null,\n });\n\n // Release mode and cursor using shared utilities\n releaseModeAndCursor(mapId, DRAW_SHAPE_LAYER_ID);\n\n // Emit canceled event\n drawShapeBus.emit(DrawShapeEvents.canceled, {\n shapeType,\n mapId,\n });\n\n notify();\n}\n\n/**\n * Draw shape store\n */\nexport const drawStore = createMapStore<DrawingState, DrawShapeActions>({\n defaultState: { ...DEFAULT_DRAWING_STATE },\n\n actions: (mapId, { get, set, notify }) => ({\n draw: (shapeType: ShapeFeatureType, options?: DrawShapeOptions) => {\n startDrawing(mapId, get(), shapeType, options, notify, set);\n },\n\n cancel: () => {\n cancelDrawingInternal(mapId, get(), notify, set);\n },\n }),\n\n bus: (mapId, { get }) => {\n // Listen for mode authorization requests - REJECT when drawing (protected mode)\n const unsubAuth = mapModeBus.on(\n MapModeEvents.changeAuthorization,\n (event) => {\n const { authId, id } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== mapId) {\n return;\n }\n\n // If we're actively drawing, reject the mode change request\n if (get().activeShapeType) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId,\n approved: false,\n owner: DRAW_SHAPE_LAYER_ID,\n reason: 'Drawing in progress - cancel drawing first',\n id: mapId,\n });\n }\n },\n );\n\n return () => {\n unsubAuth();\n };\n },\n\n onCleanup: (mapId, state) => {\n // Cancel any active drawing before cleanup\n if (state.activeShapeType) {\n // Release mode and cursor using shared utilities\n releaseModeAndCursor(mapId, DRAW_SHAPE_LAYER_ID);\n\n // Emit canceled event\n drawShapeBus.emit(DrawShapeEvents.canceled, {\n shapeType: state.activeShapeType,\n mapId,\n });\n }\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Get the current drawing state for a mapId\n * Returns null if no store instance exists\n */\nexport function getDrawingState(mapId: UniqueId): DrawingState | null {\n if (!drawStore.exists(mapId)) {\n return null;\n }\n return drawStore.get(mapId);\n}\n\n/**\n * Hook for drawing state\n */\nexport function useDrawingState(\n mapId: UniqueId,\n): { state: DrawingState } & DrawShapeActions {\n return drawStore.use(mapId);\n}\n\n/**\n * Manually clear drawing state for a specific mapId.\n */\nexport function clearDrawingState(mapId: UniqueId): void {\n drawStore.clear(mapId);\n}\n\n/**\n * Complete drawing with a feature (called by the layer component)\n */\nexport function completeDrawingFromLayer(\n mapId: UniqueId,\n feature: Feature,\n): Shape {\n const state = drawStore.get(mapId);\n return completeDrawingInternal(\n mapId,\n state,\n feature,\n () => {\n /* notify handled by set */\n },\n (updates) => drawStore.set(mapId, updates),\n );\n}\n\n/**\n * Cancel drawing (called by the layer component)\n */\nexport function cancelDrawingFromLayer(mapId: UniqueId): void {\n drawStore.actions(mapId).cancel();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,MAAM,eAAe,UAAU,aAA6B;AAC5D,MAAM,aAAa,UAAU,aAA+B;;;;AAK5D,MAAMA,wBAAsC;CAC1C,iBAAiB;CACjB,kBAAkB;CAClB,eAAe;CACf,gBAAgB;CACjB;;;;AAeD,SAAS,aACP,OACA,OACA,WACA,SACA,QACA,UACM;AAEN,KAAI,MAAM,gBACR,uBAAsB,OAAO,OAAO,QAAQ,SAAS;AAIvD,UAAS;EACP,iBAAiB;EACjB,kBAAkB;EAClB,eAAe,SAAS,iBAAiB;EACzC,gBAAgB,SAAS,kBAAkB;EAC5C,CAAC;CAGF,MAAM,SAAS,gBAAgB;AAC/B,sBAAqB,OAAO,iBAAiB,QAAQ,oBAAoB;AAGzE,cAAa,KAAK,gBAAgB,SAAS;EACzC;EACA;EACD,CAAC;AAEF,SAAQ;;;;;AAMV,SAAS,wBACP,OACA,OACA,SACA,QACA,UACO;AACP,KAAI,CAAC,MAAM,gBACT,OAAM,IAAI,MAAM,kDAAkD;CAGpE,MAAM,YAAY,MAAM;CACxB,MAAM,gBAAgB,MAAM;CAG5B,MAAM,QAAQ,sBAAsB,SAAS,WAAW,cAAc;AAGtE,UAAS;EACP,iBAAiB;EACjB,kBAAkB;EAClB,eAAe;EACf,gBAAgB;EACjB,CAAC;AAGF,sBAAqB,OAAO,oBAAoB;AAGhD,cAAa,KAAK,gBAAgB,OAAO;EACvC;EACA;EACD,CAA0C;AAE3C,SAAQ;AAER,QAAO;;;;;AAMT,SAAS,sBACP,OACA,OACA,QACA,UACM;AACN,KAAI,CAAC,MAAM,gBACT;CAGF,MAAM,YAAY,MAAM;AAGxB,UAAS;EACP,iBAAiB;EACjB,kBAAkB;EAClB,eAAe;EACf,gBAAgB;EACjB,CAAC;AAGF,sBAAqB,OAAO,oBAAoB;AAGhD,cAAa,KAAK,gBAAgB,UAAU;EAC1C;EACA;EACD,CAAC;AAEF,SAAQ;;;;;AAMV,MAAa,YAAY,eAA+C;CACtE,cAAc,EAAE,GAAG,uBAAuB;CAE1C,UAAU,OAAO,EAAE,KAAK,KAAK,cAAc;EACzC,OAAO,WAA6B,YAA+B;AACjE,gBAAa,OAAO,KAAK,EAAE,WAAW,SAAS,QAAQ,IAAI;;EAG7D,cAAc;AACZ,yBAAsB,OAAO,KAAK,EAAE,QAAQ,IAAI;;EAEnD;CAED,MAAM,OAAO,EAAE,UAAU;EAEvB,MAAM,YAAY,WAAW,GAC3B,cAAc,sBACb,UAAU;GACT,MAAM,EAAE,QAAQ,OAAO,MAAM;AAG7B,OAAI,OAAO,MACT;AAIF,OAAI,KAAK,CAAC,gBACR,YAAW,KAAK,cAAc,gBAAgB;IAC5C;IACA,UAAU;IACV,OAAO;IACP,QAAQ;IACR,IAAI;IACL,CAAC;IAGP;AAED,eAAa;AACX,cAAW;;;CAIf,YAAY,OAAO,UAAU;AAE3B,MAAI,MAAM,iBAAiB;AAEzB,wBAAqB,OAAO,oBAAoB;AAGhD,gBAAa,KAAK,gBAAgB,UAAU;IAC1C,WAAW,MAAM;IACjB;IACD,CAAC;;;CAGP,CAAC;;;;AAoCF,SAAgB,yBACd,OACA,SACO;AAEP,QAAO,wBACL,OAFY,UAAU,IAAI,MAAM,EAIhC,eACM,KAGL,YAAY,UAAU,IAAI,OAAO,QAAQ,CAC3C;;;;;AAMH,SAAgB,uBAAuB,OAAuB;AAC5D,WAAU,QAAQ,MAAM,CAAC,QAAQ"}
1
+ {"version":3,"file":"store.js","names":["DEFAULT_DRAWING_STATE: DrawingState"],"sources":["../../../../src/deckgl/shapes/draw-shape-layer/store.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\n'use client';\n\n/**\n * Draw Shape Store\n *\n * Manages drawing state for shape creation.\n *\n * @example\n * ```tsx\n * import { drawStore } from '@accelint/map-toolkit/deckgl/shapes';\n *\n * function DrawControls({ mapId }) {\n * const { state, draw, cancel } = drawStore.use(mapId);\n *\n * return (\n * <div>\n * <p>Drawing: {state.activeShapeType ?? 'none'}</p>\n * <button onClick={() => draw('Polygon')}>Draw Polygon</button>\n * <button onClick={cancel}>Cancel</button>\n * </div>\n * );\n * }\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { MapModeEvents } from '@/map-mode/events';\nimport { createMapStore } from '@/shared/create-map-store';\nimport {\n releaseModeAndCursor,\n requestModeAndCursor,\n} from '../shared/utils/mode-utils';\nimport {\n DRAW_CURSOR_MAP,\n DRAW_SHAPE_LAYER_ID,\n DRAW_SHAPE_MODE,\n} from './constants';\nimport { DrawShapeEvents } from './events';\nimport { convertFeatureToShape } from './utils/feature-conversion';\nimport type { UniqueId } from '@accelint/core';\nimport type { Feature } from 'geojson';\nimport type { MapModeEventType } from '@/map-mode/types';\nimport type { Shape, ShapeFeatureType } from '../shared/types';\nimport type { DrawShapeEvent, ShapeDrawnEvent } from './events';\nimport type { DrawFunction, DrawingState, DrawShapeOptions } from './types';\n\n/**\n * Typed event bus instances\n */\nconst drawShapeBus = Broadcast.getInstance<DrawShapeEvent>();\nconst mapModeBus = Broadcast.getInstance<MapModeEventType>();\n\n/**\n * Default drawing state\n */\nconst DEFAULT_DRAWING_STATE: DrawingState = {\n activeShapeType: null,\n tentativeFeature: null,\n styleDefaults: null,\n circleDefaults: null,\n};\n\n/**\n * Actions for draw shape store\n */\ntype DrawShapeActions = {\n /** Start drawing a shape of the specified type */\n draw: DrawFunction;\n /** Cancel the current drawing operation */\n cancel: () => void;\n};\n\n/**\n * Start drawing a shape\n */\nfunction startDrawing(\n mapId: UniqueId,\n state: DrawingState,\n shapeType: ShapeFeatureType,\n options: DrawShapeOptions | undefined,\n notify: () => void,\n setState: (updates: Partial<DrawingState>) => void,\n): void {\n // Already drawing - cancel first\n if (state.activeShapeType) {\n cancelDrawingInternal(mapId, state, notify, setState);\n }\n\n // Update state with new object reference\n setState({\n activeShapeType: shapeType,\n tentativeFeature: null,\n styleDefaults: options?.styleDefaults ?? null,\n circleDefaults: options?.circleDefaults ?? null,\n });\n\n // Request map mode and cursor using shared utilities\n const cursor = DRAW_CURSOR_MAP[shapeType];\n requestModeAndCursor(mapId, DRAW_SHAPE_MODE, cursor, DRAW_SHAPE_LAYER_ID);\n\n // Emit drawing started event\n drawShapeBus.emit(DrawShapeEvents.drawing, {\n shapeType,\n mapId,\n });\n\n notify();\n}\n\n/**\n * Complete drawing and create a shape\n */\nfunction completeDrawingInternal(\n mapId: UniqueId,\n state: DrawingState,\n feature: Feature,\n notify: () => void,\n setState: (updates: Partial<DrawingState>) => void,\n): Shape {\n if (!state.activeShapeType) {\n throw new Error('Cannot complete drawing - not currently drawing');\n }\n\n const shapeType = state.activeShapeType;\n const styleDefaults = state.styleDefaults;\n\n // Convert feature to Shape\n const shape = convertFeatureToShape(feature, shapeType, styleDefaults);\n\n // Reset state with new object reference\n setState({\n activeShapeType: null,\n tentativeFeature: null,\n styleDefaults: null,\n circleDefaults: null,\n });\n\n // Release mode and cursor using shared utilities\n releaseModeAndCursor(mapId, DRAW_SHAPE_LAYER_ID);\n\n // Emit shape drawn event\n drawShapeBus.emit(DrawShapeEvents.drawn, {\n shape,\n mapId,\n } as unknown as ShapeDrawnEvent['payload']);\n\n notify();\n\n return shape;\n}\n\n/**\n * Cancel the current drawing operation\n */\nfunction cancelDrawingInternal(\n mapId: UniqueId,\n state: DrawingState,\n notify: () => void,\n setState: (updates: Partial<DrawingState>) => void,\n): void {\n if (!state.activeShapeType) {\n return; // Nothing to cancel\n }\n\n const shapeType = state.activeShapeType;\n\n // Reset state with new object reference\n setState({\n activeShapeType: null,\n tentativeFeature: null,\n styleDefaults: null,\n circleDefaults: null,\n });\n\n // Release mode and cursor using shared utilities\n releaseModeAndCursor(mapId, DRAW_SHAPE_LAYER_ID);\n\n // Emit canceled event\n drawShapeBus.emit(DrawShapeEvents.canceled, {\n shapeType,\n mapId,\n });\n\n notify();\n}\n\n/**\n * Draw shape store\n */\nexport const drawStore = createMapStore<DrawingState, DrawShapeActions>({\n defaultState: { ...DEFAULT_DRAWING_STATE },\n\n actions: (mapId, { get, set, notify }) => ({\n draw: (shapeType: ShapeFeatureType, options?: DrawShapeOptions) => {\n startDrawing(mapId, get(), shapeType, options, notify, set);\n },\n\n cancel: () => {\n cancelDrawingInternal(mapId, get(), notify, set);\n },\n }),\n\n bus: (mapId, { get }) => {\n // Listen for mode authorization requests - REJECT when drawing (protected mode)\n const unsubAuth = mapModeBus.on(\n MapModeEvents.changeAuthorization,\n (event) => {\n const { authId, id } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== mapId) {\n return;\n }\n\n // If we're actively drawing, reject the mode change request\n if (get().activeShapeType) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId,\n approved: false,\n owner: DRAW_SHAPE_LAYER_ID,\n reason: 'Drawing in progress - cancel drawing first',\n id: mapId,\n });\n }\n },\n );\n\n return () => {\n unsubAuth();\n };\n },\n\n onCleanup: (mapId, state) => {\n // Cancel any active drawing before cleanup\n if (state.activeShapeType) {\n // Release mode and cursor using shared utilities\n releaseModeAndCursor(mapId, DRAW_SHAPE_LAYER_ID);\n\n // Emit canceled event\n drawShapeBus.emit(DrawShapeEvents.canceled, {\n shapeType: state.activeShapeType,\n mapId,\n });\n }\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Get the current drawing state for a mapId\n * Returns null if no store instance exists\n */\nexport function getDrawingState(mapId: UniqueId): DrawingState | null {\n if (!drawStore.exists(mapId)) {\n return null;\n }\n return drawStore.get(mapId);\n}\n\n/**\n * Hook for drawing state\n */\nexport function useDrawingState(\n mapId: UniqueId,\n): { state: DrawingState } & DrawShapeActions {\n return drawStore.use(mapId);\n}\n\n/**\n * Manually clear drawing state for a specific mapId.\n */\nexport function clearDrawingState(mapId: UniqueId): void {\n drawStore.clear(mapId);\n}\n\n/**\n * Complete drawing with a feature (called by the layer component)\n */\nexport function completeDrawingFromLayer(\n mapId: UniqueId,\n feature: Feature,\n): Shape {\n const state = drawStore.get(mapId);\n return completeDrawingInternal(\n mapId,\n state,\n feature,\n () => {\n /* notify handled by set */\n },\n (updates) => drawStore.set(mapId, updates),\n );\n}\n\n/**\n * Cancel drawing (called by the layer component)\n */\nexport function cancelDrawingFromLayer(mapId: UniqueId): void {\n drawStore.actions(mapId).cancel();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,MAAM,eAAe,UAAU,aAA6B;AAC5D,MAAM,aAAa,UAAU,aAA+B;;;;AAK5D,MAAMA,wBAAsC;CAC1C,iBAAiB;CACjB,kBAAkB;CAClB,eAAe;CACf,gBAAgB;CACjB;;;;AAeD,SAAS,aACP,OACA,OACA,WACA,SACA,QACA,UACM;AAEN,KAAI,MAAM,gBACR,uBAAsB,OAAO,OAAO,QAAQ,SAAS;AAIvD,UAAS;EACP,iBAAiB;EACjB,kBAAkB;EAClB,eAAe,SAAS,iBAAiB;EACzC,gBAAgB,SAAS,kBAAkB;EAC5C,CAAC;CAGF,MAAM,SAAS,gBAAgB;AAC/B,sBAAqB,OAAO,iBAAiB,QAAQ,oBAAoB;AAGzE,cAAa,KAAK,gBAAgB,SAAS;EACzC;EACA;EACD,CAAC;AAEF,SAAQ;;;;;AAMV,SAAS,wBACP,OACA,OACA,SACA,QACA,UACO;AACP,KAAI,CAAC,MAAM,gBACT,OAAM,IAAI,MAAM,kDAAkD;CAGpE,MAAM,YAAY,MAAM;CACxB,MAAM,gBAAgB,MAAM;CAG5B,MAAM,QAAQ,sBAAsB,SAAS,WAAW,cAAc;AAGtE,UAAS;EACP,iBAAiB;EACjB,kBAAkB;EAClB,eAAe;EACf,gBAAgB;EACjB,CAAC;AAGF,sBAAqB,OAAO,oBAAoB;AAGhD,cAAa,KAAK,gBAAgB,OAAO;EACvC;EACA;EACD,CAA0C;AAE3C,SAAQ;AAER,QAAO;;;;;AAMT,SAAS,sBACP,OACA,OACA,QACA,UACM;AACN,KAAI,CAAC,MAAM,gBACT;CAGF,MAAM,YAAY,MAAM;AAGxB,UAAS;EACP,iBAAiB;EACjB,kBAAkB;EAClB,eAAe;EACf,gBAAgB;EACjB,CAAC;AAGF,sBAAqB,OAAO,oBAAoB;AAGhD,cAAa,KAAK,gBAAgB,UAAU;EAC1C;EACA;EACD,CAAC;AAEF,SAAQ;;;;;AAMV,MAAa,YAAY,eAA+C;CACtE,cAAc,EAAE,GAAG,uBAAuB;CAE1C,UAAU,OAAO,EAAE,KAAK,KAAK,cAAc;EACzC,OAAO,WAA6B,YAA+B;AACjE,gBAAa,OAAO,KAAK,EAAE,WAAW,SAAS,QAAQ,IAAI;;EAG7D,cAAc;AACZ,yBAAsB,OAAO,KAAK,EAAE,QAAQ,IAAI;;EAEnD;CAED,MAAM,OAAO,EAAE,UAAU;EAEvB,MAAM,YAAY,WAAW,GAC3B,cAAc,sBACb,UAAU;GACT,MAAM,EAAE,QAAQ,OAAO,MAAM;AAG7B,OAAI,OAAO,MACT;AAIF,OAAI,KAAK,CAAC,gBACR,YAAW,KAAK,cAAc,gBAAgB;IAC5C;IACA,UAAU;IACV,OAAO;IACP,QAAQ;IACR,IAAI;IACL,CAAC;IAGP;AAED,eAAa;AACX,cAAW;;;CAIf,YAAY,OAAO,UAAU;AAE3B,MAAI,MAAM,iBAAiB;AAEzB,wBAAqB,OAAO,oBAAoB;AAGhD,gBAAa,KAAK,gBAAgB,UAAU;IAC1C,WAAW,MAAM;IACjB;IACD,CAAC;;;CAGP,CAAC;;;;AAoCF,SAAgB,yBACd,OACA,SACO;AAEP,QAAO,wBACL,OAFY,UAAU,IAAI,MAAM,EAIhC,eACM,KAGL,YAAY,UAAU,IAAI,OAAO,QAAQ,CAC3C;;;;;AAMH,SAAgB,uBAAuB,OAAuB;AAC5D,WAAU,QAAQ,MAAM,CAAC,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-draw-shape.js","names":[],"sources":["../../../../src/deckgl/shapes/draw-shape-layer/use-draw-shape.ts"],"sourcesContent":["/*\n * Copyright 2025 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 'client-only';\nimport { useBus } from '@accelint/bus/react';\nimport { useContext, useMemo } from 'react';\nimport { MapContext } from '../../base-map/provider';\nimport { DrawShapeEvents } from './events';\nimport { drawStore } from './store';\nimport type { UniqueId } from '@accelint/core';\nimport type { DrawShapeEvent } from './events';\nimport type { UseDrawShapeOptions, UseDrawShapeReturn } from './types';\n\n/**\n * Hook to access the shape drawing state and actions.\n *\n * This hook uses `useSyncExternalStore` to subscribe to drawing state changes,\n * providing concurrent-safe state updates. Uses a fan-out pattern where\n * a single bus listener per map instance notifies N React component subscribers.\n *\n * @param mapId - Optional map instance ID. If not provided, will use the ID from `MapContext`.\n * @param options - Optional callbacks for onCreate and onCancel events\n * @returns Drawing state, draw function, cancel function, and convenience flags\n * @throws Error if no `mapId` is provided and hook is used outside of `MapProvider`\n *\n * @example\n * ```tsx\n * // Inside MapProvider (within BaseMap children) - uses context\n * function DrawingToolbar() {\n * const { draw, cancel, isDrawing, activeShapeType } = useDrawShape(undefined, {\n * onCreate: (shape) => {\n * console.log('Shape created:', shape);\n * setShapes(prev => [...prev, shape]);\n * },\n * onCancel: (shapeType) => {\n * console.log('Drawing canceled:', shapeType);\n * },\n * });\n *\n * return (\n * <div>\n * <button onClick={() => draw(ShapeFeatureType.Polygon)}>\n * Draw Polygon\n * </button>\n * <button onClick={() => draw(ShapeFeatureType.Circle)}>\n * Draw Circle\n * </button>\n * {isDrawing && (\n * <button onClick={cancel}>\n * Cancel ({activeShapeType})\n * </button>\n * )}\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Outside MapProvider - pass mapId directly\n * function ExternalDrawingControl({ mapId }: { mapId: UniqueId }) {\n * const { draw, isDrawing } = useDrawShape(mapId);\n *\n * return (\n * <button\n * onClick={() => draw(ShapeFeatureType.Point)}\n * disabled={isDrawing}\n * >\n * Add Point\n * </button>\n * );\n * }\n * ```\n */\nexport function useDrawShape(\n mapId?: UniqueId,\n options?: UseDrawShapeOptions,\n): UseDrawShapeReturn {\n const contextId = useContext(MapContext);\n const actualId = mapId ?? contextId;\n\n if (!actualId) {\n throw new Error(\n 'useDrawShape requires either a mapId parameter or to be used within a MapProvider',\n );\n }\n\n const { onCreate, onCancel } = options ?? {};\n\n // Use the v2 store API directly\n const { state: drawingState, draw, cancel } = drawStore.use(actualId);\n\n // Listen for completion/cancellation events to trigger callbacks\n // useOn handles cleanup automatically and uses useEffectEvent for stable callbacks\n const { useOn } = useBus<DrawShapeEvent>();\n\n useOn(DrawShapeEvents.drawn, (event) => {\n if (event.payload.mapId === actualId && onCreate) {\n onCreate(event.payload.shape);\n }\n });\n\n useOn(DrawShapeEvents.canceled, (event) => {\n if (event.payload.mapId === actualId && onCancel) {\n onCancel(event.payload.shapeType);\n }\n });\n\n // Memoize the return value to prevent unnecessary re-renders\n return useMemo(\n () => ({\n drawingState,\n draw,\n cancel,\n isDrawing: !!drawingState?.activeShapeType,\n activeShapeType: drawingState?.activeShapeType ?? null,\n }),\n [drawingState, draw, cancel],\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqFA,SAAgB,aACd,OACA,SACoB;CACpB,MAAM,YAAY,WAAW,WAAW;CACxC,MAAM,WAAW,SAAS;AAE1B,KAAI,CAAC,SACH,OAAM,IAAI,MACR,oFACD;CAGH,MAAM,EAAE,UAAU,aAAa,WAAW,EAAE;CAG5C,MAAM,EAAE,OAAO,cAAc,MAAM,WAAW,UAAU,IAAI,SAAS;CAIrE,MAAM,EAAE,mBAAU,QAAwB;AAE1C,SAAM,gBAAgB,QAAQ,UAAU;AACtC,MAAI,MAAM,QAAQ,UAAU,YAAY,SACtC,UAAS,MAAM,QAAQ,MAAM;GAE/B;AAEF,SAAM,gBAAgB,WAAW,UAAU;AACzC,MAAI,MAAM,QAAQ,UAAU,YAAY,SACtC,UAAS,MAAM,QAAQ,UAAU;GAEnC;AAGF,QAAO,eACE;EACL;EACA;EACA;EACA,WAAW,CAAC,CAAC,cAAc;EAC3B,iBAAiB,cAAc,mBAAmB;EACnD,GACD;EAAC;EAAc;EAAM;EAAO,CAC7B"}
1
+ {"version":3,"file":"use-draw-shape.js","names":[],"sources":["../../../../src/deckgl/shapes/draw-shape-layer/use-draw-shape.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\n'use client';\n\nimport 'client-only';\nimport { useBus } from '@accelint/bus/react';\nimport { useContext, useMemo } from 'react';\nimport { MapContext } from '../../base-map/provider';\nimport { DrawShapeEvents } from './events';\nimport { drawStore } from './store';\nimport type { UniqueId } from '@accelint/core';\nimport type { DrawShapeEvent } from './events';\nimport type { UseDrawShapeOptions, UseDrawShapeReturn } from './types';\n\n/**\n * Hook to access the shape drawing state and actions.\n *\n * This hook uses `useSyncExternalStore` to subscribe to drawing state changes,\n * providing concurrent-safe state updates. Uses a fan-out pattern where\n * a single bus listener per map instance notifies N React component subscribers.\n *\n * @param mapId - Optional map instance ID. If not provided, will use the ID from `MapContext`.\n * @param options - Optional callbacks for onCreate and onCancel events\n * @returns Drawing state, draw function, cancel function, and convenience flags\n * @throws Error if no `mapId` is provided and hook is used outside of `MapProvider`\n *\n * @example\n * ```tsx\n * // Inside MapProvider (within BaseMap children) - uses context\n * function DrawingToolbar() {\n * const { draw, cancel, isDrawing, activeShapeType } = useDrawShape(undefined, {\n * onCreate: (shape) => {\n * console.log('Shape created:', shape);\n * setShapes(prev => [...prev, shape]);\n * },\n * onCancel: (shapeType) => {\n * console.log('Drawing canceled:', shapeType);\n * },\n * });\n *\n * return (\n * <div>\n * <button onClick={() => draw(ShapeFeatureType.Polygon)}>\n * Draw Polygon\n * </button>\n * <button onClick={() => draw(ShapeFeatureType.Circle)}>\n * Draw Circle\n * </button>\n * {isDrawing && (\n * <button onClick={cancel}>\n * Cancel ({activeShapeType})\n * </button>\n * )}\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Outside MapProvider - pass mapId directly\n * function ExternalDrawingControl({ mapId }: { mapId: UniqueId }) {\n * const { draw, isDrawing } = useDrawShape(mapId);\n *\n * return (\n * <button\n * onClick={() => draw(ShapeFeatureType.Point)}\n * disabled={isDrawing}\n * >\n * Add Point\n * </button>\n * );\n * }\n * ```\n */\nexport function useDrawShape(\n mapId?: UniqueId,\n options?: UseDrawShapeOptions,\n): UseDrawShapeReturn {\n const contextId = useContext(MapContext);\n const actualId = mapId ?? contextId;\n\n if (!actualId) {\n throw new Error(\n 'useDrawShape requires either a mapId parameter or to be used within a MapProvider',\n );\n }\n\n const { onCreate, onCancel } = options ?? {};\n\n // Use the v2 store API directly\n const { state: drawingState, draw, cancel } = drawStore.use(actualId);\n\n // Listen for completion/cancellation events to trigger callbacks\n // useOn handles cleanup automatically and uses useEffectEvent for stable callbacks\n const { useOn } = useBus<DrawShapeEvent>();\n\n useOn(DrawShapeEvents.drawn, (event) => {\n if (event.payload.mapId === actualId && onCreate) {\n onCreate(event.payload.shape);\n }\n });\n\n useOn(DrawShapeEvents.canceled, (event) => {\n if (event.payload.mapId === actualId && onCancel) {\n onCancel(event.payload.shapeType);\n }\n });\n\n // Memoize the return value to prevent unnecessary re-renders\n return useMemo(\n () => ({\n drawingState,\n draw,\n cancel,\n isDrawing: !!drawingState?.activeShapeType,\n activeShapeType: drawingState?.activeShapeType ?? null,\n }),\n [drawingState, draw, cancel],\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqFA,SAAgB,aACd,OACA,SACoB;CACpB,MAAM,YAAY,WAAW,WAAW;CACxC,MAAM,WAAW,SAAS;AAE1B,KAAI,CAAC,SACH,OAAM,IAAI,MACR,oFACD;CAGH,MAAM,EAAE,UAAU,aAAa,WAAW,EAAE;CAG5C,MAAM,EAAE,OAAO,cAAc,MAAM,WAAW,UAAU,IAAI,SAAS;CAIrE,MAAM,EAAE,mBAAU,QAAwB;AAE1C,SAAM,gBAAgB,QAAQ,UAAU;AACtC,MAAI,MAAM,QAAQ,UAAU,YAAY,SACtC,UAAS,MAAM,QAAQ,MAAM;GAE/B;AAEF,SAAM,gBAAgB,WAAW,UAAU;AACzC,MAAI,MAAM,QAAQ,UAAU,YAAY,SACtC,UAAS,MAAM,QAAQ,UAAU;GAEnC;AAGF,QAAO,eACE;EACL;EACA;EACA;EACA,WAAW,CAAC,CAAC,cAAc;EAC3B,iBAAiB,cAAc,mBAAmB;EACnD,GACD;EAAC;EAAc;EAAM;EAAO,CAC7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"feature-conversion.js","names":["styleProperties: StyleProperties","circleProperties: CircleProperties | undefined","ShapeFeatureTypeEnum","ellipseProperties: EllipseProperties | undefined"],"sources":["../../../../../src/deckgl/shapes/draw-shape-layer/utils/feature-conversion.ts"],"sourcesContent":["/*\n * Copyright 2025 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 { uuid } from '@accelint/core';\nimport { getLogger } from '@accelint/logger';\nimport { centroid, distance } from '@turf/turf';\nimport { DEFAULT_DISTANCE_UNITS } from '@/shared/units';\nimport { DEFAULT_STYLE_PROPERTIES } from '../../shared/constants';\nimport {\n type CircleProperties,\n type EllipseProperties,\n type Shape,\n type ShapeFeature,\n type ShapeFeatureType,\n ShapeFeatureType as ShapeFeatureTypeEnum,\n type StyleProperties,\n} from '../../shared/types';\nimport type { Feature, Polygon, Position } from 'geojson';\n\nconst logger = getLogger({\n enabled: process.env.NODE_ENV !== 'production',\n level: 'warn',\n prefix: '[FeatureConversion]',\n pretty: true,\n});\n\n/**\n * Generate a default name for a shape based on its type\n */\nfunction generateShapeName(shape: ShapeFeatureType): string {\n const timestamp = new Date().toLocaleTimeString();\n return `New ${shape} (${timestamp})`;\n}\n\n/**\n * Compute circle properties from a polygon geometry (circle approximation)\n *\n * The EditableGeoJsonLayer creates circles as polygon approximations.\n * This function extracts the center and radius from that polygon.\n */\nfunction computeCircleProperties(\n geometry: Polygon,\n): CircleProperties | undefined {\n const coordinates = geometry.coordinates[0];\n if (!coordinates || coordinates.length < 3) {\n logger.warn(\n 'Cannot compute circle properties: polygon has insufficient coordinates',\n );\n return undefined;\n }\n\n // Calculate center using turf centroid\n const centerFeature = centroid({\n type: 'Polygon',\n coordinates: geometry.coordinates,\n });\n const center = centerFeature.geometry.coordinates as [number, number];\n\n // Validate center coordinates are valid numbers\n const isCenterValid =\n Number.isFinite(center[0]) && Number.isFinite(center[1]);\n if (!isCenterValid) {\n logger.warn('Cannot compute circle properties: invalid center coordinates');\n return undefined;\n }\n\n // Calculate radius as distance from center to first point\n const firstPoint = coordinates[0] as Position;\n\n // Validate first point coordinates\n const isFirstPointValid =\n firstPoint &&\n Number.isFinite(firstPoint[0]) &&\n Number.isFinite(firstPoint[1]);\n if (!isFirstPointValid) {\n logger.warn(\n 'Cannot compute circle properties: invalid edge point coordinates',\n );\n return undefined;\n }\n\n const radius = distance(center, firstPoint, {\n units: DEFAULT_DISTANCE_UNITS,\n });\n\n // Validate computed radius\n if (!Number.isFinite(radius) || radius <= 0) {\n logger.warn('Cannot compute circle properties: invalid radius computed');\n return undefined;\n }\n\n return {\n center,\n radius: {\n value: radius,\n units: DEFAULT_DISTANCE_UNITS,\n },\n };\n}\n\n/**\n * Edit properties attached by DrawEllipseUsingThreePointsMode\n */\ninterface EllipseEditProperties {\n shape: 'Ellipse';\n xSemiAxis: { value: number; unit: string };\n ySemiAxis: { value: number; unit: string };\n angle: number;\n center: [number, number];\n}\n\n/**\n * Compute ellipse properties from a feature's editProperties\n *\n * The DrawEllipseUsingThreePointsMode attaches ellipse metadata to the feature's\n * properties.editProperties. This function extracts and normalizes that data.\n */\nfunction computeEllipseProperties(\n feature: Feature,\n): EllipseProperties | undefined {\n const editProps = (\n feature.properties as { editProperties?: EllipseEditProperties } | null\n )?.editProperties;\n\n if (!editProps || editProps.shape !== 'Ellipse') {\n logger.warn(\n 'Cannot compute ellipse properties: feature missing editProperties or not an ellipse',\n );\n return undefined;\n }\n\n return {\n center: editProps.center,\n xSemiAxis: {\n value: editProps.xSemiAxis.value,\n units: DEFAULT_DISTANCE_UNITS,\n },\n ySemiAxis: {\n value: editProps.ySemiAxis.value,\n units: DEFAULT_DISTANCE_UNITS,\n },\n angle: editProps.angle,\n };\n}\n\n/**\n * Convert a raw GeoJSON Feature from EditableGeoJsonLayer to a Shape\n *\n * The returned Shape includes:\n * - Auto-generated UUID\n * - Auto-generated name with timestamp (e.g., \"New Polygon (2:30:45 PM)\")\n * - Merged style properties (defaults + overrides)\n * - Circle/ellipse properties computed from geometry if applicable\n * - `lastUpdated` timestamp\n * - `locked: false` (newly created shapes are always unlocked)\n *\n * @param feature - The raw GeoJSON feature from the editable layer\n * @param shape - The type of shape being created\n * @param styleDefaults - Optional style overrides\n * @returns A complete Shape ready for use\n */\nexport function convertFeatureToShape(\n feature: Feature,\n shape: ShapeFeatureType,\n styleDefaults?: Partial<StyleProperties> | null,\n): Shape {\n const id = uuid();\n const name = generateShapeName(shape);\n\n // Merge default styles with any provided defaults\n const styleProperties: StyleProperties = {\n ...DEFAULT_STYLE_PROPERTIES,\n ...(styleDefaults ?? {}),\n };\n\n // Compute circle properties if this is a circle\n let circleProperties: CircleProperties | undefined;\n if (\n shape === ShapeFeatureTypeEnum.Circle &&\n feature.geometry.type === 'Polygon'\n ) {\n circleProperties = computeCircleProperties(feature.geometry);\n }\n\n // Compute ellipse properties if this is an ellipse\n let ellipseProperties: EllipseProperties | undefined;\n if (\n shape === ShapeFeatureTypeEnum.Ellipse &&\n feature.geometry.type === 'Polygon'\n ) {\n ellipseProperties = computeEllipseProperties(feature);\n }\n\n // Create the styled feature\n const styledFeature: ShapeFeature = {\n type: 'Feature',\n geometry: feature.geometry,\n properties: {\n styleProperties,\n circleProperties,\n ellipseProperties,\n shapeId: id,\n },\n };\n\n // Type assertion needed because TypeScript can't narrow the return type\n // based on the runtime shape value. The constructed object satisfies\n // the Shape union at runtime based on which shape was passed in.\n return {\n id,\n name,\n shape,\n feature: styledFeature,\n lastUpdated: Date.now(),\n locked: false,\n } as Shape;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,SAAS,UAAU;CACvB,SAAS,QAAQ,IAAI,aAAa;CAClC,OAAO;CACP,QAAQ;CACR,QAAQ;CACT,CAAC;;;;AAKF,SAAS,kBAAkB,OAAiC;AAE1D,QAAO,OAAO,MAAM,qBADF,IAAI,MAAM,EAAC,oBAAoB,CACf;;;;;;;;AASpC,SAAS,wBACP,UAC8B;CAC9B,MAAM,cAAc,SAAS,YAAY;AACzC,KAAI,CAAC,eAAe,YAAY,SAAS,GAAG;AAC1C,SAAO,KACL,yEACD;AACD;;CAQF,MAAM,SAJgB,SAAS;EAC7B,MAAM;EACN,aAAa,SAAS;EACvB,CAAC,CAC2B,SAAS;AAKtC,KAAI,EADF,OAAO,SAAS,OAAO,GAAG,IAAI,OAAO,SAAS,OAAO,GAAG,GACtC;AAClB,SAAO,KAAK,+DAA+D;AAC3E;;CAIF,MAAM,aAAa,YAAY;AAO/B,KAAI,EAHF,cACA,OAAO,SAAS,WAAW,GAAG,IAC9B,OAAO,SAAS,WAAW,GAAG,GACR;AACtB,SAAO,KACL,mEACD;AACD;;CAGF,MAAM,SAAS,SAAS,QAAQ,YAAY,EAC1C,OAAO,wBACR,CAAC;AAGF,KAAI,CAAC,OAAO,SAAS,OAAO,IAAI,UAAU,GAAG;AAC3C,SAAO,KAAK,4DAA4D;AACxE;;AAGF,QAAO;EACL;EACA,QAAQ;GACN,OAAO;GACP,OAAO;GACR;EACF;;;;;;;;AAoBH,SAAS,yBACP,SAC+B;CAC/B,MAAM,YACJ,QAAQ,YACP;AAEH,KAAI,CAAC,aAAa,UAAU,UAAU,WAAW;AAC/C,SAAO,KACL,sFACD;AACD;;AAGF,QAAO;EACL,QAAQ,UAAU;EAClB,WAAW;GACT,OAAO,UAAU,UAAU;GAC3B,OAAO;GACR;EACD,WAAW;GACT,OAAO,UAAU,UAAU;GAC3B,OAAO;GACR;EACD,OAAO,UAAU;EAClB;;;;;;;;;;;;;;;;;;AAmBH,SAAgB,sBACd,SACA,OACA,eACO;CACP,MAAM,KAAK,MAAM;CACjB,MAAM,OAAO,kBAAkB,MAAM;CAGrC,MAAMA,kBAAmC;EACvC,GAAG;EACH,GAAI,iBAAiB,EAAE;EACxB;CAGD,IAAIC;AACJ,KACE,UAAUC,iBAAqB,UAC/B,QAAQ,SAAS,SAAS,UAE1B,oBAAmB,wBAAwB,QAAQ,SAAS;CAI9D,IAAIC;AACJ,KACE,UAAUD,iBAAqB,WAC/B,QAAQ,SAAS,SAAS,UAE1B,qBAAoB,yBAAyB,QAAQ;AAkBvD,QAAO;EACL;EACA;EACA;EACA,SAlBkC;GAClC,MAAM;GACN,UAAU,QAAQ;GAClB,YAAY;IACV;IACA;IACA;IACA,SAAS;IACV;GACF;EAUC,aAAa,KAAK,KAAK;EACvB,QAAQ;EACT"}
1
+ {"version":3,"file":"feature-conversion.js","names":["styleProperties: StyleProperties","circleProperties: CircleProperties | undefined","ShapeFeatureTypeEnum","ellipseProperties: EllipseProperties | undefined"],"sources":["../../../../../src/deckgl/shapes/draw-shape-layer/utils/feature-conversion.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\n'use client';\n\nimport { uuid } from '@accelint/core';\nimport { getLogger } from '@accelint/logger';\nimport { centroid, distance } from '@turf/turf';\nimport { DEFAULT_DISTANCE_UNITS } from '@/shared/units';\nimport { DEFAULT_STYLE_PROPERTIES } from '../../shared/constants';\nimport {\n type CircleProperties,\n type EllipseProperties,\n type Shape,\n type ShapeFeature,\n type ShapeFeatureType,\n ShapeFeatureType as ShapeFeatureTypeEnum,\n type StyleProperties,\n} from '../../shared/types';\nimport type { Feature, Polygon, Position } from 'geojson';\n\nconst logger = getLogger({\n enabled: process.env.NODE_ENV !== 'production',\n level: 'warn',\n prefix: '[FeatureConversion]',\n pretty: true,\n});\n\n/**\n * Generate a default name for a shape based on its type\n */\nfunction generateShapeName(shape: ShapeFeatureType): string {\n const timestamp = new Date().toLocaleTimeString();\n return `New ${shape} (${timestamp})`;\n}\n\n/**\n * Compute circle properties from a polygon geometry (circle approximation)\n *\n * The EditableGeoJsonLayer creates circles as polygon approximations.\n * This function extracts the center and radius from that polygon.\n */\nfunction computeCircleProperties(\n geometry: Polygon,\n): CircleProperties | undefined {\n const coordinates = geometry.coordinates[0];\n if (!coordinates || coordinates.length < 3) {\n logger.warn(\n 'Cannot compute circle properties: polygon has insufficient coordinates',\n );\n return undefined;\n }\n\n // Calculate center using turf centroid\n const centerFeature = centroid({\n type: 'Polygon',\n coordinates: geometry.coordinates,\n });\n const center = centerFeature.geometry.coordinates as [number, number];\n\n // Validate center coordinates are valid numbers\n const isCenterValid =\n Number.isFinite(center[0]) && Number.isFinite(center[1]);\n if (!isCenterValid) {\n logger.warn('Cannot compute circle properties: invalid center coordinates');\n return undefined;\n }\n\n // Calculate radius as distance from center to first point\n const firstPoint = coordinates[0] as Position;\n\n // Validate first point coordinates\n const isFirstPointValid =\n firstPoint &&\n Number.isFinite(firstPoint[0]) &&\n Number.isFinite(firstPoint[1]);\n if (!isFirstPointValid) {\n logger.warn(\n 'Cannot compute circle properties: invalid edge point coordinates',\n );\n return undefined;\n }\n\n const radius = distance(center, firstPoint, {\n units: DEFAULT_DISTANCE_UNITS,\n });\n\n // Validate computed radius\n if (!Number.isFinite(radius) || radius <= 0) {\n logger.warn('Cannot compute circle properties: invalid radius computed');\n return undefined;\n }\n\n return {\n center,\n radius: {\n value: radius,\n units: DEFAULT_DISTANCE_UNITS,\n },\n };\n}\n\n/**\n * Edit properties attached by DrawEllipseUsingThreePointsMode\n */\ninterface EllipseEditProperties {\n shape: 'Ellipse';\n xSemiAxis: { value: number; unit: string };\n ySemiAxis: { value: number; unit: string };\n angle: number;\n center: [number, number];\n}\n\n/**\n * Compute ellipse properties from a feature's editProperties\n *\n * The DrawEllipseUsingThreePointsMode attaches ellipse metadata to the feature's\n * properties.editProperties. This function extracts and normalizes that data.\n */\nfunction computeEllipseProperties(\n feature: Feature,\n): EllipseProperties | undefined {\n const editProps = (\n feature.properties as { editProperties?: EllipseEditProperties } | null\n )?.editProperties;\n\n if (!editProps || editProps.shape !== 'Ellipse') {\n logger.warn(\n 'Cannot compute ellipse properties: feature missing editProperties or not an ellipse',\n );\n return undefined;\n }\n\n return {\n center: editProps.center,\n xSemiAxis: {\n value: editProps.xSemiAxis.value,\n units: DEFAULT_DISTANCE_UNITS,\n },\n ySemiAxis: {\n value: editProps.ySemiAxis.value,\n units: DEFAULT_DISTANCE_UNITS,\n },\n angle: editProps.angle,\n };\n}\n\n/**\n * Convert a raw GeoJSON Feature from EditableGeoJsonLayer to a Shape\n *\n * The returned Shape includes:\n * - Auto-generated UUID\n * - Auto-generated name with timestamp (e.g., \"New Polygon (2:30:45 PM)\")\n * - Merged style properties (defaults + overrides)\n * - Circle/ellipse properties computed from geometry if applicable\n * - `lastUpdated` timestamp\n * - `locked: false` (newly created shapes are always unlocked)\n *\n * @param feature - The raw GeoJSON feature from the editable layer\n * @param shape - The type of shape being created\n * @param styleDefaults - Optional style overrides\n * @returns A complete Shape ready for use\n */\nexport function convertFeatureToShape(\n feature: Feature,\n shape: ShapeFeatureType,\n styleDefaults?: Partial<StyleProperties> | null,\n): Shape {\n const id = uuid();\n const name = generateShapeName(shape);\n\n // Merge default styles with any provided defaults\n const styleProperties: StyleProperties = {\n ...DEFAULT_STYLE_PROPERTIES,\n ...(styleDefaults ?? {}),\n };\n\n // Compute circle properties if this is a circle\n let circleProperties: CircleProperties | undefined;\n if (\n shape === ShapeFeatureTypeEnum.Circle &&\n feature.geometry.type === 'Polygon'\n ) {\n circleProperties = computeCircleProperties(feature.geometry);\n }\n\n // Compute ellipse properties if this is an ellipse\n let ellipseProperties: EllipseProperties | undefined;\n if (\n shape === ShapeFeatureTypeEnum.Ellipse &&\n feature.geometry.type === 'Polygon'\n ) {\n ellipseProperties = computeEllipseProperties(feature);\n }\n\n // Create the styled feature\n const styledFeature: ShapeFeature = {\n type: 'Feature',\n geometry: feature.geometry,\n properties: {\n styleProperties,\n circleProperties,\n ellipseProperties,\n shapeId: id,\n },\n };\n\n // Type assertion needed because TypeScript can't narrow the return type\n // based on the runtime shape value. The constructed object satisfies\n // the Shape union at runtime based on which shape was passed in.\n return {\n id,\n name,\n shape,\n feature: styledFeature,\n lastUpdated: Date.now(),\n locked: false,\n } as Shape;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,SAAS,UAAU;CACvB,SAAS,QAAQ,IAAI,aAAa;CAClC,OAAO;CACP,QAAQ;CACR,QAAQ;CACT,CAAC;;;;AAKF,SAAS,kBAAkB,OAAiC;AAE1D,QAAO,OAAO,MAAM,qBADF,IAAI,MAAM,EAAC,oBAAoB,CACf;;;;;;;;AASpC,SAAS,wBACP,UAC8B;CAC9B,MAAM,cAAc,SAAS,YAAY;AACzC,KAAI,CAAC,eAAe,YAAY,SAAS,GAAG;AAC1C,SAAO,KACL,yEACD;AACD;;CAQF,MAAM,SAJgB,SAAS;EAC7B,MAAM;EACN,aAAa,SAAS;EACvB,CAAC,CAC2B,SAAS;AAKtC,KAAI,EADF,OAAO,SAAS,OAAO,GAAG,IAAI,OAAO,SAAS,OAAO,GAAG,GACtC;AAClB,SAAO,KAAK,+DAA+D;AAC3E;;CAIF,MAAM,aAAa,YAAY;AAO/B,KAAI,EAHF,cACA,OAAO,SAAS,WAAW,GAAG,IAC9B,OAAO,SAAS,WAAW,GAAG,GACR;AACtB,SAAO,KACL,mEACD;AACD;;CAGF,MAAM,SAAS,SAAS,QAAQ,YAAY,EAC1C,OAAO,wBACR,CAAC;AAGF,KAAI,CAAC,OAAO,SAAS,OAAO,IAAI,UAAU,GAAG;AAC3C,SAAO,KAAK,4DAA4D;AACxE;;AAGF,QAAO;EACL;EACA,QAAQ;GACN,OAAO;GACP,OAAO;GACR;EACF;;;;;;;;AAoBH,SAAS,yBACP,SAC+B;CAC/B,MAAM,YACJ,QAAQ,YACP;AAEH,KAAI,CAAC,aAAa,UAAU,UAAU,WAAW;AAC/C,SAAO,KACL,sFACD;AACD;;AAGF,QAAO;EACL,QAAQ,UAAU;EAClB,WAAW;GACT,OAAO,UAAU,UAAU;GAC3B,OAAO;GACR;EACD,WAAW;GACT,OAAO,UAAU,UAAU;GAC3B,OAAO;GACR;EACD,OAAO,UAAU;EAClB;;;;;;;;;;;;;;;;;;AAmBH,SAAgB,sBACd,SACA,OACA,eACO;CACP,MAAM,KAAK,MAAM;CACjB,MAAM,OAAO,kBAAkB,MAAM;CAGrC,MAAMA,kBAAmC;EACvC,GAAG;EACH,GAAI,iBAAiB,EAAE;EACxB;CAGD,IAAIC;AACJ,KACE,UAAUC,iBAAqB,UAC/B,QAAQ,SAAS,SAAS,UAE1B,oBAAmB,wBAAwB,QAAQ,SAAS;CAI9D,IAAIC;AACJ,KACE,UAAUD,iBAAqB,WAC/B,QAAQ,SAAS,SAAS,UAE1B,qBAAoB,yBAAyB,QAAQ;AAkBvD,QAAO;EACL;EACA;EACA;EACA,SAlBkC;GAClC,MAAM;GACN,UAAU,QAAQ;GAClB,YAAY;IACV;IACA;IACA;IACA,SAAS;IACV;GACF;EAUC,aAAa,KAAK,KAAK;EACvB,QAAQ;EACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","names":["EDIT_CURSOR_MAP: Record<EditMode, CSSCursorType>"],"sources":["../../../../src/deckgl/shapes/edit-shape-layer/constants.ts"],"sourcesContent":["/*\n * Copyright 2025 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 type { CSSCursorType } from '@/map-cursor/types';\nimport type { EditMode } from './types';\n\n// Re-export edit event type sets from shared constants\nexport {\n COMPLETION_EDIT_TYPES,\n CONTINUOUS_EDIT_TYPES,\n} from '../shared/constants';\n\n/**\n * Mode name for the map-mode integration\n */\nexport const EDIT_SHAPE_MODE = 'edit-shape';\n\n/**\n * Identifier for the edit shape layer.\n * Used as the owner for map-mode/cursor and as the default layer ID.\n */\nexport const EDIT_SHAPE_LAYER_ID = 'edit-shape-layer';\n\n/**\n * Cursor mapping for each edit mode\n */\nexport const EDIT_CURSOR_MAP: Record<EditMode, CSSCursorType> = {\n view: 'default',\n 'bounding-transform': 'crosshair',\n 'vertex-transform': 'crosshair',\n 'circle-transform': 'crosshair',\n translate: 'crosshair',\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA0BA,MAAa,kBAAkB;;;;;AAM/B,MAAa,sBAAsB;;;;AAKnC,MAAaA,kBAAmD;CAC9D,MAAM;CACN,sBAAsB;CACtB,oBAAoB;CACpB,oBAAoB;CACpB,WAAW;CACZ"}
1
+ {"version":3,"file":"constants.js","names":["EDIT_CURSOR_MAP: Record<EditMode, CSSCursorType>"],"sources":["../../../../src/deckgl/shapes/edit-shape-layer/constants.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\n'use client';\n\nimport type { CSSCursorType } from '@/map-cursor/types';\nimport type { EditMode } from './types';\n\n// Re-export edit event type sets from shared constants\nexport {\n COMPLETION_EDIT_TYPES,\n CONTINUOUS_EDIT_TYPES,\n} from '../shared/constants';\n\n/**\n * Mode name for the map-mode integration\n */\nexport const EDIT_SHAPE_MODE = 'edit-shape';\n\n/**\n * Identifier for the edit shape layer.\n * Used as the owner for map-mode/cursor and as the default layer ID.\n */\nexport const EDIT_SHAPE_LAYER_ID = 'edit-shape-layer';\n\n/**\n * Cursor mapping for each edit mode\n */\nexport const EDIT_CURSOR_MAP: Record<EditMode, CSSCursorType> = {\n view: 'default',\n 'bounding-transform': 'crosshair',\n 'vertex-transform': 'crosshair',\n 'circle-transform': 'crosshair',\n translate: 'crosshair',\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA0BA,MAAa,kBAAkB;;;;;AAM/B,MAAa,sBAAsB;;;;AAKnC,MAAaA,kBAAmD;CAC9D,MAAM;CACN,sBAAsB;CACtB,oBAAoB;CACpB,oBAAoB;CACpB,WAAW;CACZ"}
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","names":[],"sources":["../../../../src/deckgl/shapes/edit-shape-layer/events.ts"],"sourcesContent":["/*\n * Copyright 2025 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/**\n * Edit Shape Events\n *\n * Note on event payload structure:\n * These events define explicit payload types rather than using the `Payload<T, P>` helper\n * from @accelint/bus. This is because the `Shape` type contains GeoJSON `Feature` objects\n * from the `geojson` package, which don't satisfy TypeScript's `StructuredCloneable` type\n * constraint used by the bus.\n *\n * The issue: `StructuredCloneable` (from type-fest) requires objects to have an index\n * signature `[key: string]: StructuredCloneable`, but GeoJSON interfaces define strict\n * property types without index signatures. At runtime, GeoJSON data IS structurally\n * cloneable (can be passed through postMessage, stored in IndexedDB, etc.), but\n * TypeScript can't verify this statically.\n *\n * Events that only contain primitive values (like ShapeId, mapId) can use the `Payload`\n * helper directly - see shared/events.ts for examples.\n *\n * When emitting these events via the bus, use type assertions:\n * @example\n * ```ts\n * bus.emit('shapes:updated', {\n * type: 'shapes:updated',\n * payload: { shape, mapId },\n * source: componentId,\n * } as unknown as Payload);\n * ```\n */\n\n'use client';\n\nimport type { UniqueId } from '@accelint/core';\nimport type { Shape } from '../shared/types';\n\n/**\n * Edit shape lifecycle events\n */\nexport const EditShapeEvents = {\n /** Editing has started for a shape */\n editing: 'shapes:editing',\n /** Shape has been successfully updated */\n updated: 'shapes:updated',\n /** Editing was canceled */\n canceled: 'shapes:edit-canceled',\n} as const;\n\nexport type EditShapeEventType =\n (typeof EditShapeEvents)[keyof typeof EditShapeEvents];\n\n/**\n * Payload for shapes:editing event.\n */\nexport type ShapeEditingPayload = {\n /** The shape being edited */\n shape: Shape;\n /** Map instance ID for multi-map event isolation */\n mapId: UniqueId;\n};\n\n/**\n * Event payload for shapes:editing\n * Emitted when editing starts\n */\nexport type ShapeEditingEvent = {\n type: 'shapes:editing';\n payload: ShapeEditingPayload;\n source: UniqueId;\n target?: UniqueId;\n};\n\n/**\n * Payload for shapes:updated event.\n */\nexport type ShapeUpdatedPayload = {\n /** The updated shape with new geometry */\n shape: Shape;\n /** Map instance ID for multi-map event isolation */\n mapId: UniqueId;\n};\n\n/**\n * Event payload for shapes:updated\n * Emitted when shape edits are saved\n */\nexport type ShapeUpdatedEvent = {\n type: 'shapes:updated';\n payload: ShapeUpdatedPayload;\n source: UniqueId;\n target?: UniqueId;\n};\n\n/**\n * Payload for shapes:edit-canceled event.\n */\nexport type ShapeEditCanceledPayload = {\n /** The shape that was being edited (original, unchanged) */\n shape: Shape;\n /** Map instance ID for multi-map event isolation */\n mapId: UniqueId;\n};\n\n/**\n * Event payload for shapes:edit-canceled\n * Emitted when editing is canceled\n */\nexport type ShapeEditCanceledEvent = {\n type: 'shapes:edit-canceled';\n payload: ShapeEditCanceledPayload;\n source: UniqueId;\n target?: UniqueId;\n};\n\n/**\n * Union of all edit shape event types\n */\nexport type EditShapeEvent =\n | ShapeEditingEvent\n | ShapeUpdatedEvent\n | ShapeEditCanceledEvent;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,MAAa,kBAAkB;CAE7B,SAAS;CAET,SAAS;CAET,UAAU;CACX"}
1
+ {"version":3,"file":"events.js","names":[],"sources":["../../../../src/deckgl/shapes/edit-shape-layer/events.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\n/**\n * Edit Shape Events\n *\n * Note on event payload structure:\n * These events define explicit payload types rather than using the `Payload<T, P>` helper\n * from @accelint/bus. This is because the `Shape` type contains GeoJSON `Feature` objects\n * from the `geojson` package, which don't satisfy TypeScript's `StructuredCloneable` type\n * constraint used by the bus.\n *\n * The issue: `StructuredCloneable` (from type-fest) requires objects to have an index\n * signature `[key: string]: StructuredCloneable`, but GeoJSON interfaces define strict\n * property types without index signatures. At runtime, GeoJSON data IS structurally\n * cloneable (can be passed through postMessage, stored in IndexedDB, etc.), but\n * TypeScript can't verify this statically.\n *\n * Events that only contain primitive values (like ShapeId, mapId) can use the `Payload`\n * helper directly - see shared/events.ts for examples.\n *\n * When emitting these events via the bus, use type assertions:\n * @example\n * ```ts\n * bus.emit('shapes:updated', {\n * type: 'shapes:updated',\n * payload: { shape, mapId },\n * source: componentId,\n * } as unknown as Payload);\n * ```\n */\n\n'use client';\n\nimport type { UniqueId } from '@accelint/core';\nimport type { Shape } from '../shared/types';\n\n/**\n * Edit shape lifecycle events\n */\nexport const EditShapeEvents = {\n /** Editing has started for a shape */\n editing: 'shapes:editing',\n /** Shape has been successfully updated */\n updated: 'shapes:updated',\n /** Editing was canceled */\n canceled: 'shapes:edit-canceled',\n} as const;\n\nexport type EditShapeEventType =\n (typeof EditShapeEvents)[keyof typeof EditShapeEvents];\n\n/**\n * Payload for shapes:editing event.\n */\nexport type ShapeEditingPayload = {\n /** The shape being edited */\n shape: Shape;\n /** Map instance ID for multi-map event isolation */\n mapId: UniqueId;\n};\n\n/**\n * Event payload for shapes:editing\n * Emitted when editing starts\n */\nexport type ShapeEditingEvent = {\n type: 'shapes:editing';\n payload: ShapeEditingPayload;\n source: UniqueId;\n target?: UniqueId;\n};\n\n/**\n * Payload for shapes:updated event.\n */\nexport type ShapeUpdatedPayload = {\n /** The updated shape with new geometry */\n shape: Shape;\n /** Map instance ID for multi-map event isolation */\n mapId: UniqueId;\n};\n\n/**\n * Event payload for shapes:updated\n * Emitted when shape edits are saved\n */\nexport type ShapeUpdatedEvent = {\n type: 'shapes:updated';\n payload: ShapeUpdatedPayload;\n source: UniqueId;\n target?: UniqueId;\n};\n\n/**\n * Payload for shapes:edit-canceled event.\n */\nexport type ShapeEditCanceledPayload = {\n /** The shape that was being edited (original, unchanged) */\n shape: Shape;\n /** Map instance ID for multi-map event isolation */\n mapId: UniqueId;\n};\n\n/**\n * Event payload for shapes:edit-canceled\n * Emitted when editing is canceled\n */\nexport type ShapeEditCanceledEvent = {\n type: 'shapes:edit-canceled';\n payload: ShapeEditCanceledPayload;\n source: UniqueId;\n target?: UniqueId;\n};\n\n/**\n * Union of all edit shape event types\n */\nexport type EditShapeEvent =\n | ShapeEditingEvent\n | ShapeUpdatedEvent\n | ShapeEditCanceledEvent;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,MAAa,kBAAkB;CAE7B,SAAS;CAET,SAAS;CAET,UAAU;CACX"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["shapeProperty: string | undefined"],"sources":["../../../../src/deckgl/shapes/edit-shape-layer/index.tsx"],"sourcesContent":["/*\n * Copyright 2025 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 { useContext, useEffect, 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 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\nfunction isContinuousEditType(editType: string): boolean {\n return CONTINUOUS_EDIT_TYPES.has(editType);\n}\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 // 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 // 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 // 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 // Helper to cancel any pending RAF update\n const cancelPendingUpdate = () => {\n if (pendingUpdateRef.current) {\n cancelAnimationFrame(pendingUpdateRef.current.rafId);\n pendingUpdateRef.current = null;\n }\n };\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAS,qBAAqB,UAA2B;AACvD,QAAO,sBAAsB,IAAI,SAAS;;AAG5C,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;AAO1D,qBAAoB,aALF,cAAc,gBAAgB,KAKL;CAG3C,MAAM,mBAAmB,OAGf,KAAK;AAGf,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,4BAA4B;AAChC,MAAI,iBAAiB,SAAS;AAC5B,wBAAqB,iBAAiB,QAAQ,MAAM;AACpD,oBAAiB,UAAU;;;CAK/B,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":["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 { useContext, useEffect, 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 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\nfunction isContinuousEditType(editType: string): boolean {\n return CONTINUOUS_EDIT_TYPES.has(editType);\n}\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 // 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 // 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 // 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 // Helper to cancel any pending RAF update\n const cancelPendingUpdate = () => {\n if (pendingUpdateRef.current) {\n cancelAnimationFrame(pendingUpdateRef.current.rafId);\n pendingUpdateRef.current = null;\n }\n };\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAS,qBAAqB,UAA2B;AACvD,QAAO,sBAAsB,IAAI,SAAS;;AAG5C,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;AAO1D,qBAAoB,aALF,cAAc,gBAAgB,KAKL;CAG3C,MAAM,mBAAmB,OAGf,KAAK;AAGf,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,4BAA4B;AAChC,MAAI,iBAAiB,SAAS;AAC5B,wBAAqB,iBAAiB,QAAQ,MAAM;AACpD,oBAAiB,UAAU;;;CAK/B,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 +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 2025 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 * - Active mode tracking during drag operations\n * - Cursor aggregation from child modes\n * - Pick-based mode selection at drag start\n * - Shift key modifier handling for scale/rotate operations\n * - Clean state reset on drag stop\n * - Pick filtering to prevent TypeError from sublayer elements\n *\n * Subclasses define their specific modes and handle matchers, while this\n * base class handles the delegation logic.\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyEA,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 */\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 * - Active mode tracking during drag operations\n * - Cursor aggregation from child modes\n * - Pick-based mode selection at drag start\n * - Shift key modifier handling for scale/rotate operations\n * - Clean state reset on drag stop\n * - Pick filtering to prevent TypeError from sublayer elements\n *\n * Subclasses define their specific modes and handle matchers, while this\n * base class handles the delegation logic.\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyEA,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 +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 2025 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 * 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 * Unlike VertexTransformMode, this mode does NOT include vertex editing handles.\n *\n * Priority logic:\n * - If hovering over a scale handle, scaling takes priority\n * - If hovering over the rotate handle, rotation takes priority\n * - Otherwise, dragging the shape body translates it\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8DA,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 * 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 * Unlike VertexTransformMode, this mode does NOT include vertex editing handles.\n *\n * Priority logic:\n * - If hovering over a scale handle, scaling takes priority\n * - If hovering over the rotate handle, rotation takes priority\n * - Otherwise, dragging the shape body translates it\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8DA,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 +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 2025 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 * 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 diameter and area during resize\n *\n * Priority logic:\n * - If dragging on the edge/handle, resize takes priority\n * - If dragging on the circle body, translate takes priority\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 diameter 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 { diameter, 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(diameter, area, unitAbbrev),\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,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,UAAU,iBAAS,0BACzB,QACA,YACA,cACD;AAID,OAAK,UAAU;GACb,UAAU;GACV,MAAM,oBAAoB,UAAUA,QALnB,4BAA4B,cAAc,CAKN;GACtD"}
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 * 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 diameter and area during resize\n *\n * Priority logic:\n * - If dragging on the edge/handle, resize takes priority\n * - If dragging on the circle body, translate takes priority\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 diameter 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 { diameter, 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(diameter, area, unitAbbrev),\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,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,UAAU,iBAAS,0BACzB,QACA,YACA,cACD;AAID,OAAK,UAAU;GACb,UAAU;GACV,MAAM,oBAAoB,UAAUA,QALnB,4BAA4B,cAAc,CAKN;GACtD"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/index.ts"],"sourcesContent":["/*\n * Copyright 2025 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 { TranslateMode, ViewMode } from '@deck.gl-community/editable-layers';\nimport { BoundingTransformMode } from './bounding-transform-mode';\nimport { CircleTransformMode } from './circle-transform-mode';\nimport { VertexTransformMode } from './vertex-transform-mode';\nimport type { EditMode } from '../types';\n\n/**\n * Cached edit mode instances.\n *\n * CRITICAL: Mode instances must be cached at module level to prevent\n * deck.gl assertion failures. Creating new mode instances on each render\n * causes the EditableGeoJsonLayer to fail with assertion errors.\n *\n * BoundingTransformMode combines ScaleModeWithFreeTransform, RotateMode, and\n * TranslateMode for shapes without vertex editing (ellipses, rectangles),\n * allowing non-uniform scaling plus rotate/translate via bounding box handles.\n * Shows live dimension tooltips during scaling.\n *\n * VertexTransformMode combines ModifyMode with ScaleModeWithFreeTransform,\n * RotateMode, and TranslateMode for shapes that support vertex editing\n * (polygons, lines), allowing vertex manipulation plus scale/rotate/translate.\n *\n * CircleTransformMode combines ResizeCircleMode with TranslateMode\n * for circles, allowing resize from edge plus drag to translate.\n * Shows live diameter/area tooltips during resize.\n *\n * TranslateMode allows dragging to move the shape (used for points).\n */\nconst EDIT_MODE_INSTANCES = {\n view: new ViewMode(),\n 'bounding-transform': new BoundingTransformMode(),\n 'vertex-transform': new VertexTransformMode(),\n 'circle-transform': new CircleTransformMode(),\n translate: new TranslateMode(),\n} as const;\n\n/**\n * Get the cached mode instance for an edit mode.\n *\n * @param mode - The edit mode to get the instance for\n * @returns The cached mode instance\n */\nexport function getEditModeInstance(\n mode: EditMode,\n): (typeof EDIT_MODE_INSTANCES)[EditMode] {\n return EDIT_MODE_INSTANCES[mode];\n}\n\n/**\n * Get the ViewMode instance (for when not editing).\n *\n * @returns The cached ViewMode instance\n */\nexport function getViewModeInstance(): ViewMode {\n return EDIT_MODE_INSTANCES.view;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,MAAM,sBAAsB;CAC1B,MAAM,IAAI,UAAU;CACpB,sBAAsB,IAAI,uBAAuB;CACjD,oBAAoB,IAAI,qBAAqB;CAC7C,oBAAoB,IAAI,qBAAqB;CAC7C,WAAW,IAAI,eAAe;CAC/B;;;;;;;AAQD,SAAgB,oBACd,MACwC;AACxC,QAAO,oBAAoB"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/index.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 { TranslateMode, ViewMode } from '@deck.gl-community/editable-layers';\nimport { BoundingTransformMode } from './bounding-transform-mode';\nimport { CircleTransformMode } from './circle-transform-mode';\nimport { VertexTransformMode } from './vertex-transform-mode';\nimport type { EditMode } from '../types';\n\n/**\n * Cached edit mode instances.\n *\n * CRITICAL: Mode instances must be cached at module level to prevent\n * deck.gl assertion failures. Creating new mode instances on each render\n * causes the EditableGeoJsonLayer to fail with assertion errors.\n *\n * BoundingTransformMode combines ScaleModeWithFreeTransform, RotateMode, and\n * TranslateMode for shapes without vertex editing (ellipses, rectangles),\n * allowing non-uniform scaling plus rotate/translate via bounding box handles.\n * Shows live dimension tooltips during scaling.\n *\n * VertexTransformMode combines ModifyMode with ScaleModeWithFreeTransform,\n * RotateMode, and TranslateMode for shapes that support vertex editing\n * (polygons, lines), allowing vertex manipulation plus scale/rotate/translate.\n *\n * CircleTransformMode combines ResizeCircleMode with TranslateMode\n * for circles, allowing resize from edge plus drag to translate.\n * Shows live diameter/area tooltips during resize.\n *\n * TranslateMode allows dragging to move the shape (used for points).\n */\nconst EDIT_MODE_INSTANCES = {\n view: new ViewMode(),\n 'bounding-transform': new BoundingTransformMode(),\n 'vertex-transform': new VertexTransformMode(),\n 'circle-transform': new CircleTransformMode(),\n translate: new TranslateMode(),\n} as const;\n\n/**\n * Get the cached mode instance for an edit mode.\n *\n * @param mode - The edit mode to get the instance for\n * @returns The cached mode instance\n */\nexport function getEditModeInstance(\n mode: EditMode,\n): (typeof EDIT_MODE_INSTANCES)[EditMode] {\n return EDIT_MODE_INSTANCES[mode];\n}\n\n/**\n * Get the ViewMode instance (for when not editing).\n *\n * @returns The cached ViewMode instance\n */\nexport function getViewModeInstance(): ViewMode {\n return EDIT_MODE_INSTANCES.view;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,MAAM,sBAAsB;CAC1B,MAAM,IAAI,UAAU;CACpB,sBAAsB,IAAI,uBAAuB;CACjD,oBAAoB,IAAI,qBAAqB;CAC7C,oBAAoB,IAAI,qBAAqB;CAC7C,WAAW,IAAI,eAAe;CAC/B;;;;;;;AAQD,SAAgB,oBACd,MACwC;AACxC,QAAO,oBAAoB"}
@@ -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 2025 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\n * - With modeConfig.snapRotation = true: Snap to 45° intervals (0°, 45°, 90°, etc.)\n *\n * This allows precise alignment of shapes to common angles.\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;;;;;;;;;;;AAYxC,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: 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\n * - With modeConfig.snapRotation = true: Snap to 45° intervals (0°, 45°, 90°, etc.)\n *\n * This allows precise alignment of shapes to common angles.\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;;;;;;;;;;;AAYxC,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 +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 2025 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 modeConfig.lockScaling = true: 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 */\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEA,IAAa,6BAAb,cAAgD,UAAU;;;;;;;;;;CAUxD,AAAS,eACP,OACA,OACA;EAEA,MAAM,OAAO;AAEb,MAAI,CAAC,KAAK,WACR;AAGF,QAAM,eAAe,KAAK,QAAQ;EAElC,MAAM,eAAe,KAAK,gBAAgB,OAAO,KAAK;AACtD,MAAI,CAAC,aACH;EAGF,MAAM,cAAc,MAAM,YAAY,eAAe;EACrD,MAAM,EAAE,QAAQ,cAAc,aAAa;EAI3C,MAAM,cAAc,cAChB,KAAK,4BAA4B,OAAO,OAAO,GAC/C,aAAa;EACjB,MAAM,cAAc,cAChB,KAAK,4BAA4B,OAAO,OAAO,GAC/C,aAAa;EAEjB,MAAM,iBAAiB,KAAK,qBAC1B,UACA,aACA,aACA,OACD;EAED,MAAM,cAAc,KAAK,gBAAgB,OAAO,eAAe;AAE/D,QAAM,OAAO;GACX;GACA,UAAU;GACV,aAAa,EACX,gBAAgB,MAAM,iBACvB;GACF,CAAC;AAEF,QAAM,WAAW;;;;;;CAOnB,AAAS,mBACP,OACA,OACA;EAEA,MAAM,OAAO;AAEb,MAAI,KAAK,YAAY;AACnB,QAAK,wBAAwB,OAAO,OAAO,KAAK;AAChD,QAAK,gBAAgB,OAAO,KAAK;;;;;;;CAQrC,AAAQ,gBACN,OAEA,MACqB;AACrB,MAAI,CAAC,KAAK,oBACR,QAAO;EAGT,MAAM,iBAAiB,KAAK,wBAC1B,KAAK,oBACN;AACD,MAAI,CAAC,eACH,QAAO;EAGT,MAAM,WAAW,KAAK;AACtB,MAAI,CAAC,SACH,QAAO;EAGT,MAAMA,SAAmB,eAAe,SAAS;AAGjD,SAAO;GAAE;GAAQ,cAFI,KAAK,sBAAsB,OAAO,OAAO;GAE/B;GAAU;;;;;;;;;;;;;;;;;;;;;CAsB3C,AAAQ,sBACN,OACA,QACc;EACd,MAAM,iBAAiB,MAAM;EAC7B,MAAM,eAAe,MAAM;EAG3B,MAAM,eAAe,eAAe,MAAM,MAAM,OAAO,MAAM;EAC7D,MAAM,eAAe,eAAe,MAAM,MAAM,OAAO,MAAM;EAC7D,MAAM,iBAAiB,aAAa,MAAM,MAAM,OAAO,MAAM;EAC7D,MAAM,iBAAiB,aAAa,MAAM,MAAM,OAAO,MAAM;EAG7D,MAAM,UAAU;EAEhB,MAAM,WAAW;EAIjB,MAAM,YACJ,KAAK,IAAI,YAAY,GAAG,UAAU,gBAAgB,cAAc;EAClE,MAAM,YACJ,KAAK,IAAI,YAAY,GAAG,UAAU,gBAAgB,cAAc;AAMlE,SAAO;GAAE,QAHM,KAAK,IAAI,WAAW,SAAS;GAG3B,QAFF,KAAK,IAAI,WAAW,SAAS;GAEnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwC3B,AAAQ,4BACN,OACA,QACQ;EACR,MAAM,iBAAiB,MAAM;EAC7B,MAAM,eAAe,MAAM;EAG3B,MAAM,eAAe,eAAe,MAAM,MAAM,OAAO,MAAM;EAC7D,MAAM,eAAe,eAAe,MAAM,MAAM,OAAO,MAAM;EAG7D,MAAM,iBAAiB,aAAa,MAAM,MAAM,OAAO,MAAM;EAC7D,MAAM,iBAAiB,aAAa,MAAM,MAAM,OAAO,MAAM;EAG7D,MAAM,YAAY,KAAK,KAAK,eAAe,IAAI,eAAe,EAAE;AAEhE,MAAI,YAAY,KACd,QAAO;EAST,MAAM,iBAHJ,gBAAgB,cAAc,gBAAgB,eAGb;EAInC,MAAM,WAAW;EACjB,MAAM,WAAW,gBAAgB;AACjC,SAAO,KAAK,IAAI,UAAU,SAAS;;;;;CAMrC,AAAQ,wBACN,OACA,OAEA,MACA;EACA,MAAM,eAAe,KAAK,gBAAgB,OAAO,KAAK;AACtD,MAAI,CAAC,aACH;EAGF,MAAM,cAAc,MAAM,YAAY,eAAe;EACrD,MAAM,EAAE,QAAQ,cAAc,aAAa;EAG3C,MAAM,cAAc,cAChB,KAAK,4BAA4B,OAAO,OAAO,GAC/C,aAAa;EACjB,MAAM,cAAc,cAChB,KAAK,4BAA4B,OAAO,OAAO,GAC/C,aAAa;EAEjB,MAAM,iBAAiB,KAAK,qBAC1B,UACA,aACA,aACA,OACD;EAED,MAAM,cAAc,KAAK,gBAAgB,OAAO,eAAe;AAE/D,QAAM,OAAO;GACX;GACA,UAAU;GACV,aAAa,EACX,gBAAgB,MAAM,iBACvB;GACF,CAAC;;;;;CAMJ,AAAQ,gBACN,OAEA,MACA;AACA,QAAM,eAAe,KAAK;AAC1B,OAAK,uBAAuB;AAC5B,OAAK,sBAAsB;AAC3B,OAAK,UAAU;AACf,OAAK,aAAa;;;;;;CAOpB,AAAQ,qBACN,UACA,QACA,QACA,QACmB;AAcnB,SAAO;GACL,MAAM;GAEN,UAhBqB,SAAS,SAAS,KAAK,YAAY;IACxD,MAAM,iBAAiB,KAAK,cAC1B,QAAQ,UACR,QACA,QACA,OACD;AACD,WAAO;KACL,GAAG;KACH,UAAU;KACX;KACD;GAMD;;;;;;;;;;;;;;;;;CAkBH,AAAQ,cAEN,UACA,QACA,QACA,QAEK;EAEL,MAAM,cAAc,UAA8B;AAChD,UAAO,EACJ,OAAO,MAAM,OAAO,MAAM,MAAM,MAAM,OAAO,MAAM,MAAM,SACzD,OAAO,MAAM,OAAO,MAAM,MAAM,MAAM,OAAO,MAAM,MAAM,OAC3D;;AAGH,UAAQ,SAAS,MAAjB;GACE,KAAK,QACH,QAAO;IACL,GAAG;IACH,aAAa,WAAW,SAAS,YAAY;IAC9C;GACH,KAAK;GACL,KAAK,aACH,QAAO;IACL,GAAG;IACH,aAAa,SAAS,YAAY,IAAI,WAAW;IAClD;GACH,KAAK;GACL,KAAK,kBACH,QAAO;IACL,GAAG;IACH,aAAa,SAAS,YAAY,KAAK,SACrC,KAAK,IAAI,WAAW,CACrB;IACF;GACH,KAAK,eACH,QAAO;IACL,GAAG;IACH,aAAa,SAAS,YAAY,KAAK,YACrC,QAAQ,KAAK,SAAqB,KAAK,IAAI,WAAW,CAAC,CACxD;IACF;GACH,QACE,QAAO"}
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 modeConfig.lockScaling = true: 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 */\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEA,IAAa,6BAAb,cAAgD,UAAU;;;;;;;;;;CAUxD,AAAS,eACP,OACA,OACA;EAEA,MAAM,OAAO;AAEb,MAAI,CAAC,KAAK,WACR;AAGF,QAAM,eAAe,KAAK,QAAQ;EAElC,MAAM,eAAe,KAAK,gBAAgB,OAAO,KAAK;AACtD,MAAI,CAAC,aACH;EAGF,MAAM,cAAc,MAAM,YAAY,eAAe;EACrD,MAAM,EAAE,QAAQ,cAAc,aAAa;EAI3C,MAAM,cAAc,cAChB,KAAK,4BAA4B,OAAO,OAAO,GAC/C,aAAa;EACjB,MAAM,cAAc,cAChB,KAAK,4BAA4B,OAAO,OAAO,GAC/C,aAAa;EAEjB,MAAM,iBAAiB,KAAK,qBAC1B,UACA,aACA,aACA,OACD;EAED,MAAM,cAAc,KAAK,gBAAgB,OAAO,eAAe;AAE/D,QAAM,OAAO;GACX;GACA,UAAU;GACV,aAAa,EACX,gBAAgB,MAAM,iBACvB;GACF,CAAC;AAEF,QAAM,WAAW;;;;;;CAOnB,AAAS,mBACP,OACA,OACA;EAEA,MAAM,OAAO;AAEb,MAAI,KAAK,YAAY;AACnB,QAAK,wBAAwB,OAAO,OAAO,KAAK;AAChD,QAAK,gBAAgB,OAAO,KAAK;;;;;;;CAQrC,AAAQ,gBACN,OAEA,MACqB;AACrB,MAAI,CAAC,KAAK,oBACR,QAAO;EAGT,MAAM,iBAAiB,KAAK,wBAC1B,KAAK,oBACN;AACD,MAAI,CAAC,eACH,QAAO;EAGT,MAAM,WAAW,KAAK;AACtB,MAAI,CAAC,SACH,QAAO;EAGT,MAAMA,SAAmB,eAAe,SAAS;AAGjD,SAAO;GAAE;GAAQ,cAFI,KAAK,sBAAsB,OAAO,OAAO;GAE/B;GAAU;;;;;;;;;;;;;;;;;;;;;CAsB3C,AAAQ,sBACN,OACA,QACc;EACd,MAAM,iBAAiB,MAAM;EAC7B,MAAM,eAAe,MAAM;EAG3B,MAAM,eAAe,eAAe,MAAM,MAAM,OAAO,MAAM;EAC7D,MAAM,eAAe,eAAe,MAAM,MAAM,OAAO,MAAM;EAC7D,MAAM,iBAAiB,aAAa,MAAM,MAAM,OAAO,MAAM;EAC7D,MAAM,iBAAiB,aAAa,MAAM,MAAM,OAAO,MAAM;EAG7D,MAAM,UAAU;EAEhB,MAAM,WAAW;EAIjB,MAAM,YACJ,KAAK,IAAI,YAAY,GAAG,UAAU,gBAAgB,cAAc;EAClE,MAAM,YACJ,KAAK,IAAI,YAAY,GAAG,UAAU,gBAAgB,cAAc;AAMlE,SAAO;GAAE,QAHM,KAAK,IAAI,WAAW,SAAS;GAG3B,QAFF,KAAK,IAAI,WAAW,SAAS;GAEnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwC3B,AAAQ,4BACN,OACA,QACQ;EACR,MAAM,iBAAiB,MAAM;EAC7B,MAAM,eAAe,MAAM;EAG3B,MAAM,eAAe,eAAe,MAAM,MAAM,OAAO,MAAM;EAC7D,MAAM,eAAe,eAAe,MAAM,MAAM,OAAO,MAAM;EAG7D,MAAM,iBAAiB,aAAa,MAAM,MAAM,OAAO,MAAM;EAC7D,MAAM,iBAAiB,aAAa,MAAM,MAAM,OAAO,MAAM;EAG7D,MAAM,YAAY,KAAK,KAAK,eAAe,IAAI,eAAe,EAAE;AAEhE,MAAI,YAAY,KACd,QAAO;EAST,MAAM,iBAHJ,gBAAgB,cAAc,gBAAgB,eAGb;EAInC,MAAM,WAAW;EACjB,MAAM,WAAW,gBAAgB;AACjC,SAAO,KAAK,IAAI,UAAU,SAAS;;;;;CAMrC,AAAQ,wBACN,OACA,OAEA,MACA;EACA,MAAM,eAAe,KAAK,gBAAgB,OAAO,KAAK;AACtD,MAAI,CAAC,aACH;EAGF,MAAM,cAAc,MAAM,YAAY,eAAe;EACrD,MAAM,EAAE,QAAQ,cAAc,aAAa;EAG3C,MAAM,cAAc,cAChB,KAAK,4BAA4B,OAAO,OAAO,GAC/C,aAAa;EACjB,MAAM,cAAc,cAChB,KAAK,4BAA4B,OAAO,OAAO,GAC/C,aAAa;EAEjB,MAAM,iBAAiB,KAAK,qBAC1B,UACA,aACA,aACA,OACD;EAED,MAAM,cAAc,KAAK,gBAAgB,OAAO,eAAe;AAE/D,QAAM,OAAO;GACX;GACA,UAAU;GACV,aAAa,EACX,gBAAgB,MAAM,iBACvB;GACF,CAAC;;;;;CAMJ,AAAQ,gBACN,OAEA,MACA;AACA,QAAM,eAAe,KAAK;AAC1B,OAAK,uBAAuB;AAC5B,OAAK,sBAAsB;AAC3B,OAAK,UAAU;AACf,OAAK,aAAa;;;;;;CAOpB,AAAQ,qBACN,UACA,QACA,QACA,QACmB;AAcnB,SAAO;GACL,MAAM;GAEN,UAhBqB,SAAS,SAAS,KAAK,YAAY;IACxD,MAAM,iBAAiB,KAAK,cAC1B,QAAQ,UACR,QACA,QACA,OACD;AACD,WAAO;KACL,GAAG;KACH,UAAU;KACX;KACD;GAMD;;;;;;;;;;;;;;;;;CAkBH,AAAQ,cAEN,UACA,QACA,QACA,QAEK;EAEL,MAAM,cAAc,UAA8B;AAChD,UAAO,EACJ,OAAO,MAAM,OAAO,MAAM,MAAM,MAAM,OAAO,MAAM,MAAM,SACzD,OAAO,MAAM,OAAO,MAAM,MAAM,MAAM,OAAO,MAAM,MAAM,OAC3D;;AAGH,UAAQ,SAAS,MAAjB;GACE,KAAK,QACH,QAAO;IACL,GAAG;IACH,aAAa,WAAW,SAAS,YAAY;IAC9C;GACH,KAAK;GACL,KAAK,aACH,QAAO;IACL,GAAG;IACH,aAAa,SAAS,YAAY,IAAI,WAAW;IAClD;GACH,KAAK;GACL,KAAK,kBACH,QAAO;IACL,GAAG;IACH,aAAa,SAAS,YAAY,KAAK,SACrC,KAAK,IAAI,WAAW,CACrB;IACF;GACH,KAAK,eACH,QAAO;IACL,GAAG;IACH,aAAa,SAAS,YAAY,KAAK,YACrC,QAAQ,KAAK,SAAqB,KAAK,IAAI,WAAW,CAAC,CACxD;IACF;GACH,QACE,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"vertex-transform-mode.js","names":["guidesToFilterOut: string[]"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/vertex-transform-mode.ts"],"sourcesContent":["/*\n * Copyright 2025 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 FeatureCollection,\n type GeoJsonEditMode,\n type GuideFeatureCollection,\n type ModeProps,\n ModifyMode,\n TranslateMode,\n} from '@deck.gl-community/editable-layers';\nimport { featureCollection } from '@turf/helpers';\nimport { BaseTransformMode, type HandleMatcher } from './base-transform-mode';\nimport { RotateModeWithSnap } from './rotate-mode-with-snap';\nimport { ScaleModeWithFreeTransform } from './scale-mode-with-free-transform';\n\n/**\n * Transform mode for shapes that support vertex editing (polygons and lines).\n *\n * Use this mode for shapes where individual vertices can be dragged to reshape\n * the geometry. This provides the most flexibility for freeform shape editing.\n *\n * This composite mode provides:\n * - **Vertex editing** (ModifyMode): Drag vertices to reshape the geometry\n * - **Translation** (TranslateMode): Drag the shape 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 *\n * Priority logic:\n * - If hovering over a scale handle, scaling takes priority\n * - If hovering over the rotate handle, rotation takes priority\n * - If hovering over a vertex (edit handle from ModifyMode), vertex editing takes priority\n * - Otherwise, dragging the shape translates it\n *\n * The guides from all modes are combined, showing both vertex handles and transform handles.\n *\n * Note: For shapes like rectangles where vertex editing is filtered out (to preserve\n * rotation), consider using BoundingTransformMode instead.\n *\n * Note: This mode does not show tooltips during editing because arbitrary polygons\n * don't have meaningful dimensions to display. Use BoundingTransformMode for shapes\n * like rectangles and ellipses where dimension tooltips are useful.\n */\nexport class VertexTransformMode extends BaseTransformMode {\n private modifyMode: ModifyMode;\n private translateMode: TranslateMode;\n private scaleMode: ScaleModeWithFreeTransform;\n private rotateMode: RotateModeWithSnap;\n\n constructor() {\n const modifyMode = new ModifyMode();\n const translateMode = new TranslateMode();\n const scaleMode = new ScaleModeWithFreeTransform();\n const rotateMode = new RotateModeWithSnap();\n\n // Order matters: first mode to handle the event wins\n // We put modify first so vertex handles take priority over translate\n super([modifyMode, scaleMode, rotateMode, translateMode]);\n\n this.modifyMode = modifyMode;\n this.translateMode = translateMode;\n this.scaleMode = scaleMode;\n this.rotateMode = rotateMode;\n }\n\n protected override getHandleMatchers(): HandleMatcher[] {\n return [\n {\n // Vertex handle: existing point on polygon/line\n match: (pick) =>\n Boolean(\n pick.isGuide &&\n pick.object?.properties?.guideType === 'editHandle' &&\n pick.object?.properties?.editHandleType === 'existing',\n ),\n mode: this.modifyMode,\n // No shift config - vertex editing doesn't have modifiers\n },\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 * Override getGuides to filter duplicate envelope guides and handle rectangles.\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 * For rectangles, we hide vertex handles because vertex editing would distort\n * the shape or force axis-alignment. Rectangles should use scale handles only.\n */\n override getGuides(\n props: ModeProps<FeatureCollection>,\n ): GuideFeatureCollection {\n // Get guides from all modes (base class handles pick filtering)\n const allGuides = super.getGuides(props);\n\n // Check if we're editing a rectangle - rectangles have shape: 'Rectangle' property\n const isRectangle =\n props.data.features[0]?.properties?.shape === 'Rectangle';\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 guideType = properties.guideType;\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 // For rectangles, hide ModifyMode vertex handles (editHandleType: 'existing')\n // Rectangles should only use scale handles for resizing to preserve rotation\n // Vertex editing would either distort the shape or force axis-alignment\n if (\n isRectangle &&\n guideType === 'editHandle' &&\n editHandleType === 'existing'\n ) {\n return false;\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,IAAa,sBAAb,cAAyC,kBAAkB;CACzD,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,cAAc;EACZ,MAAM,aAAa,IAAI,YAAY;EACnC,MAAM,gBAAgB,IAAI,eAAe;EACzC,MAAM,YAAY,IAAI,4BAA4B;EAClD,MAAM,aAAa,IAAI,oBAAoB;AAI3C,QAAM;GAAC;GAAY;GAAW;GAAY;GAAc,CAAC;AAEzD,OAAK,aAAa;AAClB,OAAK,gBAAgB;AACrB,OAAK,YAAY;AACjB,OAAK,aAAa;;CAGpB,AAAmB,oBAAqC;AACtD,SAAO;GACL;IAEE,QAAQ,SACN,QACE,KAAK,WACH,KAAK,QAAQ,YAAY,cAAc,gBACvC,KAAK,QAAQ,YAAY,mBAAmB,WAC/C;IACH,MAAM,KAAK;IAEZ;GACD;IAEE,QAAQ,SACN,QACE,KAAK,WAAW,KAAK,QAAQ,YAAY,mBAAmB,QAC7D;IACH,MAAM,KAAK;IACX,aAAa,EAAE,WAAW,eAAe;IAC1C;GACD;IAEE,QAAQ,SACN,QACE,KAAK,WACH,KAAK,QAAQ,YAAY,mBAAmB,SAC/C;IACH,MAAM,KAAK;IACX,aAAa,EAAE,WAAW,gBAAgB;IAC3C;GACF;;CAGH,AAAmB,iBAAkC;AACnD,SAAO,KAAK;;;;;;;;;;;;CAad,AAAS,UACP,OACwB;EAExB,MAAM,YAAY,MAAM,UAAU,MAAM;EAGxC,MAAM,cACJ,MAAM,KAAK,SAAS,IAAI,YAAY,UAAU;AAgChD,SAAO,kBA7BgB,UAAU,SAAS,QAAQ,UAAe;GAC/D,MAAM,aAAa,MAAM,cAAc,EAAE;GACzC,MAAM,iBAAiB,WAAW;GAClC,MAAM,YAAY,WAAW;GAI7B,MAAMA,oBAA8B,CAHvB,WAAW,KAG4B;AAGpD,OAAI,KAAK,WAAW,eAAe,CACjC,mBAAkB,KAAK,eAAyB;AAMlD,OACE,eACA,cAAc,gBACd,mBAAmB,WAEnB,QAAO;AAGT,UAAO,CAAC,kBAAkB,SAAS,QAAQ;IAC3C,CAG6C"}
1
+ {"version":3,"file":"vertex-transform-mode.js","names":["guidesToFilterOut: string[]"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/vertex-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 FeatureCollection,\n type GeoJsonEditMode,\n type GuideFeatureCollection,\n type ModeProps,\n ModifyMode,\n TranslateMode,\n} from '@deck.gl-community/editable-layers';\nimport { featureCollection } from '@turf/helpers';\nimport { BaseTransformMode, type HandleMatcher } from './base-transform-mode';\nimport { RotateModeWithSnap } from './rotate-mode-with-snap';\nimport { ScaleModeWithFreeTransform } from './scale-mode-with-free-transform';\n\n/**\n * Transform mode for shapes that support vertex editing (polygons and lines).\n *\n * Use this mode for shapes where individual vertices can be dragged to reshape\n * the geometry. This provides the most flexibility for freeform shape editing.\n *\n * This composite mode provides:\n * - **Vertex editing** (ModifyMode): Drag vertices to reshape the geometry\n * - **Translation** (TranslateMode): Drag the shape 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 *\n * Priority logic:\n * - If hovering over a scale handle, scaling takes priority\n * - If hovering over the rotate handle, rotation takes priority\n * - If hovering over a vertex (edit handle from ModifyMode), vertex editing takes priority\n * - Otherwise, dragging the shape translates it\n *\n * The guides from all modes are combined, showing both vertex handles and transform handles.\n *\n * Note: For shapes like rectangles where vertex editing is filtered out (to preserve\n * rotation), consider using BoundingTransformMode instead.\n *\n * Note: This mode does not show tooltips during editing because arbitrary polygons\n * don't have meaningful dimensions to display. Use BoundingTransformMode for shapes\n * like rectangles and ellipses where dimension tooltips are useful.\n */\nexport class VertexTransformMode extends BaseTransformMode {\n private modifyMode: ModifyMode;\n private translateMode: TranslateMode;\n private scaleMode: ScaleModeWithFreeTransform;\n private rotateMode: RotateModeWithSnap;\n\n constructor() {\n const modifyMode = new ModifyMode();\n const translateMode = new TranslateMode();\n const scaleMode = new ScaleModeWithFreeTransform();\n const rotateMode = new RotateModeWithSnap();\n\n // Order matters: first mode to handle the event wins\n // We put modify first so vertex handles take priority over translate\n super([modifyMode, scaleMode, rotateMode, translateMode]);\n\n this.modifyMode = modifyMode;\n this.translateMode = translateMode;\n this.scaleMode = scaleMode;\n this.rotateMode = rotateMode;\n }\n\n protected override getHandleMatchers(): HandleMatcher[] {\n return [\n {\n // Vertex handle: existing point on polygon/line\n match: (pick) =>\n Boolean(\n pick.isGuide &&\n pick.object?.properties?.guideType === 'editHandle' &&\n pick.object?.properties?.editHandleType === 'existing',\n ),\n mode: this.modifyMode,\n // No shift config - vertex editing doesn't have modifiers\n },\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 * Override getGuides to filter duplicate envelope guides and handle rectangles.\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 * For rectangles, we hide vertex handles because vertex editing would distort\n * the shape or force axis-alignment. Rectangles should use scale handles only.\n */\n override getGuides(\n props: ModeProps<FeatureCollection>,\n ): GuideFeatureCollection {\n // Get guides from all modes (base class handles pick filtering)\n const allGuides = super.getGuides(props);\n\n // Check if we're editing a rectangle - rectangles have shape: 'Rectangle' property\n const isRectangle =\n props.data.features[0]?.properties?.shape === 'Rectangle';\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 guideType = properties.guideType;\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 // For rectangles, hide ModifyMode vertex handles (editHandleType: 'existing')\n // Rectangles should only use scale handles for resizing to preserve rotation\n // Vertex editing would either distort the shape or force axis-alignment\n if (\n isRectangle &&\n guideType === 'editHandle' &&\n editHandleType === 'existing'\n ) {\n return false;\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,IAAa,sBAAb,cAAyC,kBAAkB;CACzD,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,cAAc;EACZ,MAAM,aAAa,IAAI,YAAY;EACnC,MAAM,gBAAgB,IAAI,eAAe;EACzC,MAAM,YAAY,IAAI,4BAA4B;EAClD,MAAM,aAAa,IAAI,oBAAoB;AAI3C,QAAM;GAAC;GAAY;GAAW;GAAY;GAAc,CAAC;AAEzD,OAAK,aAAa;AAClB,OAAK,gBAAgB;AACrB,OAAK,YAAY;AACjB,OAAK,aAAa;;CAGpB,AAAmB,oBAAqC;AACtD,SAAO;GACL;IAEE,QAAQ,SACN,QACE,KAAK,WACH,KAAK,QAAQ,YAAY,cAAc,gBACvC,KAAK,QAAQ,YAAY,mBAAmB,WAC/C;IACH,MAAM,KAAK;IAEZ;GACD;IAEE,QAAQ,SACN,QACE,KAAK,WAAW,KAAK,QAAQ,YAAY,mBAAmB,QAC7D;IACH,MAAM,KAAK;IACX,aAAa,EAAE,WAAW,eAAe;IAC1C;GACD;IAEE,QAAQ,SACN,QACE,KAAK,WACH,KAAK,QAAQ,YAAY,mBAAmB,SAC/C;IACH,MAAM,KAAK;IACX,aAAa,EAAE,WAAW,gBAAgB;IAC3C;GACF;;CAGH,AAAmB,iBAAkC;AACnD,SAAO,KAAK;;;;;;;;;;;;CAad,AAAS,UACP,OACwB;EAExB,MAAM,YAAY,MAAM,UAAU,MAAM;EAGxC,MAAM,cACJ,MAAM,KAAK,SAAS,IAAI,YAAY,UAAU;AAgChD,SAAO,kBA7BgB,UAAU,SAAS,QAAQ,UAAe;GAC/D,MAAM,aAAa,MAAM,cAAc,EAAE;GACzC,MAAM,iBAAiB,WAAW;GAClC,MAAM,YAAY,WAAW;GAI7B,MAAMA,oBAA8B,CAHvB,WAAW,KAG4B;AAGpD,OAAI,KAAK,WAAW,eAAe,CACjC,mBAAkB,KAAK,eAAyB;AAMlD,OACE,eACA,cAAc,gBACd,mBAAmB,WAEnB,QAAO;AAGT,UAAO,CAAC,kBAAkB,SAAS,QAAQ;IAC3C,CAG6C"}