fix(sticky): restore zoom-aware preset creation and live editing sync

This commit is contained in:
Hiren Kangad
2026-03-13 00:42:32 +05:30
parent 2d05226ddd
commit ce1cc60d67
4 changed files with 60 additions and 16 deletions
+37 -3
View File
@@ -1,9 +1,9 @@
/** /**
* stickyPresets — sticky note size presets (shared domain module). * stickyPresets — sticky note size presets (shared domain module).
* *
* Defines the S/M/L/XL/XXL presets for sticky text size and card width. * Presets are defined in screen-space so they preserve the same visual intent
* Used by both canvas logic (tools, SceneManager) and UI (TextFormatToolbar). * regardless of viewport zoom. Scene data still stores world-space width and
* No React dependencies. * fontSize; callers convert through the helpers below.
*/ */
export type StickyTextSize = 'S' | 'M' | 'L' | 'XL' | 'XXL'; export type StickyTextSize = 'S' | 'M' | 'L' | 'XL' | 'XXL';
@@ -36,3 +36,37 @@ export function nearestStickySize(fontSize: number): StickyTextSize {
export function getStickyWidthForSize(fontSize: number): number { export function getStickyWidthForSize(fontSize: number): number {
return STICKY_WIDTH_MAP[nearestStickySize(fontSize)]; return STICKY_WIDTH_MAP[nearestStickySize(fontSize)];
} }
/**
* Convert a preset's screen-space target to world-space sticky metrics.
* objectScale is included so old non-1 scaled stickies still respond
* predictably when a preset is applied.
*/
export function getStickyWorldMetricsForPreset(
preset: StickyTextSize,
zoom: number,
objectScale: number = 1,
): { fontSize: number; width: number } {
const safeZoom = Math.max(zoom, 0.001);
const safeScale = Math.max(Math.abs(objectScale), 0.001);
return {
fontSize: STICKY_FONT_MAP[preset] / (safeZoom * safeScale),
width: STICKY_WIDTH_MAP[preset] / (safeZoom * safeScale),
};
}
/**
* Convert a desired on-screen font size into world-space sticky metrics by
* snapping to the nearest preset first.
*/
export function getStickyWorldMetricsForScreenFont(
screenFontSize: number,
zoom: number,
objectScale: number = 1,
): { fontSize: number; width: number; preset: StickyTextSize } {
const preset = nearestStickySize(screenFontSize);
return {
...getStickyWorldMetricsForPreset(preset, zoom, objectScale),
preset,
};
}
+2 -2
View File
@@ -5,8 +5,8 @@
* Direct manipulation (resize-bake) intentionally bypasses these * Direct manipulation (resize-bake) intentionally bypasses these
* clamps so users can scale text to any size. * clamps so users can scale text to any size.
* *
* Sticky notes use discrete presets (S/M/L/XL/XXL) defined in * Sticky notes use discrete presets defined in stickyPresets.ts — not
* TextFormatToolbar.tsx — not continuous clamps. * continuous clamps.
*/ */
const TEXT_FONT_MIN = 6; const TEXT_FONT_MIN = 6;
+8 -4
View File
@@ -14,7 +14,7 @@ import { DrawingSprite } from './sprites/DrawingSprite';
import type { DrawingObject } from './scene-format'; import type { DrawingObject } from './scene-format';
import { TextEditor } from './TextEditor'; import { TextEditor } from './TextEditor';
import { clampTextFontSize } from './textLimits'; import { clampTextFontSize } from './textLimits';
import { DEFAULT_STICKY_PRESET, STICKY_FONT_MAP, STICKY_WIDTH_MAP } from './stickyPresets'; import { DEFAULT_STICKY_PRESET, getStickyWorldMetricsForPreset } from './stickyPresets';
export enum ToolType { export enum ToolType {
SELECT = 'SELECT', SELECT = 'SELECT',
@@ -324,8 +324,10 @@ export function activateTool(
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
const world = viewport.toWorld(e.clientX - rect.left, e.clientY - rect.top); const world = viewport.toWorld(e.clientX - rect.left, e.clientY - rect.top);
const cardW = STICKY_WIDTH_MAP[DEFAULT_STICKY_PRESET]; const zoom = viewport.scale.x;
const cardH = 60; // auto-computed by StickySprite const presetMetrics = getStickyWorldMetricsForPreset(DEFAULT_STICKY_PRESET, zoom);
const cardW = presetMetrics.width;
const cardH = screenToWorld(60, zoom, 40, 200);
const stickyData = { const stickyData = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
type: 'sticky' as const, type: 'sticky' as const,
@@ -342,7 +344,7 @@ export function activateTool(
name: '', name: '',
visible: true, visible: true,
text: '', text: '',
fontSize: STICKY_FONT_MAP[DEFAULT_STICKY_PRESET], fontSize: presetMetrics.fontSize,
fontFamily: 'Inter, system-ui, sans-serif', fontFamily: 'Inter, system-ui, sans-serif',
fill: '#ffd43b', // default yellow fill: '#ffd43b', // default yellow
textColor: '#1a1a1a', textColor: '#1a1a1a',
@@ -374,6 +376,8 @@ export function activateTool(
} }
ctx.broadcastElements?.([item.id]); ctx.broadcastElements?.([item.id]);
ctx.onChange(); ctx.onChange();
}, (resizedItem) => {
ctx.scene.updateSpatialEntry(resizedItem);
}); });
ctx.textEditor!.clearText(); ctx.textEditor!.clearText();
}); });
+13 -7
View File
@@ -21,7 +21,7 @@ import ContextMenu from '../components/ContextMenu';
import LayerPanel from '../components/LayerPanel'; import LayerPanel from '../components/LayerPanel';
import SelectionToolbar from '../components/SelectionToolbar'; import SelectionToolbar from '../components/SelectionToolbar';
import TextFormatToolbar from '../components/TextFormatToolbar'; import TextFormatToolbar from '../components/TextFormatToolbar';
import { getStickyWidthForSize } from '../canvas/stickyPresets'; import { getStickyWorldMetricsForScreenFont } from '../canvas/stickyPresets';
import VideoControls from '../components/VideoControls'; import VideoControls from '../components/VideoControls';
import ShortcutsHelp from '../components/ShortcutsHelp'; import ShortcutsHelp from '../components/ShortcutsHelp';
import MattermostImport from '../components/MattermostImport'; import MattermostImport from '../components/MattermostImport';
@@ -629,7 +629,7 @@ export default function Editor({ isPublicView }: EditorProps) {
kind: 'sticky', x: posX, y: posY, kind: 'sticky', x: posX, y: posY,
fontFamily: sd.fontFamily || 'Inter, system-ui, sans-serif', fontFamily: sd.fontFamily || 'Inter, system-ui, sans-serif',
textColor: sd.textColor || '#1a1a1a', fill: sd.fill || '#ffd43b', textColor: sd.textColor || '#1a1a1a', fill: sd.fill || '#ffd43b',
stickyFontSize: sd.fontSize || 14, stickyFontSize: (sd.fontSize || 14) * vp.scale.x * Math.abs(sd.sx || 1),
items: stickyItems, items: stickyItems,
}); });
} }
@@ -974,20 +974,26 @@ export default function Editor({ isPublicView }: EditorProps) {
onCanvasChange(textToolbar.items.map(i => i.id)); onCanvasChange(textToolbar.items.map(i => i.id));
updateOverlays(); updateOverlays();
} : undefined} } : undefined}
onStickySizeChange={textToolbar.kind === 'sticky' ? (fontSize) => { onStickySizeChange={textToolbar.kind === 'sticky' ? (screenFontSize) => {
const scene = canvasRef.current?.getScene(); const scene = canvasRef.current?.getScene();
const newWidth = getStickyWidthForSize(fontSize); const vp = canvasRef.current?.getViewport();
const zoom = vp?.scale.x ?? 1;
for (const item of textToolbar.items) { for (const item of textToolbar.items) {
const d = item.data as StickyObject; const d = item.data as StickyObject;
d.fontSize = fontSize; const metrics = getStickyWorldMetricsForScreenFont(
d.w = newWidth; screenFontSize,
zoom,
Math.abs(d.sx || 1),
);
d.fontSize = metrics.fontSize;
d.w = metrics.width;
if (item.displayObject instanceof StickySprite) { if (item.displayObject instanceof StickySprite) {
item.displayObject.updateFromData(d); item.displayObject.updateFromData(d);
d.h = item.displayObject.computedHeight; d.h = item.displayObject.computedHeight;
} }
if (scene) scene.updateSpatialEntry(item); if (scene) scene.updateSpatialEntry(item);
} }
setTextToolbar((prev) => prev && prev.kind === 'sticky' ? { ...prev, stickyFontSize: fontSize } : prev); setTextToolbar((prev) => prev && prev.kind === 'sticky' ? { ...prev, stickyFontSize: screenFontSize } : prev);
selectionRef.current?.transformBox.update(textToolbar.items); selectionRef.current?.transformBox.update(textToolbar.items);
onCanvasChange(textToolbar.items.map(i => i.id)); onCanvasChange(textToolbar.items.map(i => i.id));
updateOverlays(); updateOverlays();