measure_3d_view.dart 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import 'dart:async';
  2. import 'package:fis_i18n/i18n.dart';
  3. import 'package:fis_measure/interfaces/process/player/play_controller.dart';
  4. import 'package:fis_measure/interfaces/process/workspace/application.dart';
  5. import 'package:fis_measure/process/workspace/measure_3d_view_controller.dart';
  6. import 'package:fis_measure/utils/prompt_box.dart';
  7. import 'package:fis_measure/view/measure/carotid_measure_tool.dart';
  8. import 'package:fis_ui/index.dart';
  9. import 'package:flutter/material.dart' hide Image;
  10. import 'package:flutter/services.dart';
  11. import 'package:get/get.dart';
  12. import 'package:vid/us/vid_us_image.dart';
  13. import 'package:webviewx/webviewx.dart';
  14. import 'dart:convert';
  15. import 'package:fis_measure/utils/js_utils.dart'
  16. if (dart.library.io) 'package:fis_measure/utils/js_utils4native.dart'
  17. if (dart.library.html) 'package:fis_measure/utils/js_utils.dart';
  18. /// [Carotid] 🚧webview 下 initPage 方法需要 i18n 由 Flutter 触发
  19. /// [Carotid] ✅启用 3D 后的亮度对比度需要通知 Shell 计算
  20. /// [Carotid] ✅需要接入 webview 控制器
  21. /// [Carotid] ✅需要判断是否为壳子,浏览器环境无 3D 操作
  22. ///
  23. class Measure3DView extends StatefulWidget implements FWidget {
  24. const Measure3DView({
  25. Key? key,
  26. }) : super(key: key);
  27. @override
  28. _Measure3DViewState createState() => _Measure3DViewState();
  29. }
  30. class _Measure3DViewState extends State<Measure3DView> {
  31. late WebViewXController webviewController;
  32. final measure3DViewController = Get.find<Measure3DViewController>();
  33. final application = Get.find<IApplication>();
  34. Size get screenSize => MediaQuery.of(context).size;
  35. String documentFromAsset = "<h2> Hello webviewX ! <h2>";
  36. Future<String> _preloadHTML() async {
  37. documentFromAsset = await _loadHTMLFromAssets(
  38. 'assets/webview/3DMeasurePage.html',
  39. );
  40. return Future.value("");
  41. }
  42. Future<String> _loadHTMLFromAssets(String path) async {
  43. try {
  44. return await rootBundle.loadString(path);
  45. } catch (e) {
  46. return Future.error(e);
  47. }
  48. }
  49. @override
  50. Widget build(BuildContext context) {
  51. return Column(
  52. children: <Widget>[
  53. Expanded(
  54. child: LayoutBuilder(
  55. builder: (BuildContext context, BoxConstraints constraints) {
  56. return _buildWebViewX(constraints);
  57. }),
  58. ),
  59. SizedBox(
  60. height: 30,
  61. child: Container(
  62. child: measure3DViewController.enable2DMeasure
  63. ? ElevatedButton(
  64. onPressed: () {
  65. _changeTo2DMeasure();
  66. },
  67. child: Text(i18nBook.measure.measureSpecifiedFace.t),
  68. )
  69. : Container(),
  70. ),
  71. ),
  72. ],
  73. );
  74. }
  75. @override
  76. void initState() {
  77. super.initState();
  78. measure3DViewController.onShellLoadedMdlFile.addListener(_mdlFileLoaded);
  79. measure3DViewController.adjustPlaneImage.addListener(_adjustedImage);
  80. }
  81. @override
  82. void dispose() {
  83. measure3DViewController.onShellLoadedMdlFile.removeListener(_mdlFileLoaded);
  84. measure3DViewController.adjustPlaneImage.removeListener(_adjustedImage);
  85. webviewController.dispose();
  86. super.dispose();
  87. }
  88. Widget _buildWebViewX(BoxConstraints constraints) {
  89. return FutureBuilder<String>(
  90. future: _preloadHTML(),
  91. builder: (context, AsyncSnapshot<String> snapshot) {
  92. if (snapshot.hasData) {
  93. return WebViewX(
  94. key: const ValueKey('webviewx'),
  95. initialContent: documentFromAsset,
  96. initialSourceType: SourceType.html,
  97. height: constraints.maxHeight,
  98. width: constraints.maxWidth,
  99. onWebViewCreated: (controller) => webviewController = controller,
  100. onPageStarted: (src) =>
  101. debugPrint('A new page has started loading'),
  102. onPageFinished: (src) {
  103. debugPrint('The page has finished loading');
  104. _setSurface().then((value) => _notifyShellToLoad());
  105. },
  106. // jsContent: _loadJSFromAssets('assets/js/webviewx.js'),
  107. dartCallBacks: {
  108. DartCallback(
  109. name: 'Dart_getClipPlaneData',
  110. callBack: (msg) => callShellGetClipPlaneData(msg)),
  111. DartCallback(
  112. name: 'Dart_deleteClipImageData',
  113. callBack: (msg) => _deleteClipImageData(msg),
  114. ),
  115. DartCallback(
  116. name: "Dart_deleteAllClipImageData",
  117. callBack: (msg) => _deleteAllClipImageData(msg),
  118. ),
  119. DartCallback(
  120. name: "Dart_meshActiveStatusChanged",
  121. callBack: (boolValue) => _meshActiveStatusChanged(boolValue),
  122. ),
  123. DartCallback(
  124. name: "Dart_aiClipStateCallBack",
  125. callBack: (boolValue) => _aiClipStateCallBack(boolValue),
  126. ),
  127. DartCallback(
  128. name: "Dart_getVesselClipPlanePoints",
  129. callBack: (msg) => callShellGetVesselClipPlanePoints(msg),
  130. ),
  131. },
  132. webSpecificParams: const WebSpecificParams(
  133. printDebugInfo: true,
  134. ),
  135. mobileSpecificParams: const MobileSpecificParams(
  136. androidEnableHybridComposition: true,
  137. ),
  138. navigationDelegate: (navigation) {
  139. debugPrint(navigation.content.sourceType.toString());
  140. return NavigationDecision.navigate;
  141. },
  142. );
  143. } else {
  144. return const Center(child: CircularProgressIndicator());
  145. }
  146. });
  147. }
  148. void _deleteClipImageData(String msg) {
  149. // debugPrint("webview 触发 Dart_deleteClipImageData :$msg");
  150. callShellMethod('deleteClipImageData', [msg]);
  151. }
  152. void _deleteAllClipImageData(msg) {
  153. // debugPrint("webview 触发 Dart_deleteAllClipImageData :$msg");
  154. callShellMethod('deleteAllClipImageData', []);
  155. }
  156. /// [Carotid] ✅触发激活状态更新,来判断是否显示 "切换2D测量" 按钮
  157. void _meshActiveStatusChanged(bool res) {
  158. debugPrint("webview 触发 Dart_meshActiveStatusChanged :$res");
  159. callShellMethod('meshActiveStatusChanged', [res]);
  160. measure3DViewController.enable2DMeasure = res;
  161. setState(() {});
  162. }
  163. /// AI 切割成功的回调
  164. void _aiClipStateCallBack(bool success) async {
  165. if (success) {
  166. measure3DViewController.needAutoDetection = true;
  167. // callShellMethod('aiClipStateCallBack', [res]);
  168. measure3DViewController.resetTone();
  169. }
  170. }
  171. ///[Carotid] ✅初始化 mesh 长宽高可以由两张小图计算得到,设置标准宽度用于缩放切面
  172. Future<void> _setSurface() async {
  173. final faceImages = measure3DViewController.carotidResult.surfaceImageList;
  174. if (faceImages!.length == 6) {
  175. final first = await loadImageSize(faceImages[0]);
  176. final last = await loadImageSize(faceImages[5]);
  177. final width = (last.width - 1).toInt();
  178. final height = (last.height - 1).toInt();
  179. final depth = (first.height - 1).toInt();
  180. measure3DViewController.stdSize =
  181. Size(width.toDouble(), depth.toDouble());
  182. try {
  183. await webviewController.callJsMethod(
  184. 'changeSurface', [width, height, depth, ...faceImages]);
  185. } catch (e) {
  186. debugPrint("callmethod fail " + e.toString());
  187. }
  188. }
  189. }
  190. /// [Carotid] ✅获取到图像并进入 2D 测量页
  191. Future<void> _changeTo2DMeasure() async {
  192. try {
  193. String res =
  194. await webviewController.callJsMethod('getCurrentClipInformation', []);
  195. callShellSwitchMeasureMode(
  196. res, measure3DViewController.curImageAdjustPara)
  197. .callMethod("then", [
  198. (res) async {
  199. VidUsImage vidUsImage = VidUsImage.fromBytes(base64Decode(res));
  200. /// [Carotid] ✅写入单帧缓存
  201. final playerController = Get.find<IPlayerController>();
  202. playerController.pause();
  203. measure3DViewController.image4Measure = vidUsImage;
  204. measure3DViewController.changeModeTo2DMeasure();
  205. application.clearRecords();
  206. if (measure3DViewController.needAutoDetection) {
  207. Future.delayed(400.milliseconds, () {
  208. /// 内膜检测
  209. final intimaDetection = carotidMeasureApplicationList.last;
  210. application.switchItem(intimaDetection);
  211. PromptBox.snackbar(i18nBook.measure.autoRunIntimaDetection.t,
  212. duration: const Duration(milliseconds: 1500),
  213. title: i18nBook.common.tip.t,
  214. textColor: Colors.white,
  215. backgroundColor: Colors.black.withOpacity(0.7));
  216. Future.delayed(400.milliseconds, () {
  217. final firstItem = carotidMeasureApplicationList.first;
  218. application.switchItem(firstItem);
  219. });
  220. });
  221. measure3DViewController.needAutoDetection = false;
  222. }
  223. }
  224. ]);
  225. } catch (e) {
  226. debugPrint("callmethod fail " + e.toString());
  227. }
  228. }
  229. /// [Carotid] ✅_mdlFileLoaded 由壳子触发,通知 webview 模型已加载,显示操作按钮
  230. void _mdlFileLoaded(Object s, dynamic e) {
  231. try {
  232. webviewController.callJsMethod('mdlFileLoaded', ["Carotid", "True"]);
  233. } catch (e) {
  234. debugPrint("callmethod fail " + e.toString());
  235. }
  236. }
  237. /// [Carotid] ✅_adjustedImage 由 Flutter 触发,通知 webview 更新所有的面数据
  238. void _adjustedImage(Object s, ImageAdjustPara e) {
  239. try {
  240. webviewController.callJsMethod(
  241. 'adjustedImageByFlutter', [e.sharpness, e.brightness, e.contrast]);
  242. } catch (e) {
  243. debugPrint("callmethod fail " + e.toString());
  244. }
  245. }
  246. /// webview 初始化完成后通知壳子切换至对应模型文件
  247. Future<void> _notifyShellToLoad() async {
  248. measure3DViewController.notifyShellSetCurModel();
  249. }
  250. }