From f13d54a850bf5a3e1d38430cfc85c39d3e358172 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Fri, 4 Oct 2019 12:45:49 +0300 Subject: [PATCH] feat: implement getBBox, getCTM, getScreenCTM make callback last argument, unless options and optional --- .../main/java/com/horcrux/svg/GroupView.java | 4 +- .../main/java/com/horcrux/svg/ImageView.java | 1 - .../horcrux/svg/RNSVGRenderableManager.java | 67 +++++++++++++++- .../java/com/horcrux/svg/RenderableView.java | 53 +++++++----- .../main/java/com/horcrux/svg/SvgView.java | 2 +- .../main/java/com/horcrux/svg/TSpanView.java | 15 +--- .../java/com/horcrux/svg/VirtualView.java | 4 + ios/Elements/RNSVGGroup.m | 14 +++- ios/Elements/RNSVGImage.m | 9 +++ ios/Elements/RNSVGUse.m | 7 ++ ios/RNSVGNode.h | 4 + ios/RNSVGRenderable.m | 8 +- ios/ViewManagers/RNSVGRenderableManager.m | 80 ++++++++++++++++++- src/elements/Shape.tsx | 35 +++++--- 14 files changed, 244 insertions(+), 59 deletions(-) diff --git a/android/src/main/java/com/horcrux/svg/GroupView.java b/android/src/main/java/com/horcrux/svg/GroupView.java index 27e0e07b0..b19f9fb93 100644 --- a/android/src/main/java/com/horcrux/svg/GroupView.java +++ b/android/src/main/java/com/horcrux/svg/GroupView.java @@ -224,7 +224,9 @@ int hitTest(final float[] src) { if (clipPath != null) { if (mClipRegionPath != clipPath) { mClipRegionPath = clipPath; - mClipRegion = getRegion(clipPath); + mClipBounds = new RectF(); + clipPath.computeBounds(mClipBounds, true); + mClipRegion = getRegion(clipPath, mClipBounds); } if (!mClipRegion.contains(x, y)) { return -1; diff --git a/android/src/main/java/com/horcrux/svg/ImageView.java b/android/src/main/java/com/horcrux/svg/ImageView.java index ce4efb2c8..1386b7428 100644 --- a/android/src/main/java/com/horcrux/svg/ImageView.java +++ b/android/src/main/java/com/horcrux/svg/ImageView.java @@ -203,7 +203,6 @@ private void doRender(Canvas canvas, Paint paint, Bitmap bitmap, float opacity) Paint alphaPaint = new Paint(); alphaPaint.setAlpha((int) (opacity * 255)); canvas.drawBitmap(bitmap, null, vbRect, alphaPaint); - //noinspection deprecation mCTM.mapRect(vbRect); this.setClientRect(vbRect); } diff --git a/android/src/main/java/com/horcrux/svg/RNSVGRenderableManager.java b/android/src/main/java/com/horcrux/svg/RNSVGRenderableManager.java index 036cf9daf..7b7a57017 100644 --- a/android/src/main/java/com/horcrux/svg/RNSVGRenderableManager.java +++ b/android/src/main/java/com/horcrux/svg/RNSVGRenderableManager.java @@ -9,7 +9,10 @@ package com.horcrux.svg; +import android.graphics.Matrix; +import android.graphics.Path; import android.graphics.PathMeasure; +import android.graphics.RectF; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; @@ -69,9 +72,9 @@ public void run() { @SuppressWarnings("unused") @ReactMethod public void isPointInFill(int tag, ReadableMap options, Callback successCallback) { - float x = (float)options.getDouble("x"); - float y = (float)options.getDouble("y"); - float[] src = new float[] { x, y }; + float x = (float) options.getDouble("x"); + float y = (float) options.getDouble("y"); + float[] src = new float[]{x, y}; isPointInFill(tag, src, successCallback, 0); } @@ -86,7 +89,7 @@ public void getTotalLength(int tag, Callback successCallback) { @SuppressWarnings("unused") @ReactMethod public void getPointAtLength(int tag, ReadableMap options, Callback successCallback) { - float length = (float)options.getDouble("length"); + float length = (float) options.getDouble("length"); RenderableView svg = RenderableViewManager.getRenderableViewByTag(tag); PathMeasure pm = new PathMeasure(svg.getPath(null, null), false); float pathLength = pm.getLength(); @@ -95,4 +98,60 @@ public void getPointAtLength(int tag, ReadableMap options, Callback successCallb pm.getPosTan(Math.max(0, Math.min(length, pathLength)), pos, tan); successCallback.invoke(pos[0], pos[1]); } + + @SuppressWarnings("unused") + @ReactMethod + public void getBBox(int tag, ReadableMap options, Callback successCallback) { + boolean fill = options.getBoolean("fill"); + boolean stroke = options.getBoolean("stroke"); + boolean markers = options.getBoolean("markers"); + boolean clipped = options.getBoolean("clipped"); + RenderableView svg = RenderableViewManager.getRenderableViewByTag(tag); + Path path = svg.getPath(null, null); + svg.initBounds(); + RectF bounds = new RectF(); + if (fill) { + bounds.union(svg.mFillBounds); + } + if (stroke) { + bounds.union(svg.mStrokeBounds); + } + if (markers) { + bounds.union(svg.mMarkerBounds); + } + if (clipped) { + bounds.intersect(svg.mClipBounds); + } + successCallback.invoke(bounds.left, bounds.top, bounds.width(), bounds.height()); + } + + @SuppressWarnings("unused") + @ReactMethod + public void getCTM(int tag, Callback successCallback) { + RenderableView svg = RenderableViewManager.getRenderableViewByTag(tag); + Matrix screenCTM = svg.mCTM; + Matrix invViewBox = svg.getSvgView().mInvViewBoxMatrix; + Matrix ctm = new Matrix(screenCTM); + ctm.preConcat(invViewBox); + + float[] values = new float[9]; + ctm.getValues(values); + successCallback.invoke( + values[Matrix.MSCALE_X], values[Matrix.MSKEW_X], values[Matrix.MTRANS_X], + values[Matrix.MSKEW_Y], values[Matrix.MSCALE_Y], values[Matrix.MTRANS_Y] + ); + } + + @SuppressWarnings("unused") + @ReactMethod + public void getScreenCTM(int tag, Callback successCallback) { + RenderableView svg = RenderableViewManager.getRenderableViewByTag(tag); + Matrix screenCTM = svg.mCTM; + float[] values = new float[9]; + screenCTM.getValues(values); + successCallback.invoke( + values[Matrix.MSCALE_X], values[Matrix.MSKEW_X], values[Matrix.MTRANS_X], + values[Matrix.MSKEW_Y], values[Matrix.MSCALE_Y], values[Matrix.MTRANS_Y] + ); + } } diff --git a/android/src/main/java/com/horcrux/svg/RenderableView.java b/android/src/main/java/com/horcrux/svg/RenderableView.java index 72d2ce8bf..a269d8347 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableView.java +++ b/android/src/main/java/com/horcrux/svg/RenderableView.java @@ -523,18 +523,8 @@ int hitTest(final float[] src) { int x = Math.round(dst[0]); int y = Math.round(dst[1]); - if (mRegion == null && mFillPath != null) { - mRegion = getRegion(mFillPath); - } - if (mRegion == null && mPath != null) { - mRegion = getRegion(mPath); - } - if (mStrokeRegion == null && mStrokePath != null) { - mStrokeRegion = getRegion(mStrokePath); - } - if (mMarkerRegion == null && mMarkerPath != null) { - mMarkerRegion = getRegion(mMarkerPath); - } + initBounds(); + if ( (mRegion == null || !mRegion.contains(x, y)) && (mStrokeRegion == null || !mStrokeRegion.contains(x, y) && @@ -545,10 +535,6 @@ int hitTest(final float[] src) { Path clipPath = getClipPath(); if (clipPath != null) { - if (mClipRegionPath != clipPath) { - mClipRegionPath = clipPath; - mClipRegion = getRegion(clipPath); - } if (!mClipRegion.contains(x, y)) { return -1; } @@ -557,10 +543,39 @@ int hitTest(final float[] src) { return getId(); } - Region getRegion(Path path) { - RectF rectF = new RectF(); - path.computeBounds(rectF, true); + void initBounds() { + if (mRegion == null && mFillPath != null) { + mFillBounds = new RectF(); + mFillPath.computeBounds(mFillBounds, true); + mRegion = getRegion(mFillPath, mFillBounds); + } + if (mRegion == null && mPath != null) { + mFillBounds = new RectF(); + mPath.computeBounds(mFillBounds, true); + mRegion = getRegion(mPath, mFillBounds); + } + if (mStrokeRegion == null && mStrokePath != null) { + mStrokeBounds = new RectF(); + mStrokePath.computeBounds(mStrokeBounds, true); + mStrokeRegion = getRegion(mStrokePath, mStrokeBounds); + } + if (mMarkerRegion == null && mMarkerPath != null) { + mMarkerBounds = new RectF(); + mMarkerPath.computeBounds(mMarkerBounds, true); + mMarkerRegion = getRegion(mMarkerPath, mMarkerBounds); + } + Path clipPath = getClipPath(); + if (clipPath != null) { + if (mClipRegionPath != clipPath) { + mClipRegionPath = clipPath; + mClipBounds = new RectF(); + clipPath.computeBounds(mClipBounds, true); + mClipRegion = getRegion(clipPath, mClipBounds); + } + } + } + Region getRegion(Path path, RectF rectF) { Region region = new Region(); region.setPath(path, new Region( diff --git a/android/src/main/java/com/horcrux/svg/SvgView.java b/android/src/main/java/com/horcrux/svg/SvgView.java index 36687c65a..d726317ee 100644 --- a/android/src/main/java/com/horcrux/svg/SvgView.java +++ b/android/src/main/java/com/horcrux/svg/SvgView.java @@ -147,7 +147,7 @@ public int reactTagForTouch(float touchX, float touchY) { private SVGLength mbbHeight; private String mAlign; private int mMeetOrSlice; - private final Matrix mInvViewBoxMatrix = new Matrix(); + final Matrix mInvViewBoxMatrix = new Matrix(); private boolean mInvertible = true; private boolean mRendered = false; int mTintColor = 0; diff --git a/android/src/main/java/com/horcrux/svg/TSpanView.java b/android/src/main/java/com/horcrux/svg/TSpanView.java index 8b53dfcca..48eb88770 100644 --- a/android/src/main/java/com/horcrux/svg/TSpanView.java +++ b/android/src/main/java/com/horcrux/svg/TSpanView.java @@ -1158,15 +1158,8 @@ int hitTest(final float[] src) { int x = Math.round(dst[0]); int y = Math.round(dst[1]); - if (mRegion == null && mFillPath != null) { - mRegion = getRegion(mFillPath); - } - if (mRegion == null && mPath != null) { - mRegion = getRegion(mPath); - } - if (mStrokeRegion == null && mStrokePath != null) { - mStrokeRegion = getRegion(mStrokePath); - } + initBounds(); + if ( (mRegion == null || !mRegion.contains(x, y)) && (mStrokeRegion == null || !mStrokeRegion.contains(x, y)) @@ -1176,10 +1169,6 @@ int hitTest(final float[] src) { Path clipPath = getClipPath(); if (clipPath != null) { - if (mClipRegionPath != clipPath) { - mClipRegionPath = clipPath; - mClipRegion = getRegion(clipPath); - } if (!mClipRegion.contains(x, y)) { return -1; } diff --git a/android/src/main/java/com/horcrux/svg/VirtualView.java b/android/src/main/java/com/horcrux/svg/VirtualView.java index cb3ba9343..5d563f42d 100644 --- a/android/src/main/java/com/horcrux/svg/VirtualView.java +++ b/android/src/main/java/com/horcrux/svg/VirtualView.java @@ -93,6 +93,10 @@ abstract public class VirtualView extends ReactViewGroup { Path mStrokePath; Path mMarkerPath; RectF mBox; + RectF mFillBounds; + RectF mStrokeBounds; + RectF mMarkerBounds; + RectF mClipBounds; Region mRegion; Region mMarkerRegion; Region mStrokeRegion; diff --git a/ios/Elements/RNSVGGroup.m b/ios/Elements/RNSVGGroup.m index f21fcad2e..10f8f850a 100644 --- a/ios/Elements/RNSVGGroup.m +++ b/ios/Elements/RNSVGGroup.m @@ -78,10 +78,16 @@ - (void)renderGroupTo:(CGContextRef)context rect:(CGRect)rect [self setHitArea:path]; if (!CGRectEqualToRect(bounds, CGRectNull)) { self.clientRect = bounds; - const CGRect fillBounds = CGPathGetBoundingBox(path); - const CGRect strokeBounds = CGPathGetBoundingBox(self.strokePath); - self.pathBounds = CGRectUnion(fillBounds, strokeBounds); - + self.fillBounds = CGPathGetBoundingBox(path); + self.strokeBounds = CGPathGetBoundingBox(self.strokePath); + self.pathBounds = CGRectUnion(self.fillBounds, self.strokeBounds); + + CGAffineTransform current = CGContextGetCTM(context); + CGAffineTransform svgToClientTransform = CGAffineTransformConcat(current, self.svgView.invInitialCTM); + + self.ctm = svgToClientTransform; + self.screenCTM = current; + CGAffineTransform transform = CGAffineTransformConcat(self.matrix, self.transforms); CGPoint mid = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); CGPoint center = CGPointApplyAffineTransform(mid, transform); diff --git a/ios/Elements/RNSVGImage.m b/ios/Elements/RNSVGImage.m index 30292d298..d294e53f7 100644 --- a/ios/Elements/RNSVGImage.m +++ b/ios/Elements/RNSVGImage.m @@ -121,6 +121,8 @@ - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect [self setHitArea:hitAreaPath]; CGPathRelease(hitAreaPath); self.pathBounds = hitArea; + self.fillBounds = hitArea; + self.strokeBounds = hitArea; // apply viewBox transform on Image render. CGRect imageBounds = CGRectMake(0, 0, _imageSize.width, _imageSize.height); @@ -136,6 +138,13 @@ - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect CGRect bounds = hitArea; self.clientRect = bounds; + + CGAffineTransform current = CGContextGetCTM(context); + CGAffineTransform svgToClientTransform = CGAffineTransformConcat(current, self.svgView.invInitialCTM); + + self.ctm = svgToClientTransform; + self.screenCTM = current; + CGAffineTransform transform = CGAffineTransformConcat(self.matrix, self.transforms); CGPoint mid = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); CGPoint center = CGPointApplyAffineTransform(mid, transform); diff --git a/ios/Elements/RNSVGUse.m b/ios/Elements/RNSVGUse.m index 14047b065..dc735f1d8 100644 --- a/ios/Elements/RNSVGUse.m +++ b/ios/Elements/RNSVGUse.m @@ -95,6 +95,13 @@ - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect } CGRect bounds = template.clientRect; self.clientRect = bounds; + + CGAffineTransform current = CGContextGetCTM(context); + CGAffineTransform svgToClientTransform = CGAffineTransformConcat(current, self.svgView.invInitialCTM); + + self.ctm = svgToClientTransform; + self.screenCTM = current; + CGAffineTransform transform = CGAffineTransformConcat(self.matrix, self.transforms); CGPoint mid = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); CGPoint center = CGPointApplyAffineTransform(mid, transform); diff --git a/ios/RNSVGNode.h b/ios/RNSVGNode.h index 6623a3725..60cef7273 100644 --- a/ios/RNSVGNode.h +++ b/ios/RNSVGNode.h @@ -35,6 +35,8 @@ extern CGFloat const RNSVG_DEFAULT_FONT_SIZE; @property (nonatomic, strong) NSString *markerMid; @property (nonatomic, strong) NSString *markerEnd; @property (nonatomic, assign) BOOL responsible; +@property (nonatomic, assign) CGAffineTransform ctm; +@property (nonatomic, assign) CGAffineTransform screenCTM; @property (nonatomic, assign) CGAffineTransform matrix; @property (nonatomic, assign) CGAffineTransform transforms; @property (nonatomic, assign) CGAffineTransform invmatrix; @@ -48,6 +50,8 @@ extern CGFloat const RNSVG_DEFAULT_FONT_SIZE; @property (nonatomic, assign) CGPathRef markerPath; @property (nonatomic, assign) CGRect clientRect; @property (nonatomic, assign) CGRect pathBounds; +@property (nonatomic, assign) CGRect fillBounds; +@property (nonatomic, assign) CGRect strokeBounds; @property (nonatomic, assign) CGRect markerBounds; @property (nonatomic, copy) RCTDirectEventBlock onLayout; diff --git a/ios/RNSVGRenderable.m b/ios/RNSVGRenderable.m index 29f33e75e..0fe61b545 100644 --- a/ios/RNSVGRenderable.m +++ b/ios/RNSVGRenderable.m @@ -356,9 +356,9 @@ - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect self.path = CGPathRetain(path); } [self setHitArea:path]; - const CGRect fillBounds = CGPathGetBoundingBox(path); - const CGRect strokeBounds = CGPathGetBoundingBox(self.strokePath); - self.pathBounds = CGRectUnion(fillBounds, strokeBounds); + self.fillBounds = CGPathGetBoundingBox(path); + self.strokeBounds = CGPathGetBoundingBox(self.strokePath); + self.pathBounds = CGRectUnion(self.fillBounds, self.strokeBounds); } const CGRect pathBounds = self.pathBounds; @@ -366,7 +366,9 @@ - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect CGAffineTransform svgToClientTransform = CGAffineTransformConcat(current, self.svgView.invInitialCTM); CGRect clientRect = CGRectApplyAffineTransform(pathBounds, svgToClientTransform); + self.ctm = svgToClientTransform; self.clientRect = clientRect; + self.screenCTM = current; if (_vectorEffect == kRNSVGVectorEffectNonScalingStroke) { path = CGPathCreateCopyByTransformingPath(path, &svgToClientTransform); diff --git a/ios/ViewManagers/RNSVGRenderableManager.m b/ios/ViewManagers/RNSVGRenderableManager.m index 538352dfe..44f7be749 100644 --- a/ios/ViewManagers/RNSVGRenderableManager.m +++ b/ios/ViewManagers/RNSVGRenderableManager.m @@ -100,7 +100,7 @@ - (void)withTag:(nonnull NSNumber *)reactTag success:(RNSVGSuccessBlock)successB withTag:reactTag success:^(RNSVGRenderable *svg){ CGPathRef target = [svg getPath:nil]; - RNSVGPathMeasure *measure = [RNSVGPathMeasure init]; + RNSVGPathMeasure *measure = [[RNSVGPathMeasure alloc]init]; [measure extractPathData:target]; CGFloat pathLegth = measure.pathLength; @@ -134,4 +134,82 @@ - (void)withTag:(nonnull NSNumber *)reactTag success:(RNSVGSuccessBlock)successB attempt:0]; } +RCT_EXPORT_METHOD(getBBox:(nonnull NSNumber *)reactTag options:(NSDictionary *)options callback:(RCTResponseSenderBlock)callback) +{ + [self + withTag:reactTag + success:^(RNSVGRenderable *svg){ + BOOL fill = [[options objectForKey:@"fill"] boolValue]; + BOOL stroke = [[options objectForKey:@"stroke"] boolValue]; + BOOL markers = [[options objectForKey:@"markers"] boolValue]; + BOOL clipped = [[options objectForKey:@"clipped"] boolValue]; + [svg getPath:nil]; + CGRect bounds = CGRectZero; + if (fill) { + bounds = CGRectUnion(bounds, svg.fillBounds); + } + if (stroke) { + bounds = CGRectUnion(bounds, svg.strokeBounds); + } + if (markers) { + bounds = CGRectUnion(bounds, svg.markerBounds); + } + if (clipped) { + CGPathRef clipPath = [svg getClipPath]; + CGRect clipBounds = CGPathGetBoundingBox(clipPath); + bounds = CGRectIntersection(bounds, clipBounds); + } + CGPoint origin = bounds.origin; + CGSize size = bounds.size; + callback(@[ + [NSNumber numberWithDouble:origin.x], + [NSNumber numberWithDouble:origin.y], + [NSNumber numberWithDouble:size.width], + [NSNumber numberWithDouble:size.height] + ]); + } + fail:^{ + callback(@[[NSNumber numberWithBool:false]]); + } + attempt:0]; +} + +RCT_EXPORT_METHOD(getCTM:(nonnull NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback) +{ + [self + withTag:reactTag + success:^(RNSVGRenderable *svg){ + CGAffineTransform ctm = svg.ctm; + callback(@[ + n(ctm.a), n(ctm.c), n(ctm.tx), + n(ctm.b), n(ctm.d), n(ctm.ty) + ]); + } + fail:^{ + callback(@[[NSNumber numberWithBool:false]]); + } + attempt:0]; +} + +static NSNumber *n(CGFloat af) { + return [NSNumber numberWithDouble:af]; +} + +RCT_EXPORT_METHOD(getScreenCTM:(nonnull NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback) +{ + [self + withTag:reactTag + success:^(RNSVGRenderable *svg){ + CGAffineTransform screenCTM = svg.screenCTM; + callback(@[ + n(screenCTM.a), n(screenCTM.c), n(screenCTM.tx), + n(screenCTM.b), n(screenCTM.d), n(screenCTM.ty) + ]); + } + fail:^{ + callback(@[[NSNumber numberWithBool:false]]); + } + attempt:0]; +} + @end diff --git a/src/elements/Shape.tsx b/src/elements/Shape.tsx index d95674ddf..85452894d 100644 --- a/src/elements/Shape.tsx +++ b/src/elements/Shape.tsx @@ -14,10 +14,10 @@ const touchVals = touchKeys.map(key => SvgTouchableMixin[key]); const numTouchKeys = touchKeys.length; interface SVGBoundingBoxOptions { - fill: boolean; - stroke: boolean; - markers: boolean; - clipped: boolean; + fill?: boolean; + stroke?: boolean; + markers?: boolean; + clipped?: boolean; } interface DOMPointInit { @@ -59,31 +59,42 @@ export default class Shape

extends Component

{ if (!callback) { return; } + const { fill = true, stroke = true, markers = true, clipped = true } = + options || {}; const handle = findNodeHandle(this.root as Component); - RNSVGRenderableManager.getBBox(handle, options, callback); // TODO + RNSVGRenderableManager.getBBox( + handle, + { + fill, + stroke, + markers, + clipped, + }, + callback, + ); }; getCTM = (callback: () => void) => { if (!callback) { return; } const handle = findNodeHandle(this.root as Component); - RNSVGRenderableManager.getCTM(handle, callback); // TODO + RNSVGRenderableManager.getCTM(handle, callback); }; getScreenCTM = (callback: () => void) => { if (!callback) { return; } const handle = findNodeHandle(this.root as Component); - RNSVGRenderableManager.getScreenCTM(handle, callback); // TODO + RNSVGRenderableManager.getScreenCTM(handle, callback); }; - isPointInFill = (callback: () => void, options: DOMPointInit) => { + isPointInFill = (options: DOMPointInit, callback: () => void) => { if (!callback) { return; } const handle = findNodeHandle(this.root as Component); RNSVGRenderableManager.isPointInFill(handle, options, callback); }; - isPointInStroke = (callback: () => void, options: DOMPointInit) => { + isPointInStroke = (options: DOMPointInit, callback: () => void) => { if (!callback) { return; } @@ -95,13 +106,13 @@ export default class Shape

extends Component

{ return; } const handle = findNodeHandle(this.root as Component); - RNSVGRenderableManager.getTotalLength(handle, callback); // TODO + RNSVGRenderableManager.getTotalLength(handle, callback); }; - getPointAtLength = (callback: () => void, options: Object) => { + getPointAtLength = (options: Object, callback: () => void) => { if (!callback) { return; } const handle = findNodeHandle(this.root as Component); - RNSVGRenderableManager.getPointAtLength(handle, options, callback); // TODO + RNSVGRenderableManager.getPointAtLength(handle, options, callback); }; }