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);
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 };
}

View file

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

View file

@ -23,6 +23,7 @@ import {
Ellipse,
Line,
Image as KonvaImage,
Text as KonvaText,
Transformer,
Path,
} from 'react-konva';
@ -36,7 +37,7 @@ import { toPx, artboardClipPath } from '../../utils/artboardShapes';
// -- Types --------------------------------------------------------------------
export type CanvasTool = 'pointer' | 'rect' | 'circle' | 'ellipse' | 'line';
export type CanvasTool = 'pointer' | 'rect' | 'circle' | 'ellipse' | 'line' | 'text';
export interface KonvaStageProps {
width: number;
@ -247,6 +248,9 @@ export default function KonvaStage({
} else if (obj.type === 'ellipse') {
(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);
} 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);
@ -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:
return null;
}
@ -426,6 +447,29 @@ export default function KonvaStage({
dash: [],
};
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) {
@ -592,6 +636,8 @@ function getObjWidth(obj: CanvasObject): number {
case 'line':
return Math.max(...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':
return Math.max(...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: '⬯',
line: '',
image: '🖼',
text: 'T',
};
// -- Props --------------------------------------------------------------------

View file

@ -23,6 +23,8 @@ function getWidth(obj: CanvasObject): number {
const xs = obj.points.filter((_, i) => i % 2 === 0);
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);
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,
}: ShapePropertiesProps) {
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 handleChange = useCallback(

View file

@ -82,12 +82,27 @@ export interface ImageObject extends BaseCanvasObject {
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 =
| RectObject
| CircleObject
| EllipseObject
| LineObject
| ImageObject;
| ImageObject
| TextObject;
export type CanvasObjectType = CanvasObject['type'];