From 56738b1f06db21c85c08b140a757096c8f6a2635 Mon Sep 17 00:00:00 2001 From: a1518 Date: Fri, 26 Jun 2026 00:23:52 -0700 Subject: [PATCH] feat: add MaskSegmentApp source code and config Co-authored-by: Cursor --- .npmignore | 34 + App.tsx | 290 +- README.md | 949 +- __tests__/App.test.tsx | 80 +- __tests__/paintShader.test.ts | 79 + android/app/src/main/AndroidManifest.xml | 2 + assets/test/mask.png | Bin 0 -> 711618 bytes assets/test/origin.png | Bin 0 -> 1609965 bytes babel.config.js | 1 + dist/components/MaskSegmentCanvas.d.ts | 6 + dist/components/MaskSegmentCanvas.d.ts.map | 1 + dist/components/MaskSegmentCanvas.js | 2012 +++ dist/components/MaskSegmentCanvas.js.map | 1 + dist/components/MaskSegmentCanvas.types.d.ts | 189 + .../MaskSegmentCanvas.types.d.ts.map | 1 + dist/components/MaskSegmentCanvas.types.js | 2 + .../components/MaskSegmentCanvas.types.js.map | 1 + dist/index.d.ts | 6 + dist/index.d.ts.map | 1 + dist/index.js | 5 + dist/index.js.map | 1 + dist/shaders/regionPaint.sksl.d.ts | 3 + dist/shaders/regionPaint.sksl.d.ts.map | 1 + dist/shaders/regionPaint.sksl.js | 72 + dist/shaders/regionPaint.sksl.js.map | 1 + dist/utils/compositePaintedImage.d.ts | 44 + dist/utils/compositePaintedImage.d.ts.map | 1 + dist/utils/compositePaintedImage.js | 146 + dist/utils/compositePaintedImage.js.map | 1 + dist/utils/exportUtils.d.ts | 20 + dist/utils/exportUtils.d.ts.map | 1 + dist/utils/exportUtils.js | 32 + dist/utils/exportUtils.js.map | 1 + dist/utils/freqLayerPrep.d.ts | 23 + dist/utils/freqLayerPrep.d.ts.map | 1 + dist/utils/freqLayerPrep.js | 168 + dist/utils/freqLayerPrep.js.map | 1 + dist/utils/maskSegmentRuntime.d.ts | 43 + dist/utils/maskSegmentRuntime.d.ts.map | 1 + dist/utils/maskSegmentRuntime.js | 181 + dist/utils/maskSegmentRuntime.js.map | 1 + dist/utils/maskSegmentation.d.ts | 133 + dist/utils/maskSegmentation.d.ts.map | 1 + dist/utils/maskSegmentation.js | 1600 ++ dist/utils/maskSegmentation.js.map | 1 + dist/utils/maskSemanticPalette.d.ts | 31 + dist/utils/maskSemanticPalette.d.ts.map | 1 + dist/utils/maskSemanticPalette.js | 125 + dist/utils/maskSemanticPalette.js.map | 1 + dist/utils/opencvAdapter.d.ts | 116 + dist/utils/opencvAdapter.d.ts.map | 1 + dist/utils/opencvAdapter.js | 353 + dist/utils/opencvAdapter.js.map | 1 + dist/utils/paintColorMapTexture.d.ts | 5 + dist/utils/paintColorMapTexture.d.ts.map | 1 + dist/utils/paintColorMapTexture.js | 203 + dist/utils/paintColorMapTexture.js.map | 1 + dist/utils/paintShaderRuntime.d.ts | 40 + dist/utils/paintShaderRuntime.d.ts.map | 1 + dist/utils/paintShaderRuntime.js | 76 + dist/utils/paintShaderRuntime.js.map | 1 + dist/utils/pickMapTexture.d.ts | 4 + dist/utils/pickMapTexture.d.ts.map | 1 + dist/utils/pickMapTexture.js | 24 + dist/utils/pickMapTexture.js.map | 1 + dist/utils/pngImage.d.ts | 49 + dist/utils/pngImage.d.ts.map | 1 + dist/utils/pngImage.js | 438 + dist/utils/pngImage.js.map | 1 + dist/utils/resolveAssetPath.d.ts | 3 + dist/utils/resolveAssetPath.d.ts.map | 1 + dist/utils/resolveAssetPath.js | 56 + dist/utils/resolveAssetPath.js.map | 1 + dist/utils/resolveImageUrl.d.ts | 3 + dist/utils/resolveImageUrl.d.ts.map | 1 + dist/utils/resolveImageUrl.js | 51 + dist/utils/resolveImageUrl.js.map | 1 + dist/utils/skiaImage.d.ts | 4 + dist/utils/skiaImage.d.ts.map | 1 + dist/utils/skiaImage.js | 12 + dist/utils/skiaImage.js.map | 1 + index.js | 6 + ios/MaskSegmentApp.xcodeproj/project.pbxproj | 49 +- .../contents.xcworkspacedata | 10 + ios/MaskSegmentApp/Info.plist | 4 + ios/MaskSegmentApp/PrivacyInfo.xcprivacy | 1 + ios/Podfile | 7 + ios/Podfile.lock | 2259 +++ ios/offline_pods.rb | 27 + ios/scripts/setup_vendor_pods.sh | 15 + package-lock.json | 13350 ++++++++++++++++ package.json | 85 +- patches/react-native-fast-opencv+0.4.8.patch | 122 + src/components/MaskSegmentCanvas.tsx | 2832 ++++ src/components/MaskSegmentCanvas.types.ts | 216 + src/globals.d.ts | 19 + src/index.ts | 45 + src/shaders/regionPaint.sksl.ts | 71 + src/upng-js.d.ts | 33 + src/utils/compositePaintedImage.ts | 201 + src/utils/exportUtils.ts | 40 + src/utils/freqLayerPrep.ts | 267 + src/utils/maskSegmentRuntime.ts | 257 + src/utils/maskSegmentation.ts | 2294 +++ src/utils/maskSemanticPalette.ts | 187 + src/utils/opencvAdapter.ts | 539 + src/utils/paintColorMapTexture.ts | 239 + src/utils/paintShaderRuntime.tsx | 150 + src/utils/pickMapTexture.ts | 37 + src/utils/pngImage.ts | 591 + src/utils/resolveAssetPath.ts | 64 + src/utils/resolveImageUrl.ts | 63 + src/utils/skiaImage.ts | 25 + tsconfig.build.json | 16 + 114 files changed, 31647 insertions(+), 223 deletions(-) create mode 100644 .npmignore create mode 100644 __tests__/paintShader.test.ts create mode 100644 assets/test/mask.png create mode 100644 assets/test/origin.png create mode 100644 dist/components/MaskSegmentCanvas.d.ts create mode 100644 dist/components/MaskSegmentCanvas.d.ts.map create mode 100644 dist/components/MaskSegmentCanvas.js create mode 100644 dist/components/MaskSegmentCanvas.js.map create mode 100644 dist/components/MaskSegmentCanvas.types.d.ts create mode 100644 dist/components/MaskSegmentCanvas.types.d.ts.map create mode 100644 dist/components/MaskSegmentCanvas.types.js create mode 100644 dist/components/MaskSegmentCanvas.types.js.map create mode 100644 dist/index.d.ts create mode 100644 dist/index.d.ts.map create mode 100644 dist/index.js create mode 100644 dist/index.js.map create mode 100644 dist/shaders/regionPaint.sksl.d.ts create mode 100644 dist/shaders/regionPaint.sksl.d.ts.map create mode 100644 dist/shaders/regionPaint.sksl.js create mode 100644 dist/shaders/regionPaint.sksl.js.map create mode 100644 dist/utils/compositePaintedImage.d.ts create mode 100644 dist/utils/compositePaintedImage.d.ts.map create mode 100644 dist/utils/compositePaintedImage.js create mode 100644 dist/utils/compositePaintedImage.js.map create mode 100644 dist/utils/exportUtils.d.ts create mode 100644 dist/utils/exportUtils.d.ts.map create mode 100644 dist/utils/exportUtils.js create mode 100644 dist/utils/exportUtils.js.map create mode 100644 dist/utils/freqLayerPrep.d.ts create mode 100644 dist/utils/freqLayerPrep.d.ts.map create mode 100644 dist/utils/freqLayerPrep.js create mode 100644 dist/utils/freqLayerPrep.js.map create mode 100644 dist/utils/maskSegmentRuntime.d.ts create mode 100644 dist/utils/maskSegmentRuntime.d.ts.map create mode 100644 dist/utils/maskSegmentRuntime.js create mode 100644 dist/utils/maskSegmentRuntime.js.map create mode 100644 dist/utils/maskSegmentation.d.ts create mode 100644 dist/utils/maskSegmentation.d.ts.map create mode 100644 dist/utils/maskSegmentation.js create mode 100644 dist/utils/maskSegmentation.js.map create mode 100644 dist/utils/maskSemanticPalette.d.ts create mode 100644 dist/utils/maskSemanticPalette.d.ts.map create mode 100644 dist/utils/maskSemanticPalette.js create mode 100644 dist/utils/maskSemanticPalette.js.map create mode 100644 dist/utils/opencvAdapter.d.ts create mode 100644 dist/utils/opencvAdapter.d.ts.map create mode 100644 dist/utils/opencvAdapter.js create mode 100644 dist/utils/opencvAdapter.js.map create mode 100644 dist/utils/paintColorMapTexture.d.ts create mode 100644 dist/utils/paintColorMapTexture.d.ts.map create mode 100644 dist/utils/paintColorMapTexture.js create mode 100644 dist/utils/paintColorMapTexture.js.map create mode 100644 dist/utils/paintShaderRuntime.d.ts create mode 100644 dist/utils/paintShaderRuntime.d.ts.map create mode 100644 dist/utils/paintShaderRuntime.js create mode 100644 dist/utils/paintShaderRuntime.js.map create mode 100644 dist/utils/pickMapTexture.d.ts create mode 100644 dist/utils/pickMapTexture.d.ts.map create mode 100644 dist/utils/pickMapTexture.js create mode 100644 dist/utils/pickMapTexture.js.map create mode 100644 dist/utils/pngImage.d.ts create mode 100644 dist/utils/pngImage.d.ts.map create mode 100644 dist/utils/pngImage.js create mode 100644 dist/utils/pngImage.js.map create mode 100644 dist/utils/resolveAssetPath.d.ts create mode 100644 dist/utils/resolveAssetPath.d.ts.map create mode 100644 dist/utils/resolveAssetPath.js create mode 100644 dist/utils/resolveAssetPath.js.map create mode 100644 dist/utils/resolveImageUrl.d.ts create mode 100644 dist/utils/resolveImageUrl.d.ts.map create mode 100644 dist/utils/resolveImageUrl.js create mode 100644 dist/utils/resolveImageUrl.js.map create mode 100644 dist/utils/skiaImage.d.ts create mode 100644 dist/utils/skiaImage.d.ts.map create mode 100644 dist/utils/skiaImage.js create mode 100644 dist/utils/skiaImage.js.map create mode 100644 ios/MaskSegmentApp.xcworkspace/contents.xcworkspacedata create mode 100644 ios/Podfile.lock create mode 100644 ios/offline_pods.rb create mode 100755 ios/scripts/setup_vendor_pods.sh create mode 100644 package-lock.json create mode 100644 patches/react-native-fast-opencv+0.4.8.patch create mode 100644 src/components/MaskSegmentCanvas.tsx create mode 100644 src/components/MaskSegmentCanvas.types.ts create mode 100644 src/globals.d.ts create mode 100644 src/index.ts create mode 100644 src/shaders/regionPaint.sksl.ts create mode 100644 src/upng-js.d.ts create mode 100644 src/utils/compositePaintedImage.ts create mode 100644 src/utils/exportUtils.ts create mode 100644 src/utils/freqLayerPrep.ts create mode 100644 src/utils/maskSegmentRuntime.ts create mode 100644 src/utils/maskSegmentation.ts create mode 100644 src/utils/maskSemanticPalette.ts create mode 100644 src/utils/opencvAdapter.ts create mode 100644 src/utils/paintColorMapTexture.ts create mode 100644 src/utils/paintShaderRuntime.tsx create mode 100644 src/utils/pickMapTexture.ts create mode 100644 src/utils/pngImage.ts create mode 100644 src/utils/resolveAssetPath.ts create mode 100644 src/utils/resolveImageUrl.ts create mode 100644 src/utils/skiaImage.ts create mode 100644 tsconfig.build.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..9c68c63 --- /dev/null +++ b/.npmignore @@ -0,0 +1,34 @@ +# Demo app +example/ +App.tsx +index.js +app.json +metro.config.js +babel.config.js + +# Tests +__tests__/ +coverage/ + +# Native demo projects +ios/ +android/ + +# Config files (not needed in published package) +tsconfig.json +tsconfig.build.json +.eslintrc.js +.prettierrc.js + +# Development +.cursor/ +agent-transcripts/ +assets/ + +# Dependencies +node_modules/ +package-lock.json + +# OS +.DS_Store +*.log diff --git a/App.tsx b/App.tsx index 7d8838a..6f1452c 100644 --- a/App.tsx +++ b/App.tsx @@ -1,130 +1,220 @@ /** - * Sample React Native App - * https://github.com/facebook/react-native + * Mask Segment Demo App + * 基于 OpenCV + Skia 的掩码分区交互画布 * - * @format + * 注意:这是库开发自测用的 Demo,直接引用 ./src。 + * 业务项目集成演示请参考 example/ 目录(使用公开包名导入)。 */ -import React from 'react'; -import type {PropsWithChildren} from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { - ScrollView, + ActivityIndicator, StatusBar, StyleSheet, Text, - useColorScheme, View, } from 'react-native'; +import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; +import MaskSegmentCanvas, { + type MaskSegmentCanvasRef, + type MaskSegmentSession, + type MaskSegmentWatchState, + MASK_SEMANTIC_COLORS, +} from './src'; +import { resolveAssetPath } from './src/utils/resolveAssetPath'; +import { prewarmPngBgrCacheAsync } from './src/utils/pngImage'; -import { - Colors, - DebugInstructions, - Header, - LearnMoreLinks, - ReloadInstructions, -} from 'react-native/Libraries/NewAppScreen'; +const TEST_ORIGIN = require('./assets/test/origin.png'); +const TEST_MASK = require('./assets/test/mask.png'); -type SectionProps = PropsWithChildren<{ - title: string; -}>; +const INTERACTIVE_WATCH_STATES: MaskSegmentWatchState[] = [ + 'interactive', + 'mask_paths_ready', +]; -function Section({children, title}: SectionProps): React.JSX.Element { - const isDarkMode = useColorScheme() === 'dark'; - return ( - - - {title} - - - {children} - - - ); +function formatWatchStatus(state: MaskSegmentWatchState | ''): string { + if (!state) { + return ''; + } + if (state === 'interactive') { + return '可上色(轮廓加载中…)'; + } + if (state === 'mask_paths_ready') { + return '就绪'; + } + if (state === 'error') { + return '失败'; + } + return `加载中:${state}`; } function App(): React.JSX.Element { - const isDarkMode = useColorScheme() === 'dark'; + const canvasRef = useRef(null); + const [testPaths, setTestPaths] = useState<{ + origin: string; + mask: string; + } | null>(null); + const [loadError, setLoadError] = useState(''); + const [watchState, setWatchState] = useState(''); + const [sessionDraft, setSessionDraft] = useState( + null, + ); + const isFullyReady = watchState === 'mask_paths_ready'; + const isInitLoading = + testPaths != null && + watchState !== '' && + !INTERACTIVE_WATCH_STATES.includes(watchState as MaskSegmentWatchState) && + watchState !== 'error'; - const backgroundStyle = { - backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, - }; + useEffect(() => { + let cancelled = false; - /* - * To keep the template simple and small we're adding padding to prevent view - * from rendering under the System UI. - * For bigger apps the recommendation is to use `react-native-safe-area-context`: - * https://github.com/AppAndFlow/react-native-safe-area-context - * - * You can read more about it here: - * https://github.com/react-native-community/discussions-and-proposals/discussions/827 - */ - const safePadding = '5%'; + (async () => { + try { + const [origin, mask] = await Promise.all([ + resolveAssetPath(TEST_ORIGIN, 'gym_test_origin.png'), + resolveAssetPath(TEST_MASK, 'gym_test_mask.png'), + ]); + await prewarmPngBgrCacheAsync([origin, mask]); + if (!cancelled) { + setTestPaths({ origin, mask }); + } + } catch (e) { + if (!cancelled) { + setLoadError(e instanceof Error ? e.message : String(e)); + } + } + })(); + + return () => { + cancelled = true; + }; + }, []); return ( - - - - -
- - -
- Edit App.tsx to change this - screen and then come back to see your edits. -
-
- -
-
- -
-
- Read the docs to discover what to do next: -
- -
- - + + + + {loadError ? ( + + {loadError} + + ) : testPaths ? ( + <> + {watchState ? ( + + 状态: {formatWatchStatus(watchState)} + {isFullyReady ? ' · 轮播虚线已就绪' : null} + + ) : null} + + { + setWatchState(state); + if (__DEV__) { + const extra = + detail && Object.keys(detail).length > 0 + ? ` ${JSON.stringify(detail)}` + : ''; + console.log( + `[Demo onWatch] ${state} ${durationMs.toFixed(0)}ms${extra}`, + ); + } + }} + onPaintCallback={payload => { + if (__DEV__) { + if (payload.kind === 'brush_required') { + console.log('[Demo onPaint]', payload.hint, payload.regionName); + } else { + console.log('[Demo onPaint]', payload.regionName, payload.color); + } + } + }} + onError={message => { + setLoadError(message); + setWatchState('error'); + }} + /> + {isInitLoading ? ( + + + + {formatWatchStatus(watchState)} + + + ) : null} + + {sessionDraft ? ( + + 已恢复 MMKV 草稿({sessionDraft.painted.length} 区域) + + ) : null} + + ) : ( + + + 加载健身房测试图… + + )} + + ); } const styles = StyleSheet.create({ - sectionContainer: { - marginTop: 32, - paddingHorizontal: 24, + root: { + flex: 1, + backgroundColor: '#fff', }, - sectionTitle: { - fontSize: 24, - fontWeight: '600', + center: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 24, }, - sectionDescription: { - marginTop: 8, - fontSize: 18, - fontWeight: '400', + loadingText: { + marginTop: 12, + color: '#666', + fontSize: 14, }, - highlight: { - fontWeight: '700', + errorText: { + color: '#c33', + fontSize: 14, + textAlign: 'center', + }, + watchText: { + paddingHorizontal: 12, + paddingTop: 8, + color: '#555', + fontSize: 12, + }, + canvasHost: { + flex: 1, + position: 'relative', + }, + initOverlay: { + ...StyleSheet.absoluteFillObject, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(255, 255, 255, 0.72)', + gap: 8, + }, + initOverlayText: { + color: '#666', + fontSize: 13, + }, + sessionText: { + paddingHorizontal: 12, + paddingBottom: 8, + color: '#2a7', + fontSize: 12, }, }); diff --git a/README.md b/README.md index 3e2c3f8..072d454 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,904 @@ -This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). +# react-native-mask-segment-canvas -# Getting Started +基于 React Native **0.79** 的掩码分区交互库,核心导出 `MaskSegmentCanvas` 组件,可通过 **npm 包** 或 **npm link** 接入其它 RN 工程。 -> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding. +- **OpenCV**(`react-native-fast-opencv`):掩码语义布局、踢脚线修补、分区提取 +- **Skia RuntimeEffect(SkSL)**:原图 + LAB 高低频纹理叠色(单次全屏 Shader) +- **Skia Path**:分区虚线轮廓高亮 +- **交互**:底部笔刷选色(可选初始化)→ 点击分区上色;未选笔刷时点击分区会通过 `onPaintCallback` 提示;未选笔刷时长按预览分区虚线轮廓 -## Step 1: Start Metro +本仓库同时作为 **库源码**(`src/index.ts`)与 **自测 Demo**(根目录 `App.tsx`)维护。 -First, you will need to run **Metro**, the JavaScript build tool for React Native. +**推荐的集成演示请查看 `example/` 目录**:它只使用公开 API,完整模拟业务项目接入方式(含 `package.json`、Metro 配置、完整可参考的 `App.tsx`)。 -To start the Metro dev server, run the following command from the root of your React Native project: +--- + +## 作为 npm 包接入其它工程 + +### 安装依赖(宿主工程) + +```bash +npm install react-native-mask-segment-canvas +# 或本地联调 +npm link ../MaskSegmentApp # 在库目录先执行 npm link +npm link react-native-mask-segment-canvas +``` + +宿主工程还需安装 **peerDependencies**(版本需与宿主 RN 对齐): + +```bash +npm install @shopify/react-native-skia react-native-reanimated react-native-fast-opencv react-native-fs buffer upng-js +# 若使用 showDebugPickers 相册选图 +npm install react-native-image-picker +``` + +### 宿主工程 postinstall(必需) + +本库依赖 `patch-package` 修补 `react-native-fast-opencv`,宿主 `package.json` 需配置: + +```json +{ + "scripts": { + "postinstall": "patch-package" + }, + "devDependencies": { + "patch-package": "^8.0.1" + } +} +``` + +安装本库后,`node_modules/react-native-mask-segment-canvas/patches/` 中的补丁会在宿主 `postinstall` 时自动应用。 + +### iOS / Android 原生依赖 + +```bash +cd ios && pod install && cd .. +``` + +确保宿主已按各原生库文档完成 Skia、Reanimated、OpenCV 等配置。 + +### Metro 配置(npm link 时) + +联调时若出现模块解析问题,在宿主 `metro.config.js` 中把本库加入 `watchFolders` / `extraNodeModules`(按宿主 Metro 版本调整): + +```js +const path = require('path'); + +module.exports = { + watchFolders: [path.resolve(__dirname, '../MaskSegmentApp')], + resolver: { + nodeModulesPaths: [path.resolve(__dirname, 'node_modules')], + }, +}; +``` + +### 业务侧引入 + +```tsx +import MaskSegmentCanvas, { + type MaskSegmentCanvasRef, + type MaskSegmentSession, + type MaskSegmentWatchState, + type MaskSegmentWatchDetail, + type BgrColor, + type MaskSemanticColor, + type PaintCallbackPayload, + type PaintedRegionRecord, + type PipelineConfig, + type MaskSegmentConfig, + type PaintConfig, + type InteractionConfig, + type SavePaintResult, + MASK_SEMANTIC_COLORS, + BASEBOARD_SEMANTIC_NAME, + prewarmPngBgrCacheAsync, + DEFAULT_PIPELINE_CONFIG, + DEFAULT_MASK_CONFIG, + DEFAULT_PAINT_CONFIG, + DEFAULT_INTERACTION_CONFIG, +} from 'react-native-mask-segment-canvas'; +``` + +主要导出一览: + +| 类别 | 名称 | +| ---- | ---- | +| 组件 | `MaskSegmentCanvas`(default) | +| Ref / Props 类型 | `MaskSegmentCanvasRef`、`MaskSegmentCanvasProps` | +| 会话 / 回调类型 | `MaskSegmentSession`、`PaintCallbackPayload`、`PaintedRegionRecord`、`SavePaintResult` | +| Watch 类型 | `MaskSegmentWatchState`、`MaskSegmentWatchDetail` | +| 配置类型 | `PipelineConfig`、`MaskSegmentConfig`、`PaintConfig`、`InteractionConfig` | +| 语义色 | `MASK_SEMANTIC_COLORS`、`BASEBOARD_SEMANTIC_NAME` | +| 工具 | `prewarmPngBgrCacheAsync`、`prewarmPngBgrCache` | +| 运行时 | `DEFAULT_*_CONFIG`、`getMaskSegmentRuntimeConfig`、`setMaskSegmentRuntimeConfig` | + +--- + +## 推荐:通过 example/ 目录学习集成 + +`example/` 是**专门为业务侧集成准备的演示文件夹**,它: + +- 只通过 `import ... from 'react-native-mask-segment-canvas'` 使用公开 API(不碰内部 src) +- 提供了独立的 `package.json`(含 peer deps + 本地 file 依赖) +- 包含针对本地联调的 `metro.config.js` +- `App.tsx` 是一个可直接参考的完整页面,涵盖预热、状态管理、ref 操作、回调处理等 + +建议: + +1. 直接阅读 `example/App.tsx` 获取最新可运行的集成写法。 +2. 按 `example/README.md` 的步骤在本机跑起来,验证安装、patch、Metro 配置是否正确。 +3. 把 `example/App.tsx` 中的核心逻辑复制到你自己的页面/组件中即可。 + +这样可以确保你接入的是「库的公开契约」,而不是内部实现细节。 + +--- + +## 环境要求 + +- Node.js >= 18(推荐 20+) +- Xcode 15+(iOS) +- Android Studio + JDK 17(Android) +- CocoaPods(iOS) + +## 快速开始(本仓库 Demo) + +根目录 `App.tsx` 是库作者自测用的完整 Demo,内部直接 import `./src`。 + +```bash +cd MaskSegmentApp + +npm install + +cd ios && bundle exec pod install && cd .. -```sh -# Using npm npm start -# OR using Yarn -yarn start -``` - -## Step 2: Build and run your app - -With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app: - -### Android - -```sh -# Using npm -npm run android - -# OR using Yarn -yarn android -``` - -### iOS - -For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps). - -The first time you create a new project, run the Ruby bundler to install CocoaPods itself: - -```sh -bundle install -``` - -Then, and every time you update your native dependencies, run: - -```sh -bundle exec pod install -``` - -For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html). - -```sh -# Using npm +# 另开终端 npm run ios - -# OR using Yarn -yarn ios +# 或 +npm run android ``` -If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device. +**想看「纯业务项目如何集成」**:请进入 `example/` 目录,按其 `README.md` 操作。它使用 `import from 'react-native-mask-segment-canvas'` + 标准的 `package.json` + Metro 配置,完全模拟消费者环境。 -This is one way to run your app — you can also build it directly from Android Studio or Xcode. +Demo 入口 `App.tsx` 通过 `./src`(即包入口 `src/index.ts`)引用组件,与业务侧 `import from 'react-native-mask-segment-canvas'` 等价。 -## Step 3: Modify your app +--- -Now that you have successfully run the app, let's make changes! +## MaskSegmentCanvas 组件 -Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh). +### 引入 -When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload: +```tsx +import React, { useRef } from 'react'; +import MaskSegmentCanvas, { + type MaskSegmentCanvasRef, + type MaskSegmentSession, + type MaskSegmentWatchState, + type MaskSegmentWatchDetail, + type BgrColor, + type MaskSemanticColor, + type PaintCallbackPayload, + MASK_SEMANTIC_COLORS, + prewarmPngBgrCacheAsync, +} from 'react-native-mask-segment-canvas'; +``` -- **Android**: Press the R key twice or select **"Reload"** from the **Dev Menu**, accessed via Ctrl + M (Windows/Linux) or Cmd ⌘ + M (macOS). -- **iOS**: Press R in iOS Simulator. +也可按需导入运行时默认值(与组件 Props 合并使用): -## Congratulations! :tada: +```tsx +import { + DEFAULT_PIPELINE_CONFIG, + DEFAULT_MASK_CONFIG, + DEFAULT_PAINT_CONFIG, + DEFAULT_INTERACTION_CONFIG, +} from 'react-native-mask-segment-canvas'; +``` -You've successfully run and modified your React Native App. :partying_face: +### 最小示例 -### Now what? +下面是一个可直接放进业务页面的完整示例,涵盖 **PNG 预热**、**state**、**配置**、**加载态**、**onWatch** 与 **ref** 常用操作。 -- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). -- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started). +```tsx +import React, { useEffect, useRef, useState } from 'react'; +import { ActivityIndicator, Text, View } from 'react-native'; +import MaskSegmentCanvas, { + type MaskSegmentCanvasRef, + type MaskSegmentSession, + type MaskSegmentWatchState, + type BgrColor, + MASK_SEMANTIC_COLORS, + prewarmPngBgrCacheAsync, +} from 'react-native-mask-segment-canvas'; -# Troubleshooting +/** 业务侧准备好的图片地址(本地 file:// 或 http(s)://) */ +type ImagePaths = { + origin: string; + mask: string; +}; -If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. +const INTERACTIVE_STATES: MaskSegmentWatchState[] = [ + 'interactive', + 'mask_paths_ready', +]; -# Learn More +export function PaintScreen() { + const canvasRef = useRef(null); -To learn more about React Native, take a look at the following resources: + const [imagePaths, setImagePaths] = useState(null); + const [pathsError, setPathsError] = useState(''); + const [watchState, setWatchState] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [sessionDraft] = useState(null); + + const isInteractive = INTERACTIVE_STATES.includes( + watchState as MaskSegmentWatchState, + ); + const isOutlineReady = watchState === 'mask_paths_ready'; + const isCanvasLoading = + imagePaths != null && + watchState !== '' && + !INTERACTIVE_STATES.includes(watchState as MaskSegmentWatchState) && + watchState !== 'error'; + + // 示例:接口下载完成后写入路径,并预热 PNG 解码缓存 + useEffect(() => { + let cancelled = false; + + (async () => { + try { + const origin = 'file:///path/to/origin.png'; + const mask = 'file:///path/to/mask.png'; + await prewarmPngBgrCacheAsync([origin, mask]); + if (!cancelled) { + setImagePaths({ origin, mask }); + } + } catch (e) { + if (!cancelled) { + setPathsError(e instanceof Error ? e.message : String(e)); + } + } + })(); + + return () => { + cancelled = true; + }; + }, []); + + const handleSave = async () => { + if (!isInteractive) return; + const result = await canvasRef.current?.save({ destDir: undefined }); + console.log('saved', result?.filePath, result?.paintedCount); + }; + + if (pathsError) { + return {pathsError}; + } + + if (!imagePaths) { + return ( + + + 等待原图与掩码… + + ); + } + + return ( + + {isCanvasLoading ? 加载中:{watchState} : null} + {watchState === 'interactive' ? ( + 可上色(轮廓加载中…) + ) : null} + {isOutlineReady ? 就绪 : null} + {errorMessage ? {errorMessage} : null} + + { + setWatchState(state); + // detail: regionCount, maskPathsReady, freqLayersReady, errorMessage + console.log('[onWatch]', state, durationMs, detail); + }} + onPaintCallback={payload => { + if (payload.kind === 'brush_required') { + toast(payload.hint); + return; + } + console.log('painted', payload.regionId, payload.regionName, payload.color, payload.configJson); + }} + onError={message => { + setErrorMessage(message); + setWatchState('error'); + }} + /> + + {/* ref 示例:disabled={!isInteractive} 时可按需绑定 */} + {/*