measure_3d_view.dart 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  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 DefaultAssetBundle.of(context).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: {
  107. EmbeddedJsContent(
  108. js: "var isCurrentChinese = ${i18nBook.isCurrentChinese}")
  109. },
  110. dartCallBacks: {
  111. DartCallback(
  112. name: 'Dart_getClipPlaneData',
  113. callBack: (msg) => callShellGetClipPlaneData(msg)),
  114. DartCallback(
  115. name: 'Dart_deleteClipImageData',
  116. callBack: (msg) => _deleteClipImageData(msg),
  117. ),
  118. DartCallback(
  119. name: "Dart_deleteAllClipImageData",
  120. callBack: (msg) => _deleteAllClipImageData(msg),
  121. ),
  122. DartCallback(
  123. name: "Dart_meshActiveStatusChanged",
  124. callBack: (boolValue) => _meshActiveStatusChanged(boolValue),
  125. ),
  126. DartCallback(
  127. name: "Dart_aiClipStateCallBack",
  128. callBack: (boolValue) => _aiClipStateCallBack(boolValue),
  129. ),
  130. DartCallback(
  131. name: "Dart_getVesselClipPlanePoints",
  132. callBack: (msg) => callShellGetVesselClipPlanePoints(msg),
  133. ),
  134. },
  135. webSpecificParams: const WebSpecificParams(
  136. printDebugInfo: true,
  137. ),
  138. mobileSpecificParams: const MobileSpecificParams(
  139. androidEnableHybridComposition: true,
  140. ),
  141. navigationDelegate: (navigation) {
  142. debugPrint(navigation.content.sourceType.toString());
  143. return NavigationDecision.navigate;
  144. },
  145. );
  146. } else {
  147. return const Center(child: CircularProgressIndicator());
  148. }
  149. });
  150. }
  151. void _deleteClipImageData(String msg) {
  152. // debugPrint("webview 触发 Dart_deleteClipImageData :$msg");
  153. callShellMethod('deleteClipImageData', [msg]);
  154. }
  155. void _deleteAllClipImageData(msg) {
  156. // debugPrint("webview 触发 Dart_deleteAllClipImageData :$msg");
  157. callShellMethod('deleteAllClipImageData', []);
  158. }
  159. /// [Carotid] ✅触发激活状态更新,来判断是否显示 [切换2D测量] 按钮
  160. void _meshActiveStatusChanged(bool res) {
  161. debugPrint("webview 触发 Dart_meshActiveStatusChanged :$res");
  162. callShellMethod('meshActiveStatusChanged', [res]);
  163. measure3DViewController.enable2DMeasure = res;
  164. setState(() {});
  165. }
  166. /// AI 切割成功的回调
  167. void _aiClipStateCallBack(bool success) async {
  168. if (success) {
  169. measure3DViewController.needAutoDetection = true;
  170. // callShellMethod('aiClipStateCallBack', [res]);
  171. measure3DViewController.resetTone();
  172. }
  173. }
  174. ///[Carotid] ✅初始化 mesh 长宽高可以由两张小图计算得到,设置标准宽度用于缩放切面
  175. Future<void> _setSurface() async {
  176. final faceImages = measure3DViewController.carotidResult.surfaceImageList;
  177. if (faceImages!.length == 6) {
  178. final first = await loadImageSize(faceImages[0]);
  179. final last = await loadImageSize(faceImages[5]);
  180. final width = (last.width - 1).toInt();
  181. final height = (last.height - 1).toInt();
  182. final depth = (first.height - 1).toInt();
  183. measure3DViewController.stdSize =
  184. Size(width.toDouble(), depth.toDouble());
  185. try {
  186. await webviewController.callJsMethod(
  187. 'changeSurface', [width, height, depth, ...faceImages]);
  188. } catch (e) {
  189. debugPrint("callmethod fail " + e.toString());
  190. }
  191. }
  192. }
  193. /// [Carotid] ✅获取到图像并进入 2D 测量页
  194. Future<void> _changeTo2DMeasure() async {
  195. try {
  196. String res =
  197. await webviewController.callJsMethod('getCurrentClipInformation', []);
  198. callShellSwitchMeasureMode(
  199. res, measure3DViewController.curImageAdjustPara)
  200. .callMethod("then", [
  201. (res) async {
  202. VidUsImage vidUsImage = VidUsImage.fromBytes(base64Decode(res));
  203. /// [Carotid] ✅写入单帧缓存
  204. final playerController = Get.find<IPlayerController>();
  205. playerController.pause();
  206. measure3DViewController.image4Measure = vidUsImage;
  207. measure3DViewController.changeModeTo2DMeasure();
  208. application.clearRecords();
  209. if (measure3DViewController.needAutoDetection) {
  210. Future.delayed(400.milliseconds, () {
  211. /// 内膜检测
  212. final intimaDetection = carotidMeasureApplicationList.last;
  213. application.switchItem(intimaDetection);
  214. PromptBox.snackbar(i18nBook.measure.autoRunIntimaDetection.t,
  215. duration: const Duration(milliseconds: 1500),
  216. title: i18nBook.common.tip.t,
  217. textColor: Colors.white,
  218. backgroundColor: Colors.black.withOpacity(0.7));
  219. Future.delayed(400.milliseconds, () {
  220. final firstItem = carotidMeasureApplicationList.first;
  221. application.switchItem(firstItem);
  222. });
  223. });
  224. measure3DViewController.needAutoDetection = false;
  225. }
  226. }
  227. ]);
  228. } catch (e) {
  229. debugPrint("callmethod fail " + e.toString());
  230. }
  231. }
  232. /// [Carotid] ✅_mdlFileLoaded 由壳子触发,通知 webview 模型已加载,显示操作按钮
  233. void _mdlFileLoaded(Object s, dynamic e) {
  234. try {
  235. webviewController.callJsMethod('mdlFileLoaded', ["Carotid", "True"]);
  236. } catch (e) {
  237. debugPrint("callmethod fail " + e.toString());
  238. }
  239. }
  240. /// [Carotid] ✅_adjustedImage 由 Flutter 触发,通知 webview 更新所有的面数据
  241. void _adjustedImage(Object s, ImageAdjustPara e) {
  242. try {
  243. webviewController.callJsMethod(
  244. 'adjustedImageByFlutter', [e.sharpness, e.brightness, e.contrast]);
  245. } catch (e) {
  246. debugPrint("callmethod fail " + e.toString());
  247. }
  248. }
  249. /// webview 初始化完成后通知壳子切换至对应模型文件
  250. Future<void> _notifyShellToLoad() async {
  251. measure3DViewController.notifyShellSetCurModel();
  252. }
  253. }