@@ -25,19 +25,19 @@ import cytoscapeStyles from "../cytoscapeStyles.js";
2525import { deserializeGraph } from "./ops.js" ;
2626import { TEST_ICON_SVG } from "../constants/testAssets.js" ;
2727import { GRAYSCALE_IMAGES } from "../config/features.js" ;
28- import { convertImageToGrayscale } from "../utils/grayscaleUtils.js" ;
28+ import {
29+ preprocessImageToGrayscale ,
30+ getCachedGrayscaleImage ,
31+ shouldProcessForGrayscale ,
32+ clearGrayscaleCache as clearGrayscaleCacheUtil
33+ } from "../utils/grayscaleUtils.js" ;
2934import { getCdnBaseUrl } from "../utils/cdnHelpers.js" ;
3035import { loadImageWithFallback , imageCache , getDefaultPlaceholderSvg , ensureDefaultPlaceholderLoaded , onDefaultPlaceholderLoaded } from "../utils/imageLoader.js" ;
3136import { printDebug } from "../utils/debug.js" ; // printWarn
3237import { installAppearOnAdd } from '../anim/appear.js' ;
3338import { isSearchInProgress , isCurrentSearchSelection , getCurrentSearchIds } from '../search/searchHighlighter.js' ;
3439import { ensure as ensureOverlays , refreshPositions as refreshOverlayPositions , attach as attachOverlayManager , detach as detachOverlayManager , setNoteCountsVisible } from './overlayManager.js' ;
3540
36- // Cache for grayscale images to avoid reprocessing
37- const GRAYSCALE_CACHE_KEY = 'shipLogGrayscaleCache' ;
38- const grayscaleCache = new Map ( ) ;
39- const pendingConversions = new Set ( ) ;
40-
4141// Track pending image loads to prevent duplicate requests
4242const pendingImageLoads = new Set ( ) ;
4343
@@ -47,144 +47,11 @@ const pendingNodeImageUpdates = [];
4747// Track current active Cytoscape instance (helps with React 18 StrictMode double mount)
4848let currentActiveCy = null ;
4949
50- // Load grayscale cache from localStorage on module initialization
51- function loadGrayscaleCacheFromStorage ( ) {
52- try {
53- const cached = localStorage . getItem ( GRAYSCALE_CACHE_KEY ) ;
54- if ( cached ) {
55- const parsedCache = JSON . parse ( cached ) ;
56- const entryCount = Object . keys ( parsedCache ) . length ;
57- printDebug ( `💾 [cyAdapter] Loading grayscale cache from localStorage with ${ entryCount } entries` ) ;
58- Object . entries ( parsedCache ) . forEach ( ( [ key , value ] ) => {
59- grayscaleCache . set ( key , value ) ;
60- printDebug ( `💾 [cyAdapter] Loaded grayscale cache entry: "${ key . substring ( 0 , 50 ) } ..." -> value length: ${ value ?. length || 0 } ` ) ;
61- } ) ;
62- printDebug ( `✅ [cyAdapter] Grayscale cache loaded successfully with ${ grayscaleCache . size } entries` ) ;
63- } else {
64- printDebug ( `💾 [cyAdapter] No grayscale cache found in localStorage` ) ;
65- }
66- } catch ( error ) {
67- console . warn ( 'Failed to load grayscale cache from localStorage:' , error ) ;
68- }
69- }
70-
71- // Save grayscale cache to localStorage
72- function saveGrayscaleCacheToStorage ( ) {
73- try {
74- const cacheObject = { } ;
75- let savedCount = 0 ;
76- grayscaleCache . forEach ( ( value , key ) => {
77- // Only save completed conversions (strings), not promises
78- if ( typeof value === 'string' ) {
79- cacheObject [ key ] = value ;
80- savedCount ++ ;
81- }
82- } ) ;
83- printDebug ( `💾 [cyAdapter] Saving grayscale cache to localStorage: ${ savedCount } entries out of ${ grayscaleCache . size } total` ) ;
84- localStorage . setItem ( GRAYSCALE_CACHE_KEY , JSON . stringify ( cacheObject ) ) ;
85- printDebug ( `✅ [cyAdapter] Grayscale cache saved successfully` ) ;
86- } catch ( error ) {
87- console . warn ( 'Failed to save grayscale cache to localStorage:' , error ) ;
88- }
89- }
90-
91- // Initialize cache from localStorage
92- loadGrayscaleCacheFromStorage ( ) ;
93-
94- // Preprocess images to grayscale in the background to avoid race conditions
95- function preprocessImageToGrayscale ( imageUrl , originalImagePath = null ) {
96- printDebug ( `🎨 [cyAdapter] preprocessImageToGrayscale called with: "${ imageUrl ?. substring ( 0 , 50 ) } ..." (originalPath: ${ originalImagePath || 'none' } )` ) ;
97-
98- if ( ! GRAYSCALE_IMAGES || ! imageUrl ) {
99- printDebug ( `⚠️ [cyAdapter] Skipping grayscale: feature disabled or no image` ) ;
100- return Promise . resolve ( imageUrl ) ;
101- }
102-
103- // Skip SVG placeholders and test icons
104- if ( imageUrl === TEST_ICON_SVG || imageUrl . includes ( 'data:image/svg+xml' ) ) {
105- printDebug ( `⚠️ [cyAdapter] Skipping grayscale for SVG placeholder` ) ;
106- return Promise . resolve ( imageUrl ) ;
107- }
108-
109- // Only process real image data URLs - skip filenames and non-image data
110- if ( ! imageUrl . startsWith ( 'data:image/' ) || imageUrl . startsWith ( 'data:image/svg+xml' ) ) {
111- printDebug ( `⚠️ [cyAdapter] Skipping grayscale for non-image data URL: "${ imageUrl ?. substring ( 0 , 50 ) } ..."` ) ;
112- return Promise . resolve ( imageUrl ) ;
113- }
114-
115- // Use original image path as cache key if available, otherwise fall back to image URL
116- const cacheKey = originalImagePath || imageUrl ;
117-
118- if ( grayscaleCache . has ( cacheKey ) ) {
119- const cached = grayscaleCache . get ( cacheKey ) ;
120- printDebug ( `💾 [cyAdapter] Found cached grayscale result for key: "${ originalImagePath ? 'path-based' : 'url-based' } "` ) ;
121- // If it's a string (completed conversion), return it
122- if ( typeof cached === 'string' ) {
123- return Promise . resolve ( cached ) ;
124- }
125- // If it's a promise (in progress), return the promise
126- return cached ;
127- }
128-
129- printDebug ( `🔄 [cyAdapter] Starting new grayscale conversion with cache key: "${ cacheKey ?. substring ( 0 , 50 ) } ..."` ) ;
130- // Start conversion in background, but don't wait for it
131- const conversionPromise = convertImageToGrayscale ( imageUrl )
132- . then ( grayscaleUrl => {
133- grayscaleCache . set ( cacheKey , grayscaleUrl ) ;
134- pendingConversions . delete ( imageUrl ) ;
135- // Save to localStorage when conversion completes
136- saveGrayscaleCacheToStorage ( ) ;
137- return grayscaleUrl ;
138- } )
139- . catch ( error => {
140- console . warn ( 'Failed to convert image to grayscale:' , error ) ;
141- grayscaleCache . set ( cacheKey , imageUrl ) ; // Cache the original to avoid retrying
142- pendingConversions . delete ( imageUrl ) ;
143- // Save to localStorage even on failure to avoid retrying
144- saveGrayscaleCacheToStorage ( ) ;
145- return imageUrl ;
146- } ) ;
147-
148- pendingConversions . add ( imageUrl ) ;
149- grayscaleCache . set ( cacheKey , conversionPromise ) ;
150- return conversionPromise ;
151- }
152-
153- // Check if there are pending conversions that might benefit from an update
154- export function hasPendingGrayscaleConversions ( ) {
155- return pendingConversions . size > 0 ;
156- }
157-
158- // Update only image data for nodes that have completed grayscale conversion
159- export function updateCompletedGrayscaleImages ( cy , graph ) {
160- if ( ! GRAYSCALE_IMAGES || pendingConversions . size === 0 ) return false ;
161-
162- let updated = false ;
163- const g = deserializeGraph ( graph ) ;
164-
165- g . nodes . forEach ( n => {
166- const imageUrl = n . imageUrl ;
167- if ( imageUrl && imageUrl !== TEST_ICON_SVG && grayscaleCache . has ( imageUrl ) ) {
168- const cached = grayscaleCache . get ( imageUrl ) ;
169- if ( typeof cached === 'string' && ! pendingConversions . has ( imageUrl ) ) {
170- // Conversion completed, update the node
171- const cyNode = cy . getElementById ( n . id ) ;
172- if ( cyNode . length > 0 && cyNode . data ( 'imageUrl' ) !== cached ) {
173- cyNode . data ( 'imageUrl' , cached ) ;
174- updated = true ;
175- }
176- }
177- }
178- } ) ;
179-
180- return updated ;
181- }
182-
18350/**
18451 * Update overlays from a notes object and visibility flag.
18552 * IMPORTANT: While a node is being dragged, we only reposition overlays
18653 * (cheap) instead of re-ensuring/recreating them (expensive). This stops
187- * badges from “ flying” or snapping.
54+ * badges from " flying" or snapping.
18855 */
18956export function updateOverlays ( cy , notes , showNoteCountOverlay , visited = null , mode = 'editing' ) {
19057 if ( ! cy || cy . destroyed ( ) ) return ;
@@ -280,9 +147,9 @@ export function buildElementsFromDomain(graph, options = {}) {
280147 }
281148 }
282149
283- if ( GRAYSCALE_IMAGES && loadedImageUrl && ( loadedImageUrl . startsWith ( 'data:image/png;' ) || loadedImageUrl . startsWith ( 'data:image/jpeg;' ) || loadedImageUrl . startsWith ( 'data:image/jpg;' ) || loadedImageUrl . startsWith ( 'data:image/webp;' ) ) ) {
150+ if ( GRAYSCALE_IMAGES && shouldProcessForGrayscale ( loadedImageUrl , TEST_ICON_SVG ) ) {
284151 printDebug ( `🎨 [cyAdapter] Starting grayscale (post-display) for: ${ originalImageUrl } ` ) ;
285- preprocessImageToGrayscale ( loadedImageUrl , originalImageUrl ) . then ( grayscaleUrl => {
152+ preprocessImageToGrayscale ( loadedImageUrl , originalImageUrl , TEST_ICON_SVG ) . then ( grayscaleUrl => {
286153 printDebug ( `✅ [cyAdapter] Grayscale conversion complete for: ${ originalImageUrl } ` ) ;
287154 if ( onImageLoaded && typeof onImageLoaded === 'function' ) {
288155 try {
@@ -313,25 +180,19 @@ export function buildElementsFromDomain(graph, options = {}) {
313180 }
314181
315182 // Apply grayscale to images that are already data URLs (cached images) - but not SVGs
316- if ( GRAYSCALE_IMAGES && imageUrl &&
317- ( imageUrl . startsWith ( 'data:image/png;' ) ||
318- imageUrl . startsWith ( 'data:image/jpeg;' ) ||
319- imageUrl . startsWith ( 'data:image/jpg;' ) ||
320- imageUrl . startsWith ( 'data:image/webp;' ) ) &&
321- imageUrl !== TEST_ICON_SVG ) {
322-
183+ if ( GRAYSCALE_IMAGES && shouldProcessForGrayscale ( imageUrl , TEST_ICON_SVG ) ) {
323184 const originalPath = n . imageUrl ; // The original filename
324- let cached = grayscaleCache . get ( originalPath ) ;
325- if ( ! cached || typeof cached !== 'string' ) {
326- cached = grayscaleCache . get ( imageUrl ) ; // Fallback to data URL key
185+ let cached = getCachedGrayscaleImage ( originalPath ) ;
186+ if ( ! cached ) {
187+ cached = getCachedGrayscaleImage ( imageUrl ) ; // Fallback to data URL key
327188 }
328189
329- if ( cached && typeof cached === 'string' ) {
190+ if ( cached ) {
330191 printDebug ( `✅ [cyAdapter] Using cached grayscale for data URL (key: ${ originalPath ? 'path-based' : 'url-based' } )` ) ;
331192 imageUrl = cached ; // already grayscale
332193 } else {
333194 printDebug ( `🔄 [cyAdapter] Queueing grayscale conversion (will update later) for cached data URL` ) ;
334- preprocessImageToGrayscale ( imageUrl , originalPath ) ; // background; node shows color first
195+ preprocessImageToGrayscale ( imageUrl , originalPath , TEST_ICON_SVG ) ; // background; node shows color first
335196 }
336197 }
337198
@@ -724,17 +585,9 @@ export function syncElements(cy, graph, options = {}) {
724585 if ( ! dataUrl ) return ;
725586
726587 let finalUrl = dataUrl ;
727- if (
728- GRAYSCALE_IMAGES &&
729- typeof dataUrl === 'string' &&
730- ! dataUrl . includes ( 'data:image/svg+xml' ) &&
731- ( dataUrl . startsWith ( 'data:image/png;' ) ||
732- dataUrl . startsWith ( 'data:image/jpeg;' ) ||
733- dataUrl . startsWith ( 'data:image/jpg;' ) ||
734- dataUrl . startsWith ( 'data:image/webp;' ) )
735- ) {
588+ if ( GRAYSCALE_IMAGES && shouldProcessForGrayscale ( dataUrl , TEST_ICON_SVG ) ) {
736589 try {
737- finalUrl = await preprocessImageToGrayscale ( dataUrl , orig ) ;
590+ finalUrl = await preprocessImageToGrayscale ( dataUrl , orig , TEST_ICON_SVG ) ;
738591 } catch {
739592 finalUrl = dataUrl ;
740593 }
@@ -918,9 +771,6 @@ export function wireEvents(cy, handlers = {}, mode = 'editing') {
918771 }
919772 container . addEventListener ( 'touchmove' , debouncedTouchMoveHandler , { passive : true } ) ;
920773
921- cy . on ( 'mouseover' , 'node.entry-parent, edge' , ( evt ) => { evt . cy . container ( ) . style . cursor = 'pointer' ; } ) ;
922- cy . on ( 'mouseout' , 'node.entry-parent, edge' , ( evt ) => { evt . cy . container ( ) . style . cursor = 'default' ; } ) ;
923-
924774 return ( ) => {
925775 printDebug ( '🧹 [cyAdapter] Removing event listeners' ) ;
926776 cy . removeListener ( '*' ) ;
@@ -933,22 +783,14 @@ export function wireEvents(cy, handlers = {}, mode = 'editing') {
933783 } ;
934784}
935785
936- export function clearGrayscaleCache ( ) {
937- const entriesBeforeClear = grayscaleCache . size ;
938- const pendingBeforeClear = pendingConversions . size ;
939-
940- grayscaleCache . clear ( ) ;
941- pendingConversions . clear ( ) ;
942-
943- printDebug ( `🧹 [cyAdapter] Cleared grayscale cache: ${ entriesBeforeClear } entries, ${ pendingBeforeClear } pending conversions` ) ;
944-
945- try {
946- localStorage . removeItem ( GRAYSCALE_CACHE_KEY ) ;
947- printDebug ( `✅ [cyAdapter] Removed grayscale cache from localStorage` ) ;
948- } catch ( e ) {
949- console . warn ( 'Failed to clear grayscale cache from localStorage:' , e ) ;
950- }
951- }
786+ // Re-export clear function from grayscale utils
787+ export const clearGrayscaleCache = clearGrayscaleCacheUtil ;
788+
789+ // Re-export hasPendingGrayscaleConversions for backward compatibility
790+ export { hasPendingGrayscaleConversions } from '../utils/grayscaleUtils.js' ;
791+
792+ // Re-export updateCompletedGrayscaleImages for backward compatibility
793+ export { updateCompletedGrayscaleImages } from '../utils/grayscaleUtils.js' ;
952794
953795// Force immediate image update for a specific node
954796export async function forceNodeImageUpdate ( nodeId , imagePath , mapName , cdnBaseUrl , immediateImageUrl = null ) {
@@ -978,7 +820,7 @@ export async function forceNodeImageUpdate(nodeId, imagePath, mapName, cdnBaseUr
978820
979821 let finalImageUrl = imageUrl ;
980822 if ( GRAYSCALE_IMAGES && ! imageUrl . includes ( 'data:image/svg+xml' ) ) {
981- finalImageUrl = await preprocessImageToGrayscale ( imageUrl , imagePath ) ;
823+ finalImageUrl = await preprocessImageToGrayscale ( imageUrl , imagePath , TEST_ICON_SVG ) ;
982824 printDebug ( `🎨 [cyAdapter] Applied grayscale for immediate update` ) ;
983825 }
984826
@@ -1013,4 +855,4 @@ export function debugPrintEntireGraph(cy) {
1013855 } ) ) ;
1014856 console . log ( "Cytoscape Nodes:" , nodes ) ;
1015857 console . log ( "Cytoscape Edges:" , edges ) ;
1016- }
858+ }
0 commit comments