point_glass 1.1.1 copy "point_glass: ^1.1.1" to clipboard
point_glass: ^1.1.1 copied to clipboard

A Flutter package for 3D point cloud visualization with interactive grid, axis, polygons, and annual sectors.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'package:vector_math/vector_math.dart' as vm;

import 'package:point_glass/point_glass.dart';
import 'widgets.dart';

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  double gridSize = 20;
  double gridStep = 1;

  double axisLength = 0.5;
  bool axisOnOff = true;

  List<PointGlassPolygon> polygons = [
    // 삼각형
    PointGlassPolygon(
      enable: true,
      enableLabel: true,
      labelGroupIndex: 100,
      points: [
        vm.Vector3(-15, -10, 0),
        vm.Vector3(-19.33, -2.5, 0),
        vm.Vector3(-10.67, -2.5, 0),
      ],
      pointSize: 3,
      pointColor: Colors.red,
      isEditable: false,
      lineColor: Colors.blue,
      lineAlpha: 100,
    ),
  ];
  bool polygonOnOff = true;
  bool isEditPolygon = false;

  bool annualSectorOnOff = true;
  double annualSectorStartAngle = 40;
  double annualSectorEndAngle = 140;
  double annualSectorInnerRadius = 2;
  double annualSectorOuterRadius = 4;

  // 필요 시 여기서 Transform3D 초기값 설정
  ValueNotifier<ViewContext> viewContext = ValueNotifier(
    ViewContext(
      model: ModelTransform(),
      camera: PinholeCamera(cameraZ: 10),
      proj: PinholeProjection(focalPx: 800, near: 1, far: 20000),
      canvasCenter: Offset(0, 0),
    ),
  );

  List<PointGlassPoints> pointsGroup = [
    PointGlassPoints(enable: true, points: []),
    PointGlassPoints(enable: true, points: []),
    PointGlassPoints(enable: true, points: []),
    PointGlassPoints(enable: true, points: []),
  ];
  int pointGroupRefreshCount = 0;
  int pointGroupRefreshInc = 1;

  PointGlassViewerMode viewMode = PointGlassViewerMode.rotate;

  bool pointCloud1OnOff = true;
  bool pointCloud2OnOff = true;

  @override
  void initState() {
    super.initState();

    rootBundle.loadString('assets/point_cloud_sample').then((value) {
      PointGlassPoints pg = PointGlassPoints(enable: true, points: []);

      final lines = value.split('\n');
      pg.points = lines.map((line) {
        final parts = line.split(',');
        return PointGlassPoint(
          point: vm.Vector3(
            (double.parse(parts[0]) / 20.0) - 5,
            (double.parse(parts[1]) / 20.0) + 5,
            double.parse(parts[2]) / 20.0,
          ),
          color: Colors.grey,
          alpha: 150,
          strokeWidth: 1,
        );
      }).toList();

      pointsGroup[1] = pg;

      pg = PointGlassPoints(enable: true, points: []);
      pg.points = lines.map((line) {
        final parts = line.split(',');
        return PointGlassPoint(
          point: vm.Vector3(
            (double.parse(parts[0]) / 20.0) + 5,
            (double.parse(parts[1]) / 20.0) + 5,
            double.parse(parts[2]) / 20.0,
          ),
          color: Color(0xFFDECAA0),
          alpha: 150,
          strokeWidth: 1,
        );
      }).toList();

      pointsGroup[2] = pg;
    });

    // 25ms 당 한번 호출
    Timer.periodic(Duration(milliseconds: 25), (timer) {
      for (var i = 0; i < pointsGroup.length; i++) {
        for (var j = 0; j < pointsGroup[i].points.length; j++) {
          setState(() {
            pointsGroup[i].points[j].point.y =
                pointsGroup[i].points[j].point.y + (pointGroupRefreshInc * 0.1);
          });
        }
      }

      if (pointGroupRefreshCount > 50) {
        pointGroupRefreshInc = -1;
      } else if (pointGroupRefreshCount < 1) {
        pointGroupRefreshInc = 1;
      }

      pointGroupRefreshCount += pointGroupRefreshInc;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color.fromARGB(255, 48, 48, 48),
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Row(
          children: [
            Expanded(
              flex: 5,
              child: PointGlassViewer(
                viewContext: viewContext,
                mode: viewMode,
                grid: PointGlassGrid(
                  enable: true,
                  gridSize: gridSize,
                  gridStep: gridStep,
                  enableLabel: true,
                  labelStyle: TextStyle(color: Colors.white.withAlpha(150)),
                ),
                axis: PointGlassAxis(enable: axisOnOff, axisLength: axisLength),
                polygons: polygons,
                annualSectors: [
                  PointGlassAnnualSector(
                    enable: annualSectorOnOff,
                    startAngle: annualSectorStartAngle,
                    endAngle: annualSectorEndAngle,
                    innerRadius: annualSectorInnerRadius,
                    outerRadius: annualSectorOuterRadius,
                    color: Colors.green,
                    alpha: 30,
                    lineColor: Colors.green,
                    lineAlpha: 255,
                    showInnerLine: false,
                    showOuterLine: true,
                  ),
                ],
                pointsGroup: pointsGroup,
              ),
            ),
            Expanded(flex: 3, child: _buildController()),
          ],
        ),
      ),
    );
  }

  Widget _buildController() {
    return Container(
      padding: EdgeInsets.all(10),
      color: Colors.white,
      width: double.infinity,
      height: double.infinity,
      child: SingleChildScrollView(
        child: Column(
          children: [
            _buildGridControlWidgets(),
            _buildAxisControlWidgets(),
            _buildPolygonControlWidgets(),
            _buildAnnualSectorControlWidgets(),
            _buildViewerControlWidgets(),
            _pointCloudControlWidgets(),
          ],
        ),
      ),
    );
  }

  Widget _buildGridControlWidgets() {
    return Column(
      children: [
        title('Grid'),
        slider(
          txt: 'Grid Size',
          value: gridSize,
          min: 10,
          max: 100,
          onChanged: (value) {
            setState(() {
              gridSize = (((value / 10).round()) * 10).toDouble();
            });
          },
        ),
        slider(
          txt: 'Grid Step',
          value: gridStep,
          min: 1,
          max: 10,
          onChanged: (value) {
            setState(() {
              if (value > 5) {
                gridStep = 10;
              } else if (value > 2) {
                gridStep = 5;
              } else if (value > 1) {
                gridStep = 2;
              } else {
                gridStep = 1;
              }
            });
          },
        ),
        horizontalLine(),
      ],
    );
  }

  Widget _buildAxisControlWidgets() {
    return Column(
      children: [
        title('Axis'),
        radioButton(
          txt: 'Axis On / Off',
          groupValue: axisOnOff,
          trueLabel: 'On',
          falseLabel: 'Off',
          onTrueAction: () {
            setState(() {
              axisOnOff = true;
            });
          },
          onFalseAction: () {
            setState(() {
              axisOnOff = false;
            });
          },
        ),
        slider(
          txt: 'Axis Length',
          value: axisLength,
          min: 0.5,
          max: 5,
          onChanged: (value) {
            setState(() {
              axisLength = (value * 2).round() / 2;
            });
          },
        ),
        horizontalLine(),
      ],
    );
  }

  Widget _buildPolygonControlWidgets() {
    return Column(
      children: [
        title('Polygon'),
        radioButton(
          txt: 'Polygon On / Off',
          groupValue: polygonOnOff,
          trueLabel: 'On',
          falseLabel: 'Off',
          onTrueAction: () {
            setState(() {
              polygonOnOff = true;
              for (var polygon in polygons) {
                polygon.enable = polygonOnOff;
              }
            });
          },
          onFalseAction: () {
            setState(() {
              polygonOnOff = false;
              for (var polygon in polygons) {
                polygon.enable = polygonOnOff;
              }
            });
          },
        ),
        radioButton(
          txt: 'Polygon Edit / View Only',
          groupValue: isEditPolygon,
          trueLabel: 'Edit',
          falseLabel: 'View',
          onTrueAction: () {
            setState(() {
              isEditPolygon = true;
              viewMode = PointGlassViewerMode.editPolygon;
              for (var polygon in polygons) {
                polygon.isEditable = isEditPolygon;
              }
            });
          },
          onFalseAction: () {
            setState(() {
              isEditPolygon = false;
              viewMode = PointGlassViewerMode.rotate;
              for (var polygon in polygons) {
                polygon.isEditable = isEditPolygon;
              }
            });
          },
        ),
        horizontalLine(),
      ],
    );
  }

  Widget _buildAnnualSectorControlWidgets() {
    return Column(
      children: [
        title('Annual Sector'),
        radioButton(
          txt: 'Annual Sector On / Off',
          groupValue: annualSectorOnOff,
          trueLabel: 'On',
          falseLabel: 'Off',
          onTrueAction: () {
            setState(() {
              annualSectorOnOff = true;
            });
          },
          onFalseAction: () {
            setState(() {
              annualSectorOnOff = false;
            });
          },
        ),
        slider(
          txt: 'Start Angle',
          value: annualSectorStartAngle,
          min: 0,
          max: 360,
          onChanged: (value) {
            setState(() {
              if (value >= annualSectorEndAngle) {
                annualSectorStartAngle = annualSectorEndAngle - 1;
              } else {
                annualSectorStartAngle = value.round().toDouble();
              }
            });
          },
        ),
        slider(
          txt: 'End Angle',
          value: annualSectorEndAngle,
          min: 0,
          max: 360,
          onChanged: (value) {
            setState(() {
              if (value <= annualSectorStartAngle) {
                annualSectorEndAngle = annualSectorStartAngle + 1;
              } else {
                annualSectorEndAngle = value.round().toDouble();
              }
            });
          },
        ),
        slider(
          txt: 'Inner Radius',
          value: annualSectorInnerRadius,
          min: 0,
          max: 100,
          onChanged: (value) {
            setState(() {
              if (value > annualSectorOuterRadius) {
                annualSectorInnerRadius = annualSectorOuterRadius;
              } else {
                annualSectorInnerRadius = value.round().toDouble();
              }
            });
          },
        ),
        slider(
          txt: 'Outer Radius',
          value: annualSectorOuterRadius,
          min: 0,
          max: 100,
          onChanged: (value) {
            setState(() {
              if (value < annualSectorInnerRadius) {
                annualSectorOuterRadius = annualSectorInnerRadius;
              } else {
                annualSectorOuterRadius = value.round().toDouble();
              }
            });
          },
        ),
        horizontalLine(),
      ],
    );
  }

  Widget _buildViewerControlWidgets() {
    return Column(
      children: [
        title('View'),
        Row(
          children: [
            Expanded(
              child: Row(
                children: [
                  Expanded(child: label('View Mode')),
                  Radio<PointGlassViewerMode>(
                    value: PointGlassViewerMode.rotate,
                    groupValue: viewMode,
                    onChanged: (value) {
                      setState(() {
                        viewMode = value!;
                        isEditPolygon = false;
                        for (var polygon in polygons) {
                          polygon.isEditable = isEditPolygon;
                        }
                      });
                    },
                  ),
                  Expanded(child: label('Rotate')),
                  Radio<PointGlassViewerMode>(
                    value: PointGlassViewerMode.translate,
                    groupValue: viewMode,
                    onChanged: (value) {
                      setState(() {
                        viewMode = value!;
                        isEditPolygon = false;
                        for (var polygon in polygons) {
                          polygon.isEditable = isEditPolygon;
                        }
                      });
                    },
                  ),
                  Expanded(child: label('Translate')),
                ],
              ),
            ),
          ],
        ),
        Row(
          children: [
            Expanded(
              child: Row(
                children: [
                  const Spacer(),
                  Radio<PointGlassViewerMode>(
                    value: PointGlassViewerMode.spin,
                    groupValue: viewMode,
                    onChanged: (value) {
                      setState(() {
                        viewMode = value!;
                        isEditPolygon = false;
                        for (var polygon in polygons) {
                          polygon.isEditable = isEditPolygon;
                        }
                      });
                    },
                  ),
                  Expanded(child: label('Spin')),
                  Radio<PointGlassViewerMode>(
                    value: PointGlassViewerMode.editPolygon,
                    groupValue: viewMode,
                    onChanged: (value) {
                      setState(() {
                        viewMode = value!;
                        isEditPolygon = true;
                        for (var polygon in polygons) {
                          polygon.isEditable = isEditPolygon;
                        }
                      });
                    },
                  ),
                  Expanded(child: label('Edit Polygon')),
                ],
              ),
            ),
          ],
        ),
        SizedBox(height: 10),
        _transformListener(),
        Row(
          children: [
            const Spacer(),
            ElevatedButton(
              child: Text('Reset'),
              onPressed: () {
                setState(() {
                  viewContext.value = ViewContext(
                    model: ModelTransform(),
                    camera: PinholeCamera(cameraZ: 10),
                    proj: PinholeProjection(focalPx: 800, near: 1, far: 20000),
                    canvasCenter: Offset(0, 0),
                  );
                });
              },
            ),
          ],
        ),
        horizontalLine(),
      ],
    );
  }

  Widget _transformListener() {
    return ValueListenableBuilder<ViewContext>(
      valueListenable: viewContext,
      builder: (context, value, child) {
        return Column(
          children: [
            Row(
              children: [
                Expanded(child: label('Transform Info')),
                const Spacer(),
                Expanded(
                  child: label(
                    'Scale: ${(viewContext.value.camera.cameraZ).toStringAsFixed(5)}',
                  ),
                ),
              ],
            ),
            Row(
              children: [
                const Spacer(flex: 2),
                Expanded(
                  child: label(
                    'Rotation X: ${viewContext.value.camera.yaw.toStringAsFixed(1)}',
                  ),
                ),
              ],
            ),
            Row(
              children: [
                const Spacer(flex: 2),
                Expanded(
                  child: label(
                    'Rotation Y: ${viewContext.value.camera.pitch.toStringAsFixed(1)}',
                  ),
                ),
              ],
            ),
            Row(
              children: [
                const Spacer(flex: 2),
                Expanded(
                  child: label(
                    'Rotation Z: ${viewContext.value.camera.roll.toStringAsFixed(1)}',
                  ),
                ),
              ],
            ),
          ],
        );
      },
    );
  }

  Widget _pointCloudControlWidgets() {
    return Column(
      children: [
        title('Point Cloud'),
        radioButton(
          txt: 'Point Cloud 1On / Off',
          groupValue: pointCloud1OnOff,
          trueLabel: 'On',
          falseLabel: 'Off',
          onTrueAction: () {
            setState(() {
              pointCloud1OnOff = true;
              pointsGroup[1].enable = pointCloud1OnOff;
            });
          },
          onFalseAction: () {
            setState(() {
              pointCloud1OnOff = false;
              pointsGroup[1].enable = pointCloud1OnOff;
            });
          },
        ),
        radioButton(
          txt: 'Point Cloud 2 On / Off',
          groupValue: pointCloud2OnOff,
          trueLabel: 'On',
          falseLabel: 'Off',
          onTrueAction: () {
            setState(() {
              pointCloud2OnOff = true;
              pointsGroup[2].enable = pointCloud2OnOff;
            });
          },
          onFalseAction: () {
            setState(() {
              pointCloud2OnOff = false;
              pointsGroup[2].enable = pointCloud2OnOff;
            });
          },
        ),
        horizontalLine(),
      ],
    );
  }
}
0
likes
150
points
262
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter package for 3D point cloud visualization with interactive grid, axis, polygons, and annual sectors.

Repository (GitHub)
View/report issues

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

flutter, universal_platform, vector_math

More

Packages that depend on point_glass