react-native-mask-segment-c.../App.tsx
a1518 84b45ef24d refactor: simplify MaskSegmentCanvas core logic and reduce bundle size
Streamline the main canvas component, type definitions, and paint
shader runtime. Removes unused code paths and redundant logic across
src/ and example/, resulting in a net reduction of ~460 lines.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 23:56:13 -07:00

221 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Mask Segment Demo App
* 基于 OpenCV + Skia 的掩码分区交互画布
*
* 注意:这是库开发自测用的 Demo直接引用 ./src。
* 业务项目集成演示请参考 example/ 目录(使用公开包名导入)。
*/
import React, { useEffect, useRef, useState } from 'react';
import {
ActivityIndicator,
StatusBar,
StyleSheet,
Text,
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';
const TEST_ORIGIN = require('./assets/test/origin.png');
const TEST_MASK = require('./assets/test/mask.png');
const INTERACTIVE_WATCH_STATES: MaskSegmentWatchState[] = [
'interactive',
'mask_paths_ready',
];
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 canvasRef = useRef<MaskSegmentCanvasRef>(null);
const [testPaths, setTestPaths] = useState<{
origin: string;
mask: string;
} | null>(null);
const [loadError, setLoadError] = useState('');
const [watchState, setWatchState] = useState<MaskSegmentWatchState | ''>('');
const [sessionDraft, setSessionDraft] = useState<MaskSegmentSession | null>(
null,
);
const isFullyReady = watchState === 'mask_paths_ready';
const isInitLoading =
testPaths != null &&
watchState !== '' &&
!INTERACTIVE_WATCH_STATES.includes(watchState as MaskSegmentWatchState) &&
watchState !== 'error';
useEffect(() => {
let cancelled = false;
(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 (
<SafeAreaProvider>
<SafeAreaView style={styles.root}>
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
{loadError ? (
<View style={styles.center}>
<Text style={styles.errorText}>{loadError}</Text>
</View>
) : testPaths ? (
<>
{watchState ? (
<Text style={styles.watchText}>
: {formatWatchStatus(watchState)}
{isFullyReady ? ' · 轮播虚线已就绪' : null}
</Text>
) : null}
<View style={styles.canvasHost}>
<MaskSegmentCanvas
ref={canvasRef}
originUrl={testPaths.origin}
maskUrl={testPaths.mask}
semanticColors={MASK_SEMANTIC_COLORS}
regionOutlineColor="rgba(20, 120, 235, 0.58)"
initialSession={sessionDraft ?? undefined}
onWatch={(state, durationMs, detail) => {
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 ? (
<View style={styles.initOverlay} pointerEvents="none">
<ActivityIndicator size="small" color="#333" />
<Text style={styles.initOverlayText}>
{formatWatchStatus(watchState)}
</Text>
</View>
) : null}
</View>
{sessionDraft ? (
<Text style={styles.sessionText}>
MMKV 稿{sessionDraft.painted.length}
</Text>
) : null}
</>
) : (
<View style={styles.center}>
<ActivityIndicator size="large" color="#333" />
<Text style={styles.loadingText}></Text>
</View>
)}
</SafeAreaView>
</SafeAreaProvider>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: '#fff',
},
center: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 24,
},
loadingText: {
marginTop: 12,
color: '#666',
fontSize: 14,
},
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,
},
});
export default App;