├── Callipers.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── simon.xcuserdatad │ └── xcschemes │ ├── Callipers.xcscheme │ └── xcschememanagement.plist ├── Callipers ├── Callipers-Info.plist ├── Callipers-Prefix.pch ├── Callipers.h ├── Callipers.m ├── CallipersOptions.xib ├── Info.plist ├── SCPathTime.h ├── SCPathTime.m └── toolbar.pdf ├── LICENSE ├── README.md └── callipers.gif /Callipers.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F43334551C256332008681AC /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F43334541C256332008681AC /* Cocoa.framework */; }; 11 | F43334581C256332008681AC /* Callipers-Prefix.pch in Resources */ = {isa = PBXBuildFile; fileRef = F43334571C256332008681AC /* Callipers-Prefix.pch */; }; 12 | F433345A1C256332008681AC /* Callipers-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F43334591C256332008681AC /* Callipers-Info.plist */; }; 13 | F433345D1C256332008681AC /* Callipers.m in Sources */ = {isa = PBXBuildFile; fileRef = F433345C1C256332008681AC /* Callipers.m */; }; 14 | F43334621C256332008681AC /* GlyphsCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F43334611C256332008681AC /* GlyphsCore.framework */; }; 15 | F433346A1C259A4C008681AC /* toolbar.pdf in Resources */ = {isa = PBXBuildFile; fileRef = F43334691C259A4C008681AC /* toolbar.pdf */; }; 16 | F4C7400B1C28200D00978B82 /* SCPathTime.m in Sources */ = {isa = PBXBuildFile; fileRef = F4C7400A1C28200C00978B82 /* SCPathTime.m */; }; 17 | F4DEB0961C2DFE9200285FB1 /* CallipersOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = F4DEB0951C2DFE9200285FB1 /* CallipersOptions.xib */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | F43334511C256332008681AC /* Callipers.glyphsTool */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Callipers.glyphsTool; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | F43334541C256332008681AC /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; 23 | F43334571C256332008681AC /* Callipers-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Callipers-Prefix.pch"; sourceTree = ""; }; 24 | F43334591C256332008681AC /* Callipers-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Callipers-Info.plist"; sourceTree = ""; }; 25 | F433345B1C256332008681AC /* Callipers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Callipers.h; sourceTree = ""; }; 26 | F433345C1C256332008681AC /* Callipers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Callipers.m; sourceTree = ""; }; 27 | F43334611C256332008681AC /* GlyphsCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GlyphsCore.framework; path = /Applications/Glyphs.app/Contents/Frameworks/GlyphsCore.framework; sourceTree = ""; }; 28 | F43334631C256332008681AC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | F43334691C259A4C008681AC /* toolbar.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = toolbar.pdf; sourceTree = ""; }; 30 | F4C740091C28200C00978B82 /* SCPathTime.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SCPathTime.h; sourceTree = ""; }; 31 | F4C7400A1C28200C00978B82 /* SCPathTime.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SCPathTime.m; sourceTree = ""; }; 32 | F4DEB0951C2DFE9200285FB1 /* CallipersOptions.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CallipersOptions.xib; sourceTree = ""; }; 33 | /* End PBXFileReference section */ 34 | 35 | /* Begin PBXFrameworksBuildPhase section */ 36 | F433344E1C256332008681AC /* Frameworks */ = { 37 | isa = PBXFrameworksBuildPhase; 38 | buildActionMask = 2147483647; 39 | files = ( 40 | F43334621C256332008681AC /* GlyphsCore.framework in Frameworks */, 41 | F43334551C256332008681AC /* Cocoa.framework in Frameworks */, 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | F43334481C256332008681AC = { 49 | isa = PBXGroup; 50 | children = ( 51 | F43334561C256332008681AC /* Callipers */, 52 | F43334531C256332008681AC /* Frameworks */, 53 | F43334521C256332008681AC /* Products */, 54 | ); 55 | sourceTree = ""; 56 | }; 57 | F43334521C256332008681AC /* Products */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | F43334511C256332008681AC /* Callipers.glyphsTool */, 61 | ); 62 | name = Products; 63 | sourceTree = ""; 64 | }; 65 | F43334531C256332008681AC /* Frameworks */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | F43334541C256332008681AC /* Cocoa.framework */, 69 | ); 70 | name = Frameworks; 71 | sourceTree = ""; 72 | }; 73 | F43334561C256332008681AC /* Callipers */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | F43334571C256332008681AC /* Callipers-Prefix.pch */, 77 | F43334591C256332008681AC /* Callipers-Info.plist */, 78 | F433345B1C256332008681AC /* Callipers.h */, 79 | F433345C1C256332008681AC /* Callipers.m */, 80 | F4C740091C28200C00978B82 /* SCPathTime.h */, 81 | F4C7400A1C28200C00978B82 /* SCPathTime.m */, 82 | F43334631C256332008681AC /* Info.plist */, 83 | F433345E1C256332008681AC /* Supporting Files */, 84 | F4DEB0951C2DFE9200285FB1 /* CallipersOptions.xib */, 85 | ); 86 | path = Callipers; 87 | sourceTree = ""; 88 | }; 89 | F433345E1C256332008681AC /* Supporting Files */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | F43334691C259A4C008681AC /* toolbar.pdf */, 93 | F43334611C256332008681AC /* GlyphsCore.framework */, 94 | ); 95 | name = "Supporting Files"; 96 | sourceTree = ""; 97 | }; 98 | /* End PBXGroup section */ 99 | 100 | /* Begin PBXNativeTarget section */ 101 | F43334501C256332008681AC /* Callipers */ = { 102 | isa = PBXNativeTarget; 103 | buildConfigurationList = F43334661C256332008681AC /* Build configuration list for PBXNativeTarget "Callipers" */; 104 | buildPhases = ( 105 | F433344D1C256332008681AC /* Sources */, 106 | F433344E1C256332008681AC /* Frameworks */, 107 | F433344F1C256332008681AC /* Resources */, 108 | ); 109 | buildRules = ( 110 | ); 111 | dependencies = ( 112 | ); 113 | name = Callipers; 114 | productName = Callipers; 115 | productReference = F43334511C256332008681AC /* Callipers.glyphsTool */; 116 | productType = "com.apple.product-type.bundle"; 117 | }; 118 | /* End PBXNativeTarget section */ 119 | 120 | /* Begin PBXProject section */ 121 | F43334491C256332008681AC /* Project object */ = { 122 | isa = PBXProject; 123 | attributes = { 124 | LastUpgradeCheck = 0710; 125 | ORGANIZATIONNAME = "Simon Cozens"; 126 | TargetAttributes = { 127 | F43334501C256332008681AC = { 128 | CreatedOnToolsVersion = 7.1.1; 129 | }; 130 | }; 131 | }; 132 | buildConfigurationList = F433344C1C256332008681AC /* Build configuration list for PBXProject "Callipers" */; 133 | compatibilityVersion = "Xcode 3.2"; 134 | developmentRegion = English; 135 | hasScannedForEncodings = 0; 136 | knownRegions = ( 137 | en, 138 | ); 139 | mainGroup = F43334481C256332008681AC; 140 | productRefGroup = F43334521C256332008681AC /* Products */; 141 | projectDirPath = ""; 142 | projectRoot = ""; 143 | targets = ( 144 | F43334501C256332008681AC /* Callipers */, 145 | ); 146 | }; 147 | /* End PBXProject section */ 148 | 149 | /* Begin PBXResourcesBuildPhase section */ 150 | F433344F1C256332008681AC /* Resources */ = { 151 | isa = PBXResourcesBuildPhase; 152 | buildActionMask = 2147483647; 153 | files = ( 154 | F4DEB0961C2DFE9200285FB1 /* CallipersOptions.xib in Resources */, 155 | F433345A1C256332008681AC /* Callipers-Info.plist in Resources */, 156 | F433346A1C259A4C008681AC /* toolbar.pdf in Resources */, 157 | F43334581C256332008681AC /* Callipers-Prefix.pch in Resources */, 158 | ); 159 | runOnlyForDeploymentPostprocessing = 0; 160 | }; 161 | /* End PBXResourcesBuildPhase section */ 162 | 163 | /* Begin PBXSourcesBuildPhase section */ 164 | F433344D1C256332008681AC /* Sources */ = { 165 | isa = PBXSourcesBuildPhase; 166 | buildActionMask = 2147483647; 167 | files = ( 168 | F433345D1C256332008681AC /* Callipers.m in Sources */, 169 | F4C7400B1C28200D00978B82 /* SCPathTime.m in Sources */, 170 | ); 171 | runOnlyForDeploymentPostprocessing = 0; 172 | }; 173 | /* End PBXSourcesBuildPhase section */ 174 | 175 | /* Begin XCBuildConfiguration section */ 176 | F43334641C256332008681AC /* Debug */ = { 177 | isa = XCBuildConfiguration; 178 | buildSettings = { 179 | ALWAYS_SEARCH_USER_PATHS = NO; 180 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 181 | CLANG_CXX_LIBRARY = "libc++"; 182 | CLANG_ENABLE_MODULES = YES; 183 | CLANG_ENABLE_OBJC_ARC = YES; 184 | CLANG_WARN_BOOL_CONVERSION = YES; 185 | CLANG_WARN_CONSTANT_CONVERSION = YES; 186 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 187 | CLANG_WARN_EMPTY_BODY = YES; 188 | CLANG_WARN_ENUM_CONVERSION = YES; 189 | CLANG_WARN_INT_CONVERSION = YES; 190 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 191 | CLANG_WARN_UNREACHABLE_CODE = YES; 192 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 193 | COPY_PHASE_STRIP = NO; 194 | DEBUG_INFORMATION_FORMAT = dwarf; 195 | ENABLE_STRICT_OBJC_MSGSEND = YES; 196 | ENABLE_TESTABILITY = YES; 197 | GCC_C_LANGUAGE_STANDARD = gnu99; 198 | GCC_DYNAMIC_NO_PIC = NO; 199 | GCC_NO_COMMON_BLOCKS = YES; 200 | GCC_OPTIMIZATION_LEVEL = 0; 201 | GCC_PREPROCESSOR_DEFINITIONS = ( 202 | "DEBUG=1", 203 | "$(inherited)", 204 | ); 205 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 206 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 207 | GCC_WARN_UNDECLARED_SELECTOR = YES; 208 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 209 | GCC_WARN_UNUSED_FUNCTION = YES; 210 | GCC_WARN_UNUSED_VARIABLE = YES; 211 | MTL_ENABLE_DEBUG_INFO = YES; 212 | ONLY_ACTIVE_ARCH = YES; 213 | }; 214 | name = Debug; 215 | }; 216 | F43334651C256332008681AC /* Release */ = { 217 | isa = XCBuildConfiguration; 218 | buildSettings = { 219 | ALWAYS_SEARCH_USER_PATHS = NO; 220 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 221 | CLANG_CXX_LIBRARY = "libc++"; 222 | CLANG_ENABLE_MODULES = YES; 223 | CLANG_ENABLE_OBJC_ARC = YES; 224 | CLANG_WARN_BOOL_CONVERSION = YES; 225 | CLANG_WARN_CONSTANT_CONVERSION = YES; 226 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 227 | CLANG_WARN_EMPTY_BODY = YES; 228 | CLANG_WARN_ENUM_CONVERSION = YES; 229 | CLANG_WARN_INT_CONVERSION = YES; 230 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 231 | CLANG_WARN_UNREACHABLE_CODE = YES; 232 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 233 | COPY_PHASE_STRIP = NO; 234 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 235 | ENABLE_NS_ASSERTIONS = NO; 236 | ENABLE_STRICT_OBJC_MSGSEND = YES; 237 | GCC_C_LANGUAGE_STANDARD = gnu99; 238 | GCC_NO_COMMON_BLOCKS = YES; 239 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 240 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 241 | GCC_WARN_UNDECLARED_SELECTOR = YES; 242 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 243 | GCC_WARN_UNUSED_FUNCTION = YES; 244 | GCC_WARN_UNUSED_VARIABLE = YES; 245 | MTL_ENABLE_DEBUG_INFO = NO; 246 | }; 247 | name = Release; 248 | }; 249 | F43334671C256332008681AC /* Debug */ = { 250 | isa = XCBuildConfiguration; 251 | buildSettings = { 252 | COMBINE_HIDPI_IMAGES = YES; 253 | DEPLOYMENT_LOCATION = YES; 254 | DSTROOT = "~/Library/Application Support/Glyphs/Plugins"; 255 | FRAMEWORK_SEARCH_PATHS = "$(SYSTEM_APPS_DIR)/Glyphs.app/Contents/Frameworks"; 256 | INFOPLIST_FILE = Callipers/Info.plist; 257 | INSTALL_PATH = /; 258 | PRODUCT_BUNDLE_IDENTIFIER = "org.simon-cozens.Callipers"; 259 | PRODUCT_NAME = "$(TARGET_NAME)"; 260 | SKIP_INSTALL = NO; 261 | WRAPPER_EXTENSION = glyphsTool; 262 | }; 263 | name = Debug; 264 | }; 265 | F43334681C256332008681AC /* Release */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | COMBINE_HIDPI_IMAGES = YES; 269 | FRAMEWORK_SEARCH_PATHS = "$(SYSTEM_APPS_DIR)/Glyphs.app/Contents/Frameworks"; 270 | INFOPLIST_FILE = Callipers/Info.plist; 271 | INSTALL_PATH = /; 272 | PRODUCT_BUNDLE_IDENTIFIER = "org.simon-cozens.Callipers"; 273 | PRODUCT_NAME = "$(TARGET_NAME)"; 274 | SKIP_INSTALL = YES; 275 | WRAPPER_EXTENSION = glyphsTool; 276 | }; 277 | name = Release; 278 | }; 279 | /* End XCBuildConfiguration section */ 280 | 281 | /* Begin XCConfigurationList section */ 282 | F433344C1C256332008681AC /* Build configuration list for PBXProject "Callipers" */ = { 283 | isa = XCConfigurationList; 284 | buildConfigurations = ( 285 | F43334641C256332008681AC /* Debug */, 286 | F43334651C256332008681AC /* Release */, 287 | ); 288 | defaultConfigurationIsVisible = 0; 289 | defaultConfigurationName = Release; 290 | }; 291 | F43334661C256332008681AC /* Build configuration list for PBXNativeTarget "Callipers" */ = { 292 | isa = XCConfigurationList; 293 | buildConfigurations = ( 294 | F43334671C256332008681AC /* Debug */, 295 | F43334681C256332008681AC /* Release */, 296 | ); 297 | defaultConfigurationIsVisible = 0; 298 | defaultConfigurationName = Release; 299 | }; 300 | /* End XCConfigurationList section */ 301 | }; 302 | rootObject = F43334491C256332008681AC /* Project object */; 303 | } 304 | -------------------------------------------------------------------------------- /Callipers.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Callipers.xcodeproj/xcuserdata/simon.xcuserdatad/xcschemes/Callipers.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Callipers.xcodeproj/xcuserdata/simon.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Callipers.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | F43334501C256332008681AC 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Callipers/Callipers-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | 6 | -------------------------------------------------------------------------------- /Callipers/Callipers-Prefix.pch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simoncozens/Callipers/d4e04321fc43694affe39f43fb82147b3d8286ec/Callipers/Callipers-Prefix.pch -------------------------------------------------------------------------------- /Callipers/Callipers.h: -------------------------------------------------------------------------------- 1 | // 2 | // Callipers.h 3 | // Callipers 4 | // 5 | // Created by Simon Cozens on 19/12/2015. 6 | // Copyright © 2015 Simon Cozens. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import 15 | #import 16 | #import 17 | 18 | #import "SCPathTime.h" 19 | 20 | typedef enum { 21 | DRAWING_START, 22 | DRAWING_END 23 | } TOOL_STATE ; 24 | 25 | typedef enum { 26 | MEASURE_CLOSEST, 27 | MEASURE_CORRESPONDING 28 | } MEASURE_MODE ; 29 | 30 | @interface Callipers : GSToolSelect { 31 | SCPathTime* segStart1; 32 | SCPathTime* segStart2; 33 | SCPathTime* segEnd1; 34 | SCPathTime* segEnd2; 35 | long cacheMin; 36 | long cacheMax; 37 | long cacheAvg; 38 | TOOL_STATE tool_state; 39 | MEASURE_MODE measure_mode; 40 | GSLayer* currentLayer; 41 | } 42 | 43 | - (IBAction) changeMeasureMode:(id)sender; 44 | 45 | @property (nonatomic, weak) IBOutlet NSWindow *optionsWindow; 46 | @property (weak) IBOutlet NSSlider *stepsSlider; 47 | @property (weak) IBOutlet NSSlider *thickSlider; 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /Callipers/Callipers.m: -------------------------------------------------------------------------------- 1 | // 2 | // Callipers.m 3 | // Callipers 4 | // 5 | // Created by Simon Cozens on 19/12/2015. 6 | // Copyright © 2015 Simon Cozens. All rights reserved. 7 | // 8 | 9 | #import "Callipers.h" 10 | #import 11 | 12 | @implementation Callipers 13 | 14 | - (id) init { 15 | self = [super init]; 16 | NSBundle * thisBundle = [NSBundle bundleForClass:[self class]]; 17 | if (thisBundle) { 18 | // The toolbar icon: 19 | _toolBarIcon = [[NSImage alloc] initWithContentsOfFile:[thisBundle pathForImageResource: @"toolbar"]]; 20 | [_toolBarIcon setTemplate:YES]; 21 | } 22 | tool_state = DRAWING_START; 23 | measure_mode = MEASURE_CLOSEST; 24 | [NSBundle loadNibNamed:@"CallipersOptions" owner:self]; 25 | [_stepsSlider setTarget:self]; 26 | [_stepsSlider setAction:@selector(redrawTheView)]; 27 | [_thickSlider setTarget:self]; 28 | [_thickSlider setAction:@selector(redrawTheView)]; 29 | 30 | segStart1 = [[SCPathTime alloc] init]; 31 | segStart2 = [[SCPathTime alloc] init]; 32 | segEnd1 = [[SCPathTime alloc] init]; 33 | segEnd2 = [[SCPathTime alloc] init]; 34 | 35 | return self; 36 | } 37 | 38 | - (NSUInteger) interfaceVersion { return 1; } 39 | 40 | - (NSUInteger) groupID { return 120; } 41 | 42 | - (NSString*) title { return @"Callipers"; } 43 | 44 | - (NSString*) trigger { return @"c"; } 45 | 46 | - (NSInteger) tempTrigger { return 0; } 47 | 48 | - (BOOL) willSelectTempTool:(id) tempTool { return NO; } 49 | 50 | - (void) mouseDown:(NSEvent*)theEvent { 51 | // Called when the mouse button is clicked. 52 | if (!([theEvent modifierFlags] & NSEventModifierFlagOption)) { 53 | return [super mouseDown:theEvent]; 54 | } 55 | _editViewController = [_windowController activeEditViewController]; 56 | currentLayer = [_editViewController.graphicView activeLayer]; 57 | // editViewController.graphicView.cursor = [NSCursor closedHandCursor]; 58 | _draggStart = [_editViewController.graphicView getActiveLocation: theEvent]; 59 | if (tool_state == DRAWING_START) { 60 | // NSLog(@"Clearing start"); 61 | segStart1 = [segStart1 init]; 62 | segStart2 = [segStart2 init]; 63 | } 64 | // NSLog(@"Clearing end"); 65 | segEnd1 = [segEnd1 init]; 66 | segEnd2 = [segEnd2 init]; 67 | cacheMin = 0; 68 | // NSLog(@"Mousedown ended"); 69 | // NSLog(@"__mouse dragged from : %@", NSStringFromPoint(_draggStart)); 70 | } 71 | 72 | - (void) mouseDragged:(NSEvent*)theEvent { 73 | // Called when the mouse is moved with the primary button down. 74 | if (!([theEvent modifierFlags] & NSEventModifierFlagOption)) { 75 | return [super mouseDragged:theEvent]; 76 | } 77 | NSPoint Loc = [_editViewController.graphicView getActiveLocation: theEvent]; 78 | [_editViewController.graphicView setNeedsDisplay: TRUE]; 79 | if ([theEvent modifierFlags] & NSShiftKeyMask) { 80 | CGFloat dx = fabs(Loc.x - _draggStart.x); 81 | CGFloat dy = fabs(Loc.y - _draggStart.y); 82 | if (dx < dy) { 83 | Loc.x = _draggStart.x; 84 | } else { 85 | Loc.y = _draggStart.y; 86 | } 87 | } 88 | _draggCurrent = Loc; 89 | _dragging = true; 90 | } 91 | 92 | - (void) mouseUp:(NSEvent*)theEvent { 93 | // Called when the primary mouse button is released. 94 | // editViewController.graphicView.cursor = [NSCursor openHandCursor]; 95 | if (!([theEvent modifierFlags] & NSEventModifierFlagOption)) { 96 | return [super mouseUp:theEvent]; 97 | } 98 | NSPoint startPoint = _draggStart; 99 | NSPoint endPoint = _draggCurrent; 100 | GSLayer* layer = [_editViewController.graphicView activeLayer]; 101 | _dragging = false; 102 | NSMutableArray* intersections = [NSMutableArray array]; 103 | /* How many segments does my line intersect? */ 104 | for (GSPath* p in [layer paths]) { 105 | int i =0; 106 | NSArray* segs = [p segments]; 107 | while (i < [segs count]) { 108 | NSArray *thisSeg = [segs objectAtIndex: i]; 109 | if ([thisSeg count] == 2) { 110 | // Set up line intersection 111 | NSPoint segstart = [[thisSeg objectAtIndex:0] pointValue]; 112 | NSPoint segend = [[thisSeg objectAtIndex:1] pointValue]; 113 | NSPoint pt = GSIntersectLineLine(startPoint, endPoint, segstart, segend); 114 | if (pt.x != NSNotFound && pt.y != NSNotFound) { 115 | CGFloat t = GSDistance(segstart,pt) / GSDistance(segstart, segend); 116 | SCPathTime *intersection = [[SCPathTime alloc] initWithPath:p SegId:i t:t]; 117 | [intersections addObject: intersection]; 118 | } 119 | } else { 120 | NSPoint segstart = [[thisSeg objectAtIndex:0] pointValue]; 121 | NSPoint handle1 = [[thisSeg objectAtIndex:1] pointValue]; 122 | NSPoint handle2 = [[thisSeg objectAtIndex:2] pointValue]; 123 | NSPoint segend = [[thisSeg objectAtIndex:3] pointValue]; 124 | NSArray* localIntersections = GSIntersectBezier3Line(segstart, handle1, handle2, segend, startPoint, endPoint); 125 | for (id _pt in localIntersections) { 126 | NSPoint pt = [_pt pointValue]; 127 | CGFloat t; 128 | [p nearestPointOnPath:pt pathTime:&t]; 129 | t = fmod(t, 1.0); 130 | SCPathTime *intersection = [[SCPathTime alloc] initWithPath:p SegId:i t:t]; 131 | [intersections addObject: intersection]; 132 | } 133 | } 134 | i++; 135 | } 136 | } 137 | // NSLog(@"Found %lu intersections!", (unsigned long)[intersections count]); 138 | if ([intersections count] != 2) { 139 | [_editViewController.graphicView setNeedsDisplay: TRUE]; 140 | return; 141 | } 142 | 143 | if (tool_state == DRAWING_START) { 144 | tool_state = DRAWING_END; 145 | segStart1 = [intersections objectAtIndex:0]; 146 | segStart2 = [intersections objectAtIndex:1]; 147 | // NSLog(@"Setting start"); 148 | // NSLog(@"start1: %@, %lu, %g", segStart1->path, segStart1->segId, segStart1->t); 149 | // NSLog(@"start2: %@, %lu, %g", segStart2->path, segStart2->segId, segStart2->t); 150 | } else { 151 | tool_state = DRAWING_START; 152 | segEnd1 = [intersections objectAtIndex:0]; 153 | segEnd2 = [intersections objectAtIndex:1]; 154 | _dragging = false; 155 | // NSLog(@"Setting end"); 156 | // NSLog(@"start1: %@, %lu, %g", segStart1->path, segStart1->segId, segStart1->t); 157 | // NSLog(@"start2: %@, %lu, %g", segStart2->path, segStart2->segId, segStart2->t); 158 | // NSLog(@"end1: %@, %lu, %g", segEnd1->path, segEnd1->segId, segEnd1->t); 159 | // NSLog(@"end2: %@, %lu, %g", segEnd2->path, segEnd2->segId, segEnd2->t); 160 | [_editViewController.graphicView setNeedsDisplay: TRUE]; 161 | } 162 | 163 | } 164 | 165 | - (void) redrawTheView { 166 | [_editViewController.graphicView setNeedsDisplay: TRUE]; 167 | } 168 | 169 | - (void) drawBackground { 170 | // Draw in the background, concerns the complete view. 171 | } 172 | 173 | - (NSPoint) minSquareDistancePoint:(NSPoint) p Curve:(SCPathTime*)c { 174 | NSPoint best = [c point]; 175 | if (measure_mode == MEASURE_CORRESPONDING) return best; 176 | 177 | long bestDist = GSSquareDistance(p, best); 178 | SCPathTime* c2 = [c copy]; 179 | while (true){ 180 | [c2 stepTimeBy:0.01]; 181 | NSPoint p2 = [c2 point]; 182 | long d = GSSquareDistance(p, p2); 183 | if (d < bestDist) { 184 | bestDist = d; 185 | best = p2; 186 | } 187 | if (d > bestDist) break; 188 | } 189 | c2 = [c copy]; 190 | while (true){ 191 | [c2 stepTimeBy:-0.01]; 192 | NSPoint p2 = [c2 point]; 193 | long d = GSSquareDistance(p, p2); 194 | if (d < bestDist) { 195 | bestDist = d; 196 | best = p2; 197 | } 198 | if (d > bestDist) break; 199 | } 200 | return best; 201 | } 202 | 203 | - (CGFloat) segLength: (GSPath*)p segId:(NSInteger)segId from:(CGFloat)t1 to:(CGFloat)t2 { 204 | NSArray* seg = p.segments[segId]; 205 | if ([ seg count] == 2) { 206 | NSPoint start = [[seg objectAtIndex:0] pointValue]; 207 | NSPoint end = [[seg objectAtIndex:1] pointValue]; 208 | CGFloat x1 = start.x + (end.x-start.x)*fmod(t1,1.0); 209 | CGFloat y1 = start.y + (end.y-start.y)*fmod(t1,1.0); 210 | CGFloat x2 = start.x + (end.x-start.x)*fmod(t2,1.0); 211 | CGFloat y2 = start.y + (end.y-start.y)*fmod(t2,1.0); 212 | return sqrtf( (float)(((x1 - x2) * (x1 - x2)) + ((y1 - y2) * (y1 - y2)))); 213 | } else { 214 | NSPoint o1, o2, o3, o4; 215 | NSPoint i1 = [[seg objectAtIndex:0] pointValue]; 216 | NSPoint i2 = [[seg objectAtIndex:1] pointValue]; 217 | NSPoint i3 = [[seg objectAtIndex:2] pointValue]; 218 | NSPoint i4 = [[seg objectAtIndex:3] pointValue]; 219 | GSSegmentBetweenPoints(i1,i2,i3,i4, &o1, &o2, &o3, &o4, GSPointAtTime(i1,i2,i3,i4,t1),GSPointAtTime(i1,i2,i3,i4,t2)); 220 | return GSLengthOfSegment(o1,o2,o3,o4); 221 | } 222 | } 223 | 224 | - (CGFloat) pathLength: (SCPathTime*)start to: (SCPathTime*) end { 225 | SCPathTime *p1, *p2; 226 | if (start->segId > end->segId || (start->segId == end->segId && start->t > end->t)) { 227 | p1 = end; p2 = start; 228 | } else { 229 | p1 = start; p2 = end; 230 | } 231 | NSInteger segId = p1->segId; 232 | CGFloat total = 0; 233 | CGFloat t = p1->t; 234 | while (segId < p2->segId) { 235 | total += [self segLength: p1->path segId:segId from:t to:1]; 236 | segId++; 237 | t = 0; 238 | } 239 | total += [self segLength:p1->path segId:segId from:t to:p2->t]; 240 | return total; 241 | } 242 | 243 | 244 | - (void) drawForegroundForLayer:(GSLayer *)Layer { 245 | 246 | if (segStart1->segId == NSNotFound || segEnd1->segId == NSNotFound || 247 | segStart2->segId == NSNotFound || segEnd2->segId == NSNotFound || 248 | !segStart1->path || !segStart2->path) { 249 | if (_dragging) { 250 | NSBezierPath * path = [NSBezierPath bezierPath]; 251 | [path setLineWidth: 1]; 252 | [path moveToPoint: _draggStart]; 253 | [path lineToPoint: _draggCurrent]; 254 | if (tool_state == DRAWING_START) { 255 | [[NSColor greenColor] set]; 256 | } else { [[NSColor redColor] set]; } 257 | [path stroke]; 258 | } 259 | return; 260 | } 261 | // NSLog(@"Drawing!"); 262 | 263 | // Measure the two paths. Swap if needed 264 | CGFloat sl1 = [self pathLength:segStart1 to:segEnd1]; 265 | CGFloat sl2 = [self pathLength:segStart2 to:segEnd2]; 266 | // NSLog(@"Length of intersections, 1: %g, 2: %g", sl1, sl2); 267 | if (sl1 < sl2) { 268 | SCPathTime* ss = segStart2; 269 | SCPathTime* se = segEnd2; 270 | segStart2 = segStart1; segStart1 = ss; 271 | segEnd2 = segEnd1; segEnd1 = se; 272 | } 273 | 274 | int steps = [_stepsSlider intValue]; 275 | CGFloat step1 = ((segEnd1->segId + segEnd1->t) - (segStart1->segId + segStart1->t)) / steps; // XXX 276 | CGFloat step2 = ((segEnd2->segId + segEnd2->t) - (segStart2->segId + segStart2->t)) / steps; 277 | long maxLen, minLen, avgLen; 278 | SCPathTime* t1, *t2; 279 | 280 | if (cacheMin == 0) { 281 | minLen = 99999; 282 | maxLen = 0; 283 | avgLen = 0; 284 | t1 = [segStart1 copy]; 285 | t2 = [segStart2 copy]; 286 | int actualSteps = 0; 287 | while ([t1 compareWith: segEnd1] != copysign(1.0, step1)) { 288 | NSPoint p1 = [t1 point]; 289 | NSPoint p2 = [self minSquareDistancePoint:p1 Curve:t2]; 290 | long dist = GSSquareDistance(p1,p2); 291 | if (dist < minLen) minLen = dist; 292 | if (dist > maxLen) maxLen = dist; 293 | avgLen += dist; 294 | [t1 stepTimeBy:step1]; 295 | [t2 stepTimeBy:step2]; 296 | actualSteps++; 297 | } 298 | cacheAvg = avgLen = avgLen / actualSteps; 299 | cacheMin = minLen; 300 | cacheMax = maxLen; 301 | } else { 302 | maxLen = cacheMax; 303 | minLen = cacheMin; 304 | avgLen = cacheAvg; 305 | } 306 | 307 | t1 = [segStart1 copy]; 308 | t2 = [segStart2 copy]; 309 | while ([t1 compareWith: segEnd1] != copysign(1.0, step1)) { 310 | NSPoint p1 = [t1 point]; 311 | NSPoint p2 = [self minSquareDistancePoint:p1 Curve:t2]; 312 | long dist = GSSquareDistance(p1,p2); 313 | NSBezierPath * path = [NSBezierPath bezierPath]; 314 | CGFloat scale = fabs((CGFloat)maxLen-minLen); 315 | if (scale < 5) scale = 5; 316 | CGFloat hue = (120+((avgLen-dist)/scale*180.0))/360; 317 | // if (hue < 0.2) hue -= 0.11; 318 | NSColor *c = [NSColor colorWithHue:hue saturation:1.0 brightness:1.0 alpha:1]; 319 | NSLog(@"Dist: %li, hue: %g. Min: %li, avg: %li, max: %li", dist, hue, minLen, avgLen, maxLen); 320 | [path setLineWidth: [_thickSlider intValue]]; 321 | [path moveToPoint: p1]; 322 | [path lineToPoint: p2]; 323 | [c set]; 324 | [path stroke]; 325 | [t1 stepTimeBy:step1]; 326 | [t2 stepTimeBy:step2]; 327 | } 328 | 329 | [super drawBackgroundForLayer:Layer]; 330 | } 331 | // 332 | //- (void) drawLayer:(GSLayer*)Layer atPoint:(NSPoint)aPoint asActive:(BOOL)Active attributes:(NSDictionary*)Attributes { 333 | // // Draw anythin for this particular layer. 334 | // [ _editViewController.graphicView drawLayerOutlines:Layer aPoint:aPoint color:[NSColor blackColor] fill:!Active]; 335 | //} 336 | 337 | - (void) willActivate { 338 | // Called when the tool is selected by the user. 339 | // editViewController.graphicView.cursor = [NSCursor openHandCursor]; 340 | // NSLog(@"Willactivate called"); 341 | GSLayer* layer = [_editViewController.graphicView activeLayer]; 342 | if (layer != currentLayer) { 343 | [_optionsWindow orderFront:self]; 344 | segStart1 = [segStart1 init]; 345 | segStart2 = [segStart2 init]; 346 | segEnd1 = [segEnd1 init]; 347 | segEnd2 = [segEnd2 init]; 348 | } 349 | } 350 | 351 | - (IBAction) changeMeasureMode:(NSButton*)sender { 352 | if ([sender tag] == 1) { measure_mode = MEASURE_CLOSEST; } 353 | else if ([sender tag] == 2) { measure_mode = MEASURE_CORRESPONDING; } 354 | [self redrawTheView]; 355 | } 356 | 357 | - (void) willDeactivate { 358 | [_optionsWindow orderOut:self]; 359 | } 360 | 361 | @end 362 | -------------------------------------------------------------------------------- /Callipers/CallipersOptions.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 74 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /Callipers/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Callipers/SCPathTime.h: -------------------------------------------------------------------------------- 1 | // 2 | // SCPathTime.h 3 | // Callipers 4 | // 5 | // Created by Simon Cozens on 21/12/2015. 6 | // Copyright © 2015 Simon Cozens. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface SCPathTime : NSObject { 13 | @public NSInteger segId; 14 | @public CGFloat t; 15 | @public GSPath* path; 16 | }; 17 | 18 | - (SCPathTime*) initWithPath:(GSPath*)p SegId:(NSInteger)i t:(CGFloat)_t; 19 | - (int) compareWith:(SCPathTime*)t2; 20 | - (void) stepTimeBy:(float)step; 21 | - (NSPoint) point; 22 | @end 23 | -------------------------------------------------------------------------------- /Callipers/SCPathTime.m: -------------------------------------------------------------------------------- 1 | // 2 | // SCPathTime.m 3 | // Callipers 4 | // 5 | // Created by Simon Cozens on 21/12/2015. 6 | // Copyright © 2015 Simon Cozens. All rights reserved. 7 | // 8 | 9 | #import "SCPathTime.h" 10 | #import 11 | 12 | @implementation SCPathTime 13 | 14 | - (void) stepTimeBy:(float)step { 15 | t += step; 16 | if (t <= 0) { t += 1; segId--; } 17 | if (t >= 1) { t -= 1; segId++; } 18 | } 19 | 20 | /* We do this madness instead of the path's pointAtTime function because 21 | a) that fails on lines 22 | b) the path time includes both on-curve and off-curve points (!) 23 | */ 24 | - (NSPoint) point { 25 | NSArray* seg = path.segments[segId]; 26 | if ([ seg count] == 2) { 27 | NSPoint p1 = [[seg objectAtIndex:0] pointValue]; 28 | NSPoint p2 = [[seg objectAtIndex:1] pointValue]; 29 | CGFloat x = p1.x + (p2.x-p1.x)*fmod(t,1.0); 30 | CGFloat y = p1.y + (p2.y-p1.y)*fmod(t,1.0); 31 | // NSLog(@"p1=[%g,%g] at t(%g)= [%g,%g] p2=[%g,%g]",p1.x,p1.y,fmod(t,1.0),x,y,p2.x,p2.y); 32 | return NSMakePoint(x, y); 33 | } 34 | NSPoint p = GSPointAtTime( 35 | [[seg objectAtIndex:0] pointValue], 36 | [[seg objectAtIndex:1] pointValue], 37 | [[seg objectAtIndex:2] pointValue], 38 | [[seg objectAtIndex:3] pointValue], 39 | t); 40 | return p; 41 | } 42 | 43 | - (int) compareWith:(SCPathTime*)t2 { 44 | if (segId < t2->segId) return -1; 45 | if (segId > t2->segId) return 1; 46 | return t < t2->t ? -1 : t > t2->t ? 1 : 0; 47 | } 48 | 49 | - (SCPathTime*) copy { 50 | SCPathTime* n = [[SCPathTime alloc] init]; 51 | n->path = self->path; 52 | n->segId = self->segId; 53 | n->t = self->t; 54 | return n; 55 | } 56 | 57 | - (SCPathTime*) initWithPath:(GSPath*)p SegId:(NSInteger)i t:(CGFloat)_t { 58 | segId = i; 59 | t = _t; 60 | path = p; 61 | return self; 62 | } 63 | 64 | - (SCPathTime*) init { 65 | segId = NSNotFound; 66 | t = 0; 67 | path = NULL; 68 | return self; 69 | } 70 | 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /Callipers/toolbar.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simoncozens/Callipers/d4e04321fc43694affe39f43fb82147b3d8286ec/Callipers/toolbar.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Simon Cozens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Callipers 2 | A Glyphs.app plugin for visualising stroke uniformity. 3 | 4 | The Callipers plugin helps you to visualize the thickness of strokes. If you are creating a low contrast font, you want to ensure that curves on the inside and outside of a stroke vary at the same rate, so that your stroke remains a consistent stem thickness throughout the curve. Presumably decent type designers can do this by eye, but I am not a decent type designer, and need help from a computer to tell me whether my stems have consistent thickness or not. That's what the Callipers plugin does. This animation should be worth a thousand words: 5 | 6 | ![Animation](https://raw.githubusercontent.com/simoncozens/Callipers/master/callipers.gif) 7 | 8 | First, select the Callipers tool from the tool bar. Then draw a line through the stem; this green line marks where the measurement will start. Then draw another line; the red one marks where it will stop. (If you don't bisect exactly two curves, you have to do it again.) Then Callipers will show you your stem thickness: green marks areas of average thickness; redder areas are thicker than average and bluer ones are thinner than average. 9 | -------------------------------------------------------------------------------- /callipers.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simoncozens/Callipers/d4e04321fc43694affe39f43fb82147b3d8286ec/callipers.gif --------------------------------------------------------------------------------