industrial_drawing_flutter 1.0.3 copy "industrial_drawing_flutter: ^1.0.3" to clipboard
industrial_drawing_flutter: ^1.0.3 copied to clipboard

A high-performance, production-ready vector drawing engine for Flutter. Designed for industrial applications and engineering tools.

example/lib/main.dart

// ignore_for_file: deprecated_member_use, unused_element, unused_local_variable, use_build_context_synchronously
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:industrial_drawing_flutter/industrial_drawing_flutter.dart';

void main() {
  runApp(const IndustrialDrawingApp());
}

class IndustrialDrawingApp extends StatelessWidget {
  const IndustrialDrawingApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Industrial Drawing',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF4361EE),
          brightness: Brightness.light,
        ),
        textTheme: GoogleFonts.interTextTheme(),
      ),
      home: const DrawingScreen(),
    );
  }
}

/// The main drawing screen — assembles all engine widgets
/// and provides app-level features (color picker, text dialogs, save/share).
class DrawingScreen extends StatefulWidget {
  const DrawingScreen({super.key});
  @override
  State<DrawingScreen> createState() => _DrawingScreenState();
}

class _DrawingScreenState extends State<DrawingScreen> {
  final DrawingController _controller = DrawingController(
    config: const DrawingConfig(
      maxUndoSteps: 80,
      enableSmoothing: true,
    ),
  );
  final GlobalKey _canvasKey = GlobalKey();
  bool _loading = false;

  @override
  void initState() {
    super.initState();
    _controller.onTextInputRequested = _showTextDialog;
    _controller.onSelectionChanged = (shape) {
      // Optional: analytics or logging
    };
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  // ════════════════ DIALOGS ════════════════

  void _pickColor() {
    Color temp = _controller.strokeColor;
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
        title: const Text('Pick a Color', style: TextStyle(fontWeight: FontWeight.w600)),
        content: SingleChildScrollView(child: ColorPicker(pickerColor: temp, onColorChanged: (c) => temp = c)),
        actions: [
          TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancel')),
          FilledButton(
            onPressed: () { _controller.strokeColor = temp; Navigator.pop(ctx); },
            style: FilledButton.styleFrom(backgroundColor: const Color(0xFF4361EE)),
            child: const Text('Select'),
          ),
        ],
      ),
    );
  }

  void _pickSelectedShapeColor() {
    if (_controller.selectedShape == null) return;
    Color temp = _controller.selectedShape!.color;
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
        title: const Text('Shape Color', style: TextStyle(fontWeight: FontWeight.w600)),
        content: SingleChildScrollView(child: ColorPicker(pickerColor: temp, onColorChanged: (c) => temp = c)),
        actions: [
          TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancel')),
          FilledButton(
            onPressed: () {
              _controller.saveStateForUndo();
              _controller.selectedShape!.color = temp;
              _controller.updateState();
              Navigator.pop(ctx);
            },
            style: FilledButton.styleFrom(backgroundColor: const Color(0xFF4361EE)),
            child: const Text('Select'),
          ),
        ],
      ),
    );
  }

  void _showTextDialog(Offset position) {
    final tc = TextEditingController();
    double fontSize = 18;
    showDialog(
      context: context,
      builder: (ctx) => StatefulBuilder(
        builder: (_, setStateSB) => AlertDialog(
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
          title: const Text("Add Text", style: TextStyle(color: Color(0xFF4361EE), fontWeight: FontWeight.w600)),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(controller: tc, autofocus: true, decoration: InputDecoration(hintText: "Enter text", border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)))),
              const SizedBox(height: 12),
              Row(
                children: [
                  const Text("Size: ", style: TextStyle(fontWeight: FontWeight.w500)),
                  Expanded(child: Slider(value: fontSize, min: 10, max: 40, divisions: 6, label: fontSize.toStringAsFixed(0), onChanged: (v) => setStateSB(() => fontSize = v))),
                ],
              ),
            ],
          ),
          actions: [
            TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Cancel")),
            FilledButton(
              onPressed: () {
                if (tc.text.isEmpty) return;
                _controller.saveStateForUndo();
                _controller.drawnShapes.add(DrawnShape(position, position, ShapeType.text, texts: {"Center": tc.text}, textPositions: {"Center": position}, color: _controller.strokeColor, strokeWidth: _controller.strokeWidth, fontSize: fontSize));
                _controller.updateState();
                Navigator.pop(ctx);
              },
              style: FilledButton.styleFrom(backgroundColor: const Color(0xFF4361EE)),
              child: const Text("Add"),
            ),
          ],
        ),
      ),
    );
  }

  void _showEditShapeTextDialog() {
    final shape = _controller.selectedShape;
    if (shape == null) return;

    List<String> positions;
    String selectedSide;
    switch (shape.type) {
      case ShapeType.rectangle: positions = ["Top", "Right", "Bottom", "Left"]; selectedSide = "Top"; break;
      case ShapeType.triangle: positions = ["Top", "Left", "Right"]; selectedSide = "Top"; break;
      case ShapeType.circle: positions = ["Center"]; selectedSide = "Center"; break;
      default: positions = ["Center"]; selectedSide = "Center";
    }

    final tc = TextEditingController(text: shape.texts[selectedSide] ?? "");

    showDialog(
      context: context,
      builder: (ctx) => StatefulBuilder(
        builder: (_, setStateD) => AlertDialog(
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
          title: const Text("Edit Text Labels", style: TextStyle(color: Color(0xFF4361EE), fontWeight: FontWeight.w600)),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Wrap(
                alignment: WrapAlignment.center, spacing: 12, runSpacing: 12,
                children: positions.map((pos) {
                  final icon = _iconForSide(pos);
                  return IconButton(
                    icon: Icon(icon, color: selectedSide == pos ? const Color(0xFF4361EE) : Colors.black54),
                    tooltip: pos,
                    onPressed: () => setStateD(() {
                      shape.texts[selectedSide] = tc.text;
                      selectedSide = pos;
                      tc.text = shape.texts[selectedSide] ?? "";
                    }),
                  );
                }).toList(),
              ),
              const SizedBox(height: 10), const Divider(),
              TextField(controller: tc, autofocus: true, decoration: InputDecoration(hintText: "Enter text", border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)))),
            ],
          ),
          actions: [
            TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Cancel")),
            FilledButton(
              onPressed: () {
                _controller.saveStateForUndo();
                shape.texts[selectedSide] = tc.text;
                final rect = Rect.fromPoints(shape.start, shape.end);
                const pad = 20.0;
                Offset offset;
                switch (selectedSide) {
                  case "Top": offset = Offset(rect.center.dx, rect.top - pad - 5); break;
                  case "Bottom": offset = Offset(rect.center.dx, rect.bottom + pad - 15); break;
                  case "Left": offset = Offset(rect.left - pad - 20, rect.center.dy); break;
                  case "Right": offset = Offset(rect.right + pad - 15, rect.center.dy); break;
                  default: offset = rect.center;
                }
                shape.textPositions[selectedSide] = offset;
                _controller.updateState();
                Navigator.pop(ctx);
              },
              style: FilledButton.styleFrom(backgroundColor: const Color(0xFF4361EE)),
              child: const Text("Update"),
            ),
          ],
        ),
      ),
    );
  }

  IconData _iconForSide(String side) {
    switch (side) {
      case "Top": return Icons.arrow_upward;
      case "Bottom": return Icons.arrow_downward;
      case "Left": return Icons.arrow_back;
      case "Right": return Icons.arrow_forward;
      case "Center": return Icons.circle;
      default: return Icons.crop_square;
    }
  }

  Future<void> _handleSave() async {
    if (_controller.drawnShapes.isEmpty) {
      _showSnackBar("Canvas is empty!", isError: true);
      return;
    }
    final confirmed = await _showConfirmDialog(
      title: "Save Drawing",
      message: "Save the current drawing locally?",
      confirmText: "Save",
      confirmColor: const Color(0xFF4361EE),
    );
    if (confirmed == true) {
      setState(() => _loading = true);
      try {
        final pngBytes = await _controller.exportAsPNG(_canvasKey, pixelRatio: 2);
        if (pngBytes == null) throw Exception("Failed to export canvas");
        final dir = await getTemporaryDirectory();
        final ts = DateTime.now().millisecondsSinceEpoch;
        await File('${dir.path}/drawing_$ts.png').writeAsBytes(pngBytes);
        await File('${dir.path}/drawing_$ts.json').writeAsString(_controller.toJson());
        if (mounted) _showSnackBar("Drawing saved successfully! ✓");
      } catch (e) {
        if (mounted) _showSnackBar("Error: $e", isError: true);
      } finally {
        setState(() => _loading = false);
      }
    }
  }

  Future<void> _handleShare() async {
    if (_controller.drawnShapes.isEmpty) {
      _showSnackBar("Canvas is empty!", isError: true);
      return;
    }
    setState(() => _loading = true);
    try {
      final pngBytes = await _controller.exportAsPNG(_canvasKey, pixelRatio: 2);
      if (pngBytes == null) throw Exception("Failed to export canvas");
      final dir = await getTemporaryDirectory();
      final file = await File('${dir.path}/drawing_${DateTime.now().millisecondsSinceEpoch}.png').writeAsBytes(pngBytes);
      await Share.shareXFiles([XFile(file.path)], text: "Check out my industrial drawing!");
    } catch (e) {
      if (mounted) _showSnackBar("Error: $e", isError: true);
    } finally {
      setState(() => _loading = false);
    }
  }

  Future<void> _handleClear() async {
    if (_controller.drawnShapes.isEmpty) return;
    final confirmed = await _showConfirmDialog(
      title: "Clear Canvas",
      message: "Are you sure? This will clear all shapes.",
      confirmText: "Clear All",
      confirmColor: const Color(0xFFE63946),
    );
    if (confirmed == true) _controller.clearAll();
  }

  Future<bool?> _showConfirmDialog({
    required String title,
    required String message,
    required String confirmText,
    required Color confirmColor,
  }) {
    return showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
        title: Text(title, style: const TextStyle(fontWeight: FontWeight.w600)),
        content: Text(message),
        actions: [
          TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text("Cancel")),
          FilledButton(
            onPressed: () => Navigator.pop(ctx, true),
            style: FilledButton.styleFrom(backgroundColor: confirmColor),
            child: Text(confirmText),
          ),
        ],
      ),
    );
  }

  void _showSnackBar(String text, {bool isError = false}) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Text(text),
      backgroundColor: isError ? const Color(0xFFE63946) : const Color(0xFF4361EE),
      behavior: SnackBarBehavior.floating,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
      margin: const EdgeInsets.all(16),
    ));
  }

  // ════════════════ UI ════════════════

  @override
  Widget build(BuildContext context) {
    if (_loading) {
      return const Scaffold(
        backgroundColor: Color(0xFF0F172A),
        body: Center(child: CircularProgressIndicator(color: Color(0xFF3B82F6))),
      );
    }

    return PopScope(
      canPop: false,
      onPopInvokedWithResult: (didPop, _) async {
        if (didPop) return;
        final shouldPop = await _showConfirmDialog(
          title: "Exit Drawing?",
          message: "Any unsaved changes will be lost.",
          confirmText: "Exit",
          confirmColor: const Color(0xFFEF4444),
        );
        if (shouldPop ?? false) { if (mounted) Navigator.pop(context); }
      },
      child: Scaffold(
        backgroundColor: const Color(0xFF0F172A), // Dark slate bg
        body: Stack(
          children: [
            // 1) The main canvas spans the entire screen
            DrawingCanvas(controller: _controller, canvasKey: _canvasKey),
            
            // 2) UI Overlays
            SafeArea(
              child: ListenableBuilder(
                listenable: _controller,
                builder: (context, _) => Stack(
                  children: [
                    // Suggestions overlay mapping (Hold to draw)
                    if (_controller.suggestedShapes != null && _controller.suggestionPosition != null)
                      Positioned(
                        top: _controller.suggestionPosition!.dy - 60,
                        left: _controller.suggestionPosition!.dx - 80,
                        child: _buildSuggestionBar(),
                      ),

                    // Measurement overlay
                    if (_controller.measurementValue.isNotEmpty)
                      Positioned(
                        top: 80, left: 0, right: 0,
                        child: Center(
                          child: Container(
                            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                            decoration: BoxDecoration(
                              color: Colors.black.withValues(alpha: 0.6),
                              borderRadius: BorderRadius.circular(20),
                              border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
                              boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.2), blurRadius: 10)],
                            ),
                            child: Text(
                              "📏 ${_controller.measurementValue}", 
                              style: const TextStyle(fontWeight: FontWeight.w700, color: Color(0xFF38BDF8), fontSize: 13, letterSpacing: 0.5)
                            ),
                          )
                        ),
                      ),

                    // Canvas controls (Left side floating)
                    Positioned(
                      left: 16, 
                      top: 80, 
                      child: CanvasControls(controller: _controller)
                    ),

                    // Property panel (Right side floating)
                    Positioned(
                      right: 16, 
                      top: 80, 
                      child: PropertyPanel(
                        controller: _controller,
                        onPickColor: _pickColor,
                        onPickSelectedShapeColor: _pickSelectedShapeColor,
                        onEditText: _showEditShapeTextDialog,
                      )
                    ),

                    // Bottom toolbar (bottom anchored)
                    Positioned(
                      left: 0,
                      right: 0,
                      bottom: 0,
                      child: DrawingToolbar(controller: _controller),
                    ),

                    // Header floating on top (MUST BE LAST to be on top layer)
                    Positioned(
                      top: 0,
                      left: 0,
                      right: 0,
                      child: DrawingHeader(
                        controller: _controller,
                        title: 'Industrial Drawing',
                        onSave: _handleSave,
                        onShare: _handleShare,
                        onClear: _handleClear,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSuggestionBar() {
    return ClipRRect(
      borderRadius: BorderRadius.circular(20),
      child: BackdropFilter(
        filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
          decoration: BoxDecoration(
            color: Colors.black.withValues(alpha: 0.6),
            borderRadius: BorderRadius.circular(20),
            border: Border.all(color: Colors.white.withValues(alpha: 0.15)),
            boxShadow: [
              BoxShadow(color: Colors.black.withValues(alpha: 0.2), blurRadius: 10, offset: const Offset(0, 4))
            ],
          ),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              ..._controller.suggestedShapes!.map((type) => IconButton(
                icon: Icon(_iconForShape(type), size: 20, color: Colors.white.withValues(alpha: 0.9)),
                tooltip: type.name,
                onPressed: () => _controller.acceptSuggestion(type),
                splashRadius: 18,
              )),
              Container(width: 1, height: 20, color: Colors.white.withValues(alpha: 0.2), margin: const EdgeInsets.symmetric(horizontal: 4)),
              IconButton(
                icon: const Icon(Icons.close_rounded, size: 18, color: Color(0xFFF87171)),
                onPressed: _controller.discardSuggestion,
                splashRadius: 18,
              ),
            ],
          ),
        ),
      ),
    );
  }

  IconData _iconForShape(ShapeType type) {
    switch (type) {
      case ShapeType.line: return Icons.horizontal_rule_rounded;
      case ShapeType.rectangle: return Icons.crop_square_rounded;
      case ShapeType.circle: return Icons.circle_outlined;
      case ShapeType.triangle: return Icons.change_history_rounded;
      case ShapeType.arrow: return Icons.north_east_rounded;
      case ShapeType.star: return Icons.star_outline_rounded;
      case ShapeType.polygon: return Icons.hexagon_outlined;
      default: return Icons.help_outline_rounded;
    }
  }
}
3
likes
160
points
159
downloads

Publisher

verified publisherakhilnasim.com

Weekly Downloads

A high-performance, production-ready vector drawing engine for Flutter. Designed for industrial applications and engineering tools.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

cupertino_icons, flutter, flutter_colorpicker, google_fonts, icons_plus, path_provider, share_plus

More

Packages that depend on industrial_drawing_flutter