├── .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 | [](https://api.sketchpacks.com/v1/plugins/com.gilesperry.photo-grid/download) [](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 | 
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 | 
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 | [](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 |
--------------------------------------------------------------------------------