Skip to content

Commit 47b8af8

Browse files
committed
feat: connectors
1 parent 61f2549 commit 47b8af8

File tree

6 files changed

+322
-65
lines changed

6 files changed

+322
-65
lines changed

lib/core/utils/enums.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ enum DrawMode {
4545
invertedTriangle(iconData: Icons.warning_amber_rounded), // Placeholder
4646
textBox(iconData: PhosphorIconsRegular.textT),
4747
stickyNote(iconData: PhosphorIconsRegular.notePencil);
48+
// connector(iconData: PhosphorIconsRegular.lineSegment);
4849

4950
const DrawMode({required this.iconData});
5051
final IconData iconData;
@@ -58,4 +59,7 @@ enum InteractionMode {
5859
resizingBottomLeft,
5960
resizingBottomRight,
6061
editingText,
62+
drawingConnector,
6163
}
64+
65+
enum ConnectorAnchor { top, bottom, left, right }

lib/features/models/canvas_models/canvas_object.dart

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import 'dart:convert';
22
import 'dart:math';
33
import 'package:cookethflow/features/models/canvas_models/objects/circle_object.dart';
4+
import 'package:cookethflow/features/models/canvas_models/objects/connector_object.dart';
45
import 'package:cookethflow/features/models/canvas_models/objects/cylinder_object.dart';
56
import 'package:cookethflow/features/models/canvas_models/objects/diamond_object.dart';
67
import 'package:cookethflow/features/models/canvas_models/objects/inverted_triangle_object.dart';
78
import 'package:cookethflow/features/models/canvas_models/objects/parallelogram_object.dart';
89
import 'package:cookethflow/features/models/canvas_models/objects/rectangle_object.dart';
910
import 'package:cookethflow/features/models/canvas_models/objects/rounded_square_object.dart';
1011
import 'package:cookethflow/features/models/canvas_models/objects/square_object.dart';
11-
import 'package:cookethflow/features/models/canvas_models/objects/sticky_note_object.dart'; // Import StickyNoteObject
12+
import 'package:cookethflow/features/models/canvas_models/objects/sticky_note_object.dart';
1213
import 'package:cookethflow/features/models/canvas_models/objects/text_box_object.dart';
1314
import 'package:cookethflow/features/models/canvas_models/objects/triangle_object.dart';
1415
import 'package:cookethflow/features/models/canvas_models/synced_object.dart';
@@ -54,11 +55,13 @@ abstract class CanvasObject extends SyncedObject {
5455
return Triangle.fromJson(json);
5556
case InvertedTriangle.type:
5657
return InvertedTriangle.fromJson(json);
57-
// NEW: Add case for StickyNoteObject
5858
case StickyNoteObject.type:
5959
return StickyNoteObject.fromJson(json);
6060
case TextBoxObject.type:
6161
return TextBoxObject.fromJson(json);
62+
// NEW: Add case for ConnectorObject
63+
case ConnectorObject.type:
64+
return ConnectorObject.fromJson(json);
6265
default:
6366
throw UnimplementedError('Unknown object_type: $objectType');
6467
}
@@ -72,4 +75,14 @@ abstract class CanvasObject extends SyncedObject {
7275
Offset newTopLeft,
7376
Offset newBottomRight,
7477
);
78+
79+
// NEW: Method to get connection point coordinates
80+
Offset getConnectionPoint(Alignment alignment) {
81+
final bounds = getBounds();
82+
if (alignment == Alignment.centerLeft) return bounds.centerLeft;
83+
if (alignment == Alignment.centerRight) return bounds.centerRight;
84+
if (alignment == Alignment.topCenter) return bounds.topCenter;
85+
if (alignment == Alignment.bottomCenter) return bounds.bottomCenter;
86+
return bounds.center; // Default
87+
}
7588
}

lib/features/models/canvas_models/canvas_painter.dart

Lines changed: 95 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:math';
33
import 'package:cookethflow/core/utils/enums.dart';
44
import 'package:cookethflow/features/models/canvas_models/canvas_object.dart';
55
import 'package:cookethflow/features/models/canvas_models/objects/circle_object.dart';
6+
import 'package:cookethflow/features/models/canvas_models/objects/connector_object.dart';
67
import 'package:cookethflow/features/models/canvas_models/objects/cylinder_object.dart';
78
import 'package:cookethflow/features/models/canvas_models/objects/diamond_object.dart';
89
import 'package:cookethflow/features/models/canvas_models/objects/inverted_triangle_object.dart';
@@ -24,15 +25,27 @@ class CanvasPainter extends CustomPainter {
2425
final double handleRadius;
2526
final InteractionMode interactionMode;
2627

28+
// NEW: Connector-related properties
29+
final double connectionPointRadius;
30+
final String? connectorSourceId;
31+
final Alignment? connectorSourceAlignment;
32+
final Offset? connectorDragPosition;
33+
2734
CanvasPainter({
2835
required this.userCursors,
2936
required this.canvasObjects,
3037
this.currentlySelectedObjectId,
3138
this.handleRadius = 8.0,
3239
required this.interactionMode,
40+
// NEW: Initialize connector properties
41+
this.connectionPointRadius = 6.0,
42+
this.connectorSourceId,
43+
this.connectorSourceAlignment,
44+
this.connectorDragPosition,
3345
});
3446

35-
Color _parseColor(String? colorString) {
47+
// ... (_parseColor, _getFontSize, _getTextStyle methods remain the same)
48+
Color _parseColor(String? colorString) {
3649
if (colorString == null) return Colors.black;
3750
try {
3851
final hex = colorString.replaceAll('#', '');
@@ -90,31 +103,56 @@ class CanvasPainter extends CustomPainter {
90103
);
91104
}
92105

106+
void _drawArrowhead(Canvas canvas, Offset start, Offset end, Paint paint) {
107+
final double arrowSize = 12;
108+
final double arrowAngle = 25 * pi / 180;
109+
final angle = atan2(end.dy - start.dy, end.dx - start.dx);
110+
111+
final path = Path();
112+
path.moveTo(end.dx - arrowSize * cos(angle - arrowAngle), end.dy - arrowSize * sin(angle - arrowAngle));
113+
path.lineTo(end.dx, end.dy);
114+
path.lineTo(end.dx - arrowSize * cos(angle + arrowAngle), end.dy - arrowSize * sin(angle + arrowAngle));
115+
canvas.drawPath(path, paint..style=PaintingStyle.stroke);
116+
}
117+
93118
@override
94119
void paint(Canvas canvas, Size size) {
95-
for (final canvasObject in canvasObjects.values) {
120+
final shapeObjects = canvasObjects.values.where((obj) => obj is! ConnectorObject);
121+
final connectorObjects = canvasObjects.values.whereType<ConnectorObject>();
122+
123+
// 1. Draw all connectors first (so they appear behind shapes)
124+
for (final connector in connectorObjects) {
125+
final source = canvasObjects[connector.sourceId];
126+
final target = canvasObjects[connector.targetId];
127+
128+
if (source != null && target != null) {
129+
final startPoint = source.getConnectionPoint(connector.sourceAlignment);
130+
final endPoint = target.getConnectionPoint(connector.targetAlignment);
131+
final paint = Paint()
132+
// ..color(Colors.black87)
133+
..strokeWidth = 2.0
134+
..style = PaintingStyle.stroke;
135+
136+
canvas.drawLine(startPoint, endPoint, paint);
137+
_drawArrowhead(canvas, startPoint, endPoint, paint);
138+
}
139+
}
140+
141+
// 2. Draw all shapes and their decorations
142+
for (final canvasObject in shapeObjects) {
96143
final paint = Paint()..color = canvasObject.color;
97144
Rect rect;
98145

146+
// ... (existing shape drawing logic remains the same)
99147
if (canvasObject is Circle) {
100148
canvas.drawCircle(canvasObject.center, canvasObject.radius, paint);
101149
rect = Rect.fromCircle(center: canvasObject.center, radius: canvasObject.radius);
102150
} else if (canvasObject is StickyNoteObject) {
103151
rect = canvasObject.getBounds();
104-
// Define a dynamic border thickness
105152
final double borderThickness = min(min(rect.width, rect.height) * 0.05, 5.0);
106-
107-
// The outer rect is the darker border
108153
final borderPaint = Paint()..color = Color.lerp(canvasObject.color, Colors.black, 0.1)!;
109154
canvas.drawRect(rect, borderPaint);
110-
111-
// The inner rect is the lighter main body
112-
final bodyRect = Rect.fromLTRB(
113-
rect.left + borderThickness,
114-
rect.top + borderThickness,
115-
rect.right - borderThickness,
116-
rect.bottom - borderThickness,
117-
);
155+
final bodyRect = Rect.fromLTRB(rect.left + borderThickness, rect.top + borderThickness, rect.right - borderThickness, rect.bottom - borderThickness);
118156
final bodyPaint = Paint()..color = canvasObject.color;
119157
canvas.drawRect(bodyRect, bodyPaint);
120158
} else if (canvasObject is TextBoxObject) {
@@ -152,41 +190,31 @@ class CanvasPainter extends CustomPainter {
152190
canvas.drawOval(Rect.fromCenter(center: bodyRect.bottomCenter, width: rect.width, height: ellipseHeight), paint);
153191
}
154192
}
155-
193+
194+
// ... (existing text drawing logic remains the same)
156195
final bool isEditingText = interactionMode == InteractionMode.editingText && currentlySelectedObjectId == canvasObject.id;
157-
158196
if (canvasObject.textDelta != null && canvasObject.textDelta!.isNotEmpty && !isEditingText) {
159197
try {
160198
final List<dynamic> delta = jsonDecode(canvasObject.textDelta!);
161-
162-
// Define a base padding and adjust it for sticky notes
163199
double textPadding = 8.0;
164200
if (canvasObject is StickyNoteObject) {
165201
final double borderThickness = min(min(rect.width, rect.height) * 0.05, 5.0);
166202
textPadding += borderThickness;
167203
}
168-
169204
double yOffset = rect.top + textPadding;
170-
171205
final List<Map<String, dynamic>> lines = [];
172206
List<Map<String, dynamic>> currentLineOps = [];
173-
174207
for (final op in delta) {
175208
final String text = op['insert'];
176209
final Map<String, dynamic>? attributes = op['attributes'] as Map<String, dynamic>?;
177-
178210
if (text.contains('\n')) {
179211
final textLines = text.split('\n');
180212
for (int i = 0; i < textLines.length; i++) {
181213
if (textLines[i].isNotEmpty) {
182214
currentLineOps.add({'insert': textLines[i], 'attributes': attributes});
183215
}
184-
185-
if (i < textLines.length - 1) { // Line break
186-
lines.add({
187-
'ops': List.from(currentLineOps),
188-
'attributes': attributes ?? {},
189-
});
216+
if (i < textLines.length - 1) {
217+
lines.add({'ops': List.from(currentLineOps), 'attributes': attributes ?? {}});
190218
currentLineOps.clear();
191219
}
192220
}
@@ -197,45 +225,38 @@ class CanvasPainter extends CustomPainter {
197225
if (currentLineOps.isNotEmpty) {
198226
lines.add({'ops': currentLineOps, 'attributes': {}});
199227
}
200-
201228
int orderedListCounter = 1;
202229
for(final line in lines) {
203230
final lineOps = List<Map<String, dynamic>>.from(line['ops'] as List);
204231
final blockAttributes = line['attributes'] as Map<String, dynamic>;
205-
206232
final lineSpans = lineOps.map((o) => TextSpan(text: o['insert'], style: _getTextStyle(o['attributes'] as Map<String, dynamic>?))).toList();
207-
208233
String prefix = '';
209234
double indent = 0;
210235
if (blockAttributes['list'] == 'bullet') {
211236
prefix = '• ';
212237
indent = 10.0;
213-
orderedListCounter = 1; // Reset ordered list
238+
orderedListCounter = 1;
214239
} else if (blockAttributes['list'] == 'ordered') {
215240
prefix = '$orderedListCounter. ';
216241
indent = 10.0;
217242
orderedListCounter++;
218243
} else {
219-
orderedListCounter = 1; // Reset
244+
orderedListCounter = 1;
220245
}
221-
222246
if (blockAttributes['blockquote'] == true) {
223247
indent = 20.0;
224248
final blockPaint = Paint()..color = Colors.grey.shade300..strokeWidth = 2;
225-
canvas.drawLine(Offset(rect.left + textPadding, yOffset), Offset(rect.left + textPadding, yOffset + 20), blockPaint); // Approximate height
249+
canvas.drawLine(Offset(rect.left + textPadding, yOffset), Offset(rect.left + textPadding, yOffset + 20), blockPaint);
226250
}
227-
228251
if (blockAttributes['code-block'] == true) {
229252
final blockPaint = Paint()..color = Colors.grey.shade200;
230-
canvas.drawRect(Rect.fromLTWH(rect.left, yOffset, rect.width, 20), blockPaint); // Approximate height
253+
canvas.drawRect(Rect.fromLTWH(rect.left, yOffset, rect.width, 20), blockPaint);
231254
}
232-
233255
final textPainter = TextPainter(
234256
text: TextSpan(children: [TextSpan(text: prefix, style: _getTextStyle(blockAttributes)), ...lineSpans]),
235257
textDirection: TextDirection.ltr,
236258
textAlign: TextAlign.start,
237259
);
238-
239260
final availableWidth = rect.width - (2 * textPadding) - indent;
240261
if (availableWidth > 0) {
241262
textPainter.layout(maxWidth: availableWidth);
@@ -248,7 +269,7 @@ class CanvasPainter extends CustomPainter {
248269
}
249270
}
250271

251-
272+
// Draw resize handles and connection points for the selected object
252273
if (canvasObject.id == currentlySelectedObjectId && !isEditingText) {
253274
final handlePaint = Paint()..color = Colors.blue..style = PaintingStyle.fill;
254275
canvas.drawCircle(rect.topLeft, handleRadius, handlePaint);
@@ -259,9 +280,42 @@ class CanvasPainter extends CustomPainter {
259280
final borderPaint = Paint()..color = Colors.blue..style = PaintingStyle.stroke..strokeWidth = 2.0;
260281
final path = Path()..addRect(rect);
261282
canvas.drawPath(dashPath(path, dashArray: CircularIntervalList<double>([5.0, 3.0])), borderPaint);
283+
284+
// NEW: Draw connection points for the selected object
285+
final connectionPointPaint = Paint()..color = Colors.white..style = PaintingStyle.fill;
286+
final connectionPointBorderPaint = Paint()..color = Colors.blue..style = PaintingStyle.stroke..strokeWidth = 1.5;
287+
288+
final points = [
289+
canvasObject.getConnectionPoint(Alignment.topCenter),
290+
canvasObject.getConnectionPoint(Alignment.bottomCenter),
291+
canvasObject.getConnectionPoint(Alignment.centerLeft),
292+
canvasObject.getConnectionPoint(Alignment.centerRight),
293+
];
294+
295+
for (final point in points) {
296+
canvas.drawCircle(point, connectionPointRadius, connectionPointPaint);
297+
canvas.drawCircle(point, connectionPointRadius, connectionPointBorderPaint);
298+
}
299+
}
300+
}
301+
302+
// 3. Draw the temporary connector line while dragging
303+
if (interactionMode == InteractionMode.drawingConnector &&
304+
connectorSourceId != null &&
305+
connectorDragPosition != null) {
306+
final sourceObject = canvasObjects[connectorSourceId!];
307+
if (sourceObject != null) {
308+
final startPoint = sourceObject.getConnectionPoint(connectorSourceAlignment!);
309+
final endPoint = connectorDragPosition!;
310+
final paint = Paint()..color = Colors.blue..strokeWidth = 2.0..style = PaintingStyle.stroke;
311+
312+
final path = Path()..moveTo(startPoint.dx, startPoint.dy)..lineTo(endPoint.dx, endPoint.dy);
313+
canvas.drawPath(dashPath(path, dashArray: CircularIntervalList<double>([5.0, 3.0])), paint);
314+
_drawArrowhead(canvas, startPoint, endPoint, paint);
262315
}
263316
}
264317

318+
// 4. Draw user cursors on top of everything
265319
for (final userCursor in userCursors.values) {
266320
final position = userCursor.position;
267321
final paint = Paint()..color = userCursor.color..strokeWidth = 2;
@@ -277,7 +331,8 @@ class CanvasPainter extends CustomPainter {
277331
oldPainter.canvasObjects.length != canvasObjects.length ||
278332
oldPainter.currentlySelectedObjectId != currentlySelectedObjectId ||
279333
oldPainter.interactionMode != interactionMode ||
280-
_hasCanvasObjectsChanged(oldPainter.canvasObjects, canvasObjects);
334+
_hasCanvasObjectsChanged(oldPainter.canvasObjects, canvasObjects) ||
335+
oldPainter.connectorDragPosition != connectorDragPosition; // Add check for connector drag
281336
}
282337

283338
bool _hasCanvasObjectsChanged(Map<String, CanvasObject> oldObjects, Map<String, CanvasObject> newObjects) {

0 commit comments

Comments
 (0)