Skip to content

Commit 5248568

Browse files
author
Zhiwen Dai
committed
feat(sync): Implement pixel highlight synchronization across panels
- Add functionality to sync pixel highlights between panels in the same group, enhancing user experience during visualizations. - Introduce new methods in SyncManager to handle pixel highlight messages and update corresponding panels. - Update webview content to support pixel highlight display and interaction, improving the visual feedback for users. - Refactor existing logic to ensure smooth integration of pixel highlighting features without disrupting current functionalities.
1 parent d1ec120 commit 5248568

10 files changed

Lines changed: 271 additions & 44 deletions

File tree

src/cvVariablesProvider.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ function getColoredIcon(kind: 'mat' | 'pointcloud' | 'plot' | 'group', color: st
3434
return { light: iconUri, dark: iconUri };
3535
}
3636

37+
function getColoredCircleIcon(color: string): { light: vscode.Uri, dark: vscode.Uri } {
38+
// Create a simple filled circle SVG
39+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><circle cx="8" cy="8" r="6" fill="${color}"/></svg>`;
40+
const base64 = Buffer.from(svg).toString('base64');
41+
const iconUri = vscode.Uri.parse(`data:image/svg+xml;base64,${base64}`);
42+
return { light: iconUri, dark: iconUri };
43+
}
44+
3745
export class CVVariable extends vscode.TreeItem {
3846
public readonly isEmpty: boolean;
3947

@@ -67,7 +75,14 @@ export class CVVariable extends vscode.TreeItem {
6775
const pointerIndicator = this.isPointer ? '→ ' : '';
6876
this.description = `${pointerIndicator}[${displaySize}]`;
6977

70-
if (isPaired && groupIndex !== undefined && kind !== 'plot') {
78+
// For mat (image) variables, always use file-media icon regardless of pairing
79+
if (kind === 'mat') {
80+
this.iconPath = new vscode.ThemeIcon('file-media');
81+
this.contextValue = isPaired
82+
? `cvVariablePaired:${kind}${this.isEmpty ? ':empty' : ''}${this.isPointer ? ':pointer' : ''}`
83+
: `cvVariable:${kind}${this.isEmpty ? ':empty' : ''}${this.isPointer ? ':pointer' : ''}`;
84+
} else if (isPaired && groupIndex !== undefined && kind !== 'plot') {
85+
// For pointcloud, use colored icon when paired
7186
const color = COLORS[groupIndex % COLORS.length];
7287
this.iconPath = getColoredIcon(kind, color);
7388
this.contextValue = `cvVariablePaired:${kind}${this.isEmpty ? ':empty' : ''}${this.isPointer ? ':pointer' : ''}`;
@@ -98,9 +113,9 @@ export class CVGroup extends vscode.TreeItem {
98113

99114
if (groupIndex !== undefined) {
100115
const color = COLORS[groupIndex % COLORS.length];
101-
this.iconPath = getColoredIcon('group', color);
116+
this.iconPath = getColoredCircleIcon(color);
102117
} else {
103-
this.iconPath = new vscode.ThemeIcon('symbol-group');
118+
this.iconPath = new vscode.ThemeIcon('circle-filled');
104119
}
105120
}
106121
}

src/extension.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ import { isPoint3Vector, isMat, is1DVector, isLikely1DMat, is1DSet, isMatx, is2D
1010
import { getMatInfoFromVariables } from "./matImage/matProvider";
1111
import { logDebug, logInfo, logError } from "./utils/logger";
1212

13-
// Request deduplication: prevent multiple simultaneous requests for the same variable
14-
const pendingRequests = new Map<string, Promise<void>>();
13+
// Request management: cancel old requests when new ones arrive
14+
interface PendingRequest {
15+
promise: Promise<void>;
16+
cancel: () => void;
17+
cancelled: boolean;
18+
}
19+
const pendingRequests = new Map<string, PendingRequest>();
1520

1621
export function activate(context: vscode.ExtensionContext) {
1722
// Global safety nets to surface unexpected errors that may desync UI
@@ -143,7 +148,10 @@ export function activate(context: vscode.ExtensionContext) {
143148
const variables = cvVariablesProvider.getVariables();
144149
const options = variables
145150
.filter(v => v.name !== cvVar.name && v.kind === cvVar.kind)
146-
.map(v => ({ label: v.name, description: v.type }));
151+
.map(v => ({
152+
label: v.name,
153+
description: v.sizeInfo ? `[${v.sizeInfo}]` : v.isEmpty ? '[empty]' : ''
154+
}));
147155

148156
if (options.length === 0) {
149157
vscode.window.showInformationMessage(`No other ${cvVar.kind === 'mat' ? 'image' : 'point cloud'} variables found to pair with.`);
@@ -193,24 +201,34 @@ export function activate(context: vscode.ExtensionContext) {
193201
// This ensures pointer and its pointee share the same panel
194202
const panelVariableName = variable.name || variableName.replace(/^\(\*/, '').replace(/\)$/, '');
195203

196-
// Request deduplication: if already processing this variable, wait for it
204+
// Request management: cancel old request if exists
197205
const requestKey = `${debugSession.id}:${panelVariableName}`;
198-
if (pendingRequests.has(requestKey)) {
199-
logDebug(`Request for ${panelVariableName} already in progress, waiting...`);
200-
await pendingRequests.get(requestKey);
201-
return;
206+
const existingRequest = pendingRequests.get(requestKey);
207+
if (existingRequest) {
208+
logDebug(`Cancelling previous request for ${panelVariableName}`);
209+
existingRequest.cancel();
210+
existingRequest.cancelled = true;
202211
}
203212

213+
// Create cancellation state for this request
214+
let isCancelled = false;
215+
const cancellationCheck = () => isCancelled;
216+
const cancel = () => { isCancelled = true; };
217+
204218
// Create promise for this request
205219
const requestPromise = (async () => {
206220
try {
207-
await visualizeVariableInternal(variable, variableName, panelVariableName, isPointer, baseType, shouldForce, reveal, debugSession);
221+
await visualizeVariableInternal(variable, variableName, panelVariableName, isPointer, baseType, shouldForce, reveal, debugSession, cancellationCheck);
208222
} finally {
209-
pendingRequests.delete(requestKey);
223+
// Only delete if this is still the current request
224+
const current = pendingRequests.get(requestKey);
225+
if (current && current.cancel === cancel) {
226+
pendingRequests.delete(requestKey);
227+
}
210228
}
211229
})();
212230

213-
pendingRequests.set(requestKey, requestPromise);
231+
pendingRequests.set(requestKey, { promise: requestPromise, cancel, cancelled: false });
214232
await requestPromise;
215233

216234
} catch (error: any) {
@@ -227,8 +245,14 @@ export function activate(context: vscode.ExtensionContext) {
227245
baseType: string,
228246
shouldForce: boolean,
229247
reveal: boolean,
230-
debugSession: vscode.DebugSession
248+
debugSession: vscode.DebugSession,
249+
cancellationCheck?: () => boolean
231250
) {
251+
// Check if cancelled before starting
252+
if (cancellationCheck && cancellationCheck()) {
253+
logDebug(`Request for ${panelVariableName} was cancelled before starting`);
254+
return;
255+
}
232256
// Check if there's an existing panel for this variable that's being disposed
233257
// If so, skip this request to avoid triggering debug operations during cleanup
234258
const existingPanels = PanelManager.getAllPanels();

src/matImage/matProvider.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ export async function drawMatImage(
302302
}
303303
if (message.command === 'viewChanged') {
304304
SyncManager.syncView(variableName, message.state);
305+
} else if (message.command === 'pixelHighlight') {
306+
SyncManager.syncPixelHighlight(variableName, message.pixelX, message.pixelY);
305307
} else if (message.command === 'reload') {
306308
const reloadStartTime = Date.now();
307309
console.log(`[DEBUG-TRACE] reload message received for ${variableName} at ${reloadStartTime}`);
@@ -764,6 +766,8 @@ export async function drawMatxImage(
764766
async (message) => {
765767
if (message.command === 'viewChanged') {
766768
SyncManager.syncView(variableName, message.state);
769+
} else if (message.command === 'pixelHighlight') {
770+
SyncManager.syncPixelHighlight(variableName, message.pixelX, message.pixelY);
767771
} else if (message.command === 'reload') {
768772
// Check if debug session is still active before reloading
769773
const currentSession = vscode.debug.activeDebugSession;
@@ -960,6 +964,8 @@ export async function draw2DStdArrayImage(
960964
async (message) => {
961965
if (message.command === 'viewChanged') {
962966
SyncManager.syncView(variableName, message.state);
967+
} else if (message.command === 'pixelHighlight') {
968+
SyncManager.syncPixelHighlight(variableName, message.pixelX, message.pixelY);
963969
} else if (message.command === 'reload') {
964970
// Check if debug session is still active before reloading
965971
const currentSession = vscode.debug.activeDebugSession;
@@ -1144,6 +1150,8 @@ export async function draw3DArrayImage(
11441150
async (message) => {
11451151
if (message.command === 'viewChanged') {
11461152
SyncManager.syncView(panelName, message.state);
1153+
} else if (message.command === 'pixelHighlight') {
1154+
SyncManager.syncPixelHighlight(panelName, message.pixelX, message.pixelY);
11471155
} else if (message.command === 'reload') {
11481156
const reloadStartTime = Date.now();
11491157
console.log(`[DEBUG-TRACE] 3D array reload message received for ${panelName} at ${reloadStartTime}`);

src/matImage/matWebview.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,14 @@ export function getWebviewContentForMat(
443443
return;
444444
}
445445
applyViewState(state);
446+
} else if (message.command === 'setPixelHighlight') {
447+
// Receive pixel highlight from synced panel
448+
highlightPixelX = message.pixelX;
449+
highlightPixelY = message.pixelY;
450+
requestRender();
451+
452+
// Update pixel info display for synced highlight
453+
updatePixelInfoForHighlight(message.pixelX, message.pixelY);
446454
}
447455
});
448456
@@ -511,6 +519,12 @@ export function getWebviewContentForMat(
511519
let pixelTextEnabled = true; // 可手动关掉
512520
let renderQueued = false;
513521
let isShuttingDown = false; // 面板即将关闭时阻断交互
522+
523+
// Pixel highlight for synchronized viewing
524+
let highlightPixelX = null;
525+
let highlightPixelY = null;
526+
let localHoverPixelX = null;
527+
let localHoverPixelY = null;
514528
515529
// Make controls draggable
516530
let controlsDragging = false;
@@ -1054,6 +1068,29 @@ export function getWebviewContentForMat(
10541068
return String(v | 0).padStart(3, ' ');
10551069
}
10561070
1071+
// Update pixel info display for synced highlight from other panels
1072+
function updatePixelInfoForHighlight(px, py) {
1073+
if (px === null || py === null) {
1074+
pixelInfo.textContent = '';
1075+
return;
1076+
}
1077+
1078+
if (px >= 0 && px < cols && py >= 0 && py < rows) {
1079+
const idx = (py * cols + px) * channels;
1080+
let valStr = '';
1081+
if (channels === 1) {
1082+
valStr = formatValue(rawData[idx]);
1083+
} else if (channels === 4) {
1084+
valStr = \`R:\${formatValue(rawData[idx])} G:\${formatValue(rawData[idx+1])} B:\${formatValue(rawData[idx+2])} A:\${formatValue(rawData[idx+3])}\`;
1085+
} else {
1086+
valStr = \`R:\${formatValue(rawData[idx])} G:\${formatValue(rawData[idx+1])} B:\${formatValue(rawData[idx+2])}\`;
1087+
}
1088+
pixelInfo.textContent = \`(\${px}, \${py}) : \${valStr}\`;
1089+
} else {
1090+
pixelInfo.textContent = '';
1091+
}
1092+
}
1093+
10571094
function updateCanvasSize() {
10581095
const containerRect = container.getBoundingClientRect();
10591096
const dpr = window.devicePixelRatio || 1;
@@ -1285,12 +1322,37 @@ export function getWebviewContentForMat(
12851322
drawGrid();
12861323
drawPixelTextOverlay();
12871324
1325+
// Draw pixel highlight (blue border) for synchronized viewing
1326+
drawPixelHighlight();
1327+
12881328
// Update zoom level display
12891329
// Fixed-width zoom display: min 1 digit, max 5 digits, padded to 5 with spaces + 4 trailing spaces
12901330
const pct = Math.max(0, Math.round(scale * 100));
12911331
const pctStr = String(pct).slice(0, 5).padStart(5, ' ');
12921332
zoomLevelDisplay.textContent = pctStr + '% ';
12931333
}
1334+
1335+
// Draw pixel highlight with blue border (for synced panels)
1336+
function drawPixelHighlight() {
1337+
// Use either synced highlight or local hover
1338+
const px = highlightPixelX !== null ? highlightPixelX : localHoverPixelX;
1339+
const py = highlightPixelY !== null ? highlightPixelY : localHoverPixelY;
1340+
1341+
if (px === null || py === null) return;
1342+
if (px < 0 || px >= cols || py < 0 || py >= rows) return;
1343+
1344+
// Calculate screen position of the pixel
1345+
const screenX = offsetX + px * scale;
1346+
const screenY = offsetY + py * scale;
1347+
const pixelSize = scale;
1348+
1349+
// Draw blue border (2px thick)
1350+
ctx.save();
1351+
ctx.strokeStyle = '#00aaff';
1352+
ctx.lineWidth = 2;
1353+
ctx.strokeRect(screenX, screenY, pixelSize, pixelSize);
1354+
ctx.restore();
1355+
}
12941356
12951357
function requestRender() {
12961358
if (renderQueued) return;
@@ -1537,8 +1599,37 @@ export function getWebviewContentForMat(
15371599
valStr = \`R:\${formatValue(rawData[idx])} G:\${formatValue(rawData[idx+1])} B:\${formatValue(rawData[idx+2])}\`;
15381600
}
15391601
pixelInfo.textContent = \`(\${imgX}, \${imgY}) : \${valStr}\`;
1602+
1603+
// Update local hover pixel and send to sync
1604+
if (localHoverPixelX !== imgX || localHoverPixelY !== imgY) {
1605+
localHoverPixelX = imgX;
1606+
localHoverPixelY = imgY;
1607+
requestRender();
1608+
// Send pixel highlight to synced panels
1609+
if (!isShuttingDown && isInitialized) {
1610+
vscode.postMessage({
1611+
command: 'pixelHighlight',
1612+
pixelX: imgX,
1613+
pixelY: imgY
1614+
});
1615+
}
1616+
}
15401617
} else {
15411618
pixelInfo.textContent = '';
1619+
// Clear local hover
1620+
if (localHoverPixelX !== null || localHoverPixelY !== null) {
1621+
localHoverPixelX = null;
1622+
localHoverPixelY = null;
1623+
requestRender();
1624+
// Clear highlight on synced panels
1625+
if (!isShuttingDown && isInitialized) {
1626+
vscode.postMessage({
1627+
command: 'pixelHighlight',
1628+
pixelX: null,
1629+
pixelY: null
1630+
});
1631+
}
1632+
}
15421633
}
15431634
});
15441635

src/plot/plotProvider.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,10 @@ export async function drawStdArrayPlot(
998998
return;
999999
}
10001000

1001-
panel.webview.html = getWebviewContentForPlot(panelName, initialData);
1001+
panel.webview.html = getWebviewContentForPlot(panelName);
1002+
1003+
// Send ready signal immediately so webview knows this is not a moved panel
1004+
panel.webview.postMessage({ command: 'ready' });
10021005

10031006
// Dispose old listener
10041007
if ((panel as any)._messageListener) {
@@ -1133,6 +1136,24 @@ export async function drawStdArrayPlot(
11331136
}
11341137
});
11351138

1139+
// Send plot data via postMessage (better memory efficiency than embedding in HTML)
1140+
console.log(`[drawStdArrayPlot] Sending ${initialData.length} data points to webview via postMessage`);
1141+
1142+
if ((panel as any)._isDisposing) {
1143+
console.log("[drawStdArrayPlot] Aborting final data send - panel is being disposed");
1144+
return;
1145+
}
1146+
1147+
try {
1148+
panel.webview.postMessage({
1149+
command: 'completeData',
1150+
data: initialData
1151+
});
1152+
} catch (e) {
1153+
console.log("[drawStdArrayPlot] Final postMessage failed - panel likely disposed");
1154+
return;
1155+
}
1156+
11361157
} catch (error: any) {
11371158
vscode.window.showErrorMessage(`Failed to draw std::array plot: ${error.message}`);
11381159
console.error(error);
@@ -1349,7 +1370,10 @@ export async function drawCStyleArrayPlot(
13491370
return;
13501371
}
13511372

1352-
panel.webview.html = getWebviewContentForPlot(panelName, initialData);
1373+
panel.webview.html = getWebviewContentForPlot(panelName);
1374+
1375+
// Send ready signal immediately so webview knows this is not a moved panel
1376+
panel.webview.postMessage({ command: 'ready' });
13531377

13541378
// Dispose old listener
13551379
if ((panel as any)._messageListener) {
@@ -1483,6 +1507,24 @@ export async function drawCStyleArrayPlot(
14831507
}
14841508
});
14851509

1510+
// Send plot data via postMessage (better memory efficiency than embedding in HTML)
1511+
console.log(`[drawCStyleArrayPlot] Sending ${initialData.length} data points to webview via postMessage`);
1512+
1513+
if ((panel as any)._isDisposing) {
1514+
console.log("[drawCStyleArrayPlot] Aborting final data send - panel is being disposed");
1515+
return;
1516+
}
1517+
1518+
try {
1519+
panel.webview.postMessage({
1520+
command: 'completeData',
1521+
data: initialData
1522+
});
1523+
} catch (e) {
1524+
console.log("[drawCStyleArrayPlot] Final postMessage failed - panel likely disposed");
1525+
return;
1526+
}
1527+
14861528
} catch (error: any) {
14871529
vscode.window.showErrorMessage(`Failed to draw C-style array plot: ${error.message}`);
14881530
console.error(error);

0 commit comments

Comments
 (0)