Skip to content

Instantly share code, notes, and snippets.

@sttk3
Created January 27, 2022 00:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sttk3/a087df0d76198e9fe3d5c1c8e76691be to your computer and use it in GitHub Desktop.
Save sttk3/a087df0d76198e9fe3d5c1c8e76691be to your computer and use it in GitHub Desktop.
Illustrator: expand dashed strokes
/**
* @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