Last active
October 15, 2025 13:27
-
-
Save kulmajaba/6ad1f94901bae0666ab14451aff6e286 to your computer and use it in GitHub Desktop.
Illustrator ExtendScript file to merge multiple vector files (such as DXF) into one project
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** DXF merger, author Mika Kuitunen | |
| * Takes multiple DXF files (2D) and merges them into a single Illustrator document using a packing algorithm for placement | |
| * Create file Adobe Illustrator 2025/Presets/en_US/Scripts/DXF-merge.jsx and paste the contents there to use | |
| * | |
| * Instructions: This script expects the file names to end with a part number, group and possible suffix, preceded by ' ' and separated by '-'. | |
| * E.g. 'Project 1 - Flat pattern of 1-a-1.dxf' | |
| * | |
| * 1 - a - 1 | |
| * <partno.> <group> <suffix> | |
| * | |
| * The parts are divided into artboards by group and labeled with the full info. | |
| * | |
| * Parts are also flipped horizontally and rotated 180 degrees. | |
| */ | |
| /** | |
| * TODO | |
| * - If an element is wider than the allowed space (A4 width with 10mm margin on both sides) the packing algorithm will fail | |
| * - The packing algorithm will not rotate elements to fill space | |
| */ | |
| #target illustrator | |
| // DEBUGGING STUFF | |
| // https://github.com/joshbduncan/illustrator-scripts | |
| function Logger(fp, mode, sizeLimit, console) { | |
| var fp = Folder.userData + "/DXF-merge.log"; | |
| this.mode = "a"; | |
| this.console = false; | |
| this.file = new File(fp); | |
| this.badPath = false; | |
| } | |
| Logger.prototype = { | |
| /** | |
| * Write data to the log file. | |
| * @param {String} text One or more strings to write, which are concatenated to form a single string. | |
| * @returns {Boolean} Returns true if log file is successfully written, false if unsuccessful. | |
| */ | |
| log: function (text) { | |
| // no need to keep alerting when the log path is bad | |
| if (this.badPath) return false; | |
| var f = this.file; | |
| var m = this.mode; | |
| var ts = new Date().toLocaleString(); | |
| // ensure parent folder exists | |
| if (!f.parent.exists) { | |
| if (!f.parent.parent.exists) { | |
| alert("Bad log file path!\n'" + this.file + "'"); | |
| this.badPath = true; | |
| return false; | |
| } | |
| f.parent.create(); | |
| } | |
| // grab all arguments | |
| var args = ["[" + ts + "]"]; | |
| for (var i = 0; i < arguments.length; ++i) args.push(arguments[i]); | |
| // write the data | |
| try { | |
| f.encoding = "UTF-8"; | |
| f.open(m); | |
| f.writeln(args.join(" ")); | |
| } catch (e) { | |
| $.writeln("Error writing file:\n" + f); | |
| return false; | |
| } finally { | |
| f.close(); | |
| } | |
| // write `text` to the console if requested | |
| if (this.console) $.writeln(args.slice(1, args.length).join(" ")); | |
| return true; | |
| }, | |
| }; | |
| var console = new Logger(); | |
| // ---------------------------------------------------------------------------- | |
| // MAIN SCRIPT | |
| function find(arr, callbackFn) { | |
| for (var i = 0; i < arr.length; i++) { | |
| if (callbackFn(arr[i])) { | |
| return arr [i]; | |
| } | |
| } | |
| return undefined; | |
| } | |
| function pushOrAddArrayInObject(obj, key, val) { | |
| if (obj[key] !== undefined) { | |
| obj[key].push(val); | |
| } else { | |
| obj[key] = [val]; | |
| } | |
| } | |
| function mmToPt(mm) { | |
| return new UnitValue(mm, "mm").as("pt"); | |
| } | |
| var docPreset = new DocumentPreset; | |
| docPreset.width = mmToPt(210); | |
| docPreset.height = mmToPt(297); | |
| docPreset.units = RulerUnits.Millimeters; | |
| docPreset.colorMode = DocumentColorSpace.CMYK; | |
| function topToBottomY(y) { | |
| return docPreset.height - y; | |
| } | |
| function addArtboard(startX) { | |
| // Left X, Top Y, Right X, Bottom Y | |
| var rect = [startX, docPreset.height, startX + docPreset.width, 0]; | |
| return app.activeDocument.artboards.add(rect); | |
| } | |
| var paddingPt = mmToPt(5); | |
| var pageMargin = mmToPt(10); | |
| // Each element has a padding of 5mm on the right edge, compensate in max width to allow full pages with 10mm margin on both sides | |
| var maxLayoutWidth = mmToPt(210 - (10 + 5)); | |
| // https://github.com/mapbox/potpack | |
| /** | |
| * @typedef {Object} PotpackBox | |
| * @property {number} w Box width. | |
| * @property {number} h Box height. | |
| * @property {number} [x] X coordinate in the resulting container. | |
| * @property {number} [y] Y coordinate in the resulting container. | |
| */ | |
| /** | |
| * @typedef {Object} PotpackStats | |
| * @property {number} w Width of the resulting container. | |
| * @property {number} h Height of the resulting container. | |
| * @property {number} fill The space utilization value (0 to 1). Higher is better. | |
| */ | |
| /** | |
| * Packs 2D rectangles into a near-square container. | |
| * | |
| * Mutates the {@link boxes} array: it's sorted (by height/width), | |
| * and box objects are augmented with `x`, `y` coordinates. | |
| * | |
| * @param {PotpackBox[]} boxes | |
| * @return {PotpackStats} | |
| */ | |
| function potpack(boxes) { | |
| // calculate total box area and maximum box width | |
| var area = 0; | |
| var maxWidth = 0; | |
| for (var k = 0; k < boxes.length; k++) { | |
| var box1 = boxes[k]; | |
| area += box1.w * box1.h; | |
| maxWidth = Math.max(maxWidth, box1.w); | |
| } | |
| // sort the boxes for insertion by height, descending | |
| boxes.sort(function(a, b) { return b.h - a.h }); | |
| // Aim for A4 width | |
| var startWidth = maxLayoutWidth; | |
| // start with a single empty space, unbounded at the bottom | |
| var spaces = [{x: 0, y: 0, w: startWidth, h: Infinity}]; | |
| var width = 0; | |
| var height = 0; | |
| var box = undefined; | |
| var space = undefined; | |
| var last = undefined | |
| for (var j = 0; j < boxes.length; j++) { | |
| box = boxes [j]; | |
| // look through spaces backwards so that we check smaller spaces first | |
| for (var i = spaces.length - 1; i >= 0; i--) { | |
| space = spaces[i]; | |
| // look for empty spaces that can accommodate the current box | |
| if (box.w > space.w || box.h > space.h) continue; | |
| // found the space; add the box to its top-left corner | |
| // |-------|-------| | |
| // | box | | | |
| // |_______| | | |
| // | space | | |
| // |_______________| | |
| box.x = space.x; | |
| box.y = space.y; | |
| height = Math.max(height, box.y + box.h); | |
| width = Math.max(width, box.x + box.w); | |
| if (box.w === space.w && box.h === space.h) { | |
| // space matches the box exactly; remove it | |
| last = spaces.pop(); | |
| if (last && i < spaces.length) spaces[i] = last; | |
| } else if (box.h === space.h) { | |
| // space matches the box height; update it accordingly | |
| // |-------|---------------| | |
| // | box | updated space | | |
| // |_______|_______________| | |
| space.x += box.w; | |
| space.w -= box.w; | |
| } else if (box.w === space.w) { | |
| // space matches the box width; update it accordingly | |
| // |---------------| | |
| // | box | | |
| // |_______________| | |
| // | updated space | | |
| // |_______________| | |
| space.y += box.h; | |
| space.h -= box.h; | |
| } else { | |
| // otherwise the box splits the space into two spaces | |
| // |-------|-----------| | |
| // | box | new space | | |
| // |_______|___________| | |
| // | updated space | | |
| // |___________________| | |
| spaces.push({ | |
| x: space.x + box.w, | |
| y: space.y, | |
| w: space.w - box.w, | |
| h: box.h | |
| }); | |
| space.y += box.h; | |
| space.h -= box.h; | |
| } | |
| break; | |
| } | |
| } | |
| return { | |
| w: width, // container width | |
| h: height, // container height | |
| fill: (area / (width * height)) || 0 // space utilization | |
| }; | |
| } | |
| function main() { | |
| console.log(""); | |
| var fList = File.openDialog("Select DXF files to merge", "DXF:*.dxf,All files:*.*", true); | |
| if (!fList) { | |
| console.log("User aborted"); | |
| return; | |
| } | |
| var currentInteractionLevel = app.userInteractionLevel; | |
| app.userInteractionLevel = UserInteractionLevel.DONTDISPLAYALERTS; | |
| var dest = app.documents.addDocument(DocumentColorSpace.CMYK, docPreset, false); | |
| for (var i = 0; i < fList.length; i++) { | |
| app.activeDocument.groupItems.createFromFile(fList[i]); | |
| } | |
| var groupItems = app.activeDocument.layers[0].groupItems; | |
| var boxes = {}; | |
| var item = undefined | |
| var itemFileName = ''; | |
| var itemName = ''; | |
| var itemGroup = ''; | |
| // Layers are found in the document in reverse order compared to the file list, reverse file list to match | |
| fList.reverse(); | |
| for (var j = 0; j < groupItems.length; j++) { | |
| item = groupItems[j]; | |
| itemFileName = decodeURI(fList[j].name); | |
| itemName = itemFileName.substring(0, itemFileName.length - 4).split(' ').pop(); | |
| itemGroup = itemName.split('-')[1]; | |
| item.name = itemName; | |
| pushOrAddArrayInObject(boxes, itemGroup, {w: item.width + paddingPt, h: item.height + paddingPt, name: itemName}) | |
| } | |
| var k = 0; | |
| var box = undefined; | |
| var layerX = 0; | |
| var layerYTop = 0; | |
| var layerY = 0; | |
| var layerName = undefined; | |
| var newBoardStartX = docPreset.width + paddingPt; | |
| var offsetY = 0; | |
| var currentArtBoard = app.activeDocument.artboards[0]; | |
| var mirrorMatrix = app.getScaleMatrix(-100, 100); | |
| var transformMatrix = app.concatenateRotationMatrix(mirrorMatrix, 180); | |
| for (var boxGroupKey in boxes) { | |
| boxGroup = boxes[boxGroupKey]; | |
| potpack(boxGroup); | |
| offsetY = 0; | |
| if (k > 0) { | |
| currentArtBoard = addArtboard(newBoardStartX); | |
| newBoardStartX = currentArtBoard.artboardRect[2] + paddingPt; | |
| offsetY = 0; | |
| } | |
| for (var l = 0; l < boxGroup.length; l++) { | |
| box = boxGroup[l]; | |
| item = find(groupItems, function (item) { return item.name === box.name }); | |
| layerYTop = offsetY + pageMargin + box.y; | |
| if (layerYTop + box.h - paddingPt > docPreset.height - pageMargin) { | |
| currentArtBoard = addArtboard(newBoardStartX); | |
| newBoardStartX = currentArtBoard.artboardRect[2] + paddingPt; | |
| offsetY = -box.y; | |
| layerYTop = offsetY + pageMargin + box.y; | |
| } | |
| layerY = topToBottomY(layerYTop); | |
| layerX = currentArtBoard.artboardRect[0] + pageMargin + box.x; | |
| item.position = [layerX, layerY]; | |
| item.transform(transformMatrix); | |
| layerName = item.textFrames.pointText([layerX + (box.w - paddingPt) / 2, layerY - (box.h - paddingPt) / 2]); | |
| layerName.contents = item.name; | |
| layerName.paragraphs[0].justification = Justification.CENTER; | |
| } | |
| k++; | |
| } | |
| app.userInteractionLevel = currentInteractionLevel; | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment