@genart-dev/plugin-distribution 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -0
- package/dist/index.cjs +903 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +872 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
default: () => index_default,
|
|
24
|
+
densityLayerType: () => densityLayerType,
|
|
25
|
+
distributionMcpTools: () => distributionMcpTools,
|
|
26
|
+
distributionPlugin: () => distributionPlugin,
|
|
27
|
+
previewLayerType: () => previewLayerType,
|
|
28
|
+
voronoiLayerType: () => voronoiLayerType
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/preview-layer.ts
|
|
33
|
+
var PREVIEW_PROPERTIES = [
|
|
34
|
+
{ key: "algorithm", label: "Algorithm", type: "string", default: "poisson-disk", group: "distribution" },
|
|
35
|
+
{ key: "params", label: "Parameters (JSON)", type: "string", default: "{}", group: "distribution" },
|
|
36
|
+
{ key: "dotSize", label: "Dot Size", type: "number", default: 3, min: 0.5, max: 20, step: 0.5, group: "style" },
|
|
37
|
+
{ key: "dotColor", label: "Dot Color", type: "color", default: "#0088ff", group: "style" },
|
|
38
|
+
{ key: "opacity", label: "Opacity", type: "number", default: 0.6, min: 0, max: 1, step: 0.01, group: "style" },
|
|
39
|
+
{ key: "_points", label: "Points (JSON)", type: "string", default: "[]", group: "data" }
|
|
40
|
+
];
|
|
41
|
+
var previewLayerType = {
|
|
42
|
+
typeId: "distribution:preview",
|
|
43
|
+
displayName: "Distribution Preview",
|
|
44
|
+
icon: "scatter_plot",
|
|
45
|
+
category: "guide",
|
|
46
|
+
properties: PREVIEW_PROPERTIES,
|
|
47
|
+
propertyEditorId: "distribution:preview-editor",
|
|
48
|
+
createDefault() {
|
|
49
|
+
return {
|
|
50
|
+
algorithm: "poisson-disk",
|
|
51
|
+
params: "{}",
|
|
52
|
+
dotSize: 3,
|
|
53
|
+
dotColor: "#0088ff",
|
|
54
|
+
opacity: 0.6,
|
|
55
|
+
_points: "[]"
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
render(properties, ctx, bounds, _resources) {
|
|
59
|
+
const points = JSON.parse(String(properties._points || "[]"));
|
|
60
|
+
if (points.length === 0) return;
|
|
61
|
+
const dotSize = Number(properties.dotSize ?? 3);
|
|
62
|
+
const dotColor = String(properties.dotColor ?? "#0088ff");
|
|
63
|
+
const opacity = Number(properties.opacity ?? 0.6);
|
|
64
|
+
ctx.save();
|
|
65
|
+
ctx.globalAlpha = opacity;
|
|
66
|
+
ctx.fillStyle = dotColor;
|
|
67
|
+
const scaleX = bounds.width;
|
|
68
|
+
const scaleY = bounds.height;
|
|
69
|
+
for (const pt of points) {
|
|
70
|
+
const x = bounds.x + pt.x * scaleX;
|
|
71
|
+
const y = bounds.y + pt.y * scaleY;
|
|
72
|
+
ctx.beginPath();
|
|
73
|
+
ctx.arc(x, y, dotSize, 0, Math.PI * 2);
|
|
74
|
+
ctx.fill();
|
|
75
|
+
}
|
|
76
|
+
ctx.restore();
|
|
77
|
+
},
|
|
78
|
+
validate() {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/voronoi-layer.ts
|
|
84
|
+
var VORONOI_PROPERTIES = [
|
|
85
|
+
{ key: "strokeColor", label: "Edge Color", type: "color", default: "#333333", group: "style" },
|
|
86
|
+
{ key: "strokeWidth", label: "Edge Width", type: "number", default: 1, min: 0.5, max: 5, step: 0.5, group: "style" },
|
|
87
|
+
{ key: "fillColor", label: "Fill Color", type: "color", default: "transparent", group: "style" },
|
|
88
|
+
{ key: "opacity", label: "Opacity", type: "number", default: 0.7, min: 0, max: 1, step: 0.01, group: "style" },
|
|
89
|
+
{ key: "_cells", label: "Cells (JSON)", type: "string", default: "[]", group: "data" }
|
|
90
|
+
];
|
|
91
|
+
var voronoiLayerType = {
|
|
92
|
+
typeId: "distribution:voronoi",
|
|
93
|
+
displayName: "Voronoi Overlay",
|
|
94
|
+
icon: "hexagon",
|
|
95
|
+
category: "guide",
|
|
96
|
+
properties: VORONOI_PROPERTIES,
|
|
97
|
+
propertyEditorId: "distribution:voronoi-editor",
|
|
98
|
+
createDefault() {
|
|
99
|
+
return {
|
|
100
|
+
strokeColor: "#333333",
|
|
101
|
+
strokeWidth: 1,
|
|
102
|
+
fillColor: "transparent",
|
|
103
|
+
opacity: 0.7,
|
|
104
|
+
_cells: "[]"
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
render(properties, ctx, bounds, _resources) {
|
|
108
|
+
const cells = JSON.parse(String(properties._cells || "[]"));
|
|
109
|
+
if (cells.length === 0) return;
|
|
110
|
+
const strokeColor = String(properties.strokeColor ?? "#333333");
|
|
111
|
+
const strokeWidth = Number(properties.strokeWidth ?? 1);
|
|
112
|
+
const fillColor = String(properties.fillColor ?? "transparent");
|
|
113
|
+
const opacity = Number(properties.opacity ?? 0.7);
|
|
114
|
+
ctx.save();
|
|
115
|
+
ctx.globalAlpha = opacity;
|
|
116
|
+
ctx.strokeStyle = strokeColor;
|
|
117
|
+
ctx.lineWidth = strokeWidth;
|
|
118
|
+
for (const cell of cells) {
|
|
119
|
+
const verts = cell.vertices;
|
|
120
|
+
if (verts.length < 3) continue;
|
|
121
|
+
ctx.beginPath();
|
|
122
|
+
ctx.moveTo(bounds.x + verts[0].x, bounds.y + verts[0].y);
|
|
123
|
+
for (let i = 1; i < verts.length; i++) {
|
|
124
|
+
ctx.lineTo(bounds.x + verts[i].x, bounds.y + verts[i].y);
|
|
125
|
+
}
|
|
126
|
+
ctx.closePath();
|
|
127
|
+
if (fillColor !== "transparent") {
|
|
128
|
+
ctx.fillStyle = fillColor;
|
|
129
|
+
ctx.fill();
|
|
130
|
+
}
|
|
131
|
+
ctx.stroke();
|
|
132
|
+
}
|
|
133
|
+
ctx.restore();
|
|
134
|
+
},
|
|
135
|
+
validate() {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/density-layer.ts
|
|
141
|
+
var DENSITY_PROPERTIES = [
|
|
142
|
+
{ key: "radius", label: "Kernel Radius", type: "number", default: 30, min: 5, max: 100, step: 5, group: "density" },
|
|
143
|
+
{ key: "colormap", label: "Color Map", type: "string", default: "viridis", group: "style" },
|
|
144
|
+
{ key: "opacity", label: "Opacity", type: "number", default: 0.65, min: 0, max: 1, step: 0.01, group: "style" },
|
|
145
|
+
{ key: "_points", label: "Points (JSON)", type: "string", default: "[]", group: "data" }
|
|
146
|
+
];
|
|
147
|
+
var COLORMAPS = {
|
|
148
|
+
viridis: [[68, 1, 84], [59, 82, 139], [33, 144, 141], [93, 201, 99], [253, 231, 37]],
|
|
149
|
+
plasma: [[13, 8, 135], [156, 23, 158], [237, 121, 83], [240, 249, 33], [252, 230, 25]],
|
|
150
|
+
inferno: [[0, 0, 4], [120, 28, 109], [238, 125, 51], [252, 225, 31], [252, 255, 164]],
|
|
151
|
+
hot: [[0, 0, 0], [160, 0, 0], [255, 80, 0], [255, 200, 0], [255, 255, 255]],
|
|
152
|
+
cool: [[0, 255, 255], [64, 191, 255], [128, 128, 255], [191, 64, 255], [255, 0, 255]]
|
|
153
|
+
};
|
|
154
|
+
function colormapLookup(name, t) {
|
|
155
|
+
const stops = COLORMAPS[name] ?? COLORMAPS["viridis"];
|
|
156
|
+
const scaled = t * (stops.length - 1);
|
|
157
|
+
const lo = Math.floor(scaled), hi = Math.min(lo + 1, stops.length - 1);
|
|
158
|
+
const f = scaled - lo;
|
|
159
|
+
const slo = stops[lo];
|
|
160
|
+
const shi = stops[hi];
|
|
161
|
+
return [
|
|
162
|
+
Math.round(slo[0] + f * (shi[0] - slo[0])),
|
|
163
|
+
Math.round(slo[1] + f * (shi[1] - slo[1])),
|
|
164
|
+
Math.round(slo[2] + f * (shi[2] - slo[2]))
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
var densityLayerType = {
|
|
168
|
+
typeId: "distribution:density",
|
|
169
|
+
displayName: "Density Map",
|
|
170
|
+
icon: "gradient",
|
|
171
|
+
category: "guide",
|
|
172
|
+
properties: DENSITY_PROPERTIES,
|
|
173
|
+
propertyEditorId: "distribution:density-editor",
|
|
174
|
+
createDefault() {
|
|
175
|
+
return { radius: 30, colormap: "viridis", opacity: 0.65, _points: "[]" };
|
|
176
|
+
},
|
|
177
|
+
render(properties, ctx, bounds, _resources) {
|
|
178
|
+
const points = JSON.parse(String(properties._points || "[]"));
|
|
179
|
+
if (points.length === 0) return;
|
|
180
|
+
const radius = Number(properties.radius ?? 30);
|
|
181
|
+
const colormap = String(properties.colormap ?? "viridis");
|
|
182
|
+
const opacity = Number(properties.opacity ?? 0.65);
|
|
183
|
+
const w = Math.floor(bounds.width);
|
|
184
|
+
const h = Math.floor(bounds.height);
|
|
185
|
+
if (w <= 0 || h <= 0) return;
|
|
186
|
+
const scale = 0.25;
|
|
187
|
+
const gw = Math.max(1, Math.floor(w * scale));
|
|
188
|
+
const gh = Math.max(1, Math.floor(h * scale));
|
|
189
|
+
const density = new Float32Array(gw * gh);
|
|
190
|
+
const gr = radius * scale;
|
|
191
|
+
const gr2 = gr * gr;
|
|
192
|
+
for (const pt of points) {
|
|
193
|
+
const px = pt.x * gw;
|
|
194
|
+
const py = pt.y * gh;
|
|
195
|
+
const minX = Math.max(0, Math.floor(px - gr));
|
|
196
|
+
const maxX = Math.min(gw - 1, Math.ceil(px + gr));
|
|
197
|
+
const minY = Math.max(0, Math.floor(py - gr));
|
|
198
|
+
const maxY = Math.min(gh - 1, Math.ceil(py + gr));
|
|
199
|
+
for (let gy = minY; gy <= maxY; gy++) {
|
|
200
|
+
for (let gx = minX; gx <= maxX; gx++) {
|
|
201
|
+
const dx = gx - px, dy = gy - py;
|
|
202
|
+
const d2 = dx * dx + dy * dy;
|
|
203
|
+
if (d2 < gr2) {
|
|
204
|
+
const idx2 = gy * gw + gx;
|
|
205
|
+
density[idx2] = (density[idx2] ?? 0) + (1 - d2 / gr2);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
let maxD = 0;
|
|
211
|
+
for (let i = 0; i < density.length; i++) if ((density[i] ?? 0) > maxD) maxD = density[i] ?? 0;
|
|
212
|
+
if (maxD === 0) return;
|
|
213
|
+
const imgData = ctx.createImageData(gw, gh);
|
|
214
|
+
for (let i = 0; i < gw * gh; i++) {
|
|
215
|
+
const t = (density[i] ?? 0) / maxD;
|
|
216
|
+
const [r, g, b] = colormapLookup(colormap, t);
|
|
217
|
+
imgData.data[i * 4] = r;
|
|
218
|
+
imgData.data[i * 4 + 1] = g;
|
|
219
|
+
imgData.data[i * 4 + 2] = b;
|
|
220
|
+
imgData.data[i * 4 + 3] = t > 0 ? Math.round(t * 200) : 0;
|
|
221
|
+
}
|
|
222
|
+
const offscreen = new OffscreenCanvas(gw, gh);
|
|
223
|
+
const octx = offscreen.getContext("2d");
|
|
224
|
+
octx.putImageData(imgData, 0, 0);
|
|
225
|
+
ctx.save();
|
|
226
|
+
ctx.globalAlpha = opacity;
|
|
227
|
+
ctx.imageSmoothingEnabled = true;
|
|
228
|
+
ctx.drawImage(offscreen, bounds.x, bounds.y, bounds.width, bounds.height);
|
|
229
|
+
ctx.restore();
|
|
230
|
+
},
|
|
231
|
+
validate() {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// src/distribution-tools.ts
|
|
237
|
+
function textResult(text) {
|
|
238
|
+
return { content: [{ type: "text", text }] };
|
|
239
|
+
}
|
|
240
|
+
function errorResult(text) {
|
|
241
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
242
|
+
}
|
|
243
|
+
function makePrng(seed) {
|
|
244
|
+
let s = (seed | 0) >>> 0;
|
|
245
|
+
return function rng() {
|
|
246
|
+
s += 1831565813;
|
|
247
|
+
let t = s;
|
|
248
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
249
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
250
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function poissonDisk(rng, width, height, minDist, maxAttempts = 30) {
|
|
254
|
+
const cellSize = minDist / Math.SQRT2;
|
|
255
|
+
const cols = Math.ceil(width / cellSize);
|
|
256
|
+
const rows = Math.ceil(height / cellSize);
|
|
257
|
+
const grid = new Array(cols * rows).fill(-1);
|
|
258
|
+
const pts = [];
|
|
259
|
+
const active = [];
|
|
260
|
+
function addPt(x, y) {
|
|
261
|
+
const i = pts.length;
|
|
262
|
+
pts.push([x, y]);
|
|
263
|
+
active.push(i);
|
|
264
|
+
grid[Math.floor(y / cellSize) * cols + Math.floor(x / cellSize)] = i;
|
|
265
|
+
}
|
|
266
|
+
addPt(rng() * width, rng() * height);
|
|
267
|
+
while (active.length > 0) {
|
|
268
|
+
const ri = Math.floor(rng() * active.length);
|
|
269
|
+
const pi = active[ri];
|
|
270
|
+
const p = pts[pi];
|
|
271
|
+
let found = false;
|
|
272
|
+
for (let a = 0; a < maxAttempts; a++) {
|
|
273
|
+
const angle = rng() * Math.PI * 2;
|
|
274
|
+
const dist = minDist + rng() * minDist;
|
|
275
|
+
const nx = p[0] + Math.cos(angle) * dist;
|
|
276
|
+
const ny = p[1] + Math.sin(angle) * dist;
|
|
277
|
+
if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
|
|
278
|
+
const gx = Math.floor(nx / cellSize);
|
|
279
|
+
const gy = Math.floor(ny / cellSize);
|
|
280
|
+
let ok = true;
|
|
281
|
+
for (let dx = -2; dx <= 2 && ok; dx++) {
|
|
282
|
+
for (let dy = -2; dy <= 2 && ok; dy++) {
|
|
283
|
+
const ngx = gx + dx, ngy = gy + dy;
|
|
284
|
+
if (ngx < 0 || ngx >= cols || ngy < 0 || ngy >= rows) continue;
|
|
285
|
+
const ni = grid[ngy * cols + ngx];
|
|
286
|
+
if (ni === void 0 || ni === -1) continue;
|
|
287
|
+
const q = pts[ni];
|
|
288
|
+
const ddx = q[0] - nx, ddy = q[1] - ny;
|
|
289
|
+
if (ddx * ddx + ddy * ddy < minDist * minDist) ok = false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (ok) {
|
|
293
|
+
addPt(nx, ny);
|
|
294
|
+
found = true;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!found) active.splice(ri, 1);
|
|
299
|
+
}
|
|
300
|
+
return pts.map(([x, y]) => ({ x, y }));
|
|
301
|
+
}
|
|
302
|
+
var distributePointsTool = {
|
|
303
|
+
name: "distribute_points",
|
|
304
|
+
description: "Generate a spatial distribution of points using a named algorithm (poisson-disk, phyllotaxis, hex-grid, jittered-grid, and more). Returns the point array and count.",
|
|
305
|
+
inputSchema: {
|
|
306
|
+
type: "object",
|
|
307
|
+
properties: {
|
|
308
|
+
algorithm: {
|
|
309
|
+
type: "string",
|
|
310
|
+
enum: [
|
|
311
|
+
"poisson-disk",
|
|
312
|
+
"phyllotaxis",
|
|
313
|
+
"hex-grid",
|
|
314
|
+
"tri-grid",
|
|
315
|
+
"jittered-grid",
|
|
316
|
+
"r2-sequence",
|
|
317
|
+
"halton",
|
|
318
|
+
"best-candidate",
|
|
319
|
+
"latin-hypercube",
|
|
320
|
+
"lloyd-relax"
|
|
321
|
+
],
|
|
322
|
+
description: "Distribution algorithm"
|
|
323
|
+
},
|
|
324
|
+
width: { type: "number", description: "Canvas width in pixels" },
|
|
325
|
+
height: { type: "number", description: "Canvas height in pixels" },
|
|
326
|
+
params: {
|
|
327
|
+
type: "object",
|
|
328
|
+
description: "Algorithm-specific parameters (minDist, count, size, jitter, etc.)",
|
|
329
|
+
additionalProperties: true
|
|
330
|
+
},
|
|
331
|
+
seed: { type: "number", description: "PRNG seed for reproducibility" }
|
|
332
|
+
},
|
|
333
|
+
required: ["algorithm", "width", "height"]
|
|
334
|
+
},
|
|
335
|
+
async handler(input, _context) {
|
|
336
|
+
const algorithm = String(input.algorithm);
|
|
337
|
+
const width = Number(input.width);
|
|
338
|
+
const height = Number(input.height);
|
|
339
|
+
const params = input.params ?? {};
|
|
340
|
+
const rng = makePrng(Number(input.seed ?? 0));
|
|
341
|
+
let points = [];
|
|
342
|
+
switch (algorithm) {
|
|
343
|
+
case "poisson-disk": {
|
|
344
|
+
const minDist = params.minDist ?? 20;
|
|
345
|
+
const raw = poissonDisk(rng, width, height, minDist, params.maxAttempts ?? 30);
|
|
346
|
+
points = raw.map((p, i) => ({ ...p, size: 1, index: i }));
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
case "phyllotaxis": {
|
|
350
|
+
const n = params.count ?? 200;
|
|
351
|
+
const PHI = Math.PI * (3 - Math.sqrt(5));
|
|
352
|
+
const scale = params.scale ?? Math.min(width, height) / 2;
|
|
353
|
+
for (let i = 0; i < n; i++) {
|
|
354
|
+
const r = Math.sqrt(i / n) * scale;
|
|
355
|
+
const theta = i * PHI;
|
|
356
|
+
points.push({ x: width / 2 + r * Math.cos(theta), y: height / 2 + r * Math.sin(theta), size: 1, index: i });
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case "hex-grid": {
|
|
361
|
+
const size = params.size ?? 20;
|
|
362
|
+
const colW = size * 1.5, rowH = size * Math.sqrt(3);
|
|
363
|
+
let idx = 0;
|
|
364
|
+
for (let col = 0; col <= Math.ceil(width / colW); col++) {
|
|
365
|
+
for (let row = 0; row <= Math.ceil(height / rowH); row++) {
|
|
366
|
+
const x = col * colW;
|
|
367
|
+
const y = row * rowH + (col % 2 === 1 ? rowH / 2 : 0);
|
|
368
|
+
if (x <= width && y <= height) points.push({ x, y, size, index: idx++ });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case "tri-grid": {
|
|
374
|
+
const size = params.size ?? 20;
|
|
375
|
+
const rowH = size * Math.sqrt(3) / 2;
|
|
376
|
+
let idx = 0;
|
|
377
|
+
for (let row = 0; row * rowH <= height; row++) {
|
|
378
|
+
const offset = row % 2 === 1 ? size / 2 : 0;
|
|
379
|
+
for (let col = 0; col * size + offset <= width; col++) {
|
|
380
|
+
points.push({ x: col * size + offset, y: row * rowH, size, index: idx++ });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
case "jittered-grid": {
|
|
386
|
+
const size = params.size ?? 30;
|
|
387
|
+
const jitter = params.jitter ?? 0.5;
|
|
388
|
+
let idx = 0;
|
|
389
|
+
for (let row = 0; row * size < height; row++) {
|
|
390
|
+
for (let col = 0; col * size < width; col++) {
|
|
391
|
+
points.push({
|
|
392
|
+
x: (col + 0.5 + (rng() - 0.5) * jitter) * size,
|
|
393
|
+
y: (row + 0.5 + (rng() - 0.5) * jitter) * size,
|
|
394
|
+
size: 1,
|
|
395
|
+
index: idx++
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case "r2-sequence": {
|
|
402
|
+
const n = params.count ?? 100;
|
|
403
|
+
const g = 1.324717957244746;
|
|
404
|
+
const a1 = 1 / g, a2 = 1 / (g * g);
|
|
405
|
+
for (let i = 0; i < n; i++) {
|
|
406
|
+
points.push({ x: (0.5 + a1 * i) % 1 * width, y: (0.5 + a2 * i) % 1 * height, size: 1, index: i });
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
case "halton": {
|
|
411
|
+
let haltonBase2 = function(i, base) {
|
|
412
|
+
let f = 1, r = 0;
|
|
413
|
+
while (i > 0) {
|
|
414
|
+
f /= base;
|
|
415
|
+
r += f * (i % base);
|
|
416
|
+
i = Math.floor(i / base);
|
|
417
|
+
}
|
|
418
|
+
return r;
|
|
419
|
+
};
|
|
420
|
+
var haltonBase = haltonBase2;
|
|
421
|
+
const n = params.count ?? 100;
|
|
422
|
+
for (let i = 0; i < n; i++) {
|
|
423
|
+
points.push({ x: haltonBase2(i + 1, 2) * width, y: haltonBase2(i + 1, 3) * height, size: 1, index: i });
|
|
424
|
+
}
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
case "best-candidate": {
|
|
428
|
+
const n = params.count ?? 100;
|
|
429
|
+
const k = params.candidates ?? 10;
|
|
430
|
+
for (let i = 0; i < n; i++) {
|
|
431
|
+
let bestX = 0, bestY = 0, bestDist = -1;
|
|
432
|
+
for (let c = 0; c < k; c++) {
|
|
433
|
+
const cx = rng() * width, cy = rng() * height;
|
|
434
|
+
let minD = Infinity;
|
|
435
|
+
for (const pt of points) {
|
|
436
|
+
const dx = cx - pt.x, dy = cy - (pt.y ?? 0);
|
|
437
|
+
const d = dx * dx + dy * dy;
|
|
438
|
+
if (d < minD) minD = d;
|
|
439
|
+
}
|
|
440
|
+
if (points.length === 0) minD = Infinity;
|
|
441
|
+
if (minD > bestDist) {
|
|
442
|
+
bestDist = minD;
|
|
443
|
+
bestX = cx;
|
|
444
|
+
bestY = cy;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
points.push({ x: bestX, y: bestY, size: 1, index: i });
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case "latin-hypercube": {
|
|
452
|
+
const n = params.count ?? 100;
|
|
453
|
+
const xs = Array.from({ length: n }, (_, i) => i);
|
|
454
|
+
const ys = Array.from({ length: n }, (_, i) => i);
|
|
455
|
+
for (let i = n - 1; i > 0; i--) {
|
|
456
|
+
const j = Math.floor(rng() * (i + 1));
|
|
457
|
+
[xs[i], xs[j]] = [xs[j], xs[i]];
|
|
458
|
+
const k2 = Math.floor(rng() * (i + 1));
|
|
459
|
+
[ys[i], ys[k2]] = [ys[k2], ys[i]];
|
|
460
|
+
}
|
|
461
|
+
const cellW = width / n, cellH = height / n;
|
|
462
|
+
for (let i = 0; i < n; i++) {
|
|
463
|
+
points.push({ x: (xs[i] + rng()) * cellW, y: (ys[i] + rng()) * cellH, size: 1, index: i });
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
default: {
|
|
468
|
+
const n = params.count ?? 100;
|
|
469
|
+
for (let i = 0; i < n; i++) {
|
|
470
|
+
points.push({ x: rng() * width, y: rng() * height, size: 1, index: i });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return textResult(JSON.stringify({
|
|
475
|
+
points,
|
|
476
|
+
count: points.length,
|
|
477
|
+
bounds: { x: 0, y: 0, width, height }
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var packCirclesTool = {
|
|
482
|
+
name: "pack_circles",
|
|
483
|
+
description: "Pack non-overlapping circles within a region using trial-and-reject sampling. Returns (x, y, radius) tuples and coverage percentage.",
|
|
484
|
+
inputSchema: {
|
|
485
|
+
type: "object",
|
|
486
|
+
properties: {
|
|
487
|
+
width: { type: "number" },
|
|
488
|
+
height: { type: "number" },
|
|
489
|
+
minRadius: { type: "number", default: 5 },
|
|
490
|
+
maxRadius: { type: "number", default: 50 },
|
|
491
|
+
count: { type: "number", default: 100 },
|
|
492
|
+
padding: { type: "number", default: 2 },
|
|
493
|
+
seed: { type: "number", default: 0 }
|
|
494
|
+
},
|
|
495
|
+
required: ["width", "height"]
|
|
496
|
+
},
|
|
497
|
+
async handler(input, _context) {
|
|
498
|
+
const width = Number(input.width);
|
|
499
|
+
const height = Number(input.height);
|
|
500
|
+
const minRadius = Number(input.minRadius ?? 5);
|
|
501
|
+
const maxRadius = Number(input.maxRadius ?? 50);
|
|
502
|
+
const count = Number(input.count ?? 100);
|
|
503
|
+
const padding = Number(input.padding ?? 2);
|
|
504
|
+
const rng = makePrng(Number(input.seed ?? 0));
|
|
505
|
+
const circles = [];
|
|
506
|
+
const maxAttempts = 500;
|
|
507
|
+
for (let c = 0; c < count; c++) {
|
|
508
|
+
for (let a = 0; a < maxAttempts; a++) {
|
|
509
|
+
const r = minRadius + rng() * (maxRadius - minRadius);
|
|
510
|
+
const x = r + padding + rng() * (width - 2 * r - 2 * padding);
|
|
511
|
+
const y = r + padding + rng() * (height - 2 * r - 2 * padding);
|
|
512
|
+
if (x < 0 || y < 0) continue;
|
|
513
|
+
let ok = true;
|
|
514
|
+
for (const ci of circles) {
|
|
515
|
+
const dx = x - ci.x, dy = y - ci.y;
|
|
516
|
+
if (dx * dx + dy * dy < (r + ci.radius + padding) ** 2) {
|
|
517
|
+
ok = false;
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (ok) {
|
|
522
|
+
circles.push({ x, y, radius: r, index: circles.length });
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const area = circles.reduce((acc, c) => acc + Math.PI * c.radius * c.radius, 0);
|
|
528
|
+
return textResult(JSON.stringify({
|
|
529
|
+
circles,
|
|
530
|
+
count: circles.length,
|
|
531
|
+
coverage: area / (width * height)
|
|
532
|
+
}));
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
var packRectsTool = {
|
|
536
|
+
name: "pack_rects",
|
|
537
|
+
description: "Pack rectangles into a bin using the guillotine algorithm. Returns placements with x, y, width, height, and rotated flag.",
|
|
538
|
+
inputSchema: {
|
|
539
|
+
type: "object",
|
|
540
|
+
properties: {
|
|
541
|
+
rects: {
|
|
542
|
+
type: "array",
|
|
543
|
+
items: {
|
|
544
|
+
type: "object",
|
|
545
|
+
properties: { w: { type: "number" }, h: { type: "number" }, id: { type: "string" } },
|
|
546
|
+
required: ["w", "h"]
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
width: { type: "number" },
|
|
550
|
+
height: { type: "number" },
|
|
551
|
+
padding: { type: "number", default: 2 },
|
|
552
|
+
allowRotation: { type: "boolean", default: false }
|
|
553
|
+
},
|
|
554
|
+
required: ["rects", "width", "height"]
|
|
555
|
+
},
|
|
556
|
+
async handler(input, _context) {
|
|
557
|
+
const rects = input.rects;
|
|
558
|
+
const width = Number(input.width);
|
|
559
|
+
const height = Number(input.height);
|
|
560
|
+
const padding = Number(input.padding ?? 2);
|
|
561
|
+
const allowRotation = Boolean(input.allowRotation ?? false);
|
|
562
|
+
const sorted = rects.slice().sort((a, b) => b.h * b.w - a.h * a.w);
|
|
563
|
+
const free = [{ x: 0, y: 0, w: width, h: height }];
|
|
564
|
+
const placements = [];
|
|
565
|
+
for (const rect of sorted) {
|
|
566
|
+
const rw = rect.w + padding, rh = rect.h + padding;
|
|
567
|
+
let bestScore = Infinity, bestFi = -1, bestRot = false;
|
|
568
|
+
for (let fi = 0; fi < free.length; fi++) {
|
|
569
|
+
const f = free[fi];
|
|
570
|
+
if (f.w >= rw && f.h >= rh) {
|
|
571
|
+
const score = Math.min(f.w - rw, f.h - rh);
|
|
572
|
+
if (score < bestScore) {
|
|
573
|
+
bestScore = score;
|
|
574
|
+
bestFi = fi;
|
|
575
|
+
bestRot = false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (allowRotation && f.w >= rh && f.h >= rw) {
|
|
579
|
+
const score = Math.min(f.w - rh, f.h - rw);
|
|
580
|
+
if (score < bestScore) {
|
|
581
|
+
bestScore = score;
|
|
582
|
+
bestFi = fi;
|
|
583
|
+
bestRot = true;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (bestFi >= 0) {
|
|
588
|
+
const f = free[bestFi];
|
|
589
|
+
const pw = bestRot ? rh : rw, ph = bestRot ? rw : rh;
|
|
590
|
+
placements.push({ x: f.x, y: f.y, w: rect.w, h: rect.h, id: rect.id, rotated: bestRot });
|
|
591
|
+
free.splice(bestFi, 1);
|
|
592
|
+
if (f.w - pw > 0 && ph > 0) free.push({ x: f.x + pw, y: f.y, w: f.w - pw, h: ph });
|
|
593
|
+
if (f.w > 0 && f.h - ph > 0) free.push({ x: f.x, y: f.y + ph, w: f.w, h: f.h - ph });
|
|
594
|
+
} else {
|
|
595
|
+
placements.push(null);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const result = new Array(rects.length).fill(null);
|
|
599
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
600
|
+
result[rects.indexOf(sorted[i])] = placements[i];
|
|
601
|
+
}
|
|
602
|
+
const packed = result.filter(Boolean).length;
|
|
603
|
+
const utilization = result.filter(Boolean).reduce((acc, p) => {
|
|
604
|
+
const pl = p;
|
|
605
|
+
return acc + pl.w * pl.h;
|
|
606
|
+
}, 0) / (width * height);
|
|
607
|
+
return textResult(JSON.stringify({ placements: result, packed, total: rects.length, utilization }));
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
var previewDistributionTool = {
|
|
611
|
+
name: "preview_distribution",
|
|
612
|
+
description: "Generate a distribution and add a distribution:preview guide layer to visualize it non-destructively.",
|
|
613
|
+
inputSchema: {
|
|
614
|
+
type: "object",
|
|
615
|
+
properties: {
|
|
616
|
+
algorithm: { type: "string" },
|
|
617
|
+
width: { type: "number" },
|
|
618
|
+
height: { type: "number" },
|
|
619
|
+
params: { type: "object", additionalProperties: true },
|
|
620
|
+
dotSize: { type: "number", default: 3 },
|
|
621
|
+
dotColor: { type: "string", default: "#0088ff" },
|
|
622
|
+
layerName: { type: "string" },
|
|
623
|
+
seed: { type: "number", default: 0 }
|
|
624
|
+
},
|
|
625
|
+
required: ["algorithm"]
|
|
626
|
+
},
|
|
627
|
+
async handler(input, context) {
|
|
628
|
+
const algorithm = String(input.algorithm);
|
|
629
|
+
const canvasW = Number(input.width ?? context.canvasWidth);
|
|
630
|
+
const canvasH = Number(input.height ?? context.canvasHeight);
|
|
631
|
+
const ptResult = await distributePointsTool.handler(
|
|
632
|
+
{ ...input, algorithm, width: canvasW, height: canvasH },
|
|
633
|
+
context
|
|
634
|
+
);
|
|
635
|
+
const ptText = ptResult.content[0];
|
|
636
|
+
if (!ptText || ptText.type !== "text" || ptResult.isError) {
|
|
637
|
+
return errorResult(`Failed to generate distribution for algorithm "${algorithm}".`);
|
|
638
|
+
}
|
|
639
|
+
const data = JSON.parse(ptText.text);
|
|
640
|
+
const layerId = `dist-preview-${Date.now().toString(36)}`;
|
|
641
|
+
context.layers.add({
|
|
642
|
+
id: layerId,
|
|
643
|
+
type: "distribution:preview",
|
|
644
|
+
name: input.layerName ?? `${algorithm} preview`,
|
|
645
|
+
visible: true,
|
|
646
|
+
locked: false,
|
|
647
|
+
opacity: 1,
|
|
648
|
+
blendMode: "normal",
|
|
649
|
+
transform: { x: 0, y: 0, width: canvasW, height: canvasH, rotation: 0, scaleX: 1, scaleY: 1, anchorX: 0.5, anchorY: 0.5 },
|
|
650
|
+
properties: {
|
|
651
|
+
algorithm,
|
|
652
|
+
params: JSON.stringify(input.params ?? {}),
|
|
653
|
+
dotSize: Number(input.dotSize ?? 3),
|
|
654
|
+
dotColor: String(input.dotColor ?? "#0088ff"),
|
|
655
|
+
opacity: 0.6,
|
|
656
|
+
_points: JSON.stringify(data.points)
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
context.emitChange("layer-added");
|
|
660
|
+
return textResult(`Added distribution preview layer '${layerId}' with ${data.count} points (algorithm: ${algorithm}).`);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
var clearDistributionPreviewTool = {
|
|
664
|
+
name: "clear_distribution_preview",
|
|
665
|
+
description: "Remove distribution:preview or distribution:voronoi guide layers from the document.",
|
|
666
|
+
inputSchema: {
|
|
667
|
+
type: "object",
|
|
668
|
+
properties: {
|
|
669
|
+
layerId: { type: "string", description: "Specific layer ID to remove; omit to remove all distribution guide layers" }
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
async handler(input, context) {
|
|
673
|
+
const layerId = input.layerId;
|
|
674
|
+
const allLayers = context.layers.getAll();
|
|
675
|
+
const toRemove = layerId ? allLayers.filter((l) => l.id === layerId) : allLayers.filter((l) => l.type.startsWith("distribution:"));
|
|
676
|
+
if (toRemove.length === 0) {
|
|
677
|
+
return textResult("No distribution guide layers found to remove.");
|
|
678
|
+
}
|
|
679
|
+
for (const l of toRemove) {
|
|
680
|
+
context.layers.remove(l.id);
|
|
681
|
+
}
|
|
682
|
+
context.emitChange("layer-removed");
|
|
683
|
+
return textResult(`Removed ${toRemove.length} distribution guide layer(s).`);
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
var growPatternTool = {
|
|
687
|
+
name: "grow_pattern",
|
|
688
|
+
description: "Run a growth algorithm (DLA, differential-growth, substrate) and return the resulting point set and/or paths.",
|
|
689
|
+
inputSchema: {
|
|
690
|
+
type: "object",
|
|
691
|
+
properties: {
|
|
692
|
+
algorithm: {
|
|
693
|
+
type: "string",
|
|
694
|
+
enum: ["dla", "differential-growth", "substrate"]
|
|
695
|
+
},
|
|
696
|
+
width: { type: "number" },
|
|
697
|
+
height: { type: "number" },
|
|
698
|
+
iterations: { type: "number", default: 100 },
|
|
699
|
+
seeds: {
|
|
700
|
+
type: "array",
|
|
701
|
+
items: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } }
|
|
702
|
+
},
|
|
703
|
+
params: { type: "object", additionalProperties: true },
|
|
704
|
+
seed: { type: "number", default: 0 }
|
|
705
|
+
},
|
|
706
|
+
required: ["algorithm", "width", "height"]
|
|
707
|
+
},
|
|
708
|
+
async handler(input, _context) {
|
|
709
|
+
const algorithm = String(input.algorithm);
|
|
710
|
+
const width = Number(input.width);
|
|
711
|
+
const height = Number(input.height);
|
|
712
|
+
const iterations = Number(input.iterations ?? 100);
|
|
713
|
+
const rng = makePrng(Number(input.seed ?? 0));
|
|
714
|
+
if (algorithm === "dla") {
|
|
715
|
+
const attached = [{ x: width / 2, y: height / 2 }];
|
|
716
|
+
const grid = new Uint8Array(width * height);
|
|
717
|
+
grid[Math.floor(height / 2) * width + Math.floor(width / 2)] = 1;
|
|
718
|
+
for (let w = 0; w < iterations * 10; w++) {
|
|
719
|
+
let wx = Math.floor(rng() * width);
|
|
720
|
+
let wy = Math.floor(rng() * height);
|
|
721
|
+
for (let step = 0; step < 500; step++) {
|
|
722
|
+
wx += Math.round(rng() * 2 - 1);
|
|
723
|
+
wy += Math.round(rng() * 2 - 1);
|
|
724
|
+
if (wx < 0 || wx >= width || wy < 0 || wy >= height) break;
|
|
725
|
+
let hasN = false;
|
|
726
|
+
for (let dx = -1; dx <= 1 && !hasN; dx++) {
|
|
727
|
+
for (let dy = -1; dy <= 1 && !hasN; dy++) {
|
|
728
|
+
const nx = wx + dx, ny = wy + dy;
|
|
729
|
+
if (nx >= 0 && nx < width && ny >= 0 && ny < height && grid[ny * width + nx]) hasN = true;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (hasN) {
|
|
733
|
+
grid[wy * width + wx] = 1;
|
|
734
|
+
attached.push({ x: wx, y: wy });
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return textResult(JSON.stringify({ points: attached.map((p, i) => ({ ...p, index: i })), count: attached.length }));
|
|
740
|
+
}
|
|
741
|
+
return textResult(JSON.stringify({
|
|
742
|
+
message: `Growth algorithm "${algorithm}" with ${iterations} iterations. Use the ${algorithm} component in sketch code for full control.`,
|
|
743
|
+
algorithm,
|
|
744
|
+
dimensions: { width, height },
|
|
745
|
+
iterations
|
|
746
|
+
}));
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
var tileRegionTool = {
|
|
750
|
+
name: "tile_region",
|
|
751
|
+
description: "Tile a region using Wave Function Collapse. Returns a 2D grid of tile IDs.",
|
|
752
|
+
inputSchema: {
|
|
753
|
+
type: "object",
|
|
754
|
+
properties: {
|
|
755
|
+
tileSet: {
|
|
756
|
+
type: "object",
|
|
757
|
+
description: "Tile set: {tiles: [{id, weight?}], adjacency: {[id]: {up, down, left, right}: string[]}}"
|
|
758
|
+
},
|
|
759
|
+
width: { type: "number", description: "Grid width in tiles" },
|
|
760
|
+
height: { type: "number", description: "Grid height in tiles" },
|
|
761
|
+
seed: { type: "number", default: 0 }
|
|
762
|
+
},
|
|
763
|
+
required: ["tileSet", "width", "height"]
|
|
764
|
+
},
|
|
765
|
+
async handler(input, _context) {
|
|
766
|
+
const tileSet = input.tileSet;
|
|
767
|
+
const width = Number(input.width);
|
|
768
|
+
const height = Number(input.height);
|
|
769
|
+
const rng = makePrng(Number(input.seed ?? 0));
|
|
770
|
+
const tiles = tileSet.tiles;
|
|
771
|
+
const adj = tileSet.adjacency;
|
|
772
|
+
const tileIds = tiles.map((t) => t.id);
|
|
773
|
+
const weights = {};
|
|
774
|
+
tiles.forEach((t) => {
|
|
775
|
+
weights[t.id] = t.weight ?? 1;
|
|
776
|
+
});
|
|
777
|
+
const wave = [];
|
|
778
|
+
for (let r = 0; r < height; r++) {
|
|
779
|
+
wave.push([]);
|
|
780
|
+
for (let c = 0; c < width; c++) {
|
|
781
|
+
const wRow = wave[r];
|
|
782
|
+
wRow.push(tileIds[Math.floor(rng() * tileIds.length)] ?? tileIds[0] ?? "");
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
for (let r = 0; r < height; r++) {
|
|
786
|
+
for (let c = 0; c < width; c++) {
|
|
787
|
+
const tid = wave[r][c];
|
|
788
|
+
const nbrs = [[-1, 0, "down"], [1, 0, "up"], [0, -1, "right"], [0, 1, "left"]];
|
|
789
|
+
for (const [dr, dc, opp] of nbrs) {
|
|
790
|
+
const nr = r + dr, nc = c + dc;
|
|
791
|
+
if (nr < 0 || nr >= height || nc < 0 || nc >= width) continue;
|
|
792
|
+
const allowed = adj[tid]?.[opp] ?? tileIds;
|
|
793
|
+
if (!allowed.includes(wave[nr][nc])) {
|
|
794
|
+
wave[nr][nc] = allowed[Math.floor(rng() * allowed.length)] ?? wave[nr][nc];
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return textResult(JSON.stringify({
|
|
800
|
+
grid: wave,
|
|
801
|
+
width,
|
|
802
|
+
height,
|
|
803
|
+
tiles: wave.flat().map((id, i) => ({ id, col: i % width, row: Math.floor(i / width) }))
|
|
804
|
+
}));
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
var distributeAlongPathTool = {
|
|
808
|
+
name: "distribute_along_path",
|
|
809
|
+
description: "Distribute points along a polyline using arc-length parameterization.",
|
|
810
|
+
inputSchema: {
|
|
811
|
+
type: "object",
|
|
812
|
+
properties: {
|
|
813
|
+
path: {
|
|
814
|
+
type: "array",
|
|
815
|
+
items: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
816
|
+
description: "Polyline points [{x, y}]"
|
|
817
|
+
},
|
|
818
|
+
count: { type: "number", default: 20 },
|
|
819
|
+
spacing: { type: "number", description: "Fixed spacing in pixels (overrides count)" },
|
|
820
|
+
offset: { type: "number", default: 0, description: "Start offset [0..1]" },
|
|
821
|
+
closed: { type: "boolean", default: false }
|
|
822
|
+
},
|
|
823
|
+
required: ["path"]
|
|
824
|
+
},
|
|
825
|
+
async handler(input, _context) {
|
|
826
|
+
const path = input.path;
|
|
827
|
+
const count = Number(input.count ?? 20);
|
|
828
|
+
const spacing = input.spacing != null ? Number(input.spacing) : null;
|
|
829
|
+
const offset = Number(input.offset ?? 0);
|
|
830
|
+
const closed = Boolean(input.closed ?? false);
|
|
831
|
+
if (path.length < 2) return textResult(JSON.stringify({ points: [], count: 0 }));
|
|
832
|
+
const lens = [0];
|
|
833
|
+
const pts = closed ? [...path, path[0]] : path;
|
|
834
|
+
for (let i = 1; i < pts.length; i++) {
|
|
835
|
+
const dx = pts[i].x - pts[i - 1].x;
|
|
836
|
+
const dy = pts[i].y - pts[i - 1].y;
|
|
837
|
+
lens.push(lens[i - 1] + Math.sqrt(dx * dx + dy * dy));
|
|
838
|
+
}
|
|
839
|
+
const totalLen = lens[lens.length - 1] ?? 0;
|
|
840
|
+
if (totalLen === 0) return textResult(JSON.stringify({ points: [], count: 0 }));
|
|
841
|
+
const n = spacing != null ? Math.floor((totalLen - offset * totalLen) / spacing) : count;
|
|
842
|
+
const step = spacing ?? totalLen / (closed ? n : Math.max(1, n - 1));
|
|
843
|
+
const result = [];
|
|
844
|
+
for (let i = 0; i < n; i++) {
|
|
845
|
+
const targetLen = offset * totalLen + i * step;
|
|
846
|
+
if (targetLen > totalLen) break;
|
|
847
|
+
let lo = 0, hi = lens.length - 2;
|
|
848
|
+
while (lo < hi) {
|
|
849
|
+
const mid = lo + hi >> 1;
|
|
850
|
+
if ((lens[mid + 1] ?? 0) < targetLen) lo = mid + 1;
|
|
851
|
+
else hi = mid;
|
|
852
|
+
}
|
|
853
|
+
const segLen = lens[lo + 1] - lens[lo];
|
|
854
|
+
const t = segLen > 0 ? (targetLen - lens[lo]) / segLen : 0;
|
|
855
|
+
const p0 = pts[lo], p1 = pts[lo + 1];
|
|
856
|
+
result.push({
|
|
857
|
+
x: p0.x + t * (p1.x - p0.x),
|
|
858
|
+
y: p0.y + t * (p1.y - p0.y),
|
|
859
|
+
t: targetLen / totalLen,
|
|
860
|
+
angle: Math.atan2(p1.y - p0.y, p1.x - p0.x),
|
|
861
|
+
index: i
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
return textResult(JSON.stringify({ points: result, count: result.length, pathLength: totalLen }));
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
var distributionMcpTools = [
|
|
868
|
+
distributePointsTool,
|
|
869
|
+
packCirclesTool,
|
|
870
|
+
packRectsTool,
|
|
871
|
+
previewDistributionTool,
|
|
872
|
+
clearDistributionPreviewTool,
|
|
873
|
+
growPatternTool,
|
|
874
|
+
tileRegionTool,
|
|
875
|
+
distributeAlongPathTool
|
|
876
|
+
];
|
|
877
|
+
|
|
878
|
+
// src/index.ts
|
|
879
|
+
var distributionPlugin = {
|
|
880
|
+
id: "distribution",
|
|
881
|
+
name: "Distribution & Packing",
|
|
882
|
+
version: "0.1.0",
|
|
883
|
+
tier: "free",
|
|
884
|
+
description: "Spatial distribution algorithms (Poisson disk, phyllotaxis, hex grid, DLA, WFC, and more) plus circle/rect packing. Includes guide layers for non-destructive distribution previews.",
|
|
885
|
+
layerTypes: [previewLayerType, voronoiLayerType, densityLayerType],
|
|
886
|
+
tools: [],
|
|
887
|
+
exportHandlers: [],
|
|
888
|
+
mcpTools: distributionMcpTools,
|
|
889
|
+
async initialize(_context) {
|
|
890
|
+
},
|
|
891
|
+
dispose() {
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
var index_default = distributionPlugin;
|
|
895
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
896
|
+
0 && (module.exports = {
|
|
897
|
+
densityLayerType,
|
|
898
|
+
distributionMcpTools,
|
|
899
|
+
distributionPlugin,
|
|
900
|
+
previewLayerType,
|
|
901
|
+
voronoiLayerType
|
|
902
|
+
});
|
|
903
|
+
//# sourceMappingURL=index.cjs.map
|