Skip to content

Commit f4ccda7

Browse files
authored
Merge pull request #3 from avidrucker/apply-perf-fixes-branch-001
fix apply performance improvements
2 parents 5d379b7 + 6369057 commit f4ccda7

File tree

9 files changed

+307
-143
lines changed

9 files changed

+307
-143
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ CUSTOM_IMAGE_FEATURE.md
3131
AppOptimized.jsx
3232
scratch.md
3333
App_Refactor1.jsx
34+
PERFORMANCE_FIXES_REVIEW.md
3435

3536
# Project-specific files
3637
ship-log-map-project.zip

src/App.jsx

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ import { appStateReducer, initialAppState, ACTION_TYPES } from "./appStateReduce
6464
import { ZOOM_TO_SELECTION, DEBUG_LOGGING, DEV_MODE, GRAYSCALE_IMAGES, CAMERA_INFO_HIDDEN } from "./config/features.js";
6565
import { handleLoadFromCdn, setCdnBaseUrl, getCdnBaseUrl } from "./utils/cdnHelpers.js"; // getMapUrlFromQuery, clearQueryParams
6666
import BgImageModal from "./components/BgImageModal.jsx";
67-
// import BgImageLayer from "./bg/BgImageLayer";
6867
import { useBgImageState } from "./bg/useBgImageState";
6968
// import { loadImageWithFallback } from "./utils/imageLoader.js";
7069
// import { dataUrlOrBlobToWebpDataUrl } from "./utils/imageUtils.js"
@@ -76,7 +75,7 @@ import { saveToLocal, loadFromLocal, saveModeToLocal, loadModeFromLocal, saveUnd
7675
// Add new persistence imports
7776
import { loadOrientationFromLocal, saveOrientationToLocal, loadCompassVisibleFromLocal, saveCompassVisibleToLocal } from './persistence/index.js';
7877
import { renameNode } from "./graph/ops.js"; // edgeId
79-
import { printDebug } from "./utils/debug.js";
78+
import { printDebug, printWarn } from "./utils/debug.js";
8079
// import { rotateNodesAndCompass } from './utils/rotation.js'; // REFACTOR STEP 1: Removed rotateCompassOnly - now using graphOps.handleRotateRight
8180
import { getCanEditFromQuery, hasAnyQueryParams } from "./utils/mapHelpers.js";
8281

@@ -418,12 +417,20 @@ function App() {
418417
setCdnBaseUrl(cdnBaseUrl);
419418
}, [cdnBaseUrl]);
420419

421-
// Save camera state to localStorage (kept as-is)
420+
// Save camera state to localStorage (debounced to prevent excessive writes)
422421
useEffect(() => {
423-
localStorage.setItem("shipLogCamera", JSON.stringify({
424-
zoom: zoomLevel,
425-
position: cameraPosition
426-
}));
422+
const timeoutId = setTimeout(() => {
423+
try {
424+
localStorage.setItem("shipLogCamera", JSON.stringify({
425+
zoom: zoomLevel,
426+
position: cameraPosition
427+
}));
428+
} catch (e) {
429+
printWarn('Failed to save camera to localStorage:', e);
430+
}
431+
}, 200); // Save at most once every 200ms
432+
433+
return () => clearTimeout(timeoutId);
427434
}, [zoomLevel, cameraPosition]);
428435

429436
// Save mode to localStorage
@@ -1045,6 +1052,17 @@ function App() {
10451052
const memoSelectedEdgeIds = useMemo(() => selectedEdgeIds, [selectedEdgeIds]);
10461053
const memoCameraPosition = useMemo(() => cameraPosition, [cameraPosition]);
10471054
const memoNotes = useMemo(() => graphData.notes, [graphData.notes]);
1055+
1056+
// Memoize background image object to prevent infinite re-renders
1057+
const memoBgImage = useMemo(() => {
1058+
if (!bgImage.imageUrl) return null;
1059+
return {
1060+
imageUrl: bgImage.imageUrl,
1061+
visible: bgImage.visible,
1062+
opacity: bgImage.opacity,
1063+
calibration: bgCalibration
1064+
};
1065+
}, [bgImage.imageUrl, bgImage.visible, bgImage.opacity, bgCalibration]);
10481066

10491067
const handleLoadFromCdnButton = useCallback((cdnBaseUrlArg) => {
10501068
handleLoadFromCdn({
@@ -1124,6 +1142,9 @@ useEffect(() => {
11241142
</div>
11251143
</div>
11261144

1145+
{/* Background image underlay - DISABLED: Now integrated into Cytoscape canvas */}
1146+
{/* Background now renders as a Cytoscape node for perfect sync with pan/zoom */}
1147+
{/* See CytoscapeGraph bgImage prop and bgNodeAdapter.js */}
11271148

11281149
{canEdit && (
11291150
<GraphControls
@@ -1330,14 +1351,7 @@ useEffect(() => {
13301351
showNoteCountOverlay={showNoteCountOverlay}
13311352
notes={memoNotes}
13321353
visited={visited} /* pass visited to drive unseen badges */
1333-
// Background node integration
1334-
bgNodeProps={{
1335-
imageUrl: bgImage?.imageUrl || '',
1336-
visible: !!bgImage?.visible,
1337-
opacity: Number.isFinite(bgImage?.opacity) ? bgImage.opacity : 100,
1338-
calibration: bgCalibration || { tx: 0, ty: 0, s: 1 }
1339-
}}
1340-
1354+
bgImage={memoBgImage}
13411355
/>
13421356

13431357
{compassVisible && (

src/bg/useBgImageState.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
* useBgImageState — Background image state + helpers
55
*
66
* Responsibilities
7+
// handy derived mapping for BgImageLayer "calibration"
8+
// ⚡ Memoized to prevent infinite render loops
9+
const calibration = useMemo(() => {
10+
console.log('🔄 [useBgImageState] Calibration recalculating:', { x: bgImage.x, y: bgImage.y, scale: bgImage.scale });
11+
return {
12+
tx: bgImage.x,
13+
ty: bgImage.y,
14+
s: (bgImage.scale ?? 100) / 100
15+
};
16+
}, [bgImage.x, bgImage.y, bgImage.scale]);s
717
* - Encapsulates bg image metadata (src, x, y, scale, opacity, included, visible).
818
* - Exposes load/delete/toggle/transform helpers for App to compose.
919
*
@@ -12,7 +22,7 @@
1222
* setIncluded(bool), setTransform({x,y,scale,opacity}) }
1323
*/
1424

15-
import { useCallback, useEffect, useState } from "react";
25+
import { useCallback, useEffect, useState, useMemo } from "react";
1626
import { printDebug } from "../utils/debug";
1727

1828
/** LocalStorage key */
@@ -35,7 +45,7 @@ function loadFromLocal() {
3545
if (!raw) return null;
3646
const parsed = JSON.parse(raw);
3747
// basic defensive defaults
38-
return {
48+
const result = {
3949
included: typeof parsed.included === "boolean" ? parsed.included : false,
4050
imageUrl: parsed.imageUrl ?? "",
4151
x: Number.isFinite(parsed.x) ? parsed.x : 0,
@@ -44,7 +54,9 @@ function loadFromLocal() {
4454
opacity: Number.isFinite(parsed.opacity) ? parsed.opacity : 100,
4555
visible: typeof parsed.visible === "boolean" ? parsed.visible : false
4656
};
47-
} catch {
57+
return result;
58+
} catch (err) {
59+
printDebug("Failed to load bg image from localStorage:", err);
4860
return null;
4961
}
5062
}
@@ -105,12 +117,13 @@ export function useBgImageState() {
105117
setBgImage((bg) => ({ ...bg, visible: !bg.visible }));
106118
}, []);
107119

108-
// handy derived mapping for BgImageLayer “calibration”
109-
const calibration = {
120+
// handy derived mapping for BgImageLayer "calibration"
121+
// ⚡ Memoized to prevent infinite render loops
122+
const calibration = useMemo(() => ({
110123
tx: bgImage.x,
111124
ty: bgImage.y,
112125
s: (bgImage.scale ?? 100) / 100
113-
};
126+
}), [bgImage.x, bgImage.y, bgImage.scale]);
114127

115128
return {
116129
bgImage,

src/components/BgImageModal.jsx

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,80 @@ function BgImageModal({
8383
)}
8484
</div>
8585
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
86-
<label>
87-
X: <input type="number" value={bgImage.x} onChange={e => onChange({ ...bgImage, x: Number(e.target.value) })} style={{ width: 60 }} />
86+
<label style={{ display: "flex", alignItems: "center", gap: "8px" }}>
87+
<input
88+
type="checkbox"
89+
checked={bgImage.visible}
90+
onChange={e => onChange({ ...bgImage, visible: e.target.checked })}
91+
/>
92+
<span>Show Background Image</span>
8893
</label>
89-
<label>
90-
Y: <input type="number" value={bgImage.y} onChange={e => onChange({ ...bgImage, y: Number(e.target.value) })} style={{ width: 60 }} />
94+
<label style={{ display: "flex", alignItems: "center", gap: "8px" }}>
95+
<input
96+
type="checkbox"
97+
checked={bgImage.included}
98+
onChange={e => onChange({ ...bgImage, included: e.target.checked })}
99+
/>
100+
<span>Include in Export</span>
91101
</label>
92-
<label>
93-
Scale (%): <input type="number" value={bgImage.scale} min={1} max={1000} onChange={e => onChange({ ...bgImage, scale: Number(e.target.value) })} style={{ width: 60 }} />
102+
<hr style={{ width: "100%", border: "none", borderTop: "1px solid #444", margin: "8px 0" }} />
103+
<label style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
104+
<span>X Offset:</span>
105+
<input
106+
type="number"
107+
value={bgImage.x}
108+
onChange={e => onChange({ ...bgImage, x: Number(e.target.value) })}
109+
style={{ width: 80, padding: "4px" }}
110+
step="10"
111+
/>
94112
</label>
95-
<label>
96-
Opacity (%): <input type="number" value={bgImage.opacity} min={0} max={100} onChange={e => onChange({ ...bgImage, opacity: Number(e.target.value) })} style={{ width: 60 }} />
113+
<label style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
114+
<span>Y Offset:</span>
115+
<input
116+
type="number"
117+
value={bgImage.y}
118+
onChange={e => onChange({ ...bgImage, y: Number(e.target.value) })}
119+
style={{ width: 80, padding: "4px" }}
120+
step="10"
121+
/>
122+
</label>
123+
<label style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
124+
<span>Scale (%):</span>
125+
<input
126+
type="range"
127+
value={bgImage.scale}
128+
min={10}
129+
max={500}
130+
onChange={e => onChange({ ...bgImage, scale: Number(e.target.value) })}
131+
style={{ width: 150 }}
132+
/>
133+
<input
134+
type="number"
135+
value={bgImage.scale}
136+
min={10}
137+
max={500}
138+
onChange={e => onChange({ ...bgImage, scale: Number(e.target.value) })}
139+
style={{ width: 60, padding: "4px", marginLeft: "8px" }}
140+
/>
141+
</label>
142+
<label style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
143+
<span>Opacity (%):</span>
144+
<input
145+
type="range"
146+
value={bgImage.opacity}
147+
min={0}
148+
max={100}
149+
onChange={e => onChange({ ...bgImage, opacity: Number(e.target.value) })}
150+
style={{ width: 150 }}
151+
/>
152+
<input
153+
type="number"
154+
value={bgImage.opacity}
155+
min={0}
156+
max={100}
157+
onChange={e => onChange({ ...bgImage, opacity: Number(e.target.value) })}
158+
style={{ width: 60, padding: "4px", marginLeft: "8px" }}
159+
/>
97160
</label>
98161
</div>
99162
<div style={{ marginTop: "18px", display: "flex", justifyContent: "flex-end", gap: "10px" }}>

0 commit comments

Comments
 (0)