diff --git a/Templates/大鱼人像美化/PARAMETERS.json b/Templates/大鱼人像美化/PARAMETERS.json
index e83652a..df2c01f 100644
--- a/Templates/大鱼人像美化/PARAMETERS.json
+++ b/Templates/大鱼人像美化/PARAMETERS.json
@@ -1,5 +1,5 @@
{
- "version": "1.0.1",
+ "version": "1.0.2",
"pluginUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
"filterObject": "510000005",
"visiblePublished": 46,
diff --git a/Templates/大鱼人像美化/README.md b/Templates/大鱼人像美化/README.md
index e49bff8..ba2c07e 100644
--- a/Templates/大鱼人像美化/README.md
+++ b/Templates/大鱼人像美化/README.md
@@ -1,6 +1,6 @@
# 大鱼人像美化 FCP 效果模板
-版本:1.0.1。滤镜 UUID:`FB14B0E6-59B2-4836-916F-CD0C559E9241`。模板必须与已安装的大鱼人像美化 FxPlug APP 配套使用。
+版本:1.0.2。滤镜 UUID:`FB14B0E6-59B2-4836-916F-CD0C559E9241`。模板必须与已安装的大鱼人像美化 FxPlug APP 配套使用。
## 默认数值
@@ -28,6 +28,6 @@ Apple项目、群组、Effect Source和滤镜节点骨架来源于用户自有
前版生成稿(SHA256 `3b7984e11a92487834ab67121fd3814bf13c22b77a70851298efa84206f91478`)将效果源的 Source Media 设为0,并删除了原生clip/footage。用户在FCP应用后实测整幅黑屏。这是模板装配错误;保留Type=3本身并不能证明FCP输入替换链完整。此前XML解析、发布路径和无缺媒体检查通过,也没有覆盖真实FCP画面输入,不能当作该模板已经可用的证据。
-本次仅恢复标准效果源clip/footage和Source Media关联,并随包加入标准`Drop Zone.tiff`;滤镜算法、49个插件参数、48个发布入口、Type=3/Drop Zone=1及版本1.0.1均保留。该占位图从Apple Motion的Ozone.framework标准资源复制,实际运行只引用本模板内的相对副本。图像SHA256为 `a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e`,420128字节。
+本次仅恢复标准效果源clip/footage和Source Media关联,并随包加入标准`Drop Zone.tiff`;滤镜算法、49个插件参数、48个发布入口、Type=3/Drop Zone=1及版本1.0.2均保留。该占位图从Apple Motion的Ozone.framework标准资源复制,实际运行只引用本模板内的相对副本。图像SHA256为 `a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e`,420128字节。
本次已经静态核验Source Media→clip→相对文件链存在、文件字节一致、48项发布仍解析、隐藏数据未发布和保存选择状态;修复后的FCP新增实例、输入替换、Mix=0直通及实际人像渲染需由主任务重新实测。不得将“恢复链条”描述为已经通过FCP黑屏复测。
diff --git a/Templates/大鱼人像美化/VALIDATION.json b/Templates/大鱼人像美化/VALIDATION.json
index e21b241..1a74251 100644
--- a/Templates/大鱼人像美化/VALIDATION.json
+++ b/Templates/大鱼人像美化/VALIDATION.json
@@ -1,6 +1,6 @@
{
- "version": "1.0.1",
- "sha256": "2f7d1e0bf91a6dab82f5dde0a20d971a75abd2056a48104d861e9c112870aae4",
+ "version": "1.0.2",
+ "sha256": "fe637290212481c34cbfd8f4de5bf09101a3fbe88f4db3e55b35d5375f3ac1ba",
"filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
"publishedFunctionalControls": 46,
"informationalControls": 2,
diff --git a/Templates/大鱼人像美化/大鱼人像美化.moef b/Templates/大鱼人像美化/大鱼人像美化.moef
index 93e12d9..0d9ebec 100644
--- a/Templates/大鱼人像美化/大鱼人像美化.moef
+++ b/Templates/大鱼人像美化/大鱼人像美化.moef
@@ -46,7 +46,7 @@
1
- 大鱼人像美化 1.0.1:人像磨皮、肤色、脸型、五官保护和三目标人脸跟踪。模板中频磨皮默认为50%。
+ 大鱼人像美化 1.0.2:人像磨皮、肤色、脸型、五官保护和三目标人脸跟踪。模板中频磨皮默认为50%。
1920
@@ -298,7 +298,7 @@
-
+
8589934608
diff --git a/beatygril/beatygril/Plugin/Info.plist b/beatygril/beatygril/Plugin/Info.plist
index c178bf6..68bcb1e 100644
--- a/beatygril/beatygril/Plugin/Info.plist
+++ b/beatygril/beatygril/Plugin/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
XPC!
CFBundleShortVersionString
- 1.0.1
+ 1.0.2
CFBundleVersion
- 3
+ 4
NSHumanReadableCopyright
PlugInKit
@@ -72,7 +72,7 @@
uuid
FB14B0E6-59B2-4836-916F-CD0C559E9241
version
- 1.0.1
+ 1.0.2
className
@@ -84,7 +84,7 @@
uuid
E0E432C8-3D71-4ABA-B006-E7F519E06C87
version
- 1.0.1
+ 1.0.2
protocolNames
FxOnScreenControl
diff --git a/beatygril/beatygril/Plugin/PGBeautyRenderer.h b/beatygril/beatygril/Plugin/PGBeautyRenderer.h
index c241837..a0a6965 100644
--- a/beatygril/beatygril/Plugin/PGBeautyRenderer.h
+++ b/beatygril/beatygril/Plugin/PGBeautyRenderer.h
@@ -14,6 +14,14 @@ NS_ASSUME_NONNULL_BEGIN
teethMask:(nullable CIImage *)teethMask
eyeMask:(nullable CIImage *)eyeMask
error:(NSError * _Nullable * _Nullable)error;
+/// Face-based callers supply uncut coverage for smoothly blended color changes.
+- (nullable CIImage *)processImage:(CIImage *)image
+ settings:(PGBeautySettings)settings
+ skinMask:(nullable CIImage *)skinMask
+ colorMask:(nullable CIImage *)colorMask
+ teethMask:(nullable CIImage *)teethMask
+ eyeMask:(nullable CIImage *)eyeMask
+ error:(NSError * _Nullable * _Nullable)error;
@end
NS_ASSUME_NONNULL_END
diff --git a/beatygril/beatygril/Plugin/PGBeautyRenderer.m b/beatygril/beatygril/Plugin/PGBeautyRenderer.m
index 4d31ead..4c9fc6f 100644
--- a/beatygril/beatygril/Plugin/PGBeautyRenderer.m
+++ b/beatygril/beatygril/Plugin/PGBeautyRenderer.m
@@ -82,6 +82,13 @@ static NSDictionary *PGKernels(void) {
"kernel vec4 pgMaskedMix(__sample source,__sample effect,__sample mask,float amount) {"
"vec4 s=unpremultiply(source); vec4 e=unpremultiply(effect);"
"return premultiply(vec4(mix(s.rgb,e.rgb,clamp(mask.r*amount,0.0,1.0)),s.a)); }");
+ k[@"colorCoverage"] = PGKernel(@
+ "kernel vec4 pgColorCoverage(__sample coverage,__sample image,float invert) {"
+ "vec3 c=unpremultiply(image).rgb; float y=dot(c,vec3(0.299,0.587,0.114));"
+ "float cb=(c.b-y)*0.564+0.5; float cr=(c.r-y)*0.713+0.5;"
+ "float likelihood=exp(-pow((cb-0.415)/0.115,2.0)-pow((cr-0.565)/0.135,2.0));"
+ "float v=clamp(coverage.r,0.0,1.0)*smoothstep(0.05,0.60,likelihood);"
+ "v=mix(v,1.0-v,invert); return vec4(v,v,v,1.0); }");
k[@"even"] = PGKernel(@
"kernel vec4 pgEven(__sample source,__sample broad,float amount) {"
"vec4 s=unpremultiply(source); vec3 b=unpremultiply(broad).rgb;"
@@ -133,6 +140,11 @@ static NSDictionary *PGKernels(void) {
- (CIImage *)processImage:(CIImage *)image settings:(PGBeautySettings)s
skinMask:(CIImage *)skinMask teethMask:(CIImage *)teethMask
eyeMask:(CIImage *)eyeMask error:(NSError **)error {
+ return [self processImage:image settings:s skinMask:skinMask colorMask:nil teethMask:teethMask eyeMask:eyeMask error:error];
+}
+- (CIImage *)processImage:(CIImage *)image settings:(PGBeautySettings)s
+ skinMask:(CIImage *)skinMask colorMask:(CIImage *)colorMask
+ teethMask:(CIImage *)teethMask eyeMask:(CIImage *)eyeMask error:(NSError **)error {
if (error) *error = nil;
CGRect extent = image.extent;
if (!image || CGRectIsEmpty(extent) || CGRectIsInfinite(extent) ||
@@ -145,7 +157,7 @@ static NSDictionary *PGKernels(void) {
float mix = PGClamp(s.mix, 0, 1);
if (mix == 0 && !s.showMask) return image;
NSDictionary *k = PGKernels();
- if (k.count != 10) {
+ if (k.count != 11) {
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:2
userInfo:@{NSLocalizedDescriptionKey:@"The beauty image kernels could not be compiled."}];
return nil;
@@ -188,6 +200,18 @@ static NSDictionary *PGKernels(void) {
float invert = s.debugChannel==1 || s.debugChannel==2 ? (s.invertMask ? 1 : 0) : 0;
return PGApply(k[@"debug"],extent,@[image,preview,@(invert)]);
}
+ // Detail protection may contain small eye, brow, nose and lip cutouts.
+ // Exposure and color must not reuse those holes or sharpen a face ellipse
+ // with the detail threshold; that creates visible rings and islands.
+ CIImage *toneMask=mask;
+ BOOL changesColor=brightness>0 || hue!=0 || saturation!=0 || even>0 ||
+ (s.skinTonePreset>0 && s.skinTonePreset<=6);
+ if(colorMask && changesColor) {
+ CGFloat feather=MAX(3,MIN(extent.size.width,extent.size.height)*0.03);
+ CIImage *support=[colorMask imageByCroppingToRect:extent];
+ toneMask=PGApply(k[@"colorCoverage"],extent,@[PGBlur(support,feather,extent),
+ PGBlur(image,MAX(1,feather*0.15),extent),@(s.invertMask?1:0)]);
+ }
CIImage *result = image;
if (local*amount>0) {
// The shipped helper derives both blur scales from the detail strength.
@@ -207,12 +231,12 @@ static NSDictionary *PGKernels(void) {
if (hue!=0) color = [color imageByApplyingFilter:@"CIHueAdjust" withInputParameters:@{kCIInputAngleKey:@(hue*M_PI/180.0)}];
if (saturation!=0) color = [color imageByApplyingFilter:@"CIColorControls" withInputParameters:@{kCIInputSaturationKey:@(1+saturation/100.f)}];
if (even>0) color = PGApply(k[@"even"],extent,@[color,PGBlur(color,radius*1.8,extent),@(even)]);
- if (color!=result) result = PGApply(k[@"maskedMix"],extent,@[result,color,mask,@1]);
+ if (color!=result) result = PGApply(k[@"maskedMix"],extent,@[result,color,toneMask,@1]);
if (sharpness*amount>0) result = [result imageByApplyingFilter:@"CISharpenLuminance"
withInputParameters:@{kCIInputSharpnessKey:@(sharpness*amount)}];
if (s.skinTonePreset>0 && s.skinTonePreset<=6) {
CIImage *preset = PGApply(k[@"preset"],extent,@[result,@(s.skinTonePreset)]);
- result = PGApply(k[@"maskedMix"],extent,@[result,preset,mask,@1]);
+ result = PGApply(k[@"maskedMix"],extent,@[result,preset,toneMask,@1]);
}
if (global>0) {
CGFloat fine = fmin(7.4,2.275+3.15*pow(global,0.72)+0.95*global);
diff --git a/beatygril/beatygril/Plugin/PGFaceProcessor.h b/beatygril/beatygril/Plugin/PGFaceProcessor.h
index 74307bb..01c3a35 100644
--- a/beatygril/beatygril/Plugin/PGFaceProcessor.h
+++ b/beatygril/beatygril/Plugin/PGFaceProcessor.h
@@ -11,6 +11,8 @@ NS_ASSUME_NONNULL_BEGIN
/// observations retain their bounding box; render geometry is stored separately.
@property (nonatomic, copy, readonly) NSArray *faces;
@property (nonatomic, strong, readonly) CIImage *skinMask;
+/// Continuous face support before eye/nose/lip detail-protection cutouts.
+@property (nonatomic, strong, readonly) CIImage *colorMask;
@property (nonatomic, strong, readonly) CIImage *teethMask;
@property (nonatomic, strong, readonly) CIImage *eyeMask;
@property (nonatomic, strong, readonly) CIImage *image;
diff --git a/beatygril/beatygril/Plugin/PGFaceProcessor.m b/beatygril/beatygril/Plugin/PGFaceProcessor.m
index ba361e4..5216715 100644
--- a/beatygril/beatygril/Plugin/PGFaceProcessor.m
+++ b/beatygril/beatygril/Plugin/PGFaceProcessor.m
@@ -10,6 +10,7 @@ static NSString * const PGFaceErrorDomain = @"media.dayu.PaintFCP.Face";
@interface PGFaceFrame ()
@property (nonatomic, copy, readwrite) NSArray *faces;
@property (nonatomic, strong, readwrite) CIImage *skinMask;
+@property (nonatomic, strong, readwrite) CIImage *colorMask;
@property (nonatomic, strong, readwrite) CIImage *teethMask;
@property (nonatomic, strong, readwrite) CIImage *eyeMask;
@property (nonatomic, strong, readwrite) CIImage *image;
@@ -340,15 +341,17 @@ static CIImage *PGMaximum(CIImage *a, CIImage *b) {
overlay = [outline imageByCompositingOverImage:overlay];
}
}
+ CIImage *colorCoverage=skin;
if (settings.featureProtection) skin = [_subtract applyWithExtent:extent arguments:@[skin, protection]];
PGFaceFrame *frame = [[PGFaceFrame alloc] init];
frame.faces = observations ?: @[];
frame.image = [self warpImage:image geometry:geometry settings:settings extent:extent];
frame.skinMask = [self warpImage:skin geometry:geometry settings:settings extent:extent];
+ frame.colorMask = [self warpImage:colorCoverage geometry:geometry settings:settings extent:extent];
frame.teethMask = [self warpImage:teeth geometry:geometry settings:settings extent:extent];
frame.eyeMask = [self warpImage:eyes geometry:geometry settings:settings extent:extent];
frame.overlayImage = [overlay imageByCroppingToRect:extent];
- if (!frame.image || !frame.skinMask || !frame.teethMask || !frame.eyeMask) {
+ if (!frame.image || !frame.skinMask || !frame.colorMask || !frame.teethMask || !frame.eyeMask) {
if (error) *error = [NSError errorWithDomain:PGFaceErrorDomain code:3 userInfo:@{NSLocalizedDescriptionKey:@"Unable to apply face geometry."}];
return nil;
}
diff --git a/beatygril/beatygril/Plugin/PGFaceSelectOSC.m b/beatygril/beatygril/Plugin/PGFaceSelectOSC.m
index e6ebc07..7de0903 100644
--- a/beatygril/beatygril/Plugin/PGFaceSelectOSC.m
+++ b/beatygril/beatygril/Plugin/PGFaceSelectOSC.m
@@ -2,9 +2,12 @@
#import
#import
#import "PGParameterIDs.h"
+#include
@interface PGFaceSelectOSC : NSObject
@property (assign) id apiManager;
+@property BOOL dragging;
+@property BOOL undoOpen;
@end
@implementation PGFaceSelectOSC
- (instancetype)initWithAPIManager:(id)api {
@@ -14,21 +17,33 @@
- (id)oscAPI {return [_apiManager apiForProtocol:@protocol(FxOnScreenControlAPI_v4)];}
- (BOOL)selectionEnabledAtTime:(CMTime)time {
BOOL selected=NO;id api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
- [api getBoolValue:&selected fromParameter:PGSingleFace atTime:time];return selected;
+ return api && [api getBoolValue:&selected fromParameter:PGSingleFace atTime:time] && selected;
+}
+- (BOOL)validObjectBounds:(CGRect *)bounds {
+ id api=[self oscAPI];
+ if(!api || !bounds) return NO;
+ CGRect b=[api objectBounds];
+ if(!isfinite(b.origin.x)||!isfinite(b.origin.y)||!isfinite(b.size.width)||!isfinite(b.size.height)||b.size.width<=0||b.size.height<=0) return NO;
+ *bounds=b;return YES;
}
- (void)drawOSCWithWidth:(NSInteger)width height:(NSInteger)height activePart:(NSInteger)part destinationImage:(FxImageTile *)tile atTime:(CMTime)time {
if(!tile.ioSurface||width<=0||height<=0) return;
- CIImage *output=[[CIImage imageWithColor:CIColor.clearColor] imageByCroppingToRect:CGRectMake(0,0,width,height)];
- if([self selectionEnabledAtTime:time]) {
+ size_t surfaceW=IOSurfaceGetWidth((__bridge IOSurfaceRef)tile.ioSurface),surfaceH=IOSurfaceGetHeight((__bridge IOSurfaceRef)tile.ioSurface);
+ if(!surfaceW||!surfaceH) return;
+ CIImage *output=[[CIImage imageWithColor:CIColor.clearColor] imageByCroppingToRect:CGRectMake(0,0,surfaceW,surfaceH)];
+ CGRect bounds=CGRectZero;
+ if([self selectionEnabledAtTime:time] && [self validObjectBounds:&bounds]) {
double x=.5,y=.5;
id params=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
- [params getXValue:&x YValue:&y fromParameter:PGSelection atTime:time];
- CGRect bounds=[[self oscAPI] objectBounds];double canvasX=width*.5,canvasY=height*.5;
- [[self oscAPI] convertPointFromSpace:kFxDrawingCoordinates_OBJECT fromX:bounds.origin.x+x*bounds.size.width fromY:bounds.origin.y+y*bounds.size.height toSpace:kFxDrawingCoordinates_CANVAS toX:&canvasX toY:&canvasY];
- CGFloat scale=MAX(1,[[self oscAPI] backingScaleFactor]);
- size_t surfaceW=IOSurfaceGetWidth((__bridge IOSurfaceRef)tile.ioSurface),surfaceH=IOSurfaceGetHeight((__bridge IOSurfaceRef)tile.ioSurface);
- CGFloat sx=(CGFloat)surfaceW/width,sy=(CGFloat)surfaceH/height;
- canvasX*=sx;canvasY*=sy;CGFloat radius=10*scale;
+ BOOL valid=[params getXValue:&x YValue:&y fromParameter:PGSelection atTime:time] && isfinite(x) && isfinite(y);
+ double canvasX=NAN,canvasY=NAN;
+ if(valid) [[self oscAPI] convertPointFromSpace:kFxDrawingCoordinates_OBJECT fromX:bounds.origin.x+x*bounds.size.width fromY:bounds.origin.y+y*bounds.size.height toSpace:kFxDrawingCoordinates_CANVAS toX:&canvasX toY:&canvasY];
+ // CANVAS coordinates already address the OSC surface. Callback width and
+ // height may describe the effect image and must not rescale this point.
+ double backing=[[self oscAPI] backingScaleFactor];
+ CGFloat scale=isfinite(backing)?MAX(1,backing):1;
+ CGFloat radius=10*scale;
+ if(valid && isfinite(canvasX) && isfinite(canvasY)) {
CIColor *color=part?[CIColor colorWithRed:1 green:.8 blue:.2]:[CIColor colorWithRed:.1 green:.9 blue:.8];
CIImage *ring=[CIFilter filterWithName:@"CIRadialGradient" withInputParameters:@{@"inputCenter":[CIVector vectorWithX:canvasX Y:canvasY],@"inputRadius0":@(radius-2),@"inputRadius1":@(radius),@"inputColor0":color,@"inputColor1":CIColor.clearColor}].outputImage;
CIImage *hole=[CIFilter filterWithName:@"CIRadialGradient" withInputParameters:@{@"inputCenter":[CIVector vectorWithX:canvasX Y:canvasY],@"inputRadius0":@(radius-3),@"inputRadius1":@(radius-2),@"inputColor0":CIColor.blackColor,@"inputColor1":CIColor.whiteColor}].outputImage;
@@ -36,6 +51,7 @@
CIImage *v=[[CIImage imageWithColor:color] imageByCroppingToRect:CGRectMake(canvasX-scale,canvasY-16*scale,2*scale,32*scale)];
CIImage *h=[[CIImage imageWithColor:color] imageByCroppingToRect:CGRectMake(canvasX-16*scale,canvasY-scale,32*scale,2*scale)];
output=[[h imageByCompositingOverImage:v] imageByCompositingOverImage:ring];
+ }
}
CIContext *context=[CIContext contextWithOptions:@{kCIContextCacheIntermediates:@NO}];
size_t w=IOSurfaceGetWidth((__bridge IOSurfaceRef)tile.ioSurface),h=IOSurfaceGetHeight((__bridge IOSurfaceRef)tile.ioSurface);
@@ -43,21 +59,38 @@
[context render:output toIOSurface:(__bridge IOSurfaceRef)tile.ioSurface bounds:CGRectMake(0,0,w,h) colorSpace:tile.colorSpace];
}
- (void)hitTestOSCAtMousePositionX:(double)x mousePositionY:(double)y activePart:(NSInteger *)part atTime:(CMTime)time {
- if(part) *part=([self selectionEnabledAtTime:time]&&CGRectContainsPoint([[self oscAPI] objectBounds],CGPointMake(x,y)))?1:0;
+ if(!part) return;*part=0;CGRect bounds=CGRectZero;
+ if(isfinite(x)&&isfinite(y)&&[self selectionEnabledAtTime:time]&&[self validObjectBounds:&bounds]&&CGRectContainsPoint(bounds,CGPointMake(x,y))) *part=1;
}
- (void)setSelectionX:(double)x y:(double)y time:(CMTime)time update:(BOOL *)update {
- CGRect bounds=[[self oscAPI] objectBounds];if(bounds.size.width<=0||bounds.size.height<=0) return;
+ if(update) *update=NO;
+ CGRect bounds=CGRectZero;
+ if(!isfinite(x)||!isfinite(y)||![self validObjectBounds:&bounds]) return;
id api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
BOOL ok=[api setXValue:MIN(1,MAX(0,(x-bounds.origin.x)/bounds.size.width)) YValue:MIN(1,MAX(0,(y-bounds.origin.y)/bounds.size.height)) toParameter:PGSelection atTime:time];
if(update) *update=ok;
}
- (void)mouseDownAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {
- id undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];[undo startUndoGroup:@"选择人脸"];
- [self setSelectionX:x y:y time:time update:update];
+ if(update) *update=NO;
+ if(part!=1 || !isfinite(x) || !isfinite(y) || ![self selectionEnabledAtTime:time]) return;
+ CGRect bounds=CGRectZero;if(![self validObjectBounds:&bounds]) return;
+ id undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];
+ if(_undoOpen) [undo endUndoGroup];
+ _undoOpen=[undo startUndoGroup:@"选择人脸"];
+ BOOL changed=NO;[self setSelectionX:x y:y time:time update:&changed];
+ _dragging=changed;
+ if(!changed && _undoOpen) {[undo endUndoGroup];_undoOpen=NO;}
+ if(update) *update=changed;
+}
+- (void)mouseDraggedAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {
+ if(update) *update=NO;
+ if(_dragging && part==1 && [self selectionEnabledAtTime:time]) [self setSelectionX:x y:y time:time update:update];
}
-- (void)mouseDraggedAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {[self setSelectionX:x y:y time:time update:update];}
- (void)mouseUpAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {
- [self setSelectionX:x y:y time:time update:update];id undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];[undo endUndoGroup];
+ if(update) *update=NO;
+ if(_dragging && part==1 && [self selectionEnabledAtTime:time]) [self setSelectionX:x y:y time:time update:update];
+ _dragging=NO;
+ if(_undoOpen) {id undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];[undo endUndoGroup];_undoOpen=NO;}
}
- (void)keyDownAtPositionX:(double)x positionY:(double)y keyPressed:(unsigned short)key modifiers:(FxModifierKeys)modifiers forceUpdate:(BOOL *)update didHandle:(BOOL *)handled atTime:(CMTime)time {if(handled)*handled=NO;if(update)*update=NO;}
- (void)keyUpAtPositionX:(double)x positionY:(double)y keyPressed:(unsigned short)key modifiers:(FxModifierKeys)modifiers forceUpdate:(BOOL *)update didHandle:(BOOL *)handled atTime:(CMTime)time {if(handled)*handled=NO;if(update)*update=NO;}
diff --git a/beatygril/beatygril/Plugin/beatygrilPlugIn.m b/beatygril/beatygril/Plugin/beatygrilPlugIn.m
index f6e9dd5..3f14895 100644
--- a/beatygril/beatygril/Plugin/beatygrilPlugIn.m
+++ b/beatygril/beatygril/Plugin/beatygrilPlugIn.m
@@ -250,7 +250,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
PGFaceFrame *frame=[_faceProcessor processImage:input settings:s time:time error:&stageError];
if(!frame) { os_log_error(PGRenderLog(), "face processing failed: %{public}@",stageError); if(error)*error=stageError; return NO; }
PGBeautySettings beauty=s; beauty.mix=1;
- output=[_beautyRenderer processImage:frame.image settings:beauty skinMask:frame.skinMask teethMask:frame.teethMask eyeMask:frame.eyeMask error:&stageError];
+ output=[_beautyRenderer processImage:frame.image settings:beauty skinMask:frame.skinMask colorMask:frame.colorMask teethMask:frame.teethMask eyeMask:frame.eyeMask error:&stageError];
if(!output) { os_log_error(PGRenderLog(), "beauty processing failed: %{public}@",stageError); if(error)*error=stageError; return NO; }
overlay=frame.overlayImage;
}
diff --git a/beatygril/beatygril/Wrapper Application/Info.plist b/beatygril/beatygril/Wrapper Application/Info.plist
index c55ff47..1460397 100644
--- a/beatygril/beatygril/Wrapper Application/Info.plist
+++ b/beatygril/beatygril/Wrapper Application/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.0.1
+ 1.0.2
CFBundleVersion
- 3
+ 4
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
NSHumanReadableCopyright
diff --git a/docs/analysis/REA-分析与复刻说明.md b/docs/analysis/REA-分析与复刻说明.md
index f235713..d871318 100644
--- a/docs/analysis/REA-分析与复刻说明.md
+++ b/docs/analysis/REA-分析与复刻说明.md
@@ -20,6 +20,10 @@ REA 直接打开内嵌插件。原包为 arm64/x86_64 通用 Mach-O,Ghidra 无
|人脸与跟踪|Vision landmarks、原版 Metal 位移网格、OpenCV 动态库、历史脸框/关键点、选择及分析数据均有证据。`renderDestinationImage:` 及函数检索见插件证据包|部分回答;运动遮挡、切镜与完整身份逻辑未等价验证|
|像素一致性|未获得原版相同输入/参数的可信渲染对照|未验证,不能声称逐像素一致|
+## 原版 FCP 模板的实际发布范围
+
+补充检查用户指定的 `~/Movies/Motion Templates.localized/Effects.localized/💙MS超级美颜/MS超级美颜V2/MS超级美颜V2.moef`:共15个发布入口,包括空名自定义面板入口、脸型、脸框预览、白牙、亮眼和混合,没有跟踪目标、跟踪开关或前向/后向分析按钮。清单和模板摘要见 [发布参数记录](reference-template-parameters.json)。二进制内部的分析/跟踪代码不能据此解释成当前FCP模板已开放的功能;本项目的三目标分析与跟踪是独立扩展,没有完成原版同类功能的用户界面或运动等价验证。
+
## 已实现的功能
- 中频磨皮、强度、半径、高频细节平滑、锐化、五官保护及各大功能分组开关。
@@ -27,7 +31,7 @@ REA 直接打开内嵌插件。原包为 arm64/x86_64 通用 Mach-O,Ghidra 无
- 牙齿美白和眼睛提亮,基于 Vision 面部区域构建软蒙版。
- 瘦脸、大眼、短脸、窄脸、下巴、V 脸、下颌、颧骨、小头和小头范围。
- 全脸/选中脸、画面点击选脸、多人高速检测、防跳脸范围、脸框和跟踪框。
-- 三个独立跟踪目标,向前/向后分析,清除当前目标;精确 CMTime 数据持久化到宿主工程,可逆序和任意顺序读取。
+- 作为扩展提供三个独立跟踪目标,向前/向后分析,清除当前目标;精确 CMTime 数据持久化到宿主工程,可逆序和任意顺序读取。
- 蒙版阈值/柔度/边缘保护、皮肤/牙齿/眼睛通道预览与反转、包含几何变形的整体混合。
- 保留透明度、完整输入范围、非零图像原点与宿主上下原点;原生 FxPlug 参数可由宿主保存和制作关键帧。
diff --git a/docs/analysis/reference-template-parameters.json b/docs/analysis/reference-template-parameters.json
new file mode 100644
index 0000000..9947e2f
--- /dev/null
+++ b/docs/analysis/reference-template-parameters.json
@@ -0,0 +1,83 @@
+{
+ "path": "/Users/dayu/Movies/Motion Templates.localized/Effects.localized/💙MS超级美颜/MS超级美颜V2/MS超级美颜V2.moef",
+ "sha256": "5c20b387f6a6438614a9182d3f93f67473881d433a25f7c3b557d063f55a5d92",
+ "published_count": 15,
+ "published": [
+ {
+ "object": "10030",
+ "channel": "./210",
+ "name": ""
+ },
+ {
+ "object": "10030",
+ "channel": "./18",
+ "name": "瘦脸"
+ },
+ {
+ "object": "10030",
+ "channel": "./33",
+ "name": "大眼"
+ },
+ {
+ "object": "10030",
+ "channel": "./22",
+ "name": "短脸"
+ },
+ {
+ "object": "10030",
+ "channel": "./23",
+ "name": "窄脸"
+ },
+ {
+ "object": "10030",
+ "channel": "./24",
+ "name": "下巴"
+ },
+ {
+ "object": "10030",
+ "channel": "./25",
+ "name": "V脸"
+ },
+ {
+ "object": "10030",
+ "channel": "./26",
+ "name": "下颚骨"
+ },
+ {
+ "object": "10030",
+ "channel": "./27",
+ "name": "颧骨"
+ },
+ {
+ "object": "10030",
+ "channel": "./28",
+ "name": "小头"
+ },
+ {
+ "object": "10030",
+ "channel": "./29",
+ "name": "小头范围"
+ },
+ {
+ "object": "10030",
+ "channel": "./19",
+ "name": "人脸框预览"
+ },
+ {
+ "object": "10030",
+ "channel": "./31",
+ "name": "白牙"
+ },
+ {
+ "object": "10030",
+ "channel": "./32",
+ "name": "亮眼"
+ },
+ {
+ "object": "10030",
+ "channel": "./10001",
+ "name": "混合"
+ }
+ ],
+ "tracking_controls_published": false
+}
diff --git a/scripts/deliver.py b/scripts/deliver.py
index 75f61ed..1ab7b19 100755
--- a/scripts/deliver.py
+++ b/scripts/deliver.py
@@ -45,6 +45,13 @@ try:
raise RuntimeError('Apple did not accept notarization; original installed app retained')
run(['xcrun','stapler','staple',str(app)])
run(['xcrun','stapler','validate',str(app)])
+ # A live host retains its old XPC/OSC connection. Replacing the bundle while
+ # that connection exists can deadlock host shutdown or select stale code.
+ running=run(['ps','-axo','comm=']).splitlines()
+ hosts={'/Applications/Motion.app/Contents/MacOS/Motion',
+ '/Applications/Final Cut Pro.app/Contents/MacOS/Final Cut Pro'}
+ if hosts.intersection(line.strip() for line in running):
+ raise RuntimeError('Close Motion and Final Cut Pro before installing; installed app retained')
if installed.exists():
info=plistlib.loads((installed/'Contents/Info.plist').read_bytes())
if info.get('CFBundleIdentifier')!=bundle_id:raise RuntimeError('The installation path belongs to another product')
@@ -60,6 +67,9 @@ try:
if info.get('CFBundleIdentifier')!=bundle_id or 'TeamIdentifier=B3W25FX5H5' not in signature:raise RuntimeError('Installed app identity mismatch')
(logs/'installed-signature.txt').write_text(signature)
run(['pluginkit','-a',str(installed/'Contents/PlugIns/beatygril XPC Service.pluginkit')])
+ # Xcode may register its build product. Only the installed plugin should
+ # be discoverable while native host validation is in progress.
+ run(['pluginkit','-r',str(plugin)])
except Exception:
if installed.exists():shutil.rmtree(installed)
if previous.exists():shutil.move(str(previous),str(installed))
diff --git a/scripts/test-osc.sh b/scripts/test-osc.sh
new file mode 100755
index 0000000..ffaed0b
--- /dev/null
+++ b/scripts/test-osc.sh
@@ -0,0 +1,20 @@
+#!/bin/zsh
+set -euo pipefail
+cd "${0:A:h:h}"
+output_dir=$(mktemp -d /tmp/paintfcp-osc-tests.XXXXXX)
+trap 'rm -rf "$output_dir"' EXIT
+plugin_dir="$PWD/beatygril/beatygril/Plugin"
+sdk_frameworks=/Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks
+runtime_frameworks=/Library/Developer/Frameworks
+compiler_extra=()
+if [[ "${PAINTFCP_OSC_ASAN:-0}" == 1 ]]; then
+ compiler_extra=(-fsanitize=address -g)
+fi
+xcrun clang "${compiler_extra[@]}" -fobjc-arc -fmodules -Wall -Wextra -Wno-unused-parameter \
+ -mmacosx-version-min=13.0 -I "$plugin_dir" -F "$sdk_frameworks" -F "$runtime_frameworks" \
+ -Wl,-rpath,"$runtime_frameworks" \
+ "$plugin_dir/PGFaceSelectOSC.m" tests/osc_tests.m \
+ -framework Foundation -framework CoreImage -framework CoreGraphics \
+ -framework CoreMedia -framework CoreVideo -framework IOSurface \
+ -framework FxPlug -framework PluginManager -o "$output_dir/osc-tests"
+"$output_dir/osc-tests" "$plugin_dir/Info.plist"
diff --git a/tests/beauty_render_tests.m b/tests/beauty_render_tests.m
index 98a7264..24050fb 100644
--- a/tests/beauty_render_tests.m
+++ b/tests/beauty_render_tests.m
@@ -103,6 +103,30 @@ int main(void) { @autoreleasepool {
s=PGBeautyDefaults();s.midtoneDetail=1;s.maskSoftness=0;s.maskEdgeProtect=0;s.featureProtection=1;
float *protected=Render(context,[renderer processImage:source settings:s skinMask:white teethMask:nil eyeMask:half error:&error],extent);
Check(Difference(base,protected,6,40)<0.00001 && Texture(protected,56,88)0.08 && Difference(base,continuous,56,88)>0.08,@"Skin exposure does not leave dark detail-protection holes");
+ s.featureProtection=0;
+ float *unprotectedColor=Render(context,[renderer processImage:source settings:s skinMask:half colorMask:white teethMask:half eyeMask:half error:&error],extent);
+ Check(Difference(continuous,unprotectedColor,4,W-4)<0.00001,@"Feature protection changes detail, not face exposure coverage");
+ free(continuous);free(unprotectedColor);
+ CIImage *flatSkin=[[CIImage imageWithColor:[CIColor colorWithRed:0.56 green:0.40 blue:0.31 alpha:1]] imageByCroppingToRect:extent];
+ float *flatPixels=Render(context,flatSkin,extent);
+ float *feathered=Render(context,[renderer processImage:flatSkin settings:s skinMask:half colorMask:half teethMask:nil eyeMask:nil error:&error],extent);
+ float largestStep=0;
+ for(int x=42;x<54;x++) {
+ int i=(32*W+x)*4,j=i+4;
+ largestStep=fmaxf(largestStep,fabsf((feathered[i]-flatPixels[i])-(feathered[j]-flatPixels[j])));
+ }
+ if(largestStep>=0.09) fprintf(stderr,"Face exposure boundary maximum step: %.6f\n",largestStep);
+ Check(largestStep<0.09,@"Face color coverage feathers a hard support boundary without a visible exposure step");
+ Check(Difference(flatPixels,feathered,64,88)<0.00001,@"Feathered face exposure stays local");free(feathered);free(flatPixels);
+ CIImage *blueHair=[[CIImage imageWithColor:[CIColor colorWithRed:0.03 green:0.16 blue:0.70 alpha:1]] imageByCroppingToRect:extent];
+ float *blueBefore=Render(context,blueHair,extent);
+ float *blueAfter=Render(context,[renderer processImage:blueHair settings:s skinMask:white colorMask:white teethMask:nil eyeMask:nil error:&error],extent);
+ Check(Difference(blueBefore,blueAfter,4,W-4)<0.00001,@"Skin exposure excludes saturated blue hair within face support");free(blueBefore);free(blueAfter);
s=PGBeautyDefaults();s.showMask=1;s.maskSoftness=0;s.maskEdgeProtect=0;s.maskThreshold=255;
float *strictMask=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:nil eyeMask:nil error:&error],extent);
s.maskThreshold=0;
diff --git a/tests/face_tests.m b/tests/face_tests.m
index c2fda37..e1b7420 100644
--- a/tests/face_tests.m
+++ b/tests/face_tests.m
@@ -79,6 +79,8 @@ int main(int argc, const char *argv[]) {
PGBeautySettings unprotected = settings; unprotected.featureProtection = 0;
PGFaceFrame *withoutProtection = [processor processImage:image settings:unprotected time:kCMTimeZero error:&error];
Check(Channel(context, frame.skinMask, 23, 71, 0) < Channel(context, withoutProtection.skinMask, 23, 71, 0), @"feature protection removes eye from skin smoothing");
+ Check([Pixels(context,frame.colorMask) isEqualToData:Pixels(context,withoutProtection.colorMask)], @"color support remains continuous when detail protection is toggled");
+ Check(Channel(context,frame.colorMask,23,71,0)>Channel(context,frame.skinMask,23,71,0), @"color support retains coverage inside eye protection cutouts");
settings.singleFace = 1; settings.selectionX = 0.78; settings.selectionY = 0.45;
frame = [processor processImage:image settings:settings time:kCMTimeZero error:&error];
diff --git a/tests/osc_tests.m b/tests/osc_tests.m
new file mode 100644
index 0000000..8b74ebc
--- /dev/null
+++ b/tests/osc_tests.m
@@ -0,0 +1,116 @@
+#import
+#import
+#import
+#import
+#import
+#import "PGParameterIDs.h"
+#include
+
+@interface PGFaceSelectOSC : NSObject
+- (instancetype)initWithAPIManager:(id)api;
+@end
+
+@interface PGOSCAPI : NSObject
+@property BOOL enabled, rejectRead, rejectPoint, rejectWrite, rejectUndo;
+@property BOOL disableOSC, disableParams, disableSetting, disableUndo;
+@property CGRect bounds;
+@property double x,y,backing;
+@property NSUInteger writes,undoStarts,undoEnds;
+@property double lastX,lastY;
+@property CMTime lastTime;
+@end
+@implementation PGOSCAPI
+- (instancetype)init {if((self=[super init])){_enabled=YES;_bounds=CGRectMake(-100,-50,200,100);_x=.25;_y=.75;_backing=1;}return self;}
+- (id)apiForProtocol:(Protocol *)protocol {
+ NSString *name=NSStringFromProtocol(protocol);
+ if([name isEqual:@"FxOnScreenControlAPI_v4"])return _disableOSC?nil:self;
+ if([name isEqual:@"FxParameterRetrievalAPI_v6"])return _disableParams?nil:self;
+ if([name isEqual:@"FxParameterSettingAPI_v5"])return _disableSetting?nil:self;
+ if([name isEqual:@"FxUndoAPI"])return _disableUndo?nil:self;
+ return nil;
+}
+- (BOOL)getBoolValue:(BOOL *)value fromParameter:(UInt32)identifier atTime:(CMTime)time {
+ if(identifier!=PGSingleFace||_rejectRead)return NO;*value=_enabled;return YES;
+}
+- (BOOL)getXValue:(double *)x YValue:(double *)y fromParameter:(UInt32)identifier atTime:(CMTime)time {
+ if(identifier!=PGSelection||_rejectPoint)return NO;*x=_x;*y=_y;return YES;
+}
+- (BOOL)setXValue:(double)x YValue:(double)y toParameter:(UInt32)identifier atTime:(CMTime)time {
+ if(identifier!=PGSelection||_rejectWrite)return NO;
+ _writes++;_lastX=x;_lastY=y;_lastTime=time;return YES;
+}
+- (CGRect)objectBounds {return _bounds;}
+- (double)backingScaleFactor {return _backing;}
+- (void)convertPointFromSpace:(FxDrawingCoordinates)from fromX:(double)x fromY:(double)y toSpace:(FxDrawingCoordinates)to toX:(double *)tx toY:(double *)ty {
+ *tx=x+200;*ty=y+100;
+}
+- (BOOL)startUndoGroup:(NSString *)name {_undoStarts++;return !_rejectUndo;}
+- (BOOL)endUndoGroup {_undoEnds++;return YES;}
+@end
+
+@interface PGOSCTile : FxImageTile
+@property (strong) IOSurface *surface;
+@property FxImageOrigin origin;
+@end
+@implementation PGOSCTile
+- (IOSurface *)ioSurface {return _surface;}
+- (FxImageOrigin)imageOrigin {return _origin;}
+- (CGColorSpaceRef)colorSpace {return nil;}
+@end
+static NSUInteger Checks=0,Failures=0;
+static void Check(BOOL value,NSString *message) {Checks++;if(!value){Failures++;fprintf(stderr,"FAIL: %s\n",message.UTF8String);}}
+static PGOSCTile *Tile(void) {
+ IOSurfaceRef surface=IOSurfaceCreate((__bridge CFDictionaryRef)@{(id)kIOSurfaceWidth:@320,(id)kIOSurfaceHeight:@240,(id)kIOSurfaceBytesPerElement:@4,(id)kIOSurfaceBytesPerRow:@1280,(id)kIOSurfacePixelFormat:@(kCVPixelFormatType_32BGRA),(id)kIOSurfaceAllocSize:@(320*240*4)});
+ PGOSCTile *tile=[PGOSCTile new];tile.surface=CFBridgingRelease(surface);return tile;
+}
+static float AlphaAt(PGOSCTile *tile,int x,int y) {
+ CIImage *image=[CIImage imageWithIOSurface:(__bridge IOSurfaceRef)tile.surface options:@{kCIImageColorSpace:NSNull.null}];
+ CIContext *context=[CIContext contextWithOptions:@{kCIContextWorkingColorSpace:NSNull.null,kCIContextOutputColorSpace:NSNull.null}];
+ float p[4]={0};[context render:image toBitmap:p rowBytes:sizeof(p) bounds:CGRectMake(x,y,1,1) format:kCIFormatRGBAf colorSpace:nil];return p[3];
+}
+int main(int argc,const char **argv) {@autoreleasepool {
+ PGOSCAPI *api=[PGOSCAPI new];PGFaceSelectOSC *osc=[[PGFaceSelectOSC alloc] initWithAPIManager:api];
+ CMTime time=CMTimeMake(121,24);NSInteger part=-1;BOOL update=YES,handled=YES;
+ Check([osc conformsToProtocol:@protocol(FxOnScreenControl_v4)],@"OSC class conforms to the real FxPlug v4 protocol");
+ NSArray *required=@[@"drawingCoordinates",@"drawOSCWithWidth:height:activePart:destinationImage:atTime:",@"hitTestOSCAtMousePositionX:mousePositionY:activePart:atTime:",@"mouseDownAtPositionX:positionY:activePart:modifiers:forceUpdate:atTime:",@"mouseDraggedAtPositionX:positionY:activePart:modifiers:forceUpdate:atTime:",@"mouseUpAtPositionX:positionY:activePart:modifiers:forceUpdate:atTime:",@"keyDownAtPositionX:positionY:keyPressed:modifiers:forceUpdate:didHandle:atTime:",@"keyUpAtPositionX:positionY:keyPressed:modifiers:forceUpdate:didHandle:atTime:"];
+ for(NSString *selector in required)Check([osc respondsToSelector:NSSelectorFromString(selector)],[NSString stringWithFormat:@"Required callback exists: %@",selector]);
+ Check(osc.drawingCoordinates==kFxDrawingCoordinates_OBJECT,@"Mouse callbacks use declared object coordinates");
+ NSDictionary *info=argc>1 ? [NSDictionary dictionaryWithContentsOfFile:[NSString stringWithUTF8String:argv[1]]] : nil;
+ NSArray *entries=info[@"ProPlugPlugInList"];NSDictionary *control=nil;NSMutableArray *filters=[NSMutableArray new];NSMutableSet *ids=[NSMutableSet new];BOOL unique=YES;
+ for(NSDictionary *entry in entries){if([ids containsObject:entry[@"uuid"]])unique=NO;[ids addObject:entry[@"uuid"]];if([entry[@"protocolNames"] containsObject:@"FxOnScreenControl"])control=entry;if([entry[@"protocolNames"] containsObject:@"FxFilter"])[filters addObject:entry];}
+ Check(unique && control && filters.count==1,@"Registration has unique filter and dedicated OSC identities");
+ Check([control[@"className"] isEqual:@"PGFaceSelectOSC"] && NSClassFromString(control[@"className"])==PGFaceSelectOSC.class,@"Registered OSC resolves to the compiled class");
+ Check([control[@"supportedPlugins"] isEqual:@[filters.firstObject[@"uuid"]]],@"Dedicated OSC links to the released filter UUID");
+ api.enabled=NO;[osc hitTestOSCAtMousePositionX:0 mousePositionY:0 activePart:&part atTime:time];Check(part==0,@"Disabled selection does not claim mouse events");
+ [osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==0 && api.undoStarts==0,@"Disabled mouse-down writes nothing and opens no undo group");
+ api.enabled=YES;[osc hitTestOSCAtMousePositionX:0 mousePositionY:0 activePart:&part atTime:time];Check(part==1,@"Enabled selection accepts a finite point in object bounds");
+ [osc hitTestOSCAtMousePositionX:1000 mousePositionY:0 activePart:&part atTime:time];Check(part==0,@"Outside-object hit test is empty");
+ [osc hitTestOSCAtMousePositionX:NAN mousePositionY:0 activePart:&part atTime:time];Check(part==0,@"Nonfinite hit coordinates are rejected");
+ [osc hitTestOSCAtMousePositionX:0 mousePositionY:0 activePart:NULL atTime:time];Check(YES,@"Null hit-test output is safe");
+ [osc mouseDownAtPositionX:0 positionY:0 activePart:0 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==0,@"Inactive part does not start selection");
+ update=YES;[osc mouseUpAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.undoEnds==0,@"Mouse-up without mouse-down leaves undo balanced");
+ [osc mouseDownAtPositionX:-50 positionY:25 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(update && fabs(api.lastX-.25)<1e-8 && fabs(api.lastY-.75)<1e-8,@"Mouse-down normalizes nonzero object bounds");
+ Check(CMTimeCompare(api.lastTime,time)==0 && api.undoStarts==1,@"Selection writes use callback time and open one undo group");
+ [osc mouseDraggedAtPositionX:500 positionY:-500 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(update && api.lastX==1 && api.lastY==0,@"Dragging clamps normalized selection to the source");
+ [osc mouseUpAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(update && api.undoEnds==1,@"Gesture completes its matching undo group");
+ NSUInteger writes=api.writes;[osc mouseDraggedAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==writes,@"Stray drag after release writes nothing");
+ api.bounds=CGRectMake(0,0,NAN,100);[osc hitTestOSCAtMousePositionX:0 mousePositionY:0 activePart:&part atTime:time];Check(part==0,@"Invalid host bounds produce an empty hit");
+ update=YES;[osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==writes,@"Invalid bounds initialize forceUpdate and avoid parameter writes");api.bounds=CGRectMake(-100,-50,200,100);
+ api.rejectWrite=YES;[osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.undoStarts==api.undoEnds,@"Rejected parameter write closes the opened undo group");api.rejectWrite=NO;
+ api.rejectUndo=YES;NSUInteger ends=api.undoEnds;[osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];[osc mouseUpAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(api.undoEnds==ends,@"Failed undo start is never paired with an invalid end");api.rejectUndo=NO;
+ for(int disabled=0;disabled<3;disabled++){
+ api.disableOSC=disabled==0;api.disableParams=disabled==1;api.disableSetting=disabled==2;
+ writes=api.writes;update=YES;[osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==writes,[NSString stringWithFormat:@"Missing host API %d is handled safely",disabled]);
+ }
+ api.disableOSC=api.disableParams=api.disableSetting=NO;
+ [osc keyDownAtPositionX:0 positionY:0 keyPressed:0 modifiers:0 forceUpdate:&update didHandle:&handled atTime:time];Check(!update && !handled,@"Unrecognized key-down clears outputs");update=handled=YES;
+ [osc keyUpAtPositionX:0 positionY:0 keyPressed:0 modifiers:0 forceUpdate:&update didHandle:&handled atTime:time];Check(!update && !handled,@"Unrecognized key-up clears outputs");
+ PGOSCTile *tile=Tile();api.x=.25;api.y=.75;
+ [osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];
+ Check(AlphaAt(tile,150,125)>.9,@"Canvas conversion directly positions the visible handle despite effect/surface size mismatch");
+ Check(AlphaAt(tile,25,28)<.01,@"OSC surface is not incorrectly rescaled by effect image dimensions");
+ api.enabled=NO;[osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];Check(AlphaAt(tile,150,125)<.01,@"Disabled drawing clears the prior handle");
+ api.enabled=YES;api.backing=NAN;[osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];Check(AlphaAt(tile,150,125)>.9,@"Nonfinite backing scale uses a finite fallback");
+ api.x=NAN;[osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];Check(AlphaAt(tile,150,125)<.01,@"Nonfinite stored selection does not create invalid drawing filters");
+ printf("OSC adapter: %lu checks, %lu failures\n",(unsigned long)Checks,(unsigned long)Failures);return Failures?1:0;
+}}