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>
This commit is contained in:
parent
bf6b6cf502
commit
84b45ef24d
1
App.tsx
1
App.tsx
@ -114,7 +114,6 @@ function App(): React.JSX.Element {
|
|||||||
maskUrl={testPaths.mask}
|
maskUrl={testPaths.mask}
|
||||||
semanticColors={MASK_SEMANTIC_COLORS}
|
semanticColors={MASK_SEMANTIC_COLORS}
|
||||||
regionOutlineColor="rgba(20, 120, 235, 0.58)"
|
regionOutlineColor="rgba(20, 120, 235, 0.58)"
|
||||||
showDebugPickers
|
|
||||||
initialSession={sessionDraft ?? undefined}
|
initialSession={sessionDraft ?? undefined}
|
||||||
onWatch={(state, durationMs, detail) => {
|
onWatch={(state, durationMs, detail) => {
|
||||||
setWatchState(state);
|
setWatchState(state);
|
||||||
|
|||||||
2
dist/components/MaskSegmentCanvas.d.ts.map
vendored
2
dist/components/MaskSegmentCanvas.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"MaskSegmentCanvas.d.ts","sourceRoot":"","sources":["../../src/components/MaskSegmentCanvas.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAqDf,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;AA4ZnC,QAAA,MAAM,iBAAiB,qGA2qErB,CAAC;AA8HH,eAAe,iBAAiB,CAAC"}
|
{"version":3,"file":"MaskSegmentCanvas.d.ts","sourceRoot":"","sources":["../../src/components/MaskSegmentCanvas.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AA8Cf,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;AA0cnC,QAAA,MAAM,iBAAiB,qGAo/DrB,CAAC;AAmBH,eAAe,iBAAiB,CAAC"}
|
||||||
476
dist/components/MaskSegmentCanvas.js
vendored
476
dist/components/MaskSegmentCanvas.js
vendored
@ -1,9 +1,8 @@
|
|||||||
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
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 { useRef, useState, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle, } from 'react';
|
||||||
import { View, StyleSheet, Button, Dimensions, Text, TouchableOpacity, ScrollView, } from 'react-native';
|
import { View, StyleSheet, } from 'react-native';
|
||||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||||
import { runOnJS } from 'react-native-reanimated';
|
import { runOnJS } from 'react-native-reanimated';
|
||||||
import { launchImageLibrary } from 'react-native-image-picker';
|
|
||||||
import cv from '../utils/opencvAdapter';
|
import cv from '../utils/opencvAdapter';
|
||||||
import { buildAllRegionOutlinePaths, buildRegionOutlinePathForRegion, downsampleMaskDataForPaths, extractRegionsFromMaskBufferSync, isBaseboardMaskPixel, upscaleBinaryMask, } from '../utils/maskSegmentation';
|
import { buildAllRegionOutlinePaths, buildRegionOutlinePathForRegion, downsampleMaskDataForPaths, extractRegionsFromMaskBufferSync, isBaseboardMaskPixel, upscaleBinaryMask, } from '../utils/maskSegmentation';
|
||||||
import { splitWallRegionsByTexture } from '../utils/wallTextureSplit';
|
import { splitWallRegionsByTexture } from '../utils/wallTextureSplit';
|
||||||
@ -15,10 +14,6 @@ import { preparePaintResourcesFromWorkBuffer, releaseFreqLayerImages, } from '..
|
|||||||
import { PaintShaderLayer, createPaintColorMapForPaint, } from '../utils/paintShaderRuntime';
|
import { PaintShaderLayer, createPaintColorMapForPaint, } from '../utils/paintShaderRuntime';
|
||||||
import { createRuntimeConfig, getMaskRuntimeRevision, getMaskSegmentRuntimeConfig, resolvePipelineConfig, setMaskSegmentRuntimeConfig, } from '../utils/maskSegmentRuntime';
|
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 { Canvas, Image as SkiaImage, Path, Group, DashPathEffect, Rect, useCanvasRef, Skia, } from '@shopify/react-native-skia';
|
||||||
/* ==========================================================================
|
|
||||||
* 配置常量(屏幕相关;其余见 maskSegmentRuntime)
|
|
||||||
* ========================================================================== */
|
|
||||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
|
||||||
function bgrColorEquals(a, b) {
|
function bgrColorEquals(a, b) {
|
||||||
return a.b === b.b && a.g === b.g && a.r === b.r;
|
return a.b === b.b && a.g === b.g && a.r === b.r;
|
||||||
}
|
}
|
||||||
@ -53,6 +48,45 @@ function canvasToNormalized(cx, cy, canvasW, canvasH, imgW, imgH) {
|
|||||||
y: (cy - rect.y) / rect.h,
|
y: (cy - rect.y) / rect.h,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/** Skia matrix: pan → scale around viewport center. Matches screenToCanvasCoords. */
|
||||||
|
function buildZoomPanMatrix(panX, panY, scale, canvasW, canvasH) {
|
||||||
|
const cx = canvasW / 2;
|
||||||
|
const cy = canvasH / 2;
|
||||||
|
const m = Skia.Matrix();
|
||||||
|
m.translate(panX, panY);
|
||||||
|
m.translate(cx, cy);
|
||||||
|
m.scale(scale, scale);
|
||||||
|
m.translate(-cx, -cy);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
/** Clamp pan so scaled containRect does not expose empty margins beyond the viewport. */
|
||||||
|
function clampPanOffset(pan, scale, canvasW, canvasH, containRect) {
|
||||||
|
if (!containRect || scale <= 1 || canvasW <= 0 || canvasH <= 0) {
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
const cx = canvasW / 2;
|
||||||
|
const cy = canvasH / 2;
|
||||||
|
const r = containRect;
|
||||||
|
const scaledMinX = cx + scale * (r.x - cx);
|
||||||
|
const scaledMaxX = cx + scale * (r.x + r.w - cx);
|
||||||
|
const scaledMinY = cy + scale * (r.y - cy);
|
||||||
|
const scaledMaxY = cy + scale * (r.y + r.h - cy);
|
||||||
|
let x = pan.x;
|
||||||
|
let y = pan.y;
|
||||||
|
if (scaledMaxX - scaledMinX > canvasW) {
|
||||||
|
x = Math.max(canvasW - scaledMaxX, Math.min(-scaledMinX, x));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
if (scaledMaxY - scaledMinY > canvasH) {
|
||||||
|
y = Math.max(canvasH - scaledMaxY, Math.min(-scaledMinY, y));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
y = 0;
|
||||||
|
}
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Inverse of the Skia Group transform applied during pinch-zoom.
|
* Inverse of the Skia Group transform applied during pinch-zoom.
|
||||||
* Converts a raw touch point (screen pixels) back to the canvas coordinate
|
* Converts a raw touch point (screen pixels) back to the canvas coordinate
|
||||||
@ -62,7 +96,6 @@ function canvasToNormalized(cx, cy, canvasW, canvasH, imgW, imgH) {
|
|||||||
function screenToCanvasCoords(screenX, screenY, canvasW, canvasH, zoomScale, panOffset) {
|
function screenToCanvasCoords(screenX, screenY, canvasW, canvasH, zoomScale, panOffset) {
|
||||||
if (zoomScale <= 1)
|
if (zoomScale <= 1)
|
||||||
return { x: screenX, y: screenY };
|
return { x: screenX, y: screenY };
|
||||||
// Reverse: translate(-pan) → unscale around center → translate(+center)
|
|
||||||
return {
|
return {
|
||||||
x: (screenX - panOffset.x - canvasW / 2) / zoomScale + canvasW / 2,
|
x: (screenX - panOffset.x - canvasW / 2) / zoomScale + canvasW / 2,
|
||||||
y: (screenY - panOffset.y - canvasH / 2) / zoomScale + canvasH / 2,
|
y: (screenY - panOffset.y - canvasH / 2) / zoomScale + canvasH / 2,
|
||||||
@ -234,10 +267,6 @@ function lookupRegionFromPickMap(normX, normY, pick, radiusPx = getMaskSegmentRu
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
/** BGR → 屏幕 RGB */
|
|
||||||
function bgrToCss(b, g, r) {
|
|
||||||
return `rgb(${r},${g},${b})`;
|
|
||||||
}
|
|
||||||
function releasePaintResourceLayers(layers) {
|
function releasePaintResourceLayers(layers) {
|
||||||
if (!layers) {
|
if (!layers) {
|
||||||
return;
|
return;
|
||||||
@ -275,7 +304,7 @@ function timeLog(tag) {
|
|||||||
* 组件主体
|
* 组件主体
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
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, showDebugPickers = true, showToolbar = true, showColorBar = true, showStatusRow = true, showOverlayButtons = true, disabled = false, style, canvasStyle, maxHeight, undoButtonStyle, compareButtonStyle, undoButtonTextStyle, compareButtonTextStyle, undoButtonText = '撤销', compareButtonText = '对比原图', compareExitButtonText = '退出对比', renderUndoButton, renderCompareButton, onWatch, onPaintCallback, onError, autoExportOnReady, onExported, } = props;
|
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 originSource = originUrlProp ?? originImgPathLegacy ?? '';
|
||||||
const maskSource = maskUrlProp ?? maskImgPathLegacy ?? '';
|
const maskSource = maskUrlProp ?? maskImgPathLegacy ?? '';
|
||||||
const resolvedMaskConfig = useMemo(() => semanticColors
|
const resolvedMaskConfig = useMemo(() => semanticColors
|
||||||
@ -488,7 +517,6 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
const paintLayersPromiseRef = useRef(null);
|
const paintLayersPromiseRef = useRef(null);
|
||||||
const loadPaintLayersRef = useRef(() => Promise.resolve());
|
const loadPaintLayersRef = useRef(() => Promise.resolve());
|
||||||
const [paintResourcesReady, setPaintResourcesReady] = useState(false);
|
const [paintResourcesReady, setPaintResourcesReady] = useState(false);
|
||||||
const [layersLoading, setLayersLoading] = useState(false);
|
|
||||||
const [maskPathsReady, setMaskPathsReady] = useState(false);
|
const [maskPathsReady, setMaskPathsReady] = useState(false);
|
||||||
const baseboardPickMaskRef = useRef(null);
|
const baseboardPickMaskRef = useRef(null);
|
||||||
const kickRegionIdRef = useRef(null);
|
const kickRegionIdRef = useRef(null);
|
||||||
@ -506,110 +534,52 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
// after the user (or initialSession seed) has painted at least one region. This keeps
|
// after the user (or initialSession seed) has painted at least one region. This keeps
|
||||||
// idle segmentation / no-paint cases cheap.
|
// idle segmentation / no-paint cases cheap.
|
||||||
const [highResSnapshotEnabled, setHighResSnapshotEnabled] = useState(false);
|
const [highResSnapshotEnabled, setHighResSnapshotEnabled] = useState(false);
|
||||||
// Layout measurement for the root container of this component. Declared early so
|
// Viewport measured from canvasWrap onLayout — single source of truth for Skia,
|
||||||
// the canvasW/canvasH memos (which decide the viewport rect for zoom centering,
|
// gestures, containRect, and pan clamp (must match the actual touch target).
|
||||||
// containRect placement, clipping, and gesture coordinate mapping) can close over it.
|
const [viewportSize, setViewportSize] = useState(null);
|
||||||
// When the host passes a fitted frame (VisualizationScreen's canvasFrame with explicit
|
|
||||||
// w/h derived from safe area + aspect, or scheme cards), we size our internal Skia
|
|
||||||
// canvas + gesture layer + zoom transform to that exact allocated rect.
|
|
||||||
const [layoutWidth, setLayoutWidth] = useState(null);
|
|
||||||
const [layoutHeight, setLayoutHeight] = useState(null);
|
|
||||||
const [segmentsReady, setSegmentsReady] = useState(false);
|
const [segmentsReady, setSegmentsReady] = useState(false);
|
||||||
const segmentsReadyRef = useRef(false);
|
const segmentsReadyRef = useRef(false);
|
||||||
const maskPathsReadyRef = useRef(false);
|
const maskPathsReadyRef = useRef(false);
|
||||||
const [canvasInteractive, setCanvasInteractive] = useState(false);
|
const [canvasInteractive, setCanvasInteractive] = useState(false);
|
||||||
const [segError, setSegError] = useState('');
|
|
||||||
const [compareMode, setCompareMode] = useState(false);
|
const [compareMode, setCompareMode] = useState(false);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const resegmentInFlightRef = useRef(false);
|
||||||
const [originSkImg, setOriginSkImg] = useState(null);
|
const [originSkImg, setOriginSkImg] = useState(null);
|
||||||
const originSkImgRef = useRef(null);
|
const originSkImgRef = useRef(null);
|
||||||
const lowFreqSkImg = paintResourceLayers?.lowFreqImage ?? null;
|
const lowFreqSkImg = paintResourceLayers?.lowFreqImage ?? null;
|
||||||
const highFreqSkImg = paintResourceLayers?.highFreqImage ?? null;
|
const highFreqSkImg = paintResourceLayers?.highFreqImage ?? null;
|
||||||
const canvasBaseW = SCREEN_WIDTH - 20;
|
const canvasW = viewportSize?.w ?? 1;
|
||||||
// The "viewport" size for this canvas component: the rect inside which we place
|
const canvasH = viewportSize?.h ?? 1;
|
||||||
// the contained image, apply the zoom Group transform (centered), clip, and receive
|
const canvasLayoutReady = viewportSize != null && viewportSize.w > 0 && viewportSize.h > 0;
|
||||||
// gestures (tap + two-finger pinch). When we have a real onLayout from the host
|
// Refs synced to the latest viewport size so async callbacks read post-layout values.
|
||||||
// (VisualizationScreen's aspect-fitted canvasFrame, or scheme card preview area),
|
|
||||||
// we use the *allocated pixel size* directly as our viewport.
|
|
||||||
//
|
|
||||||
// Accurate canvasW/H is still critical for:
|
|
||||||
// - Correct centering of the scaled content around the viewport center.
|
|
||||||
// - Proper containRect (letterbox/centering of the source photo inside the viewport).
|
|
||||||
// - Clip rect and gesture-to-canvas coordinate conversion used by painting.
|
|
||||||
//
|
|
||||||
// Previously the code fell back to aspect-derived sizes even after layout, which
|
|
||||||
// could cause the effective viewport to not match the host frame. Using the onLayout
|
|
||||||
// result (with maxHeight fallback only when no layout yet) keeps zoom, clip, and
|
|
||||||
// touch mapping consistent with what the user actually sees and touches.
|
|
||||||
const viewportW = useMemo(() => {
|
|
||||||
if (layoutWidth != null && layoutHeight != null) {
|
|
||||||
// Primary path for viz screen and scheme cards: the exact size the host
|
|
||||||
// decided for this component (after its own safe-area + aspect fit).
|
|
||||||
return layoutWidth;
|
|
||||||
}
|
|
||||||
if (!maxHeight || maxHeight <= 0) {
|
|
||||||
return canvasBaseW;
|
|
||||||
}
|
|
||||||
// Fallback (no layout yet, or other usages that pass maxHeight without a
|
|
||||||
// tightly sized parent frame). Replicate a contain-style budget.
|
|
||||||
const availableW = canvasBaseW;
|
|
||||||
let auxHeight = 0;
|
|
||||||
if (showToolbar)
|
|
||||||
auxHeight += 40;
|
|
||||||
if (showStatusRow)
|
|
||||||
auxHeight += 30;
|
|
||||||
if (showColorBar)
|
|
||||||
auxHeight += 70;
|
|
||||||
const availableH = Math.max(100, maxHeight - 20 - auxHeight);
|
|
||||||
const imgAspect = imageSize ? imageSize.w / imageSize.h : 1;
|
|
||||||
const containerAspect = availableW / availableH;
|
|
||||||
if (containerAspect > imgAspect) {
|
|
||||||
return Math.floor(availableH * imgAspect);
|
|
||||||
}
|
|
||||||
return availableW;
|
|
||||||
}, [layoutWidth, layoutHeight, maxHeight, showToolbar, showStatusRow, showColorBar, canvasBaseW, imageSize]);
|
|
||||||
const viewportH = useMemo(() => {
|
|
||||||
if (layoutWidth != null && layoutHeight != null) {
|
|
||||||
return layoutHeight;
|
|
||||||
}
|
|
||||||
if (!maxHeight || maxHeight <= 0) {
|
|
||||||
const imgAspect = imageSize ? imageSize.w / imageSize.h : 1;
|
|
||||||
return Math.floor(viewportW / imgAspect);
|
|
||||||
}
|
|
||||||
let auxHeight = 0;
|
|
||||||
if (showToolbar)
|
|
||||||
auxHeight += 40;
|
|
||||||
if (showStatusRow)
|
|
||||||
auxHeight += 30;
|
|
||||||
if (showColorBar)
|
|
||||||
auxHeight += 70;
|
|
||||||
return Math.max(100, maxHeight - 20 - auxHeight);
|
|
||||||
}, [layoutWidth, layoutHeight, maxHeight, showToolbar, showStatusRow, showColorBar, viewportW, imageSize]);
|
|
||||||
// For the rest of the component, "canvasW/H" means the viewport rect size.
|
|
||||||
// All zoom center, wrap size, touch layer, Canvas size, clip, containRect, and
|
|
||||||
// gesture coordinate mapping are based on this, so that two-finger zoom centering
|
|
||||||
// and single-finger tap painting stay consistent with the host-allocated area.
|
|
||||||
const canvasW = viewportW;
|
|
||||||
const canvasH = viewportH;
|
|
||||||
// Refs synced to the latest viewport size so that async callbacks
|
|
||||||
// (segmentAndPrepareLayers) always read post-layout values instead of
|
|
||||||
// stale closure captures. This fixes dashed-outline offset to the bottom
|
|
||||||
// when the initial pathMapRect was computed with fallback SCREEN_WIDTH.
|
|
||||||
// Declared before segmentAndPrepareLayers so the async body can reference them.
|
|
||||||
const canvasWRef = useRef(canvasW);
|
const canvasWRef = useRef(canvasW);
|
||||||
const canvasHRef = useRef(canvasH);
|
const canvasHRef = useRef(canvasH);
|
||||||
// ── Pinch-zoom (two-finger only; single-finger drag/pan disabled) ─────────
|
// ── Pinch-zoom (focal-point) + single-finger pan when zoomed ─────────────
|
||||||
const [zoomScale, setZoomScale] = useState(1);
|
const [zoomScale, setZoomScale] = useState(1);
|
||||||
const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
|
const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
|
||||||
// Refs for gesture callbacks (closures don't capture fresh state mid-gesture)
|
// Refs for gesture callbacks (closures don't capture fresh state mid-gesture)
|
||||||
const zoomScaleRef = useRef(1);
|
const zoomScaleRef = useRef(1);
|
||||||
const panOffsetRef = useRef({ x: 0, y: 0 });
|
const panOffsetRef = useRef({ x: 0, y: 0 });
|
||||||
// Baseline value captured at gesture start to avoid jump on re-creation for pinch
|
const pinchBaseScaleRef = useRef(1);
|
||||||
const zoomBaseRef = 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).
|
// Ref to the latest containRect (the actual placed photo rect inside the viewport).
|
||||||
const containRectRef = useRef(null);
|
const containRectRef = useRef(null);
|
||||||
useEffect(() => { zoomScaleRef.current = zoomScale; }, [zoomScale]);
|
useEffect(() => { zoomScaleRef.current = zoomScale; }, [zoomScale]);
|
||||||
useEffect(() => { panOffsetRef.current = panOffset; }, [panOffset]);
|
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(() => {
|
const resetZoom = useCallback(() => {
|
||||||
setZoomScale(1);
|
setZoomScale(1);
|
||||||
setPanOffset({ x: 0, y: 0 });
|
setPanOffset({ x: 0, y: 0 });
|
||||||
@ -708,7 +678,6 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
timeLog('▶ start segmentation');
|
timeLog('▶ start segmentation');
|
||||||
segmentsReadyRef.current = false;
|
segmentsReadyRef.current = false;
|
||||||
setSegmentsReady(false);
|
setSegmentsReady(false);
|
||||||
setSegError('');
|
|
||||||
setActiveBrushIndex(null);
|
setActiveBrushIndex(null);
|
||||||
const clearedPainted = new Map();
|
const clearedPainted = new Map();
|
||||||
paintedRegionsRef.current = clearedPainted;
|
paintedRegionsRef.current = clearedPainted;
|
||||||
@ -740,7 +709,6 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
setRegionOutlinePaths(new Map());
|
setRegionOutlinePaths(new Map());
|
||||||
setMaskPathsReady(false);
|
setMaskPathsReady(false);
|
||||||
setPaintResourcesReady(false);
|
setPaintResourcesReady(false);
|
||||||
setLayersLoading(false);
|
|
||||||
baseboardPickMaskRef.current = null;
|
baseboardPickMaskRef.current = null;
|
||||||
kickRegionIdRef.current = null;
|
kickRegionIdRef.current = null;
|
||||||
maskPathsContainRectRef.current = null;
|
maskPathsContainRectRef.current = null;
|
||||||
@ -902,7 +870,6 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
if (!isCancelled()) {
|
if (!isCancelled()) {
|
||||||
console.error('[SDK-SEGMENT] segmentation failed', e);
|
console.error('[SDK-SEGMENT] segmentation failed', e);
|
||||||
setSegError(msg);
|
|
||||||
reportError(msg, e);
|
reportError(msg, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -924,7 +891,6 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
const runId = segmentRunIdRef.current;
|
const runId = segmentRunIdRef.current;
|
||||||
let promise;
|
let promise;
|
||||||
promise = (async () => {
|
promise = (async () => {
|
||||||
setLayersLoading(true);
|
|
||||||
timeLog('▶ start loading paint shader textures');
|
timeLog('▶ start loading paint shader textures');
|
||||||
try {
|
try {
|
||||||
const result = await preparePaintResourcesFromWorkBuffer(work.buffer, work.cols, work.rows, layers => {
|
const result = await preparePaintResourcesFromWorkBuffer(work.buffer, work.cols, work.rows, layers => {
|
||||||
@ -965,7 +931,6 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setLayersLoading(false);
|
|
||||||
if (paintLayersPromiseRef.current === promise) {
|
if (paintLayersPromiseRef.current === promise) {
|
||||||
paintLayersPromiseRef.current = null;
|
paintLayersPromiseRef.current = null;
|
||||||
}
|
}
|
||||||
@ -975,29 +940,11 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
return promise;
|
return promise;
|
||||||
}, [emitInteractiveIfReady]);
|
}, [emitInteractiveIfReady]);
|
||||||
loadPaintLayersRef.current = loadPaintLayersIfNeeded;
|
loadPaintLayersRef.current = loadPaintLayersIfNeeded;
|
||||||
const pickOriginImage = async () => {
|
|
||||||
const res = await launchImageLibrary({ mediaType: 'photo' });
|
|
||||||
const uri = res.assets?.[0]?.uri;
|
|
||||||
if (!uri) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pngPath = await cv.ensurePngPath(uri, `picked_origin_${Date.now()}.png`);
|
|
||||||
setOriginImgPath(pngPath);
|
|
||||||
};
|
|
||||||
const pickMaskImage = async () => {
|
|
||||||
const res = await launchImageLibrary({ mediaType: 'photo' });
|
|
||||||
const uri = res.assets?.[0]?.uri;
|
|
||||||
if (!uri) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pngPath = await cv.ensurePngPath(uri, `picked_mask_${Date.now()}.png`);
|
|
||||||
setMaskImgPath(pngPath);
|
|
||||||
};
|
|
||||||
const clearCacheAndResegment = useCallback(async () => {
|
const clearCacheAndResegment = useCallback(async () => {
|
||||||
if (isRefreshing || !originImgPath || !maskImgPath) {
|
if (resegmentInFlightRef.current || !originImgPath || !maskImgPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsRefreshing(true);
|
resegmentInFlightRef.current = true;
|
||||||
try {
|
try {
|
||||||
resetZoom();
|
resetZoom();
|
||||||
const layers = paintResourceLayersRef.current;
|
const layers = paintResourceLayersRef.current;
|
||||||
@ -1012,14 +959,12 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
setSegError(msg);
|
|
||||||
reportError(msg, e);
|
reportError(msg, e);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setIsRefreshing(false);
|
resegmentInFlightRef.current = false;
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isRefreshing,
|
|
||||||
originImgPath,
|
originImgPath,
|
||||||
maskImgPath,
|
maskImgPath,
|
||||||
segmentAndPrepareLayers,
|
segmentAndPrepareLayers,
|
||||||
@ -1035,6 +980,7 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
segmentInFlightKeyRef.current === segmentKey) {
|
segmentInFlightKeyRef.current === segmentKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
resetZoom();
|
||||||
segmentInFlightKeyRef.current = segmentKey;
|
segmentInFlightKeyRef.current = segmentKey;
|
||||||
void segmentAndPrepareLayers(originImgPath, maskImgPath).finally(() => {
|
void segmentAndPrepareLayers(originImgPath, maskImgPath).finally(() => {
|
||||||
if (segmentInFlightKeyRef.current === segmentKey) {
|
if (segmentInFlightKeyRef.current === segmentKey) {
|
||||||
@ -1066,7 +1012,7 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
}
|
}
|
||||||
setInitFlashRegionId(null);
|
setInitFlashRegionId(null);
|
||||||
};
|
};
|
||||||
}, [originImgPath, maskImgPath]);
|
}, [originImgPath, maskImgPath, resetZoom]);
|
||||||
const buildPaintedRecords = useCallback(() => {
|
const buildPaintedRecords = useCallback(() => {
|
||||||
const records = [];
|
const records = [];
|
||||||
// Prefer the live ref so getPaintedRegions() / session() used by host for
|
// Prefer the live ref so getPaintedRegions() / session() used by host for
|
||||||
@ -1340,13 +1286,6 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [segmentsReady, imageSize, canvasW, canvasH]);
|
}, [segmentsReady, imageSize, canvasW, canvasH]);
|
||||||
const selectBrushColor = useCallback((brushIndex) => {
|
|
||||||
onUserInteraction();
|
|
||||||
setCustomPaintColor(null);
|
|
||||||
customPaintConfigJsonRef.current = undefined;
|
|
||||||
setActiveBrushIndex(brushIndex);
|
|
||||||
void loadPaintLayersIfNeeded();
|
|
||||||
}, [onUserInteraction, loadPaintLayersIfNeeded]);
|
|
||||||
const onCanvasTap = useCallback((x, y) => {
|
const onCanvasTap = useCallback((x, y) => {
|
||||||
onUserInteraction();
|
onUserInteraction();
|
||||||
if (!segmentsReady ||
|
if (!segmentsReady ||
|
||||||
@ -1675,21 +1614,13 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
return null;
|
return null;
|
||||||
return (_jsxs(_Fragment, { children: [_jsx(Path, { path: path, color: paintRuntime.regionOverlayFill, style: "fill", opacity: 0.05 }, `${keyPrefix}-fill-${regionId}`), _jsx(Path, { path: path, color: paintRuntime.regionOverlayFill, style: "stroke", strokeWidth: 3, strokeJoin: "round", antiAlias: true, children: _jsx(DashPathEffect, { intervals: [8, 6] }) }, `${keyPrefix}-stroke-${regionId}`)] }));
|
return (_jsxs(_Fragment, { children: [_jsx(Path, { path: path, color: paintRuntime.regionOverlayFill, style: "fill", opacity: 0.05 }, `${keyPrefix}-fill-${regionId}`), _jsx(Path, { path: path, color: paintRuntime.regionOverlayFill, style: "stroke", strokeWidth: 3, strokeJoin: "round", antiAlias: true, children: _jsx(DashPathEffect, { intervals: [8, 6] }) }, `${keyPrefix}-stroke-${regionId}`)] }));
|
||||||
};
|
};
|
||||||
// ── Zoom transform for the Skia Group ─────────────────────────────────
|
// ── Zoom matrix for the Skia Group (pan + scale around center) ──────────
|
||||||
// Note: single-finger pan/drag is disabled. Zoom is always centered around the
|
const zoomMatrix = useMemo(() => {
|
||||||
// viewport center (no additional panOffset translation). The panOffset state
|
if (zoomScale <= 1) {
|
||||||
// is kept (and forced to 0 during pinch) for compatibility with screenToCanvasCoords
|
|
||||||
// inverse mapping used by tap/paint logic, and for resetZoom.
|
|
||||||
const zoomTransform = useMemo(() => {
|
|
||||||
if (zoomScale <= 1)
|
|
||||||
return undefined;
|
return undefined;
|
||||||
return [
|
}
|
||||||
{ translateX: 0, translateY: 0 },
|
return buildZoomPanMatrix(panOffset.x, panOffset.y, zoomScale, canvasW, canvasH);
|
||||||
{ translateX: canvasW / 2, translateY: canvasH / 2 },
|
}, [zoomScale, panOffset, canvasW, canvasH]);
|
||||||
{ scale: zoomScale },
|
|
||||||
{ translateX: -canvasW / 2, translateY: -canvasH / 2 },
|
|
||||||
];
|
|
||||||
}, [zoomScale, canvasW, canvasH]);
|
|
||||||
const renderDraw = () => {
|
const renderDraw = () => {
|
||||||
const displayImg = originSkImg ?? lowFreqSkImg;
|
const displayImg = originSkImg ?? lowFreqSkImg;
|
||||||
if (!displayImg || !containRect) {
|
if (!displayImg || !containRect) {
|
||||||
@ -1708,7 +1639,7 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
// (shader leakage or edge sampling artifacts) from appearing outside the
|
// (shader leakage or edge sampling artifacts) from appearing outside the
|
||||||
// photo rect when the content is scaled up.
|
// photo rect when the content is scaled up.
|
||||||
const previewBg = '#F0F1F3';
|
const previewBg = '#F0F1F3';
|
||||||
return (_jsxs(_Fragment, { children: [_jsx(Rect, { x: 0, y: 0, width: canvasW, height: canvasH, color: previewBg }), _jsx(Group, { clip: Skia.XYWHRect(0, 0, canvasW, canvasH), children: _jsxs(Group, { transform: zoomTransform, children: [useShader && shaderOrigin ? (_jsx(PaintShaderLayer, { originImage: shaderOrigin, paintColorMap: paintColorMapSkImg, lowFreqImage: lowFreqSkImg, highFreqImage: highFreqSkImg, x: containRect.x, y: containRect.y, width: containRect.w, height: containRect.h, showOrigin: false })) : (renderImageLayer(displayImg)), showOverlay &&
|
return (_jsxs(_Fragment, { children: [_jsx(Rect, { x: 0, y: 0, width: canvasW, height: canvasH, color: previewBg }), _jsx(Group, { clip: Skia.XYWHRect(0, 0, canvasW, canvasH), children: _jsxs(Group, { matrix: zoomMatrix, children: [useShader && shaderOrigin ? (_jsx(PaintShaderLayer, { originImage: shaderOrigin, paintColorMap: paintColorMapSkImg, lowFreqImage: lowFreqSkImg, highFreqImage: highFreqSkImg, x: containRect.x, y: containRect.y, width: containRect.w, height: containRect.h, showOrigin: false })) : (renderImageLayer(displayImg)), showOverlay &&
|
||||||
initFlashRegionId != null &&
|
initFlashRegionId != null &&
|
||||||
renderRegionMaskOverlay(initFlashRegionId, 'init-overlay'), showOverlay &&
|
renderRegionMaskOverlay(initFlashRegionId, 'init-overlay'), showOverlay &&
|
||||||
heldRegionId != null &&
|
heldRegionId != null &&
|
||||||
@ -1813,20 +1744,42 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
runOnJS(onFinalizeJS)(e.x, e.y);
|
runOnJS(onFinalizeJS)(e.x, e.y);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
// ── Gesture: pinch-zoom (two-finger scale; max 5×) ─────────────────────
|
// ── Gesture: pinch-zoom (focal-point scale + two-finger pan; max 5×) ────
|
||||||
const pinchGesture = useMemo(() => {
|
const pinchGesture = useMemo(() => {
|
||||||
const onStartJS = () => {
|
const onStartJS = (focalX, focalY) => {
|
||||||
zoomBaseRef.current = zoomScaleRef.current;
|
pinchBaseScaleRef.current = zoomScaleRef.current;
|
||||||
|
pinchBasePanRef.current = { ...panOffsetRef.current };
|
||||||
|
pinchBaseFocalRef.current = { x: focalX, y: focalY };
|
||||||
};
|
};
|
||||||
const onUpdateJS = (scale) => {
|
const onUpdateJS = (scale, focalX, focalY) => {
|
||||||
const newScale = Math.max(1, Math.min(zoomBaseRef.current * scale, 5));
|
const cw = canvasWRef.current;
|
||||||
|
const ch = canvasHRef.current;
|
||||||
|
if (cw <= 0 || ch <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cx = cw / 2;
|
||||||
|
const cy = ch / 2;
|
||||||
|
const baseScale = pinchBaseScaleRef.current;
|
||||||
|
const basePan = pinchBasePanRef.current;
|
||||||
|
const baseFocal = pinchBaseFocalRef.current;
|
||||||
|
let newScale = Math.max(1, Math.min(baseScale * scale, 5));
|
||||||
|
const anchorX = (baseFocal.x - basePan.x - cx) / baseScale + cx;
|
||||||
|
const anchorY = (baseFocal.y - basePan.y - cy) / baseScale + cy;
|
||||||
|
let newPan = {
|
||||||
|
x: focalX - cx - newScale * (anchorX - cx),
|
||||||
|
y: focalY - cy - newScale * (anchorY - cy),
|
||||||
|
};
|
||||||
|
if (newScale <= 1) {
|
||||||
|
newScale = 1;
|
||||||
|
newPan = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
newPan = clampPanOffset(newPan, newScale, cw, ch, containRectRef.current);
|
||||||
|
}
|
||||||
setZoomScale(newScale);
|
setZoomScale(newScale);
|
||||||
|
setPanOffset(newPan);
|
||||||
zoomScaleRef.current = newScale;
|
zoomScaleRef.current = newScale;
|
||||||
// Lock pan offset to zero: only two-finger pinch zoom is supported.
|
panOffsetRef.current = newPan;
|
||||||
// Single-finger drag/pan after zoom is intentionally disabled per product decision.
|
|
||||||
// Zoom is always centered; no additional translate from panOffset is applied in practice.
|
|
||||||
setPanOffset({ x: 0, y: 0 });
|
|
||||||
panOffsetRef.current = { x: 0, y: 0 };
|
|
||||||
};
|
};
|
||||||
const onEndJS = () => {
|
const onEndJS = () => {
|
||||||
if (zoomScaleRef.current <= 1.01) {
|
if (zoomScaleRef.current <= 1.01) {
|
||||||
@ -1834,63 +1787,63 @@ const MaskSegmentCanvas = forwardRef(function MaskSegmentCanvas(props, ref) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return Gesture.Pinch()
|
return Gesture.Pinch()
|
||||||
.onStart(() => {
|
.onStart((e) => {
|
||||||
'worklet';
|
'worklet';
|
||||||
runOnJS(onStartJS)();
|
runOnJS(onStartJS)(e.focalX, e.focalY);
|
||||||
})
|
})
|
||||||
.onUpdate((e) => {
|
.onUpdate((e) => {
|
||||||
'worklet';
|
'worklet';
|
||||||
runOnJS(onUpdateJS)(e.scale);
|
runOnJS(onUpdateJS)(e.scale, e.focalX, e.focalY);
|
||||||
})
|
})
|
||||||
.onEnd(() => {
|
.onEnd(() => {
|
||||||
'worklet';
|
'worklet';
|
||||||
runOnJS(onEndJS)();
|
runOnJS(onEndJS)();
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
// ── Composed: Race ensures single-tap and pinch-zoom never conflict ─────────
|
// ── Gesture: single-finger pan (active only when zoomed) ───────────────
|
||||||
// 依赖置空 + 内部的 tap/pinch 也用 [],保证整个手势描述符树只在 mount 时创建一次。
|
const panGesture = useMemo(() => {
|
||||||
// 避免初始化阶段反复创建导致 Reanimated / RNGH 内部节点重复分配。
|
const onStartJS = () => {
|
||||||
const composedGesture = useMemo(() => Gesture.Race(tapGesture, pinchGesture), []);
|
panBaseRef.current = { ...panOffsetRef.current };
|
||||||
return (_jsxs(View, { style: [styles.container, style], onLayout: (e) => {
|
};
|
||||||
const { width, height } = e.nativeEvent.layout;
|
const onUpdateJS = (translationX, translationY) => {
|
||||||
setLayoutWidth(width);
|
if (zoomScaleRef.current <= 1) {
|
||||||
setLayoutHeight(height);
|
return;
|
||||||
}, children: [(showToolbar || showDebugPickers || showStatusRow || showColorBar) ? (_jsxs(ScrollView, { style: styles.scroll, showsVerticalScrollIndicator: false, contentContainerStyle: styles.scrollContent, keyboardShouldPersistTaps: "always", children: [showToolbar && (_jsx(View, { style: styles.toolbarRow, children: _jsx(Button, { title: isRefreshing ? 're-segmenting…' : 'clear cache and re-segment', onPress: clearCacheAndResegment, disabled: isRefreshing || !originImgPath || !maskImgPath }) })), showDebugPickers && (_jsxs(View, { style: styles.btnRow, children: [_jsx(Button, { title: "pick origin image", onPress: pickOriginImage }), _jsx(View, { style: styles.btnGap }), _jsx(Button, { title: "pick mask image", onPress: pickMaskImage })] })), showStatusRow && (_jsx(View, { style: styles.statusRow, children: segError ? (_jsxs(Text, { style: styles.err, children: ["\u274C ", segError] })) : !segmentsReady ? (_jsx(Text, { style: styles.hint, children: "\u23F3 segmenting\u2026" })) : layersLoading || !paintResourcesReady ? (_jsxs(Text, { style: styles.hint, children: ["\u2705 ", regionCount, " regions \u00B7 Shader textures preparing\u2026"] })) : !(originSkImg ?? lowFreqSkImg) ? (_jsx(Text, { style: styles.hint, children: "\u23F3 image loading\u2026" })) : (_jsxs(Text, { style: styles.hint, children: ["\u2705 ", regionCount, " regions \u00B7 painted ", paintedRegions.size, " \u00B7", ' ', hasActiveBrush
|
}
|
||||||
? customPaintColor
|
const newPan = clampPanOffset({
|
||||||
? 'custom paint color'
|
x: panBaseRef.current.x + translationX,
|
||||||
: `paint color ${(activeBrushIndex ?? 0) + 1}`
|
y: panBaseRef.current.y + translationY,
|
||||||
: 'please select the bottom paint color first', ' · ', compareMode ? 'compare original image' : 'paint mode'] })) })), showColorBar && (_jsxs(View, { style: styles.colorBar, children: [_jsx(Text, { style: styles.colorBarLabel, children: "paint color (tap to select, then tap canvas to paint)" }), _jsxs(View, { style: styles.colorSwatches, children: [paintPalette.map((color, index) => {
|
}, zoomScaleRef.current, canvasWRef.current, canvasHRef.current, containRectRef.current);
|
||||||
const isActive = activeBrushIndex === index && customPaintColor == null;
|
setPanOffset(newPan);
|
||||||
const { b, g, r } = color;
|
panOffsetRef.current = newPan;
|
||||||
return (_jsx(TouchableOpacity, { style: [
|
};
|
||||||
styles.colorSwatch,
|
const onEndJS = () => {
|
||||||
{ backgroundColor: bgrToCss(b, g, r) },
|
if (zoomScaleRef.current <= 1.01) {
|
||||||
isActive && styles.colorSwatchSelected,
|
resetZoomRef.current?.();
|
||||||
], activeOpacity: 0.8, disabled: !segmentsReady || disabled, onPress: () => selectBrushColor(index) }, index));
|
}
|
||||||
}), !segmentsReady && (_jsx(Text, { style: styles.colorBarEmpty, children: "loading\u2026" }))] }), customPaintColor && (_jsx(Text, { style: styles.hint, children: "current custom paint color is set by ref.setPaintColor" }))] }))] })) : null, _jsx(View, { style: [styles.canvasOuter, canvasStyle], children: _jsxs(View, { style: [styles.canvasWrap, { width: canvasW, height: canvasH }], children: [_jsx(GestureDetector, { gesture: composedGesture, children: _jsx(View, { style: [styles.canvasTouchLayer, { width: canvasW, height: canvasH }], children: _jsx(Canvas, { style: { width: canvasW, height: canvasH }, pointerEvents: "none", children: renderDraw() }) }) }), showOverlayButtons &&
|
};
|
||||||
(renderUndoButton ? (renderUndoButton({
|
return Gesture.Pan()
|
||||||
onPress: undoSelection,
|
.minPointers(1)
|
||||||
disabled: paintHistory.length === 0,
|
.maxPointers(1)
|
||||||
text: undoButtonText,
|
.minDistance(10)
|
||||||
})) : (_jsx(TouchableOpacity, { style: [styles.overlayBtn, styles.btnBottomLeft, undoButtonStyle], activeOpacity: 0.7, disabled: paintHistory.length === 0 || disabled, onPress: undoSelection, children: _jsx(Text, { style: [
|
.onStart(() => {
|
||||||
styles.btnText,
|
'worklet';
|
||||||
undoButtonTextStyle,
|
runOnJS(onStartJS)();
|
||||||
{ opacity: paintHistory.length === 0 ? 0.4 : 1 },
|
})
|
||||||
], children: undoButtonText }) }))), showOverlayButtons &&
|
.onUpdate((e) => {
|
||||||
(renderCompareButton ? (renderCompareButton({
|
'worklet';
|
||||||
onPress: () => {
|
runOnJS(onUpdateJS)(e.translationX, e.translationY);
|
||||||
onUserInteraction();
|
})
|
||||||
setCompareMode(v => !v);
|
.onEnd(() => {
|
||||||
},
|
'worklet';
|
||||||
text: compareMode ? compareExitButtonText : compareButtonText,
|
runOnJS(onEndJS)();
|
||||||
})) : (_jsx(TouchableOpacity, { style: [
|
});
|
||||||
styles.overlayBtn,
|
}, []);
|
||||||
styles.btnBottomRight,
|
// ── Composed: pinch + pan simultaneous; tap only when neither claims ───
|
||||||
compareButtonStyle,
|
const composedGesture = useMemo(() => Gesture.Exclusive(Gesture.Simultaneous(pinchGesture, panGesture), tapGesture), []);
|
||||||
], activeOpacity: 0.7, disabled: disabled, onPress: () => {
|
return (_jsxs(View, { style: [styles.container, style], children: [_jsx(View, { style: styles.canvasWrap, onLayout: (e) => {
|
||||||
onUserInteraction();
|
const { width, height } = e.nativeEvent.layout;
|
||||||
setCompareMode(v => !v);
|
handleCanvasWrapLayout(width, height);
|
||||||
}, children: _jsx(Text, { style: [styles.btnText, compareButtonTextStyle], children: compareMode ? compareExitButtonText : compareButtonText }) })))] }) }), highResSnapshotEnabled && exportCanvasSize ? (_jsx(View, { style: {
|
}, children: canvasLayoutReady ? (_jsx(GestureDetector, { gesture: composedGesture, children: _jsx(View, { style: { width: canvasW, height: canvasH }, children: _jsx(Canvas, { style: { width: canvasW, height: canvasH }, pointerEvents: "none", children: renderDraw() }) }) })) : null }), highResSnapshotEnabled && exportCanvasSize ? (_jsx(View, { style: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: -exportCanvasSize.w - 200,
|
left: -exportCanvasSize.w - 200,
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -1905,123 +1858,16 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
},
|
},
|
||||||
scroll: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
scrollContent: {
|
|
||||||
padding: 10,
|
|
||||||
paddingBottom: 28,
|
|
||||||
},
|
|
||||||
toolbarRow: {
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
btnRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
btnGap: {
|
|
||||||
width: 10,
|
|
||||||
},
|
|
||||||
statusRow: {
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
hint: {
|
|
||||||
color: '#333',
|
|
||||||
fontSize: 13,
|
|
||||||
},
|
|
||||||
err: {
|
|
||||||
color: '#c33',
|
|
||||||
fontSize: 13,
|
|
||||||
},
|
|
||||||
canvasWrap: {
|
canvasWrap: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
alignSelf: 'stretch',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: '#f5f5f5',
|
||||||
borderWidth: StyleSheet.hairlineWidth,
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
borderColor: '#ddd',
|
borderColor: '#ddd',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
canvasOuter: {
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
// When the internal control ScrollView is omitted (the normal case for
|
|
||||||
// VisualizationScreen and non-interactive scheme previews), this makes the
|
|
||||||
// canvas area fill the bounds provided by the host (via style + maxHeight)
|
|
||||||
// and centers the fixed-size image rect. This also ensures the
|
|
||||||
// GestureDetector sits in the right place to receive taps and two-finger pinches.
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
canvasTouchLayer: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
canvas: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
overlayBtn: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 10,
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
paddingVertical: 7,
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.62)',
|
|
||||||
borderRadius: 6,
|
|
||||||
},
|
|
||||||
btnBottomLeft: {
|
|
||||||
left: 10,
|
|
||||||
},
|
|
||||||
btnBottomRight: {
|
|
||||||
right: 10,
|
|
||||||
},
|
|
||||||
btnText: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
colorBar: {
|
|
||||||
marginTop: 12,
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 4,
|
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
|
||||||
borderTopColor: '#e0e0e0',
|
|
||||||
},
|
|
||||||
colorBarLabel: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: '#666',
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
colorSwatches: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
colorSwatch: {
|
|
||||||
width: 44,
|
|
||||||
height: 44,
|
|
||||||
borderRadius: 22,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: 'rgba(0,0,0,0.12)',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
colorSwatchSelected: {
|
|
||||||
borderColor: '#1e96ff',
|
|
||||||
borderWidth: 3,
|
|
||||||
transform: [{ scale: 1.08 }],
|
|
||||||
},
|
|
||||||
colorSwatchPainted: {
|
|
||||||
borderColor: 'rgba(255,255,255,0.9)',
|
|
||||||
},
|
|
||||||
colorSwatchDot: {
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.92)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(0,0,0,0.25)',
|
|
||||||
},
|
|
||||||
colorBarEmpty: {
|
|
||||||
fontSize: 13,
|
|
||||||
color: '#999',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
export default MaskSegmentCanvas;
|
export default MaskSegmentCanvas;
|
||||||
//# sourceMappingURL=MaskSegmentCanvas.js.map
|
//# sourceMappingURL=MaskSegmentCanvas.js.map
|
||||||
2
dist/components/MaskSegmentCanvas.js.map
vendored
2
dist/components/MaskSegmentCanvas.js.map
vendored
File diff suppressed because one or more lines are too long
26
dist/components/MaskSegmentCanvas.types.d.ts
vendored
26
dist/components/MaskSegmentCanvas.types.d.ts
vendored
@ -1,5 +1,4 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { StyleProp, ViewStyle } from 'react-native';
|
||||||
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
|
||||||
import type { SegmentRegion } from '../utils/maskSegmentation';
|
import type { SegmentRegion } from '../utils/maskSegmentation';
|
||||||
import type { MaskSemanticColor } from '../utils/maskSemanticPalette';
|
import type { MaskSemanticColor } from '../utils/maskSemanticPalette';
|
||||||
export type BgrColor = {
|
export type BgrColor = {
|
||||||
@ -118,11 +117,6 @@ export type PaintBrushRequiredPayload = {
|
|||||||
regionName: string;
|
regionName: string;
|
||||||
};
|
};
|
||||||
export type PaintCallbackPayload = PaintSuccessPayload | PaintBrushRequiredPayload;
|
export type PaintCallbackPayload = PaintSuccessPayload | PaintBrushRequiredPayload;
|
||||||
export type OverlayButtonRenderProps = {
|
|
||||||
onPress: () => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
export type MaskSegmentCanvasRef = {
|
export type MaskSegmentCanvasRef = {
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
swap: (showOrigin?: boolean) => void;
|
swap: (showOrigin?: boolean) => void;
|
||||||
@ -160,30 +154,14 @@ export type MaskSegmentCanvasProps = {
|
|||||||
initialSession?: MaskSegmentSession;
|
initialSession?: MaskSegmentSession;
|
||||||
initialPaintColor?: BgrColor;
|
initialPaintColor?: BgrColor;
|
||||||
initialPaintConfigJson?: Record<string, unknown>;
|
initialPaintConfigJson?: Record<string, unknown>;
|
||||||
showDebugPickers?: boolean;
|
|
||||||
showToolbar?: boolean;
|
|
||||||
showColorBar?: boolean;
|
|
||||||
showStatusRow?: boolean;
|
|
||||||
showOverlayButtons?: boolean;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
style?: StyleProp<ViewStyle>;
|
style?: StyleProp<ViewStyle>;
|
||||||
canvasStyle?: StyleProp<ViewStyle>;
|
|
||||||
/**
|
/**
|
||||||
* Max container height available for this component (px). When set, the SDK
|
* Max container height available for this component (px). When set, the SDK
|
||||||
* computes canvas dimensions as a fit-contain within (screenWidth - 20, maxHeight)
|
* computes canvas dimensions as a fit-contain within (screenWidth - 20, maxHeight)
|
||||||
* instead of using the full image aspect at full screen width. This prevents
|
* instead of using the full image aspect at full screen width.
|
||||||
* internal ScrollView scrolling for tall images.
|
|
||||||
*/
|
*/
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
undoButtonStyle?: StyleProp<ViewStyle>;
|
|
||||||
compareButtonStyle?: StyleProp<ViewStyle>;
|
|
||||||
undoButtonTextStyle?: StyleProp<TextStyle>;
|
|
||||||
compareButtonTextStyle?: StyleProp<TextStyle>;
|
|
||||||
undoButtonText?: string;
|
|
||||||
compareButtonText?: string;
|
|
||||||
compareExitButtonText?: string;
|
|
||||||
renderUndoButton?: (props: OverlayButtonRenderProps) => ReactNode;
|
|
||||||
renderCompareButton?: (props: OverlayButtonRenderProps) => ReactNode;
|
|
||||||
onWatch?: (state: MaskSegmentWatchState, durationMs: number, detail?: MaskSegmentWatchDetail) => void;
|
onWatch?: (state: MaskSegmentWatchState, durationMs: number, detail?: MaskSegmentWatchDetail) => void;
|
||||||
onPaintCallback?: (payload: PaintCallbackPayload) => void;
|
onPaintCallback?: (payload: PaintCallbackPayload) => void;
|
||||||
onError?: (message: string, error?: unknown) => void;
|
onError?: (message: string, error?: unknown) => void;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
2
dist/index.d.ts
vendored
2
dist/index.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
export { default } from './components/MaskSegmentCanvas';
|
export { default } from './components/MaskSegmentCanvas';
|
||||||
export type { BgrColor, InteractionConfig, MaskSegmentCanvasProps, MaskSegmentCanvasRef, MaskSegmentConfig, MaskSegmentSession, MaskSegmentWatchDetail, MaskSegmentWatchState, MaskSemanticColor, OverlayButtonRenderProps, PaintBrushRequiredPayload, PaintCallbackPayload, PaintSuccessPayload, PaintConfig, PaintedRegionRecord, PipelineConfig, PipelinePreset, SavePaintOptions, SavePaintResult, SegmentRegion, } from './components/MaskSegmentCanvas.types';
|
export type { BgrColor, InteractionConfig, MaskSegmentCanvasProps, MaskSegmentCanvasRef, MaskSegmentConfig, MaskSegmentSession, MaskSegmentWatchDetail, MaskSegmentWatchState, MaskSemanticColor, PaintBrushRequiredPayload, PaintCallbackPayload, PaintSuccessPayload, PaintConfig, PaintedRegionRecord, PipelineConfig, PipelinePreset, SavePaintOptions, SavePaintResult, SegmentRegion, } from './components/MaskSegmentCanvas.types';
|
||||||
export { BASEBOARD_SEMANTIC_NAME, MASK_SEMANTIC_COLORS, } from './utils/maskSemanticPalette';
|
export { BASEBOARD_SEMANTIC_NAME, MASK_SEMANTIC_COLORS, } from './utils/maskSemanticPalette';
|
||||||
export { createRuntimeConfig, DEFAULT_INTERACTION_CONFIG, DEFAULT_MASK_CONFIG, DEFAULT_PAINT_CONFIG, DEFAULT_PIPELINE_CONFIG, PIPELINE_HIGH, PIPELINE_LOW, PIPELINE_MEDIUM, PIPELINE_PRESETS, getMaskSegmentRuntimeConfig, resolvePipelineConfig, setMaskSegmentRuntimeConfig, } from './utils/maskSegmentRuntime';
|
export { createRuntimeConfig, DEFAULT_INTERACTION_CONFIG, DEFAULT_MASK_CONFIG, DEFAULT_PAINT_CONFIG, DEFAULT_PIPELINE_CONFIG, PIPELINE_HIGH, PIPELINE_LOW, PIPELINE_MEDIUM, PIPELINE_PRESETS, getMaskSegmentRuntimeConfig, resolvePipelineConfig, setMaskSegmentRuntimeConfig, } from './utils/maskSegmentRuntime';
|
||||||
export { prewarmPngBgrCache, prewarmPngBgrCacheAsync, } from './utils/pngImage';
|
export { prewarmPngBgrCache, prewarmPngBgrCacheAsync, } from './utils/pngImage';
|
||||||
|
|||||||
2
dist/index.d.ts.map
vendored
2
dist/index.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AACzD,YAAY,EACV,QAAQ,EACR,iBAAiB,EACjB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,wBAAwB,EACxB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,aAAa,GACd,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,aAAa,EACb,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,2BAA2B,EAC3B,qBAAqB,EACrB,2BAA2B,GAC5B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC"}
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AACzD,YAAY,EACV,QAAQ,EACR,iBAAiB,EACjB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,aAAa,GACd,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,aAAa,EACb,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,2BAA2B,EAC3B,qBAAqB,EACrB,2BAA2B,GAC5B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC"}
|
||||||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAuBzD,OAAO,EACL,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,aAAa,EACb,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,2BAA2B,EAC3B,qBAAqB,EACrB,2BAA2B,GAC5B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC"}
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAsBzD,OAAO,EACL,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,aAAa,EACb,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,2BAA2B,EAC3B,qBAAqB,EACrB,2BAA2B,GAC5B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC"}
|
||||||
4
dist/utils/paintShaderRuntime.js
vendored
4
dist/utils/paintShaderRuntime.js
vendored
@ -1,5 +1,5 @@
|
|||||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
import { Skia, Fill, Shader, ImageShader, Group, drawAsImage, } from '@shopify/react-native-skia';
|
import { Skia, Shader, ImageShader, Group, Rect, drawAsImage, } from '@shopify/react-native-skia';
|
||||||
import { REGION_PAINT_SKSL } from '../shaders/regionPaint.sksl';
|
import { REGION_PAINT_SKSL } from '../shaders/regionPaint.sksl';
|
||||||
import { getMaskSegmentRuntimeConfig } from './maskSegmentRuntime';
|
import { getMaskSegmentRuntimeConfig } from './maskSegmentRuntime';
|
||||||
import { buildPaintColorMapImage } from './paintColorMapTexture';
|
import { buildPaintColorMapImage } from './paintColorMapTexture';
|
||||||
@ -34,7 +34,7 @@ function createPaintShaderTree(props) {
|
|||||||
ty: 'clamp',
|
ty: 'clamp',
|
||||||
rect: { x, y, width, height },
|
rect: { x, y, width, height },
|
||||||
};
|
};
|
||||||
return (_jsx(Fill, { children: _jsxs(Shader, { source: effect, uniforms: uniforms, children: [_jsx(ImageShader, { image: originImage, ...imageShaderProps }), _jsx(ImageShader, { image: paintColorMap, ...imageShaderProps }), _jsx(ImageShader, { image: lowFreqImage, ...imageShaderProps }), _jsx(ImageShader, { image: highFreqImage, ...imageShaderProps })] }) }));
|
return (_jsx(Rect, { x: x, y: y, width: width, height: height, children: _jsxs(Shader, { source: effect, uniforms: uniforms, children: [_jsx(ImageShader, { image: originImage, ...imageShaderProps }), _jsx(ImageShader, { image: paintColorMap, ...imageShaderProps }), _jsx(ImageShader, { image: lowFreqImage, ...imageShaderProps }), _jsx(ImageShader, { image: highFreqImage, ...imageShaderProps })] }) }));
|
||||||
}
|
}
|
||||||
/** Canvas 内全屏上色 Shader 层 */
|
/** Canvas 内全屏上色 Shader 层 */
|
||||||
export function PaintShaderLayer(props) {
|
export function PaintShaderLayer(props) {
|
||||||
|
|||||||
2
dist/utils/paintShaderRuntime.js.map
vendored
2
dist/utils/paintShaderRuntime.js.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"paintShaderRuntime.js","sourceRoot":"","sources":["../../src/utils/paintShaderRuntime.tsx"],"names":[],"mappings":"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"}
|
{"version":3,"file":"paintShaderRuntime.js","sourceRoot":"","sources":["../../src/utils/paintShaderRuntime.tsx"],"names":[],"mappings":"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"}
|
||||||
@ -409,16 +409,8 @@ function App(): React.JSX.Element {
|
|||||||
initRegionFlashMs: 1000,
|
initRegionFlashMs: 1000,
|
||||||
enableInitRegionFlash: true,
|
enableInitRegionFlash: true,
|
||||||
}}
|
}}
|
||||||
showDebugPickers={false}
|
|
||||||
showToolbar={false}
|
|
||||||
showColorBar
|
|
||||||
showStatusRow={false}
|
|
||||||
showOverlayButtons
|
|
||||||
disabled={!isInteractive}
|
disabled={!isInteractive}
|
||||||
initialSession={sessionDraft ?? undefined}
|
initialSession={sessionDraft ?? undefined}
|
||||||
undoButtonText="撤销"
|
|
||||||
compareButtonText="对比原图"
|
|
||||||
compareExitButtonText="退出对比"
|
|
||||||
onWatch={handleWatch}
|
onWatch={handleWatch}
|
||||||
onPaintCallback={handlePaintCallback}
|
onPaintCallback={handlePaintCallback}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
@ -611,6 +603,7 @@ const styles = StyleSheet.create({
|
|||||||
canvasHost: {
|
canvasHost: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
height: 280,
|
||||||
},
|
},
|
||||||
canvas: {
|
canvas: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
@ -10,16 +10,9 @@ import React, {
|
|||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Button,
|
|
||||||
Dimensions,
|
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
|
||||||
ScrollView,
|
|
||||||
type GestureResponderEvent,
|
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||||
import { runOnJS } from 'react-native-reanimated';
|
import { runOnJS } from 'react-native-reanimated';
|
||||||
import { launchImageLibrary } from 'react-native-image-picker';
|
|
||||||
import cv from '../utils/opencvAdapter';
|
import cv from '../utils/opencvAdapter';
|
||||||
import {
|
import {
|
||||||
buildAllRegionOutlinePaths,
|
buildAllRegionOutlinePaths,
|
||||||
@ -92,11 +85,6 @@ export type {
|
|||||||
MaskSemanticColor,
|
MaskSemanticColor,
|
||||||
} from './MaskSegmentCanvas.types';
|
} from './MaskSegmentCanvas.types';
|
||||||
|
|
||||||
/* ==========================================================================
|
|
||||||
* 配置常量(屏幕相关;其余见 maskSegmentRuntime)
|
|
||||||
* ========================================================================== */
|
|
||||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
* 类型
|
* 类型
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
@ -166,6 +154,63 @@ function canvasToNormalized(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Skia matrix: pan → scale around viewport center. Matches screenToCanvasCoords. */
|
||||||
|
function buildZoomPanMatrix(
|
||||||
|
panX: number,
|
||||||
|
panY: number,
|
||||||
|
scale: number,
|
||||||
|
canvasW: number,
|
||||||
|
canvasH: number,
|
||||||
|
) {
|
||||||
|
const cx = canvasW / 2;
|
||||||
|
const cy = canvasH / 2;
|
||||||
|
const m = Skia.Matrix();
|
||||||
|
m.translate(panX, panY);
|
||||||
|
m.translate(cx, cy);
|
||||||
|
m.scale(scale, scale);
|
||||||
|
m.translate(-cx, -cy);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clamp pan so scaled containRect does not expose empty margins beyond the viewport. */
|
||||||
|
function clampPanOffset(
|
||||||
|
pan: { x: number; y: number },
|
||||||
|
scale: number,
|
||||||
|
canvasW: number,
|
||||||
|
canvasH: number,
|
||||||
|
containRect: ContainRect | null,
|
||||||
|
): { x: number; y: number } {
|
||||||
|
if (!containRect || scale <= 1 || canvasW <= 0 || canvasH <= 0) {
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cx = canvasW / 2;
|
||||||
|
const cy = canvasH / 2;
|
||||||
|
const r = containRect;
|
||||||
|
|
||||||
|
const scaledMinX = cx + scale * (r.x - cx);
|
||||||
|
const scaledMaxX = cx + scale * (r.x + r.w - cx);
|
||||||
|
const scaledMinY = cy + scale * (r.y - cy);
|
||||||
|
const scaledMaxY = cy + scale * (r.y + r.h - cy);
|
||||||
|
|
||||||
|
let x = pan.x;
|
||||||
|
let y = pan.y;
|
||||||
|
|
||||||
|
if (scaledMaxX - scaledMinX > canvasW) {
|
||||||
|
x = Math.max(canvasW - scaledMaxX, Math.min(-scaledMinX, x));
|
||||||
|
} else {
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scaledMaxY - scaledMinY > canvasH) {
|
||||||
|
y = Math.max(canvasH - scaledMaxY, Math.min(-scaledMinY, y));
|
||||||
|
} else {
|
||||||
|
y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inverse of the Skia Group transform applied during pinch-zoom.
|
* Inverse of the Skia Group transform applied during pinch-zoom.
|
||||||
* Converts a raw touch point (screen pixels) back to the canvas coordinate
|
* Converts a raw touch point (screen pixels) back to the canvas coordinate
|
||||||
@ -181,7 +226,6 @@ function screenToCanvasCoords(
|
|||||||
panOffset: { x: number; y: number },
|
panOffset: { x: number; y: number },
|
||||||
): { x: number; y: number } {
|
): { x: number; y: number } {
|
||||||
if (zoomScale <= 1) return { x: screenX, y: screenY };
|
if (zoomScale <= 1) return { x: screenX, y: screenY };
|
||||||
// Reverse: translate(-pan) → unscale around center → translate(+center)
|
|
||||||
return {
|
return {
|
||||||
x: (screenX - panOffset.x - canvasW / 2) / zoomScale + canvasW / 2,
|
x: (screenX - panOffset.x - canvasW / 2) / zoomScale + canvasW / 2,
|
||||||
y: (screenY - panOffset.y - canvasH / 2) / zoomScale + canvasH / 2,
|
y: (screenY - panOffset.y - canvasH / 2) / zoomScale + canvasH / 2,
|
||||||
@ -447,11 +491,6 @@ function lookupRegionFromPickMap(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** BGR → 屏幕 RGB */
|
|
||||||
function bgrToCss(b: number, g: number, r: number): string {
|
|
||||||
return `rgb(${r},${g},${b})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function releasePaintResourceLayers(layers: PaintResourceLayers | null) {
|
function releasePaintResourceLayers(layers: PaintResourceLayers | null) {
|
||||||
if (!layers) {
|
if (!layers) {
|
||||||
return;
|
return;
|
||||||
@ -519,24 +558,8 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
initialSession,
|
initialSession,
|
||||||
initialPaintColor,
|
initialPaintColor,
|
||||||
initialPaintConfigJson,
|
initialPaintConfigJson,
|
||||||
showDebugPickers = true,
|
|
||||||
showToolbar = true,
|
|
||||||
showColorBar = true,
|
|
||||||
showStatusRow = true,
|
|
||||||
showOverlayButtons = true,
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
style,
|
style,
|
||||||
canvasStyle,
|
|
||||||
maxHeight,
|
|
||||||
undoButtonStyle,
|
|
||||||
compareButtonStyle,
|
|
||||||
undoButtonTextStyle,
|
|
||||||
compareButtonTextStyle,
|
|
||||||
undoButtonText = '撤销',
|
|
||||||
compareButtonText = '对比原图',
|
|
||||||
compareExitButtonText = '退出对比',
|
|
||||||
renderUndoButton,
|
|
||||||
renderCompareButton,
|
|
||||||
onWatch,
|
onWatch,
|
||||||
onPaintCallback,
|
onPaintCallback,
|
||||||
onError,
|
onError,
|
||||||
@ -816,7 +839,6 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
const paintLayersPromiseRef = useRef<Promise<void> | null>(null);
|
const paintLayersPromiseRef = useRef<Promise<void> | null>(null);
|
||||||
const loadPaintLayersRef = useRef<() => Promise<void>>(() => Promise.resolve());
|
const loadPaintLayersRef = useRef<() => Promise<void>>(() => Promise.resolve());
|
||||||
const [paintResourcesReady, setPaintResourcesReady] = useState(false);
|
const [paintResourcesReady, setPaintResourcesReady] = useState(false);
|
||||||
const [layersLoading, setLayersLoading] = useState(false);
|
|
||||||
const [maskPathsReady, setMaskPathsReady] = useState(false);
|
const [maskPathsReady, setMaskPathsReady] = useState(false);
|
||||||
const baseboardPickMaskRef = useRef<Uint8Array | null>(null);
|
const baseboardPickMaskRef = useRef<Uint8Array | null>(null);
|
||||||
const kickRegionIdRef = useRef<number | null>(null);
|
const kickRegionIdRef = useRef<number | null>(null);
|
||||||
@ -839,115 +861,62 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
// idle segmentation / no-paint cases cheap.
|
// idle segmentation / no-paint cases cheap.
|
||||||
const [highResSnapshotEnabled, setHighResSnapshotEnabled] = useState(false);
|
const [highResSnapshotEnabled, setHighResSnapshotEnabled] = useState(false);
|
||||||
|
|
||||||
// Layout measurement for the root container of this component. Declared early so
|
// Viewport measured from canvasWrap onLayout — single source of truth for Skia,
|
||||||
// the canvasW/canvasH memos (which decide the viewport rect for zoom centering,
|
// gestures, containRect, and pan clamp (must match the actual touch target).
|
||||||
// containRect placement, clipping, and gesture coordinate mapping) can close over it.
|
const [viewportSize, setViewportSize] = useState<{ w: number; h: number } | null>(null);
|
||||||
// When the host passes a fitted frame (VisualizationScreen's canvasFrame with explicit
|
|
||||||
// w/h derived from safe area + aspect, or scheme cards), we size our internal Skia
|
|
||||||
// canvas + gesture layer + zoom transform to that exact allocated rect.
|
|
||||||
const [layoutWidth, setLayoutWidth] = useState<number | null>(null);
|
|
||||||
const [layoutHeight, setLayoutHeight] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const [segmentsReady, setSegmentsReady] = useState(false);
|
const [segmentsReady, setSegmentsReady] = useState(false);
|
||||||
const segmentsReadyRef = useRef(false);
|
const segmentsReadyRef = useRef(false);
|
||||||
const maskPathsReadyRef = useRef(false);
|
const maskPathsReadyRef = useRef(false);
|
||||||
const [canvasInteractive, setCanvasInteractive] = useState(false);
|
const [canvasInteractive, setCanvasInteractive] = useState(false);
|
||||||
const [segError, setSegError] = useState('');
|
|
||||||
const [compareMode, setCompareMode] = useState(false);
|
const [compareMode, setCompareMode] = useState(false);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const resegmentInFlightRef = useRef(false);
|
||||||
|
|
||||||
const [originSkImg, setOriginSkImg] = useState<SkImage | null>(null);
|
const [originSkImg, setOriginSkImg] = useState<SkImage | null>(null);
|
||||||
const originSkImgRef = useRef<SkImage | null>(null);
|
const originSkImgRef = useRef<SkImage | null>(null);
|
||||||
const lowFreqSkImg = paintResourceLayers?.lowFreqImage ?? null;
|
const lowFreqSkImg = paintResourceLayers?.lowFreqImage ?? null;
|
||||||
const highFreqSkImg = paintResourceLayers?.highFreqImage ?? null;
|
const highFreqSkImg = paintResourceLayers?.highFreqImage ?? null;
|
||||||
|
|
||||||
const canvasBaseW = SCREEN_WIDTH - 20;
|
const canvasW = viewportSize?.w ?? 1;
|
||||||
|
const canvasH = viewportSize?.h ?? 1;
|
||||||
|
const canvasLayoutReady =
|
||||||
|
viewportSize != null && viewportSize.w > 0 && viewportSize.h > 0;
|
||||||
|
|
||||||
// The "viewport" size for this canvas component: the rect inside which we place
|
// Refs synced to the latest viewport size so async callbacks read post-layout values.
|
||||||
// the contained image, apply the zoom Group transform (centered), clip, and receive
|
|
||||||
// gestures (tap + two-finger pinch). When we have a real onLayout from the host
|
|
||||||
// (VisualizationScreen's aspect-fitted canvasFrame, or scheme card preview area),
|
|
||||||
// we use the *allocated pixel size* directly as our viewport.
|
|
||||||
//
|
|
||||||
// Accurate canvasW/H is still critical for:
|
|
||||||
// - Correct centering of the scaled content around the viewport center.
|
|
||||||
// - Proper containRect (letterbox/centering of the source photo inside the viewport).
|
|
||||||
// - Clip rect and gesture-to-canvas coordinate conversion used by painting.
|
|
||||||
//
|
|
||||||
// Previously the code fell back to aspect-derived sizes even after layout, which
|
|
||||||
// could cause the effective viewport to not match the host frame. Using the onLayout
|
|
||||||
// result (with maxHeight fallback only when no layout yet) keeps zoom, clip, and
|
|
||||||
// touch mapping consistent with what the user actually sees and touches.
|
|
||||||
const viewportW = useMemo(() => {
|
|
||||||
if (layoutWidth != null && layoutHeight != null) {
|
|
||||||
// Primary path for viz screen and scheme cards: the exact size the host
|
|
||||||
// decided for this component (after its own safe-area + aspect fit).
|
|
||||||
return layoutWidth;
|
|
||||||
}
|
|
||||||
if (!maxHeight || maxHeight <= 0) {
|
|
||||||
return canvasBaseW;
|
|
||||||
}
|
|
||||||
// Fallback (no layout yet, or other usages that pass maxHeight without a
|
|
||||||
// tightly sized parent frame). Replicate a contain-style budget.
|
|
||||||
const availableW = canvasBaseW;
|
|
||||||
let auxHeight = 0;
|
|
||||||
if (showToolbar) auxHeight += 40;
|
|
||||||
if (showStatusRow) auxHeight += 30;
|
|
||||||
if (showColorBar) auxHeight += 70;
|
|
||||||
const availableH = Math.max(100, maxHeight - 20 - auxHeight);
|
|
||||||
const imgAspect = imageSize ? imageSize.w / imageSize.h : 1;
|
|
||||||
const containerAspect = availableW / availableH;
|
|
||||||
if (containerAspect > imgAspect) {
|
|
||||||
return Math.floor(availableH * imgAspect);
|
|
||||||
}
|
|
||||||
return availableW;
|
|
||||||
}, [layoutWidth, layoutHeight, maxHeight, showToolbar, showStatusRow, showColorBar, canvasBaseW, imageSize]);
|
|
||||||
|
|
||||||
const viewportH = useMemo(() => {
|
|
||||||
if (layoutWidth != null && layoutHeight != null) {
|
|
||||||
return layoutHeight;
|
|
||||||
}
|
|
||||||
if (!maxHeight || maxHeight <= 0) {
|
|
||||||
const imgAspect = imageSize ? imageSize.w / imageSize.h : 1;
|
|
||||||
return Math.floor(viewportW / imgAspect);
|
|
||||||
}
|
|
||||||
let auxHeight = 0;
|
|
||||||
if (showToolbar) auxHeight += 40;
|
|
||||||
if (showStatusRow) auxHeight += 30;
|
|
||||||
if (showColorBar) auxHeight += 70;
|
|
||||||
return Math.max(100, maxHeight - 20 - auxHeight);
|
|
||||||
}, [layoutWidth, layoutHeight, maxHeight, showToolbar, showStatusRow, showColorBar, viewportW, imageSize]);
|
|
||||||
|
|
||||||
// For the rest of the component, "canvasW/H" means the viewport rect size.
|
|
||||||
// All zoom center, wrap size, touch layer, Canvas size, clip, containRect, and
|
|
||||||
// gesture coordinate mapping are based on this, so that two-finger zoom centering
|
|
||||||
// and single-finger tap painting stay consistent with the host-allocated area.
|
|
||||||
const canvasW = viewportW;
|
|
||||||
const canvasH = viewportH;
|
|
||||||
|
|
||||||
// Refs synced to the latest viewport size so that async callbacks
|
|
||||||
// (segmentAndPrepareLayers) always read post-layout values instead of
|
|
||||||
// stale closure captures. This fixes dashed-outline offset to the bottom
|
|
||||||
// when the initial pathMapRect was computed with fallback SCREEN_WIDTH.
|
|
||||||
// Declared before segmentAndPrepareLayers so the async body can reference them.
|
|
||||||
const canvasWRef = useRef(canvasW);
|
const canvasWRef = useRef(canvasW);
|
||||||
const canvasHRef = useRef(canvasH);
|
const canvasHRef = useRef(canvasH);
|
||||||
|
|
||||||
// ── Pinch-zoom (two-finger only; single-finger drag/pan disabled) ─────────
|
// ── Pinch-zoom (focal-point) + single-finger pan when zoomed ─────────────
|
||||||
const [zoomScale, setZoomScale] = useState(1);
|
const [zoomScale, setZoomScale] = useState(1);
|
||||||
const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
|
const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
// Refs for gesture callbacks (closures don't capture fresh state mid-gesture)
|
// Refs for gesture callbacks (closures don't capture fresh state mid-gesture)
|
||||||
const zoomScaleRef = useRef(1);
|
const zoomScaleRef = useRef(1);
|
||||||
const panOffsetRef = useRef({ x: 0, y: 0 });
|
const panOffsetRef = useRef({ x: 0, y: 0 });
|
||||||
// Baseline value captured at gesture start to avoid jump on re-creation for pinch
|
const pinchBaseScaleRef = useRef(1);
|
||||||
const zoomBaseRef = 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).
|
// Ref to the latest containRect (the actual placed photo rect inside the viewport).
|
||||||
const containRectRef = useRef<ContainRect | null>(null);
|
const containRectRef = useRef<ContainRect | null>(null);
|
||||||
|
|
||||||
useEffect(() => { zoomScaleRef.current = zoomScale; }, [zoomScale]);
|
useEffect(() => { zoomScaleRef.current = zoomScale; }, [zoomScale]);
|
||||||
useEffect(() => { panOffsetRef.current = panOffset; }, [panOffset]);
|
useEffect(() => { panOffsetRef.current = panOffset; }, [panOffset]);
|
||||||
|
|
||||||
|
const handleCanvasWrapLayout = useCallback((width: number, height: number) => {
|
||||||
|
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(() => {
|
const resetZoom = useCallback(() => {
|
||||||
setZoomScale(1);
|
setZoomScale(1);
|
||||||
setPanOffset({ x: 0, y: 0 });
|
setPanOffset({ x: 0, y: 0 });
|
||||||
@ -1067,7 +1036,6 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
timeLog('▶ start segmentation');
|
timeLog('▶ start segmentation');
|
||||||
segmentsReadyRef.current = false;
|
segmentsReadyRef.current = false;
|
||||||
setSegmentsReady(false);
|
setSegmentsReady(false);
|
||||||
setSegError('');
|
|
||||||
setActiveBrushIndex(null);
|
setActiveBrushIndex(null);
|
||||||
const clearedPainted = new Map<number, BgrColor>();
|
const clearedPainted = new Map<number, BgrColor>();
|
||||||
paintedRegionsRef.current = clearedPainted;
|
paintedRegionsRef.current = clearedPainted;
|
||||||
@ -1099,7 +1067,6 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
setRegionOutlinePaths(new Map());
|
setRegionOutlinePaths(new Map());
|
||||||
setMaskPathsReady(false);
|
setMaskPathsReady(false);
|
||||||
setPaintResourcesReady(false);
|
setPaintResourcesReady(false);
|
||||||
setLayersLoading(false);
|
|
||||||
baseboardPickMaskRef.current = null;
|
baseboardPickMaskRef.current = null;
|
||||||
kickRegionIdRef.current = null;
|
kickRegionIdRef.current = null;
|
||||||
maskPathsContainRectRef.current = null;
|
maskPathsContainRectRef.current = null;
|
||||||
@ -1315,7 +1282,6 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
if (!isCancelled()) {
|
if (!isCancelled()) {
|
||||||
console.error('[SDK-SEGMENT] segmentation failed', e);
|
console.error('[SDK-SEGMENT] segmentation failed', e);
|
||||||
setSegError(msg);
|
|
||||||
reportError(msg, e);
|
reportError(msg, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1342,7 +1308,6 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
const runId = segmentRunIdRef.current;
|
const runId = segmentRunIdRef.current;
|
||||||
let promise!: Promise<void>;
|
let promise!: Promise<void>;
|
||||||
promise = (async () => {
|
promise = (async () => {
|
||||||
setLayersLoading(true);
|
|
||||||
timeLog('▶ start loading paint shader textures');
|
timeLog('▶ start loading paint shader textures');
|
||||||
try {
|
try {
|
||||||
const result = await preparePaintResourcesFromWorkBuffer(
|
const result = await preparePaintResourcesFromWorkBuffer(
|
||||||
@ -1386,7 +1351,6 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
console.warn('[MaskSegment] failed to prepare paint shader textures', error);
|
console.warn('[MaskSegment] failed to prepare paint shader textures', error);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLayersLoading(false);
|
|
||||||
if (paintLayersPromiseRef.current === promise) {
|
if (paintLayersPromiseRef.current === promise) {
|
||||||
paintLayersPromiseRef.current = null;
|
paintLayersPromiseRef.current = null;
|
||||||
}
|
}
|
||||||
@ -1398,32 +1362,12 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
|
|
||||||
loadPaintLayersRef.current = loadPaintLayersIfNeeded;
|
loadPaintLayersRef.current = loadPaintLayersIfNeeded;
|
||||||
|
|
||||||
const pickOriginImage = async () => {
|
|
||||||
const res = await launchImageLibrary({ mediaType: 'photo' });
|
|
||||||
const uri = res.assets?.[0]?.uri;
|
|
||||||
if (!uri) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pngPath = await cv.ensurePngPath(uri, `picked_origin_${Date.now()}.png`);
|
|
||||||
setOriginImgPath(pngPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
const pickMaskImage = async () => {
|
|
||||||
const res = await launchImageLibrary({ mediaType: 'photo' });
|
|
||||||
const uri = res.assets?.[0]?.uri;
|
|
||||||
if (!uri) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pngPath = await cv.ensurePngPath(uri, `picked_mask_${Date.now()}.png`);
|
|
||||||
setMaskImgPath(pngPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearCacheAndResegment = useCallback(async () => {
|
const clearCacheAndResegment = useCallback(async () => {
|
||||||
if (isRefreshing || !originImgPath || !maskImgPath) {
|
if (resegmentInFlightRef.current || !originImgPath || !maskImgPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsRefreshing(true);
|
resegmentInFlightRef.current = true;
|
||||||
try {
|
try {
|
||||||
resetZoom();
|
resetZoom();
|
||||||
const layers = paintResourceLayersRef.current;
|
const layers = paintResourceLayersRef.current;
|
||||||
@ -1438,13 +1382,11 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
await segmentAndPrepareLayers(originImgPath, maskImgPath);
|
await segmentAndPrepareLayers(originImgPath, maskImgPath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
setSegError(msg);
|
|
||||||
reportError(msg, e);
|
reportError(msg, e);
|
||||||
} finally {
|
} finally {
|
||||||
setIsRefreshing(false);
|
resegmentInFlightRef.current = false;
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isRefreshing,
|
|
||||||
originImgPath,
|
originImgPath,
|
||||||
maskImgPath,
|
maskImgPath,
|
||||||
segmentAndPrepareLayers,
|
segmentAndPrepareLayers,
|
||||||
@ -1465,6 +1407,7 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetZoom();
|
||||||
segmentInFlightKeyRef.current = segmentKey;
|
segmentInFlightKeyRef.current = segmentKey;
|
||||||
void segmentAndPrepareLayers(originImgPath, maskImgPath).finally(() => {
|
void segmentAndPrepareLayers(originImgPath, maskImgPath).finally(() => {
|
||||||
if (segmentInFlightKeyRef.current === segmentKey) {
|
if (segmentInFlightKeyRef.current === segmentKey) {
|
||||||
@ -1497,7 +1440,7 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
}
|
}
|
||||||
setInitFlashRegionId(null);
|
setInitFlashRegionId(null);
|
||||||
};
|
};
|
||||||
}, [originImgPath, maskImgPath]);
|
}, [originImgPath, maskImgPath, resetZoom]);
|
||||||
|
|
||||||
const buildPaintedRecords = useCallback((): PaintedRegionRecord[] => {
|
const buildPaintedRecords = useCallback((): PaintedRegionRecord[] => {
|
||||||
const records: PaintedRegionRecord[] = [];
|
const records: PaintedRegionRecord[] = [];
|
||||||
@ -1837,17 +1780,6 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
[segmentsReady, imageSize, canvasW, canvasH],
|
[segmentsReady, imageSize, canvasW, canvasH],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectBrushColor = useCallback(
|
|
||||||
(brushIndex: number) => {
|
|
||||||
onUserInteraction();
|
|
||||||
setCustomPaintColor(null);
|
|
||||||
customPaintConfigJsonRef.current = undefined;
|
|
||||||
setActiveBrushIndex(brushIndex);
|
|
||||||
void loadPaintLayersIfNeeded();
|
|
||||||
},
|
|
||||||
[onUserInteraction, loadPaintLayersIfNeeded],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onCanvasTap = useCallback(
|
const onCanvasTap = useCallback(
|
||||||
(x: number, y: number) => {
|
(x: number, y: number) => {
|
||||||
onUserInteraction();
|
onUserInteraction();
|
||||||
@ -2244,20 +2176,19 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Zoom transform for the Skia Group ─────────────────────────────────
|
// ── Zoom matrix for the Skia Group (pan + scale around center) ──────────
|
||||||
// Note: single-finger pan/drag is disabled. Zoom is always centered around the
|
const zoomMatrix = useMemo(() => {
|
||||||
// viewport center (no additional panOffset translation). The panOffset state
|
if (zoomScale <= 1) {
|
||||||
// is kept (and forced to 0 during pinch) for compatibility with screenToCanvasCoords
|
return undefined;
|
||||||
// inverse mapping used by tap/paint logic, and for resetZoom.
|
}
|
||||||
const zoomTransform = useMemo(() => {
|
return buildZoomPanMatrix(
|
||||||
if (zoomScale <= 1) return undefined;
|
panOffset.x,
|
||||||
return [
|
panOffset.y,
|
||||||
{ translateX: 0, translateY: 0 }, // panning disabled; content stays centered when zoomed
|
zoomScale,
|
||||||
{ translateX: canvasW / 2, translateY: canvasH / 2 },
|
canvasW,
|
||||||
{ scale: zoomScale },
|
canvasH,
|
||||||
{ translateX: -canvasW / 2, translateY: -canvasH / 2 },
|
);
|
||||||
];
|
}, [zoomScale, panOffset, canvasW, canvasH]);
|
||||||
}, [zoomScale, canvasW, canvasH]);
|
|
||||||
|
|
||||||
const renderDraw = () => {
|
const renderDraw = () => {
|
||||||
const displayImg = originSkImg ?? lowFreqSkImg;
|
const displayImg = originSkImg ?? lowFreqSkImg;
|
||||||
@ -2293,7 +2224,7 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
The clip keeps drawing contained within the logical viewport and
|
The clip keeps drawing contained within the logical viewport and
|
||||||
prevents shader content from leaking outside during zoom. */}
|
prevents shader content from leaking outside during zoom. */}
|
||||||
<Group clip={Skia.XYWHRect(0, 0, canvasW, canvasH)}>
|
<Group clip={Skia.XYWHRect(0, 0, canvasW, canvasH)}>
|
||||||
<Group transform={zoomTransform}>
|
<Group matrix={zoomMatrix}>
|
||||||
{useShader && shaderOrigin ? (
|
{useShader && shaderOrigin ? (
|
||||||
<PaintShaderLayer
|
<PaintShaderLayer
|
||||||
originImage={shaderOrigin}
|
originImage={shaderOrigin}
|
||||||
@ -2465,21 +2396,54 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
[], // 仅创建一次手势对象;所有回调都通过 Ref 读取最新值,避免初始化期间重复创建导致 Reanimated 节点冲突
|
[], // 仅创建一次手势对象;所有回调都通过 Ref 读取最新值,避免初始化期间重复创建导致 Reanimated 节点冲突
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Gesture: pinch-zoom (two-finger scale; max 5×) ─────────────────────
|
// ── Gesture: pinch-zoom (focal-point scale + two-finger pan; max 5×) ────
|
||||||
const pinchGesture = useMemo(
|
const pinchGesture = useMemo(
|
||||||
() => {
|
() => {
|
||||||
const onStartJS = () => {
|
const onStartJS = (focalX: number, focalY: number) => {
|
||||||
zoomBaseRef.current = zoomScaleRef.current;
|
pinchBaseScaleRef.current = zoomScaleRef.current;
|
||||||
|
pinchBasePanRef.current = { ...panOffsetRef.current };
|
||||||
|
pinchBaseFocalRef.current = { x: focalX, y: focalY };
|
||||||
};
|
};
|
||||||
const onUpdateJS = (scale: number) => {
|
const onUpdateJS = (scale: number, focalX: number, focalY: number) => {
|
||||||
const newScale = Math.max(1, Math.min(zoomBaseRef.current * scale, 5));
|
const cw = canvasWRef.current;
|
||||||
|
const ch = canvasHRef.current;
|
||||||
|
if (cw <= 0 || ch <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cx = cw / 2;
|
||||||
|
const cy = ch / 2;
|
||||||
|
|
||||||
|
const baseScale = pinchBaseScaleRef.current;
|
||||||
|
const basePan = pinchBasePanRef.current;
|
||||||
|
const baseFocal = pinchBaseFocalRef.current;
|
||||||
|
|
||||||
|
let newScale = Math.max(1, Math.min(baseScale * scale, 5));
|
||||||
|
|
||||||
|
const anchorX = (baseFocal.x - basePan.x - cx) / baseScale + cx;
|
||||||
|
const anchorY = (baseFocal.y - basePan.y - cy) / baseScale + cy;
|
||||||
|
|
||||||
|
let newPan = {
|
||||||
|
x: focalX - cx - newScale * (anchorX - cx),
|
||||||
|
y: focalY - cy - newScale * (anchorY - cy),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newScale <= 1) {
|
||||||
|
newScale = 1;
|
||||||
|
newPan = { x: 0, y: 0 };
|
||||||
|
} else {
|
||||||
|
newPan = clampPanOffset(
|
||||||
|
newPan,
|
||||||
|
newScale,
|
||||||
|
cw,
|
||||||
|
ch,
|
||||||
|
containRectRef.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setZoomScale(newScale);
|
setZoomScale(newScale);
|
||||||
|
setPanOffset(newPan);
|
||||||
zoomScaleRef.current = newScale;
|
zoomScaleRef.current = newScale;
|
||||||
// Lock pan offset to zero: only two-finger pinch zoom is supported.
|
panOffsetRef.current = newPan;
|
||||||
// Single-finger drag/pan after zoom is intentionally disabled per product decision.
|
|
||||||
// Zoom is always centered; no additional translate from panOffset is applied in practice.
|
|
||||||
setPanOffset({ x: 0, y: 0 });
|
|
||||||
panOffsetRef.current = { x: 0, y: 0 };
|
|
||||||
};
|
};
|
||||||
const onEndJS = () => {
|
const onEndJS = () => {
|
||||||
if (zoomScaleRef.current <= 1.01) {
|
if (zoomScaleRef.current <= 1.01) {
|
||||||
@ -2488,211 +2452,103 @@ const MaskSegmentCanvas = forwardRef<MaskSegmentCanvasRef, MaskSegmentCanvasProp
|
|||||||
};
|
};
|
||||||
|
|
||||||
return Gesture.Pinch()
|
return Gesture.Pinch()
|
||||||
.onStart(() => {
|
.onStart((e) => {
|
||||||
'worklet';
|
'worklet';
|
||||||
runOnJS(onStartJS)();
|
runOnJS(onStartJS)(e.focalX, e.focalY);
|
||||||
})
|
})
|
||||||
.onUpdate((e) => {
|
.onUpdate((e) => {
|
||||||
'worklet';
|
'worklet';
|
||||||
runOnJS(onUpdateJS)(e.scale);
|
runOnJS(onUpdateJS)(e.scale, e.focalX, e.focalY);
|
||||||
})
|
})
|
||||||
.onEnd(() => {
|
.onEnd(() => {
|
||||||
'worklet';
|
'worklet';
|
||||||
runOnJS(onEndJS)();
|
runOnJS(onEndJS)();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[], // 仅创建一次;通过 Ref 访问 resetZoom
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Composed: Race ensures single-tap and pinch-zoom never conflict ─────────
|
// ── Gesture: single-finger pan (active only when zoomed) ───────────────
|
||||||
// 依赖置空 + 内部的 tap/pinch 也用 [],保证整个手势描述符树只在 mount 时创建一次。
|
const panGesture = useMemo(
|
||||||
// 避免初始化阶段反复创建导致 Reanimated / RNGH 内部节点重复分配。
|
() => {
|
||||||
|
const onStartJS = () => {
|
||||||
|
panBaseRef.current = { ...panOffsetRef.current };
|
||||||
|
};
|
||||||
|
const onUpdateJS = (translationX: number, translationY: number) => {
|
||||||
|
if (zoomScaleRef.current <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newPan = clampPanOffset(
|
||||||
|
{
|
||||||
|
x: panBaseRef.current.x + translationX,
|
||||||
|
y: panBaseRef.current.y + translationY,
|
||||||
|
},
|
||||||
|
zoomScaleRef.current,
|
||||||
|
canvasWRef.current,
|
||||||
|
canvasHRef.current,
|
||||||
|
containRectRef.current,
|
||||||
|
);
|
||||||
|
setPanOffset(newPan);
|
||||||
|
panOffsetRef.current = newPan;
|
||||||
|
};
|
||||||
|
const onEndJS = () => {
|
||||||
|
if (zoomScaleRef.current <= 1.01) {
|
||||||
|
resetZoomRef.current?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Gesture.Pan()
|
||||||
|
.minPointers(1)
|
||||||
|
.maxPointers(1)
|
||||||
|
.minDistance(10)
|
||||||
|
.onStart(() => {
|
||||||
|
'worklet';
|
||||||
|
runOnJS(onStartJS)();
|
||||||
|
})
|
||||||
|
.onUpdate((e) => {
|
||||||
|
'worklet';
|
||||||
|
runOnJS(onUpdateJS)(e.translationX, e.translationY);
|
||||||
|
})
|
||||||
|
.onEnd(() => {
|
||||||
|
'worklet';
|
||||||
|
runOnJS(onEndJS)();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Composed: pinch + pan simultaneous; tap only when neither claims ───
|
||||||
const composedGesture = useMemo(
|
const composedGesture = useMemo(
|
||||||
() => Gesture.Race(tapGesture, pinchGesture),
|
() =>
|
||||||
|
Gesture.Exclusive(
|
||||||
|
Gesture.Simultaneous(pinchGesture, panGesture),
|
||||||
|
tapGesture,
|
||||||
|
),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View style={[styles.container, style]}>
|
||||||
style={[styles.container, style]}
|
|
||||||
onLayout={(e) => {
|
|
||||||
const { width, height } = e.nativeEvent.layout;
|
|
||||||
setLayoutWidth(width);
|
|
||||||
setLayoutHeight(height);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Only render the internal ScrollView (and its control bars) when any of
|
|
||||||
the auxiliary UI rows are requested. In the main VisualizationScreen
|
|
||||||
(and scheme card live previews) all of showToolbar/showDebug/showStatus/showColorBar
|
|
||||||
are false, so we omit this entirely.
|
|
||||||
|
|
||||||
This conditional is still important for gesture integrity:
|
|
||||||
- An always-mounted vertical ScrollView can participate in the responder system
|
|
||||||
and steal touches (vertical moves, or even interfere with two-finger pinch).
|
|
||||||
- The GestureDetector (for tap/pinch) is a sibling after the ScrollView in the
|
|
||||||
tree when the ScrollView is rendered, so it may not receive the intended gestures.
|
|
||||||
- By omitting the ScrollView when unused, the canvas touch layer + GestureDetector
|
|
||||||
become direct children and reliably receive single-finger taps and two-finger pinches. */}
|
|
||||||
{(showToolbar || showDebugPickers || showStatusRow || showColorBar) ? (
|
|
||||||
<ScrollView
|
|
||||||
style={styles.scroll}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.scrollContent}
|
|
||||||
keyboardShouldPersistTaps="always"
|
|
||||||
>
|
|
||||||
{showToolbar && (
|
|
||||||
<View style={styles.toolbarRow}>
|
|
||||||
<Button
|
|
||||||
title={isRefreshing ? 're-segmenting…' : 'clear cache and re-segment'}
|
|
||||||
onPress={clearCacheAndResegment}
|
|
||||||
disabled={isRefreshing || !originImgPath || !maskImgPath}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showDebugPickers && (
|
|
||||||
<View style={styles.btnRow}>
|
|
||||||
<Button title="pick origin image" onPress={pickOriginImage} />
|
|
||||||
<View style={styles.btnGap} />
|
|
||||||
<Button title="pick mask image" onPress={pickMaskImage} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showStatusRow && (
|
|
||||||
<View style={styles.statusRow}>
|
|
||||||
{segError ? (
|
|
||||||
<Text style={styles.err}>❌ {segError}</Text>
|
|
||||||
) : !segmentsReady ? (
|
|
||||||
<Text style={styles.hint}>⏳ segmenting…</Text>
|
|
||||||
) : layersLoading || !paintResourcesReady ? (
|
|
||||||
<Text style={styles.hint}>
|
|
||||||
✅ {regionCount} regions · Shader textures preparing…
|
|
||||||
</Text>
|
|
||||||
) : !(originSkImg ?? lowFreqSkImg) ? (
|
|
||||||
<Text style={styles.hint}>⏳ image loading…</Text>
|
|
||||||
) : (
|
|
||||||
<Text style={styles.hint}>
|
|
||||||
✅ {regionCount} regions · painted {paintedRegions.size} ·{' '}
|
|
||||||
{hasActiveBrush
|
|
||||||
? customPaintColor
|
|
||||||
? 'custom paint color'
|
|
||||||
: `paint color ${(activeBrushIndex ?? 0) + 1}`
|
|
||||||
: 'please select the bottom paint color first'}
|
|
||||||
{' · '}
|
|
||||||
{compareMode ? 'compare original image' : 'paint mode'}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showColorBar && (
|
|
||||||
<View style={styles.colorBar}>
|
|
||||||
<Text style={styles.colorBarLabel}>
|
|
||||||
paint color (tap to select, then tap canvas to paint)
|
|
||||||
</Text>
|
|
||||||
<View style={styles.colorSwatches}>
|
|
||||||
{paintPalette.map((color, index) => {
|
|
||||||
const isActive =
|
|
||||||
activeBrushIndex === index && customPaintColor == null;
|
|
||||||
const { b, g, r } = color;
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={index}
|
|
||||||
style={[
|
|
||||||
styles.colorSwatch,
|
|
||||||
{ backgroundColor: bgrToCss(b, g, r) },
|
|
||||||
isActive && styles.colorSwatchSelected,
|
|
||||||
]}
|
|
||||||
activeOpacity={0.8}
|
|
||||||
disabled={!segmentsReady || disabled}
|
|
||||||
onPress={() => selectBrushColor(index)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{!segmentsReady && (
|
|
||||||
<Text style={styles.colorBarEmpty}>loading…</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
{customPaintColor && (
|
|
||||||
<Text style={styles.hint}>
|
|
||||||
current custom paint color is set by ref.setPaintColor
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={[styles.canvasOuter, canvasStyle]}
|
style={styles.canvasWrap}
|
||||||
|
onLayout={(e) => {
|
||||||
|
const { width, height } = e.nativeEvent.layout;
|
||||||
|
handleCanvasWrapLayout(width, height);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<View
|
{canvasLayoutReady ? (
|
||||||
style={[styles.canvasWrap, { width: canvasW, height: canvasH }]}
|
|
||||||
>
|
|
||||||
<GestureDetector gesture={composedGesture}>
|
<GestureDetector gesture={composedGesture}>
|
||||||
<View style={[styles.canvasTouchLayer, { width: canvasW, height: canvasH }]}>
|
<View style={{ width: canvasW, height: canvasH }}>
|
||||||
<Canvas style={{ width: canvasW, height: canvasH }} pointerEvents="none">
|
<Canvas
|
||||||
|
style={{ width: canvasW, height: canvasH }}
|
||||||
|
pointerEvents="none"
|
||||||
|
>
|
||||||
{renderDraw()}
|
{renderDraw()}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</View>
|
</View>
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
|
) : null}
|
||||||
{showOverlayButtons &&
|
|
||||||
(renderUndoButton ? (
|
|
||||||
renderUndoButton({
|
|
||||||
onPress: undoSelection,
|
|
||||||
disabled: paintHistory.length === 0,
|
|
||||||
text: undoButtonText,
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.overlayBtn, styles.btnBottomLeft, undoButtonStyle]}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
disabled={paintHistory.length === 0 || disabled}
|
|
||||||
onPress={undoSelection}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.btnText,
|
|
||||||
undoButtonTextStyle,
|
|
||||||
{ opacity: paintHistory.length === 0 ? 0.4 : 1 },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{undoButtonText}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{showOverlayButtons &&
|
|
||||||
(renderCompareButton ? (
|
|
||||||
renderCompareButton({
|
|
||||||
onPress: () => {
|
|
||||||
onUserInteraction();
|
|
||||||
setCompareMode(v => !v);
|
|
||||||
},
|
|
||||||
text: compareMode ? compareExitButtonText : compareButtonText,
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[
|
|
||||||
styles.overlayBtn,
|
|
||||||
styles.btnBottomRight,
|
|
||||||
compareButtonStyle,
|
|
||||||
]}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
disabled={disabled}
|
|
||||||
onPress={() => {
|
|
||||||
onUserInteraction();
|
|
||||||
setCompareMode(v => !v);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={[styles.btnText, compareButtonTextStyle]}>
|
|
||||||
{compareMode ? compareExitButtonText : compareButtonText}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{highResSnapshotEnabled && exportCanvasSize ? (
|
{highResSnapshotEnabled && exportCanvasSize ? (
|
||||||
@ -2728,123 +2584,16 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
},
|
},
|
||||||
scroll: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
scrollContent: {
|
|
||||||
padding: 10,
|
|
||||||
paddingBottom: 28,
|
|
||||||
},
|
|
||||||
toolbarRow: {
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
btnRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
btnGap: {
|
|
||||||
width: 10,
|
|
||||||
},
|
|
||||||
statusRow: {
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
hint: {
|
|
||||||
color: '#333',
|
|
||||||
fontSize: 13,
|
|
||||||
},
|
|
||||||
err: {
|
|
||||||
color: '#c33',
|
|
||||||
fontSize: 13,
|
|
||||||
},
|
|
||||||
canvasWrap: {
|
canvasWrap: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
alignSelf: 'stretch',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: '#f5f5f5',
|
||||||
borderWidth: StyleSheet.hairlineWidth,
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
borderColor: '#ddd',
|
borderColor: '#ddd',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
canvasOuter: {
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
// When the internal control ScrollView is omitted (the normal case for
|
|
||||||
// VisualizationScreen and non-interactive scheme previews), this makes the
|
|
||||||
// canvas area fill the bounds provided by the host (via style + maxHeight)
|
|
||||||
// and centers the fixed-size image rect. This also ensures the
|
|
||||||
// GestureDetector sits in the right place to receive taps and two-finger pinches.
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
canvasTouchLayer: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
canvas: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
overlayBtn: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 10,
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
paddingVertical: 7,
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.62)',
|
|
||||||
borderRadius: 6,
|
|
||||||
},
|
|
||||||
btnBottomLeft: {
|
|
||||||
left: 10,
|
|
||||||
},
|
|
||||||
btnBottomRight: {
|
|
||||||
right: 10,
|
|
||||||
},
|
|
||||||
btnText: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
colorBar: {
|
|
||||||
marginTop: 12,
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 4,
|
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
|
||||||
borderTopColor: '#e0e0e0',
|
|
||||||
},
|
|
||||||
colorBarLabel: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: '#666',
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
colorSwatches: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
colorSwatch: {
|
|
||||||
width: 44,
|
|
||||||
height: 44,
|
|
||||||
borderRadius: 22,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: 'rgba(0,0,0,0.12)',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
colorSwatchSelected: {
|
|
||||||
borderColor: '#1e96ff',
|
|
||||||
borderWidth: 3,
|
|
||||||
transform: [{ scale: 1.08 }],
|
|
||||||
},
|
|
||||||
colorSwatchPainted: {
|
|
||||||
borderColor: 'rgba(255,255,255,0.9)',
|
|
||||||
},
|
|
||||||
colorSwatchDot: {
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.92)',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(0,0,0,0.25)',
|
|
||||||
},
|
|
||||||
colorBarEmpty: {
|
|
||||||
fontSize: 13,
|
|
||||||
color: '#999',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default MaskSegmentCanvas;
|
export default MaskSegmentCanvas;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { StyleProp, ViewStyle } from 'react-native';
|
||||||
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
|
||||||
import type { SegmentRegion } from '../utils/maskSegmentation';
|
import type { SegmentRegion } from '../utils/maskSegmentation';
|
||||||
import type { MaskSemanticColor } from '../utils/maskSemanticPalette';
|
import type { MaskSemanticColor } from '../utils/maskSemanticPalette';
|
||||||
|
|
||||||
@ -139,12 +138,6 @@ export type PaintBrushRequiredPayload = {
|
|||||||
|
|
||||||
export type PaintCallbackPayload = PaintSuccessPayload | PaintBrushRequiredPayload;
|
export type PaintCallbackPayload = PaintSuccessPayload | PaintBrushRequiredPayload;
|
||||||
|
|
||||||
export type OverlayButtonRenderProps = {
|
|
||||||
onPress: () => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MaskSegmentCanvasRef = {
|
export type MaskSegmentCanvasRef = {
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
swap: (showOrigin?: boolean) => void;
|
swap: (showOrigin?: boolean) => void;
|
||||||
@ -183,30 +176,14 @@ export type MaskSegmentCanvasProps = {
|
|||||||
initialSession?: MaskSegmentSession;
|
initialSession?: MaskSegmentSession;
|
||||||
initialPaintColor?: BgrColor;
|
initialPaintColor?: BgrColor;
|
||||||
initialPaintConfigJson?: Record<string, unknown>;
|
initialPaintConfigJson?: Record<string, unknown>;
|
||||||
showDebugPickers?: boolean;
|
|
||||||
showToolbar?: boolean;
|
|
||||||
showColorBar?: boolean;
|
|
||||||
showStatusRow?: boolean;
|
|
||||||
showOverlayButtons?: boolean;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
style?: StyleProp<ViewStyle>;
|
style?: StyleProp<ViewStyle>;
|
||||||
canvasStyle?: StyleProp<ViewStyle>;
|
|
||||||
/**
|
/**
|
||||||
* Max container height available for this component (px). When set, the SDK
|
* Max container height available for this component (px). When set, the SDK
|
||||||
* computes canvas dimensions as a fit-contain within (screenWidth - 20, maxHeight)
|
* computes canvas dimensions as a fit-contain within (screenWidth - 20, maxHeight)
|
||||||
* instead of using the full image aspect at full screen width. This prevents
|
* instead of using the full image aspect at full screen width.
|
||||||
* internal ScrollView scrolling for tall images.
|
|
||||||
*/
|
*/
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
undoButtonStyle?: StyleProp<ViewStyle>;
|
|
||||||
compareButtonStyle?: StyleProp<ViewStyle>;
|
|
||||||
undoButtonTextStyle?: StyleProp<TextStyle>;
|
|
||||||
compareButtonTextStyle?: StyleProp<TextStyle>;
|
|
||||||
undoButtonText?: string;
|
|
||||||
compareButtonText?: string;
|
|
||||||
compareExitButtonText?: string;
|
|
||||||
renderUndoButton?: (props: OverlayButtonRenderProps) => ReactNode;
|
|
||||||
renderCompareButton?: (props: OverlayButtonRenderProps) => ReactNode;
|
|
||||||
onWatch?: (
|
onWatch?: (
|
||||||
state: MaskSegmentWatchState,
|
state: MaskSegmentWatchState,
|
||||||
durationMs: number,
|
durationMs: number,
|
||||||
|
|||||||
@ -9,7 +9,6 @@ export type {
|
|||||||
MaskSegmentWatchDetail,
|
MaskSegmentWatchDetail,
|
||||||
MaskSegmentWatchState,
|
MaskSegmentWatchState,
|
||||||
MaskSemanticColor,
|
MaskSemanticColor,
|
||||||
OverlayButtonRenderProps,
|
|
||||||
PaintBrushRequiredPayload,
|
PaintBrushRequiredPayload,
|
||||||
PaintCallbackPayload,
|
PaintCallbackPayload,
|
||||||
PaintSuccessPayload,
|
PaintSuccessPayload,
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Skia,
|
Skia,
|
||||||
Fill,
|
|
||||||
Shader,
|
Shader,
|
||||||
ImageShader,
|
ImageShader,
|
||||||
Group,
|
Group,
|
||||||
|
Rect,
|
||||||
drawAsImage,
|
drawAsImage,
|
||||||
type SkImage,
|
type SkImage,
|
||||||
type SkRuntimeEffect,
|
type SkRuntimeEffect,
|
||||||
@ -75,14 +75,14 @@ function createPaintShaderTree(props: PaintShaderLayerProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fill>
|
<Rect x={x} y={y} width={width} height={height}>
|
||||||
<Shader source={effect} uniforms={uniforms}>
|
<Shader source={effect} uniforms={uniforms}>
|
||||||
<ImageShader image={originImage} {...imageShaderProps} />
|
<ImageShader image={originImage} {...imageShaderProps} />
|
||||||
<ImageShader image={paintColorMap} {...imageShaderProps} />
|
<ImageShader image={paintColorMap} {...imageShaderProps} />
|
||||||
<ImageShader image={lowFreqImage} {...imageShaderProps} />
|
<ImageShader image={lowFreqImage} {...imageShaderProps} />
|
||||||
<ImageShader image={highFreqImage} {...imageShaderProps} />
|
<ImageShader image={highFreqImage} {...imageShaderProps} />
|
||||||
</Shader>
|
</Shader>
|
||||||
</Fill>
|
</Rect>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user