measure_3d_view.dart 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import 'dart:async';
  2. import 'package:fis_i18n/i18n.dart';
  3. import 'package:fis_measure/interfaces/process/items/item_metas.dart';
  4. import 'package:fis_measure/interfaces/process/items/terms.dart';
  5. import 'package:fis_measure/interfaces/process/player/play_controller.dart';
  6. import 'package:fis_measure/interfaces/process/workspace/application.dart';
  7. import 'package:fis_measure/process/workspace/measure_3d_view_controller.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:vid/us/vid_us_unit.dart';
  14. import 'package:webviewx/webviewx.dart';
  15. import 'dart:convert';
  16. import 'package:fis_measure/utils/js_utils.dart'
  17. if (dart.library.io) 'package:fis_measure/utils/js_utils4native.dart'
  18. if (dart.library.html) 'package:fis_measure/utils/js_utils.dart';
  19. /// [Carotid] 🚧webview 下 initPage 方法需要 i18n 由 Flutter 触发
  20. /// [Carotid] ✅启用 3D 后的亮度对比度需要通知 Shell 计算
  21. /// [Carotid] ✅需要接入 webview 控制器
  22. /// [Carotid] ✅需要判断是否为壳子,浏览器环境无 3D 操作
  23. ///
  24. class Measure3DView extends StatefulWidget implements FWidget {
  25. const Measure3DView({
  26. Key? key,
  27. }) : super(key: key);
  28. @override
  29. _Measure3DViewState createState() => _Measure3DViewState();
  30. }
  31. class _Measure3DViewState extends State<Measure3DView> {
  32. late WebViewXController webviewController;
  33. final measure3DViewController = Get.find<Measure3DViewController>();
  34. final application = Get.find<IApplication>();
  35. Size get screenSize => MediaQuery.of(context).size;
  36. String documentFromAsset = "<h2> Hello webviewX ! <h2>";
  37. Future<String> _preloadHTML() async {
  38. documentFromAsset = await _loadHTMLFromAssets(
  39. 'assets/webview/3DMeasurePage.html',
  40. );
  41. return Future.value("");
  42. }
  43. Future<String> _loadHTMLFromAssets(String path) async {
  44. try {
  45. return await rootBundle.loadString(path);
  46. } catch (e) {
  47. return Future.error(e);
  48. }
  49. }
  50. @override
  51. Widget build(BuildContext context) {
  52. return Column(
  53. children: <Widget>[
  54. Expanded(
  55. child: LayoutBuilder(
  56. builder: (BuildContext context, BoxConstraints constraints) {
  57. return _buildWebViewX(constraints);
  58. }),
  59. ),
  60. SizedBox(
  61. height: 30,
  62. child: Container(
  63. child: measure3DViewController.enable2DMeasure
  64. ? ElevatedButton(
  65. onPressed: () {
  66. _changeTo2DMeasure();
  67. },
  68. child: Text(i18nBook.measure.measureSpecifiedFace.t),
  69. )
  70. : Container(),
  71. ),
  72. ),
  73. ],
  74. );
  75. }
  76. @override
  77. void initState() {
  78. super.initState();
  79. measure3DViewController.onShellLoadedMdlFile.addListener(_mdlFileLoaded);
  80. measure3DViewController.adjustPlaneImage.addListener(_adjustedImage);
  81. }
  82. @override
  83. void dispose() {
  84. measure3DViewController.onShellLoadedMdlFile.removeListener(_mdlFileLoaded);
  85. measure3DViewController.adjustPlaneImage.removeListener(_adjustedImage);
  86. webviewController.dispose();
  87. super.dispose();
  88. }
  89. Widget _buildWebViewX(BoxConstraints constraints) {
  90. return FutureBuilder<String>(
  91. future: _preloadHTML(),
  92. builder: (context, AsyncSnapshot<String> snapshot) {
  93. if (snapshot.hasData) {
  94. return WebViewX(
  95. key: const ValueKey('webviewx'),
  96. initialContent: documentFromAsset,
  97. initialSourceType: SourceType.html,
  98. height: constraints.maxHeight,
  99. width: constraints.maxWidth,
  100. onWebViewCreated: (controller) => webviewController = controller,
  101. onPageStarted: (src) =>
  102. debugPrint('A new page has started loading'),
  103. onPageFinished: (src) {
  104. debugPrint('The page has finished loading');
  105. _setSurface().then((value) => _notifyShellToLoad());
  106. },
  107. // jsContent: _loadJSFromAssets('assets/js/webviewx.js'),
  108. dartCallBacks: {
  109. DartCallback(
  110. name: 'Dart_getClipPlaneData',
  111. callBack: (msg) => callShellGetClipPlaneData(msg)),
  112. DartCallback(
  113. name: 'Dart_deleteClipImageData',
  114. callBack: (msg) => _deleteClipImageData(msg),
  115. ),
  116. DartCallback(
  117. name: "Dart_deleteAllClipImageData",
  118. callBack: (msg) => _deleteAllClipImageData(msg),
  119. ),
  120. DartCallback(
  121. name: "Dart_meshActiveStatusChanged",
  122. callBack: (boolValue) => _meshActiveStatusChanged(boolValue),
  123. ),
  124. DartCallback(
  125. name: "Dart_aiClipStateCallBack",
  126. callBack: (boolValue) => _aiClipStateCallBack(boolValue),
  127. ),
  128. DartCallback(
  129. name: "Dart_getVesselClipPlanePoints",
  130. callBack: (msg) => callShellGetVesselClipPlanePoints(msg),
  131. ),
  132. },
  133. webSpecificParams: const WebSpecificParams(
  134. printDebugInfo: true,
  135. ),
  136. mobileSpecificParams: const MobileSpecificParams(
  137. androidEnableHybridComposition: true,
  138. ),
  139. navigationDelegate: (navigation) {
  140. debugPrint(navigation.content.sourceType.toString());
  141. return NavigationDecision.navigate;
  142. },
  143. );
  144. } else {
  145. return const Center(child: CircularProgressIndicator());
  146. }
  147. });
  148. }
  149. void _deleteClipImageData(String msg) {
  150. // debugPrint("webview 触发 Dart_deleteClipImageData :$msg");
  151. callShellMethod('deleteClipImageData', [msg]);
  152. }
  153. void _deleteAllClipImageData(msg) {
  154. // debugPrint("webview 触发 Dart_deleteAllClipImageData :$msg");
  155. callShellMethod('deleteAllClipImageData', []);
  156. }
  157. /// [Carotid] ✅触发激活状态更新,来判断是否显示 "切换2D测量" 按钮
  158. void _meshActiveStatusChanged(bool res) {
  159. debugPrint("webview 触发 Dart_meshActiveStatusChanged :$res");
  160. callShellMethod('meshActiveStatusChanged', [res]);
  161. measure3DViewController.enable2DMeasure = res;
  162. setState(() {});
  163. }
  164. /// AI 切割成功的回调
  165. void _aiClipStateCallBack(bool success) async {
  166. // debugPrint("webview 触发 Dart_aiClipStateCallBack :$success");
  167. print("webview 触发 Dart_aiClipStateCallBack :$success");
  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(600.milliseconds, () {
  211. final itemMeta = ItemMeta(
  212. MeasureTerms.IntimaDetection,
  213. description: MeasureTerms.IntimaDetection,
  214. measureType: MeasureTerms.IntimaDetection,
  215. outputs: [
  216. ItemOutputMeta(
  217. MeasureTerms.IntimaDetection, "", VidUsUnit.cm2),
  218. ],
  219. );
  220. application.switchItem(itemMeta);
  221. });
  222. measure3DViewController.needAutoDetection = false;
  223. }
  224. }
  225. ]);
  226. } catch (e) {
  227. debugPrint("callmethod fail " + e.toString());
  228. }
  229. }
  230. /// [Carotid] ✅_mdlFileLoaded 由壳子触发,通知 webview 模型已加载,显示操作按钮
  231. void _mdlFileLoaded(Object s, dynamic e) {
  232. try {
  233. webviewController.callJsMethod('mdlFileLoaded', ["Carotid", "True"]);
  234. } catch (e) {
  235. debugPrint("callmethod fail " + e.toString());
  236. }
  237. }
  238. /// [Carotid] ✅_adjustedImage 由 Flutter 触发,通知 webview 更新所有的面数据
  239. void _adjustedImage(Object s, ImageAdjustPara e) {
  240. try {
  241. webviewController.callJsMethod(
  242. 'adjustedImageByFlutter', [e.sharpness, e.brightness, e.contrast]);
  243. } catch (e) {
  244. debugPrint("callmethod fail " + e.toString());
  245. }
  246. }
  247. /// webview 初始化完成后通知壳子切换至对应模型文件
  248. Future<void> _notifyShellToLoad() async {
  249. measure3DViewController.notifyShellSetCurModel();
  250. }
  251. }