react-native-mask-segment-canvas 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +904 -0
- package/dist/components/MaskSegmentCanvas.d.ts +6 -0
- package/dist/components/MaskSegmentCanvas.d.ts.map +1 -0
- package/dist/components/MaskSegmentCanvas.js +2012 -0
- package/dist/components/MaskSegmentCanvas.js.map +1 -0
- package/dist/components/MaskSegmentCanvas.types.d.ts +189 -0
- package/dist/components/MaskSegmentCanvas.types.d.ts.map +1 -0
- package/dist/components/MaskSegmentCanvas.types.js +2 -0
- package/dist/components/MaskSegmentCanvas.types.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/shaders/regionPaint.sksl.d.ts +3 -0
- package/dist/shaders/regionPaint.sksl.d.ts.map +1 -0
- package/dist/shaders/regionPaint.sksl.js +72 -0
- package/dist/shaders/regionPaint.sksl.js.map +1 -0
- package/dist/utils/compositePaintedImage.d.ts +44 -0
- package/dist/utils/compositePaintedImage.d.ts.map +1 -0
- package/dist/utils/compositePaintedImage.js +146 -0
- package/dist/utils/compositePaintedImage.js.map +1 -0
- package/dist/utils/exportUtils.d.ts +20 -0
- package/dist/utils/exportUtils.d.ts.map +1 -0
- package/dist/utils/exportUtils.js +32 -0
- package/dist/utils/exportUtils.js.map +1 -0
- package/dist/utils/freqLayerPrep.d.ts +23 -0
- package/dist/utils/freqLayerPrep.d.ts.map +1 -0
- package/dist/utils/freqLayerPrep.js +168 -0
- package/dist/utils/freqLayerPrep.js.map +1 -0
- package/dist/utils/maskSegmentRuntime.d.ts +43 -0
- package/dist/utils/maskSegmentRuntime.d.ts.map +1 -0
- package/dist/utils/maskSegmentRuntime.js +181 -0
- package/dist/utils/maskSegmentRuntime.js.map +1 -0
- package/dist/utils/maskSegmentation.d.ts +133 -0
- package/dist/utils/maskSegmentation.d.ts.map +1 -0
- package/dist/utils/maskSegmentation.js +1600 -0
- package/dist/utils/maskSegmentation.js.map +1 -0
- package/dist/utils/maskSemanticPalette.d.ts +31 -0
- package/dist/utils/maskSemanticPalette.d.ts.map +1 -0
- package/dist/utils/maskSemanticPalette.js +125 -0
- package/dist/utils/maskSemanticPalette.js.map +1 -0
- package/dist/utils/opencvAdapter.d.ts +116 -0
- package/dist/utils/opencvAdapter.d.ts.map +1 -0
- package/dist/utils/opencvAdapter.js +353 -0
- package/dist/utils/opencvAdapter.js.map +1 -0
- package/dist/utils/paintColorMapTexture.d.ts +5 -0
- package/dist/utils/paintColorMapTexture.d.ts.map +1 -0
- package/dist/utils/paintColorMapTexture.js +203 -0
- package/dist/utils/paintColorMapTexture.js.map +1 -0
- package/dist/utils/paintShaderRuntime.d.ts +40 -0
- package/dist/utils/paintShaderRuntime.d.ts.map +1 -0
- package/dist/utils/paintShaderRuntime.js +76 -0
- package/dist/utils/paintShaderRuntime.js.map +1 -0
- package/dist/utils/pickMapTexture.d.ts +4 -0
- package/dist/utils/pickMapTexture.d.ts.map +1 -0
- package/dist/utils/pickMapTexture.js +24 -0
- package/dist/utils/pickMapTexture.js.map +1 -0
- package/dist/utils/pngImage.d.ts +49 -0
- package/dist/utils/pngImage.d.ts.map +1 -0
- package/dist/utils/pngImage.js +438 -0
- package/dist/utils/pngImage.js.map +1 -0
- package/dist/utils/resolveAssetPath.d.ts +3 -0
- package/dist/utils/resolveAssetPath.d.ts.map +1 -0
- package/dist/utils/resolveAssetPath.js +56 -0
- package/dist/utils/resolveAssetPath.js.map +1 -0
- package/dist/utils/resolveImageUrl.d.ts +3 -0
- package/dist/utils/resolveImageUrl.d.ts.map +1 -0
- package/dist/utils/resolveImageUrl.js +51 -0
- package/dist/utils/resolveImageUrl.js.map +1 -0
- package/dist/utils/skiaImage.d.ts +4 -0
- package/dist/utils/skiaImage.d.ts.map +1 -0
- package/dist/utils/skiaImage.js +12 -0
- package/dist/utils/skiaImage.js.map +1 -0
- package/package.json +100 -0
- package/patches/react-native-fast-opencv+0.4.8.patch +122 -0
- package/src/components/MaskSegmentCanvas.tsx +2832 -0
- package/src/components/MaskSegmentCanvas.types.ts +216 -0
- package/src/globals.d.ts +19 -0
- package/src/index.ts +45 -0
- package/src/shaders/regionPaint.sksl.ts +71 -0
- package/src/upng-js.d.ts +33 -0
- package/src/utils/compositePaintedImage.ts +201 -0
- package/src/utils/exportUtils.ts +40 -0
- package/src/utils/freqLayerPrep.ts +267 -0
- package/src/utils/maskSegmentRuntime.ts +257 -0
- package/src/utils/maskSegmentation.ts +2294 -0
- package/src/utils/maskSemanticPalette.ts +187 -0
- package/src/utils/opencvAdapter.ts +539 -0
- package/src/utils/paintColorMapTexture.ts +239 -0
- package/src/utils/paintShaderRuntime.tsx +150 -0
- package/src/utils/pickMapTexture.ts +37 -0
- package/src/utils/pngImage.ts +591 -0
- package/src/utils/resolveAssetPath.ts +64 -0
- package/src/utils/resolveImageUrl.ts +63 -0
- package/src/utils/skiaImage.ts +25 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
|
3
|
+
import type { SegmentRegion } from '../utils/maskSegmentation';
|
|
4
|
+
import type { MaskSemanticColor } from '../utils/maskSemanticPalette';
|
|
5
|
+
|
|
6
|
+
export type BgrColor = { b: number; g: number; r: number };
|
|
7
|
+
|
|
8
|
+
export type MaskSegmentWatchState =
|
|
9
|
+
| 'init'
|
|
10
|
+
| 'images_loaded'
|
|
11
|
+
| 'mask_aligned'
|
|
12
|
+
| 'mask_sampled'
|
|
13
|
+
| 'regions_ready'
|
|
14
|
+
| 'layers_ready'
|
|
15
|
+
| 'interactive'
|
|
16
|
+
| 'mask_paths_ready'
|
|
17
|
+
| 'error';
|
|
18
|
+
|
|
19
|
+
export type MaskSegmentWatchDetail = {
|
|
20
|
+
regionCount?: number;
|
|
21
|
+
maskPathsReady?: boolean;
|
|
22
|
+
freqLayersReady?: boolean;
|
|
23
|
+
errorMessage?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type PipelinePreset = 'high' | 'medium' | 'low';
|
|
27
|
+
|
|
28
|
+
export type PipelineConfig = {
|
|
29
|
+
maxImageLongSide?: number;
|
|
30
|
+
/** 高低频 LAB 处理最长边(可低于 maxImageLongSide,Shader 拉伸采样) */
|
|
31
|
+
paintFreqMaxLongSide?: number;
|
|
32
|
+
originPreviewMaxLongSide?: number;
|
|
33
|
+
maskPathMaxLongSide?: number;
|
|
34
|
+
minContourArea?: number;
|
|
35
|
+
contourApproxEpsilon?: number;
|
|
36
|
+
maxRegions?: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type MaskSegmentConfig = {
|
|
40
|
+
semanticColors?: MaskSemanticColor[];
|
|
41
|
+
baseboardMaxColorDist?: number;
|
|
42
|
+
blackThreshold?: number;
|
|
43
|
+
quantStep?: number;
|
|
44
|
+
baseboardStripQuantKeys?: string[];
|
|
45
|
+
wallQuantKeys?: string[];
|
|
46
|
+
cabinetQuantKeys?: string[];
|
|
47
|
+
maxRegionColors?: number;
|
|
48
|
+
secondarySemanticNames?: string[];
|
|
49
|
+
secondaryMinPixelRatio?: number;
|
|
50
|
+
junctionHRadiusPx?: number;
|
|
51
|
+
junctionVRadiusPx?: number;
|
|
52
|
+
kickBridgeHalfWPx?: number;
|
|
53
|
+
baseboardJunctionRowMarginPx?: number;
|
|
54
|
+
baseboardJunctionVReachPx?: number;
|
|
55
|
+
baseboardMinRunPx?: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type PaintConfig = {
|
|
59
|
+
palette?: BgrColor[];
|
|
60
|
+
colorBaseOpacity?: number;
|
|
61
|
+
lLightOpacity?: number;
|
|
62
|
+
textureOpacity?: number;
|
|
63
|
+
lLowBlurKernel?: number;
|
|
64
|
+
lLowContrast?: number;
|
|
65
|
+
lLowBrightness?: number;
|
|
66
|
+
lHighGain?: number;
|
|
67
|
+
maskFeatherColor?: number;
|
|
68
|
+
maskFeatherTexture?: number;
|
|
69
|
+
regionOverlayFill?: string;
|
|
70
|
+
regionOutlineStrokeWidth?: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type InteractionConfig = {
|
|
74
|
+
kickMaskPickRadiusPx?: number;
|
|
75
|
+
pickMapSearchRadiusPx?: number;
|
|
76
|
+
thinStripPadding?: number;
|
|
77
|
+
regionPadding?: number;
|
|
78
|
+
initRegionFlashMs?: number;
|
|
79
|
+
enableInitRegionFlash?: boolean;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type PaintedRegionRecord = {
|
|
83
|
+
regionId: number;
|
|
84
|
+
regionName: string;
|
|
85
|
+
color: BgrColor;
|
|
86
|
+
configJson?: Record<string, unknown>;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type MaskSegmentSession = {
|
|
90
|
+
version: 1;
|
|
91
|
+
originUrl: string;
|
|
92
|
+
maskUrl: string;
|
|
93
|
+
painted: PaintedRegionRecord[];
|
|
94
|
+
paintHistory: number[];
|
|
95
|
+
currentColor?: BgrColor;
|
|
96
|
+
currentColorConfigJson?: Record<string, unknown>;
|
|
97
|
+
savedAt: number;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type SavePaintResult = {
|
|
101
|
+
filePath: string;
|
|
102
|
+
width: number;
|
|
103
|
+
height: number;
|
|
104
|
+
paintedCount: number;
|
|
105
|
+
previewPath?: string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type SavePaintOptions = {
|
|
109
|
+
destDir?: string;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export type PaintSuccessPayload = {
|
|
113
|
+
kind: 'painted';
|
|
114
|
+
regionId: number;
|
|
115
|
+
regionName: string;
|
|
116
|
+
color: BgrColor;
|
|
117
|
+
configJson?: Record<string, unknown>;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export type PaintBrushRequiredPayload = {
|
|
121
|
+
kind: 'brush_required';
|
|
122
|
+
/** 未选笔刷时的提示文案 */
|
|
123
|
+
hint: string;
|
|
124
|
+
regionId: number;
|
|
125
|
+
regionName: string;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export type PaintCallbackPayload = PaintSuccessPayload | PaintBrushRequiredPayload;
|
|
129
|
+
|
|
130
|
+
export type OverlayButtonRenderProps = {
|
|
131
|
+
onPress: () => void;
|
|
132
|
+
disabled?: boolean;
|
|
133
|
+
text: string;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type MaskSegmentCanvasRef = {
|
|
137
|
+
reset: () => void;
|
|
138
|
+
swap: (showOrigin?: boolean) => void;
|
|
139
|
+
save: (options?: SavePaintOptions) => Promise<SavePaintResult>;
|
|
140
|
+
session: () => MaskSegmentSession;
|
|
141
|
+
loadSession: (session: MaskSegmentSession) => void;
|
|
142
|
+
setPaintColor: (color: BgrColor, configJson?: Record<string, unknown>) => void;
|
|
143
|
+
setMaskConfig: (config: MaskSegmentConfig) => void;
|
|
144
|
+
clearAllPaint: () => void;
|
|
145
|
+
/** Undo the most recent single coloring (paint) step. Distinct from clearAllPaint (full reset). */
|
|
146
|
+
undoSelection?: () => void;
|
|
147
|
+
resegment: () => Promise<void>;
|
|
148
|
+
getRegions: () => SegmentRegion[];
|
|
149
|
+
getPaintedRegions: () => PaintedRegionRecord[];
|
|
150
|
+
/** Returns the most recent auto-export or save() result, if any. */
|
|
151
|
+
getLastExport?: () => SavePaintResult | null;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export type MaskSegmentCanvasProps = {
|
|
155
|
+
originUrl?: string;
|
|
156
|
+
maskUrl?: string;
|
|
157
|
+
/** @deprecated 使用 originUrl */
|
|
158
|
+
originImgPath?: string;
|
|
159
|
+
/** @deprecated 使用 maskUrl */
|
|
160
|
+
maskImgPath?: string;
|
|
161
|
+
/** 掩码语义识别色,初始化配置;等同 maskConfig.semanticColors */
|
|
162
|
+
semanticColors?: MaskSemanticColor[];
|
|
163
|
+
/** 分区虚线高亮色,初始化配置;等同 paintConfig.regionOverlayFill */
|
|
164
|
+
regionOutlineColor?: string;
|
|
165
|
+
maskConfig?: MaskSegmentConfig;
|
|
166
|
+
/** Performance preset (high / medium / low). Merged with pipelineConfig overrides. */
|
|
167
|
+
pipelinePreset?: PipelinePreset;
|
|
168
|
+
pipelineConfig?: PipelineConfig;
|
|
169
|
+
paintConfig?: PaintConfig;
|
|
170
|
+
interactionConfig?: InteractionConfig;
|
|
171
|
+
initialSession?: MaskSegmentSession;
|
|
172
|
+
initialPaintColor?: BgrColor;
|
|
173
|
+
initialPaintConfigJson?: Record<string, unknown>;
|
|
174
|
+
showDebugPickers?: boolean;
|
|
175
|
+
showToolbar?: boolean;
|
|
176
|
+
showColorBar?: boolean;
|
|
177
|
+
showStatusRow?: boolean;
|
|
178
|
+
showOverlayButtons?: boolean;
|
|
179
|
+
disabled?: boolean;
|
|
180
|
+
style?: StyleProp<ViewStyle>;
|
|
181
|
+
canvasStyle?: StyleProp<ViewStyle>;
|
|
182
|
+
/**
|
|
183
|
+
* Max container height available for this component (px). When set, the SDK
|
|
184
|
+
* computes canvas dimensions as a fit-contain within (screenWidth - 20, maxHeight)
|
|
185
|
+
* instead of using the full image aspect at full screen width. This prevents
|
|
186
|
+
* internal ScrollView scrolling for tall images.
|
|
187
|
+
*/
|
|
188
|
+
maxHeight?: number;
|
|
189
|
+
undoButtonStyle?: StyleProp<ViewStyle>;
|
|
190
|
+
compareButtonStyle?: StyleProp<ViewStyle>;
|
|
191
|
+
undoButtonTextStyle?: StyleProp<TextStyle>;
|
|
192
|
+
compareButtonTextStyle?: StyleProp<TextStyle>;
|
|
193
|
+
undoButtonText?: string;
|
|
194
|
+
compareButtonText?: string;
|
|
195
|
+
compareExitButtonText?: string;
|
|
196
|
+
renderUndoButton?: (props: OverlayButtonRenderProps) => ReactNode;
|
|
197
|
+
renderCompareButton?: (props: OverlayButtonRenderProps) => ReactNode;
|
|
198
|
+
onWatch?: (
|
|
199
|
+
state: MaskSegmentWatchState,
|
|
200
|
+
durationMs: number,
|
|
201
|
+
detail?: MaskSegmentWatchDetail,
|
|
202
|
+
) => void;
|
|
203
|
+
onPaintCallback?: (payload: PaintCallbackPayload) => void;
|
|
204
|
+
onError?: (message: string, error?: unknown) => void;
|
|
205
|
+
/**
|
|
206
|
+
* When true, once the canvas reaches a ready interactive state (segmentation complete
|
|
207
|
+
* + any initialSession / painted colors applied), the SDK will automatically call its
|
|
208
|
+
* internal save pipeline to produce the recolored result image and fire onExported.
|
|
209
|
+
* This moves "auto-generate After preview" capability inside the SDK.
|
|
210
|
+
*/
|
|
211
|
+
autoExportOnReady?: boolean;
|
|
212
|
+
/** Fired by SDK when autoExportOnReady produced a result (the recolored file). */
|
|
213
|
+
onExported?: (result: SavePaintResult) => void;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export type { SegmentRegion, MaskSemanticColor };
|
package/src/globals.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// 补充 React Native 运行时可用的全局 API 类型声明
|
|
2
|
+
|
|
3
|
+
declare var performance: {
|
|
4
|
+
now(): number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
declare function btoa(data: string): string;
|
|
8
|
+
declare function atob(data: string): string;
|
|
9
|
+
|
|
10
|
+
declare var TextEncoder: {
|
|
11
|
+
prototype: TextEncoder;
|
|
12
|
+
new (): TextEncoder;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
declare interface TextEncoder {
|
|
16
|
+
encode(input?: string): Uint8Array;
|
|
17
|
+
encodeInto(input: string, dest: Uint8Array): { read: number; written: number };
|
|
18
|
+
readonly encoding: string;
|
|
19
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export { default } from './components/MaskSegmentCanvas';
|
|
2
|
+
export type {
|
|
3
|
+
BgrColor,
|
|
4
|
+
InteractionConfig,
|
|
5
|
+
MaskSegmentCanvasProps,
|
|
6
|
+
MaskSegmentCanvasRef,
|
|
7
|
+
MaskSegmentConfig,
|
|
8
|
+
MaskSegmentSession,
|
|
9
|
+
MaskSegmentWatchDetail,
|
|
10
|
+
MaskSegmentWatchState,
|
|
11
|
+
MaskSemanticColor,
|
|
12
|
+
OverlayButtonRenderProps,
|
|
13
|
+
PaintBrushRequiredPayload,
|
|
14
|
+
PaintCallbackPayload,
|
|
15
|
+
PaintSuccessPayload,
|
|
16
|
+
PaintConfig,
|
|
17
|
+
PaintedRegionRecord,
|
|
18
|
+
PipelineConfig,
|
|
19
|
+
PipelinePreset,
|
|
20
|
+
SavePaintOptions,
|
|
21
|
+
SavePaintResult,
|
|
22
|
+
SegmentRegion,
|
|
23
|
+
} from './components/MaskSegmentCanvas.types';
|
|
24
|
+
export {
|
|
25
|
+
BASEBOARD_SEMANTIC_NAME,
|
|
26
|
+
MASK_SEMANTIC_COLORS,
|
|
27
|
+
} from './utils/maskSemanticPalette';
|
|
28
|
+
export {
|
|
29
|
+
createRuntimeConfig,
|
|
30
|
+
DEFAULT_INTERACTION_CONFIG,
|
|
31
|
+
DEFAULT_MASK_CONFIG,
|
|
32
|
+
DEFAULT_PAINT_CONFIG,
|
|
33
|
+
DEFAULT_PIPELINE_CONFIG,
|
|
34
|
+
PIPELINE_HIGH,
|
|
35
|
+
PIPELINE_LOW,
|
|
36
|
+
PIPELINE_MEDIUM,
|
|
37
|
+
PIPELINE_PRESETS,
|
|
38
|
+
getMaskSegmentRuntimeConfig,
|
|
39
|
+
resolvePipelineConfig,
|
|
40
|
+
setMaskSegmentRuntimeConfig,
|
|
41
|
+
} from './utils/maskSegmentRuntime';
|
|
42
|
+
export {
|
|
43
|
+
prewarmPngBgrCache,
|
|
44
|
+
prewarmPngBgrCacheAsync,
|
|
45
|
+
} from './utils/pngImage';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** SkSL:分区上色(保留原图明暗与纹理,按 paintColorMap 叠色) */
|
|
2
|
+
export const REGION_PAINT_SKSL = `
|
|
3
|
+
uniform shader originTex;
|
|
4
|
+
uniform shader paintColorTex;
|
|
5
|
+
uniform shader lowFreqTex;
|
|
6
|
+
uniform shader highFreqTex;
|
|
7
|
+
|
|
8
|
+
uniform float colorBaseOpacity;
|
|
9
|
+
uniform float lLightOpacity;
|
|
10
|
+
uniform float textureOpacity;
|
|
11
|
+
uniform float showOrigin;
|
|
12
|
+
|
|
13
|
+
float luminance(half3 c) {
|
|
14
|
+
return dot(c, half3(0.2126, 0.7152, 0.0722));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
half3 setLuminance(float lum, half3 base) {
|
|
18
|
+
float diff = lum - luminance(base);
|
|
19
|
+
return base + diff;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
half3 luminosityBlend(half3 base, half3 blend) {
|
|
23
|
+
return setLuminance(luminance(blend), base);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
half3 overlayBlend(half3 base, half3 blend) {
|
|
27
|
+
half3 low = 2.0 * base * blend;
|
|
28
|
+
half3 high = 1.0 - 2.0 * (1.0 - base) * (1.0 - blend);
|
|
29
|
+
return mix(low, high, step(half3(0.5), base));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
half4 main(float2 coord) {
|
|
33
|
+
half4 origin = originTex.eval(coord);
|
|
34
|
+
if (showOrigin > 0.5) {
|
|
35
|
+
return origin;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
half4 paintEntry = paintColorTex.eval(coord);
|
|
39
|
+
// The paintColorMap uses Unpremul alpha: painted pixels → (R,G,B,255),
|
|
40
|
+
// transparent pixels → (0,0,0,0). GPU bilinear sampling interpolates
|
|
41
|
+
// straight-alpha values, so at boundaries rgb = trueColor * sampled.a
|
|
42
|
+
// (contaminated with black from the transparent neighbour).
|
|
43
|
+
//
|
|
44
|
+
// Unpremultiply to recover the true paint color:
|
|
45
|
+
// trueColor = sampled.rgb / sampled.a
|
|
46
|
+
// This eliminates dark fringing at region boundaries.
|
|
47
|
+
float pa = paintEntry.a + 0.0001;
|
|
48
|
+
paintEntry.rgb /= pa;
|
|
49
|
+
|
|
50
|
+
// Gate sub-pixel alpha to kill residual sampling noise.
|
|
51
|
+
// Thresholds are deliberately low (≈1.3–3.8 in byte space) because
|
|
52
|
+
// post-unpremul the RGB is correct at any alpha ≥ 0 — we only need
|
|
53
|
+
// to suppress samples that contribute negligibly to the final blend.
|
|
54
|
+
// Using *= preserves the smooth edge; higher-alpha samples pass through.
|
|
55
|
+
paintEntry.a *= smoothstep(0.005, 0.015, paintEntry.a);
|
|
56
|
+
|
|
57
|
+
half3 paintRgb = paintEntry.rgb;
|
|
58
|
+
half lowL = lowFreqTex.eval(coord).r;
|
|
59
|
+
half highL = highFreqTex.eval(coord).r;
|
|
60
|
+
|
|
61
|
+
half3 base = paintRgb * colorBaseOpacity;
|
|
62
|
+
half3 lit = luminosityBlend(base, half3(lowL));
|
|
63
|
+
half3 withLight = mix(base, lit, lLightOpacity);
|
|
64
|
+
half3 tex = overlayBlend(withLight, half3(highL));
|
|
65
|
+
half3 finalRgb = mix(withLight, tex, textureOpacity);
|
|
66
|
+
|
|
67
|
+
// Soft edge blend using the (feathered) alpha from the paint color map as coverage.
|
|
68
|
+
half3 blended = mix(origin.rgb, finalRgb, paintEntry.a);
|
|
69
|
+
return half4(blended, 1.0);
|
|
70
|
+
}
|
|
71
|
+
`;
|
package/src/upng-js.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
declare module 'upng-js' {
|
|
2
|
+
export interface UPNGImage {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
depth: number;
|
|
6
|
+
ctype: number;
|
|
7
|
+
frames: number;
|
|
8
|
+
tabs: Record<string, string>;
|
|
9
|
+
data: ArrayBuffer;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function decode(buffer: ArrayBuffer): UPNGImage;
|
|
13
|
+
|
|
14
|
+
export function toRGBA8(img: UPNGImage): ArrayBuffer[];
|
|
15
|
+
|
|
16
|
+
export function encode(
|
|
17
|
+
imgs: ArrayBuffer[],
|
|
18
|
+
w: number,
|
|
19
|
+
h: number,
|
|
20
|
+
cnum: number,
|
|
21
|
+
dels?: number[],
|
|
22
|
+
): ArrayBuffer;
|
|
23
|
+
|
|
24
|
+
export function encodeLL(
|
|
25
|
+
imgs: ArrayBuffer[],
|
|
26
|
+
w: number,
|
|
27
|
+
h: number,
|
|
28
|
+
cc: number,
|
|
29
|
+
ac: number,
|
|
30
|
+
depth: number,
|
|
31
|
+
dels?: number[],
|
|
32
|
+
): ArrayBuffer;
|
|
33
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import RNFS from 'react-native-fs';
|
|
3
|
+
import type { BgrColor, SavePaintResult } from '../components/MaskSegmentCanvas.types';
|
|
4
|
+
import type { SkImage } from '@shopify/react-native-skia';
|
|
5
|
+
// upng-js: used for PNG encode of CPU recolor.
|
|
6
|
+
import UPNG from 'upng-js';
|
|
7
|
+
import { renderPaintedImageOffscreen } from './paintShaderRuntime';
|
|
8
|
+
import { writePngBase64ToFile, writePngBytesToFile } from './exportUtils';
|
|
9
|
+
|
|
10
|
+
export type CompositePaintInput = {
|
|
11
|
+
originBuffer: Uint8Array;
|
|
12
|
+
cols: number;
|
|
13
|
+
rows: number;
|
|
14
|
+
pickBuffer: Uint8Array;
|
|
15
|
+
paintedRegions: Map<number, BgrColor>;
|
|
16
|
+
destDir?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Preferred path for rich export: PNG base64 from makeImageSnapshot() — written
|
|
19
|
+
* directly to disk without an extra decode/re-encode round trip.
|
|
20
|
+
*/
|
|
21
|
+
exportPngBase64?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Preferred path for rich export: if the caller (MaskSegmentCanvas) provides bytes
|
|
24
|
+
* that were produced by makeImageSnapshot() on a high-resolution Canvas rendering the
|
|
25
|
+
* exact same PaintShaderLayer + regionPaint SkSL at work resolution, we write them
|
|
26
|
+
* directly. This captures the live editor 质感 (lighting + high/low-freq texture)
|
|
27
|
+
* without CPU pixel math and without a second declarative drawAsImage.
|
|
28
|
+
*/
|
|
29
|
+
exportPngBytes?: Uint8Array;
|
|
30
|
+
/**
|
|
31
|
+
* Fallback rich path (when no pre-captured snapshot bytes): pass the live textures
|
|
32
|
+
* so we can try renderPaintedImageOffscreen (drawAsImage with the shader tree).
|
|
33
|
+
*/
|
|
34
|
+
shaderTextures?: {
|
|
35
|
+
originImage: SkImage;
|
|
36
|
+
paintColorMap: SkImage;
|
|
37
|
+
lowFreqImage: SkImage;
|
|
38
|
+
highFreqImage: SkImage;
|
|
39
|
+
};
|
|
40
|
+
/** The logical size at which to render the shader tree for export (typically the work image res). */
|
|
41
|
+
renderWidth?: number;
|
|
42
|
+
renderHeight?: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* CPU recolor: directly map pick codes to painted BGR colors (or copy origin).
|
|
47
|
+
* Produces RGBA PNG bytes via upng-js. This is the *fallback* path when rich shader offscreen
|
|
48
|
+
* is not available or fails. It produces flat colors without the editor's lighting + freq texture.
|
|
49
|
+
*/
|
|
50
|
+
function cpuRecolorToPngBytes(
|
|
51
|
+
originBgr: Uint8Array,
|
|
52
|
+
pickBuffer: Uint8Array,
|
|
53
|
+
paintedRegions: Map<number, BgrColor>,
|
|
54
|
+
cols: number,
|
|
55
|
+
rows: number,
|
|
56
|
+
): Uint8Array {
|
|
57
|
+
const pixelCount = cols * rows;
|
|
58
|
+
const rgba = new Uint8Array(pixelCount * 4);
|
|
59
|
+
const colorByPickCode = new Map<number, BgrColor>();
|
|
60
|
+
for (const [regionId, color] of paintedRegions) {
|
|
61
|
+
colorByPickCode.set(regionId + 1, color);
|
|
62
|
+
}
|
|
63
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
64
|
+
const code = pickBuffer[i];
|
|
65
|
+
const color = code > 0 ? colorByPickCode.get(code) : undefined;
|
|
66
|
+
const d = i * 4;
|
|
67
|
+
if (color) {
|
|
68
|
+
rgba[d] = color.r;
|
|
69
|
+
rgba[d + 1] = color.g;
|
|
70
|
+
rgba[d + 2] = color.b;
|
|
71
|
+
rgba[d + 3] = 255;
|
|
72
|
+
} else {
|
|
73
|
+
const s = i * 3;
|
|
74
|
+
rgba[d] = originBgr[s + 2]; // RGB <- BGR
|
|
75
|
+
rgba[d + 1] = originBgr[s + 1];
|
|
76
|
+
rgba[d + 2] = originBgr[s];
|
|
77
|
+
rgba[d + 3] = 255;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const png = UPNG.encode([rgba.buffer], cols, rows, 0);
|
|
81
|
+
return new Uint8Array(png as ArrayBuffer);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 将上色区域导出为 recolored PNG。
|
|
85
|
+
* 优先级(从好到保底):
|
|
86
|
+
* 1. exportPngBytes(调用方用 makeImageSnapshot 在高分辨率 Canvas 上捕获的完整 shader 结果)—— 推荐的“保存快照”路径,无 CPU 逐像素,无二次 drawAsImage。
|
|
87
|
+
* 2. shaderTextures + render*(通过 renderPaintedImageOffscreen / drawAsImage 重建同一套 PaintShaderLayer + SkSL)。
|
|
88
|
+
* 3. CPU 逐像素 recolor(flat,无光照/纹理,仅作最后兜底,保证保存不中断)。
|
|
89
|
+
*/
|
|
90
|
+
export async function compositePaintedImage(
|
|
91
|
+
input: CompositePaintInput,
|
|
92
|
+
): Promise<SavePaintResult> {
|
|
93
|
+
const {
|
|
94
|
+
originBuffer,
|
|
95
|
+
cols,
|
|
96
|
+
rows,
|
|
97
|
+
pickBuffer,
|
|
98
|
+
paintedRegions,
|
|
99
|
+
destDir,
|
|
100
|
+
exportPngBase64,
|
|
101
|
+
exportPngBytes,
|
|
102
|
+
shaderTextures,
|
|
103
|
+
renderWidth,
|
|
104
|
+
renderHeight,
|
|
105
|
+
} = input;
|
|
106
|
+
if (paintedRegions.size === 0) {
|
|
107
|
+
throw new Error('No painted regions, cannot save');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (pickBuffer.length !== cols * rows) {
|
|
111
|
+
const msg = 'pickMap size does not match image';
|
|
112
|
+
console.error('[VIZ-SAVE] composite will throw:', msg, { pickLen: pickBuffer.length, expected: cols * rows, cols, rows });
|
|
113
|
+
throw new Error(msg);
|
|
114
|
+
}
|
|
115
|
+
if (originBuffer.length !== cols * rows * 3) {
|
|
116
|
+
const msg = 'Original buffer size does not match image';
|
|
117
|
+
console.error('[VIZ-SAVE] composite will throw:', msg, { originLen: originBuffer.length, expected: cols * rows * 3, cols, rows });
|
|
118
|
+
throw new Error(msg);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let pngBytesForWrite: Uint8Array | undefined;
|
|
122
|
+
let pngBase64ForWrite: string | undefined;
|
|
123
|
+
let usedSnapshot = false;
|
|
124
|
+
let usedRichShader = false;
|
|
125
|
+
|
|
126
|
+
// 1) Highest priority: pre-encoded PNG base64 from makeImageSnapshot (no extra conversion).
|
|
127
|
+
if (exportPngBase64 && exportPngBase64.length > 0) {
|
|
128
|
+
pngBase64ForWrite = exportPngBase64;
|
|
129
|
+
usedSnapshot = true;
|
|
130
|
+
}
|
|
131
|
+
// 1b) Snapshot bytes fallback (legacy callers).
|
|
132
|
+
else if (exportPngBytes && exportPngBytes.length > 0) {
|
|
133
|
+
pngBytesForWrite = exportPngBytes;
|
|
134
|
+
usedSnapshot = true;
|
|
135
|
+
}
|
|
136
|
+
// 2) 回退 rich:用 live 纹理 + drawAsImage 重建与编辑器一致的 shader 结果(带光照 + 频率纹理)。
|
|
137
|
+
else if (shaderTextures && renderWidth && renderHeight) {
|
|
138
|
+
try {
|
|
139
|
+
const offImg = await renderPaintedImageOffscreen({
|
|
140
|
+
originImage: shaderTextures.originImage,
|
|
141
|
+
paintColorMap: shaderTextures.paintColorMap,
|
|
142
|
+
lowFreqImage: shaderTextures.lowFreqImage,
|
|
143
|
+
highFreqImage: shaderTextures.highFreqImage,
|
|
144
|
+
width: renderWidth,
|
|
145
|
+
height: renderHeight,
|
|
146
|
+
showOrigin: false,
|
|
147
|
+
});
|
|
148
|
+
if (offImg) {
|
|
149
|
+
let b64 = '';
|
|
150
|
+
try {
|
|
151
|
+
const enc = (offImg as any).encodeToBase64;
|
|
152
|
+
if (typeof enc === 'function') {
|
|
153
|
+
b64 = enc.call(offImg) || '';
|
|
154
|
+
}
|
|
155
|
+
} catch (encErr) {
|
|
156
|
+
console.warn('[VIZ-SAVE] offscreen encodeToBase64 failed:', encErr);
|
|
157
|
+
}
|
|
158
|
+
if (b64 && b64.length > 0) {
|
|
159
|
+
pngBytesForWrite = new Uint8Array(Buffer.from(b64, 'base64'));
|
|
160
|
+
usedRichShader = true;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const disp = (offImg as any).dispose;
|
|
164
|
+
if (typeof disp === 'function') disp.call(offImg);
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.warn('[VIZ-SAVE] rich shader offscreen for export failed (will fallback):', e);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 3) 最后兜底:CPU 逐像素(flat 颜色,无 editor 质感)。
|
|
173
|
+
if (!pngBase64ForWrite && !pngBytesForWrite) {
|
|
174
|
+
try {
|
|
175
|
+
pngBytesForWrite = cpuRecolorToPngBytes(originBuffer, pickBuffer, paintedRegions, cols, rows);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
throw new Error('CPU recolor PNG decoding failed');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const dir = destDir ?? RNFS.CachesDirectoryPath;
|
|
181
|
+
const filePath = `${dir}/painted_${Date.now()}.png`;
|
|
182
|
+
try {
|
|
183
|
+
if (pngBase64ForWrite) {
|
|
184
|
+
await writePngBase64ToFile(filePath, pngBase64ForWrite);
|
|
185
|
+
} else {
|
|
186
|
+
await writePngBytesToFile(filePath, pngBytesForWrite!);
|
|
187
|
+
}
|
|
188
|
+
void usedSnapshot;
|
|
189
|
+
void usedRichShader;
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.error('[VIZ-SAVE] composite writeFile threw:', e, { filePath, dir });
|
|
192
|
+
throw e;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
filePath,
|
|
197
|
+
width: cols,
|
|
198
|
+
height: rows,
|
|
199
|
+
paintedCount: paintedRegions.size,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import RNFS from 'react-native-fs';
|
|
3
|
+
import type { BgrColor } from '../components/MaskSegmentCanvas.types';
|
|
4
|
+
|
|
5
|
+
/** Stable fingerprint for painted region colors — used to reuse cached exports. */
|
|
6
|
+
export function paintedRegionsFingerprint(painted: Map<number, BgrColor>): string {
|
|
7
|
+
if (painted.size === 0) {
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
const entries = [...painted.entries()].sort((a, b) => a[0] - b[0]);
|
|
11
|
+
return entries.map(([id, c]) => `${id}:${c.r},${c.g},${c.b}`).join('|');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function writePngBase64ToFile(filePath: string, base64: string): Promise<void> {
|
|
15
|
+
await RNFS.writeFile(filePath, base64, 'base64');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function writePngBytesToFile(filePath: string, bytes: Uint8Array): Promise<void> {
|
|
19
|
+
await RNFS.writeFile(filePath, Buffer.from(bytes).toString('base64'), 'base64');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function stripFilePrefix(uri: string): string {
|
|
23
|
+
return uri.startsWith('file://') ? uri.slice('file://'.length) : uri;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function resolveExportResultForDestDir(
|
|
27
|
+
cached: { filePath: string; width: number; height: number; paintedCount: number; previewPath?: string },
|
|
28
|
+
destDir?: string,
|
|
29
|
+
): Promise<{ filePath: string; width: number; height: number; paintedCount: number; previewPath?: string }> {
|
|
30
|
+
if (!destDir) {
|
|
31
|
+
return cached;
|
|
32
|
+
}
|
|
33
|
+
const src = stripFilePrefix(cached.filePath);
|
|
34
|
+
if (src.startsWith(destDir)) {
|
|
35
|
+
return cached;
|
|
36
|
+
}
|
|
37
|
+
const filePath = `${destDir}/painted_${Date.now()}.png`;
|
|
38
|
+
await RNFS.copyFile(src, filePath);
|
|
39
|
+
return { ...cached, filePath };
|
|
40
|
+
}
|