用脚本一键处理:类似于下图中的同一形状元素在特定区域内,随机缩放大小,随机位置排列
使用方法:
1、画出两形状,小的在上面,大的在下面;

2、同时选中两个形状,按 CTRL + F12,找到你保存的脚本(在本页最下面)打开,调整参数(可参考下图),点击开始精准填充。

3、效果(不满意可以撤销后再来一遍重新调整参数,或者直接在此效果上手动调节)

【将以下代码全部完整复制,保存到记事本,另存为所有文档,后缀名改成 .jsx 即可】
| /* Script: 随机填充
Author: sucaizip.com Version: V5.0 */ (function() { // --- 0. 基础检查 --- if (app.documents.length === 0 || app.selection.length < 2) { alert('错误:请至少选择 2 个对象!\n\n1. 上方对象:填充物 (矢量)\n2. 下方对象:容器 (矢量形状)\n\n提示:对于圆形/曲线容器,建议先执行[对象>路径>添加锚点]以提高边缘精度。'); return; } var scriptName = 'Fillinger_V5_Precision', settingFile = { name: scriptName + '__setting.json', folder: Folder.myDocuments + '/LA_AI_Scripts/' }; // --- 1. 构建界面 --- var win = new Window('dialog', '精准形状填充 V5'); win.orientation = 'column'; win.alignChildren = ['fill', 'fill']; win.spacing = 10; win.margins = 16; // A. 间距面板 var panelGap = win.add('panel', undefined, '1. 间距设置'); panelGap.orientation = 'column'; panelGap.alignChildren = ['left', 'top']; var grpGap = panelGap.add('group'); grpGap.add('statictext', undefined, '间距倍数:'); var inputGap = grpGap.add('edittext', undefined, '1.5'); inputGap.characters = 6; grpGap.add('statictext', undefined, '(1.0=紧贴, >1=空隙)'); // B. 缩放面板 var panelScale = win.add('panel', undefined, '2. 随机大小'); panelScale.orientation = 'column'; panelScale.alignChildren = ['left', 'top']; var chkScale = panelScale.add('checkbox', undefined, '启用随机缩放'); chkScale.value = false; var grpScaleRange = panelScale.add('group'); grpScaleRange.enabled = false; grpScaleRange.add('statictext', undefined, '最小:'); var inputMinScale = grpScaleRange.add('edittext', undefined, '0.5'); inputMinScale.characters = 5; grpScaleRange.add('statictext', undefined, ' 最大:'); var inputMaxScale = grpScaleRange.add('edittext', undefined, '1.5'); inputMaxScale.characters = 5; chkScale.onClick = function() { grpScaleRange.enabled = chkScale.value; } // C. 旋转面板 var panelRotate = win.add('panel', undefined, '3. 旋转设置'); panelRotate.alignChildren = 'left'; var grpRot = panelRotate.add('group'); var radRotRandom = grpRot.add('radiobutton', undefined, '随机 360°'); var radRotFixed = grpRot.add('radiobutton', undefined, '固定角度:'); var inputRotate = grpRot.add('edittext', undefined, '0'); inputRotate.characters = 5; inputRotate.enabled = false; radRotRandom.value = true; radRotRandom.onClick = function() { inputRotate.enabled = false; } radRotFixed.onClick = function() { inputRotate.enabled = true; } // D. 按钮 var grpOptions = win.add('group'); grpOptions.alignment = 'left'; var chkDelContainer = grpOptions.add('checkbox', undefined, '完成后删除容器'); var grpBtns = win.add('group'); grpBtns.alignment = 'center'; var btnCancel = grpBtns.add('button', undefined, '取消'); var btnOk = grpBtns.add('button', undefined, '开始精准填充'); btnCancel.onClick = function() { win.close(); } btnOk.onClick = function() { if (isNaN(Number(inputGap.text))) { alert("间距需为数字"); return; } win.close(); runScript(); } win.center(); win.show(); // --- 2. 核心算法 --- // 算法:射线法判断点是否在多边形内 // 参数:pt=[x,y], vs=[[x,y], [x,y]...] (多边形顶点数组) function isPointInPolygon(pt, vs) { var x = pt[0], y = pt[1]; var inside = false; for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) { var xi = vs[i][0], yi = vs[i][1]; var xj = vs[j][0], yj = vs[j][1]; var intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); if (intersect) inside = !inside; } return inside; } // 提取路径的所有锚点坐标 function getPathPoints(pathItem) { var pts = []; var pPoints = pathItem.pathPoints; for (var i = 0; i < pPoints.length; i++) { pts.push([pPoints[i].anchor[0], pPoints[i].anchor[1]]); } return pts; } // --- 3. 主逻辑 --- function runScript() { var gapMultiplier = Number(inputGap.text); var useScale = chkScale.value; var minScale = Number(inputMinScale.text); var maxScale = Number(inputMaxScale.text); // 排序与提取 var items = []; for (var i = 0; i < app.selection.length; i++) items.push(app.selection[i]); items.sort(function(a, b) { return b.top - a.top; }); var container = items.pop(); var seeds = items; if (container.typename === "RasterItem") { alert("容器必须是路径!"); return; } // --- 准备容器数据 --- // 我们需要解析容器的几何形状。 // 为了处理复合路径(如甜甜圈),我们需要提取所有子路径。 var polyList = []; // 存放所有多边形轮廓 [[pt,pt...], [pt,pt...]] if (container.typename === "CompoundPathItem") { for (var i = 0; i < container.pathItems.length; i++) { polyList.push(getPathPoints(container.pathItems[i])); } } else if (container.typename === "PathItem") { polyList.push(getPathPoints(container)); } else { alert("容器类型不支持 (请取消编组或扩展外观)"); return; } // 准备边界盒 (作为第一轮粗筛) var lb = container.geometricBounds; var cLeft = lb[0], cTop = lb[1], cRight = lb[2], cBottom = lb[3]; var cWidth = cRight - cLeft; var cHeight = cTop - cBottom; // 准备结果组 var resultGroup = app.activeDocument.groupItems.add(); resultGroup.move(container, ElementPlacement.PLACEBEFORE); var seedRef = seeds[0]; var baseSize = Math.max(seedRef.width, seedRef.height); var placedItems = []; // [x, y, radius] // 增加尝试次数,因为现在很多点会被“形状外”的条件剔除 var maxAttempts = 5000; // 进度条 var progWin = new Window('palette', '精准计算中...'); var pBar = progWin.add('progressbar', undefined, 0, maxAttempts); pBar.preferredSize.width = 300; progWin.center(); progWin.show(); for (var i = 0; i < maxAttempts; i++) { // 1. 随机参数 var currentScale = useScale ? (minScale + Math.random() * (maxScale - minScale)) : 1.0; var currentRadius = (baseSize * currentScale) / 2; // 2. 在外框矩形内随机取点 var rndX = cLeft + Math.random() * cWidth; var rndY = cTop - Math.random() * cHeight; var pt = [rndX, rndY]; // 3. 【关键步骤】检查点是否在精准形状内部 var isInsideShape = false; // 奇偶规则 (Even-Odd Rule) 处理复合路径 // 简单来说:在实心区域内,包含该点的子路径数量通常是奇数(1个)。 // 在中空区域(洞)内,包含该点的子路径数量通常是偶数(2个:外圈+内圈)。 var insideCount = 0; for (var p = 0; p < polyList.length; p++) { if (isPointInPolygon(pt, polyList[p])) { insideCount++; } } // 如果是普通路径,count=1即为内部。如果是复合路径,奇数层为实体。 // 大多数情况下,insideCount % 2 !== 0 是通用的。 if (insideCount % 2 !== 0) { isInsideShape = true; } // 如果中心点不在形状内,直接跳过 if (!isInsideShape) { if (i % 10 === 0) i--; continue; } // ====== 新增:边界不外溢检测(外接圆采样点都必须在容器内)====== // currentRadius = (baseSize * currentScale) / 2; // 你上面已经算好了 var r = currentRadius; // 采样 8 个方向 + 可选 4 个更密的点(更严谨) var offsets = [ [ r, 0], [-r, 0], [0, r], [0, -r], [ r*0.7071, r*0.7071], [ r*0.7071, -r*0.7071], [-r*0.7071, r*0.7071], [-r*0.7071, -r*0.7071], // 更严谨可再加一圈(建议保留) [ r*0.5, r*0.8660], [ r*0.5, -r*0.8660], [-r*0.5, r*0.8660], [-r*0.5, -r*0.8660] ]; var allInside = true; for (var t = 0; t < offsets.length; t++) { var testPt = [rndX + offsets[t][0], rndY + offsets[t][1]]; // 复用你原来的 even-odd 逻辑:insideCount%2!==0 为实心区 var insideCount2 = 0; for (var pp = 0; pp < polyList.length; pp++) { if (isPointInPolygon(testPt, polyList[pp])) insideCount2++; } if (insideCount2 % 2 === 0) { allInside = false; break; } } if (!allInside) { if (i % 10 === 0) i--; continue; } // =============================================================== // 4. 碰撞检测 (检查是否与已有物体重叠) var fits = true; for (var j = 0; j < placedItems.length; j++) { var other = placedItems[j]; var dist = Math.sqrt(Math.pow(rndX - other[0], 2) + Math.pow(rndY - other[1], 2)); var minSafeDist = (currentRadius + other[2]) * gapMultiplier; if (dist < minSafeDist) { fits = false; break; } } // 5. 放置 if (fits) { placedItems.push([rndX, rndY, currentRadius, currentScale]); } if (i % 50 === 0) pBar.value = i; } progWin.close(); // --- 生成 --- if (placedItems.length === 0) { alert("未生成对象。\n可能原因:容器太小、间距太大,或未检测到内部区域。\n提示:请给容器[添加锚点]后重试。"); return; } for (var k = 0; k < placedItems.length; k++) { var data = placedItems[k]; var newItem = seedRef.duplicate(); if (data[3] !== 1.0) newItem.resize(data[3] * 100, data[3] * 100); newItem.position = [data[0] - newItem.width/2, data[1] + newItem.height/2]; newItem.move(resultGroup, ElementPlacement.PLACEATEND); if (radRotRandom.value) newItem.rotate(Math.random() * 360); else if (inputRotate.text !== "0") newItem.rotate(Number(inputRotate.text)); } if (chkDelContainer.value) container.remove(); } })(); |