diff --git a/beatygril/beatygril/Plugin/PGFaceSelectOSC.m b/beatygril/beatygril/Plugin/PGFaceSelectOSC.m index 7de0903..cc89c31 100644 --- a/beatygril/beatygril/Plugin/PGFaceSelectOSC.m +++ b/beatygril/beatygril/Plugin/PGFaceSelectOSC.m @@ -8,6 +8,9 @@ @property (assign) id apiManager; @property BOOL dragging; @property BOOL undoOpen; +@property BOOL pointSetterUsesPixels; +@property BOOL loggedCoordinates; +@property BOOL loggedPointSetter; @end @implementation PGFaceSelectOSC - (instancetype)initWithAPIManager:(id)api { @@ -19,27 +22,28 @@ BOOL selected=NO;id api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]; 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; 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]) { + if([self selectionEnabledAtTime:time] && [self oscAPI]) { double x=.5,y=.5; id params=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]; 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. + // FxPlug OBJECT space is normalized 0..1, exactly like point parameters. + // objectBounds is a pixel/document rectangle and must not be applied here. + if(valid) [[self oscAPI] convertPointFromSpace:kFxDrawingCoordinates_OBJECT fromX:x fromY:y toSpace:kFxDrawingCoordinates_CANVAS toX:&canvasX toY:&canvasY]; + if(!_loggedCoordinates) { + _loggedCoordinates=YES; + CGRect objectBounds=[[self oscAPI] objectBounds]; + NSLog(@"PGFaceOSC coordinates selection=(%.9g,%.9g) objectBounds=(%.9g,%.9g,%.9g,%.9g) canvas=(%.9g,%.9g) surface=%zux%zu origin=%lu", + x,y,objectBounds.origin.x,objectBounds.origin.y,objectBounds.size.width,objectBounds.size.height, + canvasX,canvasY,surfaceW,surfaceH,(unsigned long)tile.imageOrigin); + } + // CANVAS coordinates address the OSC surface. Callback width/height can + // describe the effect image and must not rescale the converted point. double backing=[[self oscAPI] backingScaleFactor]; CGFloat scale=isfinite(backing)?MAX(1,backing):1; CGFloat radius=10*scale; @@ -59,21 +63,71 @@ [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) return;*part=0;CGRect bounds=CGRectZero; - if(isfinite(x)&&isfinite(y)&&[self selectionEnabledAtTime:time]&&[self validObjectBounds:&bounds]&&CGRectContainsPoint(bounds,CGPointMake(x,y))) *part=1; + if(!part) return;*part=0; + if(isfinite(x)&&isfinite(y)&&x>=0&&x<=1&&y>=0&&y<=1&&[self selectionEnabledAtTime:time]&&[self oscAPI]) *part=1; +} +- (BOOL)pointWrite:(id)setter x:(double)x y:(double)y time:(CMTime)time { + return setter && isfinite(x) && isfinite(y) && [setter setXValue:x YValue:y toParameter:PGSelection atTime:time]; +} +- (BOOL)readPoint:(id)reader x:(double *)x y:(double *)y time:(CMTime)time { + return reader && [reader getXValue:x YValue:y fromParameter:PGSelection atTime:time] && isfinite(*x) && isfinite(*y); +} +- (BOOL)pointPixelSize:(CGSize *)size { + id api=[self oscAPI];if(!api || !size)return NO; + CGRect bounds=[api objectBounds]; + if(!isfinite(bounds.size.width)||!isfinite(bounds.size.height)||bounds.size.width<=4||bounds.size.height<=4)return NO; + *size=bounds.size;return YES; } - (void)setSelectionX:(double)x y:(double)y time:(CMTime)time update:(BOOL *)update { 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; + if(!isfinite(x)||!isfinite(y)) return; + double nx=fmin(1,fmax(0,x)),ny=fmin(1,fmax(0,y)); + id setter=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)]; + id reader=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]; + double previousX=NAN,previousY=NAN; + if(!setter || ![self readPoint:reader x:&previousX y:&previousY time:time])return; + CGSize pixels=CGSizeZero; + if(_pointSetterUsesPixels && ![self pointPixelSize:&pixels])return; + double rawX=_pointSetterUsesPixels ? (nx-.5)*pixels.width : nx; + double rawY=_pointSetterUsesPixels ? (ny-.5)*pixels.height : ny; + BOOL accepted=[self pointWrite:setter x:rawX y:rawY time:time]; + BOOL changed=accepted,pixelUnits=_pointSetterUsesPixels; + double actualX=NAN,actualY=NAN; + BOOL readBack=accepted && [self readPoint:reader x:&actualX y:&actualY time:time]; + const double tolerance=0.00001; + BOOL verified=readBack && fabs(actualX-nx)<=tolerance && fabs(actualY-ny)<=tolerance; + // Motion's FxPlug 4 bridge can normalize point reads while its setter still + // writes centered pixel values directly to OZChannel2D. Detect that exact + // response instead of choosing units from the app version or coordinate size. + if(readBack && !verified && !_pointSetterUsesPixels && [self pointPixelSize:&pixels] && + fabs(actualX-(.5+nx/pixels.width))<=tolerance && + fabs(actualY-(.5+ny/pixels.height))<=tolerance) { + pixelUnits=YES; + rawX=(nx-.5)*pixels.width;rawY=(ny-.5)*pixels.height; + accepted=[self pointWrite:setter x:rawX y:rawY time:time];changed=changed||accepted; + actualX=actualY=NAN; + readBack=accepted && [self readPoint:reader x:&actualX y:&actualY time:time]; + verified=readBack && fabs(actualX-nx)<=tolerance && fabs(actualY-ny)<=tolerance; + if(verified)_pointSetterUsesPixels=YES; + } + if(!verified && changed) { + // Restore the previous point when verification fails. A failure must not + // look like a successful drag or silently retain the calibration point. + double restoreX=pixelUnits ? (previousX-.5)*pixels.width : previousX; + double restoreY=pixelUnits ? (previousY-.5)*pixels.height : previousY; + [self pointWrite:setter x:restoreX y:restoreY time:time]; + } + if(!_loggedPointSetter) { + _loggedPointSetter=YES; + NSLog(@"PGFaceOSC point-write desired=(%.9g,%.9g) setter=(%.9g,%.9g) readback=(%.9g,%.9g) mode=%@ verified=%d", + nx,ny,rawX,rawY,actualX,actualY,_pointSetterUsesPixels?@"centered-pixels":@"normalized",verified); + } + if(update)*update=verified; } - (void)mouseDownAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time { if(update) *update=NO; if(part!=1 || !isfinite(x) || !isfinite(y) || ![self selectionEnabledAtTime:time]) return; - CGRect bounds=CGRectZero;if(![self validObjectBounds:&bounds]) return; + if(![self oscAPI]) return; id undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)]; if(_undoOpen) [undo endUndoGroup]; _undoOpen=[undo startUndoGroup:@"选择人脸"]; diff --git a/tests/osc_tests.m b/tests/osc_tests.m index 8b74ebc..1ed92e0 100644 --- a/tests/osc_tests.m +++ b/tests/osc_tests.m @@ -12,11 +12,13 @@ @interface PGOSCAPI : NSObject @property BOOL enabled, rejectRead, rejectPoint, rejectWrite, rejectUndo; -@property BOOL disableOSC, disableParams, disableSetting, disableUndo; +@property BOOL disableOSC, disableParams, disableSetting, disableUndo,pixelSetter; +@property BOOL corruptReadback; @property CGRect bounds; @property double x,y,backing; @property NSUInteger writes,undoStarts,undoEnds; -@property double lastX,lastY; +@property double lastX,lastY,lastConvertX,lastConvertY; +@property NSUInteger objectBoundsReads; @property CMTime lastTime; @end @implementation PGOSCAPI @@ -37,12 +39,17 @@ } - (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; + _writes++;_lastX=x;_lastY=y; + _x=_pixelSetter ? .5+x/_bounds.size.width : x; + _y=_pixelSetter ? .5+y/_bounds.size.height : y; + if(_corruptReadback){_x=.12345;_y=.87654;} + _lastTime=time;return YES; } -- (CGRect)objectBounds {return _bounds;} +- (CGRect)objectBounds {_objectBoundsReads++;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; + // Match the SDK contract: OBJECT inputs are normalized; CANVAS is pixels. + _lastConvertX=x;_lastConvertY=y;*tx=x*320;*ty=y*240; } - (BOOL)startUndoGroup:(NSString *)name {_undoStarts++;return !_rejectUndo;} - (BOOL)endUndoGroup {_undoEnds++;return YES;} @@ -83,19 +90,19 @@ int main(int argc,const char **argv) {@autoreleasepool { 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"); + api.enabled=YES;[osc hitTestOSCAtMousePositionX:0 mousePositionY:0 activePart:&part atTime:time];Check(part==1,@"Enabled selection accepts a finite normalized object point"); [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"); + [osc mouseDownAtPositionX:.25 positionY:.75 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(update && fabs(api.lastX-.25)<1e-8 && fabs(api.lastY-.75)<1e-8,@"OBJECT mouse-down writes normalized point coordinates directly"); 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.bounds=CGRectMake(0,0,NAN,100);[osc hitTestOSCAtMousePositionX:.5 mousePositionY:.5 activePart:&part atTime:time];Check(part==1,@"Normalized hit testing does not depend on document pixel bounds"); + update=YES;[osc mouseDownAtPositionX:NAN positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==writes,@"Nonfinite event initializes forceUpdate and avoids 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++){ @@ -107,10 +114,36 @@ int main(int argc,const char **argv) {@autoreleasepool { [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,80,180)>.9,@"Normalized selection converts to correct CANVAS pixels despite effect/surface size mismatch"); + Check(fabs(api.lastConvertX-.25)<1e-8 && fabs(api.lastConvertY-.75)<1e-8,@"Host receives normalized OBJECT coordinates rather than document pixel coordinates"); 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"); + api.enabled=NO;[osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];Check(AlphaAt(tile,80,180)<.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,80,180)>.9,@"Nonfinite backing scale uses a finite fallback"); + api.x=.5;api.y=.5;api.backing=1; + [osc drawOSCWithWidth:1921 height:1080 activePart:0 destinationImage:tile atTime:time]; + Check(AlphaAt(tile,160,120)>.9 && AlphaAt(tile,0,0)<.01,@"Default .5/.5 selection draws in the center instead of a corner"); + Check(api.objectBoundsReads==1,@"Drawing reads object bounds once for diagnostics and never uses them to scale the normalized point"); + api.x=NAN;[osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];Check(AlphaAt(tile,80,180)<.01,@"Nonfinite stored selection does not create invalid drawing filters"); + api.x=.5;api.y=.5; + [osc mouseDownAtPositionX:.8 positionY:.3 activePart:1 modifiers:0 forceUpdate:&update atTime:time]; + [osc mouseUpAtPositionX:.8 positionY:.3 activePart:1 modifiers:0 forceUpdate:&update atTime:time]; + [osc drawOSCWithWidth:1921 height:1080 activePart:0 destinationImage:tile atTime:time]; + Check(fabs(api.x-.8)<1e-8 && fabs(api.y-.3)<1e-8 && AlphaAt(tile,256,72)>.9,@"Click writes normalized selection and redraws the handle at that point"); + PGOSCAPI *motion=[PGOSCAPI new];motion.pixelSetter=YES;motion.bounds=CGRectMake(-1920,-1080,3840,2160); + PGFaceSelectOSC *motionOSC=[[PGFaceSelectOSC alloc] initWithAPIManager:motion]; + [motionOSC mouseDownAtPositionX:.8 positionY:.3 activePart:1 modifiers:0 forceUpdate:&update atTime:time]; + Check(update && fabs(motion.x-.8)<1e-5 && fabs(motion.y-.3)<1e-5,@"Motion pixel-setter bridge is detected by verified parameter readback"); + Check(motion.writes==2 && fabs(motion.lastX-1152)<1e-5 && fabs(motion.lastY+432)<1e-5,@"Detected bridge receives the centered pixel inverse transform"); + NSUInteger beforeCached=motion.writes; + [motionOSC mouseDraggedAtPositionX:.7 positionY:.4 activePart:1 modifiers:0 forceUpdate:&update atTime:time]; + Check(update && motion.writes==beforeCached+1 && fabs(motion.x-.7)<1e-5 && fabs(motion.y-.4)<1e-5,@"Pixel-setter mode is cached only on the same OSC instance"); + [motionOSC mouseUpAtPositionX:.7 positionY:.4 activePart:1 modifiers:0 forceUpdate:&update atTime:time]; + [motionOSC drawOSCWithWidth:3840 height:2160 activePart:0 destinationImage:tile atTime:time]; + Check(AlphaAt(tile,224,96)>.9,@"Motion-compatible setter and normalized drawing round-trip to the same visible point"); + Check(motion.undoStarts==1 && motion.undoEnds==1,@"Calibration writes remain in one balanced undo gesture"); + PGOSCAPI *bad=[PGOSCAPI new];bad.corruptReadback=YES; + PGFaceSelectOSC *badOSC=[[PGFaceSelectOSC alloc] initWithAPIManager:bad]; + [badOSC mouseDownAtPositionX:.8 positionY:.3 activePart:1 modifiers:0 forceUpdate:&update atTime:time]; + Check(!update && bad.undoStarts==bad.undoEnds,@"Unrecognized readback response is reported as failure with balanced undo"); printf("OSC adapter: %lu checks, %lu failures\n",(unsigned long)Checks,(unsigned long)Failures);return Failures?1:0; }}