diff --git a/frontend/src/canvas/stickyPresets.ts b/frontend/src/canvas/stickyPresets.ts index 5ca47cd..9d7bb8f 100644 --- a/frontend/src/canvas/stickyPresets.ts +++ b/frontend/src/canvas/stickyPresets.ts @@ -1,9 +1,9 @@ /** * stickyPresets — sticky note size presets (shared domain module). * - * Defines the S/M/L/XL/XXL presets for sticky text size and card width. - * Used by both canvas logic (tools, SceneManager) and UI (TextFormatToolbar). - * No React dependencies. + * Presets are defined in screen-space so they preserve the same visual intent + * regardless of viewport zoom. Scene data still stores world-space width and + * fontSize; callers convert through the helpers below. */ export type StickyTextSize = 'S' | 'M' | 'L' | 'XL' | 'XXL'; @@ -36,3 +36,37 @@ export function nearestStickySize(fontSize: number): StickyTextSize { export function getStickyWidthForSize(fontSize: number): number { 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, + }; +} diff --git a/frontend/src/canvas/textLimits.ts b/frontend/src/canvas/textLimits.ts index b72c06c..89658a6 100644 --- a/frontend/src/canvas/textLimits.ts +++ b/frontend/src/canvas/textLimits.ts @@ -5,8 +5,8 @@ * Direct manipulation (resize-bake) intentionally bypasses these * clamps so users can scale text to any size. * - * Sticky notes use discrete presets (S/M/L/XL/XXL) defined in - * TextFormatToolbar.tsx — not continuous clamps. + * Sticky notes use discrete presets defined in stickyPresets.ts — not + * continuous clamps. */ const TEXT_FONT_MIN = 6; diff --git a/frontend/src/canvas/tools.ts b/frontend/src/canvas/tools.ts index a4d4dbb..33b548b 100644 --- a/frontend/src/canvas/tools.ts +++ b/frontend/src/canvas/tools.ts @@ -14,7 +14,7 @@ import { DrawingSprite } from './sprites/DrawingSprite'; import type { DrawingObject } from './scene-format'; import { TextEditor } from './TextEditor'; 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 { SELECT = 'SELECT', @@ -324,8 +324,10 @@ export function activateTool( const rect = container.getBoundingClientRect(); const world = viewport.toWorld(e.clientX - rect.left, e.clientY - rect.top); - const cardW = STICKY_WIDTH_MAP[DEFAULT_STICKY_PRESET]; - const cardH = 60; // auto-computed by StickySprite + const zoom = viewport.scale.x; + const presetMetrics = getStickyWorldMetricsForPreset(DEFAULT_STICKY_PRESET, zoom); + const cardW = presetMetrics.width; + const cardH = screenToWorld(60, zoom, 40, 200); const stickyData = { id: crypto.randomUUID(), type: 'sticky' as const, @@ -342,7 +344,7 @@ export function activateTool( name: '', visible: true, text: '', - fontSize: STICKY_FONT_MAP[DEFAULT_STICKY_PRESET], + fontSize: presetMetrics.fontSize, fontFamily: 'Inter, system-ui, sans-serif', fill: '#ffd43b', // default yellow textColor: '#1a1a1a', @@ -374,6 +376,8 @@ export function activateTool( } ctx.broadcastElements?.([item.id]); ctx.onChange(); + }, (resizedItem) => { + ctx.scene.updateSpatialEntry(resizedItem); }); ctx.textEditor!.clearText(); }); diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index b5896e7..29a8dc0 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -21,7 +21,7 @@ import ContextMenu from '../components/ContextMenu'; import LayerPanel from '../components/LayerPanel'; import SelectionToolbar from '../components/SelectionToolbar'; import TextFormatToolbar from '../components/TextFormatToolbar'; -import { getStickyWidthForSize } from '../canvas/stickyPresets'; +import { getStickyWorldMetricsForScreenFont } from '../canvas/stickyPresets'; import VideoControls from '../components/VideoControls'; import ShortcutsHelp from '../components/ShortcutsHelp'; import MattermostImport from '../components/MattermostImport'; @@ -629,7 +629,7 @@ export default function Editor({ isPublicView }: EditorProps) { kind: 'sticky', x: posX, y: posY, fontFamily: sd.fontFamily || 'Inter, system-ui, sans-serif', 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, }); } @@ -974,20 +974,26 @@ export default function Editor({ isPublicView }: EditorProps) { onCanvasChange(textToolbar.items.map(i => i.id)); updateOverlays(); } : undefined} - onStickySizeChange={textToolbar.kind === 'sticky' ? (fontSize) => { + onStickySizeChange={textToolbar.kind === 'sticky' ? (screenFontSize) => { 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) { const d = item.data as StickyObject; - d.fontSize = fontSize; - d.w = newWidth; + const metrics = getStickyWorldMetricsForScreenFont( + screenFontSize, + zoom, + Math.abs(d.sx || 1), + ); + d.fontSize = metrics.fontSize; + d.w = metrics.width; if (item.displayObject instanceof StickySprite) { item.displayObject.updateFromData(d); d.h = item.displayObject.computedHeight; } 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); onCanvasChange(textToolbar.items.map(i => i.id)); updateOverlays();