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:
parent
ff246b3d52
commit
40690e9c80
11 changed files with 238 additions and 16 deletions
|
|
@ -26,3 +26,4 @@
|
||||||
{"cmd":"complete-slice","params":{"milestoneId":"M002","sliceId":"S02"},"ts":"2026-03-26T05:44:01.083Z","actor":"agent","hash":"7c28ef1e308c7de7","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
{"cmd":"complete-slice","params":{"milestoneId":"M002","sliceId":"S02"},"ts":"2026-03-26T05:44:01.083Z","actor":"agent","hash":"7c28ef1e308c7de7","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||||
{"cmd":"plan-slice","params":{"milestoneId":"M002","sliceId":"S03"},"ts":"2026-03-26T05:48:40.020Z","actor":"agent","hash":"22ad4efa07f9be81","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
{"cmd":"plan-slice","params":{"milestoneId":"M002","sliceId":"S03"},"ts":"2026-03-26T05:48:40.020Z","actor":"agent","hash":"22ad4efa07f9be81","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||||
{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S03","taskId":"T01"},"ts":"2026-03-26T05:52:47.302Z","actor":"agent","hash":"c631fa7e62a3f1cc","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S03","taskId":"T01"},"ts":"2026-03-26T05:52:47.302Z","actor":"agent","hash":"c631fa7e62a3f1cc","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||||
|
{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S03","taskId":"T02"},"ts":"2026-03-26T05:55:43.882Z","actor":"agent","hash":"e4c20836b9c0ee25","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ This is the riskiest piece of the slice — if opentype.js path extraction doesn
|
||||||
- Estimate: 1h
|
- Estimate: 1h
|
||||||
- Files: app/src/utils/fontService.ts, app/src/utils/__tests__/fontService.test.ts, app/public/fonts/, app/src/App.css, app/package.json
|
- Files: app/src/utils/fontService.ts, app/src/utils/__tests__/fontService.test.ts, app/public/fonts/, app/src/App.css, app/package.json
|
||||||
- Verify: cd app && npx vitest run src/utils/__tests__/fontService.test.ts --reporter=verbose && npx tsc --noEmit && ls public/fonts/*.ttf | wc -l
|
- Verify: cd app && npx vitest run src/utils/__tests__/fontService.test.ts --reporter=verbose && npx tsc --noEmit && ls public/fonts/*.ttf | wc -l
|
||||||
- [ ] **T02: Add TextObject type and wire text tool into canvas rendering** — Extend the CanvasObject discriminated union with a TextObject type. Wire text objects into all canvas components: KonvaStage (render via Konva Text + case in getObjWidth/getObjHeight + text tool creation in handleStageMouseDown), CanvasToolbar (add 'text' to CanvasTool union and TOOLS array), ObjectPanel (add 'text' icon to TYPE_ICONS), and DesignCanvas (pass through). This follows the exact pattern established in S02 for adding object types.
|
- [x] **T02: Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage** — Extend the CanvasObject discriminated union with a TextObject type. Wire text objects into all canvas components: KonvaStage (render via Konva Text + case in getObjWidth/getObjHeight + text tool creation in handleStageMouseDown), CanvasToolbar (add 'text' to CanvasTool union and TOOLS array), ObjectPanel (add 'text' icon to TYPE_ICONS), and DesignCanvas (pass through). This follows the exact pattern established in S02 for adding object types.
|
||||||
|
|
||||||
## Key Implementation Details
|
## Key Implementation Details
|
||||||
- TextObject interface: extends BaseCanvasObject with type: 'text', text: string, fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number, fill: string, stroke: string, strokeWidth: number, width: number (for wrapping)
|
- TextObject interface: extends BaseCanvasObject with type: 'text', text: string, fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number, fill: string, stroke: string, strokeWidth: number, width: number (for wrapping)
|
||||||
|
|
|
||||||
30
.gsd/milestones/M002/slices/S03/tasks/T01-VERIFY.json
Normal file
30
.gsd/milestones/M002/slices/S03/tasks/T01-VERIFY.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"taskId": "T01",
|
||||||
|
"unitId": "M002/S03/T01",
|
||||||
|
"timestamp": 1774504384634,
|
||||||
|
"passed": false,
|
||||||
|
"discoverySource": "task-plan",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"command": "cd app",
|
||||||
|
"exitCode": 0,
|
||||||
|
"durationMs": 4,
|
||||||
|
"verdict": "pass"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "npx vitest run src/utils/__tests__/fontService.test.ts --reporter=verbose",
|
||||||
|
"exitCode": 0,
|
||||||
|
"durationMs": 1408,
|
||||||
|
"verdict": "pass"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "npx tsc --noEmit",
|
||||||
|
"exitCode": 1,
|
||||||
|
"durationMs": 734,
|
||||||
|
"verdict": "fail"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"retryAttempt": 1,
|
||||||
|
"maxRetries": 2
|
||||||
|
}
|
||||||
86
.gsd/milestones/M002/slices/S03/tasks/T02-SUMMARY.md
Normal file
86
.gsd/milestones/M002/slices/S03/tasks/T02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
---
|
||||||
|
id: T02
|
||||||
|
parent: S03
|
||||||
|
milestone: M002
|
||||||
|
provides: []
|
||||||
|
requires: []
|
||||||
|
affects: []
|
||||||
|
key_files: ["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"]
|
||||||
|
key_decisions: ["Text objects default to fill '#000000' and stroke 'transparent' (opposite of shape defaults) for natural text appearance", "Text transform scales width only (for wrapping), keeping fontSize unchanged — consistent with design tool UX"]
|
||||||
|
patterns_established: []
|
||||||
|
drill_down_paths: []
|
||||||
|
observability_surfaces: []
|
||||||
|
duration: ""
|
||||||
|
verification_result: "Ran `cd app && npx tsc --noEmit` — zero errors under strict mode. Ran `cd app && npx vitest run --reporter=verbose` — all 95 tests pass across 7 test files in 2.45s."
|
||||||
|
completed_at: 2026-03-26T05:55:43.833Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T02: Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage
|
||||||
|
|
||||||
|
> Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
---
|
||||||
|
id: T02
|
||||||
|
parent: S03
|
||||||
|
milestone: M002
|
||||||
|
key_files:
|
||||||
|
- 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
|
||||||
|
key_decisions:
|
||||||
|
- Text objects default to fill '#000000' and stroke 'transparent' (opposite of shape defaults) for natural text appearance
|
||||||
|
- Text transform scales width only (for wrapping), keeping fontSize unchanged — consistent with design tool UX
|
||||||
|
duration: ""
|
||||||
|
verification_result: passed
|
||||||
|
completed_at: 2026-03-26T05:55:43.846Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T02: Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage
|
||||||
|
|
||||||
|
**Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage**
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
|
||||||
|
Added a TextObject interface to canvas.ts extending BaseCanvasObject with text-specific fields (text, fontFamily, fontSize, letterSpacing, lineHeight, fill, stroke, strokeWidth, width for wrapping). Added it to the CanvasObject discriminated union. Then wired the text type through all six canvas component files: KonvaStage (rendering, tool creation, sizing, transform), CanvasToolbar (TOOLS array), ObjectPanel (TYPE_ICONS), ShapeProperties (getWidth/getHeight + hasFill), and AlignmentBar (toBoundingRect). All switch statements are exhaustive for the updated union. TypeScript compiles cleanly and all 95 tests pass.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Ran `cd app && npx tsc --noEmit` — zero errors under strict mode. Ran `cd app && npx vitest run --reporter=verbose` — all 95 tests pass across 7 test files in 2.45s.
|
||||||
|
|
||||||
|
## Verification Evidence
|
||||||
|
|
||||||
|
| # | Command | Exit Code | Verdict | Duration |
|
||||||
|
|---|---------|-----------|---------|----------|
|
||||||
|
| 1 | `cd app && npx tsc --noEmit` | 0 | ✅ pass | 2000ms |
|
||||||
|
| 2 | `cd app && npx vitest run --reporter=verbose` | 0 | ✅ pass | 2450ms |
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
Added case 'text' to AlignmentBar.tsx toBoundingRect — not listed in task plan but required for exhaustive switch coverage.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `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`
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
Added case 'text' to AlignmentBar.tsx toBoundingRect — not listed in task plan but required for exhaustive switch coverage.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
None.
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"exported_at": "2026-03-26T05:52:47.299Z",
|
"exported_at": "2026-03-26T05:55:43.881Z",
|
||||||
"milestones": [
|
"milestones": [
|
||||||
{
|
{
|
||||||
"id": "M001",
|
"id": "M001",
|
||||||
|
|
@ -1329,19 +1329,29 @@
|
||||||
"milestone_id": "M002",
|
"milestone_id": "M002",
|
||||||
"slice_id": "S03",
|
"slice_id": "S03",
|
||||||
"id": "T02",
|
"id": "T02",
|
||||||
"title": "Add TextObject type and wire text tool into canvas rendering",
|
"title": "Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage",
|
||||||
"status": "pending",
|
"status": "complete",
|
||||||
"one_liner": "",
|
"one_liner": "Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage",
|
||||||
"narrative": "",
|
"narrative": "Added a TextObject interface to canvas.ts extending BaseCanvasObject with text-specific fields (text, fontFamily, fontSize, letterSpacing, lineHeight, fill, stroke, strokeWidth, width for wrapping). Added it to the CanvasObject discriminated union. Then wired the text type through all six canvas component files: KonvaStage (rendering, tool creation, sizing, transform), CanvasToolbar (TOOLS array), ObjectPanel (TYPE_ICONS), ShapeProperties (getWidth/getHeight + hasFill), and AlignmentBar (toBoundingRect). All switch statements are exhaustive for the updated union. TypeScript compiles cleanly and all 95 tests pass.",
|
||||||
"verification_result": "",
|
"verification_result": "Ran `cd app && npx tsc --noEmit` — zero errors under strict mode. Ran `cd app && npx vitest run --reporter=verbose` — all 95 tests pass across 7 test files in 2.45s.",
|
||||||
"duration": "",
|
"duration": "",
|
||||||
"completed_at": null,
|
"completed_at": "2026-03-26T05:55:43.833Z",
|
||||||
"blocker_discovered": false,
|
"blocker_discovered": false,
|
||||||
"deviations": "",
|
"deviations": "Added case 'text' to AlignmentBar.tsx toBoundingRect — not listed in task plan but required for exhaustive switch coverage.",
|
||||||
"known_issues": "",
|
"known_issues": "None.",
|
||||||
"key_files": [],
|
"key_files": [
|
||||||
"key_decisions": [],
|
"app/src/types/canvas.ts",
|
||||||
"full_summary_md": "",
|
"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"
|
||||||
|
],
|
||||||
|
"key_decisions": [
|
||||||
|
"Text objects default to fill '#000000' and stroke 'transparent' (opposite of shape defaults) for natural text appearance",
|
||||||
|
"Text transform scales width only (for wrapping), keeping fontSize unchanged — consistent with design tool UX"
|
||||||
|
],
|
||||||
|
"full_summary_md": "---\nid: T02\nparent: S03\nmilestone: M002\nkey_files:\n - app/src/types/canvas.ts\n - app/src/components/canvas/KonvaStage.tsx\n - app/src/components/canvas/CanvasToolbar.tsx\n - app/src/components/canvas/ObjectPanel.tsx\n - app/src/components/canvas/ShapeProperties.tsx\n - app/src/components/canvas/AlignmentBar.tsx\nkey_decisions:\n - Text objects default to fill '#000000' and stroke 'transparent' (opposite of shape defaults) for natural text appearance\n - Text transform scales width only (for wrapping), keeping fontSize unchanged — consistent with design tool UX\nduration: \"\"\nverification_result: passed\ncompleted_at: 2026-03-26T05:55:43.846Z\nblocker_discovered: false\n---\n\n# T02: Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage\n\n**Added TextObject to CanvasObject union and wired text tool into KonvaStage rendering, CanvasToolbar, ObjectPanel, ShapeProperties, and AlignmentBar with exhaustive switch coverage**\n\n## What Happened\n\nAdded a TextObject interface to canvas.ts extending BaseCanvasObject with text-specific fields (text, fontFamily, fontSize, letterSpacing, lineHeight, fill, stroke, strokeWidth, width for wrapping). Added it to the CanvasObject discriminated union. Then wired the text type through all six canvas component files: KonvaStage (rendering, tool creation, sizing, transform), CanvasToolbar (TOOLS array), ObjectPanel (TYPE_ICONS), ShapeProperties (getWidth/getHeight + hasFill), and AlignmentBar (toBoundingRect). All switch statements are exhaustive for the updated union. TypeScript compiles cleanly and all 95 tests pass.\n\n## Verification\n\nRan `cd app && npx tsc --noEmit` — zero errors under strict mode. Ran `cd app && npx vitest run --reporter=verbose` — all 95 tests pass across 7 test files in 2.45s.\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `cd app && npx tsc --noEmit` | 0 | ✅ pass | 2000ms |\n| 2 | `cd app && npx vitest run --reporter=verbose` | 0 | ✅ pass | 2450ms |\n\n\n## Deviations\n\nAdded case 'text' to AlignmentBar.tsx toBoundingRect — not listed in task plan but required for exhaustive switch coverage.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `app/src/types/canvas.ts`\n- `app/src/components/canvas/KonvaStage.tsx`\n- `app/src/components/canvas/CanvasToolbar.tsx`\n- `app/src/components/canvas/ObjectPanel.tsx`\n- `app/src/components/canvas/ShapeProperties.tsx`\n- `app/src/components/canvas/AlignmentBar.tsx`\n",
|
||||||
"description": "Extend the CanvasObject discriminated union with a TextObject type. Wire text objects into all canvas components: KonvaStage (render via Konva Text + case in getObjWidth/getObjHeight + text tool creation in handleStageMouseDown), CanvasToolbar (add 'text' to CanvasTool union and TOOLS array), ObjectPanel (add 'text' icon to TYPE_ICONS), and DesignCanvas (pass through). This follows the exact pattern established in S02 for adding object types.\n\n## Key Implementation Details\n- TextObject interface: extends BaseCanvasObject with type: 'text', text: string, fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number, fill: string, stroke: string, strokeWidth: number, width: number (for wrapping)\n- CanvasTool union in KonvaStage.tsx must add 'text' — this is imported by CanvasToolbar\n- KonvaStage renderObject switch: case 'text' renders <Text> from react-konva with fontFamily, fontSize, fill, stroke, strokeWidth, width, letterSpacing, lineHeight props\n- KonvaStage handleStageMouseDown switch: case 'text' creates a TextObject with defaults (text: 'Text', fontFamily: 'Roboto', fontSize: 24, letterSpacing: 0, lineHeight: 1.2)\n- KonvaStage getObjWidth for text: use obj.width (wrapping width) or estimate from fontSize * text.length * 0.6\n- KonvaStage getObjHeight for text: obj.fontSize * obj.lineHeight\n- KonvaStage onTransformEnd for text: scale width, keep fontSize unchanged (text wraps to new width)\n- ObjectPanel TYPE_ICONS: text → 'T'\n- ShapeProperties getWidth/getHeight: add case 'text' returning obj.width and obj.fontSize * obj.lineHeight\n- All switch statements must be exhaustive — TypeScript noFallthroughCasesInSwitch enforces this\n- TypeScript strict mode — no unused locals/params, erasableSyntaxOnly, use import type",
|
"description": "Extend the CanvasObject discriminated union with a TextObject type. Wire text objects into all canvas components: KonvaStage (render via Konva Text + case in getObjWidth/getObjHeight + text tool creation in handleStageMouseDown), CanvasToolbar (add 'text' to CanvasTool union and TOOLS array), ObjectPanel (add 'text' icon to TYPE_ICONS), and DesignCanvas (pass through). This follows the exact pattern established in S02 for adding object types.\n\n## Key Implementation Details\n- TextObject interface: extends BaseCanvasObject with type: 'text', text: string, fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number, fill: string, stroke: string, strokeWidth: number, width: number (for wrapping)\n- CanvasTool union in KonvaStage.tsx must add 'text' — this is imported by CanvasToolbar\n- KonvaStage renderObject switch: case 'text' renders <Text> from react-konva with fontFamily, fontSize, fill, stroke, strokeWidth, width, letterSpacing, lineHeight props\n- KonvaStage handleStageMouseDown switch: case 'text' creates a TextObject with defaults (text: 'Text', fontFamily: 'Roboto', fontSize: 24, letterSpacing: 0, lineHeight: 1.2)\n- KonvaStage getObjWidth for text: use obj.width (wrapping width) or estimate from fontSize * text.length * 0.6\n- KonvaStage getObjHeight for text: obj.fontSize * obj.lineHeight\n- KonvaStage onTransformEnd for text: scale width, keep fontSize unchanged (text wraps to new width)\n- ObjectPanel TYPE_ICONS: text → 'T'\n- ShapeProperties getWidth/getHeight: add case 'text' returning obj.width and obj.fontSize * obj.lineHeight\n- All switch statements must be exhaustive — TypeScript noFallthroughCasesInSwitch enforces this\n- TypeScript strict mode — no unused locals/params, erasableSyntaxOnly, use import type",
|
||||||
"estimate": "1h",
|
"estimate": "1h",
|
||||||
"files": [
|
"files": [
|
||||||
|
|
@ -1858,6 +1868,28 @@
|
||||||
"verdict": "✅ pass",
|
"verdict": "✅ pass",
|
||||||
"duration_ms": 100,
|
"duration_ms": 100,
|
||||||
"created_at": "2026-03-26T05:52:47.243Z"
|
"created_at": "2026-03-26T05:52:47.243Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 34,
|
||||||
|
"task_id": "T02",
|
||||||
|
"slice_id": "S03",
|
||||||
|
"milestone_id": "M002",
|
||||||
|
"command": "cd app && npx tsc --noEmit",
|
||||||
|
"exit_code": 0,
|
||||||
|
"verdict": "✅ pass",
|
||||||
|
"duration_ms": 2000,
|
||||||
|
"created_at": "2026-03-26T05:55:43.833Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 35,
|
||||||
|
"task_id": "T02",
|
||||||
|
"slice_id": "S03",
|
||||||
|
"milestone_id": "M002",
|
||||||
|
"command": "cd app && npx vitest run --reporter=verbose",
|
||||||
|
"exit_code": 0,
|
||||||
|
"verdict": "✅ pass",
|
||||||
|
"duration_ms": 2450,
|
||||||
|
"created_at": "2026-03-26T05:55:43.833Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 --------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ const TYPE_ICONS: Record<CanvasObject['type'], string> = {
|
||||||
ellipse: '⬯',
|
ellipse: '⬯',
|
||||||
line: '╱',
|
line: '╱',
|
||||||
image: '🖼',
|
image: '🖼',
|
||||||
|
text: 'T',
|
||||||
};
|
};
|
||||||
|
|
||||||
// -- Props --------------------------------------------------------------------
|
// -- Props --------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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'];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue