diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 5a10c70d..b3207ffd 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -258,8 +258,9 @@ TargetAttributes = { B02E75EC1C16104900D1971D = { CreatedOnToolsVersion = 7.1.1; + DevelopmentTeam = FZXCM5CJ7P; LastSwiftMigration = 1020; - ProvisioningStyle = Manual; + ProvisioningStyle = Automatic; }; }; }; @@ -502,7 +503,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - DEVELOPMENT_TEAM = ""; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = FZXCM5CJ7P; INFOPLIST_FILE = Example/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -521,13 +523,15 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - DEVELOPMENT_TEAM = ""; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = FZXCM5CJ7P; INFOPLIST_FILE = Example/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.exyte.Example.Example; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/Macaw.xcodeproj/project.pbxproj b/Macaw.xcodeproj/project.pbxproj index 78ce34f8..e9df761b 100644 --- a/Macaw.xcodeproj/project.pbxproj +++ b/Macaw.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ 30FF496D215CF27E00FF653C /* MCAShapeLayerLineCap_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FF496C215CF27E00FF653C /* MCAShapeLayerLineCap_macOS.swift */; }; 30FF496F215CF3B000FF653C /* MCAMediaTimingFunctionName_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FF496E215CF3B000FF653C /* MCAMediaTimingFunctionName_iOS.swift */; }; 30FF4971215CF4CE00FF653C /* MCAMediaTimingFunctionName_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FF4970215CF4CE00FF653C /* MCAMediaTimingFunctionName_macOS.swift */; }; + 421C66502225196900DD73F5 /* color-prop-04-t-manual.svg in Resources */ = {isa = PBXBuildFile; fileRef = 421C664F2225196900DD73F5 /* color-prop-04-t-manual.svg */; }; + 4269F43E2257265800D91393 /* color-prop-04-t-manual.reference in Resources */ = {isa = PBXBuildFile; fileRef = 4269F43D2257265700D91393 /* color-prop-04-t-manual.reference */; }; 5713C4E21E51EC8F00BBA4D9 /* TouchEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5713C4E11E51EC8F00BBA4D9 /* TouchEvent.swift */; }; 5713C4F31E5AD46800BBA4D9 /* ControlStatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5713C4F21E5AD46800BBA4D9 /* ControlStatesTests.swift */; }; 5713C4F51E5AE2C300BBA4D9 /* CombineAnimationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5713C4F41E5AE2C300BBA4D9 /* CombineAnimationTests.swift */; }; @@ -232,6 +234,8 @@ 5852891620B29D67003E51D1 /* TransformedLocus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5852891520B29D67003E51D1 /* TransformedLocus.swift */; }; 5852891720B29D67003E51D1 /* TransformedLocus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5852891520B29D67003E51D1 /* TransformedLocus.swift */; }; 5874CCB720DA8A860090DBD5 /* ColorMatrix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5874CCB620DA8A860090DBD5 /* ColorMatrix.swift */; }; + 5876C63222572859000B31B6 /* MacawZoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5876C63122572859000B31B6 /* MacawZoom.swift */; }; + 5876C63322572859000B31B6 /* MacawZoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5876C63122572859000B31B6 /* MacawZoom.swift */; }; 58944BDA20AC8A9A00657640 /* logo_base64.txt in Resources */ = {isa = PBXBuildFile; fileRef = 57B7A4E01EE70DA5009D78D7 /* logo_base64.txt */; }; 58944BDB20AC8A9A00657640 /* clip.svg in Resources */ = {isa = PBXBuildFile; fileRef = C43B064C1F9738EF00787A35 /* clip.svg */; }; 58B0523920E10E7100D45008 /* ColorMatrix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5874CCB620DA8A860090DBD5 /* ColorMatrix.swift */; }; @@ -555,6 +559,8 @@ 30FF496C215CF27E00FF653C /* MCAShapeLayerLineCap_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCAShapeLayerLineCap_macOS.swift; sourceTree = ""; }; 30FF496E215CF3B000FF653C /* MCAMediaTimingFunctionName_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCAMediaTimingFunctionName_iOS.swift; sourceTree = ""; }; 30FF4970215CF4CE00FF653C /* MCAMediaTimingFunctionName_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCAMediaTimingFunctionName_macOS.swift; sourceTree = ""; }; + 421C664F2225196900DD73F5 /* color-prop-04-t-manual.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "color-prop-04-t-manual.svg"; sourceTree = ""; }; + 4269F43D2257265700D91393 /* color-prop-04-t-manual.reference */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "color-prop-04-t-manual.reference"; sourceTree = ""; }; 5713C4E11E51EC8F00BBA4D9 /* TouchEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchEvent.swift; sourceTree = ""; }; 5713C4F21E5AD46800BBA4D9 /* ControlStatesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControlStatesTests.swift; sourceTree = ""; }; 5713C4F41E5AE2C300BBA4D9 /* CombineAnimationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombineAnimationTests.swift; sourceTree = ""; }; @@ -672,6 +678,7 @@ 585288F320AD96A2003E51D1 /* ContentLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentLayout.swift; sourceTree = ""; }; 5852891520B29D67003E51D1 /* TransformedLocus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformedLocus.swift; sourceTree = ""; }; 5874CCB620DA8A860090DBD5 /* ColorMatrix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorMatrix.swift; sourceTree = ""; }; + 5876C63122572859000B31B6 /* MacawZoom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacawZoom.swift; sourceTree = ""; }; 5B1A8C7520A15F7300E5FFAE /* SVGNodeLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SVGNodeLayout.swift; sourceTree = ""; }; 5B1AE18420B6A669007EECCB /* text-align-01-b-manual.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "text-align-01-b-manual.svg"; sourceTree = ""; }; 5B1AE18520B6A669007EECCB /* paths-data-06-t-manual.reference */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "paths-data-06-t-manual.reference"; sourceTree = ""; }; @@ -1291,6 +1298,7 @@ 57F108731F502A3600DC365B /* Touchable.swift */, 57E5E1501E3B393900D1CB28 /* MacawView.swift */, 57E5E1521E3B393900D1CB28 /* ShapeLayer.swift */, + 5876C63122572859000B31B6 /* MacawZoom.swift */, ); path = views; sourceTree = ""; @@ -1356,6 +1364,8 @@ 5B1AE18320B6A669007EECCB /* w3cSVGTests */ = { isa = PBXGroup; children = ( + 421C664F2225196900DD73F5 /* color-prop-04-t-manual.svg */, + 4269F43D2257265700D91393 /* color-prop-04-t-manual.reference */, 5B1AE22D20B6A669007EECCB /* color-prop-01-b-manual.reference */, 5B1AE18A20B6A669007EECCB /* color-prop-01-b-manual.svg */, 5B1AE22C20B6A669007EECCB /* color-prop-02-f-manual.reference */, @@ -1765,6 +1775,7 @@ 5B1AE26920B6A669007EECCB /* struct-use-03-t-manual.reference in Resources */, 5B1AE27820B6A669007EECCB /* struct-group-01-t-manual.reference in Resources */, 5B1AE2B820B6A669007EECCB /* painting-stroke-03-t-manual.reference in Resources */, + 4269F43E2257265800D91393 /* color-prop-04-t-manual.reference in Resources */, 5B1AE2C720B6A669007EECCB /* paths-data-02-t-manual.svg in Resources */, 5B1AE27120B6A669007EECCB /* painting-stroke-04-t-manual.reference in Resources */, 5B1AE2C220B6A669007EECCB /* coords-trans-05-t-manual.svg in Resources */, @@ -1851,6 +1862,7 @@ 5B1AE23C20B6A669007EECCB /* paths-data-15-t-manual.svg in Resources */, 5BAE2039208E163D006BF277 /* polygon.reference in Resources */, 5B1AE27220B6A669007EECCB /* struct-frag-06-t-manual.svg in Resources */, + 421C66502225196900DD73F5 /* color-prop-04-t-manual.svg in Resources */, 5B1AE2A020B6A669007EECCB /* coords-trans-08-t-manual.svg in Resources */, 5B1AE23A20B6A669007EECCB /* painting-fill-03-t-manual.reference in Resources */, 5BAE2043208E163D006BF277 /* textBasicTransform.reference in Resources */, @@ -2074,6 +2086,7 @@ 57614B3A1F83D15600875933 /* AnimationProducer.swift in Sources */, 585288F620AD96A2003E51D1 /* ContentLayout.swift in Sources */, 57614B3C1F83D15600875933 /* ShapeInterpolation.swift in Sources */, + 5876C63322572859000B31B6 /* MacawZoom.swift in Sources */, 57614B3D1F83D15600875933 /* Graphics_iOS.swift in Sources */, 57614BDB1F8739EE00875933 /* MacawView+PDF.swift in Sources */, 57614B411F83D15600875933 /* Text.swift in Sources */, @@ -2214,6 +2227,7 @@ 57A27BD51E44C5840057BD3A /* ShapeInterpolation.swift in Sources */, A718CD471F45C28700966E06 /* Graphics_iOS.swift in Sources */, 57614BDA1F8739EE00875933 /* MacawView+PDF.swift in Sources */, + 5876C63222572859000B31B6 /* MacawZoom.swift in Sources */, 57E5E1A21E3B393900D1CB28 /* Text.swift in Sources */, 57F1087C1F53CA7E00DC365B /* MDisplayLink_iOS.swift in Sources */, 57E5E1A61E3B393900D1CB28 /* RenderContext.swift in Sources */, diff --git a/MacawTests/MacawSVGTests.swift b/MacawTests/MacawSVGTests.swift index af5ec5f9..c3bc656b 100644 --- a/MacawTests/MacawSVGTests.swift +++ b/MacawTests/MacawSVGTests.swift @@ -485,6 +485,10 @@ class MacawSVGTests: XCTestCase { validateJSON("color-prop-03-t-manual") } + func testColorProp04() { + validateJSON("color-prop-04-t-manual") + } + func testTypesBasic01() { validateJSON("types-basic-01-f-manual") } diff --git a/MacawTests/w3cSVGTests/color-prop-04-t-manual.reference b/MacawTests/w3cSVGTests/color-prop-04-t-manual.reference new file mode 100644 index 00000000..e29b0b97 --- /dev/null +++ b/MacawTests/w3cSVGTests/color-prop-04-t-manual.reference @@ -0,0 +1,642 @@ +{ + "contents" : [ + { + "contents" : [ + { + "contents" : [ + { + "fill" : { + "type" : "Color", + "val" : 6513614 + }, + "form" : { + "h" : 300, + "type" : "Rect", + "w" : 460, + "x" : -230, + "y" : -170 + }, + "node" : "Shape" + }, + { + "fill" : { + "type" : "Color", + "val" : 16777215 + }, + "form" : { + "h" : 280, + "type" : "Rect", + "w" : 440, + "x" : -220, + "y" : -160 + }, + "node" : "Shape" + }, + { + "fill" : { + "type" : "Color", + "val" : 16777215 + }, + "form" : { + "h" : 221, + "type" : "Rect", + "w" : 317, + "x" : -152, + "y" : -108 + }, + "node" : "Shape" + }, + { + "form" : { + "h" : 221, + "type" : "Rect", + "w" : 317, + "x" : -152, + "y" : -108 + }, + "node" : "Shape", + "stroke" : { + "cap" : "butt", + "dashes" : [ + + ], + "fill" : { + "type" : "Color", + "val" : 13421772 + }, + "join" : "miter", + "width" : 3 + } + }, + { + "contents" : [ + { + "align" : "min", + "baseline" : "bottom", + "fill" : { + "type" : "Color", + "val" : 0 + }, + "font" : { + "name" : "SVGFreeSansASCII,sans-serif", + "size" : 12, + "weight" : "normal" + }, + "node" : "Text", + "place" : "1, 0, 0, 1, -148, 0", + "text" : "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. " + }, + { + "align" : "min", + "baseline" : "bottom", + "fill" : { + "type" : "Color", + "val" : 0 + }, + "font" : { + "name" : "SVGFreeSansASCII,sans-serif", + "size" : 12, + "weight" : "normal" + }, + "node" : "Text", + "place" : "1, 0, 0, 1, -148, 20", + "text" : "Vestibulum pulvinar. Duis laoreet, nunc vitae facilisis " + }, + { + "align" : "min", + "baseline" : "bottom", + "fill" : { + "type" : "Color", + "val" : 0 + }, + "font" : { + "name" : "SVGFreeSansASCII,sans-serif", + "size" : 12, + "weight" : "normal" + }, + "node" : "Text", + "place" : "1, 0, 0, 1, -148, 40", + "text" : "tristique, pede sem iaculis mi, non consectetuer lorem " + }, + { + "align" : "min", + "baseline" : "bottom", + "fill" : { + "type" : "Color", + "val" : 0 + }, + "font" : { + "name" : "SVGFreeSansASCII,sans-serif", + "size" : 12, + "weight" : "normal" + }, + "node" : "Text", + "place" : "1, 0, 0, 1, -148, 60", + "text" : "libero et est. Donec imperdiet purus sed odio. Duis " + }, + { + "align" : "min", + "baseline" : "bottom", + "fill" : { + "type" : "Color", + "val" : 0 + }, + "font" : { + "name" : "SVGFreeSansASCII,sans-serif", + "size" : 12, + "weight" : "normal" + }, + "node" : "Text", + "place" : "1, 0, 0, 1, -148, 80", + "text" : "venenatis tortor eu lectus. Suspendisse sed metus at " + }, + { + "align" : "min", + "baseline" : "bottom", + "fill" : { + "type" : "Color", + "val" : 0 + }, + "font" : { + "name" : "SVGFreeSansASCII,sans-serif", + "size" : 12, + "weight" : "normal" + }, + "node" : "Text", + "place" : "1, 0, 0, 1, -148, 100", + "text" : "metus viverra ultricies. Mauris porttitor, justo a vulputate" + } + ], + "node" : "Group" + }, + { + "contents" : [ + { + "fill" : { + "type" : "Color", + "val" : 12632256 + }, + "form" : { + "h" : 190, + "type" : "Rect", + "w" : 118, + "x" : -150, + "y" : -107 + }, + "node" : "Shape" + }, + { + "fill" : { + "type" : "Color", + "val" : 12632256 + }, + "form" : { + "h" : 34, + "type" : "Rect", + "w" : 102, + "x" : -143, + "y" : 0 + }, + "node" : "Shape" + }, + { + "align" : "min", + "baseline" : "bottom", + "fill" : { + "type" : "Color", + "val" : 0 + }, + "font" : { + "name" : "SVGFreeSansASCII,sans-serif", + "size" : 20, + "weight" : "bold" + }, + "node" : "Text", + "place" : "1, 0, 0, 1, -138, 24", + "text" : "Load" + }, + { + "fill" : { + "type" : "Color", + "val" : 12632256 + }, + "form" : { + "h" : 34, + "type" : "Rect", + "w" : 102, + "x" : -143, + "y" : 40 + }, + "node" : "Shape" + }, + { + "align" : "min", + "baseline" : "bottom", + "fill" : { + "type" : "Color", + "val" : 0 + }, + "font" : { + "name" : "SVGFreeSansASCII,sans-serif", + "size" : 20, + "weight" : "bold" + }, + "node" : "Text", + "place" : "1, 0, 0, 1, -138, 64", + "text" : "Save" + }, + { + "form" : { + "segments" : [ + { + "data" : [ + -149, + 83 + ], + "type" : "M" + }, + { + "data" : [ + 114 + ], + "type" : "h" + }, + { + "data" : [ + -94 + ], + "type" : "v" + } + ], + "type" : "Path" + }, + "node" : "Shape", + "stroke" : { + "cap" : "butt", + "dashes" : [ + + ], + "fill" : { + "type" : "Color", + "val" : 6710886 + }, + "join" : "miter", + "width" : 4 + } + }, + { + "form" : { + "segments" : [ + { + "data" : [ + -149, + 83 + ], + "type" : "M" + }, + { + "data" : [ + -94 + ], + "type" : "v" + }, + { + "data" : [ + 114 + ], + "type" : "h" + } + ], + "type" : "Path" + }, + "node" : "Shape", + "stroke" : { + "cap" : "butt", + "dashes" : [ + + ], + "fill" : { + "type" : "Color", + "val" : 12632256 + }, + "join" : "miter", + "width" : 4 + } + } + ], + "node" : "Group", + "place" : "1, 0, 0, 1, 2, 0" + }, + { + "contents" : [ + { + "fill" : { + "type" : "Color", + "val" : 12632256 + }, + "form" : { + "h" : 46, + "type" : "Rect", + "w" : 310, + "x" : -148, + "y" : -62 + }, + "node" : "Shape" + }, + { + "fill" : { + "type" : "Color", + "val" : 6710886 + }, + "form" : { + "segments" : [ + { + "data" : [ + 152, + -52 + ], + "type" : "M" + }, + { + "data" : [ + 10, + -10 + ], + "type" : "l" + }, + { + "data" : [ + 46 + ], + "type" : "v" + }, + { + "data" : [ + -311 + ], + "type" : "h" + }, + { + "data" : [ + 10, + -10 + ], + "type" : "l" + }, + { + "data" : [ + + ], + "type" : "z" + } + ], + "type" : "Path" + }, + "node" : "Shape" + }, + { + "fill" : { + "type" : "Color", + "val" : 12632256 + }, + "form" : { + "h" : 37, + "type" : "Rect", + "w" : 302, + "x" : -144, + "y" : -58 + }, + "node" : "Shape" + }, + { + "align" : "min", + "baseline" : "bottom", + "fill" : { + "type" : "Color", + "val" : 0 + }, + "font" : { + "name" : "SVGFreeSansASCII,sans-serif", + "size" : 20, + "weight" : "bold" + }, + "node" : "Text", + "place" : "1, 0, 0, 1, -141, -32", + "text" : "File" + }, + { + "align" : "min", + "baseline" : "bottom", + "fill" : { + "type" : "Color", + "val" : 0 + }, + "font" : { + "name" : "SVGFreeSansASCII,sans-serif", + "size" : 20, + "weight" : "bold" + }, + "node" : "Text", + "place" : "1, 0, 0, 1, -90, -32", + "text" : "Edit" + } + ], + "node" : "Group" + }, + { + "contents" : [ + { + "fill" : { + "type" : "Color", + "val" : 13421772 + }, + "form" : { + "h" : 42, + "type" : "Rect", + "w" : 311, + "x" : -149, + "y" : -106 + }, + "node" : "Shape", + "stroke" : { + "cap" : "butt", + "dashes" : [ + + ], + "fill" : { + "type" : "Color", + "val" : 16777215 + }, + "join" : "miter", + "width" : 4 + } + }, + { + "align" : "mid", + "baseline" : "bottom", + "fill" : { + "type" : "Color", + "val" : 0 + }, + "font" : { + "name" : "SVGFreeSansASCII,sans-serif", + "size" : 24, + "weight" : "bold" + }, + "node" : "Text", + "place" : "1, 0, 0, 1, 5, -78", + "text" : "Lorem" + }, + { + "contents" : [ + { + "fill" : { + "type" : "Color", + "val" : 12632256 + }, + "form" : { + "rect" : { + "h" : 26, + "type" : "Rect", + "w" : 26, + "x" : 120, + "y" : -99 + }, + "rx" : 8, + "ry" : 8, + "type" : "RoundRect" + }, + "node" : "Shape" + }, + { + "fill" : { + "type" : "Color", + "val" : 13421772 + }, + "form" : { + "rect" : { + "h" : 24, + "type" : "Rect", + "w" : 24, + "x" : 120, + "y" : -99 + }, + "rx" : 8, + "ry" : 8, + "type" : "RoundRect" + }, + "node" : "Shape" + }, + { + "fill" : { + "type" : "Color", + "val" : 8947848 + }, + "form" : { + "rect" : { + "h" : 24, + "type" : "Rect", + "w" : 24, + "x" : 122, + "y" : -97 + }, + "rx" : 8, + "ry" : 8, + "type" : "RoundRect" + }, + "node" : "Shape" + }, + { + "fill" : { + "type" : "Color", + "val" : 12632256 + }, + "form" : { + "rect" : { + "h" : 22, + "type" : "Rect", + "w" : 22, + "x" : 122, + "y" : -97 + }, + "rx" : 8, + "ry" : 8, + "type" : "RoundRect" + }, + "node" : "Shape" + } + ], + "node" : "Group" + } + ], + "node" : "Group" + } + ], + "node" : "Group", + "place" : "1, 0, 0, 1, 240, 180" + } + ], + "node" : "Group" + }, + { + "contents" : [ + { + "align" : "min", + "baseline" : "bottom", + "fill" : { + "type" : "Color", + "val" : 0 + }, + "font" : { + "name" : "SVGFreeSansASCII,sans-serif", + "size" : 32, + "weight" : "normal" + }, + "node" : "Text", + "place" : "1, 0, 0, 1, 10, 340", + "text" : "$Revision: 1.7 $" + } + ], + "node" : "Group" + }, + { + "form" : { + "h" : 358, + "type" : "Rect", + "w" : 478, + "x" : 1, + "y" : 1 + }, + "node" : "Shape", + "stroke" : { + "cap" : "butt", + "dashes" : [ + + ], + "fill" : { + "type" : "Color", + "val" : 0 + }, + "join" : "miter", + "width" : 1 + } + } + ], + "layout" : { + "scalingMode" : "meet", + "svgSize" : { + "height" : "100.0%", + "width" : "100.0%" + }, + "viewBox" : { + "h" : 360, + "type" : "Rect", + "w" : 480, + "x" : 0, + "y" : 0 + }, + "xAligningMode" : "mid", + "yAligningMode" : "mid" + }, + "node" : "Canvas" +} \ No newline at end of file diff --git a/MacawTests/w3cSVGTests/color-prop-04-t-manual.svg b/MacawTests/w3cSVGTests/color-prop-04-t-manual.svg new file mode 100644 index 00000000..34521761 --- /dev/null +++ b/MacawTests/w3cSVGTests/color-prop-04-t-manual.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + +

+ This tests the 'system' colors. +

+ + +

Run the test. No interaction required.

+
+ +

+ This test has no specific pass criteria, except that no error must be indicated. +

+

+ The colors on your screen might not match the reference + image at all, but they should at minimum be legible and should + preferably resemble the colors used on menus and other user interface + elements on your computer, pda or phone. +

+
+ + $RCSfile: color-prop-04-t.svg,v $ + + + + + + + + + + + + + + + Lorem ipsum dolor sit amet, consectetuer adipiscing elit. + Vestibulum pulvinar. Duis laoreet, nunc vitae facilisis + tristique, pede sem iaculis mi, non consectetuer lorem + libero et est. Donec imperdiet purus sed odio. Duis + venenatis tortor eu lectus. Suspendisse sed metus at + metus viverra ultricies. Mauris porttitor, justo a vulputate + + + + + Load + + Save + + + + + + + + File + Edit + + + + Lorem + + + + + + + + + + + $Revision: 1.7 $ + + + + + diff --git a/Source/animation/AnimationProducer.swift b/Source/animation/AnimationProducer.swift index 244aac25..8ecac9a9 100644 --- a/Source/animation/AnimationProducer.swift +++ b/Source/animation/AnimationProducer.swift @@ -384,8 +384,8 @@ class AnimationContext { func getLayoutTransform(_ renderer: NodeRenderer?) -> Transform { if rootTransform == nil { - if let view = renderer?.view, let node = view.renderer?.node() { - rootTransform = LayoutHelper.calcTransform(node, view.contentLayout, view.bounds.size.toMacaw()) + if let view = renderer?.view { + rootTransform = view.place } } return rootTransform ?? Transform.identity diff --git a/Source/model/geom2d/Arc.swift b/Source/model/geom2d/Arc.swift index 5c728df5..c94035c9 100644 --- a/Source/model/geom2d/Arc.swift +++ b/Source/model/geom2d/Arc.swift @@ -1,4 +1,4 @@ -import Darwin +import Foundation open class Arc: Locus { diff --git a/Source/model/geom2d/Point.swift b/Source/model/geom2d/Point.swift index 97f3125f..cac4f508 100644 --- a/Source/model/geom2d/Point.swift +++ b/Source/model/geom2d/Point.swift @@ -1,3 +1,5 @@ +import Foundation + open class Point: Locus { public let x: Double @@ -11,24 +13,35 @@ open class Point: Locus { } override open func bounds() -> Rect { - return Rect( - x: x, - y: y, - w: 0.0, - h: 0.0) + return Rect(x: x, y: y, w: 0.0, h: 0.0) } open func add(_ point: Point) -> Point { - return Point( - x: self.x + point.x, - y: self.y + point.y) + return Point( x: x + point.x, y: y + point.y) } open func rect(size: Size) -> Rect { return Rect(point: self, size: size) } + open func distance(to point: Point) -> Double { + let dx = point.x - x + let dy = point.y - y + return sqrt(dx * dx + dy * dy) + } + override open func toPath() -> Path { return MoveTo(x: x, y: y).lineTo(x: x, y: y).build() } } + +extension Point: Equatable { + public static func == (lhs: Point, rhs: Point) -> Bool { + return lhs.x == rhs.x + && lhs.y == rhs.y + } + + public static func - (lhs: Point, rhs: Point) -> Size { + return Size(w: lhs.x - rhs.x, h: lhs.y - rhs.y) + } +} diff --git a/Source/model/geom2d/Size.swift b/Source/model/geom2d/Size.swift index 12df24c7..b4a09805 100644 --- a/Source/model/geom2d/Size.swift +++ b/Source/model/geom2d/Size.swift @@ -1,3 +1,5 @@ +import Foundation + open class Size { public let w: Double @@ -13,11 +15,25 @@ open class Size { open func rect(at point: Point = Point.origin) -> Rect { return Rect(point: point, size: self) } + + open func angle() -> Double { + return atan2(h, w) + } + } extension Size { + public static func == (lhs: Size, rhs: Size) -> Bool { - return lhs.w == rhs.w - && lhs.h == rhs.h + return lhs.w == rhs.w && lhs.h == rhs.h } + + public static func + (lhs: Size, rhs: Size) -> Size { + return Size(w: lhs.w + rhs.w, h: lhs.h + rhs.h) + } + + public static func - (lhs: Size, rhs: Size) -> Size { + return Size(w: lhs.w - rhs.w, h: lhs.h - rhs.h) + } + } diff --git a/Source/model/geom2d/Transform.swift b/Source/model/geom2d/Transform.swift index 808822ad..8f731b6c 100644 --- a/Source/model/geom2d/Transform.swift +++ b/Source/model/geom2d/Transform.swift @@ -75,12 +75,19 @@ public final class Transform { return Transform(m11: nm11, m12: nm12, m21: nm21, m22: nm22, dx: ndx, dy: ndy) } + public func apply(to: Point) -> Point { + let x2 = m11 * to.x + m12 * to.x + dx + let y2 = m21 * to.y + m22 * to.y + dy + return Point(x: x2, y: y2) + } + public func invert() -> Transform? { let det = self.m11 * self.m22 - self.m12 * self.m21 if det == 0 { return nil } - return Transform(m11: m22 / det, m12: -m12 / det, m21: -m21 / det, m22: m11 / det, + return Transform(m11: m22 / det, m12: -m12 / det, + m21: -m21 / det, m22: m11 / det, dx: (m21 * dy - m22 * dx) / det, dy: (m12 * dx - m11 * dy) / det) } diff --git a/Source/platform/iOS/MView_iOS.swift b/Source/platform/iOS/MView_iOS.swift index b97c7006..61d8d32e 100644 --- a/Source/platform/iOS/MView_iOS.swift +++ b/Source/platform/iOS/MView_iOS.swift @@ -26,70 +26,34 @@ open class MView: UIView, Touchable { open override func touchesBegan(_ touches: Set, with event: MEvent?) { super.touchesBegan(touches, with: event) - - let touchPoints = touches.map { touch -> MTouchEvent in - let location = touch.location(in: self) - let id = Int(bitPattern: Unmanaged.passUnretained(touch).toOpaque()) - - return MTouchEvent(x: Double(location.x), y: Double(location.y), id: id) - } - - mTouchesBegan(touchPoints) + mTouchesBegan(touches, with: event) } open override func touchesMoved(_ touches: Set, with event: MEvent?) { super.touchesMoved(touches, with: event) - - let touchPoints = touches.map { touch -> MTouchEvent in - let location = touch.location(in: self) - let id = Int(bitPattern: Unmanaged.passUnretained(touch).toOpaque()) - - return MTouchEvent(x: Double(location.x), y: Double(location.y), id: id) - } - - self.mTouchesMoved(touchPoints) + mTouchesMoved(touches, with: event) } open override func touchesEnded(_ touches: Set, with event: MEvent?) { super.touchesEnded(touches, with: event) - - let touchPoints = touches.map { touch -> MTouchEvent in - let location = touch.location(in: self) - let id = Int(bitPattern: Unmanaged.passUnretained(touch).toOpaque()) - - return MTouchEvent(x: Double(location.x), y: Double(location.y), id: id) - } - - mTouchesEnded(touchPoints) + mTouchesEnded(touches, with: event) } override open func touchesCancelled(_ touches: Set, with event: MEvent?) { super.touchesCancelled(touches, with: event) - - let touchPoints = touches.map { touch -> MTouchEvent in - let location = touch.location(in: self) - let id = Int(bitPattern: Unmanaged.passUnretained(touch).toOpaque()) - - return MTouchEvent(x: Double(location.x), y: Double(location.y), id: id) - } - - mTouchesCancelled(touchPoints) + mTouchesCancelled(touches, with: event) } - func mTouchesBegan(_ touches: [MTouchEvent]) { - + func mTouchesBegan(_ touches: Set, with event: MEvent?) { } - func mTouchesMoved(_ touches: [MTouchEvent]) { - + func mTouchesMoved(_ touches: Set, with event: MEvent?) { } - func mTouchesEnded(_ touches: [MTouchEvent]) { - + func mTouchesEnded(_ touches: Set, with event: MEvent?) { } - func mTouchesCancelled(_ touches: [MTouchEvent]) { - + func mTouchesCancelled(_ touches: Set, with event: MEvent?) { } } diff --git a/Source/platform/macOS/MView_macOS.swift b/Source/platform/macOS/MView_macOS.swift index b2bfd231..141346b7 100644 --- a/Source/platform/macOS/MView_macOS.swift +++ b/Source/platform/macOS/MView_macOS.swift @@ -38,7 +38,6 @@ open class MView: NSView, Touchable { super.init(coder: coder) self.wantsLayer = true - setupMouse() } open override var isFlipped: Bool { @@ -80,118 +79,39 @@ open class MView: NSView, Touchable { func layoutSubviews() { super.resizeSubviews(withOldSize: self.bounds.size) } - + // MARK: - Touch pad open override func touchesBegan(with event: NSEvent) { super.touchesBegan(with: event) - - let touchPoints = event.touches(matching: .any, in: self).map { touch -> MTouchEvent in - let location = touch.location(in: self) - let id = Int(bitPattern: Unmanaged.passUnretained(touch).toOpaque()) - - return MTouchEvent(x: Double(location.x), y: Double(location.y), id: id) - } - - mTouchesBegan(touchPoints) + mTouchesBegan(event.touches(matching: .any, in: self), with: event) } open override func touchesEnded(with event: NSEvent) { super.touchesEnded(with: event) - - let touchPoints = event.touches(matching: .any, in: self).map { touch -> MTouchEvent in - let location = touch.location(in: self) - let id = Int(bitPattern: Unmanaged.passUnretained(touch).toOpaque()) - - return MTouchEvent(x: Double(location.x), y: Double(location.y), id: id) - } - - mTouchesEnded(touchPoints) + mTouchesEnded(event.touches(matching: .any, in: self), with: event) } open override func touchesMoved(with event: NSEvent) { super.touchesMoved(with: event) - - let touchPoints = event.touches(matching: .any, in: self).map { touch -> MTouchEvent in - let location = touch.location(in: self) - let id = Int(bitPattern: Unmanaged.passUnretained(touch).toOpaque()) - - return MTouchEvent(x: Double(location.x), y: Double(location.y), id: id) - } - - mTouchesMoved(touchPoints) + mTouchesMoved(event.touches(matching: .any, in: self), with: event) } open override func touchesCancelled(with event: NSEvent) { super.touchesCancelled(with: event) - - let touchPoints = event.touches(matching: .any, in: self).map { touch -> MTouchEvent in - let location = touch.location(in: self) - let id = Int(bitPattern: Unmanaged.passUnretained(touch).toOpaque()) - - return MTouchEvent(x: Double(location.x), y: Double(location.y), id: id) - } - - mTouchesCancelled(touchPoints) - } - - // MARK: - Mouse - private func setupMouse() { - subscribeForMouseDown() - subscribeForMouseUp() - subscribeForMouseDragged() - } - - private func subscribeForMouseDown() { - NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { [weak self] event -> NSEvent? in - self?.handleInput(event: event) { touches in - self?.mTouchesBegan(touches) - } - return event - } - } - - private func subscribeForMouseUp() { - NSEvent.addLocalMonitorForEvents(matching: .leftMouseUp) { [weak self] event -> NSEvent? in - self?.handleInput(event: event) { touches in - self?.mTouchesEnded(touches) - } - return event - } - } - - private func subscribeForMouseDragged() { - NSEvent.addLocalMonitorForEvents(matching: .leftMouseDragged) { [weak self] event -> NSEvent? in - self?.handleInput(event: event) { touches in - self?.mTouchesMoved(touches) - } - return event - } - } - - private func handleInput(event: NSEvent, handler: (_ touches: [MTouchEvent]) -> Void ) { - let location = self.convert(event.locationInWindow, to: .none) - let touchPoint = MTouchEvent(x: Double(location.x), y: Double(location.y), id: 0) - - handler([touchPoint]) - - return + mTouchesCancelled(event.touches(matching: .any, in: self), with: event) } // MARK: - Touchable - func mTouchesBegan(_ touches: [MTouchEvent]) { - + func mTouchesBegan(_ touches: Set, with event: MEvent?) { } - func mTouchesMoved(_ touches: [MTouchEvent]) { - + func mTouchesMoved(_ touches: Set, with event: MEvent?) { } - func mTouchesEnded(_ touches: [MTouchEvent]) { - + func mTouchesEnded(_ touches: Set, with event: MEvent?) { } - func mTouchesCancelled(_ touches: [MTouchEvent]) { - + func mTouchesCancelled(_ touches: Set, with event: MEvent?) { } } #endif diff --git a/Source/svg/SVGConstants.swift b/Source/svg/SVGConstants.swift index 7c1991ae..5111644d 100644 --- a/Source/svg/SVGConstants.swift +++ b/Source/svg/SVGConstants.swift @@ -293,6 +293,48 @@ open class SVGConstants { "yellowgreen": 0x9acd32 ] + #if os(iOS) + public static let systemColorList = [ + "AppWorkspace": 0xffffff, + "ActiveBorder": 0xffffff, + "ActiveCaption": 0xcccccc, + "Background": 0x6363ce, + "ButtonFace": 0xc0c0c0, + "ButtonHighlight": 0xcccccc, + "ButtonShadow": 0x888888, + "CaptionText": 0x000000, + "HighlightText": 0x000000, + "Menu": 0xc0c0c0, + "MenuText": 0x000000, + "ThreeDFace": 0xc0c0c0, + "ThreeDDarkShadow": 0x666666, + "ThreeDLightShadow": 0xc0c0c0, + "Window": 0xffffff, + "WindowFrame": 0xcccccc, + "WindowText": 0x000000 + ] + #elseif os(OSX) + public static let systemColorList = [ + "AppWorkspace": 0xaaaaaa, + "ActiveBorder": 0x992a6ccd, + "ActiveCaption": 0x242424, + "Background": 0x6363ce, + "ButtonFace": 0xc0c0c0, + "ButtonHighlight": 0xffffff, + "ButtonShadow": 0x8d8d8d, + "CaptionText": 0x000000, + "HighlightText": 0x000000, + "Menu": 0xf6f6f6, + "MenuText": 0xffffff, + "ThreeDFace": 0xc0c0c0, + "ThreeDDarkShadow": 0x000000, + "ThreeDLightShadow": 0xffffff, + "Window": 0xececec, + "WindowFrame": 0xaaaaaa, + "WindowText": 0x242424 + ] + #endif + public static func valueToColor(_ color: Int) -> String? { return SVGConstants.colorList.filter { _, v -> Bool in v == color }.map { k, _ -> String in k }.first } diff --git a/Source/svg/SVGParser.swift b/Source/svg/SVGParser.swift index 3d9d80df..2b21bd02 100644 --- a/Source/svg/SVGParser.swift +++ b/Source/svg/SVGParser.swift @@ -608,6 +608,10 @@ open class SVGParser { let color = Color(val: defaultColor) return opacity != 1 ? color.with(a: opacity) : color } + if let systemColor = SVGConstants.systemColorList[colorString] { + let color = Color(val: systemColor) + return opacity != 1 ? color.with(a: opacity) : color + } if colorString.hasPrefix("rgb") { let color = parseRGBNotation(colorString: colorString) return opacity != 1 ? color.with(a: opacity) : color diff --git a/Source/views/MacawView.swift b/Source/views/MacawView.swift index 45c53ea3..78c52d15 100644 --- a/Source/views/MacawView.swift +++ b/Source/views/MacawView.swift @@ -5,6 +5,7 @@ import UIKit #elseif os(OSX) import AppKit #endif + /// /// MacawView is a main class used to embed Macaw scene into your Cocoa UI. /// You could create your own view extended from MacawView with predefined scene. @@ -57,6 +58,18 @@ open class MacawView: MView, MGestureRecognizerDelegate { } } + public let zoom = MacawZoom() + + public var place: Transform { + return placeManager.placeVar.value + } + + public var placeVar: Variable { + return placeManager.placeVar + } + + private let placeManager = RootPlaceManager() + override open var frame: CGRect { didSet { super.frame = frame @@ -123,6 +136,7 @@ open class MacawView: MView, MGestureRecognizerDelegate { @objc public init?(node: Node, coder aDecoder: NSCoder) { super.init(coder: aDecoder) + zoom.initialize(view: self, onChange: onZoomChange) #if os(iOS) initializeView() @@ -154,6 +168,7 @@ open class MacawView: MView, MGestureRecognizerDelegate { public override init(frame: CGRect) { super.init(frame: frame) + zoom.initialize(view: self, onChange: onZoomChange) #if os(iOS) initializeView() @@ -176,6 +191,11 @@ open class MacawView: MView, MGestureRecognizerDelegate { } #endif + private func onZoomChange(t: Transform) { + placeManager.setZoom(place: t) + self.setNeedsDisplay() + } + func initializeView() { self.contentLayout = .none self.context = RenderContext(view: self) @@ -252,8 +272,12 @@ open class MacawView: MView, MGestureRecognizerDelegate { } renderer.calculateZPositionRecursively() - ctx.concatenate(strongSelf.layoutHelper.getTransform(renderer, strongSelf.contentLayout, strongSelf.myBounds.size.toMacaw())) - renderer.render(in: ctx, force: false, opacity: strongSelf.node.opacity) + + // TODO: actually we should track all changes + placeManager.setLayout(place: layoutHelper.getTransform(renderer, contentLayout, bounds.size.toMacaw())) + + ctx.concatenate(strongSelf.place.toCG()) + renderer.render(in: ctx, force: false, opacity: node.opacity) } #endif @@ -272,7 +296,7 @@ open class MacawView: MView, MGestureRecognizerDelegate { defer { ctx.restoreGState() } - let transform = layoutHelper.getTransform(renderer, contentLayout, bounds.size.toMacaw()) + let transform = place.toCG() ctx.concatenate(transform) let loc = location.applying(transform.inverted()) return renderer.findNodeAt(parentNodePath: NodePath(node: Node(), location: loc), ctx: ctx) @@ -288,9 +312,10 @@ open class MacawView: MView, MGestureRecognizerDelegate { } // MARK: - Touches + override func mTouchesBegan(_ touches: Set, with event: MEvent?) { + zoom.touchesBegan(touches) - override func mTouchesBegan(_ touches: [MTouchEvent]) { - + let touchPoints = convert(touches: touches) if !self.node.shouldCheckForPressed() && !self.node.shouldCheckForMoved() && !self.node.shouldCheckForReleased () { @@ -301,7 +326,7 @@ open class MacawView: MView, MGestureRecognizerDelegate { return } - for touch in touches { + for touch in touchPoints { let location = CGPoint(x: touch.x, y: touch.y) var nodePath = doFindNode(location: location) @@ -333,7 +358,8 @@ open class MacawView: MView, MGestureRecognizerDelegate { } } - override func mTouchesMoved(_ touches: [MTouchEvent]) { + override func mTouchesMoved(_ touches: Set, with event: MEvent?) { + zoom.touchesMoved(touches) if !self.node.shouldCheckForMoved() { return } @@ -342,6 +368,7 @@ open class MacawView: MView, MGestureRecognizerDelegate { return } + let touchPoints = convert(touches: touches) touchesOfNode.keys.forEach { currentNode in guard let initialTouches = touchesOfNode[currentNode] else { return @@ -349,10 +376,10 @@ open class MacawView: MView, MGestureRecognizerDelegate { var points = [TouchPoint]() for initialTouch in initialTouches { - guard let currentIndex = touches.firstIndex(of: initialTouch) else { + guard let currentIndex = touchPoints.firstIndex(of: initialTouch) else { continue } - let currentTouch = touches[currentIndex] + let currentTouch = touchPoints[currentIndex] guard let nodePath = touchesMap[currentTouch]?.first else { continue } @@ -368,20 +395,30 @@ open class MacawView: MView, MGestureRecognizerDelegate { } } - override func mTouchesCancelled(_ touches: [MTouchEvent]) { + override func mTouchesCancelled(_ touches: Set, with event: MEvent?) { touchesEnded(touches: touches) } - override func mTouchesEnded(_ touches: [MTouchEvent]) { + override func mTouchesEnded(_ touches: Set, with event: MEvent?) { touchesEnded(touches: touches) } - private func touchesEnded(touches: [MTouchEvent]) { + private func convert(touches: Set) -> [MTouchEvent] { + return touches.map { touch -> MTouchEvent in + let location = touch.location(in: self) + let id = Int(bitPattern: Unmanaged.passUnretained(touch).toOpaque()) + return MTouchEvent(x: Double(location.x), y: Double(location.y), id: id) + } + } + + private func touchesEnded(touches: Set) { + zoom.touchesEnded(touches) guard let _ = renderer else { return } - for touch in touches { + let touchPoints = convert(touches: touches) + for touch in touchPoints { touchesMap[touch]?.forEach { nodePath in @@ -609,10 +646,10 @@ class LayoutHelper { private var prevSize: Size? private var prevRect: Rect? - private var prevTransform: CGAffineTransform? + private var prevTransform: Transform? private var myBounds = CGRect(x: 0, y: 0, width: 0, height: 0) - public func getTransform(_ nodeRenderer: NodeRenderer, _ layout: ContentLayout, _ size: Size) -> CGAffineTransform { + public func getTransform(_ nodeRenderer: NodeRenderer, _ layout: ContentLayout, _ size: Size) -> Transform { setSize(size: size) let node = nodeRenderer.node() var rect = node?.bounds @@ -630,9 +667,9 @@ class LayoutHelper { if let transform = prevTransform { return transform } - return setTransform(transform: layout.layout(rect: prevRect!, into: size).toCG()) + return setTransform(transform: layout.layout(rect: rect, into: size)) } - return CGAffineTransform.identity + return Transform.identity } public class func calcTransform(_ node: Node, _ layout: ContentLayout, _ size: Size) -> Transform { @@ -688,7 +725,7 @@ class LayoutHelper { prevTransform = nil } - private func setTransform(transform: CGAffineTransform) -> CGAffineTransform { + private func setTransform(transform: Transform) -> Transform { prevTransform = transform return transform } @@ -701,3 +738,33 @@ extension MacawView: CALayerDelegate { } #endif + +class RootPlaceManager { + + var placeVar = Variable(Transform.identity) + private var places: [Transform] = [.identity, .identity] + + func setLayout(place: Transform) { + if places[1] !== place { + places[1] = place + placeVar.value = recalc() + } + } + + func setZoom(place: Transform) { + if places[0] !== place { + places[0] = place + placeVar.value = recalc() + } + } + + private func recalc() -> Transform { + if places[0] === Transform.identity { + return places[1] + } else if places[1] === Transform.identity { + return places[0] + } + return places[0].concat(with: places[1]) + } + +} diff --git a/Source/views/MacawZoom.swift b/Source/views/MacawZoom.swift new file mode 100644 index 00000000..1831593b --- /dev/null +++ b/Source/views/MacawZoom.swift @@ -0,0 +1,159 @@ +// +// MacawZoom.swift +// Macaw +// +// Created by Yuri Strot on 4/5/19. +// Copyright © 2019 Exyte. All rights reserved. +// + +import Foundation + +#if os(iOS) +import UIKit +#elseif os(OSX) +import AppKit +#endif + +open class MacawZoom { + + private var view: MView! + private var onChange: ((Transform) -> Void)! + private var touches = [TouchData]() + private var zoomData = ZoomData() + + private var trackMove = false + private var trackScale = false + private var trackRotate = false + + open func enable(move: Bool = true, scale: Bool = true, rotate: Bool = false) { + trackMove = move + trackScale = scale + trackRotate = rotate + if scale || rotate { + #if os(iOS) + view.isMultipleTouchEnabled = true + #endif + } + } + + open func disable() { + trackMove = false + trackScale = false + trackRotate = false + } + + open func set(offset: Size? = nil, scale: Double? = nil, angle: Double? = nil) { + let o = offset ?? zoomData.offset + let s = scale ?? zoomData.scale + let a = angle ?? zoomData.angle + zoomData = ZoomData(offset: o, scale: s, angle: a) + onChange(zoomData.transform()) + } + + func initialize(view: MView, onChange: @escaping ((Transform) -> Void)) { + self.view = view + self.onChange = onChange + } + + func touchesBegan(_ touches: Set) { + zoomData = getNewZoom() + self.touches = self.touches.map { TouchData(touch: $0.touch, in: view) } + self.touches.append(contentsOf: touches.map { TouchData(touch: $0, in: view) }) + } + + func touchesMoved(_ touches: Set) { + let zoom = cleanTouches() ?? getNewZoom() + onChange(zoom.transform()) + } + + func touchesEnded(_ touches: Set) { + cleanTouches() + } + + @discardableResult private func cleanTouches() -> ZoomData? { + let newTouches = touches.filter { $0.touch.phase.rawValue < MTouch.Phase.ended.rawValue } + if newTouches.count != touches.count { + zoomData = getNewZoom() + touches = newTouches.map { TouchData(touch: $0.touch, in: view) } + return zoomData + } + return nil + } + + private func getNewZoom() -> ZoomData { + if touches.count == 0 || (touches.count == 1 && !trackMove) { + return zoomData + } + let s1 = touches[0].point + let e1 = touches[0].current(in: view) + if touches.count == 1 { + return zoomData.move(delta: e1 - s1) + } + let s2 = touches[1].point + let e2 = touches[1].current(in: view) + let scale = trackScale ? e1.distance(to: e2) / s1.distance(to: s2) : 1 + let a = trackRotate ? (e1 - e2).angle() - (s1 - s2).angle() : 0 + var offset = Size.zero + if trackMove { + let sina = sin(a) + let cosa = cos(a) + let w = e1.x - scale * (s1.x * cosa - s1.y * sina) + let h = e1.y - scale * (s1.x * sina + s1.y * cosa) + offset = Size(w: w, h: h) + } + return ZoomData(offset: offset, scale: scale, angle: a).combine(with: zoomData) + } + +} + +fileprivate class ZoomData { + + let offset: Size + let scale: Double + let angle: Double + + init(offset: Size = Size.zero, scale: Double = 1, angle: Double = 0) { + self.offset = offset + self.scale = scale + self.angle = angle + } + + func transform() -> Transform { + return Transform.move(dx: offset.w, dy: offset.h).scale(sx: scale, sy: scale).rotate(angle: angle) + } + + func move(delta: Size) -> ZoomData { + return ZoomData(offset: offset + delta, scale: scale, angle: angle) + } + + func combine(with: ZoomData) -> ZoomData { + let sina = sin(angle) + let cosa = cos(angle) + let w = offset.w + scale * (cosa * with.offset.w - sina * with.offset.h) + let h = offset.h + scale * (sina * with.offset.w + cosa * with.offset.h) + let s = scale * with.scale + let a = angle + with.angle + return ZoomData(offset: Size(w: w, h: h), scale: s, angle: a) + } + +} + +fileprivate class TouchData { + + let touch: MTouch + let point: Point + + convenience init(touch: MTouch, in view: MView) { + self.init(touch: touch, point: touch.location(in: view).toMacaw()) + } + + init(touch: MTouch, point: Point) { + self.touch = touch + self.point = point + } + + func current(in view: MView) -> Point { + return touch.location(in: view).toMacaw() + } + +} diff --git a/Source/views/Touchable.swift b/Source/views/Touchable.swift index a830e63d..f5229470 100644 --- a/Source/views/Touchable.swift +++ b/Source/views/Touchable.swift @@ -19,8 +19,8 @@ class MTouchEvent: Hashable { } protocol Touchable { - func mTouchesBegan(_ touches: [MTouchEvent]) - func mTouchesMoved(_ touches: [MTouchEvent]) - func mTouchesEnded(_ touches: [MTouchEvent]) - func mTouchesCancelled(_ touches: [MTouchEvent]) + func mTouchesBegan(_ touches: Set, with event: MEvent?) + func mTouchesMoved(_ touches: Set, with event: MEvent?) + func mTouchesEnded(_ touches: Set, with event: MEvent?) + func mTouchesCancelled(_ touches: Set, with event: MEvent?) }