@accelint/map-toolkit 1.1.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 +17 -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 +21 -6
- 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 +10 -0
- 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 +30 -0
- package/dist/deckgl/base-map/index.js.map +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/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 +137 -16
- 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 +55 -5
- 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 +7 -7
|
@@ -40,7 +40,21 @@ type CursorActions = {
|
|
|
40
40
|
*/
|
|
41
41
|
declare const cursorStore: MapStore<CursorState, CursorActions>;
|
|
42
42
|
/**
|
|
43
|
-
* Get effective cursor
|
|
43
|
+
* Get effective cursor value for a map instance.
|
|
44
|
+
*
|
|
45
|
+
* Computes the effective cursor from the current state by applying
|
|
46
|
+
* the priority system (mode owner > most recent > default).
|
|
47
|
+
*
|
|
48
|
+
* @param mapId - Unique identifier for the map instance
|
|
49
|
+
* @returns The effective cursor to display
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* import { getCursor } from '@accelint/map-toolkit/map-cursor';
|
|
54
|
+
*
|
|
55
|
+
* const currentCursor = getCursor(mapId);
|
|
56
|
+
* console.log('Current cursor:', currentCursor); // 'default', 'pointer', etc.
|
|
57
|
+
* ```
|
|
44
58
|
*/
|
|
45
59
|
declare function getCursor(mapId: UniqueId): CSSCursorType;
|
|
46
60
|
/**
|
|
@@ -53,10 +67,28 @@ declare function getCursor(mapId: UniqueId): CSSCursorType;
|
|
|
53
67
|
* - Better ergonomics for consumers
|
|
54
68
|
*
|
|
55
69
|
* This hook exists for internal composition (used by useMapCursor).
|
|
70
|
+
*
|
|
71
|
+
* @param mapId - Unique identifier for the map instance
|
|
72
|
+
* @returns The effective cursor to display
|
|
56
73
|
*/
|
|
57
74
|
declare function useCursor(mapId: UniqueId): CSSCursorType;
|
|
58
75
|
/**
|
|
59
|
-
* Clear cursor state
|
|
76
|
+
* Clear cursor state for a specific map instance.
|
|
77
|
+
*
|
|
78
|
+
* Removes all cursor ownership data and resets to default state.
|
|
79
|
+
* This is typically not needed as cleanup happens automatically.
|
|
80
|
+
* Use only in advanced scenarios where manual cleanup is required.
|
|
81
|
+
*
|
|
82
|
+
* @param mapId - Unique identifier for the map instance to clear
|
|
83
|
+
* @returns void
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* import { clearCursorState } from '@accelint/map-toolkit/map-cursor';
|
|
88
|
+
*
|
|
89
|
+
* // Manual cleanup when destroying a map
|
|
90
|
+
* clearCursorState(mapId);
|
|
91
|
+
* ```
|
|
60
92
|
*/
|
|
61
93
|
declare function clearCursorState(mapId: UniqueId): void;
|
|
62
94
|
//#endregion
|
package/dist/map-cursor/store.js
CHANGED
|
@@ -73,7 +73,16 @@ function isRegisteredOwner(mapId, owner) {
|
|
|
73
73
|
return isRegisteredModeOwnerFn?.(mapId, owner) ?? false;
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
|
-
* Calculate effective cursor based on priority
|
|
76
|
+
* Calculate effective cursor based on priority.
|
|
77
|
+
*
|
|
78
|
+
* Priority order:
|
|
79
|
+
* 1. Mode owner's cursor (if mode is owned)
|
|
80
|
+
* 2. Most recent cursor request (if owner still has entry)
|
|
81
|
+
* 3. Default cursor
|
|
82
|
+
*
|
|
83
|
+
* @param mapId - Unique identifier for the map instance
|
|
84
|
+
* @param state - Current cursor state
|
|
85
|
+
* @returns The effective cursor to display
|
|
77
86
|
*/
|
|
78
87
|
function getEffectiveCursor(mapId, state) {
|
|
79
88
|
const modeOwner = getModeOwner?.(mapId);
|
|
@@ -191,7 +200,21 @@ const cursorStore = createMapStore({
|
|
|
191
200
|
}
|
|
192
201
|
});
|
|
193
202
|
/**
|
|
194
|
-
* Get effective cursor
|
|
203
|
+
* Get effective cursor value for a map instance.
|
|
204
|
+
*
|
|
205
|
+
* Computes the effective cursor from the current state by applying
|
|
206
|
+
* the priority system (mode owner > most recent > default).
|
|
207
|
+
*
|
|
208
|
+
* @param mapId - Unique identifier for the map instance
|
|
209
|
+
* @returns The effective cursor to display
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* import { getCursor } from '@accelint/map-toolkit/map-cursor';
|
|
214
|
+
*
|
|
215
|
+
* const currentCursor = getCursor(mapId);
|
|
216
|
+
* console.log('Current cursor:', currentCursor); // 'default', 'pointer', etc.
|
|
217
|
+
* ```
|
|
195
218
|
*/
|
|
196
219
|
function getCursor(mapId) {
|
|
197
220
|
return getEffectiveCursor(mapId, cursorStore.get(mapId));
|
|
@@ -206,12 +229,30 @@ function getCursor(mapId) {
|
|
|
206
229
|
* - Better ergonomics for consumers
|
|
207
230
|
*
|
|
208
231
|
* This hook exists for internal composition (used by useMapCursor).
|
|
232
|
+
*
|
|
233
|
+
* @param mapId - Unique identifier for the map instance
|
|
234
|
+
* @returns The effective cursor to display
|
|
209
235
|
*/
|
|
210
236
|
function useCursor(mapId) {
|
|
211
237
|
return cursorStore.useSelector(mapId, (state) => getEffectiveCursor(mapId, state));
|
|
212
238
|
}
|
|
213
239
|
/**
|
|
214
|
-
* Clear cursor state
|
|
240
|
+
* Clear cursor state for a specific map instance.
|
|
241
|
+
*
|
|
242
|
+
* Removes all cursor ownership data and resets to default state.
|
|
243
|
+
* This is typically not needed as cleanup happens automatically.
|
|
244
|
+
* Use only in advanced scenarios where manual cleanup is required.
|
|
245
|
+
*
|
|
246
|
+
* @param mapId - Unique identifier for the map instance to clear
|
|
247
|
+
* @returns void
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```typescript
|
|
251
|
+
* import { clearCursorState } from '@accelint/map-toolkit/map-cursor';
|
|
252
|
+
*
|
|
253
|
+
* // Manual cleanup when destroying a map
|
|
254
|
+
* clearCursorState(mapId);
|
|
255
|
+
* ```
|
|
215
256
|
*/
|
|
216
257
|
function clearCursorState(mapId) {
|
|
217
258
|
cursorStore.clear(mapId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","names":["DEFAULT_CURSOR: CSSCursorType","getModeOwnerFn: ((mapId: UniqueId) => string | undefined) | null","isRegisteredModeOwnerFn:\n | ((mapId: UniqueId, owner: string) => boolean)\n | null","importPromise: Promise<void> | null","updates: Partial<CursorState>","newState: CursorState"],"sources":["../../src/map-cursor/store.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n/**\n * Map Cursor Store\n *\n * Manages cursor state with ownership-based priority.\n *\n * Priority order:\n * 1. Mode owner's cursor (if mode is owned)\n * 2. Most recent cursor request\n * 3. Default cursor\n *\n * @example\n * ```tsx\n * import { cursorStore } from '@accelint/map-toolkit/map-cursor';\n *\n * function MapContainer({ mapId }) {\n * const cursor = cursorStore.useSelector(mapId, (s) => getEffectiveCursor(mapId, s));\n * return <div style={{ cursor }}>...</div>;\n * }\n *\n * // Request cursor change from a layer:\n * cursorStore.actions(mapId).requestCursorChange('crosshair', 'draw-layer');\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { MapModeEvents } from '../map-mode/events';\nimport { createMapStore, mapDelete, mapSet } from '../shared/create-map-store';\nimport { MapCursorEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { ModeChangedEvent } from '../map-mode/types';\nimport type { CSSCursorType, MapCursorEventType } from './types';\n\nconst DEFAULT_CURSOR: CSSCursorType = 'default';\n\n/**\n * State shape for map cursor\n */\ntype CursorState = {\n /** Map of owner -> cursor */\n cursorOwners: Map<string, CSSCursorType>;\n /** Current active cursor (for priority tracking) */\n currentCursor: CSSCursorType | null;\n /** Current cursor owner (for priority tracking) */\n currentOwner: string | null;\n};\n\n/**\n * Actions for cursor management\n */\ntype CursorActions = {\n /** Request a cursor change */\n requestCursorChange: (cursor: CSSCursorType, owner: string) => void;\n /** Clear cursor for an owner */\n clearCursor: (owner: string) => void;\n};\n\nconst cursorBus = Broadcast.getInstance<MapCursorEventType>();\nconst modeBus = Broadcast.getInstance<ModeChangedEvent>();\n\n/**\n * Lazy import to avoid circular dependency between cursor and mode stores.\n * The mode store doesn't depend on cursor store, but importing it synchronously\n * at module load time can cause initialization order issues.\n *\n * This pattern ensures the import is resolved before first use (bus listeners\n * are set up on first subscriber, not at module load time).\n */\nlet getModeOwnerFn: ((mapId: UniqueId) => string | undefined) | null = null;\nlet isRegisteredModeOwnerFn:\n | ((mapId: UniqueId, owner: string) => boolean)\n | null = null;\nlet importPromise: Promise<void> | null = null;\nlet importFailed = false;\n\nfunction ensureModeStoreImported(): void {\n if (getModeOwnerFn !== null || importFailed) {\n return;\n }\n if (importPromise === null) {\n importPromise = import('../map-mode/store')\n .then((mod) => {\n getModeOwnerFn = mod.getCurrentModeOwner;\n isRegisteredModeOwnerFn = mod.isRegisteredModeOwner;\n })\n .catch((error) => {\n importFailed = true;\n // Log error in development only - in production this is a silent fallback\n if (process.env.NODE_ENV !== 'production') {\n console.error('[MapCursor] Failed to import mode store:', error);\n }\n });\n }\n}\n\n// Start the import immediately so it's likely resolved by first use\nensureModeStoreImported();\n\nfunction getModeOwner(mapId: UniqueId): string | undefined {\n // getModeOwnerFn will be available by the time bus listeners are set up\n // (which happens on first React subscriber, not at module load)\n return getModeOwnerFn?.(mapId);\n}\n\nfunction isRegisteredOwner(mapId: UniqueId, owner: string): boolean {\n return isRegisteredModeOwnerFn?.(mapId, owner) ?? false;\n}\n\n/**\n * Calculate effective cursor based on priority\n */\nfunction getEffectiveCursor(\n mapId: UniqueId,\n state: CursorState,\n): CSSCursorType {\n // Priority 1: Mode owner's cursor\n const modeOwner = getModeOwner?.(mapId);\n if (modeOwner) {\n const modeOwnerCursor = state.cursorOwners.get(modeOwner);\n if (modeOwnerCursor) {\n return modeOwnerCursor;\n }\n }\n\n // Priority 2: Current cursor (if owner still has entry)\n if (state.currentCursor && state.currentOwner) {\n if (state.cursorOwners.has(state.currentOwner)) {\n return state.currentCursor;\n }\n }\n\n // Priority 3: Default\n return DEFAULT_CURSOR;\n}\n\n/**\n * Cursor store\n */\nexport const cursorStore = createMapStore<CursorState, CursorActions>({\n defaultState: {\n cursorOwners: new Map(),\n currentCursor: null,\n currentOwner: null,\n },\n\n actions: (mapId, { get, set }) => ({\n requestCursorChange: (cursor: CSSCursorType, owner: string) => {\n const trimmedCursor = cursor.trim() as CSSCursorType;\n const trimmedOwner = owner.trim();\n\n if (!trimmedCursor) {\n throw new Error('requestCursorChange requires non-empty cursor');\n }\n if (!trimmedOwner) {\n throw new Error('requestCursorChange requires non-empty owner');\n }\n\n cursorBus.emit(MapCursorEvents.changeRequest, {\n cursor: trimmedCursor,\n owner: trimmedOwner,\n id: mapId,\n });\n },\n\n clearCursor: (owner: string) => {\n const state = get();\n const hadCursor = state.cursorOwners.has(owner);\n\n if (hadCursor) {\n // Clear current tracking if this was the owner\n const updates: Partial<CursorState> = {\n cursorOwners: mapDelete(state.cursorOwners, owner),\n };\n if (state.currentOwner === owner) {\n updates.currentCursor = null;\n updates.currentOwner = null;\n }\n\n set(updates);\n }\n },\n }),\n\n bus: (mapId, { get, set }) => {\n // Handle cursor change requests\n const unsubRequest = cursorBus.on(\n MapCursorEvents.changeRequest,\n (event) => {\n const { cursor, owner: requestOwner, id } = event.payload;\n if (id !== mapId) {\n return;\n }\n\n const state = get();\n const previousCursor = getEffectiveCursor(mapId, state);\n\n // Skip if same cursor already stored for this owner\n if (state.cursorOwners.get(requestOwner) === cursor) {\n return;\n }\n\n // Create new Map for immutable update\n const newCursorOwners = mapSet(\n state.cursorOwners,\n requestOwner,\n cursor,\n );\n\n // Check ownership\n const currentModeOwner = getModeOwner?.(mapId);\n const isOwnerless = !currentModeOwner;\n const isCurrentModeOwner = requestOwner === currentModeOwner;\n const isAnyModeOwner = isRegisteredOwner(mapId, requestOwner);\n\n if (isOwnerless || isCurrentModeOwner) {\n // Accept: apply cursor with immutable update\n const newState: CursorState = {\n cursorOwners: newCursorOwners,\n currentCursor: cursor,\n currentOwner: requestOwner,\n };\n\n // Calculate new cursor with updated state\n const newCursor = getEffectiveCursor(mapId, newState);\n\n if (previousCursor !== newCursor) {\n set(newState);\n cursorBus.emit(MapCursorEvents.changed, {\n previousCursor,\n currentCursor: newCursor,\n owner: requestOwner,\n id: mapId,\n });\n } else {\n // Still need to update state even if cursor didn't change visually\n set(newState);\n }\n } else if (isAnyModeOwner) {\n // Store but don't apply: requester owns a different mode (pending or not current).\n // When their mode becomes active, getEffectiveCursor will find their cursor.\n set({ cursorOwners: newCursorOwners });\n\n cursorBus.emit(MapCursorEvents.rejected, {\n rejectedCursor: cursor,\n rejectedOwner: requestOwner,\n currentOwner: state.currentOwner || currentModeOwner || 'unknown',\n reason: 'not-current-owner',\n id: mapId,\n });\n } else {\n // Reject: don't store. Non-owners should only set cursor in default mode.\n cursorBus.emit(MapCursorEvents.rejected, {\n rejectedCursor: cursor,\n rejectedOwner: requestOwner,\n currentOwner: state.currentOwner || currentModeOwner || 'unknown',\n reason: 'not-owner',\n id: mapId,\n });\n }\n },\n );\n\n // Handle mode changes\n const unsubMode = modeBus.on(MapModeEvents.changed, (event) => {\n if (event.payload.id !== mapId) {\n return;\n }\n\n const state = get();\n const previousCursor = getEffectiveCursor(mapId, state);\n\n // Clear current tracking on mode change with immutable update\n if (\n event.payload.currentMode === 'default' ||\n event.payload.previousMode !== event.payload.currentMode\n ) {\n set({\n currentCursor: null,\n currentOwner: null,\n });\n }\n\n // Defer check until new mode owner is registered\n queueMicrotask(() => {\n // Re-get state after microtask since it may have changed\n const currentState = get();\n const newCursor = getEffectiveCursor(mapId, currentState);\n if (previousCursor !== newCursor) {\n const newModeOwner = getModeOwner?.(mapId) || 'system';\n cursorBus.emit(MapCursorEvents.changed, {\n previousCursor,\n currentCursor: newCursor,\n owner: newModeOwner,\n id: mapId,\n });\n }\n });\n });\n\n return () => {\n unsubRequest();\n unsubMode();\n };\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Get effective cursor (computed from state)\n */\nexport function getCursor(mapId: UniqueId): CSSCursorType {\n return getEffectiveCursor(mapId, cursorStore.get(mapId));\n}\n\n/**\n * Hook for effective cursor value.\n *\n * **Internal use only** - not exported from the public API.\n * Use `useMapCursor` instead, which provides:\n * - MapContext integration (auto-resolves mapId inside MapProvider)\n * - Actions (requestCursorChange, clearCursor)\n * - Better ergonomics for consumers\n *\n * This hook exists for internal composition (used by useMapCursor).\n */\nexport function useCursor(mapId: UniqueId): CSSCursorType {\n return cursorStore.useSelector(mapId, (state) =>\n getEffectiveCursor(mapId, state),\n );\n}\n\n/**\n * Clear cursor state\n */\nexport function clearCursorState(mapId: UniqueId): void {\n cursorStore.clear(mapId);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"store.js","names":["DEFAULT_CURSOR: CSSCursorType","getModeOwnerFn: ((mapId: UniqueId) => string | undefined) | null","isRegisteredModeOwnerFn:\n | ((mapId: UniqueId, owner: string) => boolean)\n | null","importPromise: Promise<void> | null","updates: Partial<CursorState>","newState: CursorState"],"sources":["../../src/map-cursor/store.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n/**\n * Map Cursor Store\n *\n * Manages cursor state with ownership-based priority.\n *\n * Priority order:\n * 1. Mode owner's cursor (if mode is owned)\n * 2. Most recent cursor request\n * 3. Default cursor\n *\n * @example\n * ```tsx\n * import { cursorStore } from '@accelint/map-toolkit/map-cursor';\n *\n * function MapContainer({ mapId }) {\n * const cursor = cursorStore.useSelector(mapId, (s) => getEffectiveCursor(mapId, s));\n * return <div style={{ cursor }}>...</div>;\n * }\n *\n * // Request cursor change from a layer:\n * cursorStore.actions(mapId).requestCursorChange('crosshair', 'draw-layer');\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { MapModeEvents } from '../map-mode/events';\nimport { createMapStore, mapDelete, mapSet } from '../shared/create-map-store';\nimport { MapCursorEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { ModeChangedEvent } from '../map-mode/types';\nimport type { CSSCursorType, MapCursorEventType } from './types';\n\nconst DEFAULT_CURSOR: CSSCursorType = 'default';\n\n/**\n * State shape for map cursor\n */\ntype CursorState = {\n /** Map of owner -> cursor */\n cursorOwners: Map<string, CSSCursorType>;\n /** Current active cursor (for priority tracking) */\n currentCursor: CSSCursorType | null;\n /** Current cursor owner (for priority tracking) */\n currentOwner: string | null;\n};\n\n/**\n * Actions for cursor management\n */\ntype CursorActions = {\n /** Request a cursor change */\n requestCursorChange: (cursor: CSSCursorType, owner: string) => void;\n /** Clear cursor for an owner */\n clearCursor: (owner: string) => void;\n};\n\nconst cursorBus = Broadcast.getInstance<MapCursorEventType>();\nconst modeBus = Broadcast.getInstance<ModeChangedEvent>();\n\n/**\n * Lazy import to avoid circular dependency between cursor and mode stores.\n * The mode store doesn't depend on cursor store, but importing it synchronously\n * at module load time can cause initialization order issues.\n *\n * This pattern ensures the import is resolved before first use (bus listeners\n * are set up on first subscriber, not at module load time).\n */\nlet getModeOwnerFn: ((mapId: UniqueId) => string | undefined) | null = null;\nlet isRegisteredModeOwnerFn:\n | ((mapId: UniqueId, owner: string) => boolean)\n | null = null;\nlet importPromise: Promise<void> | null = null;\nlet importFailed = false;\n\nfunction ensureModeStoreImported(): void {\n if (getModeOwnerFn !== null || importFailed) {\n return;\n }\n if (importPromise === null) {\n importPromise = import('../map-mode/store')\n .then((mod) => {\n getModeOwnerFn = mod.getCurrentModeOwner;\n isRegisteredModeOwnerFn = mod.isRegisteredModeOwner;\n })\n .catch((error) => {\n importFailed = true;\n // Log error in development only - in production this is a silent fallback\n if (process.env.NODE_ENV !== 'production') {\n console.error('[MapCursor] Failed to import mode store:', error);\n }\n });\n }\n}\n\n// Start the import immediately so it's likely resolved by first use\nensureModeStoreImported();\n\nfunction getModeOwner(mapId: UniqueId): string | undefined {\n // getModeOwnerFn will be available by the time bus listeners are set up\n // (which happens on first React subscriber, not at module load)\n return getModeOwnerFn?.(mapId);\n}\n\nfunction isRegisteredOwner(mapId: UniqueId, owner: string): boolean {\n return isRegisteredModeOwnerFn?.(mapId, owner) ?? false;\n}\n\n/**\n * Calculate effective cursor based on priority.\n *\n * Priority order:\n * 1. Mode owner's cursor (if mode is owned)\n * 2. Most recent cursor request (if owner still has entry)\n * 3. Default cursor\n *\n * @param mapId - Unique identifier for the map instance\n * @param state - Current cursor state\n * @returns The effective cursor to display\n */\nfunction getEffectiveCursor(\n mapId: UniqueId,\n state: CursorState,\n): CSSCursorType {\n // Priority 1: Mode owner's cursor\n const modeOwner = getModeOwner?.(mapId);\n if (modeOwner) {\n const modeOwnerCursor = state.cursorOwners.get(modeOwner);\n if (modeOwnerCursor) {\n return modeOwnerCursor;\n }\n }\n\n // Priority 2: Current cursor (if owner still has entry)\n if (state.currentCursor && state.currentOwner) {\n if (state.cursorOwners.has(state.currentOwner)) {\n return state.currentCursor;\n }\n }\n\n // Priority 3: Default\n return DEFAULT_CURSOR;\n}\n\n/**\n * Cursor store\n */\nexport const cursorStore = createMapStore<CursorState, CursorActions>({\n defaultState: {\n cursorOwners: new Map(),\n currentCursor: null,\n currentOwner: null,\n },\n\n actions: (mapId, { get, set }) => ({\n requestCursorChange: (cursor: CSSCursorType, owner: string) => {\n const trimmedCursor = cursor.trim() as CSSCursorType;\n const trimmedOwner = owner.trim();\n\n if (!trimmedCursor) {\n throw new Error('requestCursorChange requires non-empty cursor');\n }\n if (!trimmedOwner) {\n throw new Error('requestCursorChange requires non-empty owner');\n }\n\n cursorBus.emit(MapCursorEvents.changeRequest, {\n cursor: trimmedCursor,\n owner: trimmedOwner,\n id: mapId,\n });\n },\n\n clearCursor: (owner: string) => {\n const state = get();\n const hadCursor = state.cursorOwners.has(owner);\n\n if (hadCursor) {\n // Clear current tracking if this was the owner\n const updates: Partial<CursorState> = {\n cursorOwners: mapDelete(state.cursorOwners, owner),\n };\n if (state.currentOwner === owner) {\n updates.currentCursor = null;\n updates.currentOwner = null;\n }\n\n set(updates);\n }\n },\n }),\n\n bus: (mapId, { get, set }) => {\n // Handle cursor change requests\n const unsubRequest = cursorBus.on(\n MapCursorEvents.changeRequest,\n (event) => {\n const { cursor, owner: requestOwner, id } = event.payload;\n if (id !== mapId) {\n return;\n }\n\n const state = get();\n const previousCursor = getEffectiveCursor(mapId, state);\n\n // Skip if same cursor already stored for this owner\n if (state.cursorOwners.get(requestOwner) === cursor) {\n return;\n }\n\n // Create new Map for immutable update\n const newCursorOwners = mapSet(\n state.cursorOwners,\n requestOwner,\n cursor,\n );\n\n // Check ownership\n const currentModeOwner = getModeOwner?.(mapId);\n const isOwnerless = !currentModeOwner;\n const isCurrentModeOwner = requestOwner === currentModeOwner;\n const isAnyModeOwner = isRegisteredOwner(mapId, requestOwner);\n\n if (isOwnerless || isCurrentModeOwner) {\n // Accept: apply cursor with immutable update\n const newState: CursorState = {\n cursorOwners: newCursorOwners,\n currentCursor: cursor,\n currentOwner: requestOwner,\n };\n\n // Calculate new cursor with updated state\n const newCursor = getEffectiveCursor(mapId, newState);\n\n if (previousCursor !== newCursor) {\n set(newState);\n cursorBus.emit(MapCursorEvents.changed, {\n previousCursor,\n currentCursor: newCursor,\n owner: requestOwner,\n id: mapId,\n });\n } else {\n // Still need to update state even if cursor didn't change visually\n set(newState);\n }\n } else if (isAnyModeOwner) {\n // Store but don't apply: requester owns a different mode (pending or not current).\n // When their mode becomes active, getEffectiveCursor will find their cursor.\n set({ cursorOwners: newCursorOwners });\n\n cursorBus.emit(MapCursorEvents.rejected, {\n rejectedCursor: cursor,\n rejectedOwner: requestOwner,\n currentOwner: state.currentOwner || currentModeOwner || 'unknown',\n reason: 'not-current-owner',\n id: mapId,\n });\n } else {\n // Reject: don't store. Non-owners should only set cursor in default mode.\n cursorBus.emit(MapCursorEvents.rejected, {\n rejectedCursor: cursor,\n rejectedOwner: requestOwner,\n currentOwner: state.currentOwner || currentModeOwner || 'unknown',\n reason: 'not-owner',\n id: mapId,\n });\n }\n },\n );\n\n // Handle mode changes\n const unsubMode = modeBus.on(MapModeEvents.changed, (event) => {\n if (event.payload.id !== mapId) {\n return;\n }\n\n const state = get();\n const previousCursor = getEffectiveCursor(mapId, state);\n\n // Clear current tracking on mode change with immutable update\n if (\n event.payload.currentMode === 'default' ||\n event.payload.previousMode !== event.payload.currentMode\n ) {\n set({\n currentCursor: null,\n currentOwner: null,\n });\n }\n\n // Defer check until new mode owner is registered\n queueMicrotask(() => {\n // Re-get state after microtask since it may have changed\n const currentState = get();\n const newCursor = getEffectiveCursor(mapId, currentState);\n if (previousCursor !== newCursor) {\n const newModeOwner = getModeOwner?.(mapId) || 'system';\n cursorBus.emit(MapCursorEvents.changed, {\n previousCursor,\n currentCursor: newCursor,\n owner: newModeOwner,\n id: mapId,\n });\n }\n });\n });\n\n return () => {\n unsubRequest();\n unsubMode();\n };\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Get effective cursor value for a map instance.\n *\n * Computes the effective cursor from the current state by applying\n * the priority system (mode owner > most recent > default).\n *\n * @param mapId - Unique identifier for the map instance\n * @returns The effective cursor to display\n *\n * @example\n * ```typescript\n * import { getCursor } from '@accelint/map-toolkit/map-cursor';\n *\n * const currentCursor = getCursor(mapId);\n * console.log('Current cursor:', currentCursor); // 'default', 'pointer', etc.\n * ```\n */\nexport function getCursor(mapId: UniqueId): CSSCursorType {\n return getEffectiveCursor(mapId, cursorStore.get(mapId));\n}\n\n/**\n * Hook for effective cursor value.\n *\n * **Internal use only** - not exported from the public API.\n * Use `useMapCursor` instead, which provides:\n * - MapContext integration (auto-resolves mapId inside MapProvider)\n * - Actions (requestCursorChange, clearCursor)\n * - Better ergonomics for consumers\n *\n * This hook exists for internal composition (used by useMapCursor).\n *\n * @param mapId - Unique identifier for the map instance\n * @returns The effective cursor to display\n */\nexport function useCursor(mapId: UniqueId): CSSCursorType {\n return cursorStore.useSelector(mapId, (state) =>\n getEffectiveCursor(mapId, state),\n );\n}\n\n/**\n * Clear cursor state for a specific map instance.\n *\n * Removes all cursor ownership data and resets to default state.\n * This is typically not needed as cleanup happens automatically.\n * Use only in advanced scenarios where manual cleanup is required.\n *\n * @param mapId - Unique identifier for the map instance to clear\n * @returns void\n *\n * @example\n * ```typescript\n * import { clearCursorState } from '@accelint/map-toolkit/map-cursor';\n *\n * // Manual cleanup when destroying a map\n * clearCursorState(mapId);\n * ```\n */\nexport function clearCursorState(mapId: UniqueId): void {\n cursorStore.clear(mapId);\n}\n"],"mappings":"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"}
|
package/dist/map-mode/store.d.ts
CHANGED
|
@@ -43,26 +43,65 @@ type MapModeActions = {
|
|
|
43
43
|
*/
|
|
44
44
|
declare const modeStore: MapStore<MapModeState, MapModeActions>;
|
|
45
45
|
/**
|
|
46
|
-
* Get the current mode for a map instance
|
|
46
|
+
* Get the current mode for a map instance.
|
|
47
|
+
*
|
|
48
|
+
* @param mapId - Unique identifier for the map instance
|
|
49
|
+
* @returns The current active mode string
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* import { getMode } from '@accelint/map-toolkit/map-mode';
|
|
54
|
+
*
|
|
55
|
+
* const currentMode = getMode(mapId);
|
|
56
|
+
* console.log('Current mode:', currentMode); // 'default', 'drawing', etc.
|
|
57
|
+
* ```
|
|
47
58
|
*/
|
|
48
59
|
declare function getMode(mapId: UniqueId): string;
|
|
49
60
|
/**
|
|
50
|
-
* Hook for current mode value
|
|
61
|
+
* Hook for current mode value.
|
|
62
|
+
*
|
|
63
|
+
* **Internal use only** - not exported from the public API.
|
|
64
|
+
* Use `useMapMode` instead for better ergonomics and MapContext integration.
|
|
65
|
+
*
|
|
66
|
+
* @param mapId - Unique identifier for the map instance
|
|
67
|
+
* @returns The current active mode string
|
|
51
68
|
*/
|
|
52
69
|
declare function useMode(mapId: UniqueId): string;
|
|
53
70
|
/**
|
|
54
|
-
* Get the owner of the current mode for a given map instance
|
|
71
|
+
* Get the owner of the current mode for a given map instance.
|
|
72
|
+
*
|
|
55
73
|
* @internal - For internal map-toolkit use only
|
|
74
|
+
* @param instanceId - Unique identifier for the map instance
|
|
75
|
+
* @returns The owner ID of the current mode, or undefined if unowned
|
|
56
76
|
*/
|
|
57
77
|
declare function getCurrentModeOwner(instanceId: UniqueId): string | undefined;
|
|
58
78
|
/**
|
|
59
79
|
* Check if a given owner is registered as the owner of any mode.
|
|
60
80
|
* This includes both active mode owners and pending mode requests.
|
|
81
|
+
*
|
|
61
82
|
* @internal - For internal map-toolkit use only
|
|
83
|
+
* @param instanceId - Unique identifier for the map instance
|
|
84
|
+
* @param owner - The owner ID to check
|
|
85
|
+
* @returns True if the owner is registered for any mode
|
|
62
86
|
*/
|
|
63
87
|
declare function isRegisteredModeOwner(instanceId: UniqueId, owner: string): boolean;
|
|
64
88
|
/**
|
|
65
|
-
* Manually clear map mode state for a specific
|
|
89
|
+
* Manually clear map mode state for a specific map instance.
|
|
90
|
+
*
|
|
91
|
+
* Removes all mode ownership data, pending requests, and resets to default state.
|
|
92
|
+
* This is typically not needed as cleanup happens automatically.
|
|
93
|
+
* Use only in advanced scenarios where manual cleanup is required.
|
|
94
|
+
*
|
|
95
|
+
* @param instanceId - Unique identifier for the map instance to clear
|
|
96
|
+
* @returns void
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* import { clearMapModeState } from '@accelint/map-toolkit/map-mode';
|
|
101
|
+
*
|
|
102
|
+
* // Manual cleanup when destroying a map
|
|
103
|
+
* clearMapModeState(mapId);
|
|
104
|
+
* ```
|
|
66
105
|
*/
|
|
67
106
|
declare function clearMapModeState(instanceId: UniqueId): void;
|
|
68
107
|
//#endregion
|
package/dist/map-mode/store.js
CHANGED
|
@@ -53,7 +53,18 @@ const DEFAULT_MODE = "default";
|
|
|
53
53
|
*/
|
|
54
54
|
const mapModeBus = Broadcast.getInstance();
|
|
55
55
|
/**
|
|
56
|
-
* Determine if a mode change request should be auto-accepted without authorization
|
|
56
|
+
* Determine if a mode change request should be auto-accepted without authorization.
|
|
57
|
+
*
|
|
58
|
+
* Auto-accept conditions:
|
|
59
|
+
* - Owner returning to default mode
|
|
60
|
+
* - Owner switching between their own modes
|
|
61
|
+
* - No ownership conflicts exist
|
|
62
|
+
* - Entering an owned mode from default mode
|
|
63
|
+
*
|
|
64
|
+
* @param state - Current mode state
|
|
65
|
+
* @param desiredMode - The mode being requested
|
|
66
|
+
* @param requestOwner - The component requesting the mode change
|
|
67
|
+
* @returns True if the request should be auto-accepted
|
|
57
68
|
*/
|
|
58
69
|
function shouldAutoAcceptRequest(state, desiredMode, requestOwner) {
|
|
59
70
|
const currentModeOwner = state.modeOwners.get(state.mode);
|
|
@@ -222,20 +233,40 @@ const modeStore = createMapStore({
|
|
|
222
233
|
}
|
|
223
234
|
});
|
|
224
235
|
/**
|
|
225
|
-
* Get the current mode for a map instance
|
|
236
|
+
* Get the current mode for a map instance.
|
|
237
|
+
*
|
|
238
|
+
* @param mapId - Unique identifier for the map instance
|
|
239
|
+
* @returns The current active mode string
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* import { getMode } from '@accelint/map-toolkit/map-mode';
|
|
244
|
+
*
|
|
245
|
+
* const currentMode = getMode(mapId);
|
|
246
|
+
* console.log('Current mode:', currentMode); // 'default', 'drawing', etc.
|
|
247
|
+
* ```
|
|
226
248
|
*/
|
|
227
249
|
function getMode(mapId) {
|
|
228
250
|
return modeStore.get(mapId).mode;
|
|
229
251
|
}
|
|
230
252
|
/**
|
|
231
|
-
* Hook for current mode value
|
|
253
|
+
* Hook for current mode value.
|
|
254
|
+
*
|
|
255
|
+
* **Internal use only** - not exported from the public API.
|
|
256
|
+
* Use `useMapMode` instead for better ergonomics and MapContext integration.
|
|
257
|
+
*
|
|
258
|
+
* @param mapId - Unique identifier for the map instance
|
|
259
|
+
* @returns The current active mode string
|
|
232
260
|
*/
|
|
233
261
|
function useMode(mapId) {
|
|
234
262
|
return modeStore.useSelector(mapId, (state) => state.mode);
|
|
235
263
|
}
|
|
236
264
|
/**
|
|
237
|
-
* Get the owner of the current mode for a given map instance
|
|
265
|
+
* Get the owner of the current mode for a given map instance.
|
|
266
|
+
*
|
|
238
267
|
* @internal - For internal map-toolkit use only
|
|
268
|
+
* @param instanceId - Unique identifier for the map instance
|
|
269
|
+
* @returns The owner ID of the current mode, or undefined if unowned
|
|
239
270
|
*/
|
|
240
271
|
function getCurrentModeOwner(instanceId) {
|
|
241
272
|
const state = modeStore.get(instanceId);
|
|
@@ -244,7 +275,11 @@ function getCurrentModeOwner(instanceId) {
|
|
|
244
275
|
/**
|
|
245
276
|
* Check if a given owner is registered as the owner of any mode.
|
|
246
277
|
* This includes both active mode owners and pending mode requests.
|
|
278
|
+
*
|
|
247
279
|
* @internal - For internal map-toolkit use only
|
|
280
|
+
* @param instanceId - Unique identifier for the map instance
|
|
281
|
+
* @param owner - The owner ID to check
|
|
282
|
+
* @returns True if the owner is registered for any mode
|
|
248
283
|
*/
|
|
249
284
|
function isRegisteredModeOwner(instanceId, owner) {
|
|
250
285
|
const state = modeStore.get(instanceId);
|
|
@@ -253,7 +288,22 @@ function isRegisteredModeOwner(instanceId, owner) {
|
|
|
253
288
|
return false;
|
|
254
289
|
}
|
|
255
290
|
/**
|
|
256
|
-
* Manually clear map mode state for a specific
|
|
291
|
+
* Manually clear map mode state for a specific map instance.
|
|
292
|
+
*
|
|
293
|
+
* Removes all mode ownership data, pending requests, and resets to default state.
|
|
294
|
+
* This is typically not needed as cleanup happens automatically.
|
|
295
|
+
* Use only in advanced scenarios where manual cleanup is required.
|
|
296
|
+
*
|
|
297
|
+
* @param instanceId - Unique identifier for the map instance to clear
|
|
298
|
+
* @returns void
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* import { clearMapModeState } from '@accelint/map-toolkit/map-mode';
|
|
303
|
+
*
|
|
304
|
+
* // Manual cleanup when destroying a map
|
|
305
|
+
* clearMapModeState(mapId);
|
|
306
|
+
* ```
|
|
257
307
|
*/
|
|
258
308
|
function clearMapModeState(instanceId) {
|
|
259
309
|
modeStore.clear(instanceId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","names":["requestsToReject: PendingRequest[]","matchingRequestOwner: string | null","matchingRequest: PendingRequest | null"],"sources":["../../src/map-mode/store.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n/**\n * Map Mode Store\n *\n * Manages mode state with ownership-based authorization.\n *\n * @example\n * ```tsx\n * import { modeStore } from '@accelint/map-toolkit/map-mode';\n *\n * function MapControls({ mapId }) {\n * const { state, requestModeChange } = modeStore.use(mapId);\n *\n * return (\n * <div>\n * <p>Current mode: {state.mode}</p>\n * <button onClick={() => requestModeChange('draw', 'draw-layer')}>\n * Draw Mode\n * </button>\n * </div>\n * );\n * }\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { uuid } from '@accelint/core';\nimport { getLogger } from '@accelint/logger';\nimport {\n createMapStore,\n mapClear,\n mapDelete,\n mapSet,\n} from '../shared/create-map-store';\nimport { MapModeEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { StoreHelpers } from '../shared/create-map-store';\nimport type { MapModeEventType, ModeChangeDecisionPayload } from './types';\n\nconst logger = getLogger({\n enabled:\n process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test',\n level: 'warn',\n prefix: '[MapMode]',\n pretty: true,\n});\n\nconst DEFAULT_MODE = 'default';\n\n/**\n * Typed event bus instance for map mode events.\n */\nconst mapModeBus = Broadcast.getInstance<MapModeEventType>();\n\n/**\n * Internal type for tracking pending authorization requests.\n */\ntype PendingRequest = {\n authId: string;\n desiredMode: string;\n currentMode: string;\n requestOwner: string;\n};\n\n/**\n * State shape for map mode\n */\ntype MapModeState = {\n mode: string;\n modeOwners: Map<string, string>;\n pendingRequests: Map<string, PendingRequest>;\n};\n\n/**\n * Actions for map mode\n */\ntype MapModeActions = {\n /** Request a mode change */\n requestModeChange: (desiredMode: string, requestOwner: string) => void;\n};\n\n/**\n * Determine if a mode change request should be auto-accepted without authorization\n */\nfunction shouldAutoAcceptRequest(\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n): boolean {\n const currentModeOwner = state.modeOwners.get(state.mode);\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Owner returning to default mode\n if (desiredMode === DEFAULT_MODE && requestOwner === currentModeOwner) {\n return true;\n }\n\n // Owner switching between their own modes\n if (requestOwner === currentModeOwner) {\n return true;\n }\n\n // No ownership conflicts exist\n if (!(currentModeOwner || desiredModeOwner)) {\n return true;\n }\n\n // Entering an owned mode from default mode\n if (state.mode === DEFAULT_MODE && requestOwner === desiredModeOwner) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Approve a request and reject all others (immutable update)\n */\nfunction approveRequestAndRejectOthers(\n instanceId: UniqueId,\n state: MapModeState,\n approvedRequest: PendingRequest,\n excludeAuthId: string,\n decisionOwner: string,\n reason: string,\n emitApproval: boolean,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n // Collect all other pending requests to emit rejections for\n const requestsToReject: PendingRequest[] = [];\n for (const request of state.pendingRequests.values()) {\n if (request.authId !== excludeAuthId) {\n requestsToReject.push(request);\n }\n }\n\n // Build immutable updates: clear pending requests, update owners\n const newModeOwners =\n approvedRequest.desiredMode !== DEFAULT_MODE\n ? mapSet(\n state.modeOwners,\n approvedRequest.desiredMode,\n approvedRequest.requestOwner,\n )\n : state.modeOwners;\n\n // Immutable update: clear pending requests, update owners, change mode\n set({\n mode: approvedRequest.desiredMode,\n pendingRequests: mapClear<string, PendingRequest>(),\n modeOwners: newModeOwners,\n });\n\n // Emit mode changed event\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode: state.mode,\n currentMode: approvedRequest.desiredMode,\n id: instanceId,\n });\n\n // Emit approval decision if requested\n if (emitApproval) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: approvedRequest.authId,\n approved: true,\n owner: decisionOwner,\n reason,\n id: instanceId,\n });\n }\n\n // Emit rejection events for all other pending requests\n for (const request of requestsToReject) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: decisionOwner,\n reason: 'Request auto-rejected because another request was approved',\n id: instanceId,\n });\n }\n}\n\n/**\n * Handle pending requests when returning to default mode (immutable update)\n */\nfunction handlePendingRequestsOnDefaultMode(\n instanceId: UniqueId,\n state: MapModeState,\n previousMode: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const firstEntry = Array.from(state.pendingRequests.values())[0];\n if (!firstEntry) {\n return;\n }\n\n const previousModeOwner = state.modeOwners.get(previousMode);\n\n if (!previousModeOwner) {\n return;\n }\n\n // If the first pending request is for default mode, reject all requests\n if (firstEntry.desiredMode === DEFAULT_MODE) {\n const allRequests = Array.from(state.pendingRequests.values());\n\n // Immutable update: clear pending requests\n set({ pendingRequests: mapClear<string, PendingRequest>() });\n\n for (const request of allRequests) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: previousModeOwner,\n reason: 'Request rejected - already in requested mode',\n id: instanceId,\n } satisfies ModeChangeDecisionPayload);\n }\n } else {\n // Auto-accept the first pending request for a different mode\n approveRequestAndRejectOthers(\n instanceId,\n state,\n firstEntry,\n firstEntry.authId,\n previousModeOwner,\n 'Auto-accepted when mode owner returned to default',\n true,\n set,\n );\n }\n}\n\n/**\n * Handle authorization decision (immutable update)\n */\nfunction handleAuthorizationDecision(\n instanceId: UniqueId,\n state: MapModeState,\n payload: {\n approved: boolean;\n authId: string;\n owner: string;\n },\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const { approved, authId, owner: decisionOwner } = payload;\n\n // Verify decision is from current mode's owner\n const currentModeOwner = state.modeOwners.get(state.mode);\n if (decisionOwner !== currentModeOwner) {\n logger.warn(\n `Authorization decision from \"${decisionOwner}\" ignored - not the owner of mode \"${state.mode}\" (owner: ${currentModeOwner || 'none'})`,\n );\n return;\n }\n\n // Find the request with matching authId\n let matchingRequestOwner: string | null = null;\n let matchingRequest: PendingRequest | null = null;\n\n for (const [requestOwner, request] of state.pendingRequests.entries()) {\n if (request.authId === authId) {\n matchingRequestOwner = requestOwner;\n matchingRequest = request;\n break;\n }\n }\n\n if (!(matchingRequest && matchingRequestOwner)) {\n return;\n }\n\n if (approved) {\n approveRequestAndRejectOthers(\n instanceId,\n state,\n matchingRequest,\n authId,\n decisionOwner,\n '',\n false,\n set,\n );\n } else {\n // Immutable update: remove the rejected request\n set({\n pendingRequests: mapDelete(state.pendingRequests, matchingRequestOwner),\n });\n }\n}\n\n/**\n * Handle mode change request logic (immutable update)\n */\nfunction handleModeChangeRequest(\n instanceId: UniqueId,\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Check if this request should be auto-accepted\n if (shouldAutoAcceptRequest(state, desiredMode, requestOwner)) {\n // Build immutable updates\n const newModeOwners =\n desiredMode !== DEFAULT_MODE && !desiredModeOwner\n ? mapSet(state.modeOwners, desiredMode, requestOwner)\n : state.modeOwners;\n\n // Clear requester's pending request since mode changed successfully\n const newPendingRequests = mapDelete(state.pendingRequests, requestOwner);\n\n const previousMode = state.mode;\n\n // Immutable update\n set({\n mode: desiredMode,\n modeOwners: newModeOwners,\n pendingRequests: newPendingRequests,\n });\n\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode,\n currentMode: desiredMode,\n id: instanceId,\n });\n\n return;\n }\n\n // Otherwise, send authorization request\n const authId = uuid();\n\n // Immutable update: add pending request\n set({\n pendingRequests: mapSet(state.pendingRequests, requestOwner, {\n authId,\n desiredMode,\n currentMode: state.mode,\n requestOwner,\n }),\n });\n\n mapModeBus.emit(MapModeEvents.changeAuthorization, {\n authId,\n desiredMode,\n currentMode: state.mode,\n id: instanceId,\n });\n}\n\n/**\n * Map mode store\n */\nexport const modeStore = createMapStore<MapModeState, MapModeActions>({\n defaultState: {\n mode: DEFAULT_MODE,\n modeOwners: new Map(),\n pendingRequests: new Map(),\n },\n\n actions: (instanceId) => ({\n requestModeChange: (desiredMode: string, requestOwner: string) => {\n const trimmedDesiredMode = desiredMode.trim();\n const trimmedRequestOwner = requestOwner.trim();\n\n if (!trimmedDesiredMode) {\n throw new Error('requestModeChange requires non-empty desiredMode');\n }\n if (!trimmedRequestOwner) {\n throw new Error('requestModeChange requires non-empty requestOwner');\n }\n\n mapModeBus.emit(MapModeEvents.changeRequest, {\n desiredMode: trimmedDesiredMode,\n owner: trimmedRequestOwner,\n id: instanceId,\n });\n },\n }),\n\n bus: (instanceId, { get, set }) => {\n // Listen for mode change requests\n const unsubRequest = mapModeBus.on(MapModeEvents.changeRequest, (event) => {\n const { desiredMode, owner: requestOwner, id } = event.payload;\n\n const state = get();\n // Filter: only handle if targeted at this map\n if (id !== instanceId || desiredMode === state.mode) {\n return;\n }\n\n handleModeChangeRequest(\n instanceId,\n state,\n desiredMode,\n requestOwner,\n set,\n );\n });\n\n // Listen for authorization decisions\n const unsubDecision = mapModeBus.on(\n MapModeEvents.changeDecision,\n (event) => {\n const { id, approved, authId, owner } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n handleAuthorizationDecision(\n instanceId,\n get(),\n { approved, authId, owner },\n set,\n );\n },\n );\n\n // Listen for mode changes to handle pending requests\n const unsubChanged = mapModeBus.on(MapModeEvents.changed, (event) => {\n const { currentMode, previousMode, id } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n const state = get();\n // When mode owner changes to default mode, handle pending requests\n if (currentMode === DEFAULT_MODE && state.pendingRequests.size > 0) {\n handlePendingRequestsOnDefaultMode(\n instanceId,\n state,\n previousMode,\n set,\n );\n }\n });\n\n return () => {\n unsubRequest();\n unsubDecision();\n unsubChanged();\n };\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Get the current mode for a map instance\n */\nexport function getMode(mapId: UniqueId): string {\n return modeStore.get(mapId).mode;\n}\n\n/**\n * Hook for current mode value\n */\nexport function useMode(mapId: UniqueId): string {\n return modeStore.useSelector(mapId, (state) => state.mode);\n}\n\n/**\n * Get the owner of the current mode for a given map instance\n * @internal - For internal map-toolkit use only\n */\nexport function getCurrentModeOwner(instanceId: UniqueId): string | undefined {\n const state = modeStore.get(instanceId);\n return state.modeOwners.get(state.mode);\n}\n\n/**\n * Check if a given owner is registered as the owner of any mode.\n * This includes both active mode owners and pending mode requests.\n * @internal - For internal map-toolkit use only\n */\nexport function isRegisteredModeOwner(\n instanceId: UniqueId,\n owner: string,\n): boolean {\n const state = modeStore.get(instanceId);\n\n // Check active mode owners\n for (const modeOwner of state.modeOwners.values()) {\n if (modeOwner === owner) {\n return true;\n }\n }\n\n // Check pending mode requests (owner is the key in pendingRequests map)\n if (state.pendingRequests.has(owner)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Manually clear map mode state for a specific instanceId.\n */\nexport function clearMapModeState(instanceId: UniqueId): void {\n modeStore.clear(instanceId);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"store.js","names":["requestsToReject: PendingRequest[]","matchingRequestOwner: string | null","matchingRequest: PendingRequest | null"],"sources":["../../src/map-mode/store.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n/**\n * Map Mode Store\n *\n * Manages mode state with ownership-based authorization.\n *\n * @example\n * ```tsx\n * import { modeStore } from '@accelint/map-toolkit/map-mode';\n *\n * function MapControls({ mapId }) {\n * const { state, requestModeChange } = modeStore.use(mapId);\n *\n * return (\n * <div>\n * <p>Current mode: {state.mode}</p>\n * <button onClick={() => requestModeChange('draw', 'draw-layer')}>\n * Draw Mode\n * </button>\n * </div>\n * );\n * }\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { uuid } from '@accelint/core';\nimport { getLogger } from '@accelint/logger';\nimport {\n createMapStore,\n mapClear,\n mapDelete,\n mapSet,\n} from '../shared/create-map-store';\nimport { MapModeEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { StoreHelpers } from '../shared/create-map-store';\nimport type { MapModeEventType, ModeChangeDecisionPayload } from './types';\n\nconst logger = getLogger({\n enabled:\n process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test',\n level: 'warn',\n prefix: '[MapMode]',\n pretty: true,\n});\n\nconst DEFAULT_MODE = 'default';\n\n/**\n * Typed event bus instance for map mode events.\n */\nconst mapModeBus = Broadcast.getInstance<MapModeEventType>();\n\n/**\n * Internal type for tracking pending authorization requests.\n */\ntype PendingRequest = {\n authId: string;\n desiredMode: string;\n currentMode: string;\n requestOwner: string;\n};\n\n/**\n * State shape for map mode\n */\ntype MapModeState = {\n mode: string;\n modeOwners: Map<string, string>;\n pendingRequests: Map<string, PendingRequest>;\n};\n\n/**\n * Actions for map mode\n */\ntype MapModeActions = {\n /** Request a mode change */\n requestModeChange: (desiredMode: string, requestOwner: string) => void;\n};\n\n/**\n * Determine if a mode change request should be auto-accepted without authorization.\n *\n * Auto-accept conditions:\n * - Owner returning to default mode\n * - Owner switching between their own modes\n * - No ownership conflicts exist\n * - Entering an owned mode from default mode\n *\n * @param state - Current mode state\n * @param desiredMode - The mode being requested\n * @param requestOwner - The component requesting the mode change\n * @returns True if the request should be auto-accepted\n */\nfunction shouldAutoAcceptRequest(\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n): boolean {\n const currentModeOwner = state.modeOwners.get(state.mode);\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Owner returning to default mode\n if (desiredMode === DEFAULT_MODE && requestOwner === currentModeOwner) {\n return true;\n }\n\n // Owner switching between their own modes\n if (requestOwner === currentModeOwner) {\n return true;\n }\n\n // No ownership conflicts exist\n if (!(currentModeOwner || desiredModeOwner)) {\n return true;\n }\n\n // Entering an owned mode from default mode\n if (state.mode === DEFAULT_MODE && requestOwner === desiredModeOwner) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Approve a request and reject all others (immutable update)\n */\nfunction approveRequestAndRejectOthers(\n instanceId: UniqueId,\n state: MapModeState,\n approvedRequest: PendingRequest,\n excludeAuthId: string,\n decisionOwner: string,\n reason: string,\n emitApproval: boolean,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n // Collect all other pending requests to emit rejections for\n const requestsToReject: PendingRequest[] = [];\n for (const request of state.pendingRequests.values()) {\n if (request.authId !== excludeAuthId) {\n requestsToReject.push(request);\n }\n }\n\n // Build immutable updates: clear pending requests, update owners\n const newModeOwners =\n approvedRequest.desiredMode !== DEFAULT_MODE\n ? mapSet(\n state.modeOwners,\n approvedRequest.desiredMode,\n approvedRequest.requestOwner,\n )\n : state.modeOwners;\n\n // Immutable update: clear pending requests, update owners, change mode\n set({\n mode: approvedRequest.desiredMode,\n pendingRequests: mapClear<string, PendingRequest>(),\n modeOwners: newModeOwners,\n });\n\n // Emit mode changed event\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode: state.mode,\n currentMode: approvedRequest.desiredMode,\n id: instanceId,\n });\n\n // Emit approval decision if requested\n if (emitApproval) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: approvedRequest.authId,\n approved: true,\n owner: decisionOwner,\n reason,\n id: instanceId,\n });\n }\n\n // Emit rejection events for all other pending requests\n for (const request of requestsToReject) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: decisionOwner,\n reason: 'Request auto-rejected because another request was approved',\n id: instanceId,\n });\n }\n}\n\n/**\n * Handle pending requests when returning to default mode (immutable update)\n */\nfunction handlePendingRequestsOnDefaultMode(\n instanceId: UniqueId,\n state: MapModeState,\n previousMode: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const firstEntry = Array.from(state.pendingRequests.values())[0];\n if (!firstEntry) {\n return;\n }\n\n const previousModeOwner = state.modeOwners.get(previousMode);\n\n if (!previousModeOwner) {\n return;\n }\n\n // If the first pending request is for default mode, reject all requests\n if (firstEntry.desiredMode === DEFAULT_MODE) {\n const allRequests = Array.from(state.pendingRequests.values());\n\n // Immutable update: clear pending requests\n set({ pendingRequests: mapClear<string, PendingRequest>() });\n\n for (const request of allRequests) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: previousModeOwner,\n reason: 'Request rejected - already in requested mode',\n id: instanceId,\n } satisfies ModeChangeDecisionPayload);\n }\n } else {\n // Auto-accept the first pending request for a different mode\n approveRequestAndRejectOthers(\n instanceId,\n state,\n firstEntry,\n firstEntry.authId,\n previousModeOwner,\n 'Auto-accepted when mode owner returned to default',\n true,\n set,\n );\n }\n}\n\n/**\n * Handle authorization decision (immutable update)\n */\nfunction handleAuthorizationDecision(\n instanceId: UniqueId,\n state: MapModeState,\n payload: {\n approved: boolean;\n authId: string;\n owner: string;\n },\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const { approved, authId, owner: decisionOwner } = payload;\n\n // Verify decision is from current mode's owner\n const currentModeOwner = state.modeOwners.get(state.mode);\n if (decisionOwner !== currentModeOwner) {\n logger.warn(\n `Authorization decision from \"${decisionOwner}\" ignored - not the owner of mode \"${state.mode}\" (owner: ${currentModeOwner || 'none'})`,\n );\n return;\n }\n\n // Find the request with matching authId\n let matchingRequestOwner: string | null = null;\n let matchingRequest: PendingRequest | null = null;\n\n for (const [requestOwner, request] of state.pendingRequests.entries()) {\n if (request.authId === authId) {\n matchingRequestOwner = requestOwner;\n matchingRequest = request;\n break;\n }\n }\n\n if (!(matchingRequest && matchingRequestOwner)) {\n return;\n }\n\n if (approved) {\n approveRequestAndRejectOthers(\n instanceId,\n state,\n matchingRequest,\n authId,\n decisionOwner,\n '',\n false,\n set,\n );\n } else {\n // Immutable update: remove the rejected request\n set({\n pendingRequests: mapDelete(state.pendingRequests, matchingRequestOwner),\n });\n }\n}\n\n/**\n * Handle mode change request logic (immutable update)\n */\nfunction handleModeChangeRequest(\n instanceId: UniqueId,\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Check if this request should be auto-accepted\n if (shouldAutoAcceptRequest(state, desiredMode, requestOwner)) {\n // Build immutable updates\n const newModeOwners =\n desiredMode !== DEFAULT_MODE && !desiredModeOwner\n ? mapSet(state.modeOwners, desiredMode, requestOwner)\n : state.modeOwners;\n\n // Clear requester's pending request since mode changed successfully\n const newPendingRequests = mapDelete(state.pendingRequests, requestOwner);\n\n const previousMode = state.mode;\n\n // Immutable update\n set({\n mode: desiredMode,\n modeOwners: newModeOwners,\n pendingRequests: newPendingRequests,\n });\n\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode,\n currentMode: desiredMode,\n id: instanceId,\n });\n\n return;\n }\n\n // Otherwise, send authorization request\n const authId = uuid();\n\n // Immutable update: add pending request\n set({\n pendingRequests: mapSet(state.pendingRequests, requestOwner, {\n authId,\n desiredMode,\n currentMode: state.mode,\n requestOwner,\n }),\n });\n\n mapModeBus.emit(MapModeEvents.changeAuthorization, {\n authId,\n desiredMode,\n currentMode: state.mode,\n id: instanceId,\n });\n}\n\n/**\n * Map mode store\n */\nexport const modeStore = createMapStore<MapModeState, MapModeActions>({\n defaultState: {\n mode: DEFAULT_MODE,\n modeOwners: new Map(),\n pendingRequests: new Map(),\n },\n\n actions: (instanceId) => ({\n requestModeChange: (desiredMode: string, requestOwner: string) => {\n const trimmedDesiredMode = desiredMode.trim();\n const trimmedRequestOwner = requestOwner.trim();\n\n if (!trimmedDesiredMode) {\n throw new Error('requestModeChange requires non-empty desiredMode');\n }\n if (!trimmedRequestOwner) {\n throw new Error('requestModeChange requires non-empty requestOwner');\n }\n\n mapModeBus.emit(MapModeEvents.changeRequest, {\n desiredMode: trimmedDesiredMode,\n owner: trimmedRequestOwner,\n id: instanceId,\n });\n },\n }),\n\n bus: (instanceId, { get, set }) => {\n // Listen for mode change requests\n const unsubRequest = mapModeBus.on(MapModeEvents.changeRequest, (event) => {\n const { desiredMode, owner: requestOwner, id } = event.payload;\n\n const state = get();\n // Filter: only handle if targeted at this map\n if (id !== instanceId || desiredMode === state.mode) {\n return;\n }\n\n handleModeChangeRequest(\n instanceId,\n state,\n desiredMode,\n requestOwner,\n set,\n );\n });\n\n // Listen for authorization decisions\n const unsubDecision = mapModeBus.on(\n MapModeEvents.changeDecision,\n (event) => {\n const { id, approved, authId, owner } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n handleAuthorizationDecision(\n instanceId,\n get(),\n { approved, authId, owner },\n set,\n );\n },\n );\n\n // Listen for mode changes to handle pending requests\n const unsubChanged = mapModeBus.on(MapModeEvents.changed, (event) => {\n const { currentMode, previousMode, id } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n const state = get();\n // When mode owner changes to default mode, handle pending requests\n if (currentMode === DEFAULT_MODE && state.pendingRequests.size > 0) {\n handlePendingRequestsOnDefaultMode(\n instanceId,\n state,\n previousMode,\n set,\n );\n }\n });\n\n return () => {\n unsubRequest();\n unsubDecision();\n unsubChanged();\n };\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Get the current mode for a map instance.\n *\n * @param mapId - Unique identifier for the map instance\n * @returns The current active mode string\n *\n * @example\n * ```typescript\n * import { getMode } from '@accelint/map-toolkit/map-mode';\n *\n * const currentMode = getMode(mapId);\n * console.log('Current mode:', currentMode); // 'default', 'drawing', etc.\n * ```\n */\nexport function getMode(mapId: UniqueId): string {\n return modeStore.get(mapId).mode;\n}\n\n/**\n * Hook for current mode value.\n *\n * **Internal use only** - not exported from the public API.\n * Use `useMapMode` instead for better ergonomics and MapContext integration.\n *\n * @param mapId - Unique identifier for the map instance\n * @returns The current active mode string\n */\nexport function useMode(mapId: UniqueId): string {\n return modeStore.useSelector(mapId, (state) => state.mode);\n}\n\n/**\n * Get the owner of the current mode for a given map instance.\n *\n * @internal - For internal map-toolkit use only\n * @param instanceId - Unique identifier for the map instance\n * @returns The owner ID of the current mode, or undefined if unowned\n */\nexport function getCurrentModeOwner(instanceId: UniqueId): string | undefined {\n const state = modeStore.get(instanceId);\n return state.modeOwners.get(state.mode);\n}\n\n/**\n * Check if a given owner is registered as the owner of any mode.\n * This includes both active mode owners and pending mode requests.\n *\n * @internal - For internal map-toolkit use only\n * @param instanceId - Unique identifier for the map instance\n * @param owner - The owner ID to check\n * @returns True if the owner is registered for any mode\n */\nexport function isRegisteredModeOwner(\n instanceId: UniqueId,\n owner: string,\n): boolean {\n const state = modeStore.get(instanceId);\n\n // Check active mode owners\n for (const modeOwner of state.modeOwners.values()) {\n if (modeOwner === owner) {\n return true;\n }\n }\n\n // Check pending mode requests (owner is the key in pendingRequests map)\n if (state.pendingRequests.has(owner)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Manually clear map mode state for a specific map instance.\n *\n * Removes all mode ownership data, pending requests, and resets to default state.\n * This is typically not needed as cleanup happens automatically.\n * Use only in advanced scenarios where manual cleanup is required.\n *\n * @param instanceId - Unique identifier for the map instance to clear\n * @returns void\n *\n * @example\n * ```typescript\n * import { clearMapModeState } from '@accelint/map-toolkit/map-mode';\n *\n * // Manual cleanup when destroying a map\n * clearMapModeState(mapId);\n * ```\n */\nexport function clearMapModeState(instanceId: UniqueId): void {\n modeStore.clear(instanceId);\n}\n"],"mappings":"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"}
|
|
@@ -16,6 +16,11 @@ import { UniqueId } from "@accelint/core";
|
|
|
16
16
|
/**
|
|
17
17
|
* Create a new Map with an entry added or updated (immutable).
|
|
18
18
|
*
|
|
19
|
+
* @param map - The Map to copy
|
|
20
|
+
* @param key - The key to set
|
|
21
|
+
* @param value - The value to set
|
|
22
|
+
* @returns A new Map with the entry added/updated
|
|
23
|
+
*
|
|
19
24
|
* @example
|
|
20
25
|
* ```ts
|
|
21
26
|
* const newMap = mapSet(state.cursorOwners, 'draw-layer', 'crosshair');
|
|
@@ -26,6 +31,10 @@ declare function mapSet<K, V>(map: Map<K, V>, key: K, value: V): Map<K, V>;
|
|
|
26
31
|
/**
|
|
27
32
|
* Create a new Map with an entry removed (immutable).
|
|
28
33
|
*
|
|
34
|
+
* @param map - The Map to copy
|
|
35
|
+
* @param key - The key to remove
|
|
36
|
+
* @returns A new Map with the entry removed
|
|
37
|
+
*
|
|
29
38
|
* @example
|
|
30
39
|
* ```ts
|
|
31
40
|
* const newMap = mapDelete(state.cursorOwners, 'draw-layer');
|
|
@@ -36,6 +45,8 @@ declare function mapDelete<K, V>(map: Map<K, V>, key: K): Map<K, V>;
|
|
|
36
45
|
/**
|
|
37
46
|
* Create a new empty Map (immutable replacement for Map.clear()).
|
|
38
47
|
*
|
|
48
|
+
* @returns A new empty Map
|
|
49
|
+
*
|
|
39
50
|
* @example
|
|
40
51
|
* ```ts
|
|
41
52
|
* set({ pendingRequests: mapClear<string, PendingRequest>() });
|
|
@@ -166,6 +177,9 @@ type MapStore<TState, TActions> = {
|
|
|
166
177
|
/**
|
|
167
178
|
* Creates a store for managing state across multiple map instances.
|
|
168
179
|
*
|
|
180
|
+
* @param config - Store configuration including default state, actions, and optional bus setup
|
|
181
|
+
* @returns A MapStore instance with hooks and methods for accessing/updating state
|
|
182
|
+
*
|
|
169
183
|
* @example
|
|
170
184
|
* ```ts
|
|
171
185
|
* const cursorStore = createMapStore({
|
|
@@ -17,6 +17,11 @@ import { useRef, useSyncExternalStore } from "react";
|
|
|
17
17
|
/**
|
|
18
18
|
* Create a new Map with an entry added or updated (immutable).
|
|
19
19
|
*
|
|
20
|
+
* @param map - The Map to copy
|
|
21
|
+
* @param key - The key to set
|
|
22
|
+
* @param value - The value to set
|
|
23
|
+
* @returns A new Map with the entry added/updated
|
|
24
|
+
*
|
|
20
25
|
* @example
|
|
21
26
|
* ```ts
|
|
22
27
|
* const newMap = mapSet(state.cursorOwners, 'draw-layer', 'crosshair');
|
|
@@ -31,6 +36,10 @@ function mapSet(map, key, value) {
|
|
|
31
36
|
/**
|
|
32
37
|
* Create a new Map with an entry removed (immutable).
|
|
33
38
|
*
|
|
39
|
+
* @param map - The Map to copy
|
|
40
|
+
* @param key - The key to remove
|
|
41
|
+
* @returns A new Map with the entry removed
|
|
42
|
+
*
|
|
34
43
|
* @example
|
|
35
44
|
* ```ts
|
|
36
45
|
* const newMap = mapDelete(state.cursorOwners, 'draw-layer');
|
|
@@ -45,6 +54,8 @@ function mapDelete(map, key) {
|
|
|
45
54
|
/**
|
|
46
55
|
* Create a new empty Map (immutable replacement for Map.clear()).
|
|
47
56
|
*
|
|
57
|
+
* @returns A new empty Map
|
|
58
|
+
*
|
|
48
59
|
* @example
|
|
49
60
|
* ```ts
|
|
50
61
|
* set({ pendingRequests: mapClear<string, PendingRequest>() });
|
|
@@ -56,6 +67,9 @@ function mapClear() {
|
|
|
56
67
|
/**
|
|
57
68
|
* Creates a store for managing state across multiple map instances.
|
|
58
69
|
*
|
|
70
|
+
* @param config - Store configuration including default state, actions, and optional bus setup
|
|
71
|
+
* @returns A MapStore instance with hooks and methods for accessing/updating state
|
|
72
|
+
*
|
|
59
73
|
* @example
|
|
60
74
|
* ```ts
|
|
61
75
|
* const cursorStore = createMapStore({
|
|
@@ -131,7 +145,10 @@ function createMapStore(config) {
|
|
|
131
145
|
return instance.actions;
|
|
132
146
|
}
|
|
133
147
|
/**
|
|
134
|
-
* Clean up instance when last subscriber unmounts
|
|
148
|
+
* Clean up instance when last subscriber unmounts.
|
|
149
|
+
*
|
|
150
|
+
* @param mapId - Unique identifier for the map instance
|
|
151
|
+
* @param instance - The instance to clean up
|
|
135
152
|
*/
|
|
136
153
|
function cleanupInstance(mapId, instance) {
|
|
137
154
|
if (onCleanup) onCleanup(mapId, instance.state);
|
|
@@ -170,7 +187,10 @@ function createMapStore(config) {
|
|
|
170
187
|
return defaultState;
|
|
171
188
|
}
|
|
172
189
|
/**
|
|
173
|
-
* Main hook - returns state and actions
|
|
190
|
+
* Main hook - returns state and actions.
|
|
191
|
+
*
|
|
192
|
+
* @param mapId - Unique identifier for the map instance
|
|
193
|
+
* @returns Object containing state and all actions
|
|
174
194
|
*/
|
|
175
195
|
function use(mapId) {
|
|
176
196
|
return {
|
|
@@ -184,6 +204,10 @@ function createMapStore(config) {
|
|
|
184
204
|
* Note: The selector function is intentionally NOT tracked as a dependency.
|
|
185
205
|
* This prevents infinite re-render loops when using inline arrow functions.
|
|
186
206
|
* If you need dynamic selector behavior, use the `use()` hook with `useMemo`.
|
|
207
|
+
*
|
|
208
|
+
* @param mapId - Unique identifier for the map instance
|
|
209
|
+
* @param selector - Function to select derived state
|
|
210
|
+
* @returns The selected value
|
|
187
211
|
*/
|
|
188
212
|
function useSelector(mapId, selector) {
|
|
189
213
|
const cache = useRef(null);
|