Created
January 27, 2022 00:19
-
-
Save sttk3/a087df0d76198e9fe3d5c1c8e76691be to your computer and use it in GitHub Desktop.
Illustrator: expand dashed strokes
This file contains 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
/** | |
* @file expand dashed strokes | |
* https://community.adobe.com/t5/illustrator-discussions/js-action-split-or-break-a-dashed-line-into-separate-real-lines-by-script/td-p/12614309 | |
* @version 1.1.0 | |
* @author sttk3.com | |
* @copyright © 2022 sttk3.com | |
*/ | |
//@target 'illustrator' | |
//@targetengine 'com.sttk3.expandDashedStroke' | |
$.localize = true ; | |
(function() { | |
if(app.documents.length <= 0) {return ;} | |
var doc = app.documents[0] ; | |
var sel = doc.selection ; | |
if(sel.length <= 0) {return ;} | |
// reject when selected item is not a path, not a dashed stroke, or has a fill | |
var patternPathItem = /^PathItem$/ ; | |
var targetItems = filterItems(sel, function(aItem) { | |
return ( | |
patternPathItem.test(aItem.constructor.name) | |
&& aItem.strokeDashes.length >= 1 | |
&& !aItem.filled | |
) ; | |
}) ; | |
if(targetItems.length <= 0) { | |
var message = { | |
en: 'There was no target. \nPlease select dashed strokes with no fill and try again.', | |
ja: '対象がありませんでした。\n塗りのない破線を選択してから実行してください。', | |
} | |
alert(message) ; | |
return ; | |
} | |
// generate a unique color | |
var tempSpot = doc.spots.add() ; | |
var tempColor = new SpotColor() ; | |
tempColor.spot = tempSpot ; | |
tempColor.tint = 100 ; | |
// expand all target stroke | |
var newSelection = [] ; | |
try { | |
var currentItem, newItem ; | |
for(var i = targetItems.length - 1 ; i >= 0 ; i--) { | |
currentItem = targetItems[i] ; | |
if(currentItem.strokeDashes[0] == 0) { | |
newItem = expandDottedStroke(doc, currentItem) ; | |
} else { | |
newItem = expandDashedStroke(doc, currentItem, tempColor) ; | |
} | |
if(newItem) {newSelection.push(newItem) ;} | |
} | |
} catch(e) { | |
alert(e) ; | |
return ; | |
} finally { | |
tempSpot.remove() ; | |
} | |
if(newSelection.length > 0) { | |
doc.selection = newSelection ; | |
} | |
})() ; | |
/** | |
* something like Array.filter | |
* @param {Array} targetItems Array or collection of targets, anything with length and index | |
* @param {Function} callback test function | |
* @return {Array} | |
*/ | |
function filterItems(targetItems, callback) { | |
var res = [] ; | |
for(var i = 0, len = targetItems.length ; i < len ; i++) { | |
if(i in targetItems) { | |
var val = targetItems[i] ; | |
if(callback.call(targetItems, val, i)) {res.push(val) ;} | |
} | |
} | |
return res ; | |
} | |
/** | |
* expand a dotted stroke | |
* @param {Document} doc target document | |
* @param {PathItem} targetItem target dashed stroke | |
* @return {GroupItem} | |
*/ | |
function expandDottedStroke(doc, targetItem) { | |
var originalPath = targetItem ; | |
// GroupItem to be returned | |
var outgoingGroup = originalPath.layer.groupItems.add() ; | |
outgoingGroup.move(originalPath, ElementPlacement.PLACEBEFORE) ; | |
// GroupItem to remove later | |
var workspaceGroup = outgoingGroup.duplicate() ; | |
originalPath.move(workspaceGroup, ElementPlacement.PLACEATEND) ; | |
try { | |
// template dot | |
var templateDot = originalPath.duplicate(outgoingGroup, ElementPlacement.PLACEATEND) ; | |
templateDot.strokeDashes = [] ; | |
var firstAnchor = templateDot.pathPoints[0].anchor ; | |
templateDot.setEntirePath([firstAnchor, firstAnchor]) ; | |
// object to get position of each dot | |
var positionDot = originalPath.duplicate(workspaceGroup, ElementPlacement.PLACEATEND) ; | |
if(positionDot.strokeCap != StrokeCap.ROUNDENDCAP) { | |
positionDot.strokeCap = StrokeCap.ROUNDENDCAP ; | |
} | |
positionDot.strokeWidth = 0.01 ; // avoid overlapping | |
doc.selection = [positionDot] ; | |
app.executeMenuCommand('OffsetPath v22') ; // Outline Stroke | |
positionDot = workspaceGroup.compoundPathItems[0] ; | |
var dots = positionDot.pathItems ; | |
var bounds, pos ; | |
for(var i = 0, len = dots.length ; i < len ; i++) { | |
// get position of a dot | |
bounds = dots[i].geometricBounds ; | |
pos = [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2] ; | |
// duplicate template dot to that position | |
templateDot.duplicate(outgoingGroup, ElementPlacement.PLACEATBEGINNING) ; | |
templateDot.position = pos ; | |
} | |
} catch(e) { | |
alert(e) ; | |
return ; | |
} finally { | |
workspaceGroup.remove() ; | |
} | |
return outgoingGroup ; | |
} | |
/** | |
* expand a dashed stroke | |
* @param {Document} doc target document | |
* @param {PathItem} targetItem target dashed stroke | |
* @param {SpotColor} tempColor unique color. a marker to identify what is unnecessary | |
* @return {GroupItem} | |
*/ | |
function expandDashedStroke(doc, targetItem, tempColor) { | |
var originalPath = targetItem ; | |
var tempSpot = tempColor.spot ; | |
var pathfinderGroup = originalPath.layer.groupItems.add() ; | |
pathfinderGroup.move(originalPath, ElementPlacement.PLACEBEFORE) ; | |
var maskWidth = originalPath.strokeWidth + 1 ; | |
// object to fill dash | |
var dashPath = originalPath.duplicate(pathfinderGroup, ElementPlacement.PLACEATEND) ; | |
dashPath.strokeWidth = maskWidth ; | |
dashPath.strokeCap = StrokeCap.BUTTENDCAP ; // make the dash length unchangeable | |
// object to fill gap | |
var gapPath = originalPath.duplicate(pathfinderGroup, ElementPlacement.PLACEATEND) ; | |
gapPath.strokeDashes = [] ; | |
gapPath.strokeColor = tempColor ; | |
var backupPath ; | |
var oldUserInteractionLevel = app.userInteractionLevel ; | |
try { | |
app.userInteractionLevel = UserInteractionLevel.DONTDISPLAYALERTS ; | |
backupPath = originalPath.duplicate(originalPath, ElementPlacement.PLACEAFTER) ; | |
// leave fill only at gaps | |
doc.selection = [pathfinderGroup] ; | |
app.executeMenuCommand('OffsetPath v22') ; // Outline Stroke | |
app.executeMenuCommand('Live Pathfinder Subtract') ; | |
app.executeMenuCommand('expandStyle') ; // Expand Appearance | |
app.executeMenuCommand('compoundPath') ; // Compound Path Make | |
// remove gaps of originalPath by live paint | |
originalPath.strokeDashes = [] ; | |
originalPath.selected = true ; | |
app.executeMenuCommand('Make Planet X') ; // Live Paint Make | |
app.executeMenuCommand('Expand Planet X') ; // Live Paint Expand | |
app.redraw() ; | |
// reject unexpected structures | |
var tempGroups = doc.selection[0].groupItems ; | |
if(tempGroups.length <= 0) { | |
doc.selection[0].remove() ; | |
var message = { | |
en: 'Paths with multiple strokes are not supported.', | |
ja: '複数の線を持つパスには対応していません。' | |
} | |
// mark it as a special error | |
var newError = new Error(message) ; | |
newError.number = -128 ; | |
throw newError ; | |
} | |
// remove gapPath | |
var itemColor ; | |
for(var i = tempGroups.length - 1 ; i >= 0 ; i--) { | |
itemColor = tempGroups[i].pathItems[0].fillColor ; | |
if((itemColor.constructor.name == 'SpotColor') && (itemColor.spot == tempSpot)) { | |
tempGroups[i].remove() ; | |
break ; | |
} | |
} | |
backupPath.remove() ; | |
res = doc.selection[0] ; | |
} catch(e) { | |
alert(e) ; | |
if(e.number == -128) {res = backupPath ;} | |
return res ; | |
} finally { | |
app.userInteractionLevel = oldUserInteractionLevel ; | |
} | |
return res ; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment