commit 64e6838fdbfe8d8fb1a43362426c4ff145e3e75a
parent b3b272fa105349e7f5f0da5f014444fe8fb43ed2
Author: Sanjeebani Parida <83271316+sanjeebani14@users.noreply.github.com>
Date: Tue, 2 Dec 2025 17:24:25 +0530
Merge pull request #33 from nilotpal-n7/sanjeebani
Sanjeebani
Diffstat:
1 file changed, 323 insertions(+), 54 deletions(-)
diff --git a/lib/ui/pages/canvas_board_page.dart b/lib/ui/pages/canvas_board_page.dart
@@ -169,12 +169,19 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
if (widget.existingFile != null) {
_loadCanvasFromFile();
} else if (widget.initialImage != null) {
+ final double imageWidth = _canvasSize.width * 0.4; // 40% of canvas width
+ final double imageHeight =
+ _canvasSize.height * 0.4; // 40% of canvas height
+ final Offset centeredPosition = Offset(
+ (_canvasSize.width - imageWidth) / 2,
+ (_canvasSize.height - imageHeight) / 2,
+ );
elements.add({
'id': 'bg_${DateTime.now().millisecondsSinceEpoch}',
'type': 'file_image',
'content': widget.initialImage!.path,
- 'position': const Offset(0, 0),
- 'size': Size(widget.width, widget.height),
+ 'position': centeredPosition,
+ 'size': Size(imageWidth, imageHeight),
'rotation': 0.0,
});
_hasUnsavedChanges = true;
@@ -820,12 +827,20 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
if (widget.injectedMedia != null) {
final oldState = _getCurrentState();
setState(() {
+ final double imageWidth =
+ _canvasSize.width * 0.4; // 40% of canvas width
+ final double imageHeight =
+ _canvasSize.height * 0.4; // 40% of canvas height
+ final Offset centeredPosition = Offset(
+ (_canvasSize.width - imageWidth) / 2,
+ (_canvasSize.height - imageHeight) / 2,
+ );
elements.add({
'id': 'shared_${DateTime.now().millisecondsSinceEpoch}',
'type': 'file_image',
'content': widget.injectedMedia!.path,
- 'position': const Offset(50, 50),
- 'size': const Size(150, 150),
+ 'position': centeredPosition,
+ 'size': Size(imageWidth, imageHeight),
'rotation': 0.0,
});
});
@@ -928,13 +943,21 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
final oldState = _getCurrentState();
setState(() {
for (var path in paths) {
+ final double imageWidth =
+ _canvasSize.width * 0.4; // 40% of canvas width
+ final double imageHeight =
+ _canvasSize.height * 0.4; // 40% of canvas height
+ final Offset centeredPosition = Offset(
+ (_canvasSize.width - imageWidth) / 2,
+ (_canvasSize.height - imageHeight) / 2,
+ );
elements.add({
'id':
'asset_${DateTime.now().millisecondsSinceEpoch}_${math.Random().nextInt(1000)}',
'type': 'file_image',
'content': path,
- 'position': const Offset(50, 50),
- 'size': const Size(150, 150),
+ 'position': centeredPosition,
+ 'size': Size(imageWidth, imageHeight),
'rotation': 0.0,
});
}
@@ -1709,12 +1732,20 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
final oldState = _getCurrentState();
setState(() {
for (int i = 0; i < images.length; i++) {
+ final double imageWidth =
+ _canvasSize.width * 0.4; // 40% of canvas width
+ final double imageHeight =
+ _canvasSize.height * 0.4; // 40% of canvas height
+ final Offset centeredPosition = Offset(
+ (_canvasSize.width - imageWidth) / 2,
+ (_canvasSize.height - imageHeight) / 2,
+ );
elements.add({
'id': '${DateTime.now().millisecondsSinceEpoch}_$i',
'type': 'file_image',
'content': images[i].path,
- 'position': const Offset(50, 50),
- 'size': const Size(150, 150),
+ 'position': centeredPosition,
+ 'size': Size(imageWidth, imageHeight),
'rotation': 0.0,
});
}
@@ -1807,10 +1838,11 @@ class _ManipulatingBoxState extends State<_ManipulatingBox> {
return ValueListenableBuilder(
valueListenable: widget.transformationController,
builder: (context, matrix, child) {
- final double currentZoom = matrix.getMaxScaleOnAxis();
- final double handleScale = (1.0 / currentZoom).clamp(0.1, 5.0);
- final double touchTargetSize = 40.0 * handleScale;
- final double visualSize = 24.0 * handleScale;
+ final double zoom = matrix.getMaxScaleOnAxis();
+ final double handleScale = (1 / zoom).clamp(0.2, 5.0);
+ final double edgeThickness = 18 * handleScale;
+ final double buttonSize = 32 * handleScale;
+ final double iconSize = 14 * handleScale;
return Positioned(
left: _pos.dx,
@@ -1821,15 +1853,15 @@ class _ManipulatingBoxState extends State<_ManipulatingBox> {
clipBehavior: Clip.none,
children: [
GestureDetector(
- behavior: HitTestBehavior.translucent,
onTap: widget.onTap,
onDoubleTap: widget.onDoubleTap,
+ //moving element on drag works
onPanStart: (_) => widget.onDragStart(),
onPanUpdate: (details) {
if (widget.isSelected && !widget.isEditing) {
final delta = details.delta;
- final globalDelta = _rotateVector(delta, _rot);
- setState(() => _pos += globalDelta);
+ final rotated = _rotateVector(delta, _rot);
+ setState(() => _pos += rotated);
widget.onUpdate(_pos, _size, _rot);
}
},
@@ -1837,22 +1869,17 @@ class _ManipulatingBoxState extends State<_ManipulatingBox> {
child: Container(
width: _size.width,
height: _size.height,
- decoration: BoxDecoration(
- border:
- widget.isSelected
- ? Border.all(
- color: Colors.blue,
- width: 2.0 * handleScale,
- )
- : (widget.type == 'text'
- ? Border.all(
- color: Colors.grey.withOpacity(0.3),
- width: 1.0 * handleScale,
- )
- : null),
- ),
+ decoration:
+ widget.isSelected
+ ? BoxDecoration(
+ border: Border.all(
+ color: Color(0xFFB44CFF),
+ width: 2 * handleScale,
+ ),
+ )
+ : null,
child:
- widget.type == 'file_image'
+ widget.type == "file_image"
? Image.file(
File(widget.content),
fit: BoxFit.contain,
@@ -1860,49 +1887,234 @@ class _ManipulatingBoxState extends State<_ManipulatingBox> {
: _buildText(),
),
),
+
+ // ======================
+ // RESIZE EDGES (4 SIDES)
+ // ======================
+
+ // RIGHT edge
+ if (widget.isSelected && !widget.isEditing)
+ Positioned(
+ right: -edgeThickness / 2,
+ top: 0,
+ bottom: 0,
+ child: GestureDetector(
+ behavior: HitTestBehavior.translucent,
+ onPanUpdate: (d) {
+ setState(() {
+ _size = Size(_size.width + d.delta.dx, _size.height);
+ });
+ widget.onUpdate(_pos, _size, _rot);
+ },
+ onPanStart: (_) => widget.onDragStart(),
+ onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot),
+ child: Container(
+ width: edgeThickness,
+ color: Colors.transparent,
+ ),
+ ),
+ ),
+
+ // LEFT edge
+ if (widget.isSelected && !widget.isEditing)
+ Positioned(
+ left: -edgeThickness / 2,
+ top: 0,
+ bottom: 0,
+ child: GestureDetector(
+ behavior: HitTestBehavior.translucent,
+ onPanUpdate: (d) {
+ setState(() {
+ _pos += Offset(d.delta.dx, 0);
+ _size = Size(_size.width - d.delta.dx, _size.height);
+ });
+ widget.onUpdate(_pos, _size, _rot);
+ },
+ onPanStart: (_) => widget.onDragStart(),
+ onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot),
+ child: Container(
+ width: edgeThickness,
+ color: Colors.transparent,
+ ),
+ ),
+ ),
+
+ // TOP edge
+ if (widget.isSelected && !widget.isEditing)
+ Positioned(
+ top: -edgeThickness / 2,
+ left: 0,
+ right: 0,
+ child: GestureDetector(
+ behavior: HitTestBehavior.translucent,
+ onPanUpdate: (d) {
+ setState(() {
+ _pos += Offset(0, d.delta.dy);
+ _size = Size(_size.width, _size.height - d.delta.dy);
+ });
+ widget.onUpdate(_pos, _size, _rot);
+ },
+ onPanStart: (_) => widget.onDragStart(),
+ onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot),
+ child: Container(
+ height: edgeThickness,
+ color: Colors.transparent,
+ ),
+ ),
+ ),
+
+ // BOTTOM edge
+ if (widget.isSelected && !widget.isEditing)
+ Positioned(
+ bottom: -edgeThickness / 2,
+ left: 0,
+ right: 0,
+ child: GestureDetector(
+ behavior: HitTestBehavior.translucent,
+ onPanUpdate: (d) {
+ setState(() {
+ _size = Size(_size.width, _size.height + d.delta.dy);
+ });
+ widget.onUpdate(_pos, _size, _rot);
+ },
+ onPanStart: (_) => widget.onDragStart(),
+ onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot),
+ child: Container(
+ height: edgeThickness,
+ color: Colors.transparent,
+ ),
+ ),
+ ),
+
+ // ======================
+ // CORNER RESIZE HANDLES
+ // ======================
if (widget.isSelected && !widget.isEditing) ...[
+ // TOP-LEFT corner
Positioned(
- right: -visualSize / 2,
- bottom: -visualSize / 2,
- child: _buildHandle(
- touchSize: touchTargetSize,
- visualSize: visualSize,
- icon: Icons.zoom_out_map,
- color: Colors.blue,
- onDrag: (delta) {
+ top: -10 * handleScale,
+ left: -10 * handleScale,
+ child: _cornerHandle(
+ size: 20 * handleScale,
+ onDrag: (d) {
+ final local = _rotateVector(d.delta, -_rot);
setState(() {
- final localDelta = _rotateVector(delta, -_rot);
+ _pos += Offset(local.dx, local.dy);
_size = Size(
- (_size.width + localDelta.dx).clamp(50.0, 10000.0),
- (_size.height + localDelta.dy).clamp(30.0, 10000.0),
+ (_size.width - local.dx).clamp(20, 5000),
+ (_size.height - local.dy).clamp(20, 5000),
);
- final offset = Offset(
- localDelta.dx / 2,
- localDelta.dy / 2,
+ });
+ widget.onUpdate(_pos, _size, _rot);
+ },
+ ),
+ ),
+
+ // TOP-RIGHT corner
+ Positioned(
+ top: -10 * handleScale,
+ right: -10 * handleScale,
+ child: _cornerHandle(
+ size: 20 * handleScale,
+ onDrag: (d) {
+ final local = _rotateVector(d.delta, -_rot);
+ setState(() {
+ _pos += Offset(0, local.dy);
+ _size = Size(
+ (_size.width + local.dx).clamp(20, 5000),
+ (_size.height - local.dy).clamp(20, 5000),
);
- _pos += _rotateVector(offset, _rot);
});
widget.onUpdate(_pos, _size, _rot);
},
),
),
+
+ // BOTTOM-LEFT corner
Positioned(
- right: -visualSize / 2,
- top: -visualSize / 2,
- child: _buildHandle(
- touchSize: touchTargetSize,
- visualSize: visualSize,
- icon: Icons.rotate_right,
- color: Colors.green,
- onDrag: (delta) {
+ bottom: -10 * handleScale,
+ left: -10 * handleScale,
+ child: _cornerHandle(
+ size: 20 * handleScale,
+ onDrag: (d) {
+ final local = _rotateVector(d.delta, -_rot);
setState(() {
- _rot += (delta.dx + delta.dy) * 0.005;
+ _pos += Offset(local.dx, 0);
+ _size = Size(
+ (_size.width - local.dx).clamp(20, 5000),
+ (_size.height + local.dy).clamp(20, 5000),
+ );
+ });
+ widget.onUpdate(_pos, _size, _rot);
+ },
+ ),
+ ),
+
+ // BOTTOM-RIGHT corner
+ Positioned(
+ bottom: -10 * handleScale,
+ right: -10 * handleScale,
+ child: _cornerHandle(
+ size: 20 * handleScale,
+ onDrag: (d) {
+ final local = _rotateVector(d.delta, -_rot);
+ setState(() {
+ _size = Size(
+ (_size.width + local.dx).clamp(20, 5000),
+ (_size.height + local.dy).clamp(20, 5000),
+ );
});
widget.onUpdate(_pos, _size, _rot);
},
),
),
],
+
+ // move not working
+ if (widget.isSelected && !widget.isEditing)
+ Positioned(
+ bottom: -buttonSize - 12,
+ left: _size.width / 2 - buttonSize - 8,
+ child: _buildCircleButton(
+ size: buttonSize,
+ icon: Icons.open_with,
+ iconSize: iconSize,
+ onDrag: (d) {
+ // FIX 1: Convert screen pixel delta (d.delta) to world/canvas units by dividing by zoom.
+ final scaledDelta = d.delta / zoom;
+
+ // FIX 2: Un-rotate the delta by the inverse of the element's rotation.
+ // This translates the screen drag back to the element's un-rotated position space.
+ final finalDelta = _rotateVector(scaledDelta, -_rot);
+
+ setState(() {
+ _pos += finalDelta;
+ });
+ widget.onUpdate(_pos, _size, _rot);
+ },
+ ),
+ ),
+
+ //rotate not working
+ if (widget.isSelected && !widget.isEditing)
+ Positioned(
+ bottom: -buttonSize - 12,
+ left: _size.width / 2 + 8,
+ child: _buildCircleButton(
+ size: buttonSize,
+ icon: Icons.rotate_right,
+ iconSize: iconSize,
+ onDrag: (d) {
+ widget.onDragStart();
+
+ // FIX 3: Ensures responsive rotation by increasing the multiplier.
+ setState(() {
+ _rot += d.delta.dx * 0.05; // Increased sensitivity
+ });
+ widget.onUpdate(_pos, _size, _rot);
+ },
+ ),
+ ),
],
),
),
@@ -1911,6 +2123,63 @@ class _ManipulatingBoxState extends State<_ManipulatingBox> {
);
}
+ Widget _cornerHandle({
+ required double size,
+ required Function(DragUpdateDetails) onDrag,
+ }) {
+ return GestureDetector(
+ behavior: HitTestBehavior.translucent,
+ onPanStart: (_) => widget.onDragStart(),
+ onPanUpdate: onDrag,
+ onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot),
+ child: Container(
+ width: size, // large invisible touch area
+ height: size,
+ alignment: Alignment.center,
+ color: Colors.transparent,
+ child: Container(
+ width: 6 * (1 / widget.viewScale), // <<< tiny visual square
+ height: 6 * (1 / widget.viewScale), // <<< tiny visual square
+ decoration: BoxDecoration(
+ color: Colors.white,
+ border: Border.all(color: Colors.white, width: 1),
+ shape: BoxShape.rectangle,
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildCircleButton({
+ required double size,
+ required IconData icon,
+ required double iconSize,
+ required Function(DragUpdateDetails) onDrag,
+ }) {
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onPanStart: (details) => widget.onDragStart(),
+ onPanUpdate: onDrag,
+ onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot),
+ child: Container(
+ width: size,
+ height: size,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ shape: BoxShape.circle,
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.15),
+ blurRadius: 4,
+ offset: Offset(0, 2),
+ ),
+ ],
+ ),
+ child: Center(child: Icon(icon, size: iconSize, color: Colors.black87)),
+ ),
+ );
+ }
+
Widget _buildText() {
final style = TextStyle(
fontSize: (widget.styleData['style_fontSize'] ?? 24.0) as double,