import 'dart:async'; import 'dart:math'; import 'dart:ui'; import 'package:fis_i18n/i18n.dart'; import 'package:fis_jsonrpc/rpc.dart'; import 'package:fis_measure/interfaces/date_types/int_size.dart'; import 'package:fis_measure/interfaces/enums/operate.dart'; import 'package:fis_measure/interfaces/process/calculators/output.dart'; import 'package:fis_measure/interfaces/process/items/item_feature.dart'; import 'package:fis_measure/interfaces/process/player/play_controller.dart'; import 'package:fis_measure/interfaces/process/standard_line/calibration.dart'; import 'package:fis_measure/interfaces/process/workspace/application.dart'; import 'package:fis_measure/interfaces/process/workspace/measure_3d_view_controller.dart'; import 'package:fis_measure/process/workspace/measure_3d_view_controller.dart'; import 'package:fis_measure/process/workspace/measure_data_controller.dart'; import 'package:fis_measure/process/workspace/measure_handler.dart'; import 'package:fis_measure/process/workspace/third_part/application.dart'; import 'package:fis_measure/process/workspace/third_part/calibration_controller.dart'; import 'package:fis_measure/utils/canvas.dart'; import 'package:fis_measure/utils/prompt_box.dart'; import 'package:fis_measure/values/colors.dart'; import 'package:fis_measure/view/menu_button_group/menu_button_group.dart'; import 'package:fis_measure/view/3d_view/carotid_player.dart'; import 'package:fis_measure/view/gesture/annotation/annotation_gesture.dart'; import 'package:fis_measure/view/measure/measure_result.dart'; import 'package:fis_measure/view/paint/ai_patint.dart'; import 'package:fis_measure/view/paint/ai_patint_controller.dart'; import 'package:fis_measure/view/paint/ai_patint_result.dart'; import 'package:fis_measure/view/result/results_panel.dart'; import 'package:fis_measure/view/standard_line/calibration_canvas.dart'; import 'package:fis_measure/view/standard_line/calibration_gesture.dart'; import 'package:fis_ui/base_define/page.dart'; import 'package:fis_ui/index.dart'; import 'package:fis_ui/widgets/layout/offstage.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:fis_measure/view/canvas/active_canvas.dart'; import 'package:fis_measure/view/canvas/annotation_canvas.dart'; import 'package:fis_measure/view/canvas/records_canvas.dart'; import 'package:fis_measure/view/gesture/mouse_gesture.dart'; import 'package:fis_measure/view/pause/pause_panel.dart'; import 'package:fis_measure/view/player/controller.dart'; import 'package:fis_measure/view/player/player.dart'; import 'package:fis_measure/view/loadding/loadding.dart'; class MeasureMainView extends StatefulWidget implements FPage { const MeasureMainView({Key? key}) : super(key: key); @override State createState() => _MeasureMainViewState(); @override String get pageName => "MeasureMainView"; } class _MeasureMainViewState extends State { late final application = Get.find(); late final playerController = Get.find(); late final measure3DViewController = Get.find(); late final measureHandler = Get.find(); /// 测量数据 final measureData = Get.find(); late bool canMeasure = application.canMeasure; bool get canMeasureDrawing => measureHandler.canMeasureDrawing; late final aiPatintController = Get.find(); late bool enableCarotid2DMeasure = false; bool isCaptureState = false; late double calibrationLine = 4; final List outputs = []; /// 是否显示loading加载提示 bool _ifShowLoadingTips = false; /// loading加载提示文字 String _loadingTipsText = ''; /// loading加载触发计时器 Timer? _streamLoadingTimer; /// 是否显示进度条 bool ifShowProgressBar = true; /// 是否是首次加载的新Vid bool isNewVid = true; /// 参考校准线控制器 IStandardLineCalibrationController? standardLineCalibrationController; final playerKey = GlobalKey(); final _captureAreaKey = GlobalKey(); bool get inAnnotation => application.currentOperateType == MeasureOperateType.annotation; @override void initState() { super.initState(); installStandardLine(); VidPlayerController vidPlayerController = playerController as VidPlayerController; WidgetsBinding.instance.addPostFrameCallback((timeStamp) { onCanMeasureChanged(this, application.canMeasure); application.canMeasureChanged.addListener(onCanMeasureChanged); application.operateTypeChanged.addListener(onOperateTypeChanged); measure3DViewController.updatePlayerMode.addListener(_onModeChanged); /// [Carotid] ✅在此判断是否为颈动脉2D图像,如果是,则不显示进度条且读取设置播放器单帧缓存 if (measure3DViewController.curMeasureMode == MeasureMode.carotid2DMode) { vidPlayerController.pause(); vidPlayerController .set2DMeasureFrame(measure3DViewController.image4Measure!); setState(() { enableCarotid2DMeasure = true; ifShowProgressBar = false; }); application.clearRecords(); } }); vidPlayerController.errorOccured.addListener(_onErrorOccured); playerController.frameLoadStateChanged.addListener(_onLoadStateChanged); measureHandler.canMeasureDrawingChanged .addListener(_onCanMeasureDrawingChanged); } @override void dispose() { application.canMeasureChanged.removeListener(onCanMeasureChanged); application.operateTypeChanged.removeListener(onOperateTypeChanged); measure3DViewController.updatePlayerMode.removeListener(_onModeChanged); playerController.frameLoadStateChanged.removeListener(_onLoadStateChanged); measureHandler.canMeasureDrawingChanged .removeListener(_onCanMeasureDrawingChanged); uninstallStandardLine(); super.dispose(); } /// 装载参考校准线 void installStandardLine() { if (application.isThirdPart) { final standradLine = (application as ThirdPartApplication).standardLine; standardLineCalibrationController = StandardLineCalibrationController(application, standradLine); standardLineCalibrationController!.editStateChanged .addListener(onStandardLineCalibrationStateChanged); Get.put( standardLineCalibrationController!); } } /// 卸载参考校准线 void uninstallStandardLine() { standardLineCalibrationController?.editStateChanged .removeListener(onStandardLineCalibrationStateChanged); Get.delete(); } /// 流式加载出错 void _onErrorOccured(Object s, String? error) { /// 第一次收到加载出错,不显示错误信息,直接重新加载一次,如果再次收到加载出错,显示错误信息 if (isNewVid) { /// 如果不重新加载,在生产环境下会由于流式加载出错而导致无法播放 /// 由于 debug 环境下不会出问题,所以下面用了 debugPrint /// debugPrint('流式加载出错: $error,尝试重新加载'); isNewVid = false; Future.delayed(const Duration(milliseconds: 100), () { playerController.locateTo(0); playerController.play(); }); setState(() { _ifShowLoadingTips = error?.isNotEmpty ?? false; }); } else { setState(() { _ifShowLoadingTips = error?.isNotEmpty ?? false; _loadingTipsText = error ?? ''; }); } } /// 流式加载loadding void _onLoadStateChanged(sender, bool e) { if (e) { _streamLoadingTimer?.cancel(); _streamLoadingTimer = Timer(const Duration(milliseconds: 100), () { setState(() { _ifShowLoadingTips = true; _loadingTipsText = i18nBook.common.loading.t; }); }); } else { _streamLoadingTimer?.cancel(); if (_ifShowLoadingTips) { setState(() { _ifShowLoadingTips = false; }); } } } /// 模式改变触发更新 /// [Carotid] ✅组件不销毁的情况下,切换模式的时候,可以触发 void _onModeChanged(Object s, MeasureMode mode) { switch (mode) { case MeasureMode.vidMode: playerController.play(); setState(() { ifShowProgressBar = true; enableCarotid2DMeasure = false; }); break; case MeasureMode.carotid2DMode: setState(() { ifShowProgressBar = false; enableCarotid2DMeasure = true; }); break; case MeasureMode.carotid3DMode: break; } } /// 保存图片 void capturePng() async { setState(() { isCaptureState = true; }); final features = []; for (var item in application.measureItems) { if (item.measuredFeatures.isNotEmpty) { features.addAll(item.measuredFeatures); } if (item.feature != null) { features.add(item.feature!); } } MeasureResult measureResult = MeasureResult( measureApplicationName: application.applicationName, features: features, ); //加延时是为了等待 UI 消失,防止截图截到 UI Future.delayed(const Duration(milliseconds: 100), () async { final RenderRepaintBoundary? boundary = _captureAreaKey.currentContext ?.findRenderObject() as RenderRepaintBoundary?; if (boundary != null) { final image = await boundary.toImage(); final byteData = await image.toByteData(format: ImageByteFormat.png); final pngBytes = byteData!.buffer.asUint8List(); PromptBox.snackbar( i18nBook.measure.saveLocation.t + ' > ' + i18nBook.measure.measureImage.t, duration: const Duration(milliseconds: 1500), title: i18nBook.measure.screenshotSavedSuccessfully.t, textColor: Colors.white, backgroundColor: Colors.black.withOpacity(0.7)); setState(() { isCaptureState = false; }); measureData.saveImage.call( pngBytes, measureData.measureImageData.patientCode ?? '', measureData.measureImageData.recordCode ?? '', measureData.measureImageData.remedicalCode ?? '', measureResult.toDisplay(), ); } }); } void onCanMeasureChanged(Object sender, bool e) { if (e != canMeasure) { setState(() { canMeasure = e; }); } } void onOperateTypeChanged(Object sender, MeasureOperateType e) { setState(() {}); } void _onCanMeasureDrawingChanged(Object sender, bool e) { setState(() {}); } void onStandardLineCalibrationStateChanged( Object sender, StandardLineCalibrationEditState e) { setState(() { if (e == StandardLineCalibrationEditState.drawn) { Get.dialog(buildCalibrationLine()); } }); } FWidget buildCalibrationLine() { return FSimpleDialog( title: FText( i18nBook.measure.guideCalibration.t, style: const TextStyle( color: Colors.white, fontSize: 18, ), ), isDefault: true, cancelString: i18nBook.common.cancel.t, okString: i18nBook.common.confirm.t, onOk: () { standardLineCalibrationController!.confirmEdit(calibrationLine); Get.back(); }, onCancel: () { standardLineCalibrationController!.cancelEdit(); Get.back(); }, children: [ buildCalibrationLineItem( i18nBook.measure.length.t, FTextField( decoration: InputDecoration( hintText: i18nBook.common.input.translate([i18nBook.measure.length.t]), hintStyle: const TextStyle( fontSize: 16, ), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.white.withOpacity(0.5), width: 0.5, style: BorderStyle.solid, ), ), focusedBorder: const OutlineInputBorder( borderSide: BorderSide( color: Colors.blue, width: 0.5, style: BorderStyle.solid, ), ), filled: true, ), onChanged: (val) => calibrationLine = double.parse(val), ), ), buildCalibrationLineItem( i18nBook.measure.unit.t, const FText('cm'), ), ], ); } FWidget buildCalibrationLineItem(String name, FWidget itemWidget) { return FContainer( padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 25), child: FRow( children: [ FContainer( width: 60, child: FText(name), ), FExpanded( child: itemWidget, ), ], ), ); } @override Widget build(BuildContext context) { MeasureCanvasExt.setFontFamily( Theme.of(context).textTheme.labelLarge?.fontFamily, ); bool canShowAI = [ DiagnosisConclusionEnum.Benign, DiagnosisConclusionEnum.Malignant, DiagnosisConclusionEnum.BenignAndMalignant ].contains(measureData.diagnosisConclusion); return Container( color: MeasureColors.Background, child: RepaintBoundary( child: Column( children: [ Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: RepaintBoundary( key: _captureAreaKey, child: CustomMultiChildLayout( delegate: _LayerLayoutDelegate(), children: [ LayoutId( id: _LayerLayoutIds.player, child: enableCarotid2DMeasure ? CarotidPlayer( measure3DViewController, ) : VidPlayer( playerController as VidPlayerController, ), ), if (canMeasure) ...[ LayoutId( id: _LayerLayoutIds.recordsCanvas, child: _enableMeasureOffstageWrap( const MeasureRecordsCanvasPanel()), ), LayoutId( id: _LayerLayoutIds.activeMeasure, child: _enableMeasureOffstageWrap( const MeasureActiveCanvasPanel()), ), LayoutId( id: _LayerLayoutIds.activeAnnotation, child: _enableMeasureOffstageWrap( const AnnotationCanvas()), ), if (application.isThirdPart) LayoutId( id: _LayerLayoutIds.standardLineCalibration, child: _enableMeasureOffstageWrap( StandardLineCalibrationCanvas( standardLineCalibrationController!)), ), LayoutId( id: _LayerLayoutIds.gesture, child: _enableMeasureOffstageWrap( _buildGestureLayer()), ), LayoutId( id: _LayerLayoutIds.result, child: _enableMeasureOffstageWrap( const MeasureResultPanel()), ), ], if (canMeasure) LayoutId( id: _LayerLayoutIds.buttonGroups, child: FOffstage( offstage: isCaptureState, child: FMenuButtonGroup( businessParent: widget, capturePng: () => capturePng(), ), ), ), if (canShowAI) ...[ LayoutId( id: _LayerLayoutIds.paintAI, child: AIPaintInfo( playerController as VidPlayerController, ), ) ], if (enableCarotid2DMeasure) ...[ LayoutId( id: _LayerLayoutIds.aiTips, child: _buildAiTips(), ) ], if (_ifShowLoadingTips) LayoutId( id: _LayerLayoutIds.loadingTipsOverlay, child: _buildLoadingOrError(_loadingTipsText), ), ], ), ), ), if (canShowAI && !measureHandler.fullScreenState) ...[ Obx(() { return Visibility( visible: aiPatintController.state.ifShowAi, maintainState: true, maintainAnimation: true, maintainSize: true, child: SizedBox( width: 200, child: AIPaintInfoReslut( playerController as VidPlayerController, ), ), ); }) ] ], ), ), ifShowProgressBar ? const MeasurePausePanel() : Container(), ], ), ), ); } Widget _buildLoadingOrError([String? loadingText = '加载失败']) { return FStack( children: [ FMouseRegion( child: FContainer( width: double.infinity, height: double.infinity, color: Colors.black.withOpacity(.5), child: const FSizedBox(), ), ), FCenter( child: FContainer( padding: const EdgeInsets.symmetric( vertical: 20, horizontal: 40, ), child: FStack(children: [ FCenter( child: FColumn(mainAxisSize: MainAxisSize.min, children: [ const SpinKitChasingDots( color: Colors.white, size: 30, ), const FSizedBox(height: 20), FText( loadingText!.isNotEmpty ? loadingText : i18nBook.common.loading.t, style: const TextStyle( color: Colors.white, ), ), ]), ) ]), ), ), ], ); } /// 仅当 canMeasureDrawing 为 true 时才显示的 Offstage 包装 Widget _enableMeasureOffstageWrap(Widget child) { return Offstage( offstage: !canMeasureDrawing, child: child, ); } Widget _buildGestureLayer() { if (application.isThirdPart) { if (standardLineCalibrationController!.isEditing) { return StandardLineCalibrationGesture( standardLineCalibrationController!); } } return inAnnotation ? const AnnotationGestureLayer() : const MeasureMouseGesturePanel(); } Widget _buildAiTips() { return Transform( transform: Matrix4.translationValues(0, -18, 0), child: Text( i18nBook.measure.carotidAiTips.t, style: const TextStyle(color: Colors.grey, fontSize: 18, height: 1), ), ); } } class _LayerLayoutDelegate extends MultiChildLayoutDelegate { Offset? layoutOffset; Size? layoutSize; _LayerLayoutDelegate(); @override void performLayout(Size size) { if (!hasChild(_LayerLayoutIds.player)) return; final application = Get.find(); final vidFrame = application.frameData; final imageSize = IntSize.fill(vidFrame?.width ?? 0, vidFrame?.height ?? 0); /// 以Contain方式填充布局,计算定位偏移量 calcSize(size, imageSize); final offset = layoutOffset!; final renderSize = layoutSize!; final rightSpace = (size.width - renderSize.width) / 2; // 如果图像右侧剩余可用空间大于60,则将按钮组放在右侧,如果小于60,则尽可能靠右,60是按钮组宽度 final buttonGroupOffsetX = min(rightSpace, 60.0); final resultPanelLayerSize = Size(renderSize.width + rightSpace, renderSize.height); /// 同步图像显示尺寸 application.displaySize = renderSize; layoutLayer(_LayerLayoutIds.player, offset, renderSize); layoutLayer(_LayerLayoutIds.loadingTipsOverlay, offset, renderSize); /// 其他层按播放器尺寸位置层叠布局 layoutLayer(_LayerLayoutIds.recordsCanvas, offset, renderSize); layoutLayer(_LayerLayoutIds.activeMeasure, offset, renderSize); layoutLayer(_LayerLayoutIds.activeAnnotation, offset, renderSize); layoutLayer(_LayerLayoutIds.standardLineCalibration, offset, renderSize); layoutLayer(_LayerLayoutIds.gesture, offset, renderSize); layoutLayer( _LayerLayoutIds.result, Offset.zero, resultPanelLayerSize, ); layoutLayer(_LayerLayoutIds.pause, offset, renderSize); layoutLayer(_LayerLayoutIds.paintAI, offset, renderSize); layoutLayer(_LayerLayoutIds.aiTips, Offset(0.0, size.height), renderSize); layoutLayer(_LayerLayoutIds.buttonGroups, offset.translate(buttonGroupOffsetX, 0), renderSize); } void layoutLayer(_LayerLayoutIds layoutId, Offset offset, Size size) { if (hasChild(layoutId)) { layoutChild( layoutId, BoxConstraints.loose(size), ); positionChild(layoutId, offset); } } void calcSize(Size size, IntSize imageSize) { final parentWHRatio = size.width / size.height; final imageWHRatio = imageSize.width / imageSize.height; if (imageWHRatio < parentWHRatio) { // 高度撑满 final layoutWidth = size.height * imageWHRatio; final layoutHeight = size.height; final offsetX = (size.width - layoutWidth) / 2; layoutOffset = Offset(offsetX, 0); layoutSize = Size(layoutWidth, layoutHeight); } else if (imageWHRatio > parentWHRatio) { // 宽度撑满 final layoutWidth = size.width; final layoutHeight = size.width / imageWHRatio; final offsetY = (size.height - layoutHeight) / 2; layoutOffset = Offset(0, offsetY); layoutSize = Size(layoutWidth, layoutHeight); } else { layoutOffset = Offset.zero; layoutSize = size; } } @override bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) { return false; } } enum _LayerLayoutIds { /// 播放器 player, /// 流式加载提示蒙层 loadingTipsOverlay, /// 测量记录画板 recordsCanvas, /// 活动测量画板 activeMeasure, /// 活动注释画板 activeAnnotation, /// 结果面板 result, /// 手势面板 gesture, /// 暂停画板 后面需要优化命名 pause, /// AI画板 paintAI, /// AI 自动测量提示语[提醒:自动测量可能存在误差,仅供参考] aiTips, /// 按钮组 buttonGroups, /// 参考校准线画板 standardLineCalibration, }