Skip to content

Commit e01e0bf

Browse files
committed
Added: Split to grid
1 parent 0c79eb7 commit e01e0bf

2 files changed

Lines changed: 252 additions & 0 deletions

File tree

registry.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,15 @@
199199
"version": "1.0.0",
200200
"category": "Color",
201201
"download_url": "https://raw.githubusercontent.com/JiriKrblich/Affinity-Community-Scripts/main/scripts/oklch_color.js"
202+
},
203+
{
204+
"id": "split_to_grid",
205+
"name": "Split to grid",
206+
"description": "Split vector object into n*n grid",
207+
"author": "JiriKrblich",
208+
"version": "1.0.0",
209+
"category": "Object",
210+
"download_url": "https://raw.githubusercontent.com/JiriKrblich/Affinity-Community-Scripts/main/scripts/split_to_grid.js"
202211
}
203212
]
204213
}

scripts/split_to_grid.js

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
'use strict';
2+
const { Document } = require('/document');
3+
const { Dialog, DialogResult } = require('/dialog');
4+
const { UnitType } = require('/units');
5+
const { DocumentCommand } = require('/commands');
6+
const { CurveBuilder } = require('/geometry');
7+
const { Selection } = require('/selections');
8+
9+
// ── State ─────────────────────────────────────────────────────────────────────
10+
let currentPieces = [];
11+
let config = { cols: 3, rows: 3, gap: 10 };
12+
13+
// ── Cleanup: remove slices from previous preview ──────────────────────────────
14+
function deletePieces() {
15+
const doc = Document.current;
16+
for (const p of currentPieces) {
17+
try { doc.executeCommand(DocumentCommand.createDeleteSelection(Selection.create(doc, p), false)); }
18+
catch (e) { /* already gone */ }
19+
}
20+
currentPieces = [];
21+
}
22+
23+
// ── Core: knife-cut grid with equal-size pieces ───────────────────────────────
24+
//
25+
// Equal-size fix: subtract all gap space first, then divide remaining area
26+
// evenly. Each divider center sits at:
27+
// midX = origBox.x + c * pieceW + (c − 0.5) * gap
28+
// so every column is exactly pieceW wide and every gap is exactly `gap` wide.
29+
//
30+
// Stability fix: null-guard after ConvertToCurves (node ref is replaced by
31+
// Affinity), try/catch around every knife call.
32+
function generateGrid(origNode, origBox) {
33+
const doc = Document.current;
34+
const { cols, rows, gap } = config;
35+
36+
const pieceW = (origBox.width - (cols - 1) * gap) / cols;
37+
const pieceH = (origBox.height - (rows - 1) * gap) / rows;
38+
const half = gap / 2;
39+
40+
// Duplicate hidden original, show & convert the duplicate
41+
const dupCmd = DocumentCommand.createTransform(
42+
origNode.selfSelection, null, { duplicateNodes: true });
43+
doc.executeCommand(dupCmd);
44+
45+
if (!dupCmd.newNodes || dupCmd.newNodes.length === 0) {
46+
throw new Error(
47+
'Duplicate failed. Object must be a shape or image — not a group or text frame.');
48+
}
49+
50+
const dup = dupCmd.newNodes[0];
51+
doc.executeCommand(DocumentCommand.createSetVisibility(dup.selfSelection, true));
52+
doc.executeCommand(DocumentCommand.createConvertToCurves(Selection.create(doc, dup)));
53+
54+
// createConvertToCurves replaces the node; read result from selection
55+
const selAfter = doc.selection.nodes;
56+
const converted = (selAfter && selAfter.length > 0) ? selAfter.first : null;
57+
if (!converted) {
58+
throw new Error(
59+
'Convert to Curves produced no output. Try manually converting the object first.');
60+
}
61+
62+
let pieces = [converted];
63+
64+
function cutAll(line) {
65+
const next = [];
66+
for (const p of pieces) {
67+
try {
68+
const cmd = DocumentCommand.createKnifeCut(line, Selection.create(doc, p));
69+
doc.executeCommand(cmd);
70+
next.push(...(cmd.newNodes && cmd.newNodes.length === 2 ? cmd.newNodes : [p]));
71+
} catch (e) { next.push(p); }
72+
}
73+
pieces = next;
74+
}
75+
76+
function deleteStrips(lo, hi, axis) {
77+
const keep = [], del = [];
78+
for (const p of pieces) {
79+
try {
80+
const bb = p.baseBox;
81+
if (!bb) { keep.push(p); continue; }
82+
const mid = axis === 'x' ? bb.x + bb.width / 2 : bb.y + bb.height / 2;
83+
(mid > lo && mid < hi ? del : keep).push(p);
84+
} catch (e) { keep.push(p); }
85+
}
86+
for (const p of del) {
87+
try { doc.executeCommand(DocumentCommand.createDeleteSelection(Selection.create(doc, p), false)); }
88+
catch (e) { /* already gone */ }
89+
}
90+
pieces = keep;
91+
}
92+
93+
// Horizontal dividers
94+
for (let r = 1; r < rows; r++) {
95+
const midY = origBox.y + r * pieceH + (r - 0.5) * gap;
96+
if (gap > 0) {
97+
cutAll(new CurveBuilder().beginXY(origBox.x - 200, midY - half).lineToXY(origBox.x + origBox.width + 200, midY - half).createCurve());
98+
cutAll(new CurveBuilder().beginXY(origBox.x - 200, midY + half).lineToXY(origBox.x + origBox.width + 200, midY + half).createCurve());
99+
deleteStrips(midY - half, midY + half, 'y');
100+
} else {
101+
cutAll(new CurveBuilder().beginXY(origBox.x - 200, midY).lineToXY(origBox.x + origBox.width + 200, midY).createCurve());
102+
}
103+
}
104+
105+
// Vertical dividers
106+
for (let c = 1; c < cols; c++) {
107+
const midX = origBox.x + c * pieceW + (c - 0.5) * gap;
108+
if (gap > 0) {
109+
cutAll(new CurveBuilder().beginXY(midX - half, origBox.y - 200).lineToXY(midX - half, origBox.y + origBox.height + 200).createCurve());
110+
cutAll(new CurveBuilder().beginXY(midX + half, origBox.y - 200).lineToXY(midX + half, origBox.y + origBox.height + 200).createCurve());
111+
deleteStrips(midX - half, midX + half, 'x');
112+
} else {
113+
cutAll(new CurveBuilder().beginXY(midX, origBox.y - 200).lineToXY(midX, origBox.y + origBox.height + 200).createCurve());
114+
}
115+
}
116+
117+
currentPieces = pieces;
118+
}
119+
120+
// ── Main ──────────────────────────────────────────────────────────────────────
121+
function run() {
122+
const doc = Document.current;
123+
const sel = doc.selection;
124+
125+
if (!sel || sel.length === 0) {
126+
const dlg = Dialog.create('Split to Grid');
127+
dlg.addColumn().addGroup('').addStaticText('',
128+
'No object selected. Please select one object first.').isFullWidth = true;
129+
dlg.show();
130+
return;
131+
}
132+
133+
const origNode = sel.nodes.first;
134+
135+
let origBox;
136+
try {
137+
origBox = origNode.baseBox;
138+
if (!origBox || origBox.width <= 0 || origBox.height <= 0) throw new Error();
139+
} catch (e) {
140+
const dlg = Dialog.create('Split to Grid');
141+
dlg.addColumn().addGroup('').addStaticText('',
142+
'Selected object has no valid dimensions.\n' +
143+
'Please select a shape or image (not a group or text frame).').isFullWidth = true;
144+
dlg.show();
145+
return;
146+
}
147+
148+
// Hide original; only the live-preview slices will be visible
149+
doc.executeCommand(DocumentCommand.createSetVisibility(origNode.selfSelection, false));
150+
151+
// ── Build dialog once (RadialRepeat pattern) ──────────────────────────────
152+
const dialog = Dialog.create('Split to Grid');
153+
const col = dialog.addColumn();
154+
155+
col.addGroup('Selected Object').addStaticText('',
156+
`"${origNode.description}" — ${Math.round(origBox.width)} × ${Math.round(origBox.height)} px`
157+
).isFullWidth = true;
158+
159+
const sg = col.addGroup('Grid Settings');
160+
161+
const colsCtrl = sg.addUnitValueEditor('Columns', UnitType.None, UnitType.None, config.cols, 1, 24);
162+
colsCtrl.showPopupSlider = true;
163+
colsCtrl.precision = 0;
164+
165+
const rowsCtrl = sg.addUnitValueEditor('Rows', UnitType.None, UnitType.None, config.rows, 1, 24);
166+
rowsCtrl.showPopupSlider = true;
167+
rowsCtrl.precision = 0;
168+
169+
const gapCtrl = sg.addUnitValueEditor('Gap', UnitType.Pixel, UnitType.Pixel, config.gap, 0, 200);
170+
gapCtrl.showPopupSlider = true;
171+
172+
// Separator + Preview / Apply button set (same pattern as RadialRepeat)
173+
const sepGrp = col.addGroup('');
174+
sepGrp.enableSeparator = true;
175+
const btns = sepGrp.addButtonSet('', ['Preview', 'Apply'], 0);
176+
177+
// ── Initial preview (before first dialog.show) ────────────────────────────
178+
try {
179+
generateGrid(origNode, origBox);
180+
} catch (e) {
181+
try { doc.executeCommand(DocumentCommand.createSetVisibility(origNode.selfSelection, true)); } catch (_) {}
182+
const errDlg = Dialog.create('Split to Grid – Error');
183+
errDlg.addColumn().addGroup('').addStaticText('',
184+
`Could not split the object:\n${e.message || e}\n\n` +
185+
'Tip: convert it to curves first via Layer › Convert to Curves.'
186+
).isFullWidth = true;
187+
errDlg.show();
188+
return;
189+
}
190+
191+
// ── Dialog loop ───────────────────────────────────────────────────────────
192+
// OK + Preview → delete old slices, regenerate, loop
193+
// OK + Apply → delete old slices, regenerate, delete hidden orig, done
194+
// Cancel → delete old slices, restore orig, done
195+
let running = true;
196+
while (running) {
197+
btns.selectedIndex = 0; // reset button highlight to "Preview"
198+
const result = dialog.show();
199+
200+
// Read current control values
201+
config.cols = Math.max(1, Math.round(colsCtrl.value));
202+
config.rows = Math.max(1, Math.round(rowsCtrl.value));
203+
config.gap = Math.max(0, gapCtrl.value);
204+
const mode = btns.selectedIndex; // 0 = Preview, 1 = Apply
205+
206+
if (result.value === DialogResult.Ok.value) {
207+
deletePieces();
208+
try {
209+
generateGrid(origNode, origBox);
210+
} catch (e) {
211+
try { doc.executeCommand(DocumentCommand.createSetVisibility(origNode.selfSelection, true)); } catch (_) {}
212+
const errDlg = Dialog.create('Split to Grid – Error');
213+
errDlg.addColumn().addGroup('').addStaticText('',
214+
`Could not split the object:\n${e.message || e}`
215+
).isFullWidth = true;
216+
errDlg.show();
217+
running = false;
218+
return;
219+
}
220+
221+
if (mode === 1) {
222+
// Apply: finalize — delete the hidden original, keep the slices
223+
try {
224+
doc.executeCommand(
225+
DocumentCommand.createDeleteSelection(origNode.selfSelection, false));
226+
} catch (e) { /* already gone */ }
227+
running = false;
228+
}
229+
// mode === 0 (Preview): loop back, dialog re-opens automatically
230+
231+
} else {
232+
// Cancel: discard slices, restore the original object
233+
deletePieces();
234+
try {
235+
doc.executeCommand(
236+
DocumentCommand.createSetVisibility(origNode.selfSelection, true));
237+
} catch (e) { /* already visible */ }
238+
running = false;
239+
}
240+
}
241+
}
242+
243+
run();

0 commit comments

Comments
 (0)