@clawdraw/skill 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/LICENSE +21 -0
- package/README.md +63 -0
- package/SKILL.md +245 -0
- package/community/README.md +69 -0
- package/community/_template.mjs +39 -0
- package/community/helpers.mjs +1 -0
- package/package.json +44 -0
- package/primitives/basic-shapes.mjs +176 -0
- package/primitives/community-palettes.json +1 -0
- package/primitives/decorative.mjs +373 -0
- package/primitives/fills.mjs +217 -0
- package/primitives/flow-abstract.mjs +276 -0
- package/primitives/helpers.mjs +291 -0
- package/primitives/index.mjs +154 -0
- package/primitives/organic.mjs +514 -0
- package/primitives/utility.mjs +342 -0
- package/references/ALGORITHM_GUIDE.md +211 -0
- package/references/COMMUNITY.md +72 -0
- package/references/EXAMPLES.md +165 -0
- package/references/PALETTES.md +46 -0
- package/references/PRIMITIVES.md +301 -0
- package/references/PRO_TIPS.md +114 -0
- package/references/SECURITY.md +58 -0
- package/references/STROKE_FORMAT.md +78 -0
- package/references/SYMMETRY.md +59 -0
- package/references/WEBSOCKET.md +83 -0
- package/scripts/auth.mjs +145 -0
- package/scripts/clawdraw.mjs +882 -0
- package/scripts/connection.mjs +330 -0
- package/scripts/symmetry.mjs +217 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper utilities for ClawDraw stroke generation.
|
|
3
|
+
*
|
|
4
|
+
* Stroke format:
|
|
5
|
+
* { id, points: [{x, y, pressure, timestamp}], brush: {size, color, opacity}, createdAt }
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Core math helpers
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
|
|
13
|
+
|
|
14
|
+
export function lerp(a, b, t) { return a + (b - a) * t; }
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Color helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export function hexToRgb(hex) {
|
|
21
|
+
const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
|
|
22
|
+
if (!m) return { r: 255, g: 255, b: 255 };
|
|
23
|
+
return { r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function rgbToHex(r, g, b) {
|
|
27
|
+
return '#' + [r, g, b].map(v => clamp(Math.round(v), 0, 255).toString(16).padStart(2, '0')).join('');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function lerpColor(hex1, hex2, t) {
|
|
31
|
+
const a = hexToRgb(hex1), b = hexToRgb(hex2);
|
|
32
|
+
return rgbToHex(lerp(a.r, b.r, t), lerp(a.g, b.g, t), lerp(a.b, b.b, t));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Convert HSL values to a hex color string.
|
|
37
|
+
* @param {number} h - Hue (0-360)
|
|
38
|
+
* @param {number} s - Saturation (0-100)
|
|
39
|
+
* @param {number} l - Lightness (0-100)
|
|
40
|
+
* @returns {string} Hex color string (e.g. '#ff6600')
|
|
41
|
+
*/
|
|
42
|
+
export function hslToHex(h, s, l) {
|
|
43
|
+
s /= 100;
|
|
44
|
+
l /= 100;
|
|
45
|
+
const k = (n) => (n + h / 30) % 12;
|
|
46
|
+
const a = s * Math.min(l, 1 - l);
|
|
47
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1));
|
|
48
|
+
return rgbToHex(Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Color Palettes
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
import { createRequire } from 'node:module';
|
|
56
|
+
const _require = createRequire(import.meta.url);
|
|
57
|
+
const _communityPalettes = _require('./community-palettes.json');
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Scientific visualization gradients (8 stops each, smooth continuous gradients).
|
|
61
|
+
* These are the primary palettes — high quality, perceptually uniform.
|
|
62
|
+
*/
|
|
63
|
+
export const PALETTES = {
|
|
64
|
+
magma: ['#000004', '#180f3d', '#440f76', '#721f81', '#b5367a', '#e55c30', '#fba40a', '#fcffa4'],
|
|
65
|
+
plasma: ['#0d0887', '#4b03a1', '#7d03a8', '#a82296', '#cb4679', '#e86825', '#f89540', '#f0f921'],
|
|
66
|
+
viridis: ['#440154', '#443a83', '#31688e', '#21908c', '#35b779', '#6ece58', '#b5de2b', '#fde725'],
|
|
67
|
+
turbo: ['#30123b', '#4662d7', '#36aaf9', '#1ae4b6', '#72fe5e', '#c8ef34', '#faba39', '#e6550d'],
|
|
68
|
+
inferno: ['#000004', '#1b0c41', '#4a0c6b', '#781c6d', '#a52c60', '#cf4446', '#ed6925', '#fcffa4'],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** All scientific palette names. */
|
|
72
|
+
export const PALETTE_NAMES = Object.keys(PALETTES);
|
|
73
|
+
|
|
74
|
+
/** Community palettes — 992 curated 5-color schemes from ColourLovers. */
|
|
75
|
+
export const COMMUNITY_PALETTES = _communityPalettes;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Sample any color-stop array as a gradient at parameter t in [0,1].
|
|
79
|
+
* Works with any palette — 5 stops, 8 stops, any length.
|
|
80
|
+
* Returns an interpolated hex color.
|
|
81
|
+
*/
|
|
82
|
+
export function samplePalette(paletteOrName, t) {
|
|
83
|
+
const stops = typeof paletteOrName === 'string'
|
|
84
|
+
? (PALETTES[paletteOrName] || PALETTES.viridis)
|
|
85
|
+
: paletteOrName;
|
|
86
|
+
t = clamp(t, 0, 1);
|
|
87
|
+
const pos = t * (stops.length - 1);
|
|
88
|
+
const lo = Math.floor(pos);
|
|
89
|
+
const hi = Math.min(lo + 1, stops.length - 1);
|
|
90
|
+
const frac = pos - lo;
|
|
91
|
+
return lerpColor(stops[lo], stops[hi], frac);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Pick a random palette. Weighted 30% toward scientific palettes (magma, plasma,
|
|
96
|
+
* viridis, turbo, inferno) and 70% toward the 992 community palettes.
|
|
97
|
+
* Returns an array of hex color strings.
|
|
98
|
+
*/
|
|
99
|
+
export function randomPalette() {
|
|
100
|
+
if (Math.random() < 0.3) {
|
|
101
|
+
const names = PALETTE_NAMES;
|
|
102
|
+
return PALETTES[names[Math.floor(Math.random() * names.length)]];
|
|
103
|
+
}
|
|
104
|
+
return _communityPalettes[Math.floor(Math.random() * _communityPalettes.length)];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Pick a random color from a random palette.
|
|
109
|
+
* Shorthand for choosing a palette and grabbing one discrete color.
|
|
110
|
+
*/
|
|
111
|
+
export function randomColor() {
|
|
112
|
+
const pal = randomPalette();
|
|
113
|
+
return pal[Math.floor(Math.random() * pal.length)];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Pressure simulation
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Simulate natural pen pressure along a stroke.
|
|
122
|
+
* Ramps up at start, sustains with organic variation, tapers at end.
|
|
123
|
+
*/
|
|
124
|
+
export function simulatePressure(index, total) {
|
|
125
|
+
if (total <= 1) return 0.7;
|
|
126
|
+
const t = index / (total - 1);
|
|
127
|
+
|
|
128
|
+
const attackEnd = 0.15;
|
|
129
|
+
const releaseStart = 0.85;
|
|
130
|
+
let envelope;
|
|
131
|
+
if (t < attackEnd) {
|
|
132
|
+
envelope = 0.3 + 0.7 * (t / attackEnd);
|
|
133
|
+
} else if (t > releaseStart) {
|
|
134
|
+
const rt = (t - releaseStart) / (1 - releaseStart);
|
|
135
|
+
envelope = 1.0 - rt * 0.6;
|
|
136
|
+
} else {
|
|
137
|
+
envelope = 1.0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const noise = (Math.random() - 0.5) * 0.16;
|
|
141
|
+
return clamp(envelope * 0.75 + noise, 0.15, 1.0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Return pressure value for a given style at position index/total.
|
|
146
|
+
* Styles: default, flat, taper, taperBoth, pulse, heavy, flick.
|
|
147
|
+
*/
|
|
148
|
+
export function getPressureForStyle(index, total, style) {
|
|
149
|
+
if (total <= 1) return 0.7;
|
|
150
|
+
const t = index / (total - 1);
|
|
151
|
+
|
|
152
|
+
switch (style) {
|
|
153
|
+
case 'flat':
|
|
154
|
+
return 0.8 + (Math.random() - 0.5) * 0.06;
|
|
155
|
+
|
|
156
|
+
case 'taper':
|
|
157
|
+
return clamp(1.0 - t * 0.85 + (Math.random() - 0.5) * 0.08, 0.1, 1.0);
|
|
158
|
+
|
|
159
|
+
case 'taperBoth': {
|
|
160
|
+
const env = Math.sin(t * Math.PI);
|
|
161
|
+
return clamp(env * 0.85 + 0.1 + (Math.random() - 0.5) * 0.08, 0.1, 1.0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case 'pulse': {
|
|
165
|
+
const wave = 0.5 + 0.4 * Math.sin(t * Math.PI * 6);
|
|
166
|
+
return clamp(wave + (Math.random() - 0.5) * 0.1, 0.15, 1.0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case 'heavy':
|
|
170
|
+
return clamp(0.9 + Math.random() * 0.1, 0.85, 1.0);
|
|
171
|
+
|
|
172
|
+
case 'flick': {
|
|
173
|
+
const ramp = t < 0.15 ? t / 0.15 : 1.0;
|
|
174
|
+
const taper = t < 0.15 ? 1.0 : 1.0 - ((t - 0.15) / 0.85) * 0.9;
|
|
175
|
+
return clamp(ramp * taper + (Math.random() - 0.5) * 0.06, 0.05, 1.0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
default:
|
|
179
|
+
return simulatePressure(index, total);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Stroke creation
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
let _strokeSeq = 0;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create a stroke object from an array of {x, y} points.
|
|
191
|
+
* Automatically adds pressure simulation and timestamps.
|
|
192
|
+
*/
|
|
193
|
+
export function makeStroke(points, color = '#ffffff', brushSize = 5, opacity = 0.9, pressureStyle = 'default') {
|
|
194
|
+
const id = `tool-${Date.now().toString(36)}-${(++_strokeSeq).toString(36)}`;
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
const n = points.length;
|
|
197
|
+
return {
|
|
198
|
+
id,
|
|
199
|
+
points: points.map((p, i) => ({
|
|
200
|
+
x: p.x,
|
|
201
|
+
y: p.y,
|
|
202
|
+
pressure: p.pressure ?? getPressureForStyle(i, n, pressureStyle),
|
|
203
|
+
timestamp: now + i * 5,
|
|
204
|
+
})),
|
|
205
|
+
brush: {
|
|
206
|
+
size: clamp(brushSize, 3, 100),
|
|
207
|
+
color: color || '#ffffff',
|
|
208
|
+
opacity: clamp(opacity, 0.01, 1),
|
|
209
|
+
},
|
|
210
|
+
createdAt: now,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Split a long point array into multiple strokes at 4990 points with 10-point overlap.
|
|
216
|
+
*/
|
|
217
|
+
export function splitIntoStrokes(points, color, brushSize, opacity, pressureStyle) {
|
|
218
|
+
const MAX = 4990;
|
|
219
|
+
const OVERLAP = 10;
|
|
220
|
+
if (points.length <= MAX) return [makeStroke(points, color, brushSize, opacity, pressureStyle)];
|
|
221
|
+
const strokes = [];
|
|
222
|
+
let start = 0;
|
|
223
|
+
while (start < points.length) {
|
|
224
|
+
const end = Math.min(start + MAX, points.length);
|
|
225
|
+
strokes.push(makeStroke(points.slice(start, end), color, brushSize, opacity, pressureStyle));
|
|
226
|
+
start = end - OVERLAP;
|
|
227
|
+
if (end === points.length) break;
|
|
228
|
+
}
|
|
229
|
+
return strokes;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// 2D Perlin Noise (inline, zero dependencies)
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
const _perm = new Uint8Array(512);
|
|
237
|
+
const _grad = [[1,1],[-1,1],[1,-1],[-1,-1],[1,0],[-1,0],[0,1],[0,-1]];
|
|
238
|
+
|
|
239
|
+
(function initNoise() {
|
|
240
|
+
const p = new Uint8Array(256);
|
|
241
|
+
for (let i = 0; i < 256; i++) p[i] = i;
|
|
242
|
+
for (let i = 255; i > 0; i--) {
|
|
243
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
244
|
+
[p[i], p[j]] = [p[j], p[i]];
|
|
245
|
+
}
|
|
246
|
+
for (let i = 0; i < 512; i++) _perm[i] = p[i & 255];
|
|
247
|
+
})();
|
|
248
|
+
|
|
249
|
+
function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
|
|
250
|
+
|
|
251
|
+
export function noise2d(x, y) {
|
|
252
|
+
const X = Math.floor(x) & 255, Y = Math.floor(y) & 255;
|
|
253
|
+
const xf = x - Math.floor(x), yf = y - Math.floor(y);
|
|
254
|
+
const u = fade(xf), v = fade(yf);
|
|
255
|
+
const aa = _perm[_perm[X] + Y], ab = _perm[_perm[X] + Y + 1];
|
|
256
|
+
const ba = _perm[_perm[X + 1] + Y], bb = _perm[_perm[X + 1] + Y + 1];
|
|
257
|
+
const dot = (g, dx, dy) => _grad[g & 7][0] * dx + _grad[g & 7][1] * dy;
|
|
258
|
+
return lerp(
|
|
259
|
+
lerp(dot(aa, xf, yf), dot(ba, xf - 1, yf), u),
|
|
260
|
+
lerp(dot(ab, xf, yf - 1), dot(bb, xf - 1, yf - 1), u),
|
|
261
|
+
v
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Geometry helpers
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Clip a line segment to a rectangle using parametric clipping.
|
|
271
|
+
* Returns array of 2 points or null if fully outside.
|
|
272
|
+
*/
|
|
273
|
+
export function clipLineToRect(p0, p1, minX, minY, maxX, maxY) {
|
|
274
|
+
const dx = p1.x - p0.x, dy = p1.y - p0.y;
|
|
275
|
+
let tMin = 0, tMax = 1;
|
|
276
|
+
const edges = [
|
|
277
|
+
[-dx, p0.x - minX], [dx, maxX - p0.x],
|
|
278
|
+
[-dy, p0.y - minY], [dy, maxY - p0.y],
|
|
279
|
+
];
|
|
280
|
+
for (const [p, q] of edges) {
|
|
281
|
+
if (Math.abs(p) < 1e-10) { if (q < 0) return null; continue; }
|
|
282
|
+
const t = q / p;
|
|
283
|
+
if (p < 0) tMin = Math.max(tMin, t);
|
|
284
|
+
else tMax = Math.min(tMax, t);
|
|
285
|
+
}
|
|
286
|
+
if (tMin > tMax) return null;
|
|
287
|
+
return [
|
|
288
|
+
{ x: p0.x + dx * tMin, y: p0.y + dy * tMin },
|
|
289
|
+
{ x: p0.x + dx * tMax, y: p0.y + dy * tMax },
|
|
290
|
+
];
|
|
291
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Primitive registry — auto-discovers built-in and community primitives.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { getPrimitive, listPrimitives, getPrimitiveInfo } from './index.mjs';
|
|
6
|
+
*
|
|
7
|
+
* const fn = getPrimitive('circle');
|
|
8
|
+
* const strokes = fn(0, 0, 100, '#ff0000', 5, 0.9);
|
|
9
|
+
*
|
|
10
|
+
* const all = listPrimitives();
|
|
11
|
+
* const info = getPrimitiveInfo('fractalTree');
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readdir } from 'node:fs/promises';
|
|
15
|
+
import { join, dirname } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Built-in primitive imports
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
import * as basicShapes from './basic-shapes.mjs';
|
|
25
|
+
import * as organic from './organic.mjs';
|
|
26
|
+
import * as flowAbstract from './flow-abstract.mjs';
|
|
27
|
+
import * as fills from './fills.mjs';
|
|
28
|
+
import * as decorative from './decorative.mjs';
|
|
29
|
+
import * as utility from './utility.mjs';
|
|
30
|
+
|
|
31
|
+
const builtinModules = [basicShapes, organic, flowAbstract, fills, decorative, utility];
|
|
32
|
+
|
|
33
|
+
/** @type {Map<string, { fn: Function, meta: object }>} */
|
|
34
|
+
const registry = new Map();
|
|
35
|
+
|
|
36
|
+
// Register all built-in primitives
|
|
37
|
+
for (const mod of builtinModules) {
|
|
38
|
+
if (!mod.METADATA) continue;
|
|
39
|
+
const metaList = Array.isArray(mod.METADATA) ? mod.METADATA : [mod.METADATA];
|
|
40
|
+
for (const meta of metaList) {
|
|
41
|
+
const fn = mod[meta.name];
|
|
42
|
+
if (typeof fn === 'function') {
|
|
43
|
+
registry.set(meta.name, { fn, meta });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Community primitive auto-discovery
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
let communityLoaded = false;
|
|
53
|
+
|
|
54
|
+
async function loadCommunityPrimitives() {
|
|
55
|
+
if (communityLoaded) return;
|
|
56
|
+
communityLoaded = true;
|
|
57
|
+
|
|
58
|
+
const communityDir = join(__dirname, '..', 'community');
|
|
59
|
+
try {
|
|
60
|
+
const files = await readdir(communityDir);
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
if (!file.endsWith('.mjs') || file.startsWith('_')) continue;
|
|
63
|
+
try {
|
|
64
|
+
const mod = await import(join(communityDir, file));
|
|
65
|
+
if (mod.METADATA) {
|
|
66
|
+
const metaList = Array.isArray(mod.METADATA) ? mod.METADATA : [mod.METADATA];
|
|
67
|
+
for (const meta of metaList) {
|
|
68
|
+
const fn = mod[meta.name];
|
|
69
|
+
if (typeof fn === 'function') {
|
|
70
|
+
registry.set(meta.name, { fn, meta: { ...meta, source: 'community', file } });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error(`Warning: Failed to load community primitive ${file}: ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// community/ dir doesn't exist or isn't readable — that's fine
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Public API
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get a primitive function by name.
|
|
89
|
+
* @param {string} name - Primitive name (e.g. 'circle', 'fractalTree')
|
|
90
|
+
* @returns {Function|null} The primitive function, or null if not found
|
|
91
|
+
*/
|
|
92
|
+
export function getPrimitive(name) {
|
|
93
|
+
const entry = registry.get(name);
|
|
94
|
+
return entry ? entry.fn : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* List all registered primitives.
|
|
99
|
+
* @param {object} [opts]
|
|
100
|
+
* @param {string} [opts.category] - Filter by category
|
|
101
|
+
* @param {boolean} [opts.includeCommunity=true] - Include community primitives
|
|
102
|
+
* @returns {Promise<Array<{name: string, description: string, category: string}>>}
|
|
103
|
+
*/
|
|
104
|
+
export async function listPrimitives(opts = {}) {
|
|
105
|
+
await loadCommunityPrimitives();
|
|
106
|
+
const results = [];
|
|
107
|
+
for (const [name, { meta }] of registry) {
|
|
108
|
+
if (opts.category && meta.category !== opts.category) continue;
|
|
109
|
+
if (opts.includeCommunity === false && meta.source === 'community') continue;
|
|
110
|
+
results.push({
|
|
111
|
+
name,
|
|
112
|
+
description: meta.description,
|
|
113
|
+
category: meta.category,
|
|
114
|
+
source: meta.source || 'builtin',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return results.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get detailed info about a specific primitive.
|
|
122
|
+
* @param {string} name - Primitive name
|
|
123
|
+
* @returns {Promise<object|null>} Full metadata including parameters, or null
|
|
124
|
+
*/
|
|
125
|
+
export async function getPrimitiveInfo(name) {
|
|
126
|
+
await loadCommunityPrimitives();
|
|
127
|
+
const entry = registry.get(name);
|
|
128
|
+
if (!entry) return null;
|
|
129
|
+
return { ...entry.meta, source: entry.meta.source || 'builtin' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Execute a primitive by name with args object.
|
|
134
|
+
* @param {string} name - Primitive name
|
|
135
|
+
* @param {object} args - Arguments as key-value pairs
|
|
136
|
+
* @returns {Array} Array of stroke objects
|
|
137
|
+
*/
|
|
138
|
+
export function executePrimitive(name, args) {
|
|
139
|
+
const entry = registry.get(name);
|
|
140
|
+
if (!entry) throw new Error(`Unknown primitive: ${name}`);
|
|
141
|
+
|
|
142
|
+
const meta = entry.meta;
|
|
143
|
+
const paramNames = Object.keys(meta.parameters || {});
|
|
144
|
+
const positionalArgs = paramNames.map(p => args[p]);
|
|
145
|
+
return entry.fn(...positionalArgs);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Re-export all primitive functions for direct import
|
|
149
|
+
export { circle, ellipse, arc, rectangle, polygon, star } from './basic-shapes.mjs';
|
|
150
|
+
export { lSystem, flower, leaf, vine, spaceColonization, mycelium, barnsleyFern } from './organic.mjs';
|
|
151
|
+
export { flowField, spiral, lissajous, strangeAttractor, spirograph } from './flow-abstract.mjs';
|
|
152
|
+
export { hatchFill, crossHatch, stipple, gradientFill, colorWash, solidFill } from './fills.mjs';
|
|
153
|
+
export { border, mandala, fractalTree, radialSymmetry, sacredGeometry } from './decorative.mjs';
|
|
154
|
+
export { bezierCurve, dashedLine, arrow, strokeText, alienGlyphs } from './utility.mjs';
|