fix: Ensure size estimate is applied to text shapes while typing (#20809)

* ensure size estimate is applied to text shape while typing

* add comment and comma
This commit is contained in:
KDSBrowne 2024-08-02 18:47:39 -04:00 committed by GitHub
parent 06d1a74cdc
commit 466e328477
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 141 additions and 6 deletions

View File

@ -16,7 +16,7 @@ import {
import Styled from './styles'; import Styled from './styles';
import PanToolInjector from './pan-tool-injector/component'; import PanToolInjector from './pan-tool-injector/component';
import { import {
findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, usePrevious, findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, usePrevious, TextUtil,
} from './utils'; } from './utils';
import { isEqual } from 'radash'; import { isEqual } from 'radash';
@ -28,6 +28,14 @@ const TOOLBAR_SMALL = 28;
const TOOLBAR_LARGE = 32; const TOOLBAR_LARGE = 32;
const MOUNTED_RESIZE_DELAY = 1500; const MOUNTED_RESIZE_DELAY = 1500;
const LETTER_SPACING = '-0.03em';
const LINE_HEIGHT = 1.2;
const fontSizeMap = {
small: 12,
medium: 16,
large: 20
};
export default function Whiteboard(props) { export default function Whiteboard(props) {
const { const {
isPresenter, isPresenter,
@ -747,6 +755,24 @@ export default function Whiteboard(props) {
const onPatch = (e, t, reason) => { const onPatch = (e, t, reason) => {
if (!e?.pageState || !reason) return; if (!e?.pageState || !reason) return;
function updateTextShapeSize(shape, textUtil, fsm) {
// Create a new object for estimated shape
const estimatedShape = {
...shape,
style: {
...shape.style,
fontFamily: shape.style.fontFamily || 'Comic Sans MS, cursive, sans-serif',
fontSize: fsm[shape.style.size] || 16,
letterSpacing: LETTER_SPACING,
lineHeight: LINE_HEIGHT,
},
};
// Calculate and set size
const measuredBounds = textUtil.getBoundsEstimate(estimatedShape);
// Assign size to a new object to avoid modifying the original shape parameter
return { ...shape, size: [measuredBounds.width, measuredBounds.height] };
}
if (((isPanning || panSelected) && (reason === 'selected' || reason === 'set_hovered_id'))) { if (((isPanning || panSelected) && (reason === 'selected' || reason === 'set_hovered_id'))) {
e.patchState( e.patchState(
{ {
@ -889,15 +915,16 @@ export default function Whiteboard(props) {
} }
if (e?.session?.initialShape?.type === 'text' && !shapes[patchedShape.id]) { if (e?.session?.initialShape?.type === 'text' && !shapes[patchedShape.id]) {
// check for maxShapes // Check for maxShapes
const currentShapes = e?.document?.pages[e?.currentPageId]?.shapes; const currentShapes = e?.document?.pages[e?.currentPageId]?.shapes;
const shapeNumberExceeded = Object.keys(currentShapes).length - 1 > maxNumberOfAnnotations; const shapeNumberExceeded = Object.keys(currentShapes).length - 1 > maxNumberOfAnnotations;
if (shapeNumberExceeded) { if (shapeNumberExceeded) {
notifyShapeNumberExceeded(intl, maxNumberOfAnnotations); notifyShapeNumberExceeded(intl, maxNumberOfAnnotations);
e?.cancelSession?.(); e?.cancelSession?.();
} else { } else {
patchedShape.userId = currentUser?.userId; const updatedShape = { ...patchedShape, userId: currentUser?.userId };
persistShape(patchedShape, whiteboardId, isModerator); const newShapeWithSize = updateTextShapeSize(updatedShape, TextUtil, fontSizeMap);
persistShape(newShapeWithSize, whiteboardId, isModerator);
} }
} else { } else {
const diff = { const diff = {
@ -905,6 +932,11 @@ export default function Whiteboard(props) {
point: patchedShape.point, point: patchedShape.point,
text: patchedShape.text, text: patchedShape.text,
}; };
if (patchedShape.type === 'text') {
const updatedShape = updateTextShapeSize(patchedShape, TextUtil, fontSizeMap);
diff.size = updatedShape.size;
}
persistShape(diff, whiteboardId, isModerator); persistShape(diff, whiteboardId, isModerator);
} }
} }

View File

@ -275,11 +275,114 @@ const getTextSize = (text, style, padding) => {
return [width + padding, height + padding]; return [width + padding, height + padding];
}; };
// getBoundsEstimate here is a function inspired by implementations in tldraw-v1:
// TextUtil: https://github.com/tldraw/tldraw-v1/blob/f786c38ac0fdce337c4405c11cbfa9223d2ee6dd/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx#L24
// getTextSize: https://github.com/tldraw/tldraw-v1/blob/f786c38ac0fdce337c4405c11cbfa9223d2ee6dd/packages/tldraw/src/state/shapes/shared/getTextSize.ts#L6
// It calculates the estimated bounds of a text shape based on its content and style.
const TextUtil = {
getBoundsEstimate(shape) {
const { text, style } = shape;
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.visibility = 'hidden';
div.style.whiteSpace = 'pre';
div.style.fontSize = this.getFontSize(style.size);
div.style.fontFamily = this.getFontFamily(style.font);
div.style.letterSpacing = style.letterSpacing || 'normal';
div.style.lineHeight = style.lineHeight || 'normal';
div.textContent = text;
document.body.appendChild(div);
// Calculate the number of lines
const numberOfLines = text.split('\n').length;
// Get the bounding box dimensions
const { width } = div.getBoundingClientRect();
const lineHeight = parseFloat(getComputedStyle(div).lineHeight);
const height = lineHeight * numberOfLines;
document.body.removeChild(div);
// Dynamic padding adjustment based on font size
const paddingAdjustment = parseInt(div.style.fontSize, 10) * 0.3;
let finalWidth = width + paddingAdjustment;
let finalHeight = height + paddingAdjustment;
// Apply width and height multipliers based on font size and style
finalWidth *= this.getWidthMultiplier(style.size, style.font);
finalHeight *= this.getHeightMultiplier(style.size, style.font, numberOfLines);
return {
width: finalWidth,
height: finalHeight,
};
},
getFontSize(size) {
switch (size) {
case 'small': return '12px';
case 'medium': return '16px';
case 'large': return '20px';
default: return '16px';
}
},
getFontFamily(font) {
switch (font) {
case 'mono': return 'monospace';
case 'script': return 'Comic Sans MS, cursive, sans-serif';
case 'sans': return 'Arial, sans-serif';
case 'serif': return 'Times New Roman, serif';
default: return 'sans-serif';
}
},
getWidthMultiplier(size, font) {
const fontMultiplier = {
small: { mono: 3, default: 2 },
medium: { mono: 4, default: 3 },
large: { mono: 7, default: 4 },
};
if (font === 'mono') {
return fontMultiplier[size].mono;
} else if (['script', 'sans', 'serif'].includes(font)) {
return fontMultiplier[size].default;
}
return 1;
},
getHeightMultiplier(size, font, numberOfLines) {
const baseMultiplier = 1.5;
if (font === 'mono') {
return baseMultiplier + (numberOfLines > 1 ? 0.5 : 0);
} else if (['script', 'sans', 'serif'].includes(font)) {
return baseMultiplier + (numberOfLines > 1 ? 0.3 : 0);
}
return baseMultiplier;
}
};
const Utils = { const Utils = {
usePrevious, findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, getTextSize, usePrevious,
findRemoved,
filterInvalidShapes,
mapLanguage,
sendShapeChanges,
getTextSize,
TextUtil
}; };
export default Utils; export default Utils;
export { export {
usePrevious, findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, getTextSize, usePrevious,
findRemoved,
filterInvalidShapes,
mapLanguage,
sendShapeChanges,
getTextSize,
TextUtil
}; };