test: Added TextObject to CanvasObject union and wired text tool into K…

- "app/src/types/canvas.ts"
- "app/src/components/canvas/KonvaStage.tsx"
- "app/src/components/canvas/CanvasToolbar.tsx"
- "app/src/components/canvas/ObjectPanel.tsx"
- "app/src/components/canvas/ShapeProperties.tsx"
- "app/src/components/canvas/AlignmentBar.tsx"

GSD-Task: S03/T02
This commit is contained in:
jlightner 2026-03-26 05:55:47 +00:00
parent ab170d8d20
commit 24fb28d622
6 changed files with 76 additions and 3 deletions

View file

@ -48,6 +48,10 @@ function toBoundingRect(obj: CanvasObject): BoundingRect {
h = Math.max(...ys) - Math.min(...ys); h = Math.max(...ys) - Math.min(...ys);
break; break;
} }
case 'text':
w = obj.width;
h = obj.fontSize * obj.lineHeight;
break;
} }
return { id: obj.id, x: obj.x, y: obj.y, width: w, height: h }; return { id: obj.id, x: obj.x, y: obj.y, width: w, height: h };
} }

View file

@ -14,6 +14,7 @@ const TOOLS: { tool: CanvasTool; label: string; icon: string }[] = [
{ tool: 'circle', label: 'Circle', icon: '○' }, { tool: 'circle', label: 'Circle', icon: '○' },
{ tool: 'ellipse', label: 'Ellipse', icon: '⬯' }, { tool: 'ellipse', label: 'Ellipse', icon: '⬯' },
{ tool: 'line', label: 'Line', icon: '' }, { tool: 'line', label: 'Line', icon: '' },
{ tool: 'text', label: 'Text', icon: 'T' },
]; ];
// -- Props -------------------------------------------------------------------- // -- Props --------------------------------------------------------------------

View file

@ -23,6 +23,7 @@ import {
Ellipse, Ellipse,
Line, Line,
Image as KonvaImage, Image as KonvaImage,
Text as KonvaText,
Transformer, Transformer,
Path, Path,
} from 'react-konva'; } from 'react-konva';
@ -36,7 +37,7 @@ import { toPx, artboardClipPath } from '../../utils/artboardShapes';
// -- Types -------------------------------------------------------------------- // -- Types --------------------------------------------------------------------
export type CanvasTool = 'pointer' | 'rect' | 'circle' | 'ellipse' | 'line'; export type CanvasTool = 'pointer' | 'rect' | 'circle' | 'ellipse' | 'line' | 'text';
export interface KonvaStageProps { export interface KonvaStageProps {
width: number; width: number;
@ -247,6 +248,9 @@ export default function KonvaStage({
} else if (obj.type === 'ellipse') { } else if (obj.type === 'ellipse') {
(changes as Record<string, unknown>).radiusX = Math.max(5, (node.width() * scaleX) / 2); (changes as Record<string, unknown>).radiusX = Math.max(5, (node.width() * scaleX) / 2);
(changes as Record<string, unknown>).radiusY = Math.max(5, (node.height() * scaleY) / 2); (changes as Record<string, unknown>).radiusY = Math.max(5, (node.height() * scaleY) / 2);
} else if (obj.type === 'text') {
// Scale width for wrapping, keep fontSize unchanged
(changes as Record<string, unknown>).width = Math.max(20, node.width() * scaleX);
} }
onUpdateObject(obj.id, changes); onUpdateObject(obj.id, changes);
@ -315,6 +319,23 @@ export default function KonvaStage({
/> />
); );
case 'text':
return (
<KonvaText
key={obj.id}
{...commonProps}
text={obj.text}
fontFamily={obj.fontFamily}
fontSize={obj.fontSize}
fill={obj.fill}
stroke={obj.stroke}
strokeWidth={obj.strokeWidth}
width={obj.width}
letterSpacing={obj.letterSpacing}
lineHeight={obj.lineHeight}
/>
);
default: default:
return null; return null;
} }
@ -426,6 +447,29 @@ export default function KonvaStage({
dash: [], dash: [],
}; };
break; break;
case 'text':
newObj = {
id: nextId('text'),
type: 'text',
name: 'Text',
x,
y,
rotation: 0,
visible: true,
locked: false,
opacity: 1,
text: 'Text',
fontFamily: 'Roboto',
fontSize: 24,
letterSpacing: 0,
lineHeight: 1.2,
fill: '#000000',
stroke: 'transparent',
strokeWidth: 0,
width: 200,
};
break;
} }
if (newObj) { if (newObj) {
@ -592,6 +636,8 @@ function getObjWidth(obj: CanvasObject): number {
case 'line': case 'line':
return Math.max(...obj.points.filter((_, i) => i % 2 === 0)) - return Math.max(...obj.points.filter((_, i) => i % 2 === 0)) -
Math.min(...obj.points.filter((_, i) => i % 2 === 0)); Math.min(...obj.points.filter((_, i) => i % 2 === 0));
case 'text':
return obj.width || obj.fontSize * obj.text.length * 0.6;
} }
} }
@ -607,6 +653,8 @@ function getObjHeight(obj: CanvasObject): number {
case 'line': case 'line':
return Math.max(...obj.points.filter((_, i) => i % 2 === 1)) - return Math.max(...obj.points.filter((_, i) => i % 2 === 1)) -
Math.min(...obj.points.filter((_, i) => i % 2 === 1)); Math.min(...obj.points.filter((_, i) => i % 2 === 1));
case 'text':
return obj.fontSize * obj.lineHeight;
} }
} }

View file

@ -17,6 +17,7 @@ const TYPE_ICONS: Record<CanvasObject['type'], string> = {
ellipse: '⬯', ellipse: '⬯',
line: '', line: '',
image: '🖼', image: '🖼',
text: 'T',
}; };
// -- Props -------------------------------------------------------------------- // -- Props --------------------------------------------------------------------

View file

@ -23,6 +23,8 @@ function getWidth(obj: CanvasObject): number {
const xs = obj.points.filter((_, i) => i % 2 === 0); const xs = obj.points.filter((_, i) => i % 2 === 0);
return Math.round((Math.max(...xs) - Math.min(...xs)) * 100) / 100; return Math.round((Math.max(...xs) - Math.min(...xs)) * 100) / 100;
} }
case 'text':
return Math.round(obj.width * 100) / 100;
} }
} }
@ -39,6 +41,8 @@ function getHeight(obj: CanvasObject): number {
const ys = obj.points.filter((_, i) => i % 2 === 1); const ys = obj.points.filter((_, i) => i % 2 === 1);
return Math.round((Math.max(...ys) - Math.min(...ys)) * 100) / 100; return Math.round((Math.max(...ys) - Math.min(...ys)) * 100) / 100;
} }
case 'text':
return Math.round(obj.fontSize * obj.lineHeight * 100) / 100;
} }
} }
@ -62,7 +66,7 @@ export default function ShapeProperties({
onUpdate, onUpdate,
}: ShapePropertiesProps) { }: ShapePropertiesProps) {
const hasStroke = object.type !== 'image'; const hasStroke = object.type !== 'image';
const hasFill = object.type === 'rect' || object.type === 'circle' || object.type === 'ellipse'; const hasFill = object.type === 'rect' || object.type === 'circle' || object.type === 'ellipse' || object.type === 'text';
const isLine = object.type === 'line'; const isLine = object.type === 'line';
const handleChange = useCallback( const handleChange = useCallback(

View file

@ -82,12 +82,27 @@ export interface ImageObject extends BaseCanvasObject {
src: string; src: string;
} }
export interface TextObject extends BaseCanvasObject {
type: 'text';
text: string;
fontFamily: string;
fontSize: number;
letterSpacing: number;
lineHeight: number;
fill: string;
stroke: string;
strokeWidth: number;
/** Wrapping width for text layout. */
width: number;
}
export type CanvasObject = export type CanvasObject =
| RectObject | RectObject
| CircleObject | CircleObject
| EllipseObject | EllipseObject
| LineObject | LineObject
| ImageObject; | ImageObject
| TextObject;
export type CanvasObjectType = CanvasObject['type']; export type CanvasObjectType = CanvasObject['type'];