文章目录
摘要
PS手工绘制大量蜂窝六边形网格、逐个录入编号耗时久,坐标难以对齐,极易出错。本文采用Photoshop ExtendScript(JSX)开发自动化脚本,实现错位蜂窝正六边形网格批量生成,针对Photoshop API兼容性问题做专项适配,解决大量开发调试遇到的报错。脚本集成进度条提示、参数输入面板,并做了代码性能优化,将全部编号合并至单一图层,提高生成效率的同时方便后期统一位移调整。
一、需求背景与方案对比
(一)业务需求
- 在PS画布批量生成标准错位排布蜂窝正六边形网格;
- 每个六边形中心自动填充对应编号;
- 可自定义六边形尺寸、画布起始坐标、网格行列数量;
- 所有文本编号合并为单个图层,支持整体位置微调;
- 大批量渲染时显示进度窗口,避免脚本假死无法判断运行状态。
(二)多种实现方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 纯手动绘制复制 | 无需脚本,上手简单 | 网格量大时效率极低,坐标错位、编号容易混乱 |
| 动作+PS变量 | 零代码 | 仅适合矩形阵列,无法实现蜂窝错位六角布局 |
| JSX原生脚本(本文方案) | PS原生运行、坐标精准 | 需要编写脚本,不同PS版本API存在兼容性差异 |
二、核心原理基础
(一)蜂窝六角网格数学模型
请阅读我之前写的一篇文章SDL2画六角格地板第一部分数学基础部分。
(二)PS JSX稳定绘图技术路线
- 图像绘制稳定方案:手动构造锚点数组,通过PathItem绘制多边形路径。
- 文字图层性能方案:批量创建文字图层后栅格合并单层,杜绝数百上千个独立文本图层造成软件卡顿。
三、完整模块化脚本实现
(一)前置准备
查阅photoshop-cc-javascript-references官方文档,需要在脚本开始做以下工作:
- 脚本开始第一行
#target photoshop为固定格式 - PS画布单位统一设置为点POINTS

涉及到Units和TypeUnits2个常量:

app.preferences.rulerUnits = Units.POINTS; //画图系统使用的尺寸单位为点
app.preferences.typeUnits = TypeUnits.POINTS; //文字系统使用的尺寸单位为点
-
设置显示对话框的样式,有
ALL、ERROR、NO3种样式,一般简单的选择NO

app.displayDialogs = DialogModes.NO;
(二)脚本模块划分
1.参数配置模块
//========配置参数=========
var CELLCOUNT = 5; //一个方里格纵向放五个六角格
var lineWidth = 8; //边框粗细
var fontSize = hexRadius / 4; //字号
var canvasW = 10200; //画布宽度
var canvasH = 6800; //画布高度
var startOffsetX = 200; //整体起点X
var startOffsetY = 600; //整体起点Y
var perRowHexCount = 10; //默认每行六边形数量10个
var maxCount = 10; //默认10行
//颜色
var strokeRGB = [0,0,0]; //黑色
var textRGB = [0,0,0]; //字体黑色
var fillRGB = null; //填充色
//分辨率DPI
var dpi = 300;
//========全局变量=========
//六边形蜂窝坐标计算常量
var hexHeight = 100; //默认100
var hexRadius = hexHeight / Math.sqrt(3);
var hexWidth = hexRadius * 2;
var xStep = hexWidth + hexRadius; //一行中相邻2块之间的距离
var yStep = hexHeight / 2; //一列中相邻2块之间的距离
var oddRowShift = hexRadius * 1.5; //奇数行与偶数行的水平错位距离
2.几何计算模块
- 根据圆心生成正六边形锚点数组
//生成正6边形6个顶点完整锚点数组(中心cx,cy,外接圆半径r) function getHexAnchorData(cx, cy, r) { //alert("生成六边形锚点"); var anchors =[]; var startRad = 0; //起始偏移角度 for (var a = 0; a < 6; a++) { var rad = startRad + Math.PI / 3 * a; //旋转60度找下一个锚点坐标 var px = cx + r * Math.cos(rad); var py = cy + r * Math.sin(rad); //UnitValue到number的转换 if (px instanceof UnitValue) px = px.value; if (py instanceof UnitValue) py = py.value; var pt = [px, py]; var pif = new PathPointInfo(); //构造PathPointInfo,用于在PathItems中加入PathPoint,这是画锚点前的固定格式,参见P142-145 pif.anchor = pt; pif.leftDirection = pt; //直线点:锚点、左右手柄坐标完全一致 pif.rightDirection = pt; pif.kind = PointKind.CORNERPOINT; anchors.push(pif); } return anchors; } - 蜂窝网格所有六边形圆心坐标批量计算
首先处理第一行的圆心,参数配置模块的xStep是关键。接着下一行的圆心yStep、oddRowShift是关键,横向下偏移oddRowShift,纵向上偏移yStep。在六角格绘制模块和编号绘制模块有具体体现。
3.绘图模块
- 单个六边形路径绘制
//给定一个图层,给出圆心和半径 function createHexShape(hexLayer, cx, cy, r) { // 新建图层 doc.activeLayer = hexLayer; doc.pathItems.removeAll(); //清空旧路径 //获取六边形新锚点 var anchors = getHexAnchorData(cx, cy, r); //创建闭合路径 var spi = new SubPathInfo(); //P173-175 spi.entireSubPath = anchors; spi.closed = true; spi.operation = ShapeOperation.SHAPEXOR; var tempPath = doc.pathItems.add("temp_",[spi]); //描边 app.foregroundColor.rgb.red = strokeRGB[0]; app.foregroundColor.rgb.green = strokeRGB[1]; app.foregroundColor.rgb.blue = strokeRGB[2]; tempPath.strokePath(ToolType.PENCIL, lineWidth); //删除临时路径 tempPath.remove(); } - 单行六边形绘制
不断重新绘制单个六边形//重复绘制六角格 for (var idx = 0; idx < maxCount; ) { //遍历所有六角格序号 var rowIndex = Math.floor(idx / perRowHexCount); //行序号 var colIndex = idx % perRowHexCount; //列序号 //奇数行横向错位偏移 var rowShift = (rowIndex % 2 === 1) ? oddRowShift : 0; var centerX = startOffsetX + colIndex * xStep + rowShift; var centerY = startOffsetY + rowIndex * yStep; if (rowIndex === 0) { //绘制第一行六边形 createHexShape(hexLayer, centerX, centerY, hexRadius); idx++; } ... ... ... } - 多行六边形绘制
直接复制第1行六边形那个图层,不用再重新计算锚点和描边了,这样可以提高绘制性能。//重复绘制六角格 for (var idx = 0; idx < maxCount; ) { var rowIndex = Math.floor(idx / perRowHexCount); var colIndex = idx % perRowHexCount; //奇数行横向错位偏移 var rowShift = (rowIndex % 2 === 1) ? oddRowShift : 0; var centerX = startOffsetX + colIndex * xStep + rowShift; var centerY = startOffsetY + rowIndex * yStep; if (rowIndex === 0) { //绘制第一行六边形 createHexShape(hexLayer, centerX, centerY, hexRadius); idx++; } else { var copyLayer = hexLayer.duplicate(); copyLayer.translate(new UnitValue(rowShift, "pt"), new UnitValue(rowIndex * yStep, "pt")); idx += perRowHexCount; } } - 单个编号绘制函数
var textLayerList = []; //第一行编号图层组 var textLayer0 = mainGroup.artLayers.add(); //第1个编号图层 textLayer0.name = "编号"; textLayer0.kind = LayerKind.TEXT; textLayer0.textItem.size = fontSize; textLayer0.textItem.kind = TextType.POINTTEXT; textLayer0.textItem.justification = Justification.CENTER; textLayer0.textItem.color.rgb.red = textRGB[0]; textLayer0.textItem.color.rgb.green = textRGB[1]; textLayer0.textItem.color.rgb.blue = textRGB[2]; - 单行编号绘制函数
与多行六边行绘制原理类似,只不过这次是水平复制的。//第一行编码绘制函数 function createFirstRowHexCode(textLayer, centerX, centerY, label) { var copyLayer = textLayer.duplicate(); copyLayer.name = label; var textItem = copyLayer.textItem; textItem.contents = label; textItem.position = Array(centerX, centerY); return copyLayer; } //重复绘制编号 for (var idx = 0; idx < maxCount;) { var rowIndex = Math.floor(idx / perRowHexCount); var colIndex = idx % perRowHexCount; //奇数行横向错位偏移 var rowShift = (rowIndex % 2 === 1) ? oddRowShift : 0; var centerX = startOffsetX + colIndex * xStep + rowShift; var centerY = startOffsetY + rowIndex * yStep; if (rowIndex === 0) { //绘制第一行编号 textLayerList.push(createFirstRowHexCode(textLayer0, centerX, centerY - yStep + fontSize, (rowIndex+1)+","+colIndex )); //绘制形如1,0这样的编号 idx++; } ... ... ... } - 多行编号绘制函数
将第一行的编号内容进行逐个改变,然后整体复制合并成1个图层,再平移至对应行
其中//其余单行编码绘制函数 function createSingleRowHexCode(textLayer, label) { var copyLayer = textLayer.duplicate(); copyLayer.name = label; copyLayer.textItem.contents = label; return copyLayer; } //重复绘制编号 for (var idx = 0; idx < maxCount;) { var rowIndex = Math.floor(idx / perRowHexCount); var colIndex = idx % perRowHexCount; //奇数行横向错位偏移 var rowShift = (rowIndex % 2 === 1) ? oddRowShift : 0; var centerX = startOffsetX + colIndex * xStep + rowShift; var centerY = startOffsetY + rowIndex * yStep; if (rowIndex === 0) { //绘制第一行编号 ... ... ... } else { //绘制一行编号 if (colIndex === 0) { //另起一行 var plainLayer = mainGroup.artLayers.add(); //作为merge的最底层 plainLayer.name = rowIndex + "_"; plainLayer.moveToBeginning(mainGroup); } for (var i = 0; i < perRowHexCount; i++) { var copyLayer = createSingleRowHexCode(textLayerList[i], (rowIndex+1)+","+i); copyLayer.moveToBeginning(mainGroup); copyLayer.merge(); //向下合并 } plainLayer.translate(new UnitValue(rowShift, "pt"), new UnitValue(rowIndex * yStep, "pt")); //整体位移 idx += perRowHexCount; } }mainGroup是图层组,通过mainGroup = doc.layerSets.add(); mainGroup.name = "编号图层组";生成。
4. 通用工具模块
(1) 获取格子大小
一开始定的配置参数都是死的,怎样通过鼠标选取一定范围,然后对应生成相应大小的六角格呢?我在初始化函数中做了实现:
//初始化函数
function initParam() {
//获取选中矩形框大小,参考P90、P166
var bl = doc.selection.bounds[0]; //left
var bt = doc.selection.bounds[1]; //top
var br = doc.selection.bounds[2]; //right
var bb = doc.selection.bounds[3]; //bottom
//初始化参数
hexHeight = (bb - bt) / CELLCOUNT;
hexRadius = hexHeight / Math.sqrt(3);
hexWidth = hexRadius * 2;
xStep = hexWidth + hexRadius;
yStep = hexHeight / 2;
oddRowShift = hexRadius * 1.5;
fontSize = hexRadius / 4;
startOffsetX = bl + hexWidth / 2; //整体起点X
startOffsetY = bt + hexHeight / 2; //整体起点Y
perRowHexCount = Math.floor((doc.width - startOffsetX) / xStep) + 1; //每行六边形数量
maxCount = (Math.floor((doc.height - startOffsetY) / yStep) + 1) * perRowHexCount; //最大行数
doc.selection.deselect(); //放弃选取
}
(2) 自定义弹窗输入框

var num = prompt("请输入1方里格纵向上被六角格分几块?",5); //默认5块

var go_on = confirm("即将绘制六角格,期间PS不会进行响应,请勿操作,等待运行完成提示,确认后继续!");
(3) 可视化进度条ProgressBar封装

初始化:
//创建进度条窗口
var win = new Window("palette", "六角格绘制进度", [120,120,420,200]);
win.orientation = "column";
win.alignItems = "fill";
win.spacing = 10;
var tipText = win.add("statictext", [10,0,250,20], "处理六角格中...0%");
var bar = win.add("progressbar",[10,20,280,60], 0, maxCount); //默认最大值maxCount
动态更新,只需在for循环重复绘制图形时加入:
//更新进度
bar.value = idx;
tipText.text = "处理六角格中..." + Math.round((idx / maxCount) * 100) + "%";
win.update();
四、完整操作流程
-
Photoshop新建画布;
-
选择画笔,设置粗细和类型;

-
使用矩形选择工具选择需要开始绘制六角格的起始格子;

-
运行脚本,根据脚本提示完成绘制操作。

-
生成结果:六边形独立图层,所有编号合并单一图层,可整体移动调整位置。

五、常见问题FAQ
- 报错:ShapeTool没有构造函数
原因:PS2020不支持直接实例化形状工具;解决方案:基于锚点数组手动构建六边形路径。 - 报错:subPathItems.add 不是函数
原因:PS路径API不支持动态追加锚点;必须一次性传入完整锚点数组创建路径。 - 大量文字图层导致PS卡顿
解决方案:全部文字渲染完成后栅格化,合并至同一个图层。 - UnitValue单位对象判断
typename无法直接调用,封装专用判断函数区分像素单位与普通数值。
六、总结
本套JSX自动化脚本适合工程点位示意图、蜂窝布局图纸、网格编号图纸等场景。相比于人工绘图,极大降低重复性工作。同时也要注意Photoshop ExtendScript存在明显版本差异,跨PS版本运行需要针对性修改API。后续可以继续拓展功能,从文件中读取编号,依次填入对应的六角格中,在附录完整代码中被我注释掉了。
附件
参考
- photoshop-cc-javacript-reference.pdf
- JavaScript-Tools-Guide-CC.pdf
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/hele_two/article/details/164340704




