diff --git a/.npmignore b/.npmignore
index 9c68c63..aa4e0d7 100644
--- a/.npmignore
+++ b/.npmignore
@@ -24,6 +24,7 @@ tsconfig.build.json
.cursor/
agent-transcripts/
assets/
+scripts/
# Dependencies
node_modules/
diff --git a/App.tsx b/App.tsx
index 6f1452c..7359785 100644
--- a/App.tsx
+++ b/App.tsx
@@ -1,9 +1,9 @@
/**
* Mask Segment Demo App
- * 基于 OpenCV + Skia 的掩码分区交互画布
+ * Mask segmentation canvas based on OpenCV + Skia
*
- * 注意:这是库开发自测用的 Demo,直接引用 ./src。
- * 业务项目集成演示请参考 example/ 目录(使用公开包名导入)。
+ * Note: This is a demo for library development testing, directly referencing ./src.
+ * For business project integration demonstration, please refer to the example/ directory (import using the public package name).
*/
import React, { useEffect, useRef, useState } from 'react';
@@ -37,15 +37,15 @@ function formatWatchStatus(state: MaskSegmentWatchState | ''): string {
return '';
}
if (state === 'interactive') {
- return '可上色(轮廓加载中…)';
+ return 'Colorable (outline loading...)';
}
if (state === 'mask_paths_ready') {
- return '就绪';
+ return 'Ready';
}
if (state === 'error') {
- return '失败';
+ return 'Failed';
}
- return `加载中:${state}`;
+ return `Loading: ${state}`;
}
function App(): React.JSX.Element {
@@ -103,8 +103,8 @@ function App(): React.JSX.Element {
<>
{watchState ? (
- 状态: {formatWatchStatus(watchState)}
- {isFullyReady ? ' · 轮播虚线已就绪' : null}
+ Status: {formatWatchStatus(watchState)}
+ {isFullyReady ? ' · Carousel dashed line ready' : null}
) : null}
@@ -153,14 +153,14 @@ function App(): React.JSX.Element {
{sessionDraft ? (
- 已恢复 MMKV 草稿({sessionDraft.painted.length} 区域)
+ Restored MMKV draft ({sessionDraft.painted.length} regions)
) : null}
>
) : (
- 加载健身房测试图…
+ Loading gym test image...
)}
diff --git a/README.md b/README.md
index 093688d..0242f26 100644
--- a/README.md
+++ b/README.md
@@ -1,40 +1,105 @@
-# react-native-mask-segment-canvas
+# 🎨 react-native-mask-segment-canvas
-基于 React Native **0.79** 的掩码分区交互库,核心导出 `MaskSegmentCanvas` 组件,可通过 **npm 包** 或 **npm link** 接入其它 RN 工程。
+A React Native **0.79** interactive mask segmentation library. The core export is the `MaskSegmentCanvas` component, consumable via **npm package** or **npm link** from any React Native project.
-- **OpenCV**(`react-native-fast-opencv`):掩码语义布局、踢脚线修补、分区提取
-- **Skia RuntimeEffect(SkSL)**:原图 + LAB 高低频纹理叠色(单次全屏 Shader)
-- **Skia Path**:分区虚线轮廓高亮
-- **交互**:底部笔刷选色(可选初始化)→ 点击分区上色;未选笔刷时点击分区会通过 `onPaintCallback` 提示;未选笔刷时长按预览分区虚线轮廓
+- 🧠 **OpenCV** (`react-native-fast-opencv`): mask semantic layout, baseboard patching, region extraction
+- 🖌️ **Skia RuntimeEffect (SkSL)**: single-pass full-screen shader blending original image + LAB low/high frequency texture color overlays
+- ✂️ **Skia Path**: dashed outline highlights for regions
+- 👆 **Interaction**: bottom color bar for brush selection (optional initialization) → tap a region to paint; tapping without a brush selected fires `onPaintCallback` with a hint; long-press without a brush previews the region's dashed outline
-本仓库同时作为 **库源码**(`src/index.ts`)与 **自测 Demo**(根目录 `App.tsx`)维护。
+This repository serves as both the **library source** (`src/index.ts`) and a **self-test demo** (root `App.tsx`).
-**推荐的集成演示请查看 `example/` 目录**:它只使用公开 API,完整模拟业务项目接入方式(含 `package.json`、Metro 配置、完整可参考的 `App.tsx`)。
+📌 **For the recommended integration demo, see the `example/` directory** — it uses only the public API, fully simulating how a consumer project would integrate (including `package.json`, Metro configuration, and a complete reference `App.tsx`).
---
-## 作为 npm 包接入其它工程
+## Table of Contents
-### 安装依赖(宿主工程)
+- [Overview](#overview)
+- [Requirements](#requirements)
+- [Installation](#installation)
+ - [Peer Dependencies](#peer-dependencies)
+ - [Postinstall Setup](#postinstall-setup)
+ - [iOS / Android Native Dependencies](#ios--android-native-dependencies)
+ - [Metro Configuration](#metro-configuration)
+ - [Troubleshooting: Duplicate Module Errors](#troubleshooting-duplicate-module-errors)
+- [Quick Start (Dev Demo)](#quick-start-dev-demo)
+- [Basic Usage](#basic-usage)
+ - [Minimal Example](#minimal-example)
+ - [State Variables](#state-variables)
+ - [Choosing Configuration Values](#choosing-configuration-values)
+ - [watchState & UI Guidance](#watchstate--ui-guidance)
+- [API Reference](#api-reference)
+ - [Imports](#imports)
+ - [Props: Image & Initialization](#props-image--initialization)
+ - [Props: Semantic Colors & Outline](#props-semantic-colors--outline)
+ - [Props: maskConfig](#props-maskconfig)
+ - [Props: pipelineConfig](#props-pipelineconfig)
+ - [Props: paintConfig](#props-paintconfig)
+ - [Props: interactionConfig](#props-interactionconfig)
+ - [Props: UI Controls & Styling](#props-ui-controls--styling)
+ - [Props: Callbacks](#props-callbacks)
+ - [Ref Methods](#ref-methods)
+ - [Storage Convention](#storage-convention)
+- [Interaction Guide](#interaction-guide)
+- [Integration Examples](#integration-examples)
+ - [Pre-warm PNG Cache (Recommended)](#pre-warm-png-cache-recommended)
+ - [Passing Local Paths from an API](#passing-local-paths-from-an-api)
+ - [Draft Recovery](#draft-recovery)
+ - [Custom Semantic Color Table](#custom-semantic-color-table)
+- [Project Structure](#project-structure)
+- [Dependencies](#dependencies)
+- [Performance](#performance)
+ - [Measured Reference (Dev Env + PNG Pre-warming)](#measured-reference-dev-env-png-pre-warming)
+ - [Resolution vs pipelineConfig](#resolution-vs-pipelineconfig)
+ - [interactive Estimation (Default Pipeline)](#interactive-estimation-default-pipeline)
+ - [Device Tier (1080p, Default Pipeline)](#device-tier-1080p-default-pipeline)
+ - [Impact of Raising maxImageLongSide](#impact-of-raising-maximagelongside)
+ - [Optimization Tips](#optimization-tips)
+- [Notes](#notes)
+- [Troubleshooting](#troubleshooting)
-```bash
-npm install react-native-mask-segment-canvas
-# 或本地联调
-npm link ../MaskSegmentApp # 在库目录先执行 npm link
-npm link react-native-mask-segment-canvas
-```
+---
-宿主工程还需安装 **peerDependencies**(版本需与宿主 RN 对齐):
+## 🔭 Overview
+
+`MaskSegmentCanvas` renders an original image with an overlaid semantic mask, allowing users to tap regions and apply colors. The pipeline:
+
+1. 📥 **Load** the origin image and mask image (local `file://` or remote `http(s)://`)
+2. 🧩 **Segment** the mask via OpenCV into semantic regions (walls, ceiling, baseboard, etc.)
+3. 🎨 **Prepare** LAB frequency-layer textures via SkSL for realistic color blending
+4. 📐 **Build** Skia dashed-outline paths for each region
+5. 👆 **Interactive** — users select a brush color and tap regions to paint; paint layers preserve the underlying texture
+6. 💾 **Save** the composited result as PNG; export a JSON session for draft recovery
+
+The component emits `onWatch` state transitions through the pipeline so the host app can show appropriate loading states.
+
+---
+
+## 📋 Requirements
+
+- 🟢 Node.js >= 18 (recommended 20+)
+- 🍎 Xcode 15+ (iOS)
+- 🤖 Android Studio + JDK 17 (Android)
+- 📦 CocoaPods (iOS)
+
+---
+
+## 📦 Installation
+
+### 📦 Peer Dependencies
+
+Install these in your host project (versions should match your host RN version):
```bash
npm install @shopify/react-native-skia react-native-reanimated react-native-fast-opencv react-native-fs buffer upng-js
-# 若使用 showDebugPickers 相册选图
+# If using showDebugPickers (photo library picker)
npm install react-native-image-picker
```
-### 宿主工程 postinstall(必需)
+### 🛠️ Postinstall Setup
-本库依赖 `patch-package` 修补 `react-native-fast-opencv`,宿主 `package.json` 需配置:
+This library relies on `patch-package` to patch `react-native-fast-opencv`. Your host `package.json` must include:
```json
{
@@ -47,19 +112,19 @@ npm install react-native-image-picker
}
```
-安装本库后,`node_modules/react-native-mask-segment-canvas/patches/` 中的补丁会在宿主 `postinstall` 时自动应用。
+After installing this library, patches from `node_modules/react-native-mask-segment-canvas/patches/` are applied automatically during the host's `postinstall`.
-### iOS / Android 原生依赖
+### 📱 iOS / Android Native Dependencies
```bash
cd ios && pod install && cd ..
```
-确保宿主已按各原生库文档完成 Skia、Reanimated、OpenCV 等配置。
+Ensure the host project has completed Skia, Reanimated, and OpenCV native setup per each library's documentation.
-### Metro 配置(npm link / monorepo / file: 依赖时)
+### 🚇 Metro Configuration
-联调时若出现模块解析问题,在宿主 `metro.config.js` 中把本库加入 `watchFolders`,并使用下面推荐的完整配置(同时包含 extraNodeModules + blockList)。这是防止所有「类似重复模块问题」(SkiaPictureView undefined、Reanimated Animated node already exists 等)的可靠做法:
+When using `npm link`, a monorepo, or `file:` dependencies, add this library to `watchFolders` and use `extraNodeModules` + `blockList` to prevent duplicate module resolution:
```js
const path = require('path');
@@ -68,7 +133,6 @@ module.exports = {
watchFolders: [path.resolve(__dirname, '../MaskSegmentApp')],
resolver: {
nodeModulesPaths: [path.resolve(__dirname, 'node_modules')],
- // 推荐同时使用 extraNodeModules + blockList
extraNodeModules: {
'react-native-reanimated': path.resolve(__dirname, 'node_modules/react-native-reanimated'),
'@shopify/react-native-skia': path.resolve(__dirname, 'node_modules/@shopify/react-native-skia'),
@@ -89,103 +153,36 @@ module.exports = {
};
```
-**强烈建议**在宿主 `index.js` 最顶部加入(在任何业务代码之前):
+**Strongly recommended** — add this at the very top of the host `index.js` (before any business code):
```js
import '@shopify/react-native-skia';
```
-(完整推荐配置见下文「故障排查」以及 `example/metro.config.js` + `example/index.js`,那里有覆盖全部 peer 的 singletons 列表。)
+See `example/metro.config.js` and `example/index.js` for the complete configuration with all peer singleton packages.
-### 故障排查:各种重复模块导致的运行时错误
+### ⚠️ Troubleshooting: Duplicate Module Errors
-常见症状(同类问题):
+Common symptoms:
- `SkiaPictureView must be a function (received 'undefined')`
- `createAnimatedNode: Animated node[...] already exists`
-**几乎总是**因为 Metro 同时解析到多份 reanimated / skia / gesture-handler / fast-opencv / safe-area 等包。
+These are almost always caused by Metro resolving multiple copies of reanimated / skia / gesture-handler / fast-opencv / safe-area packages.
-**最佳实践**:
+**Best practice:**
-- 直接复制 `example/metro.config.js` 里的 `singletonPackages` + extraNodeModules + blockList 写法
-- 在你的 `index.js` 最顶部加入 gesture-handler → reanimated → skia 三个 import
-- 重启 Metro (`--reset-cache`) + 重装 app
-
-详细清单和模板见 `example/README.md` 的「运行时出现类似错误」一节。
-### 业务侧引入
-
-```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` |
+1. Copy the `singletonPackages` + `extraNodeModules` + `blockList` pattern from `example/metro.config.js`
+2. At the top of your `index.js`, import gesture-handler → reanimated → skia in order
+3. Restart Metro with `--reset-cache` and reinstall the app
+See `example/README.md` for a detailed checklist and template.
---
-## 推荐:通过 example/ 目录学习集成
+## 🚀 Quick Start (Dev Demo)
-`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`。
+The root `App.tsx` is a full self-test demo that imports directly from `./src`.
```bash
cd MaskSegmentApp
@@ -196,51 +193,21 @@ cd ios && bundle exec pod install && cd ..
npm start
-# 另开终端
+# In another terminal
npm run ios
-# 或
+# or
npm run android
```
-**想看「纯业务项目如何集成」**:请进入 `example/` 目录,按其 `README.md` 操作。它使用 `import from 'react-native-mask-segment-canvas'` + 标准的 `package.json` + Metro 配置,完全模拟消费者环境。
-
-Demo 入口 `App.tsx` 通过 `./src`(即包入口 `src/index.ts`)引用组件,与业务侧 `import from 'react-native-mask-segment-canvas'` 等价。
+**To see how a consumer project integrates:** go to the `example/` directory and follow its `README.md`. It uses `import from 'react-native-mask-segment-canvas'` with standard `package.json` and Metro config, fully simulating a consumer environment.
---
-## MaskSegmentCanvas 组件
+## 💡 Basic Usage
-### 引入
+### 🧑💻 Minimal Example
-```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';
-```
-
-也可按需导入运行时默认值(与组件 Props 合并使用):
-
-```tsx
-import {
- DEFAULT_PIPELINE_CONFIG,
- DEFAULT_MASK_CONFIG,
- DEFAULT_PAINT_CONFIG,
- DEFAULT_INTERACTION_CONFIG,
-} from 'react-native-mask-segment-canvas';
-```
-
-### 最小示例
-
-下面是一个可直接放进业务页面的完整示例,涵盖 **PNG 预热**、**state**、**配置**、**加载态**、**onWatch** 与 **ref** 常用操作。
+A complete, copy-pasteable example covering **PNG pre-warming**, **state management**, **configuration**, **loading states**, **onWatch**, and **ref operations**.
```tsx
import React, { useEffect, useRef, useState } from 'react';
@@ -254,7 +221,7 @@ import MaskSegmentCanvas, {
prewarmPngBgrCacheAsync,
} from 'react-native-mask-segment-canvas';
-/** 业务侧准备好的图片地址(本地 file:// 或 http(s)://) */
+/** Image paths prepared by the host app (local file:// or http(s)://) */
type ImagePaths = {
origin: string;
mask: string;
@@ -284,7 +251,7 @@ export function PaintScreen() {
!INTERACTIVE_STATES.includes(watchState as MaskSegmentWatchState) &&
watchState !== 'error';
- // 示例:接口下载完成后写入路径,并预热 PNG 解码缓存
+ // Example: download images, then pre-warm PNG decode cache
useEffect(() => {
let cancelled = false;
@@ -322,18 +289,18 @@ export function PaintScreen() {
return (
- 等待原图与掩码…
+ Waiting for origin and mask images...
);
}
return (
- {isCanvasLoading ? 加载中:{watchState} : null}
+ {isCanvasLoading ? Loading: {watchState} : null}
{watchState === 'interactive' ? (
- 可上色(轮廓加载中…)
+ Paintable (outlines loading...)
) : null}
- {isOutlineReady ? 就绪 : null}
+ {isOutlineReady ? Ready : null}
{errorMessage ? {errorMessage} : null}
{
setWatchState(state);
- // detail: regionCount, maskPathsReady, freqLayersReady, errorMessage
console.log('[onWatch]', state, durationMs, detail);
}}
onPaintCallback={payload => {
@@ -374,49 +340,44 @@ export function PaintScreen() {
setWatchState('error');
}}
/>
-
- {/* ref 示例:disabled={!isInteractive} 时可按需绑定 */}
- {/* */}
- {/*
);
}
```
-#### 示例里涉及的 state 说明
+### 📊 State Variables
-| state | 类型 | 用途 |
-| ----------------- | ---------------------------- | ------------------------------------------------ |
-| `imagePaths` | `{ origin, mask } | null` | 业务侧解析后的本地/远程图片路径 |
-| `pathsError` | `string` | 路径解析或 PNG 预热失败文案 |
-| `watchState` | `MaskSegmentWatchState | ''` | `onWatch` 上报的初始化阶段 |
-| `isInteractive` | 派生 | `interactive` 或 `mask_paths_ready` 时为 true,可开放操作 |
-| `isOutlineReady` | 派生 | `mask_paths_ready` 时为 true,轮播虚线已就绪 |
-| `isCanvasLoading` | 派生 | 画布初始化阻塞 Loading(不含 PNG 路径等待) |
-| `errorMessage` | `string` | 由 `onError` 写入的分割/加载失败文案 |
-| `sessionDraft` | `MaskSegmentSession | null` | MMKV 等恢复的草稿 |
+| State | Type | Purpose |
+| ----------------- | ---------------------------- | ------------------------------------------------------------------------ |
+| `imagePaths` | `{ origin, mask } | null` | Local/remote image paths resolved by the host |
+| `pathsError` | `string` | Error message when path resolution or PNG pre-warming fails |
+| `watchState` | `MaskSegmentWatchState | ''` | Initialization stage reported by `onWatch` |
+| `isInteractive` | derived | `true` when `interactive` or `mask_paths_ready` — operations are allowed |
+| `isOutlineReady` | derived | `true` when `mask_paths_ready` — carousel dashed outlines are ready |
+| `isCanvasLoading` | derived | Canvas init is blocking (not including PNG path waiting) |
+| `errorMessage` | `string` | Segmentation/loading failure message written by `onError` |
+| `sessionDraft` | `MaskSegmentSession | null` | Draft restored from MMKV or similar storage |
-#### 配置项怎么选
+### ⚙️ Choosing Configuration Values
-| 配置 | 何时用顶层属性 | 何时用嵌套 Config |
-| ---------- | -------------------------------- | ----------------------------------------- |
-| 语义识别色 | `semanticColors={...}` 大多数场景 | `maskConfig.semanticColors` 需与更多掩码参数一起传时 |
-| 虚线颜色 | `regionOutlineColor="..."` 大多数场景 | `paintConfig.regionOverlayFill` 需同时改笔刷盘等时 |
-| 黑色阈值、最大分区数 | — | `maskConfig` |
-| 图片处理尺寸 | — | `pipelineConfig` |
-| 轮播间隔、点击容差 | — | `interactionConfig` |
+| Config | Use top-level prop when... | Use nested Config when... |
+| ----------------------------- | ----------------------------------------- | ----------------------------------------------------------------------- |
+| Semantic colors | `semanticColors={...}` for most cases | `maskConfig.semanticColors` when paired with other mask params |
+| Outline color | `regionOutlineColor="..."` for most cases | `paintConfig.regionOverlayFill` when also customizing the brush palette |
+| Black threshold, max regions | — | `maskConfig` |
+| Image processing size | — | `pipelineConfig` |
+| Flash interval, tap tolerance | — | `interactionConfig` |
-顶层属性与嵌套 Config **可同时传**,顶层 `semanticColors` / `regionOutlineColor` 优先级更高。
+Top-level props and nested Configs **can coexist**; top-level `semanticColors` / `regionOutlineColor` take priority.
-#### watchState 与 UI 建议
+### 🔄 watchState & UI Guidance
```ts
-// 阻塞式 Loading(分区 + 上色图层就绪前)
+// Blocking loading (before regions + paint layers are ready)
const isLoading = ![
'interactive',
'mask_paths_ready',
@@ -424,200 +385,241 @@ const isLoading = ![
'',
].includes(watchState);
-// 允许点击选区、选色、上色(不必等轮廓路径)
+// Allow tapping regions, selecting colors, painting (no need to wait for outline paths)
const canOperate =
watchState === 'interactive' || watchState === 'mask_paths_ready';
-// 初始化轮播虚线已全部就绪(可选,用于收起「轮廓准备中」提示)
+// Carousel dashed outlines are fully ready (optional — can dismiss "outlines preparing" hint)
const isOutlineReady = watchState === 'mask_paths_ready';
-// 显示错误页
+// Show error screen
const hasError = watchState === 'error';
```
-`interactive` 时 `detail.maskPathsReady` 一般为 `false`;`mask_paths_ready` 时为 `true`。两者通常相差约 100ms(异步构建 Skia 轮廓路径),不影响点击上色。
+At `interactive`, `detail.maskPathsReady` is typically `false`; at `mask_paths_ready`, it is `true`. The gap is roughly ~100ms (async Skia path construction) and does not block tap-to-paint.
-`originUrl` / `maskUrl` 支持:
+`originUrl` / `maskUrl` support:
-- 本地路径:`file:///...` 或绝对路径
-- 远程地址:`http(s)://...`(组件内部会下载/解析)
+- Local paths: `file:///...` or absolute paths
+- Remote URLs: `http(s)://...` (the component handles download and decoding internally)
-> 兼容旧属性 `originImgPath` / `maskImgPath`(已标记 deprecated,请改用 `originUrl` / `maskUrl`)。
+> Legacy props `originImgPath` / `maskImgPath` are deprecated; use `originUrl` / `maskUrl` instead.
---
-## Props
+## 📖 API Reference
-### 图片与初始化
+### 📥 Imports
+
+```tsx
+import MaskSegmentCanvas, {
+ type MaskSegmentCanvasRef,
+ type MaskSegmentCanvasProps,
+ 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';
+```
-| 属性 | 类型 | 必填 | 默认 | 说明 |
-| ------------------------ | ------------------------- | --- | --- | ----------------------------------------------------------------- |
-| `originUrl` | `string` | 是* | — | 原图地址(`file://`、绝对路径或 `http(s)://`) |
-| `maskUrl` | `string` | 是* | — | 掩码图地址(语义色块图,建议与原图同尺寸) |
-| `originImgPath` | `string` | — | — | **deprecated**,请用 `originUrl` |
-| `maskImgPath` | `string` | — | — | **deprecated**,请用 `maskUrl` |
-| `initialSession` | `MaskSegmentSession` | 否 | — | 从 MMKV 等恢复的草稿;分区就绪后自动 `loadSession` |
-| `initialPaintColor` | `BgrColor` | 否 | — | **可选**。初始自定义笔刷色 `{ b, g, r }`;不传则默认无笔刷,需用户选色或 `ref.setPaintColor` |
-| `initialPaintConfigJson` | `Record` | 否 | — | **可选**。与 `initialPaintColor` 配套的笔刷配置,上色成功时随 `onPaintCallback` 回传 |
+| Category | Names |
+| ------------------------ | -------------------------------------------------------------------------------------- |
+| Component | `MaskSegmentCanvas` (default) |
+| Ref / Props types | `MaskSegmentCanvasRef`, `MaskSegmentCanvasProps` |
+| Session / callback types | `MaskSegmentSession`, `PaintCallbackPayload`, `PaintedRegionRecord`, `SavePaintResult` |
+| Watch types | `MaskSegmentWatchState`, `MaskSegmentWatchDetail` |
+| Config types | `PipelineConfig`, `MaskSegmentConfig`, `PaintConfig`, `InteractionConfig` |
+| Semantic colors | `MASK_SEMANTIC_COLORS`, `BASEBOARD_SEMANTIC_NAME` |
+| Utilities | `prewarmPngBgrCacheAsync` |
+| Runtime defaults | `DEFAULT_*_CONFIG` |
-### 识别色与虚线(顶层便捷配置)
+### 🖼️ Props: Image & Initialization
-| 属性 | 类型 | 默认 | 说明 |
-| -------------------- | --------------------- | -------------------------- | ------------------------------------------ |
-| `semanticColors` | `MaskSemanticColor[]` | `MASK_SEMANTIC_COLORS` | 掩码语义识别色,等同 `maskConfig.semanticColors` |
-| `regionOutlineColor` | `string` | `rgba(20, 120, 235, 0.58)` | 分区虚线高亮色,等同 `paintConfig.regionOverlayFill` |
+| Prop | Type | Required | Default | Description |
+| ------------------------ | ------------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `originUrl` | `string` | yes* | — | Origin image URL (`file://`, absolute path, or `http(s)://`) |
+| `maskUrl` | `string` | yes* | — | Mask image URL (semantic color-block image; recommended same dimensions as origin) |
+| `originImgPath` | `string` | — | — | **Deprecated** — use `originUrl` |
+| `maskImgPath` | `string` | — | — | **Deprecated** — use `maskUrl` |
+| `initialSession` | `MaskSegmentSession` | no | — | Draft restored from MMKV etc.; automatically calls `loadSession` after regions are ready |
+| `initialPaintColor` | `BgrColor` | no | — | **Optional**. Initial custom brush color `{ b, g, r }`; if omitted, no brush is selected by default; user must select a color or call `ref.setPaintColor` |
+| `initialPaintConfigJson` | `Record` | no | — | **Optional**. Accompanying brush config for `initialPaintColor`; passed back via `onPaintCallback` on successful paint |
-顶层属性优先级高于嵌套 `maskConfig` / `paintConfig`。
+### 🎨 Props: Semantic Colors & Outline
-`MaskSemanticColor` 结构:
+
+| Prop | Type | Default | Description |
+| -------------------- | --------------------- | -------------------------- | ---------------------------------------------------------------------------- |
+| `semanticColors` | `MaskSemanticColor[]` | `MASK_SEMANTIC_COLORS` | Mask semantic recognition colors; equivalent to `maskConfig.semanticColors` |
+| `regionOutlineColor` | `string` | `rgba(20, 120, 235, 0.58)` | Region dashed highlight color; equivalent to `paintConfig.regionOverlayFill` |
+
+
+Top-level props take priority over nested `maskConfig` / `paintConfig`.
+
+`MaskSemanticColor` structure:
```ts
{
- name: string; // 语义名,如 wall / ceiling / baseboard
- hex: string; // 展示用十六进制色
- bgr: { b: number; g: number; r: number }; // 与掩码像素 BGR 通道一致
+ name: string; // Semantic name, e.g. wall / ceiling / baseboard
+ hex: string; // Display hex color
+ bgr: { b: number; g: number; r: number }; // Must match mask pixel BGR channels
}
```
-内置色表:`MASK_SEMANTIC_COLORS`(`src/utils/maskSemanticPalette.ts`)。
+Built-in palette: `MASK_SEMANTIC_COLORS` (see `src/utils/maskSemanticPalette.ts`).
-### maskConfig
+### 🧩 Props: maskConfig
-| 字段 | 类型 | 默认 | 说明 |
-| ------------------------------ | --------------------- | ------------------------ | ------------------------------------- |
-| `semanticColors` | `MaskSemanticColor[]` | 内置色表 | 掩码语义色(可被顶层 `semanticColors` 覆盖) |
-| `blackThreshold` | `number` | `30` | BGR 最大值低于此值的像素视为黑色背景 |
-| `maxRegionColors` | `number` | `6` | 最终保留的最大语义分区数 |
-| `quantStep` | `number` | `64` | 踢脚线量化步长 |
-| `baseboardMaxColorDist` | `number` | `42` | 踢脚线色距阈值 |
-| `baseboardStripQuantKeys` | `string[]` | 内置键集 | 踢脚线条带量化键,格式 `"b,g,r"` |
-| `wallQuantKeys` | `string[]` | 内置键集 | 墙面量化键 |
-| `cabinetQuantKeys` | `string[]` | 内置键集 | 柜体量化键 |
-| `secondarySemanticNames` | `string[]` | `garageDoor, roof, eave` | 次要语义名 |
-| `secondaryMinPixelRatio` | `number` | `0.002` | 次要语义最小像素占比 |
-| `junctionHRadiusPx` | `number` | `24` | 踢脚线交界水平半径 |
-| `junctionVRadiusPx` | `number` | `2` | 踢脚线交界垂直半径 |
-| `kickBridgeHalfWPx` | `number` | `6` | 踢脚线横向补缝半宽 |
-| `baseboardJunctionRowMarginPx` | `number` | `1` | 踢脚线交界行边距 |
-| `baseboardJunctionVReachPx` | `number` | `2` | 踢脚线交界纵向延伸 |
-| `baseboardMinRunPx` | `number` | `2` | 蒙版条带最小 run 长度 |
-| `splitWalls` | `boolean` | `false` | 在 wall 掩码内按纹理边界细分为 `wall-1`、`wall-2`… |
-| `splitWallsMaxCount` | `number` | `8` | 墙壁子区最大数量 |
-| `splitWallsMinAreaRatio` | `number` | `0.002` | 碎块最小面积比(相对 seg 总像素) |
-| `splitWallsColorDistSq` | `number` | `1400` | 连通域色度均值距离平方阈值(墙内光影容忍,材质间更严) |
-| `splitWallsChromaBlurRadius` | `number` | `5` | 预留:色度平滑半径 |
-| `splitWallsNeutralChromaMax` | `number` | `14` | 白/灰墙低饱和判定半径;与有色墙强制分界 |
+| Field | Type | Default | Description |
+| ------------------------------ | --------------------- | ------------------------ | ----------------------------------------------------------------------- |
+| `semanticColors` | `MaskSemanticColor[]` | built-in palette | Mask semantic colors (overridable by top-level `semanticColors`) |
+| `blackThreshold` | `number` | `30` | Pixels with max(B,G,R) below this value are treated as black background |
+| `maxRegionColors` | `number` | `6` | Maximum semantic regions retained |
+| `quantStep` | `number` | `64` | Baseboard quantization step |
+| `baseboardMaxColorDist` | `number` | `42` | Baseboard color distance threshold |
+| `baseboardStripQuantKeys` | `string[]` | built-in keys | Baseboard strip quantization keys, format `"b,g,r"` |
+| `wallQuantKeys` | `string[]` | built-in keys | Wall quantization keys |
+| `cabinetQuantKeys` | `string[]` | built-in keys | Cabinet quantization keys |
+| `secondarySemanticNames` | `string[]` | `garageDoor, roof, eave` | Secondary semantic names |
+| `secondaryMinPixelRatio` | `number` | `0.002` | Minimum pixel ratio for secondary semantics |
+| `junctionHRadiusPx` | `number` | `24` | Baseboard junction horizontal radius |
+| `junctionVRadiusPx` | `number` | `2` | Baseboard junction vertical radius |
+| `kickBridgeHalfWPx` | `number` | `6` | Baseboard horizontal gap bridge half-width |
+| `baseboardJunctionRowMarginPx` | `number` | `1` | Baseboard junction row margin |
+| `baseboardJunctionVReachPx` | `number` | `2` | Baseboard junction vertical reach |
+| `baseboardMinRunPx` | `number` | `2` | Minimum run length for mask strips |
+| `splitWalls` | `boolean` | `false` | Split wall mask into `wall-1`, `wall-2`… by texture boundaries |
+| `splitWallsMaxCount` | `number` | `8` | Max wall sub-region count |
+| `splitWallsMinAreaRatio` | `number` | `0.002` | Minimum area ratio for fragments (relative to total seg pixels) |
+| `splitWallsColorDistSq` | `number` | `1400` | Connected-component chroma mean distance squared threshold |
+| `splitWallsChromaBlurRadius` | `number` | `5` | Reserved: chroma smoothing radius |
+| `splitWallsNeutralChromaMax` | `number` | `14` | White/gray wall low-chroma radius; forced boundary from colored walls |
-开启 `splitWalls` 后,原有单一 `wall` 区域会被替换为多个 `wall-N` 子区,各自独立上色与撤销。旧 Session 中 `regionName: 'wall'` 无法映射到新子区名,需重新上色。
+When `splitWalls` is enabled, the single `wall` region is replaced by multiple `wall-N` sub-regions, each independently paintable and undoable. Old sessions with `regionName: 'wall'` cannot map to new sub-region names and must be repainted.
-### pipelineConfig
+### 🔬 Props: pipelineConfig
-| 字段 | 类型 | 默认 | 说明 |
-| -------------------------- | -------- | ------- | ----------------------- |
-| `maxImageLongSide` | `number` | `720` | 分割 / pickMap / 工作区缩放最长边 |
-| `paintFreqMaxLongSide` | `number` | `480` | OpenCV LAB 高低频最长边 |
-| `originPreviewMaxLongSide` | `number` | `360` | 预览最长边(主路径走工作区分辨率) |
-| `maskPathMaxLongSide` | `number` | `480` | 虚线轮廓降采样最长边 |
-| `minContourArea` | `number` | `100` | 最小轮廓面积(随缩放同比缩放) |
-| `contourApproxEpsilon` | `number` | `0.003` | 轮廓多边形逼近系数 |
-| `maxRegions` | `number` | `500` | 分割阶段最大区域数上限 |
+| Field | Type | Default | Description |
+| -------------------------- | -------- | ------- | ------------------------------------------------------------------- |
+| `maxImageLongSide` | `number` | `720` | Maximum long side for segmentation / pickMap / working area scaling |
+| `paintFreqMaxLongSide` | `number` | `480` | Maximum long side for OpenCV LAB frequency layers |
+| `originPreviewMaxLongSide` | `number` | `360` | Maximum long side for preview (main path uses working resolution) |
+| `maskPathMaxLongSide` | `number` | `480` | Maximum long side for outline contour downsampling |
+| `minContourArea` | `number` | `100` | Minimum contour area (scales proportionally with resolution) |
+| `contourApproxEpsilon` | `number` | `0.003` | Contour polygon approximation coefficient |
+| `maxRegions` | `number` | `500` | Maximum region count during segmentation |
-### paintConfig
+### 🖌️ Props: paintConfig
-| 字段 | 类型 | 默认 | 说明 |
-| -------------------------- | ------------ | ----------------------- | -------------------------- |
-| `palette` | `BgrColor[]` | 6 色内置盘 | 底部笔刷色条 |
-| `colorBaseOpacity` | `number` | `0.88` | 底色不透明度 |
-| `lLightOpacity` | `number` | `0.50` | L 通道叠加强度 |
-| `textureOpacity` | `number` | `0.85` | 高频纹理叠加强度(纹理保留更强) |
-| `lLowBlurKernel` | `number` | `7` | 低频高斯核(奇数) |
-| `lLowContrast` | `number` | `1.15` | 低频对比度 |
-| `lLowBrightness` | `number` | `0.9` | 低频亮度 |
-| `lHighGain` | `number` | `1.22` | 高频增益 |
-| `maskFeatherColor` | `number` | `1.6` | 上色边缘羽化(颜色)——软边 alpha 半径,像素 |
-| `maskFeatherTexture` | `number` | `0.9` | 上色边缘羽化(纹理)——预留/辅助 |
-| `regionOverlayFill` | `string` | `rgba(20,120,235,0.58)` | 虚线/高亮填充色 |
-| `regionOutlineStrokeWidth` | `number` | `4` | 虚线描边宽度 |
+| Field | Type | Default | Description |
+| -------------------------- | ------------ | ----------------------- | ------------------------------------------------------------------------ |
+| `palette` | `BgrColor[]` | 6-color built-in | Bottom brush color strip |
+| `colorBaseOpacity` | `number` | `0.88` | Base color opacity |
+| `lLightOpacity` | `number` | `0.50` | L-channel overlay intensity |
+| `textureOpacity` | `number` | `0.85` | High-frequency texture overlay intensity (stronger texture preservation) |
+| `lLowBlurKernel` | `number` | `7` | Low-frequency Gaussian kernel (odd number) |
+| `lLowContrast` | `number` | `1.15` | Low-frequency contrast |
+| `lLowBrightness` | `number` | `0.9` | Low-frequency brightness |
+| `lHighGain` | `number` | `1.22` | High-frequency gain |
+| `maskFeatherColor` | `number` | `1.6` | Paint edge feathering (color) — soft-edge alpha radius, in pixels |
+| `maskFeatherTexture` | `number` | `0.9` | Paint edge feathering (texture) — reserved/auxiliary |
+| `regionOverlayFill` | `string` | `rgba(20,120,235,0.58)` | Dashed line / highlight fill color |
+| `regionOutlineStrokeWidth` | `number` | `4` | Dashed outline stroke width |
-### interactionConfig
+### 👆 Props: interactionConfig
-| 字段 | 类型 | 默认 | 说明 |
-| ----------------------- | --------- | ------- | ------------------- |
-| `pickMapSearchRadiusPx` | `number` | `14` | 点击 pickMap 搜索半径(像素) |
-| `kickMaskPickRadiusPx` | `number` | `36` | 踢脚线掩码拾取半径 |
-| `thinStripPadding` | `number` | `0.008` | 细条带(踢脚线)点击扩展比例 |
-| `regionPadding` | `number` | `0.003` | 普通分区点击扩展比例 |
-| `initRegionFlashMs` | `number` | `1000` | 初始化轮播每条虚线停留毫秒 |
-| `enableInitRegionFlash` | `boolean` | `true` | 是否启用初始化轮播 |
+| Field | Type | Default | Description |
+| ----------------------- | --------- | ------- | --------------------------------------------------------------- |
+| `pickMapSearchRadiusPx` | `number` | `14` | Click pickMap search radius (pixels) |
+| `kickMaskPickRadiusPx` | `number` | `36` | Baseboard mask pick radius |
+| `thinStripPadding` | `number` | `0.008` | Thin strip (baseboard) tap expansion ratio |
+| `regionPadding` | `number` | `0.003` | Normal region tap expansion ratio |
+| `initRegionFlashMs` | `number` | `1000` | Duration each dashed outline stays during initial carousel (ms) |
+| `enableInitRegionFlash` | `boolean` | `true` | Enable initial carousel animation |
-> 完整默认值常量:`DEFAULT_MASK_CONFIG`、`DEFAULT_PIPELINE_CONFIG`、`DEFAULT_PAINT_CONFIG`、`DEFAULT_INTERACTION_CONFIG`(自包入口导出)。
+> Full default constants: `DEFAULT_MASK_CONFIG`, `DEFAULT_PIPELINE_CONFIG`, `DEFAULT_PAINT_CONFIG`, `DEFAULT_INTERACTION_CONFIG` (exported from the package entry).
-### UI 开关与样式
+### 🎛️ Props: UI Controls & Styling
-| 属性 | 类型 | 默认 | 说明 |
-| ------------------------------------------------ | ---------------------- | ------- | ---------------------- |
-| `showToolbar` | `boolean` | `true` | 顶部「清空缓存重新分区」工具栏 |
-| `showColorBar` | `boolean` | `true` | 底部笔刷色条 |
-| `showStatusRow` | `boolean` | `true` | 分割/加载状态文案 |
-| `showOverlayButtons` | `boolean` | `true` | 左下撤销、右下对比原图按钮 |
-| `showDebugPickers` | `boolean` | `true` | 相册选图调试入口(生产建议 `false`) |
-| `disabled` | `boolean` | `false` | 禁用上色交互 |
-| `style` | `ViewStyle` | — | 外层容器样式 |
-| `canvasStyle` | `ViewStyle` | — | 画布区域样式 |
-| `undoButtonStyle` / `compareButtonStyle` | `ViewStyle` | — | 浮层按钮样式 |
-| `undoButtonTextStyle` / `compareButtonTextStyle` | `TextStyle` | — | 浮层按钮文字样式 |
-| `undoButtonText` | `string` | `撤销` | 撤销按钮文案 |
-| `compareButtonText` | `string` | `对比原图` | 进入对比模式文案 |
-| `compareExitButtonText` | `string` | `退出对比` | 退出对比模式文案 |
-| `renderUndoButton` | `(props) => ReactNode` | — | 自定义撤销按钮 |
-| `renderCompareButton` | `(props) => ReactNode` | — | 自定义对比按钮 |
+| Prop | Type | Default | Description |
+| ------------------------------------------------ | ---------------------- | ------------------- | --------------------------------------------------------- |
+| `showToolbar` | `boolean` | `true` | Top toolbar ("Clear cache & re-segment") |
+| `showColorBar` | `boolean` | `true` | Bottom brush color strip |
+| `showStatusRow` | `boolean` | `true` | Segmentation/loading status text |
+| `showOverlayButtons` | `boolean` | `true` | Bottom-left undo, bottom-right compare buttons |
+| `showDebugPickers` | `boolean` | `true` | Photo library debug picker (set to `false` in production) |
+| `disabled` | `boolean` | `false` | Disable paint interaction |
+| `style` | `ViewStyle` | — | Outer container style |
+| `canvasStyle` | `ViewStyle` | — | Canvas area style |
+| `undoButtonStyle` / `compareButtonStyle` | `ViewStyle` | — | Overlay button styles |
+| `undoButtonTextStyle` / `compareButtonTextStyle` | `TextStyle` | — | Overlay button text styles |
+| `undoButtonText` | `string` | `Undo` (zh) | Undo button label |
+| `compareButtonText` | `string` | `Compare` (zh) | Enter compare mode label |
+| `compareExitButtonText` | `string` | `Exit Compare` (zh) | Exit compare mode label |
+| `renderUndoButton` | `(props) => ReactNode` | — | Custom undo button renderer |
+| `renderCompareButton` | `(props) => ReactNode` | — | Custom compare button renderer |
-### 回调
+### 📞 Props: Callbacks
-| 属性 | 签名 | 说明 |
-| ----------------- | ----------------------------------------- | ---------------------------------- |
-| `onWatch` | `(state, durationMs, detail?) => void` | 初始化阶段回调;`durationMs` 自本次 `init` 起算 |
-| `onPaintCallback` | `(payload: PaintCallbackPayload) => void` | 上色成功或未选笔刷时点击分区 |
-| `onError` | `(message, error?) => void` | 分割或加载失败 |
+| Prop | Signature | Description |
+| ----------------- | ----------------------------------------- | ---------------------------------------------------------------------------- |
+| `onWatch` | `(state, durationMs, detail?) => void` | Initialization stage callback; `durationMs` is relative to this `init` start |
+| `onPaintCallback` | `(payload: PaintCallbackPayload) => void` | Fires on successful paint, or when tapping a region without a brush selected |
+| `onError` | `(message, error?) => void` | Segmentation or loading failure |
-`PaintCallbackPayload`(判别联合,`payload.kind` 区分):
+`PaintCallbackPayload` (discriminated union, distinguished by `payload.kind`):
```ts
-// 上色成功
+// Successful paint
{
kind: 'painted';
regionId: number;
regionName: string;
color: BgrColor;
- configJson?: Record; // setPaintColor / initialPaintConfigJson 传入
+ configJson?: Record; // from setPaintColor / initialPaintConfigJson
}
-// 未选笔刷时点击有效分区(不会上色)
+// Tapped a valid region without selecting a brush (no paint performed)
{
kind: 'brush_required';
- hint: string; // 如「请先选择笔刷颜色(底部色条或 ref.setPaintColor)」
+ hint: string; // e.g. "Please select a brush color first (bottom color bar or ref.setPaintColor)"
regionId: number;
regionName: string;
}
```
-示例:
+Example:
```tsx
onPaintCallback={payload => {
@@ -629,65 +631,63 @@ onPaintCallback={payload => {
}}
```
-`onWatch` 的 `detail`(`MaskSegmentWatchDetail`):
+`onWatch` `detail` (`MaskSegmentWatchDetail`):
-| 字段 | 类型 | 说明 |
-| ----------------- | --------- | ----------------- |
-| `regionCount` | `number` | 当前有效分区数 |
-| `maskPathsReady` | `boolean` | 轮廓 Skia 路径是否就绪 |
-| `freqLayersReady` | `boolean` | 高低频 Shader 纹理是否就绪 |
-| `errorMessage` | `string` | `error` 状态下的失败说明 |
+| Field | Type | Description |
+| ----------------- | --------- | ------------------------------------------- |
+| `regionCount` | `number` | Current effective region count |
+| `maskPathsReady` | `boolean` | Whether outline Skia paths are ready |
+| `freqLayersReady` | `boolean` | Whether frequency Shader textures are ready |
+| `errorMessage` | `string` | Failure description in `error` state |
-#### onWatch 状态流转
+#### onWatch State Flow
```
init
- → images_loaded 原图 + 掩码读取完成
- → mask_aligned 掩码尺寸对齐完成
- → mask_sampled 掩码像素采样完成
- → regions_ready 分区提取成功
- → layers_ready 上色纹理图层就绪(detail.maskPathsReady 可能仍为 false)
- → interactive 可交互(可点击选区、选色、上色)
- → mask_paths_ready 轮廓路径就绪(初始化轮播虚线可显示,detail.maskPathsReady 为 true)
- → error 失败(detail.errorMessage 有说明)
+ → images_loaded Origin + mask read complete
+ → mask_aligned Mask dimensions aligned
+ → mask_sampled Mask pixel sampling complete
+ → regions_ready Region extraction succeeded
+ → layers_ready Paint texture layers ready (detail.maskPathsReady may still be false)
+ → interactive Interactive (can tap regions, select colors, paint)
+ → mask_paths_ready Outline paths ready (carousel dashed outlines can display; detail.maskPathsReady is true)
+ → error Failure (detail.errorMessage has description)
```
-`layers_ready` / `interactive` 可能在轮廓路径算完之前触发;业务侧若以 `interactive` 关闭 Loading,用户即可操作,轮播虚线会在 `mask_paths_ready` 后自动出现。
+`layers_ready` / `interactive` may fire before outline paths finish computing. If the host dismisses a blocking loader at `interactive`, the user can already operate; carousel dashed outlines appear automatically after `mask_paths_ready`.
----
+### 🔧 Ref Methods
-## Ref 方法
-
-通过 `ref` 调用(类型 `MaskSegmentCanvasRef`):
+Accessed via `ref` (type `MaskSegmentCanvasRef`):
-| 方法 | 签名 | 说明 |
-| ------------------- | ---------------------------------------- | ------------------------------------- |
-| `reset` | `() => void` | 撤销上一步上色(按 `paintHistory`) |
-| `swap` | `(showOrigin?: boolean) => void` | 对比原图;不传参 toggle,传 `true`/`false` 显式开关 |
-| `save` | `(options?) => Promise` | 合成并保存 PNG;`options.destDir` 可选输出目录 |
-| `session` | `() => MaskSegmentSession` | 导出可 JSON 序列化会话(存 MMKV) |
-| `loadSession` | `(session) => void` | 恢复上色状态(也可通过 `initialSession`) |
-| `setPaintColor` | `(color, configJson?) => void` | 设置当前笔刷色,清空底部色条选中 |
-| `setMaskConfig` | `(config) => void` | 运行时更新掩码配置并**重新分割** |
-| `clearAllPaint` | `() => void` | 清空全部上色记录 |
-| `resegment` | `() => Promise` | 清空 PNG 缓存并重新分割 |
-| `getRegions` | `() => SegmentRegion[]` | 当前分区列表快照 |
-| `getPaintedRegions` | `() => PaintedRegionRecord[]` | 当前上色记录快照 |
+| Method | Signature | Description |
+| ------------------- | ---------------------------------------- | --------------------------------------------------------------------------- |
+| `reset` | `() => void` | Undo last paint step (by `paintHistory`) |
+| `swap` | `(showOrigin?: boolean) => void` | Toggle origin image comparison; omit arg to toggle, `true`/`false` to force |
+| `save` | `(options?) => Promise` | Composite and save PNG; `options.destDir` optional output directory |
+| `session` | `() => MaskSegmentSession` | Export JSON-serializable session (for MMKV storage) |
+| `loadSession` | `(session) => void` | Restore paint state (also available via `initialSession`) |
+| `setPaintColor` | `(color, configJson?) => void` | Set current brush color; clears bottom color bar selection |
+| `setMaskConfig` | `(config) => void` | Update mask config at runtime and **re-segment** |
+| `clearAllPaint` | `() => void` | Clear all paint records |
+| `resegment` | `() => Promise` | Clear PNG cache and re-segment |
+| `getRegions` | `() => SegmentRegion[]` | Snapshot of current region list |
+| `getPaintedRegions` | `() => PaintedRegionRecord[]` | Snapshot of current paint records |
-`SavePaintResult`:`{ filePath, width, height, paintedCount, previewPath? }`
+`SavePaintResult`: `{ filePath, width, height, paintedCount, previewPath? }`
-代码示例:
+Code examples:
```tsx
const ref = useRef(null);
ref.current?.reset();
ref.current?.swap(); // toggle
-ref.current?.swap(true); // 强制显示原图
+ref.current?.swap(true); // force show origin
const result = await ref.current?.save({ destDir: '/path/to/dir' });
@@ -704,18 +704,18 @@ const regions = ref.current?.getRegions();
const painted = ref.current?.getPaintedRegions();
```
-> `save` 依赖工作区 buffer 与 pickMap 就绪(通常 `interactive` 之后);未就绪时 throw `'图像尚未就绪,无法保存'`。
+> `save` depends on the working buffer and pickMap being ready (typically after `interactive`); throws `'Image not ready, cannot save'` if not ready.
-### 存储约定
+### 💾 Storage Convention
-| 能力 | 建议存储 | 内容 |
-| --------------- | ------------------- | ----------------------- |
-| `ref.save()` | 文件系统 | 大图 PNG 路径 |
-| `ref.session()` | MMKV / AsyncStorage | JSON 元数据(URL、上色记录、笔刷色等) |
+| Capability | Recommended Storage | Content |
+| --------------- | ------------------- | ------------------------------------------------------ |
+| `ref.save()` | File system | Full-res PNG path |
+| `ref.session()` | MMKV / AsyncStorage | JSON metadata (URLs, paint records, brush color, etc.) |
-`MaskSegmentSession` 结构:
+`MaskSegmentSession` structure:
```ts
{
@@ -732,20 +732,20 @@ const painted = ref.current?.getPaintedRegions();
---
-## 交互说明
+## 🎮 Interaction Guide
-1. **初始化轮播**:分区就绪后,按 `initRegionFlashMs`(默认 1s)轮播各分区虚线轮廓;用户触摸画布后停止。
-2. **预览(未选笔刷)**:长按分区显示当前触摸点所在连通块的虚线轮廓;点击黑色区域不显示虚线。
-3. **上色(已选笔刷)**:先点底部色条或 `ref.setPaintColor`(也可通过 `initialPaintColor` 预设),再点击分区上色;同一分区重复点击会覆盖颜色。
-4. **未选笔刷点击分区**:不会上色;`onPaintCallback` 以 `kind: 'brush_required'` 回调,携带 `hint` 与目标分区信息,由业务侧 Toast / 弹窗提示用户选色。
-5. **撤销**:左下角按钮或 `ref.reset()`,按上色历史逐步撤回。
-6. **对比原图**:右下角按钮或 `ref.swap()`,隐藏上色层查看原图。
+1. 🔁 **Initial Carousel**: After regions are ready, each region's dashed outline flashes sequentially per `initRegionFlashMs` (default 1s); stops on first user touch.
+2. 🔍 **Preview (no brush selected)**: Long-press a region to show dashed outline for the connected component under the touch point; tapping a black area shows no outline.
+3. 🎨 **Paint (brush selected)**: Tap a color in the bottom color bar or call `ref.setPaintColor` (or preselect via `initialPaintColor`), then tap a region to paint; tapping the same region again overwrites the color.
+4. 💬 **Tap without brush**: No paint is performed; `onPaintCallback` fires with `kind: 'brush_required'`, carrying a `hint` and target region info for the host to show a toast/modal prompting color selection.
+5. ↩️ **Undo**: Bottom-left button or `ref.reset()`; steps backward through paint history one action at a time.
+6. 👁️ **Compare with Origin**: Bottom-right button or `ref.swap()`; hides the paint layer to show the original image.
---
-## 接入业务示例
+## 🧩 Integration Examples
-### 下载后预热再挂载(推荐)
+### 🔥 Pre-warm PNG Cache (Recommended)
```tsx
import { prewarmPngBgrCacheAsync } from 'react-native-mask-segment-canvas';
@@ -756,7 +756,7 @@ async function openPaintScreen(originUrl: string, maskUrl: string) {
}
```
-### 接口下载后传入本地路径
+### 🌐 Passing Local Paths from an API
```tsx
```
-### 草稿恢复
+### 💾 Draft Recovery
```tsx
const draft = JSON.parse(mmkv.getString('paint_draft'));
@@ -785,7 +785,7 @@ const draft = JSON.parse(mmkv.getString('paint_draft'));
/>
```
-### 自定义语义色表
+### 🎨 Custom Semantic Color Table
```tsx
const gymColors: MaskSemanticColor[] = [
@@ -802,13 +802,13 @@ const gymColors: MaskSemanticColor[] = [
---
-## 项目结构
+## 📁 Project Structure
```
-MaskSegmentApp/ # 仓库根目录(npm 包 react-native-mask-segment-canvas)
-├── App.tsx # 库自测 Demo(直接引用 ./src)
+MaskSegmentApp/ # Repo root (npm package react-native-mask-segment-canvas)
+├── App.tsx # Dev self-test Demo (imports from ./src directly)
├── src/
-│ ├── index.ts # npm 包入口(业务 import 'react-native-mask-segment-canvas')
+│ ├── index.ts # Package entry (consumer: import 'react-native-mask-segment-canvas')
│ ├── components/
│ │ ├── MaskSegmentCanvas.tsx
│ │ └── MaskSegmentCanvas.types.ts
@@ -817,148 +817,148 @@ MaskSegmentApp/ # 仓库根目录(npm 包 react-n
│ ├── maskSegmentRuntime.ts
│ ├── maskSemanticPalette.ts
│ └── ...
-├── example/ # ★ 推荐:业务集成演示(消费者视角)
-│ ├── App.tsx # 只使用公开 API 的完整示例页面
+├── example/ # ★ Recommended: consumer-side integration demo
+│ ├── App.tsx # Full example using only the public API
│ ├── index.js / app.json
-│ ├── package.json # 展示所需依赖 + "react-native-mask-segment-canvas": "file:.."
+│ ├── package.json # Required deps + "react-native-mask-segment-canvas": "file:.."
│ ├── metro.config.js / babel.config.js / tsconfig.json
-│ └── README.md # 如何在真实项目中接入的说明
-├── patches/ # 随包发布,宿主 postinstall 应用
-├── ios/ # 根 Demo 原生工程(不发布到 npm)
+│ └── README.md # How to integrate in a real project
+├── patches/ # Shipped with the package; applied by host postinstall
+├── ios/ # Root Demo native project (not published to npm)
└── android/
```
---
-## 依赖说明
+## 📚 Dependencies
-| 包 | 用途 |
-| -------------------------------- | ---------------------------- |
-| `@shopify/react-native-skia` | Canvas 渲染、Path、虚线描边、Blend 混合 |
-| `react-native-fast-opencv` | 掩码形态学、轮廓处理 |
-| `react-native-fs` | 图层缓存、保存 PNG |
-| `react-native-image-picker` | Demo 相册选图 |
-| `react-native-reanimated` | Skia 动画依赖 |
-| `react-native-safe-area-context` | 安全区适配 |
+| Package | Purpose |
+| -------------------------------- | --------------------------------------------------------- |
+| `@shopify/react-native-skia` | Canvas rendering, Path, dashed strokes, Blend compositing |
+| `react-native-fast-opencv` | Mask morphology, contour processing |
+| `react-native-fs` | Layer caching, PNG save |
+| `react-native-image-picker` | Demo photo library picker |
+| `react-native-reanimated` | Skia animation dependency |
+| `react-native-safe-area-context` | Safe area insets |
---
-## 性能评测
+## ⚡ Performance
-以下数据基于 Demo 测试图(`assets/test/origin.png` **1080×1920**、6 个语义分区)、**默认 `pipelineConfig`**,以及 `onWatch` 的 `durationMs`(从 `init` 起算)。为**经验区间**,非严格基准测试;真机因 CPU、存储、RN 版本会有波动。
+The data below is based on the Demo test image (`assets/test/origin.png` **1080×1920**, 6 semantic regions), **default `pipelineConfig`**, and `onWatch` `durationMs` (measured from `init`). These are **empirical ranges**, not strict benchmarks; actual device results vary with CPU, storage, and RN version.
-### 实测参考(开发环境 + PNG 预热)
+### 📏 Measured Reference (Dev Env + PNG Pre-warming)
-Demo 在挂载画布前调用 `prewarmPngBgrCacheAsync([origin, mask])`,PNG 解码命中内存缓存。典型日志:
+The Demo calls `prewarmPngBgrCacheAsync([origin, mask])` before mounting the canvas, so PNG decoding hits the memory cache. Typical logs:
-| 阶段 | watchState | 约耗时 | 说明 |
-| ------- | -------------------------------- | -------------- | ------------------------------------ |
-| 掩码对齐 | `mask_aligned` | ~160ms | 掩码缩放到分割工作分辨率 |
-| 分区完成 | `regions_ready` / `mask_sampled` | ~320ms | 布局扫描 + 踢脚线 + pickMap |
-| **可交互** | `**interactive`** | **~320–450ms** | 可点击选区、选色、Shader 上色 |
-| 轮廓就绪 | `mask_paths_ready` | ~430–550ms | 比 `interactive` 晚 **~100ms**,轮播虚线可显示 |
+| Stage | watchState | Approx. Duration | Notes |
+| --------------- | -------------------------------- | ---------------- | --------------------------------------------------------- |
+| Mask aligned | `mask_aligned` | ~160ms | Mask scaled to segmentation working resolution |
+| Regions ready | `regions_ready` / `mask_sampled` | ~320ms | Layout scan + baseboard + pickMap |
+| **Interactive** | `**interactive`** | **~320–450ms** | Can tap regions, select colors, Shader paint |
+| Outlines ready | `mask_paths_ready` | ~430–550ms | ~100ms after `interactive`; carousel outlines can display |
-`interactive` **不等待**轮廓路径;`mask_paths_ready` 仅影响初始化轮播虚线与可选 UI 提示。
+`interactive` does **not wait** for outline paths; `mask_paths_ready` only affects the initial carousel and optional UI hints.
-同图各子步骤(`__DEV__` 日志,默认 pipeline)量级:
+Same-image sub-step magnitudes (`__DEV__` logs, default pipeline):
-| 子步骤 | 约耗时 | 工作分辨率 |
-| ----------------- | --------- | ------------------------------ |
-| OpenCV LAB 高低频 | ~10–40ms | 270×480 |
-| 高低频 Skia 纹理 | ~20–30ms | 同上 |
-| 布局扫描 + 踢脚线 + 点击查表 | ~90–120ms | 405×720(1080p 缩至 longSide 720) |
-| 全量轮廓路径(异步,不阻塞交互) | ~80–150ms | 270×480 |
+| Sub-step | Approx. Duration | Working Resolution |
+| ---------------------------------------- | ---------------- | ------------------------------ |
+| OpenCV LAB high/low freq | ~10–40ms | 270×480 |
+| High/low freq Skia textures | ~20–30ms | same |
+| Layout scan + baseboard + pick table | ~90–120ms | 405×720 (1080p → longSide 720) |
+| Full contour paths (async, non-blocking) | ~80–150ms | 270×480 |
-### 分辨率与 `pipelineConfig` 的关系
+### 📐 Resolution vs pipelineConfig
-计算密集型步骤被 **最长边上限** 截断,**不随 4K/8K 原图线性放大**;**PNG 全图解码**仍随像素量线性增长。
+Compute-intensive steps are capped by **maximum long side limits** and do **not scale linearly with 4K/8K origin images**. **Full PNG decoding** still scales linearly with pixel count.
-| 步骤 | 配置项 | 1080×1920 实际处理尺寸 | 随原图像素增长 |
-| -------------- | --------------------------- | ---------------- | ------------------------ |
-| PNG 解码 | — | 1080×1920 × 2 张 | **是** |
-| 掩码分割 / pickMap | `maxImageLongSide: 720` | ~405×720 | **否**(长边 >720 时固定) |
-| Shader 高低频 | `paintFreqMaxLongSide: 480` | ~270×480 | **否** |
-| 工作区 Skia 原图 | 同 `maxImageLongSide` | ~405×720 | **否** |
-| 虚线轮廓 | `maskPathMaxLongSide: 480` | ~270×480 | **否**(不阻塞 `interactive`) |
+| Step | Config Key | 1080×1920 Actual Size | Scales with Origin Pixels |
+| ------------------------ | --------------------------- | --------------------- | ------------------------------------- |
+| PNG decode | — | 1080×1920 × 2 images | **Yes** |
+| Mask seg / pickMap | `maxImageLongSide: 720` | ~405×720 | **No** (fixed when long side >720) |
+| Shader high/low freq | `paintFreqMaxLongSide: 480` | ~270×480 | **No** |
+| Working area Skia origin | same as `maxImageLongSide` | ~405×720 | **No** |
+| Dashed outlines | `maskPathMaxLongSide: 480` | ~270×480 | **No** (does not block `interactive`) |
-### `interactive` 预估(默认 pipeline)
+### ⏱️ interactive Estimation (Default Pipeline)
-| 原图规格 | 相对 1080p 像素 | 有 PNG 预热 | 冷启动(无预热) |
-| -------------- | ----------- | ------------- | -------------- |
-| 1080×1920 | 1× | **320–450ms** | **450–700ms** |
-| 1440×2560 (2K) | ~1.8× | **400–550ms** | **600–900ms** |
-| 3840×2160 (4K) | ~4× | **500–750ms** | **800–1200ms** |
-| 7680×4320 (8K) | ~16× | **0.8–1.5s** | **1.5–3s+** |
+| Origin Spec | Relative to 1080p Pixels | With PNG Pre-warm | Cold Start (no pre-warm) |
+| -------------- | ------------------------ | ----------------- | ------------------------ |
+| 1080×1920 | 1× | **320–450ms** | **450–700ms** |
+| 1440×2560 (2K) | ~1.8× | **400–550ms** | **600–900ms** |
+| 3840×2160 (4K) | ~4× | **500–750ms** | **800–1200ms** |
+| 7680×4320 (8K) | ~16× | **0.8–1.5s** | **1.5–3s+** |
-> **300ms 内可交互**:在 1080p + 预热 + 默认 pipeline + 中高端机上**接近但偏乐观**;不宜作为全机型 SLA。
+> **<300ms interactive**: achievable on 1080p + pre-warm + default pipeline + high-end devices, but **optimistic** — do not treat as an all-device SLA.
-### 机型档位(1080p,默认 pipeline)
+### 📱 Device Tier (1080p, Default Pipeline)
-相对上述开发环境 ~320ms 的量级:
+Relative to the ~320ms dev-environment baseline:
-| 档位 | 相对倍数 | 有预热 `interactive` | 冷启动 |
-| -------------------- | -------- | ----------------- | ---------- |
-| 旗舰 iOS / 新旗舰 Android | 0.8–1.2× | 300–450ms | 500–800ms |
-| 中端 Android | 1.5–2.5× | 500–800ms | 700ms–1.2s |
-| 低端 Android(4GB、老 U) | 2.5–4× | 800ms–1.3s | 1–2s+ |
+| Tier | Relative Multiplier | Pre-warm `interactive` | Cold Start |
+| ----------------------------------- | ------------------- | ---------------------- | ---------- |
+| Flagship iOS / new flagship Android | 0.8–1.2× | 300–450ms | 500–800ms |
+| Mid-range Android | 1.5–2.5× | 500–800ms | 700ms–1.2s |
+| Low-end Android (4GB, old SoC) | 2.5–4× | 800ms–1.3s | 1–2s+ |
-Android 额外开销主要来自:JS ↔ OpenCV bridge、内存带宽/GC、Skia 纹理上传。
+Android overhead primarily comes from: JS ↔ OpenCV bridge, memory bandwidth/GC, Skia texture upload.
-### 提高 `maxImageLongSide` 的影响
+### 📈 Impact of Raising maxImageLongSide
-若将 `pipelineConfig.maxImageLongSide` 设为 **1280**(高于默认 720),分割工作区约 **720×1280**,像素约为 720 档的 **3×**:
+Setting `pipelineConfig.maxImageLongSide` to **1280** (above the default 720) results in a segmentation working area of ~720×1280, roughly **3×** the pixel count of the 720 tier:
-| 场景 | 默认 720 | 改为 1280 |
-| ------------------------ | ---------- | ------------- |
-| 1080p `interactive`(中端机) | ~320–800ms | **500ms–1s+** |
-| 分割 / pickMap 耗时 | ~90–120ms | ~250–350ms |
+| Scenario | Default 720 | Raised to 1280 |
+| ------------------------------- | ----------- | -------------- |
+| 1080p `interactive` (mid-range) | ~320–800ms | **500ms–1s+** |
+| Segmentation / pickMap duration | ~90–120ms | ~250–350ms |
-更高精度换更长初始化;若目标仍是 **<500ms 可交互**,建议维持默认 **720**,必要时降至 **640**。
+Higher precision for longer init time. To stay **<500ms interactive**, keep the default **720**; reduce to **640** if needed.
-### 优化建议
+### 💨 Optimization Tips
-1. **PNG 预热(推荐)**:下载或解压完成后、进入画面前调用 `prewarmPngBgrCacheAsync`,通常可省 **100–250ms**(低端机收益最大)。
+1. 🚀 **PNG pre-warming (recommended)**: Call `prewarmPngBgrCacheAsync` after download/extraction and before navigating to the paint screen. Typically saves **100–250ms** (greatest benefit on low-end devices).
```tsx
import { prewarmPngBgrCacheAsync } from 'react-native-mask-segment-canvas';
await prewarmPngBgrCacheAsync([originPath, maskPath]);
-// 再挂载 MaskSegmentCanvas
+// Then mount MaskSegmentCanvas
```
-1. **Loading 时机**:阻塞式 Loading 在 `interactive` 关闭;「轮廓准备中」可选监听 `mask_paths_ready`。
-2. **大图 / 低端机**:保持默认 `maxImageLongSide: 720`;可再将 `paintFreqMaxLongSide` 降至 **360**。
-3. **4K 素材**:业务侧先下采样再传入,或接受 **0.8–1.5s** 量级的 `interactive`(预热后)。
-4. **观测**:开发环境关注 Metro 中 `[MaskSegment]`、`[⏱ ...]` 与 `onWatch` 的 `durationMs`。
+1. ⏱️ **Loading timing**: Dismiss the blocking loader at `interactive`; optionally listen for `mask_paths_ready` for "outlines preparing" hints.
+2. 🖼️ **Large images / low-end devices**: Keep default `maxImageLongSide: 720`; optionally lower `paintFreqMaxLongSide` to **360**.
+3. 📷 **4K assets**: Downsample on the host side before passing in, or accept ~0.8–1.5s `interactive` (with pre-warm).
+4. 🔍 **Observability**: Watch Metro logs for `[MaskSegment]`, `[⏱ ...]` prefixes and `onWatch` `durationMs`.
---
-## 注意事项
+## 📝 Notes
-- 掩码图应为与原图同尺寸的语义色块图(黑底 + 纯色分区);黑色区域(`blackThreshold` 默认 30 以下)不参与分区。
-- OpenCV 分割在 JS 线程执行,超大图可能卡顿;可通过 `pipelineConfig.maxImageLongSide` 限制处理尺寸。
-- iOS 相册选图需相册权限(仅 `showDebugPickers` 开启时用到)。
-- `semanticColors` 须与后端/标注掩码的语义色保持一致,否则识别会偏移。
+- The mask image should be a semantic color-block image with the same dimensions as the origin (black background + solid-color regions). Pixels with `max(B,G,R) < blackThreshold` (default 30) are excluded from segmentation.
+- OpenCV segmentation runs on the JS thread; very large images may cause frame drops. Use `pipelineConfig.maxImageLongSide` to cap processing resolution.
+- iOS photo library access requires photo permissions (only needed when `showDebugPickers` is enabled).
+- `semanticColors` must match the semantic colors used in the backend/labeled mask; mismatch will cause recognition drift.
---
-## 故障排查
+## 🔧 Troubleshooting
-**iOS pod install 失败**
+**iOS pod install fails**
```bash
cd ios
@@ -966,20 +966,20 @@ bundle install
bundle exec pod install --repo-update
```
-**Android 编译报错**
+**Android build errors**
```bash
cd android && ./gradlew clean && cd ..
```
-**分割失败 / 分区为空**
+**Segmentation fails / zero regions**
-- 确认 `originUrl` / `maskUrl` 可访问
-- 确认掩码语义色与 `semanticColors` 配置一致
-- 查看 Metro 日志中的 `[MaskSegment]` / `[⏱ ...]` 输出
+- Verify `originUrl` / `maskUrl` are accessible
+- Confirm mask semantic colors match the `semanticColors` config
+- Check Metro logs for `[MaskSegment]` / `[⏱ ...]` output
-**虚线不贴边 / 出现多余轮廓**
+**Dashed outlines misaligned / extra contours**
-- 虚线基于掩码像素外轮廓生成,长按仅显示触摸点所在连通块
-- 初始化轮播仅显示该语义分区最大连通块
+- Outlines are generated from mask pixel external contours; long-press only shows the connected component at the touch point
+- The initial carousel only shows the largest connected component for each semantic region
diff --git a/__tests__/wallTextureSplit.test.ts b/__tests__/wallTextureSplit.test.ts
index fe5366d..5b2f34e 100644
--- a/__tests__/wallTextureSplit.test.ts
+++ b/__tests__/wallTextureSplit.test.ts
@@ -128,7 +128,7 @@ test('splitWalls:true splits wall into wall-1 and wall-2 by chroma', () => {
const cols = 24;
const rows = 12;
const base = buildSyntheticWallResult(cols, rows);
- // 左蓝右暖色(色度差异明显)
+ // Left blue, right warm (obvious chroma difference)
const origin = buildSplitOrigin(cols, rows, [220, 50, 30], [50, 200, 240]);
setMaskSegmentRuntimeConfig({
@@ -163,7 +163,7 @@ test('same-color wall with lighting gradient stays one region', () => {
for (let x = 0; x < cols; x++) {
const t = x / Math.max(1, cols - 1);
const i = (y * cols + x) * 3;
- // 同一蓝色墙面,从左暗阴影到右亮部
+ // Same blue wall, from dark shadow on left to bright area on right
origin[i] = Math.round(160 + t * 70);
origin[i + 1] = Math.round(70 + t * 50);
origin[i + 2] = Math.round(40 + t * 30);
diff --git a/dist/components/MaskSegmentCanvas.d.ts b/dist/components/MaskSegmentCanvas.d.ts
index 940726f..580db39 100644
--- a/dist/components/MaskSegmentCanvas.d.ts
+++ b/dist/components/MaskSegmentCanvas.d.ts
@@ -3,4 +3,3 @@ import type { MaskSegmentCanvasProps, MaskSegmentCanvasRef } from './MaskSegment
export type { MaskSegmentCanvasProps, MaskSegmentCanvasRef, MaskSegmentSession, MaskSegmentWatchState, PaintedRegionRecord, BgrColor, MaskSemanticColor, } from './MaskSegmentCanvas.types';
declare const MaskSegmentCanvas: React.ForwardRefExoticComponent>;
export default MaskSegmentCanvas;
-//# sourceMappingURL=MaskSegmentCanvas.d.ts.map
\ No newline at end of file
diff --git a/dist/components/MaskSegmentCanvas.d.ts.map b/dist/components/MaskSegmentCanvas.d.ts.map
deleted file mode 100644
index 5a69a5d..0000000
--- a/dist/components/MaskSegmentCanvas.d.ts.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"MaskSegmentCanvas.d.ts","sourceRoot":"","sources":["../../src/components/MaskSegmentCanvas.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AA6Cf,OAAO,KAAK,EAEV,sBAAsB,EACtB,oBAAoB,EAMrB,MAAM,2BAA2B,CAAC;AAcnC,YAAY,EACV,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,QAAQ,EACR,iBAAiB,GAClB,MAAM,2BAA2B,CAAC;AA+BnC,QAAA,MAAM,iBAAiB,qGAo/DrB,CAAC;AAmBH,eAAe,iBAAiB,CAAC"}
\ No newline at end of file
diff --git a/dist/components/MaskSegmentCanvas.js b/dist/components/MaskSegmentCanvas.js
index 623f47b..ddba574 100644
--- a/dist/components/MaskSegmentCanvas.js
+++ b/dist/components/MaskSegmentCanvas.js
@@ -1,1588 +1 @@
-import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
-import { useRef, useState, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle, } from 'react';
-import { View, StyleSheet, } from 'react-native';
-import { Gesture, GestureDetector } from 'react-native-gesture-handler';
-import { runOnJS } from 'react-native-reanimated';
-import cv from '../utils/opencvAdapter';
-import { buildAllRegionOutlinePaths, buildRegionOutlinePathForRegion, downsampleMaskDataForPaths, extractRegionsFromMaskBufferSync, upscaleBinaryMask, } from '../utils/maskSegmentation';
-import { splitWallRegionsByTexture } from '../utils/wallTextureSplit';
-import { clearDerivedImageCache, readPngBgrBuffer, prewarmPngBgrCache, resizeBgrBuffer, } from '../utils/pngImage';
-import { hashUrl, resolveImageUrl } from '../utils/resolveImageUrl';
-import { compositePaintedImage } from '../utils/compositePaintedImage';
-import { paintedRegionsFingerprint, resolveExportResultForDestDir, } from '../utils/exportUtils';
-import { preparePaintResourcesFromWorkBuffer, releaseFreqLayerImages, } from '../utils/freqLayerPrep';
-import { PaintShaderLayer, createPaintColorMapForPaint, } from '../utils/paintShaderRuntime';
-import { createRuntimeConfig, getMaskRuntimeRevision, getMaskSegmentRuntimeConfig, resolvePipelineConfig, setMaskSegmentRuntimeConfig, } from '../utils/maskSegmentRuntime';
-import { Canvas, Image as SkiaImage, Path, Group, DashPathEffect, Rect, useCanvasRef, Skia, } from '@shopify/react-native-skia';
-import { bgrColorEquals, rectsEqual, getContainRect, canvasToNormalized, buildZoomPanMatrix, clampPanOffset, screenToCanvasCoords, resolveRegionHit, pickKickRegionFromMask, pickKickNearStrip, lookupRegionFromPickMap, releasePaintResourceLayers, releaseOriginSkImage, prepareWorkScaledBgrBuffer, timeLog, } from '../utils/canvasGeometry';
-/* ==========================================================================
- * 组件主体
- * ========================================================================== */
-const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
- const { originUrl: originUrlProp, maskUrl: maskUrlProp, originImgPath: originImgPathLegacy, maskImgPath: maskImgPathLegacy, maskConfig, pipelinePreset, pipelineConfig, paintConfig, interactionConfig, semanticColors, regionOutlineColor, initialSession, initialPaintColor, initialPaintConfigJson, disabled = false, style, onWatch, onPaintCallback, onError, autoExportOnReady, onExported, } = props;
- const originSource = originUrlProp ?? originImgPathLegacy ?? '';
- const maskSource = maskUrlProp ?? maskImgPathLegacy ?? '';
- const resolvedMaskConfig = useMemo(() => semanticColors
- ? { ...maskConfig, semanticColors }
- : maskConfig, [maskConfig, semanticColors]);
- const resolvedPaintConfig = useMemo(() => regionOutlineColor
- ? { ...paintConfig, regionOverlayFill: regionOutlineColor }
- : paintConfig, [paintConfig, regionOutlineColor]);
- const [resolvedOriginPath, setResolvedOriginPath] = useState('');
- const [resolvedMaskPath, setResolvedMaskPath] = useState('');
- const [originImgPath, setOriginImgPath] = useState(resolvedOriginPath);
- const [maskImgPath, setMaskImgPath] = useState(resolvedMaskPath);
- // Latest desired image paths (updated when the internal path states settle).
- // Used by segmentAndPrepareLayers to decide whether a stale runId (from effect cleanup due to
- // unrelated parent re-renders) should actually abort the current async read/segment work.
- // If the image pair we are processing is still the one the caller ultimately wants, we continue.
- const latestOriginPathRef = useRef('');
- const latestMaskPathRef = useRef('');
- const resolvedPipelineConfig = useMemo(() => resolvePipelineConfig(pipelinePreset, pipelineConfig), [pipelinePreset, pipelineConfig]);
- const runtimeRef = useRef(createRuntimeConfig({
- maskConfig: resolvedMaskConfig,
- pipelineConfig: resolvedPipelineConfig,
- paintConfig: resolvedPaintConfig,
- interactionConfig,
- }));
- // Track last *values* we pushed for paintConfig. We only call the global setMaskSegmentRuntimeConfig
- // (which bumps runtimeRevision) when the actual numbers change. This prevents repeated parent
- // re-renders that pass a new object literal with identical values from causing:
- // - global revision bump
- // - paintColorMap useMemo invalidation (full-res Uint8Array + boxBlur + Skia image alloc)
- // - extra main-thread work during the critical segmentation / freq-layers / outline paths window.
- // The local runtimeRef is still kept in sync for any synchronous readers.
- const lastAppliedPaintConfigRef = useRef(null);
- const lastAppliedPipelineSignatureRef = useRef(null);
- useEffect(() => {
- const prevPaint = lastAppliedPaintConfigRef.current;
- const currPaint = resolvedPaintConfig || {};
- const paintKeys = [
- 'colorBaseOpacity',
- 'lLightOpacity',
- 'textureOpacity',
- 'lLowBlurKernel',
- 'lLowContrast',
- 'lLowBrightness',
- 'lHighGain',
- 'maskFeatherColor',
- 'maskFeatherTexture',
- 'regionOverlayFill',
- ];
- const paintChanged = !prevPaint || paintKeys.some((k) => currPaint[k] !== prevPaint[k]);
- const pipelineSignature = JSON.stringify(resolvedPipelineConfig);
- const pipelineChanged = lastAppliedPipelineSignatureRef.current !== pipelineSignature;
- if (paintChanged || pipelineChanged) {
- if (paintChanged) {
- lastAppliedPaintConfigRef.current = { ...currPaint };
- }
- if (pipelineChanged) {
- lastAppliedPipelineSignatureRef.current = pipelineSignature;
- }
- runtimeRef.current = setMaskSegmentRuntimeConfig({
- maskConfig: resolvedMaskConfig,
- pipelineConfig: resolvedPipelineConfig,
- paintConfig: resolvedPaintConfig,
- interactionConfig,
- });
- }
- }, [
- resolvedMaskConfig,
- resolvedPipelineConfig,
- resolvedPaintConfig,
- interactionConfig,
- ]);
- const paintPalette = runtimeRef.current.paint.palette;
- const paintRuntime = getMaskSegmentRuntimeConfig().paint;
- const interactionRuntime = getMaskSegmentRuntimeConfig().interaction;
- const onWatchRef = useRef(onWatch);
- const onPaintCallbackRef = useRef(onPaintCallback);
- const onErrorRef = useRef(onError);
- const onExportedRef = useRef(onExported);
- useEffect(() => {
- onWatchRef.current = onWatch;
- onPaintCallbackRef.current = onPaintCallback;
- onErrorRef.current = onError;
- onExportedRef.current = onExported;
- }, [onWatch, onPaintCallback, onError, onExported]);
- const watchStartRef = useRef(0);
- const lastWatchStateRef = useRef(null);
- const lastWatchSignatureRef = useRef(null);
- const emitWatch = useCallback((state, detail) => {
- const signature = [
- state,
- detail?.regionCount ?? '',
- detail?.maskPathsReady ?? '',
- detail?.freqLayersReady ?? '',
- detail?.errorMessage ?? '',
- ].join('|');
- if (lastWatchSignatureRef.current === signature) {
- return;
- }
- lastWatchSignatureRef.current = signature;
- lastWatchStateRef.current = state;
- const durationMs = watchStartRef.current
- ? performance.now() - watchStartRef.current
- : 0;
- onWatchRef.current?.(state, durationMs, detail);
- }, []);
- const reportError = useCallback((message, error) => {
- emitWatch('error', { errorMessage: message });
- if (onErrorRef.current) {
- onErrorRef.current(message, error);
- }
- else if (__DEV__) {
- console.error('[MaskSegment]', message, error);
- }
- }, [emitWatch]);
- const [customPaintColor, setCustomPaintColor] = useState(initialPaintColor ?? null);
- const customPaintConfigJsonRef = useRef(initialPaintConfigJson);
- const originUrlRef = useRef(originSource);
- const maskUrlRef = useRef(maskSource);
- useEffect(() => {
- originUrlRef.current = originSource;
- maskUrlRef.current = maskSource;
- }, [originSource, maskSource]);
- useEffect(() => {
- let cancelled = false;
- if (!originSource || !maskSource) {
- setResolvedOriginPath('');
- setResolvedMaskPath('');
- return;
- }
- void (async () => {
- try {
- const [originPath, maskPath] = await Promise.all([
- resolveImageUrl(originSource, `origin_${hashUrl(originSource)}.png`),
- resolveImageUrl(maskSource, `mask_${hashUrl(maskSource)}.png`),
- ]);
- if (!cancelled) {
- setResolvedOriginPath(originPath);
- setResolvedMaskPath(maskPath);
- }
- }
- catch (e) {
- if (!cancelled) {
- const msg = e instanceof Error ? e.message : String(e);
- reportError(msg, e);
- }
- }
- })();
- return () => {
- cancelled = true;
- };
- }, [originSource, maskSource, reportError]);
- useEffect(() => {
- setOriginImgPath(resolvedOriginPath);
- setMaskImgPath(resolvedMaskPath);
- if (resolvedOriginPath && resolvedMaskPath) {
- prewarmPngBgrCache([resolvedOriginPath, resolvedMaskPath]);
- }
- }, [resolvedOriginPath, resolvedMaskPath]);
- // Keep latest desired paths for cancellation decisions inside the long async segment pipeline.
- useEffect(() => {
- latestOriginPathRef.current = originImgPath || '';
- latestMaskPathRef.current = maskImgPath || '';
- }, [originImgPath, maskImgPath]);
- const [paintResourceLayers, setPaintResourceLayers] = useState(null);
- const paintResourceLayersRef = useRef(null);
- const paintColorMapSkImgRef = useRef(null);
- const [activeBrushIndex, setActiveBrushIndex] = useState(null);
- const [paintedRegions, setPaintedRegions] = useState(() => new Map());
- const paintedRegionsRef = useRef(new Map());
- const [paintHistory, setPaintHistory] = useState([]);
- // Seed the ref with the initial empty map so early reads (before any paint effect)
- // are consistent.
- paintedRegionsRef.current = paintedRegions;
- const [heldRegionId, setHeldRegionId] = useState(null);
- const [heldRegionAnchor, setHeldRegionAnchor] = useState(null);
- const [initFlashRegionId, setInitFlashRegionId] = useState(null);
- const initFlashTimerRef = useRef(null);
- const initFlashIndexRef = useRef(0);
- const initFlashActiveRef = useRef(false);
- // List of regions still eligible for the init dashed-outline flash (discovery aid).
- // Computed at the start of the flash loop, excluding any that are already painted.
- // This ensures that on continue-edit (or any partial seed), already-colored regions
- // do not get the flashing dashed outline.
- const initFlashListRef = useRef([]);
- // Guard so that initialSession (seed from host bootstrap or scheme) is applied only once
- // after segmentsReady. Prevents later prop identity changes (e.g. caused by host slot/brush
- // selection re-renders) from calling restoreSession again, which would clobber live
- // paintedRegions with a re-derived snapshot (often causing already-painted regions to
- // "follow" the newly selected brush color).
- const hasAppliedInitialSessionRef = useRef(false);
- // Keep a ref to the absolute latest paintedRegions so that imperative save()
- // (called from host performSaveProject for new schemes, or manually) and
- // internal composites always see the most up-to-date painted state, even
- // if the useImperativeHandle closure was created in a prior render.
- // This fixes cases where the last user paint's colors were missing from
- // the recolored After image captured at "save scheme" time.
- useEffect(() => {
- paintedRegionsRef.current = paintedRegions;
- }, [paintedRegions]);
- // Cached export from the most recent auto-export or save() — keyed by painted fingerprint.
- const lastExportCacheRef = useRef(null);
- const autoExportDebounceRef = useRef(null);
- const exportInFlightRef = useRef(false);
- const regionsRef = useRef([]);
- const maskPickRef = useRef(null);
- const regionPickRef = useRef(null);
- const regionMaskDataRef = useRef(null);
- const workBufferRef = useRef(null);
- const paintLayersPromiseRef = useRef(null);
- const loadPaintLayersRef = useRef(() => Promise.resolve());
- const [paintResourcesReady, setPaintResourcesReady] = useState(false);
- const [maskPathsReady, setMaskPathsReady] = useState(false);
- const baseboardPickMaskRef = useRef(null);
- const kickRegionIdRef = useRef(null);
- const [regionPalette, setRegionPalette] = useState([]);
- const [regionCount, setRegionCount] = useState(0);
- const [imageSize, setImageSize] = useState(null);
- // High-resolution offscreen Canvas (sized to the work buffer resolution) whose content
- // is the full shader composition (PaintShaderLayer at 0,0,workW,workH). On save() we
- // call makeImageSnapshot() on it to get a "what you see in the editor, at source res"
- // PNG bytes. This is the preferred "保存快照" path and avoids CPU recolor entirely
- // for the exported After.
- const highResExportCanvasRef = useCanvasRef();
- const [exportCanvasSize, setExportCanvasSize] = useState(null);
- // Gate the (potentially expensive) high-res snapshot canvas so it is only mounted
- // after the user (or initialSession seed) has painted at least one region. This keeps
- // idle segmentation / no-paint cases cheap.
- const [highResSnapshotEnabled, setHighResSnapshotEnabled] = useState(false);
- // Viewport measured from canvasWrap onLayout — single source of truth for Skia,
- // gestures, containRect, and pan clamp (must match the actual touch target).
- const [viewportSize, setViewportSize] = useState(null);
- const [segmentsReady, setSegmentsReady] = useState(false);
- const segmentsReadyRef = useRef(false);
- const maskPathsReadyRef = useRef(false);
- const [canvasInteractive, setCanvasInteractive] = useState(false);
- const [compareMode, setCompareMode] = useState(false);
- const resegmentInFlightRef = useRef(false);
- const [originSkImg, setOriginSkImg] = useState(null);
- const originSkImgRef = useRef(null);
- const lowFreqSkImg = paintResourceLayers?.lowFreqImage ?? null;
- const highFreqSkImg = paintResourceLayers?.highFreqImage ?? null;
- const canvasW = viewportSize?.w ?? 1;
- const canvasH = viewportSize?.h ?? 1;
- const canvasLayoutReady = viewportSize != null && viewportSize.w > 0 && viewportSize.h > 0;
- // Refs synced to the latest viewport size so async callbacks read post-layout values.
- const canvasWRef = useRef(canvasW);
- const canvasHRef = useRef(canvasH);
- // ── Pinch-zoom (focal-point) + single-finger pan when zoomed ─────────────
- const [zoomScale, setZoomScale] = useState(1);
- const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
- // Refs for gesture callbacks (closures don't capture fresh state mid-gesture)
- const zoomScaleRef = useRef(1);
- const panOffsetRef = useRef({ x: 0, y: 0 });
- const pinchBaseScaleRef = useRef(1);
- const pinchBasePanRef = useRef({ x: 0, y: 0 });
- const pinchBaseFocalRef = useRef({ x: 0, y: 0 });
- const panBaseRef = useRef({ x: 0, y: 0 });
- // Ref to the latest containRect (the actual placed photo rect inside the viewport).
- const containRectRef = useRef(null);
- useEffect(() => { zoomScaleRef.current = zoomScale; }, [zoomScale]);
- useEffect(() => { panOffsetRef.current = panOffset; }, [panOffset]);
- const handleCanvasWrapLayout = useCallback((width, height) => {
- if (width <= 0 || height <= 0) {
- return;
- }
- canvasWRef.current = width;
- canvasHRef.current = height;
- setViewportSize(prev => {
- if (prev?.w === width && prev?.h === height) {
- return prev;
- }
- return { w: width, h: height };
- });
- }, []);
- const resetZoom = useCallback(() => {
- setZoomScale(1);
- setPanOffset({ x: 0, y: 0 });
- zoomScaleRef.current = 1;
- panOffsetRef.current = { x: 0, y: 0 };
- }, []);
- const containRect = useMemo(() => {
- if (!imageSize)
- return null;
- return getContainRect(canvasW, canvasH, imageSize.w, imageSize.h);
- }, [imageSize, canvasW, canvasH]);
- // Keep a ref in sync so early-defined callbacks can read the
- // latest containRect without TDZ or stale closure issues.
- useEffect(() => {
- containRectRef.current = containRect;
- }, [containRect]);
- const segmentRunIdRef = useRef(0);
- const lastSegmentKeyRef = useRef('');
- const segmentInFlightKeyRef = useRef('');
- const maskPathsContainRectRef = useRef(null);
- useEffect(() => {
- paintResourceLayersRef.current = paintResourceLayers;
- }, [paintResourceLayers]);
- useEffect(() => {
- segmentsReadyRef.current = segmentsReady;
- }, [segmentsReady]);
- useEffect(() => {
- maskPathsReadyRef.current = maskPathsReady;
- }, [maskPathsReady]);
- const emitLayersReadyIfReady = useCallback(() => {
- if (!segmentsReadyRef.current || !paintResourceLayersRef.current) {
- return;
- }
- emitWatch('layers_ready', {
- regionCount: regionsRef.current.length,
- maskPathsReady: maskPathsReadyRef.current,
- freqLayersReady: true,
- });
- }, [emitWatch]);
- const emitMaskPathsReadyIfReady = useCallback(() => {
- if (!segmentsReadyRef.current || !maskPathsReadyRef.current) {
- return;
- }
- emitWatch('mask_paths_ready', {
- regionCount: regionsRef.current.length,
- maskPathsReady: true,
- freqLayersReady: true,
- });
- }, [emitWatch]);
- const emitInteractiveIfReady = useCallback(() => {
- if (!segmentsReadyRef.current || !paintResourceLayersRef.current) {
- return;
- }
- emitLayersReadyIfReady();
- emitWatch('interactive', {
- regionCount: regionsRef.current.length,
- maskPathsReady: maskPathsReadyRef.current,
- freqLayersReady: true,
- });
- setCanvasInteractive(true);
- }, [emitWatch, emitLayersReadyIfReady]);
- const paintColorMapSkImg = useMemo(() => {
- const pick = regionPickRef.current;
- paintColorMapSkImgRef.current?.dispose();
- // Early out: no pick buffer yet, or no regions have been painted (initial load or fresh session).
- // Avoids repeated full-resolution RGBA allocation + boxBlur (for maskFeather) + Skia.Image.MakeImage
- // on every re-render during the hot init path. When initialSession restores paints, paintedRegions
- // will update and legitimately trigger a single build of the (feathered) color map.
- if (!pick || paintedRegions.size === 0) {
- paintColorMapSkImgRef.current = null;
- return null;
- }
- const map = createPaintColorMapForPaint(pick.buffer, pick.cols, pick.rows, paintedRegions);
- paintColorMapSkImgRef.current = map;
- return map;
- }, [paintedRegions, paintResourcesReady, segmentsReady, getMaskRuntimeRevision()]);
- const paintedRegionConfigRef = useRef(new Map());
- const segmentAndPrepareLayers = useCallback(async (originPath, maskPath) => {
- const runId = ++segmentRunIdRef.current;
- // isCancelled: a runId bump alone is not enough to abort if the image pair we were asked to process
- // is still the latest desired pair from the caller (protects against effect cleanups caused by
- // unrelated parent re-renders / state updates that do not change the two images).
- const isCancelled = () => {
- if (runId === segmentRunIdRef.current)
- return false;
- const desiredOrigin = latestOriginPathRef.current || originPath;
- const desiredMask = latestMaskPathRef.current || maskPath;
- const stillWanted = desiredOrigin === originPath && desiredMask === maskPath;
- return !stillWanted;
- };
- const pipeline = getMaskSegmentRuntimeConfig().pipeline;
- watchStartRef.current = performance.now();
- lastWatchStateRef.current = null;
- lastWatchSignatureRef.current = null;
- emitWatch('init');
- timeLog('▶ start segmentation');
- segmentsReadyRef.current = false;
- setSegmentsReady(false);
- setActiveBrushIndex(null);
- const clearedPainted = new Map();
- paintedRegionsRef.current = clearedPainted;
- setPaintedRegions(clearedPainted);
- setPaintHistory([]);
- setHeldRegionId(null);
- setHeldRegionAnchor(null);
- setInitFlashRegionId(null);
- initFlashActiveRef.current = false;
- initFlashIndexRef.current = 0;
- initFlashListRef.current = [];
- if (initFlashTimerRef.current) {
- clearTimeout(initFlashTimerRef.current);
- initFlashTimerRef.current = null;
- }
- hasAppliedInitialSessionRef.current = false;
- lastExportCacheRef.current = null;
- if (autoExportDebounceRef.current) {
- clearTimeout(autoExportDebounceRef.current);
- autoExportDebounceRef.current = null;
- }
- exportInFlightRef.current = false;
- regionsRef.current = [];
- maskPickRef.current = null;
- regionPickRef.current = null;
- regionMaskDataRef.current = null;
- workBufferRef.current = null;
- paintLayersPromiseRef.current = null;
- setRegionOutlinePaths(new Map());
- setMaskPathsReady(false);
- setPaintResourcesReady(false);
- baseboardPickMaskRef.current = null;
- kickRegionIdRef.current = null;
- maskPathsContainRectRef.current = null;
- setRegionPalette([]);
- setRegionCount(0);
- // Reset high-res snapshot state so a fresh segmentation starts clean (no stale size/ref).
- setExportCanvasSize(null);
- setHighResSnapshotEnabled(false);
- const prevLayers = paintResourceLayersRef.current;
- if (prevLayers) {
- releasePaintResourceLayers(prevLayers);
- paintResourceLayersRef.current = null;
- setPaintResourceLayers(null);
- }
- releaseOriginSkImage(originSkImgRef.current);
- originSkImgRef.current = null;
- setOriginSkImg(null);
- try {
- timeLog('▶ reading origin PNG');
- const originPromise = readPngBgrBuffer(originPath);
- timeLog('▶ reading mask PNG');
- const maskPromise = readPngBgrBuffer(maskPath);
- const originDecoded = await originPromise;
- const afterOriginCancelled = isCancelled();
- if (afterOriginCancelled) {
- return;
- }
- const imgW = originDecoded.cols;
- const imgH = originDecoded.rows;
- setImageSize({ w: imgW, h: imgH });
- let scale = 1;
- if (Math.max(imgW, imgH) > pipeline.maxImageLongSide) {
- scale = pipeline.maxImageLongSide / Math.max(imgW, imgH);
- }
- const segW = Math.floor(imgW * scale);
- const segH = Math.floor(imgH * scale);
- const minArea = pipeline.minContourArea * scale * scale;
- const workScaledTask = prepareWorkScaledBgrBuffer(originDecoded.buffer, imgW, imgH, scale).then(workScaled => {
- if (isCancelled()) {
- return workScaled;
- }
- workBufferRef.current = workScaled;
- setExportCanvasSize({ w: workScaled.cols, h: workScaled.rows });
- // Enable the offscreen high-res export canvas as soon as we know the work resolution.
- // This gives the hidden