fix(sticky): restore zoom-aware preset creation and live editing sync
This commit is contained in:
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user