@accelint/map-toolkit 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/catalog-info.yaml +3 -3
- package/dist/camera/events.d.ts +45 -0
- package/dist/camera/events.js +45 -0
- package/dist/camera/events.js.map +1 -1
- package/dist/camera/store.d.ts +47 -0
- package/dist/camera/store.js +81 -0
- package/dist/camera/store.js.map +1 -1
- package/dist/camera/types.d.ts +81 -0
- package/dist/cursor-coordinates/constants.d.ts +8 -0
- package/dist/cursor-coordinates/constants.js +22 -0
- package/dist/cursor-coordinates/constants.js.map +1 -0
- package/dist/cursor-coordinates/store.d.ts +1 -0
- package/dist/cursor-coordinates/store.js +1 -0
- package/dist/cursor-coordinates/store.js.map +1 -1
- package/dist/cursor-coordinates/use-cursor-coordinates.d.ts +5 -0
- package/dist/cursor-coordinates/use-cursor-coordinates.js +23 -8
- package/dist/cursor-coordinates/use-cursor-coordinates.js.map +1 -1
- package/dist/deckgl/base-map/constants.d.ts +12 -0
- package/dist/deckgl/base-map/constants.js +12 -0
- package/dist/deckgl/base-map/constants.js.map +1 -1
- package/dist/deckgl/base-map/controls.d.ts +11 -1
- package/dist/deckgl/base-map/controls.js +5 -0
- package/dist/deckgl/base-map/controls.js.map +1 -1
- package/dist/deckgl/base-map/events.d.ts +30 -0
- package/dist/deckgl/base-map/events.js +30 -0
- package/dist/deckgl/base-map/events.js.map +1 -1
- package/dist/deckgl/base-map/index.d.ts +2 -2
- package/dist/deckgl/base-map/index.js +33 -3
- package/dist/deckgl/base-map/index.js.map +1 -1
- package/dist/deckgl/base-map/provider.d.ts +2 -2
- package/dist/deckgl/index.js +1 -1
- package/dist/deckgl/saved-viewports/index.d.ts +75 -0
- package/dist/deckgl/saved-viewports/index.js +58 -0
- package/dist/deckgl/saved-viewports/index.js.map +1 -1
- package/dist/deckgl/saved-viewports/storage.d.ts +51 -0
- package/dist/deckgl/saved-viewports/storage.js +64 -0
- package/dist/deckgl/saved-viewports/storage.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/constants.js +18 -6
- package/dist/deckgl/shapes/display-shape-layer/constants.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/fiber.d.ts +7 -0
- package/dist/deckgl/shapes/display-shape-layer/fiber.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/utils/display-style.js +61 -4
- package/dist/deckgl/shapes/display-shape-layer/utils/display-style.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/utils/labels.d.ts +22 -8
- package/dist/deckgl/shapes/display-shape-layer/utils/labels.js +75 -4
- package/dist/deckgl/shapes/display-shape-layer/utils/labels.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/constants.js +30 -0
- package/dist/deckgl/shapes/draw-shape-layer/constants.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/fiber.js +36 -0
- package/dist/deckgl/shapes/draw-shape-layer/fiber.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/index.d.ts +2 -2
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-circle-mode-with-tooltip.js +32 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-circle-mode-with-tooltip.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-ellipse-mode-with-tooltip.js +37 -8
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-ellipse-mode-with-tooltip.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-line-string-mode-with-tooltip.js +43 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-line-string-mode-with-tooltip.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-polygon-mode-with-tooltip.js +44 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-polygon-mode-with-tooltip.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-rectangle-mode-with-tooltip.js +46 -3
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-rectangle-mode-with-tooltip.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/index.js +37 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/index.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/store.js +50 -2
- package/dist/deckgl/shapes/draw-shape-layer/store.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/utils/feature-conversion.js +138 -17
- package/dist/deckgl/shapes/draw-shape-layer/utils/feature-conversion.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/events.js +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/events.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/index.d.ts +2 -2
- package/dist/deckgl/shapes/edit-shape-layer/index.js +14 -0
- package/dist/deckgl/shapes/edit-shape-layer/index.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/base-transform-mode.js +56 -8
- package/dist/deckgl/shapes/edit-shape-layer/modes/base-transform-mode.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.js +26 -4
- package/dist/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode.js +28 -3
- package/dist/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/index.js +24 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/index.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap.js +33 -4
- package/dist/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform.js +21 -2
- package/dist/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/vertex-transform-mode.js +35 -11
- package/dist/deckgl/shapes/edit-shape-layer/modes/vertex-transform-mode.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/store.js +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/store.js.map +1 -1
- package/dist/deckgl/shapes/shared/hooks/use-shift-zoom-disable.js +12 -0
- package/dist/deckgl/shapes/shared/hooks/use-shift-zoom-disable.js.map +1 -1
- package/dist/deckgl/shapes/shared/types.d.ts +3 -3
- package/dist/deckgl/shapes/shared/types.js +2 -2
- package/dist/deckgl/shapes/shared/types.js.map +1 -1
- package/dist/deckgl/shapes/shared/utils/geometry-measurements.js +3 -3
- package/dist/deckgl/shapes/shared/utils/geometry-measurements.js.map +1 -1
- package/dist/deckgl/shapes/shared/utils/pick-filtering.js +1 -1
- package/dist/deckgl/shapes/shared/utils/pick-filtering.js.map +1 -1
- package/dist/deckgl/symbol-layer/fiber.d.ts +18 -0
- package/dist/deckgl/symbol-layer/fiber.js.map +1 -1
- package/dist/deckgl/symbol-layer/index.d.ts +79 -1
- package/dist/deckgl/symbol-layer/index.js +72 -1
- package/dist/deckgl/symbol-layer/index.js.map +1 -1
- package/dist/deckgl/text-layer/character-sets.d.ts +30 -0
- package/dist/deckgl/text-layer/character-sets.js +26 -0
- package/dist/deckgl/text-layer/character-sets.js.map +1 -1
- package/dist/deckgl/text-layer/default-settings.d.ts +29 -0
- package/dist/deckgl/text-layer/default-settings.js +28 -0
- package/dist/deckgl/text-layer/default-settings.js.map +1 -1
- package/dist/deckgl/text-layer/index.d.ts +65 -0
- package/dist/deckgl/text-layer/index.js +56 -0
- package/dist/deckgl/text-layer/index.js.map +1 -1
- package/dist/map-cursor/events.d.ts +19 -0
- package/dist/map-cursor/events.js +19 -0
- package/dist/map-cursor/events.js.map +1 -1
- package/dist/map-cursor/store.d.ts +34 -2
- package/dist/map-cursor/store.js +44 -3
- package/dist/map-cursor/store.js.map +1 -1
- package/dist/map-mode/store.d.ts +43 -4
- package/dist/map-mode/store.js +56 -6
- package/dist/map-mode/store.js.map +1 -1
- package/dist/shared/create-map-store.d.ts +14 -0
- package/dist/shared/create-map-store.js +26 -2
- package/dist/shared/create-map-store.js.map +1 -1
- package/dist/shared/units.d.ts +24 -0
- package/dist/shared/units.js +24 -0
- package/dist/shared/units.js.map +1 -1
- package/dist/viewport/store.d.ts +1 -0
- package/dist/viewport/store.js +4 -0
- package/dist/viewport/store.js.map +1 -1
- package/package.json +3 -3
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
import { DEFAULT_STYLE_PROPERTIES } from "../../shared/constants.js";
|
|
17
17
|
import { ShapeFeatureType } from "../../shared/types.js";
|
|
18
18
|
import { DEFAULT_DISTANCE_UNITS } from "../../../../shared/units.js";
|
|
19
|
-
import { uuid } from "@accelint/core";
|
|
20
19
|
import { getLogger } from "@accelint/logger";
|
|
20
|
+
import { uuid } from "@accelint/core";
|
|
21
21
|
import { centroid, distance } from "@turf/turf";
|
|
22
22
|
|
|
23
23
|
//#region src/deckgl/shapes/draw-shape-layer/utils/feature-conversion.ts
|
|
@@ -28,16 +28,58 @@ const logger = getLogger({
|
|
|
28
28
|
pretty: true
|
|
29
29
|
});
|
|
30
30
|
/**
|
|
31
|
-
* Generate a default name for a shape based on its type
|
|
31
|
+
* Generate a default name for a shape based on its type.
|
|
32
|
+
*
|
|
33
|
+
* Creates a name in the format "New {ShapeType} (HH:MM:SS AM/PM)" using the
|
|
34
|
+
* current time. This provides a default name for shapes created through the
|
|
35
|
+
* drawing interface that includes a timestamp for uniqueness.
|
|
36
|
+
*
|
|
37
|
+
* @param shape - The shape type to generate a name for
|
|
38
|
+
* @returns A formatted name string with timestamp
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { generateShapeName } from '@accelint/map-toolkit/deckgl/shapes/draw-shape-layer/utils/feature-conversion';
|
|
43
|
+
* import { ShapeFeatureType } from '@accelint/map-toolkit/deckgl/shapes/shared/types';
|
|
44
|
+
*
|
|
45
|
+
* const name = generateShapeName(ShapeFeatureType.Polygon);
|
|
46
|
+
* // Returns: "New Polygon (2:30:45 PM)"
|
|
47
|
+
* ```
|
|
32
48
|
*/
|
|
33
49
|
function generateShapeName(shape) {
|
|
34
50
|
return `New ${shape} (${(/* @__PURE__ */ new Date()).toLocaleTimeString()})`;
|
|
35
51
|
}
|
|
36
52
|
/**
|
|
37
|
-
* Compute circle properties from a polygon geometry (circle approximation)
|
|
53
|
+
* Compute circle properties from a polygon geometry (circle approximation).
|
|
54
|
+
*
|
|
55
|
+
* The EditableGeoJsonLayer creates circles as polygon approximations with multiple
|
|
56
|
+
* vertices arranged in a circular pattern. This function extracts the original circle's
|
|
57
|
+
* center and radius from that polygon approximation.
|
|
58
|
+
*
|
|
59
|
+
* The center is calculated using Turf's centroid function, and the radius is computed
|
|
60
|
+
* as the distance from the center to the first edge point.
|
|
61
|
+
*
|
|
62
|
+
* @param geometry - Polygon geometry representing a circle approximation
|
|
63
|
+
* @returns Circle properties with center and radius, or undefined if computation fails
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* import { computeCircleProperties } from '@accelint/map-toolkit/deckgl/shapes/draw-shape-layer/utils/feature-conversion';
|
|
68
|
+
* import type { Polygon } from 'geojson';
|
|
69
|
+
*
|
|
70
|
+
* const polygonGeometry: Polygon = {
|
|
71
|
+
* type: 'Polygon',
|
|
72
|
+
* coordinates: [[
|
|
73
|
+
* [-122.4, 37.8],
|
|
74
|
+
* [-122.39, 37.81],
|
|
75
|
+
* // ... more points forming a circle
|
|
76
|
+
* [-122.4, 37.8], // closing point
|
|
77
|
+
* ]],
|
|
78
|
+
* };
|
|
38
79
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
80
|
+
* const circleProps = computeCircleProperties(polygonGeometry);
|
|
81
|
+
* // Returns: { center: [-122.395, 37.805], radius: { value: 1.5, units: 'kilometers' } }
|
|
82
|
+
* ```
|
|
41
83
|
*/
|
|
42
84
|
function computeCircleProperties(geometry) {
|
|
43
85
|
const coordinates = geometry.coordinates[0];
|
|
@@ -72,10 +114,40 @@ function computeCircleProperties(geometry) {
|
|
|
72
114
|
};
|
|
73
115
|
}
|
|
74
116
|
/**
|
|
75
|
-
* Compute ellipse properties from a feature's editProperties
|
|
117
|
+
* Compute ellipse properties from a feature's editProperties.
|
|
76
118
|
*
|
|
77
119
|
* The DrawEllipseUsingThreePointsMode attaches ellipse metadata to the feature's
|
|
78
|
-
* properties.editProperties. This function extracts and normalizes that data
|
|
120
|
+
* properties.editProperties field. This function extracts and normalizes that data
|
|
121
|
+
* into the standard EllipseProperties format used by Shape objects.
|
|
122
|
+
*
|
|
123
|
+
* The function validates that editProperties exists and has the correct shape
|
|
124
|
+
* discriminator before extracting the ellipse parameters.
|
|
125
|
+
*
|
|
126
|
+
* @param feature - GeoJSON feature with editProperties attached by the draw mode
|
|
127
|
+
* @returns Normalized ellipse properties, or undefined if extraction fails
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* import { computeEllipseProperties } from '@accelint/map-toolkit/deckgl/shapes/draw-shape-layer/utils/feature-conversion';
|
|
132
|
+
* import type { Feature } from 'geojson';
|
|
133
|
+
*
|
|
134
|
+
* const feature: Feature = {
|
|
135
|
+
* type: 'Feature',
|
|
136
|
+
* geometry: { type: 'Polygon', coordinates: [[...]] },
|
|
137
|
+
* properties: {
|
|
138
|
+
* editProperties: {
|
|
139
|
+
* shape: 'Ellipse',
|
|
140
|
+
* center: [-122.4, 37.8],
|
|
141
|
+
* xSemiAxis: { value: 2.5, unit: 'kilometers' },
|
|
142
|
+
* ySemiAxis: { value: 1.5, unit: 'kilometers' },
|
|
143
|
+
* angle: 45,
|
|
144
|
+
* },
|
|
145
|
+
* },
|
|
146
|
+
* };
|
|
147
|
+
*
|
|
148
|
+
* const ellipseProps = computeEllipseProperties(feature);
|
|
149
|
+
* // Returns normalized ellipse properties with standard distance units
|
|
150
|
+
* ```
|
|
79
151
|
*/
|
|
80
152
|
function computeEllipseProperties(feature) {
|
|
81
153
|
const editProps = feature.properties?.editProperties;
|
|
@@ -97,20 +169,69 @@ function computeEllipseProperties(feature) {
|
|
|
97
169
|
};
|
|
98
170
|
}
|
|
99
171
|
/**
|
|
100
|
-
* Convert a raw GeoJSON Feature from EditableGeoJsonLayer to a Shape
|
|
172
|
+
* Convert a raw GeoJSON Feature from EditableGeoJsonLayer to a Shape.
|
|
173
|
+
*
|
|
174
|
+
* This function transforms the raw GeoJSON features produced by deck.gl's
|
|
175
|
+
* EditableGeoJsonLayer into the Shape format used throughout the map-toolkit.
|
|
176
|
+
* It handles geometry normalization, style property merging, and special handling
|
|
177
|
+
* for Circle and Ellipse shapes which are stored as polygon approximations.
|
|
101
178
|
*
|
|
102
|
-
* The returned Shape includes:
|
|
103
|
-
* - Auto-generated UUID
|
|
104
|
-
* - Auto-generated name
|
|
105
|
-
* - Merged style properties
|
|
106
|
-
* - Circle/ellipse properties
|
|
107
|
-
* -
|
|
108
|
-
* -
|
|
179
|
+
* ## The returned Shape includes:
|
|
180
|
+
* - **Auto-generated UUID**: Unique identifier for the shape
|
|
181
|
+
* - **Auto-generated name**: Format "New {ShapeType} (HH:MM:SS AM/PM)"
|
|
182
|
+
* - **Merged style properties**: Defaults + optional custom overrides
|
|
183
|
+
* - **Circle/ellipse properties**: Computed from geometry if applicable
|
|
184
|
+
* - **lastUpdated timestamp**: UTC timestamp when created
|
|
185
|
+
* - **locked: false**: Newly created shapes are always unlocked
|
|
186
|
+
*
|
|
187
|
+
* ## Special Handling
|
|
188
|
+
* - **Circles**: Extracts center and radius from polygon approximation
|
|
189
|
+
* - **Ellipses**: Extracts center, semi-axes, and angle from editProperties
|
|
109
190
|
*
|
|
110
191
|
* @param feature - The raw GeoJSON feature from the editable layer
|
|
111
192
|
* @param shape - The type of shape being created
|
|
112
|
-
* @param styleDefaults - Optional style overrides
|
|
113
|
-
* @returns A complete Shape ready for use
|
|
193
|
+
* @param styleDefaults - Optional style property overrides (colors, line width, etc.)
|
|
194
|
+
* @returns A complete Shape object ready for use in DisplayShapeLayer
|
|
195
|
+
*
|
|
196
|
+
* @example Basic usage with polygon
|
|
197
|
+
* ```typescript
|
|
198
|
+
* import { convertFeatureToShape } from '@accelint/map-toolkit/deckgl/shapes/draw-shape-layer/utils/feature-conversion';
|
|
199
|
+
* import { ShapeFeatureType } from '@accelint/map-toolkit/deckgl/shapes/shared/types';
|
|
200
|
+
* import type { Feature } from 'geojson';
|
|
201
|
+
*
|
|
202
|
+
* const feature: Feature = {
|
|
203
|
+
* type: 'Feature',
|
|
204
|
+
* geometry: {
|
|
205
|
+
* type: 'Polygon',
|
|
206
|
+
* coordinates: [[
|
|
207
|
+
* [-122.4, 37.8],
|
|
208
|
+
* [-122.3, 37.8],
|
|
209
|
+
* [-122.3, 37.9],
|
|
210
|
+
* [-122.4, 37.9],
|
|
211
|
+
* [-122.4, 37.8],
|
|
212
|
+
* ]],
|
|
213
|
+
* },
|
|
214
|
+
* properties: {},
|
|
215
|
+
* };
|
|
216
|
+
*
|
|
217
|
+
* const shape = convertFeatureToShape(feature, ShapeFeatureType.Polygon);
|
|
218
|
+
* // Returns: { id: 'uuid...', name: 'New Polygon (2:30:45 PM)', shape: 'Polygon', ... }
|
|
219
|
+
* ```
|
|
220
|
+
*
|
|
221
|
+
* @example With custom style defaults
|
|
222
|
+
* ```typescript
|
|
223
|
+
* const shape = convertFeatureToShape(
|
|
224
|
+
* feature,
|
|
225
|
+
* ShapeFeatureType.Circle,
|
|
226
|
+
* {
|
|
227
|
+
* fillColor: [255, 100, 100, 180], // RGBA: red fill
|
|
228
|
+
* lineColor: [200, 0, 0, 255], // RGBA: dark red line
|
|
229
|
+
* lineWidth: 4,
|
|
230
|
+
* linePattern: 'solid',
|
|
231
|
+
* }
|
|
232
|
+
* );
|
|
233
|
+
* // Returns a shape with custom colors applied
|
|
234
|
+
* ```
|
|
114
235
|
*/
|
|
115
236
|
function convertFeatureToShape(feature, shape, styleDefaults) {
|
|
116
237
|
const id = uuid();
|
|
@@ -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 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
|
+
{"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 *\n * Creates a name in the format \"New {ShapeType} (HH:MM:SS AM/PM)\" using the\n * current time. This provides a default name for shapes created through the\n * drawing interface that includes a timestamp for uniqueness.\n *\n * @param shape - The shape type to generate a name for\n * @returns A formatted name string with timestamp\n *\n * @example\n * ```typescript\n * import { generateShapeName } from '@accelint/map-toolkit/deckgl/shapes/draw-shape-layer/utils/feature-conversion';\n * import { ShapeFeatureType } from '@accelint/map-toolkit/deckgl/shapes/shared/types';\n *\n * const name = generateShapeName(ShapeFeatureType.Polygon);\n * // Returns: \"New Polygon (2:30:45 PM)\"\n * ```\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 with multiple\n * vertices arranged in a circular pattern. This function extracts the original circle's\n * center and radius from that polygon approximation.\n *\n * The center is calculated using Turf's centroid function, and the radius is computed\n * as the distance from the center to the first edge point.\n *\n * @param geometry - Polygon geometry representing a circle approximation\n * @returns Circle properties with center and radius, or undefined if computation fails\n *\n * @example\n * ```typescript\n * import { computeCircleProperties } from '@accelint/map-toolkit/deckgl/shapes/draw-shape-layer/utils/feature-conversion';\n * import type { Polygon } from 'geojson';\n *\n * const polygonGeometry: Polygon = {\n * type: 'Polygon',\n * coordinates: [[\n * [-122.4, 37.8],\n * [-122.39, 37.81],\n * // ... more points forming a circle\n * [-122.4, 37.8], // closing point\n * ]],\n * };\n *\n * const circleProps = computeCircleProperties(polygonGeometry);\n * // Returns: { center: [-122.395, 37.805], radius: { value: 1.5, units: 'kilometers' } }\n * ```\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 *\n * The DrawEllipseUsingThreePointsMode from @deck.gl-community/editable-layers\n * attaches ellipse metadata to the feature's properties.editProperties field.\n * This interface defines the structure of that metadata.\n *\n * @internal\n */\ninterface EllipseEditProperties {\n /** Shape discriminator - always 'Ellipse' */\n shape: 'Ellipse';\n /** X semi-axis (horizontal radius) with value and unit */\n xSemiAxis: { value: number; unit: string };\n /** Y semi-axis (vertical radius) with value and unit */\n ySemiAxis: { value: number; unit: string };\n /** Rotation angle in degrees */\n angle: number;\n /** Center point coordinates [longitude, latitude] */\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 field. This function extracts and normalizes that data\n * into the standard EllipseProperties format used by Shape objects.\n *\n * The function validates that editProperties exists and has the correct shape\n * discriminator before extracting the ellipse parameters.\n *\n * @param feature - GeoJSON feature with editProperties attached by the draw mode\n * @returns Normalized ellipse properties, or undefined if extraction fails\n *\n * @example\n * ```typescript\n * import { computeEllipseProperties } from '@accelint/map-toolkit/deckgl/shapes/draw-shape-layer/utils/feature-conversion';\n * import type { Feature } from 'geojson';\n *\n * const feature: Feature = {\n * type: 'Feature',\n * geometry: { type: 'Polygon', coordinates: [[...]] },\n * properties: {\n * editProperties: {\n * shape: 'Ellipse',\n * center: [-122.4, 37.8],\n * xSemiAxis: { value: 2.5, unit: 'kilometers' },\n * ySemiAxis: { value: 1.5, unit: 'kilometers' },\n * angle: 45,\n * },\n * },\n * };\n *\n * const ellipseProps = computeEllipseProperties(feature);\n * // Returns normalized ellipse properties with standard distance units\n * ```\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 * This function transforms the raw GeoJSON features produced by deck.gl's\n * EditableGeoJsonLayer into the Shape format used throughout the map-toolkit.\n * It handles geometry normalization, style property merging, and special handling\n * for Circle and Ellipse shapes which are stored as polygon approximations.\n *\n * ## The returned Shape includes:\n * - **Auto-generated UUID**: Unique identifier for the shape\n * - **Auto-generated name**: Format \"New {ShapeType} (HH:MM:SS AM/PM)\"\n * - **Merged style properties**: Defaults + optional custom overrides\n * - **Circle/ellipse properties**: Computed from geometry if applicable\n * - **lastUpdated timestamp**: UTC timestamp when created\n * - **locked: false**: Newly created shapes are always unlocked\n *\n * ## Special Handling\n * - **Circles**: Extracts center and radius from polygon approximation\n * - **Ellipses**: Extracts center, semi-axes, and angle from editProperties\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 property overrides (colors, line width, etc.)\n * @returns A complete Shape object ready for use in DisplayShapeLayer\n *\n * @example Basic usage with polygon\n * ```typescript\n * import { convertFeatureToShape } from '@accelint/map-toolkit/deckgl/shapes/draw-shape-layer/utils/feature-conversion';\n * import { ShapeFeatureType } from '@accelint/map-toolkit/deckgl/shapes/shared/types';\n * import type { Feature } from 'geojson';\n *\n * const feature: Feature = {\n * type: 'Feature',\n * geometry: {\n * type: 'Polygon',\n * coordinates: [[\n * [-122.4, 37.8],\n * [-122.3, 37.8],\n * [-122.3, 37.9],\n * [-122.4, 37.9],\n * [-122.4, 37.8],\n * ]],\n * },\n * properties: {},\n * };\n *\n * const shape = convertFeatureToShape(feature, ShapeFeatureType.Polygon);\n * // Returns: { id: 'uuid...', name: 'New Polygon (2:30:45 PM)', shape: 'Polygon', ... }\n * ```\n *\n * @example With custom style defaults\n * ```typescript\n * const shape = convertFeatureToShape(\n * feature,\n * ShapeFeatureType.Circle,\n * {\n * fillColor: [255, 100, 100, 180], // RGBA: red fill\n * lineColor: [200, 0, 0, 255], // RGBA: dark red line\n * lineWidth: 4,\n * linePattern: 'solid',\n * }\n * );\n * // Returns a shape with custom colors applied\n * ```\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;;;;;;;;;;;;;;;;;;;;AAqBF,SAAS,kBAAkB,OAAiC;AAE1D,QAAO,OAAO,MAAM,qBADF,IAAI,MAAM,EAAC,oBAAoB,CACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCpC,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DH,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEH,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":"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 * ```
|
|
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 * ```typescript\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"}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { EditShapeLayerProps } from "./types.js";
|
|
14
|
-
import * as
|
|
14
|
+
import * as react_jsx_runtime1 from "react/jsx-runtime";
|
|
15
15
|
|
|
16
16
|
//#region src/deckgl/shapes/edit-shape-layer/index.d.ts
|
|
17
17
|
|
|
@@ -57,7 +57,7 @@ declare function EditShapeLayer({
|
|
|
57
57
|
id,
|
|
58
58
|
mapId,
|
|
59
59
|
unit
|
|
60
|
-
}: EditShapeLayerProps):
|
|
60
|
+
}: EditShapeLayerProps): react_jsx_runtime1.JSX.Element | null;
|
|
61
61
|
//#endregion
|
|
62
62
|
export { EditShapeLayer };
|
|
63
63
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -26,9 +26,23 @@ import { useContext, useEffect, useRef } from "react";
|
|
|
26
26
|
import { jsx } from "react/jsx-runtime";
|
|
27
27
|
|
|
28
28
|
//#region src/deckgl/shapes/edit-shape-layer/index.tsx
|
|
29
|
+
/**
|
|
30
|
+
* Check if an edit type is a continuous event (fires during drag).
|
|
31
|
+
* Continuous events are batched with RAF for smooth performance.
|
|
32
|
+
*
|
|
33
|
+
* @param editType - The edit type string from EditableGeoJsonLayer
|
|
34
|
+
* @returns True if the edit type is continuous (e.g., 'translating', 'scaling', 'rotating')
|
|
35
|
+
*/
|
|
29
36
|
function isContinuousEditType(editType) {
|
|
30
37
|
return CONTINUOUS_EDIT_TYPES.has(editType);
|
|
31
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if an edit type is a completion event (fires at drag end).
|
|
41
|
+
* Completion events update state immediately without RAF batching.
|
|
42
|
+
*
|
|
43
|
+
* @param editType - The edit type string from EditableGeoJsonLayer
|
|
44
|
+
* @returns True if the edit type is a completion event (e.g., 'translated', 'scaled', 'rotated')
|
|
45
|
+
*/
|
|
32
46
|
function isCompletionEditType(editType) {
|
|
33
47
|
return COMPLETION_EDIT_TYPES.has(editType);
|
|
34
48
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["shapeProperty: string | undefined"],"sources":["../../../../src/deckgl/shapes/edit-shape-layer/index.tsx"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n'use client';\n\nimport { 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\n/**\n * Check if an edit type is a continuous event (fires during drag).\n * Continuous events are batched with RAF for smooth performance.\n *\n * @param editType - The edit type string from EditableGeoJsonLayer\n * @returns True if the edit type is continuous (e.g., 'translating', 'scaling', 'rotating')\n */\nfunction isContinuousEditType(editType: string): boolean {\n return CONTINUOUS_EDIT_TYPES.has(editType);\n}\n\n/**\n * Check if an edit type is a completion event (fires at drag end).\n * Completion events update state immediately without RAF batching.\n *\n * @param editType - The edit type string from EditableGeoJsonLayer\n * @returns True if the edit type is a completion event (e.g., 'translated', 'scaled', 'rotated')\n */\nfunction isCompletionEditType(editType: string): boolean {\n return COMPLETION_EDIT_TYPES.has(editType);\n}\n\n/**\n * Convert a GeoJSON Feature to a FeatureCollection for EditableGeoJsonLayer.\n * The editable-layers library accepts standard GeoJSON FeatureCollection at runtime,\n * but has stricter internal types. We use the geojson types which are compatible.\n *\n * For circles, adds the `shape: 'Circle'` property required by ResizeCircleMode.\n * ResizeCircleMode checks `properties.shape.includes('Circle')` to identify circles.\n *\n * For rectangles, adds the `shape: 'Rectangle'` property required by ModifyMode's\n * lockRectangles feature. ModifyMode checks `properties.shape === 'Rectangle'`.\n */\nfunction toFeatureCollection(\n feature: Feature,\n shape: ShapeFeatureTypeValues,\n): import('geojson').FeatureCollection {\n // Add shape property for modes that require it\n // - ResizeCircleMode requires shape: 'Circle'\n // - ModifyMode lockRectangles requires shape: 'Rectangle'\n let shapeProperty: string | undefined;\n if (shape === ShapeFeatureType.Circle) {\n shapeProperty = 'Circle';\n } else if (shape === ShapeFeatureType.Rectangle) {\n shapeProperty = 'Rectangle';\n }\n\n const featureWithShape = shapeProperty\n ? {\n ...feature,\n properties: {\n ...feature.properties,\n shape: shapeProperty,\n },\n }\n : feature;\n\n return {\n type: 'FeatureCollection',\n features: [featureWithShape],\n };\n}\n\n/**\n * EditShapeLayer - A React component for editing existing shapes on the map.\n *\n * This component wraps the EditableGeoJsonLayer from @deck.gl-community/editable-layers\n * and integrates with the map-mode and map-cursor systems for proper coordination.\n *\n * Key features:\n * - Renders only when actively editing (returns null otherwise)\n * - Uses cached mode instances to prevent deck.gl assertion errors\n * - Integrates with the editing store for state management\n * - Neutral mode authorization (lets UI decide how to handle mode conflicts)\n * - Circles use ResizeCircleMode with tooltip, other shapes use ModifyMode\n * - Fill colors rendered at 20% opacity, edit handles are white\n * - Live dimension/area tooltips during editing\n * - requestAnimationFrame() batching for smooth drag performance\n *\n * @example\n * ```tsx\n * // Import the fiber registration for JSX support\n * import '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/fiber';\n * import '@accelint/map-toolkit/deckgl/shapes/display-shape-layer/fiber';\n *\n * function Map({ mapId }) {\n * const { editingShape } = useEditShape(mapId);\n *\n * return (\n * <BaseMap id={mapId}>\n * <displayShapeLayer\n * data={shapes}\n * mapId={mapId}\n * selectedShapeId={editingShape?.id}\n * />\n * <EditShapeLayer mapId={mapId} />\n * </BaseMap>\n * );\n * }\n * ```\n */\nexport function EditShapeLayer({\n id = EDIT_SHAPE_LAYER_ID,\n mapId,\n unit,\n}: EditShapeLayerProps) {\n // Get mapId from context if not provided\n const contextId = useContext(MapContext);\n const actualMapId = mapId ?? contextId;\n\n if (!actualMapId) {\n throw new Error(\n 'EditShapeLayer requires either a mapId prop or to be used within a MapProvider',\n );\n }\n\n // Subscribe to editing state using the v2 store API\n const { state: editingState } = editStore.use(actualMapId);\n\n const isEditing = editingState?.editingShape != null;\n\n // 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,SAAS,qBAAqB,UAA2B;AACvD,QAAO,sBAAsB,IAAI,SAAS;;;;;;;;;AAU5C,SAAS,qBAAqB,UAA2B;AACvD,QAAO,sBAAsB,IAAI,SAAS;;;;;;;;;;;;;AAc5C,SAAS,oBACP,SACA,OACqC;CAIrC,IAAIA;AACJ,KAAI,UAAU,iBAAiB,OAC7B,iBAAgB;UACP,UAAU,iBAAiB,UACpC,iBAAgB;AAalB,QAAO;EACL,MAAM;EACN,UAAU,CAZa,gBACrB;GACE,GAAG;GACH,YAAY;IACV,GAAG,QAAQ;IACX,OAAO;IACR;GACF,GACD,QAI0B;EAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCH,SAAgB,eAAe,EAC7B,KAAK,qBACL,OACA,QACsB;CAEtB,MAAM,YAAY,WAAW,WAAW;CACxC,MAAM,cAAc,SAAS;AAE7B,KAAI,CAAC,YACH,OAAM,IAAI,MACR,iFACD;CAIH,MAAM,EAAE,OAAO,iBAAiB,UAAU,IAAI,YAAY;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"}
|
|
@@ -21,15 +21,63 @@ import { CompositeMode } from "@deck.gl-community/editable-layers";
|
|
|
21
21
|
* This class extracts the common patterns shared by CircleTransformMode,
|
|
22
22
|
* BoundingTransformMode, and VertexTransformMode:
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
* -
|
|
26
|
-
* -
|
|
27
|
-
* -
|
|
28
|
-
* -
|
|
29
|
-
* -
|
|
24
|
+
* ## Core Responsibilities
|
|
25
|
+
* - **Active mode tracking**: Tracks which child mode is handling the drag operation
|
|
26
|
+
* - **Cursor aggregation**: Combines cursor updates from all child modes
|
|
27
|
+
* - **Pick-based delegation**: Selects mode at drag start based on picked handles
|
|
28
|
+
* - **Shift key modifiers**: Dynamic Shift key handling for scale/rotate operations
|
|
29
|
+
* - **State management**: Clean state reset on drag stop
|
|
30
|
+
* - **Pick filtering**: Prevents TypeError from sublayer elements without geometry
|
|
30
31
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
32
|
+
* ## Implementation Pattern
|
|
33
|
+
* Subclasses define their specific child modes and handle matchers, while this
|
|
34
|
+
* base class handles the delegation logic. The pattern is:
|
|
35
|
+
*
|
|
36
|
+
* 1. Define child mode instances in constructor
|
|
37
|
+
* 2. Implement `getHandleMatchers()` to map picks to modes
|
|
38
|
+
* 3. Implement `getDefaultMode()` for fallback behavior
|
|
39
|
+
* 4. Optionally override `onDragging()` for tooltips or side effects
|
|
40
|
+
*
|
|
41
|
+
* ## How Mode Selection Works
|
|
42
|
+
* At drag start, the base class evaluates handle matchers in order:
|
|
43
|
+
* - First matcher that matches any pick wins
|
|
44
|
+
* - If no matcher matches, uses the default mode
|
|
45
|
+
* - Selected mode remains active for the entire drag operation
|
|
46
|
+
*
|
|
47
|
+
* @example Implementing a custom transform mode
|
|
48
|
+
* ```typescript
|
|
49
|
+
* import { BaseTransformMode, type HandleMatcher } from './base-transform-mode';
|
|
50
|
+
* import { TranslateMode, RotateMode } from '@deck.gl-community/editable-layers';
|
|
51
|
+
*
|
|
52
|
+
* class MyTransformMode extends BaseTransformMode {
|
|
53
|
+
* private translateMode: TranslateMode;
|
|
54
|
+
* private rotateMode: RotateMode;
|
|
55
|
+
*
|
|
56
|
+
* constructor() {
|
|
57
|
+
* const translateMode = new TranslateMode();
|
|
58
|
+
* const rotateMode = new RotateMode();
|
|
59
|
+
* super([rotateMode, translateMode]); // Order matters for cursor handling
|
|
60
|
+
*
|
|
61
|
+
* this.translateMode = translateMode;
|
|
62
|
+
* this.rotateMode = rotateMode;
|
|
63
|
+
* }
|
|
64
|
+
*
|
|
65
|
+
* protected getHandleMatchers(): HandleMatcher[] {
|
|
66
|
+
* return [
|
|
67
|
+
* {
|
|
68
|
+
* match: (pick) =>
|
|
69
|
+
* Boolean(pick.isGuide && pick.object?.properties?.editHandleType === 'rotate'),
|
|
70
|
+
* mode: this.rotateMode,
|
|
71
|
+
* shiftConfig: { configKey: 'snapRotation' },
|
|
72
|
+
* },
|
|
73
|
+
* ];
|
|
74
|
+
* }
|
|
75
|
+
*
|
|
76
|
+
* protected getDefaultMode() {
|
|
77
|
+
* return this.translateMode; // Dragging body translates by default
|
|
78
|
+
* }
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
33
81
|
*/
|
|
34
82
|
var BaseTransformMode = class extends CompositeMode {
|
|
35
83
|
/** Track which mode is currently handling the drag operation */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-transform-mode.js","names":["updatedCursor: string | null | undefined","propsWithConfig: ModeProps<FeatureCollection>","filteredProps: ModeProps<FeatureCollection>"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/base-transform-mode.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n CompositeMode,\n type DraggingEvent,\n type FeatureCollection,\n type GeoJsonEditMode,\n type GuideFeatureCollection,\n type ModeProps,\n type PointerMoveEvent,\n type StartDraggingEvent,\n type StopDraggingEvent,\n type Tooltip,\n} from '@deck.gl-community/editable-layers';\nimport { filterGeometryAwarePicks } from '../../shared/utils/pick-filtering';\n\n/**\n * Configuration for how a mode handles the Shift key modifier.\n */\nexport type ShiftKeyConfig = {\n /** The modeConfig property name to set (e.g., 'lockScaling', 'snapRotation') */\n configKey: string;\n /** The value to set when Shift is held (defaults to true) */\n value?: boolean;\n};\n\n/**\n * Definition for how to detect and handle a specific edit handle type.\n */\nexport type HandleMatcher = {\n /** Function to determine if a pick matches this handle type */\n match: (pick: {\n isGuide?: boolean;\n object?: {\n properties?: {\n guideType?: string;\n editHandleType?: string;\n mode?: string;\n };\n };\n }) => boolean;\n /** The mode instance to delegate to when this handle is matched */\n mode: GeoJsonEditMode;\n /** Optional Shift key configuration for this mode */\n shiftConfig?: ShiftKeyConfig;\n};\n\n/**\n * Abstract base class for composite transform modes.\n *\n * This class extracts the common patterns shared by CircleTransformMode,\n * BoundingTransformMode, and VertexTransformMode:\n *\n * - 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 * ## Core Responsibilities\n * - **Active mode tracking**: Tracks which child mode is handling the drag operation\n * - **Cursor aggregation**: Combines cursor updates from all child modes\n * - **Pick-based delegation**: Selects mode at drag start based on picked handles\n * - **Shift key modifiers**: Dynamic Shift key handling for scale/rotate operations\n * - **State management**: Clean state reset on drag stop\n * - **Pick filtering**: Prevents TypeError from sublayer elements without geometry\n *\n * ## Implementation Pattern\n * Subclasses define their specific child modes and handle matchers, while this\n * base class handles the delegation logic. The pattern is:\n *\n * 1. Define child mode instances in constructor\n * 2. Implement `getHandleMatchers()` to map picks to modes\n * 3. Implement `getDefaultMode()` for fallback behavior\n * 4. Optionally override `onDragging()` for tooltips or side effects\n *\n * ## How Mode Selection Works\n * At drag start, the base class evaluates handle matchers in order:\n * - First matcher that matches any pick wins\n * - If no matcher matches, uses the default mode\n * - Selected mode remains active for the entire drag operation\n *\n * @example Implementing a custom transform mode\n * ```typescript\n * import { BaseTransformMode, type HandleMatcher } from './base-transform-mode';\n * import { TranslateMode, RotateMode } from '@deck.gl-community/editable-layers';\n *\n * class MyTransformMode extends BaseTransformMode {\n * private translateMode: TranslateMode;\n * private rotateMode: RotateMode;\n *\n * constructor() {\n * const translateMode = new TranslateMode();\n * const rotateMode = new RotateMode();\n * super([rotateMode, translateMode]); // Order matters for cursor handling\n *\n * this.translateMode = translateMode;\n * this.rotateMode = rotateMode;\n * }\n *\n * protected getHandleMatchers(): HandleMatcher[] {\n * return [\n * {\n * match: (pick) =>\n * Boolean(pick.isGuide && pick.object?.properties?.editHandleType === 'rotate'),\n * mode: this.rotateMode,\n * shiftConfig: { configKey: 'snapRotation' },\n * },\n * ];\n * }\n *\n * protected getDefaultMode() {\n * return this.translateMode; // Dragging body translates by default\n * }\n * }\n * ```\n */\nexport abstract class BaseTransformMode extends CompositeMode {\n /** Track which mode is currently handling the drag operation */\n protected activeDragMode: GeoJsonEditMode | null = null;\n\n /** Track current Shift state for dynamic modifier behavior */\n protected isShiftHeld = false;\n\n /** Tooltip for operations that show live measurements */\n protected tooltip: Tooltip | null = null;\n\n /**\n * Get the handle matchers that define how picks map to modes.\n * Matchers are evaluated in order; first match wins.\n * The last matcher should typically be a catch-all for the default mode.\n */\n protected abstract getHandleMatchers(): HandleMatcher[];\n\n /**\n * Get the default mode to use when no handle matchers match.\n * This is typically TranslateMode for dragging the shape body.\n */\n protected abstract getDefaultMode(): GeoJsonEditMode;\n\n /**\n * Optional hook called during drag when the active mode is set.\n * Subclasses can override to update tooltips or perform other side effects.\n */\n protected onDragging?(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void;\n\n /**\n * Aggregates cursor updates from all child modes.\n * The first non-null cursor from any mode is used.\n */\n override handlePointerMove(\n event: PointerMoveEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n let updatedCursor: string | null | undefined = null;\n\n super.handlePointerMove(event, {\n ...props,\n onUpdateCursor: (cursor: string | null | undefined) => {\n updatedCursor = cursor || updatedCursor;\n },\n });\n\n props.onUpdateCursor(updatedCursor);\n }\n\n /**\n * Determines which mode should handle the drag based on picked handles.\n * Cancels map panning and delegates to the matched mode.\n */\n override handleStartDragging(\n event: StartDraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n if (event.picks.length) {\n event.cancelPan();\n }\n\n const picks = event.picks ?? [];\n const matchers = this.getHandleMatchers();\n\n // Find the first matcher that matches any pick\n for (const matcher of matchers) {\n if (picks.some(matcher.match)) {\n this.activeDragMode = matcher.mode;\n break;\n }\n }\n\n // Fall back to default mode if no matcher matched\n if (!this.activeDragMode) {\n this.activeDragMode = this.getDefaultMode();\n }\n\n this.activeDragMode.handleStartDragging(event, props);\n }\n\n /**\n * Delegates dragging to the active mode with Shift key handling.\n * Reads Shift state from the source event and applies configured modifiers.\n */\n override handleDragging(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n if (!this.activeDragMode) {\n return;\n }\n\n const sourceEvent = event.sourceEvent as KeyboardEvent | undefined;\n this.isShiftHeld = sourceEvent?.shiftKey ?? false;\n\n // Find the matcher for the active mode to get shift config\n const matchers = this.getHandleMatchers();\n const activeMatcher = matchers.find((m) => m.mode === this.activeDragMode);\n const shiftConfig = activeMatcher?.shiftConfig;\n\n // Apply shift key modifier if configured\n if (shiftConfig && this.isShiftHeld) {\n const propsWithConfig: ModeProps<FeatureCollection> = {\n ...props,\n modeConfig: {\n ...props.modeConfig,\n [shiftConfig.configKey]: shiftConfig.value ?? true,\n },\n };\n this.activeDragMode.handleDragging(event, propsWithConfig);\n } else {\n this.activeDragMode.handleDragging(event, props);\n }\n\n // Call subclass hook for tooltips or other side effects\n this.onDragging?.(event, props);\n }\n\n /**\n * Delegates stop dragging to the active mode with final Shift state.\n * Resets all drag-related state.\n */\n override handleStopDragging(\n event: StopDraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n if (!this.activeDragMode) {\n return;\n }\n\n // Find the matcher for the active mode to get shift config\n const matchers = this.getHandleMatchers();\n const activeMatcher = matchers.find((m) => m.mode === this.activeDragMode);\n const shiftConfig = activeMatcher?.shiftConfig;\n\n // Apply shift key modifier if configured (use last known state)\n if (shiftConfig && this.isShiftHeld) {\n const propsWithConfig: ModeProps<FeatureCollection> = {\n ...props,\n modeConfig: {\n ...props.modeConfig,\n [shiftConfig.configKey]: shiftConfig.value ?? true,\n },\n };\n this.activeDragMode.handleStopDragging(event, propsWithConfig);\n } else {\n this.activeDragMode.handleStopDragging(event, props);\n }\n\n this.resetDragState();\n }\n\n /**\n * Returns tooltips for display during drag operations.\n * Subclasses update `this.tooltip` in their `onDragging` hook.\n */\n override getTooltips(): Tooltip[] {\n return this.tooltip ? [this.tooltip] : [];\n }\n\n /**\n * Filters picks to prevent TypeError from sublayer elements without geometry.\n *\n * Some child modes (like ModifyMode, ResizeCircleMode) access\n * `pick.object.geometry.type` which throws if the pick doesn't have\n * a geometry property. This happens when picks include sublayer elements\n * like tooltip text that aren't GeoJSON features.\n */\n override getGuides(\n props: ModeProps<FeatureCollection>,\n ): GuideFeatureCollection {\n const picks = props.lastPointerMoveEvent?.picks;\n\n if (picks && picks.length > 0) {\n const { filteredPicks, didFilter } = filterGeometryAwarePicks(picks);\n\n if (didFilter) {\n const filteredProps: ModeProps<FeatureCollection> = {\n ...props,\n lastPointerMoveEvent: {\n ...props.lastPointerMoveEvent,\n picks: filteredPicks,\n },\n };\n return super.getGuides(filteredProps);\n }\n }\n\n return super.getGuides(props);\n }\n\n /**\n * Resets drag-related state. Called after drag stops.\n * Subclasses can override to reset additional state but should call super.\n */\n protected resetDragState(): void {\n this.activeDragMode = null;\n this.isShiftHeld = false;\n this.tooltip = null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyHA,IAAsB,oBAAtB,cAAgD,cAAc;;CAE5D,AAAU,iBAAyC;;CAGnD,AAAU,cAAc;;CAGxB,AAAU,UAA0B;;;;;CA4BpC,AAAS,kBACP,OACA,OACA;EACA,IAAIA,gBAA2C;AAE/C,QAAM,kBAAkB,OAAO;GAC7B,GAAG;GACH,iBAAiB,WAAsC;AACrD,oBAAgB,UAAU;;GAE7B,CAAC;AAEF,QAAM,eAAe,cAAc;;;;;;CAOrC,AAAS,oBACP,OACA,OACA;AACA,MAAI,MAAM,MAAM,OACd,OAAM,WAAW;EAGnB,MAAM,QAAQ,MAAM,SAAS,EAAE;EAC/B,MAAM,WAAW,KAAK,mBAAmB;AAGzC,OAAK,MAAM,WAAW,SACpB,KAAI,MAAM,KAAK,QAAQ,MAAM,EAAE;AAC7B,QAAK,iBAAiB,QAAQ;AAC9B;;AAKJ,MAAI,CAAC,KAAK,eACR,MAAK,iBAAiB,KAAK,gBAAgB;AAG7C,OAAK,eAAe,oBAAoB,OAAO,MAAM;;;;;;CAOvD,AAAS,eACP,OACA,OACA;AACA,MAAI,CAAC,KAAK,eACR;AAIF,OAAK,cADe,MAAM,aACM,YAAY;EAK5C,MAAM,cAFW,KAAK,mBAAmB,CACV,MAAM,MAAM,EAAE,SAAS,KAAK,eAAe,EACvC;AAGnC,MAAI,eAAe,KAAK,aAAa;GACnC,MAAMC,kBAAgD;IACpD,GAAG;IACH,YAAY;KACV,GAAG,MAAM;MACR,YAAY,YAAY,YAAY,SAAS;KAC/C;IACF;AACD,QAAK,eAAe,eAAe,OAAO,gBAAgB;QAE1D,MAAK,eAAe,eAAe,OAAO,MAAM;AAIlD,OAAK,aAAa,OAAO,MAAM;;;;;;CAOjC,AAAS,mBACP,OACA,OACA;AACA,MAAI,CAAC,KAAK,eACR;EAMF,MAAM,cAFW,KAAK,mBAAmB,CACV,MAAM,MAAM,EAAE,SAAS,KAAK,eAAe,EACvC;AAGnC,MAAI,eAAe,KAAK,aAAa;GACnC,MAAMA,kBAAgD;IACpD,GAAG;IACH,YAAY;KACV,GAAG,MAAM;MACR,YAAY,YAAY,YAAY,SAAS;KAC/C;IACF;AACD,QAAK,eAAe,mBAAmB,OAAO,gBAAgB;QAE9D,MAAK,eAAe,mBAAmB,OAAO,MAAM;AAGtD,OAAK,gBAAgB;;;;;;CAOvB,AAAS,cAAyB;AAChC,SAAO,KAAK,UAAU,CAAC,KAAK,QAAQ,GAAG,EAAE;;;;;;;;;;CAW3C,AAAS,UACP,OACwB;EACxB,MAAM,QAAQ,MAAM,sBAAsB;AAE1C,MAAI,SAAS,MAAM,SAAS,GAAG;GAC7B,MAAM,EAAE,eAAe,cAAc,yBAAyB,MAAM;AAEpE,OAAI,WAAW;IACb,MAAMC,gBAA8C;KAClD,GAAG;KACH,sBAAsB;MACpB,GAAG,MAAM;MACT,OAAO;MACR;KACF;AACD,WAAO,MAAM,UAAU,cAAc;;;AAIzC,SAAO,MAAM,UAAU,MAAM;;;;;;CAO/B,AAAU,iBAAuB;AAC/B,OAAK,iBAAiB;AACtB,OAAK,cAAc;AACnB,OAAK,UAAU"}
|
|
@@ -28,6 +28,7 @@ import { featureCollection } from "@turf/helpers";
|
|
|
28
28
|
* editing is not meaningful or desired. Instead, shapes are manipulated via their
|
|
29
29
|
* bounding box handles.
|
|
30
30
|
*
|
|
31
|
+
* ## Capabilities
|
|
31
32
|
* This composite mode provides:
|
|
32
33
|
* - **Translation** (TranslateMode): Drag the shape body to move it
|
|
33
34
|
* - **Scaling** (ScaleModeWithFreeTransform): Drag corner handles to resize
|
|
@@ -38,12 +39,33 @@ import { featureCollection } from "@turf/helpers";
|
|
|
38
39
|
* - With Shift: Snap to 45° intervals
|
|
39
40
|
* - **Live tooltip**: Shows dimensions and area during scaling
|
|
40
41
|
*
|
|
42
|
+
* ## Differences from VertexTransformMode
|
|
41
43
|
* Unlike VertexTransformMode, this mode does NOT include vertex editing handles.
|
|
44
|
+
* This prevents accidental distortion of shapes that have specific geometric constraints
|
|
45
|
+
* (e.g., ellipses must maintain their elliptical shape, rectangles must maintain right angles).
|
|
42
46
|
*
|
|
43
|
-
* Priority
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
+
* ## Handle Priority Logic
|
|
48
|
+
* When drag starts, modes are evaluated in this priority order:
|
|
49
|
+
* 1. If hovering over a scale handle → scaling takes priority
|
|
50
|
+
* 2. If hovering over the rotate handle → rotation takes priority
|
|
51
|
+
* 3. Otherwise → dragging the shape body translates it
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* import { BoundingTransformMode } from '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode';
|
|
56
|
+
* import { EditableGeoJsonLayer } from '@deck.gl-community/editable-layers';
|
|
57
|
+
*
|
|
58
|
+
* // Used internally by EditShapeLayer for rectangles and ellipses
|
|
59
|
+
* const mode = new BoundingTransformMode();
|
|
60
|
+
*
|
|
61
|
+
* const layer = new EditableGeoJsonLayer({
|
|
62
|
+
* mode,
|
|
63
|
+
* data: rectangleFeatureCollection,
|
|
64
|
+
* selectedFeatureIndexes: [0],
|
|
65
|
+
* onEdit: handleEdit,
|
|
66
|
+
* // ... other props
|
|
67
|
+
* });
|
|
68
|
+
* ```
|
|
47
69
|
*/
|
|
48
70
|
var BoundingTransformMode = class extends BaseTransformMode {
|
|
49
71
|
translateMode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bounding-transform-mode.js","names":["guidesToFilterOut: string[]","text: string"],"sources":["../../../../../src/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n type DraggingEvent,\n type FeatureCollection,\n type GeoJsonEditMode,\n type GuideFeatureCollection,\n type ModeProps,\n TranslateMode,\n} from '@deck.gl-community/editable-layers';\nimport { featureCollection } from '@turf/helpers';\nimport {\n DEFAULT_DISTANCE_UNITS,\n getDistanceUnitAbbreviation,\n} from '@/shared/units';\nimport {\n formatEllipseTooltip,\n formatRectangleTooltip,\n} from '../../shared/constants';\nimport {\n computeEllipseMeasurementsFromPolygon,\n computeRectangleMeasurementsFromCorners,\n} from '../../shared/utils/geometry-measurements';\nimport { BaseTransformMode, type HandleMatcher } from './base-transform-mode';\nimport { RotateModeWithSnap } from './rotate-mode-with-snap';\nimport { ScaleModeWithFreeTransform } from './scale-mode-with-free-transform';\nimport type { Feature, Polygon } from 'geojson';\n\n/**\n * Transform mode for shapes that use bounding box manipulation (no vertex editing).\n *\n * Use this mode for shapes like ellipses and rectangles where individual vertex\n * editing is not meaningful or desired. Instead, shapes are manipulated via their\n * bounding box handles.\n *\n * 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 * ## Capabilities\n * This composite mode provides:\n * - **Translation** (TranslateMode): Drag the shape body to move it\n * - **Scaling** (ScaleModeWithFreeTransform): Drag corner handles to resize\n * - Default: Non-uniform scaling (can stretch/squish)\n * - With Shift: Uniform scaling (maintains aspect ratio)\n * - **Rotation** (RotateModeWithSnap): Drag top handle to rotate\n * - Default: Free rotation\n * - With Shift: Snap to 45° intervals\n * - **Live tooltip**: Shows dimensions and area during scaling\n *\n * ## Differences from VertexTransformMode\n * Unlike VertexTransformMode, this mode does NOT include vertex editing handles.\n * This prevents accidental distortion of shapes that have specific geometric constraints\n * (e.g., ellipses must maintain their elliptical shape, rectangles must maintain right angles).\n *\n * ## Handle Priority Logic\n * When drag starts, modes are evaluated in this priority order:\n * 1. If hovering over a scale handle → scaling takes priority\n * 2. If hovering over the rotate handle → rotation takes priority\n * 3. Otherwise → dragging the shape body translates it\n *\n * @example\n * ```typescript\n * import { BoundingTransformMode } from '@accelint/map-toolkit/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode';\n * import { EditableGeoJsonLayer } from '@deck.gl-community/editable-layers';\n *\n * // Used internally by EditShapeLayer for rectangles and ellipses\n * const mode = new BoundingTransformMode();\n *\n * const layer = new EditableGeoJsonLayer({\n * mode,\n * data: rectangleFeatureCollection,\n * selectedFeatureIndexes: [0],\n * onEdit: handleEdit,\n * // ... other props\n * });\n * ```\n */\nexport class BoundingTransformMode extends BaseTransformMode {\n private translateMode: TranslateMode;\n private scaleMode: ScaleModeWithFreeTransform;\n private rotateMode: RotateModeWithSnap;\n\n constructor() {\n const translateMode = new TranslateMode();\n const scaleMode = new ScaleModeWithFreeTransform();\n const rotateMode = new RotateModeWithSnap();\n\n // Order: scale and rotate first so their handles take priority over translate\n super([scaleMode, rotateMode, translateMode]);\n\n this.translateMode = translateMode;\n this.scaleMode = scaleMode;\n this.rotateMode = rotateMode;\n }\n\n protected override getHandleMatchers(): HandleMatcher[] {\n return [\n {\n // Scale handle: corner handles on bounding box\n match: (pick) =>\n Boolean(\n pick.isGuide && pick.object?.properties?.editHandleType === 'scale',\n ),\n mode: this.scaleMode,\n shiftConfig: { configKey: 'lockScaling' },\n },\n {\n // Rotate handle: top handle on bounding box\n match: (pick) =>\n Boolean(\n pick.isGuide &&\n pick.object?.properties?.editHandleType === 'rotate',\n ),\n mode: this.rotateMode,\n shiftConfig: { configKey: 'snapRotation' },\n },\n ];\n }\n\n protected override getDefaultMode(): GeoJsonEditMode {\n return this.translateMode;\n }\n\n /**\n * Update tooltip with shape dimensions during scaling.\n */\n protected override onDragging(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ): void {\n // Only show tooltip when scaling\n if (this.activeDragMode !== this.scaleMode) {\n return;\n }\n\n this.updateShapeTooltip(event, props);\n }\n\n /**\n * Override getGuides to filter duplicate envelope guides.\n *\n * Both ScaleMode and RotateMode render the same bounding box envelope.\n * We keep scale's envelope and filter rotate's duplicate.\n * We also hide scale handles while rotating to avoid visual clutter.\n */\n override getGuides(\n props: ModeProps<FeatureCollection>,\n ): GuideFeatureCollection {\n const allGuides = super.getGuides(props);\n\n // biome-ignore lint/suspicious/noExplicitAny: Guide properties vary by mode, safely accessing with optional chaining\n const filteredGuides = allGuides.features.filter((guide: any) => {\n const properties = guide.properties || {};\n const editHandleType = properties.editHandleType;\n const mode = properties.mode;\n\n // Both scale and rotate modes have the same enveloping box as a guide - only need one\n const guidesToFilterOut: string[] = [mode as string];\n\n // Do not render scaling edit handles if rotating\n if (this.rotateMode.getIsRotating()) {\n guidesToFilterOut.push(editHandleType as string);\n }\n\n return !guidesToFilterOut.includes('scale');\n });\n\n // biome-ignore lint/suspicious/noExplicitAny: turf types mismatch with editable-layers GeoJSON types\n return featureCollection(filteredGuides as any) as any;\n }\n\n /**\n * Update the tooltip with shape dimensions and area.\n * Called during scaling to show live measurements.\n * Handles both rectangles (5 points) and ellipses (many points).\n */\n private updateShapeTooltip(\n event: DraggingEvent,\n props: ModeProps<FeatureCollection>,\n ) {\n const { mapCoords } = event;\n const distanceUnits =\n props.modeConfig?.distanceUnits ?? DEFAULT_DISTANCE_UNITS;\n\n // Get the selected feature\n const selectedIndexes = props.selectedIndexes;\n const selectedIndex = selectedIndexes?.[0];\n if (selectedIndex === undefined) {\n this.tooltip = null;\n return;\n }\n\n const feature = props.data.features[selectedIndex] as\n | Feature<Polygon>\n | undefined;\n if (!feature || feature.geometry.type !== 'Polygon') {\n this.tooltip = null;\n return;\n }\n\n const coordinates = feature.geometry.coordinates[0];\n if (!coordinates || coordinates.length < 4) {\n this.tooltip = null;\n return;\n }\n\n // Check if this is a rectangle (has shape: 'Rectangle' property)\n const isRectangle = feature.properties?.shape === 'Rectangle';\n\n let text: string;\n const unitAbbrev = getDistanceUnitAbbreviation(distanceUnits);\n\n if (isRectangle) {\n // Rectangle: calculate width and height from corners\n const corner0 = coordinates[0] as [number, number];\n const corner1 = coordinates[1] as [number, number];\n const corner2 = coordinates[2] as [number, number];\n\n const { width, height, area } = computeRectangleMeasurementsFromCorners(\n corner0,\n corner1,\n corner2,\n distanceUnits,\n );\n\n text = formatRectangleTooltip(width, height, area, unitAbbrev);\n } else {\n // Ellipse: calculate major/minor axes using consolidated utility\n const {\n majorAxis,\n minorAxis,\n area: ellipseArea,\n } = computeEllipseMeasurementsFromPolygon(\n coordinates as [number, number][],\n distanceUnits,\n );\n\n text = formatEllipseTooltip(\n majorAxis,\n minorAxis,\n ellipseArea,\n unitAbbrev,\n );\n }\n\n // Position tooltip at cursor - offset is applied via getPixelOffset in sublayer props\n this.tooltip = {\n position: mapCoords,\n text,\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,IAAa,wBAAb,cAA2C,kBAAkB;CAC3D,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,cAAc;EACZ,MAAM,gBAAgB,IAAI,eAAe;EACzC,MAAM,YAAY,IAAI,4BAA4B;EAClD,MAAM,aAAa,IAAI,oBAAoB;AAG3C,QAAM;GAAC;GAAW;GAAY;GAAc,CAAC;AAE7C,OAAK,gBAAgB;AACrB,OAAK,YAAY;AACjB,OAAK,aAAa;;CAGpB,AAAmB,oBAAqC;AACtD,SAAO,CACL;GAEE,QAAQ,SACN,QACE,KAAK,WAAW,KAAK,QAAQ,YAAY,mBAAmB,QAC7D;GACH,MAAM,KAAK;GACX,aAAa,EAAE,WAAW,eAAe;GAC1C,EACD;GAEE,QAAQ,SACN,QACE,KAAK,WACH,KAAK,QAAQ,YAAY,mBAAmB,SAC/C;GACH,MAAM,KAAK;GACX,aAAa,EAAE,WAAW,gBAAgB;GAC3C,CACF;;CAGH,AAAmB,iBAAkC;AACnD,SAAO,KAAK;;;;;CAMd,AAAmB,WACjB,OACA,OACM;AAEN,MAAI,KAAK,mBAAmB,KAAK,UAC/B;AAGF,OAAK,mBAAmB,OAAO,MAAM;;;;;;;;;CAUvC,AAAS,UACP,OACwB;AAqBxB,SAAO,kBApBW,MAAM,UAAU,MAAM,CAGP,SAAS,QAAQ,UAAe;GAC/D,MAAM,aAAa,MAAM,cAAc,EAAE;GACzC,MAAM,iBAAiB,WAAW;GAIlC,MAAMA,oBAA8B,CAHvB,WAAW,KAG4B;AAGpD,OAAI,KAAK,WAAW,eAAe,CACjC,mBAAkB,KAAK,eAAyB;AAGlD,UAAO,CAAC,kBAAkB,SAAS,QAAQ;IAC3C,CAG6C;;;;;;;CAQjD,AAAQ,mBACN,OACA,OACA;EACA,MAAM,EAAE,cAAc;EACtB,MAAM,gBACJ,MAAM,YAAY,iBAAiB;EAIrC,MAAM,gBADkB,MAAM,kBACU;AACxC,MAAI,kBAAkB,QAAW;AAC/B,QAAK,UAAU;AACf;;EAGF,MAAM,UAAU,MAAM,KAAK,SAAS;AAGpC,MAAI,CAAC,WAAW,QAAQ,SAAS,SAAS,WAAW;AACnD,QAAK,UAAU;AACf;;EAGF,MAAM,cAAc,QAAQ,SAAS,YAAY;AACjD,MAAI,CAAC,eAAe,YAAY,SAAS,GAAG;AAC1C,QAAK,UAAU;AACf;;EAIF,MAAM,cAAc,QAAQ,YAAY,UAAU;EAElD,IAAIC;EACJ,MAAM,aAAa,4BAA4B,cAAc;AAE7D,MAAI,aAAa;GAEf,MAAM,UAAU,YAAY;GAC5B,MAAM,UAAU,YAAY;GAC5B,MAAM,UAAU,YAAY;GAE5B,MAAM,EAAE,OAAO,QAAQ,SAAS,wCAC9B,SACA,SACA,SACA,cACD;AAED,UAAO,uBAAuB,OAAO,QAAQ,MAAM,WAAW;SACzD;GAEL,MAAM,EACJ,WACA,WACA,MAAM,gBACJ,sCACF,aACA,cACD;AAED,UAAO,qBACL,WACA,WACA,aACA,WACD;;AAIH,OAAK,UAAU;GACb,UAAU;GACV;GACD"}
|