├── .appcast.xml ├── .gitignore ├── .sketchpacks.json ├── LICENSE ├── README.md ├── assets └── icon.png ├── package-lock.json ├── package.json └── src ├── interface.js ├── manifest.json ├── options.js └── photo-grid.js /.appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artifacts 2 | PhotoGrid.sketchplugin 3 | 4 | # npm 5 | node_modules 6 | .npm 7 | npm-debug.log 8 | 9 | # mac 10 | .DS_Store 11 | 12 | # WebStorm 13 | .idea 14 | -------------------------------------------------------------------------------- /.sketchpacks.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "1.0.0", 3 | "manifest_path": "src/manifest.json", 4 | "appcast_path": "https://raw.githubusercontent.com/perrysmotors/photo-grid/master/.appcast.xml" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Giles Perry 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 | # Photo Grid plugin for Sketch 2 | [![Download from Sketchpacks.com](https://badges.sketchpacks.com/plugins/com.gilesperry.photo-grid/version.svg)](https://api.sketchpacks.com/v1/plugins/com.gilesperry.photo-grid/download) [![Compatible Sketch Version](https://badges.sketchpacks.com/plugins/com.gilesperry.photo-grid/compatibility.svg)](https://sketchpacks.com/perrysmotors/photo-grid) 3 | 4 | A Sketch plugin that can size layers to common photo dimensions and scale them to fit in a row. 5 | 6 | ![photo grid video](https://user-images.githubusercontent.com/12557727/39623844-e1a84eb4-4f8e-11e8-850f-2bfb0476f35d.gif) 7 | 8 | ## Features 9 | - Apply random aspect ratios to selected layers corresponding to common photo sizes. 10 | - Scale and space layers to fit between the furthest left and right layers in the selection. 11 | - Choose row or column layout and set the spacing between layers. 12 | - Option to set a fixed width when scaling rows. 13 | 14 | ## Supports row or column layouts 15 | 16 | ![layout](https://user-images.githubusercontent.com/12557727/39624616-dd46651a-4f91-11e8-8a00-d89f29f55fcd.png) 17 | 18 | ## Installation 19 | 20 | * [Download](../../releases/latest/download/PhotoGrid.sketchplugin.zip) the latest release of the plugin 21 | * Un-zip 22 | * Double-click on `PhotoGrid.sketchplugin` 23 | 24 | or... 25 | 26 | [![Install Photo Grid with Sketchpacks](http://sketchpacks-com.s3.amazonaws.com/assets/badges/sketchpacks-badge-install.png "Install Photo Grid with Sketchpacks")](https://sketchpacks.com/perrysmotors/photo-grid/install) 27 | 28 | --- 29 | 30 | **If you are using this plugin, please 'star' the project**. It's a simple way to help me see how many people are using it. 31 | 32 | If you ***love*** this plugin, why not shout me a coffee ☕️ via [PayPal](https://www.paypal.me/perrysmotors/2) to share the love! 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perrysmotors/photo-grid/a5646890c90be66b203c387f58699fbd50da63cd/assets/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "photo_grid", 3 | "description": "Size layers to common photo dimensions and scale them to fit in rows or columns", 4 | "author": "Giles Perry (http://gilesperry.info/)", 5 | "version": "3.1.3", 6 | "engines": { 7 | "sketch": ">=3.0" 8 | }, 9 | "skpm": { 10 | "name": "Photo Grid", 11 | "manifest": "src/manifest.json", 12 | "identifier": "com.gilesperry.photo-grid", 13 | "main": "PhotoGrid.sketchplugin", 14 | "assets": [ 15 | "assets/**/*" 16 | ] 17 | }, 18 | "scripts": { 19 | "build": "skpm-build", 20 | "watch": "skpm-build --watch", 21 | "start": "skpm-build --watch --run", 22 | "postinstall": "npm run build && skpm-link" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/perrysmotors/photo-grid.git" 27 | }, 28 | "license": "MIT", 29 | "devDependencies": { 30 | "@skpm/builder": "^0.7.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/interface.js: -------------------------------------------------------------------------------- 1 | import UI from "sketch/ui" 2 | import Settings from "sketch/settings" 3 | 4 | import { options } from "./options" 5 | 6 | const form = {} 7 | 8 | export function onSettings(context) { 9 | const alert = createDialog() 10 | const response = alert.runModal() 11 | 12 | if (response == "1000") { 13 | // This code only runs when the user clicks 'OK'; 14 | 15 | // Get Spacing 16 | let spacingTextFieldInput = form.spacingTextField.stringValue() 17 | let spacingValue = parseInt(spacingTextFieldInput) 18 | 19 | if (isNaN(spacingValue) || spacingTextFieldInput === "") { 20 | UI.message("⚠️ The spacing was not changed. Try entering a number.") 21 | } else if (spacingValue < 0 || spacingValue > 1000) { 22 | UI.message("⚠️ Enter a spacing value between 0 and 1000") 23 | } else { 24 | options.padding = spacingValue 25 | Settings.setSettingForKey("padding", spacingValue) 26 | } 27 | 28 | // Get Layout 29 | options.isRowLayout = form.rowsRadioButton.state() === NSOnState 30 | Settings.setSettingForKey("isRowLayout", options.isRowLayout) 31 | 32 | // Get max width setting 33 | options.hasWidthLimit = form.hasWidthLimitCheckbox.state() === NSOnState 34 | Settings.setSettingForKey("hasWidthLimit", options.hasWidthLimit) 35 | 36 | // Get width value 37 | let maxWidthTextFieldInput = form.maxWidthTextField.stringValue() 38 | let maxWidthValue = parseInt(maxWidthTextFieldInput) 39 | 40 | if (isNaN(maxWidthValue) || maxWidthTextFieldInput === "") { 41 | UI.message( 42 | "⚠️ The maximum width was not changed. Try entering a number." 43 | ) 44 | } else if (maxWidthValue < 10 || maxWidthValue > 10000) { 45 | UI.message("⚠️ Enter a maximum width between 10 and 10,000") 46 | } else { 47 | options.maxWidth = maxWidthValue 48 | Settings.setSettingForKey("maxWidth", maxWidthValue) 49 | } 50 | } 51 | } 52 | 53 | function createDialog() { 54 | const viewWidth = 360 55 | const viewHeight = 250 56 | 57 | // Setup the window 58 | const dialog = NSAlert.alloc().init() 59 | dialog.setMessageText("Photo Grid Settings") 60 | dialog.addButtonWithTitle("Ok") 61 | dialog.addButtonWithTitle("Cancel") 62 | 63 | // Create the main view 64 | const view = NSView.alloc().initWithFrame( 65 | NSMakeRect(0, 0, viewWidth, viewHeight) 66 | ) 67 | dialog.setAccessoryView(view) 68 | 69 | // -------------------------------------------------------------------------- 70 | 71 | // Create labels 72 | const infoLabel = createTextField( 73 | "Choose row or column layout and set the layer spacing. Photo Grid will try to keep layers in existing rows or columns.", 74 | NSMakeRect(0, viewHeight - 40, viewWidth - 10, 40) 75 | ) 76 | const spacingLabel = createTextField( 77 | "Spacing:", 78 | NSMakeRect(0, viewHeight - 70, 200, 20) 79 | ) 80 | const layoutLabel = createTextField( 81 | "Layout:", 82 | NSMakeRect(0, viewHeight - 135, 200, 20) 83 | ) 84 | const maxWidthLabel = createTextField( 85 | "Scale and Fit Rows to Fixed Width:", 86 | NSMakeRect(0, viewHeight - 200, viewWidth - 10, 20) 87 | ) 88 | 89 | // Create textfields 90 | form.spacingTextField = NSTextField.alloc().initWithFrame( 91 | NSMakeRect(0, viewHeight - 95, 70, 20) 92 | ) 93 | form.maxWidthTextField = NSTextField.alloc().initWithFrame( 94 | NSMakeRect(90, viewHeight - 225, 70, 20) 95 | ) 96 | 97 | // Create radiobuttons 98 | form.rowsRadioButton = createRadioButton( 99 | "Rows →", 100 | NSMakeRect(0, viewHeight - 160, 90, 20) 101 | ) 102 | form.columnsRadioButton = createRadioButton( 103 | "Columns ↓", 104 | NSMakeRect(80, viewHeight - 160, 90, 20) 105 | ) 106 | 107 | // Create checkbox 108 | form.hasWidthLimitCheckbox = createCheckbox( 109 | "On", 110 | NSMakeRect(0, viewHeight - 225, 90, 20) 111 | ) 112 | 113 | // -------------------------------------------------------------------------- 114 | 115 | // Set initial input values and enabled states 116 | form.spacingTextField.setStringValue(String(options.padding)) 117 | form.maxWidthTextField.setStringValue(String(options.maxWidth)) 118 | 119 | if (options.hasWidthLimit) { 120 | form.hasWidthLimitCheckbox.setState(NSOnState) 121 | } else { 122 | form.maxWidthTextField.setEnabled(false) 123 | } 124 | 125 | if (options.isRowLayout) { 126 | form.rowsRadioButton.setState(NSOnState) 127 | } else { 128 | form.columnsRadioButton.setState(NSOnState) 129 | form.hasWidthLimitCheckbox.setEnabled(false) 130 | form.maxWidthTextField.setEnabled(false) 131 | } 132 | 133 | // -------------------------------------------------------------------------- 134 | 135 | // Handle Enable / Disable Events 136 | form.hasWidthLimitCheckbox.setCOSJSTargetFunction(sender => { 137 | form.maxWidthTextField.setEnabled(sender.state() === NSOnState) 138 | }) 139 | 140 | let radioTargetFunction = sender => { 141 | let isRowLayout = sender === form.rowsRadioButton 142 | let hasWidthLimit = form.hasWidthLimitCheckbox.state() === NSOnState 143 | if (isRowLayout) { 144 | form.hasWidthLimitCheckbox.setEnabled(true) 145 | form.maxWidthTextField.setEnabled(hasWidthLimit) 146 | } else { 147 | form.hasWidthLimitCheckbox.setEnabled(false) 148 | form.maxWidthTextField.setEnabled(false) 149 | } 150 | } 151 | 152 | form.rowsRadioButton.setCOSJSTargetFunction(sender => 153 | radioTargetFunction(sender) 154 | ) 155 | form.columnsRadioButton.setCOSJSTargetFunction(sender => 156 | radioTargetFunction(sender) 157 | ) 158 | 159 | // -------------------------------------------------------------------------- 160 | 161 | // Add inputs to view 162 | view.addSubview(infoLabel) 163 | view.addSubview(spacingLabel) 164 | view.addSubview(layoutLabel) 165 | view.addSubview(maxWidthLabel) 166 | view.addSubview(form.spacingTextField) 167 | view.addSubview(form.maxWidthTextField) 168 | view.addSubview(form.rowsRadioButton) 169 | view.addSubview(form.columnsRadioButton) 170 | view.addSubview(form.hasWidthLimitCheckbox) 171 | 172 | // -------------------------------------------------------------------------- 173 | 174 | // Show the dialog window 175 | return dialog 176 | } 177 | 178 | function createTextField(stringValue, frame) { 179 | let textField = NSTextField.alloc().initWithFrame(frame) 180 | textField.setStringValue(stringValue) 181 | textField.setSelectable(false) 182 | textField.setEditable(false) 183 | textField.setBezeled(false) 184 | textField.setDrawsBackground(false) 185 | return textField 186 | } 187 | 188 | function createCheckbox(title, frame) { 189 | let checkbox = NSButton.alloc().initWithFrame(frame) 190 | checkbox.setButtonType(NSSwitchButton) 191 | checkbox.setBezelStyle(0) 192 | checkbox.setTitle(title) 193 | return checkbox 194 | } 195 | 196 | function createRadioButton(title, frame) { 197 | let radioButton = NSButton.alloc().initWithFrame(frame) 198 | radioButton.setButtonType(NSRadioButton) 199 | radioButton.setTitle(title) 200 | return radioButton 201 | } 202 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "compatibleVersion": 49, 3 | "bundleVersion": 1, 4 | "icon": "icon.png", 5 | "commands": [ 6 | { 7 | "name": "Randomize Aspect Ratios", 8 | "identifier": "randomizeAspectRatios", 9 | "script": "./photo-grid.js", 10 | "handler": "onRandomizeAspectRatios" 11 | }, 12 | { 13 | "name": "Scale and Fit to Bounds", 14 | "identifier": "fit", 15 | "script": "./photo-grid.js", 16 | "handler": "onFit" 17 | }, 18 | { 19 | "name": "Settings", 20 | "identifier": "settings", 21 | "script": "./interface.js", 22 | "handler": "onSettings" 23 | } 24 | ], 25 | "menu": { 26 | "title": "Photo Grid", 27 | "items": [ 28 | "randomizeAspectRatios", 29 | "fit", 30 | "-", 31 | "settings" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | import Settings from "sketch/settings" 2 | 3 | export const options = initOptions() 4 | 5 | function initOptions() { 6 | const defaults = { 7 | isRowLayout: true, 8 | padding: 16, 9 | hasWidthLimit: false, 10 | maxWidth: 1200, 11 | } 12 | for (let option in defaults) { 13 | let value = eval(Settings.settingForKey(option)) 14 | if (value === undefined) { 15 | Settings.setSettingForKey(option, defaults[option]) 16 | } else { 17 | defaults[option] = value 18 | } 19 | } 20 | return defaults 21 | } 22 | -------------------------------------------------------------------------------- /src/photo-grid.js: -------------------------------------------------------------------------------- 1 | import UI from "sketch/ui" 2 | import DOM from "sketch/dom" 3 | 4 | import { options } from "./options" 5 | 6 | export function onRandomizeAspectRatios(context) { 7 | const document = DOM.getSelectedDocument(), 8 | selection = document.selectedLayers 9 | 10 | if (selection.length === 0) { 11 | UI.message("Select one or more layers") 12 | } else { 13 | let bounds = getBoundingBox(selection.layers) 14 | let groups = findGroups(selection.layers) 15 | 16 | groups.forEach(group => { 17 | randomizeAspectRatios(group, bounds) 18 | }) 19 | } 20 | } 21 | 22 | export function onFit(context) { 23 | const document = DOM.getSelectedDocument(), 24 | selection = document.selectedLayers 25 | 26 | if (selection.length === 0) { 27 | UI.message("Select one or more layers") 28 | } else { 29 | let bounds = getBoundingBox(selection.layers) 30 | let groups = findGroups(selection.layers) 31 | 32 | if (options.isRowLayout) { 33 | if (options.hasWidthLimit) { 34 | bounds.width = options.maxWidth 35 | } 36 | 37 | let y = bounds.y 38 | groups.forEach(group => { 39 | fitLayersInRows(group, bounds, y) 40 | y = 41 | group[0].sketchObject.absoluteRect().y() + 42 | group[0].frame.height + 43 | options.padding 44 | }) 45 | } else { 46 | let x = bounds.x 47 | groups.forEach(group => { 48 | fitLayersInColumns(group, bounds, x) 49 | x = 50 | group[0].sketchObject.absoluteRect().x() + 51 | group[0].frame.width + 52 | options.padding 53 | }) 54 | } 55 | } 56 | } 57 | 58 | function randomizeAspectRatios(layers, bounds) { 59 | let orderedLayers 60 | 61 | let x = bounds.x, 62 | y = bounds.y 63 | 64 | if (options.isRowLayout) { 65 | orderedLayers = layers.sort( 66 | (a, b) => 67 | a.sketchObject.absoluteRect().x() - 68 | b.sketchObject.absoluteRect().x() 69 | ) 70 | y = orderedLayers[0].sketchObject.absoluteRect().y() 71 | } else { 72 | orderedLayers = layers.sort( 73 | (a, b) => 74 | a.sketchObject.absoluteRect().y() - 75 | b.sketchObject.absoluteRect().y() 76 | ) 77 | x = orderedLayers[0].sketchObject.absoluteRect().x() 78 | } 79 | 80 | orderedLayers.forEach(layer => { 81 | layer.sketchObject.setConstrainProportions(0) 82 | 83 | let ratio = randomAspectRatio() 84 | let delta = getDelta(layer, x, y) 85 | let frame = layer.frame 86 | 87 | frame.x += delta.x 88 | frame.y += delta.y 89 | 90 | if (options.isRowLayout) { 91 | frame.width = Math.round(frame.height * ratio) 92 | x += frame.width + options.padding 93 | } else { 94 | frame.height = Math.round(frame.width / ratio) 95 | y += frame.height + options.padding 96 | } 97 | 98 | layer.frame = frame 99 | }) 100 | } 101 | 102 | function randomAspectRatio() { 103 | const aspectRatios = [ 104 | 1, 105 | 10 / 8, 106 | 4 / 3, 107 | 7 / 5, 108 | 3 / 2, 109 | 16 / 9, 110 | 2 / 3, 111 | 5 / 7, 112 | 3 / 4, 113 | 8 / 10, 114 | ] 115 | return aspectRatios[Math.floor(Math.random() * aspectRatios.length)] 116 | } 117 | 118 | function fitLayersInRows(layers, bounds, y) { 119 | let min = bounds.x 120 | let max = bounds.x + bounds.width 121 | 122 | let orderedLayers = layers.sort( 123 | (a, b) => 124 | a.sketchObject.absoluteRect().x() - 125 | b.sketchObject.absoluteRect().x() 126 | ) 127 | let lastLayer = orderedLayers[orderedLayers.length - 1] 128 | 129 | let height = Math.round(median(layers.map(layer => layer.frame.height))) 130 | let widths = layers.map( 131 | layer => (layer.frame.width * height) / layer.frame.height 132 | ) 133 | let totalWidth = widths.reduce((total, current) => total + current) 134 | 135 | let totalPadding = (layers.length - 1) * options.padding 136 | let scale = (max - min) / (totalWidth + totalPadding) 137 | 138 | let x = min 139 | 140 | orderedLayers.forEach(layer => { 141 | layer.sketchObject.setConstrainProportions(0) 142 | 143 | let delta = getDelta(layer, x, y) 144 | let frame = layer.frame 145 | 146 | frame.x += delta.x 147 | frame.y += delta.y 148 | 149 | frame.width = Math.round( 150 | ((frame.width * height) / frame.height) * scale 151 | ) 152 | frame.height = Math.round(height * scale) 153 | x += frame.width + options.padding 154 | 155 | layer.frame = frame 156 | }) 157 | 158 | let frame = lastLayer.frame 159 | frame.width = max - lastLayer.sketchObject.absoluteRect().x() 160 | lastLayer.frame = frame 161 | } 162 | 163 | function fitLayersInColumns(layers, bounds, x) { 164 | let min = bounds.y 165 | let max = bounds.y + bounds.height 166 | 167 | let orderedLayers = layers.sort( 168 | (a, b) => 169 | a.sketchObject.absoluteRect().y() - 170 | b.sketchObject.absoluteRect().y() 171 | ) 172 | let lastLayer = orderedLayers[orderedLayers.length - 1] 173 | 174 | let width = Math.round(median(layers.map(layer => layer.frame.width))) 175 | let heights = layers.map( 176 | layer => (layer.frame.height * width) / layer.frame.width 177 | ) 178 | let totalHeight = heights.reduce((total, current) => total + current) 179 | 180 | let totalPadding = (layers.length - 1) * options.padding 181 | let scale = (max - min) / (totalHeight + totalPadding) 182 | 183 | let y = min 184 | 185 | orderedLayers.forEach(layer => { 186 | layer.sketchObject.setConstrainProportions(0) 187 | 188 | let delta = getDelta(layer, x, y) 189 | let frame = layer.frame 190 | 191 | frame.x += delta.x 192 | frame.y += delta.y 193 | 194 | frame.height = Math.round( 195 | ((frame.height * width) / frame.width) * scale 196 | ) 197 | frame.width = Math.round(width * scale) 198 | y += frame.height + options.padding 199 | 200 | layer.frame = frame 201 | }) 202 | 203 | let frame = lastLayer.frame 204 | frame.height = max - lastLayer.sketchObject.absoluteRect().y() 205 | lastLayer.frame = frame 206 | } 207 | 208 | function getDelta(layer, x, y) { 209 | let absoluteRect = layer.sketchObject.absoluteRect() 210 | let deltaX = x - absoluteRect.x() 211 | let deltaY = y - absoluteRect.y() 212 | return { x: deltaX, y: deltaY } 213 | } 214 | 215 | function findGroups(layers) { 216 | let groups = [] 217 | let remainingLayers = new Set(layers) 218 | 219 | let range 220 | if (options.isRowLayout) { 221 | range = Math.round(median(layers.map(layer => layer.frame.height))) 222 | } else { 223 | range = Math.round(median(layers.map(layer => layer.frame.width))) 224 | } 225 | 226 | while (remainingLayers.size > 0) { 227 | let largestGroup = [] 228 | remainingLayers.forEach(layer => { 229 | let group = findLayersInGroup(remainingLayers, layer, range) 230 | if (group.length > largestGroup.length) { 231 | largestGroup = group 232 | } 233 | }) 234 | 235 | largestGroup.forEach(layer => { 236 | remainingLayers.delete(layer) 237 | }) 238 | 239 | groups.push(largestGroup) 240 | } 241 | 242 | if (options.isRowLayout) { 243 | return groups.sort( 244 | (groupA, groupB) => 245 | groupA[0].sketchObject.absoluteRect().y() - 246 | groupB[0].sketchObject.absoluteRect().y() 247 | ) 248 | } else { 249 | return groups.sort( 250 | (groupA, groupB) => 251 | groupA[0].sketchObject.absoluteRect().x() - 252 | groupB[0].sketchObject.absoluteRect().x() 253 | ) 254 | } 255 | } 256 | 257 | function findLayersInGroup(layers, referenceLayer, range) { 258 | let found = [] 259 | let rowCentre = getLayerCentre(referenceLayer) 260 | 261 | if (options.isRowLayout) { 262 | let lower = rowCentre.y - range / 2 263 | let upper = rowCentre.y + range / 2 264 | 265 | layers.forEach(layer => { 266 | let centre = getLayerCentre(layer) 267 | if (centre.y > lower && centre.y < upper) { 268 | found.push(layer) 269 | } 270 | }) 271 | } else { 272 | let lower = rowCentre.x - range / 2 273 | let upper = rowCentre.x + range / 2 274 | 275 | layers.forEach(layer => { 276 | let centre = getLayerCentre(layer) 277 | if (centre.x > lower && centre.x < upper) { 278 | found.push(layer) 279 | } 280 | }) 281 | } 282 | 283 | return found 284 | } 285 | 286 | function median(values) { 287 | values.sort((a, b) => a - b) 288 | let half = Math.floor(values.length / 2) 289 | 290 | if (values.length % 2) { 291 | return values[half] 292 | } else { 293 | return (values[half - 1] + values[half]) / 2.0 294 | } 295 | } 296 | 297 | function getBoundingBox(layers) { 298 | let lefts = layers 299 | .map(layer => layer.sketchObject.absoluteRect().x()) 300 | .sort((a, b) => a - b) 301 | let rights = layers 302 | .map(layer => layer.sketchObject.absoluteRect().x() + layer.frame.width) 303 | .sort((a, b) => a - b) 304 | let tops = layers 305 | .map(layer => layer.sketchObject.absoluteRect().y()) 306 | .sort((a, b) => a - b) 307 | let bottoms = layers 308 | .map( 309 | layer => layer.sketchObject.absoluteRect().y() + layer.frame.height 310 | ) 311 | .sort((a, b) => a - b) 312 | return { 313 | x: lefts[0], 314 | y: tops[0], 315 | width: rights[layers.length - 1] - lefts[0], 316 | height: bottoms[layers.length - 1] - tops[0], 317 | } 318 | } 319 | 320 | function getLayerCentre(layer) { 321 | return { 322 | x: layer.sketchObject.absoluteRect().x() + layer.frame.width / 2, 323 | y: layer.sketchObject.absoluteRect().y() + layer.frame.height / 2, 324 | } 325 | } 326 | --------------------------------------------------------------------------------