@pixerate/editor 0.260.0 → 0.262.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/dist/agent/index.cjs +19 -3
- package/dist/agent/index.js +21 -5
- package/dist/bridge/cli.cjs +4 -3
- package/dist/bridge/cli.js +3 -2
- package/dist/bridge/index.cjs +4 -3
- package/dist/bridge/index.js +3 -2
- package/dist/{chunk-KBNCEZZB.cjs → chunk-5NRD76T7.cjs} +9 -7
- package/dist/{chunk-SPPDMY7T.js → chunk-6LWEL5DO.js} +4 -2
- package/dist/{chunk-RTPBM46F.cjs → chunk-7TD63DZP.cjs} +1 -294
- package/dist/{chunk-ABHOYBZD.js → chunk-K4BGRCSH.js} +1 -294
- package/dist/chunk-SYEOA5MS.js +301 -0
- package/dist/{chunk-5DLCOQOQ.cjs → chunk-YBC3RKT2.cjs} +177 -16
- package/dist/chunk-ZEKI6EPI.cjs +301 -0
- package/dist/{chunk-UCYW4M66.js → chunk-ZQI3WUPP.js} +163 -2
- package/dist/paint-editor/contract.d.cts +11 -0
- package/dist/paint-editor/contract.d.ts +11 -0
- package/dist/paint-editor/controller.d.cts +17 -0
- package/dist/paint-editor/controller.d.ts +17 -0
- package/dist/paint-editor/index.cjs +245 -84
- package/dist/paint-editor/index.d.cts +2 -1
- package/dist/paint-editor/index.d.ts +2 -1
- package/dist/paint-editor/index.js +163 -2
- package/dist/paint-editor/path-samples.d.cts +19 -0
- package/dist/paint-editor/path-samples.d.ts +19 -0
- package/dist/paint-editor/ui.d.cts +6 -0
- package/dist/paint-editor/ui.d.ts +6 -0
- package/dist/tools/conformance.d.cts +138 -0
- package/dist/tools/conformance.d.ts +138 -0
- package/dist/tools/index.cjs +19 -3
- package/dist/tools/index.d.cts +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +21 -5
- package/package.json +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +2 -0
- package/skills/pixerate-paint-sketch-to-finish/SKILL.md +5 -1
|
@@ -57,4 +57,5 @@ export type { StickerProvider, StickerRef, StickerSummary } from '../stickers/ty
|
|
|
57
57
|
export * from './sticker-art.cjs';
|
|
58
58
|
export * from './collaboration.cjs';
|
|
59
59
|
export * from './collaboration-tools.cjs';
|
|
60
|
-
export {
|
|
60
|
+
export { samplePath, MAX_PATH_SAMPLES, type PathSampleOptions } from './path-samples.cjs';
|
|
61
|
+
export { createPaintConformanceHost, paintContracts, PAINT_CONTRACT_FORMAT_VERSION, PAINT_DOCUMENT_JSON_SCHEMA } from './contract.cjs';
|
|
@@ -57,4 +57,5 @@ export type { StickerProvider, StickerRef, StickerSummary } from '../stickers/ty
|
|
|
57
57
|
export * from './sticker-art.js';
|
|
58
58
|
export * from './collaboration.js';
|
|
59
59
|
export * from './collaboration-tools.js';
|
|
60
|
-
export {
|
|
60
|
+
export { samplePath, MAX_PATH_SAMPLES, type PathSampleOptions } from './path-samples.js';
|
|
61
|
+
export { createPaintConformanceHost, paintContracts, PAINT_CONTRACT_FORMAT_VERSION, PAINT_DOCUMENT_JSON_SCHEMA } from './contract.js';
|
|
@@ -19,6 +19,9 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
toVideoFfmpeg
|
|
21
21
|
} from "../chunk-MWUZCEFT.js";
|
|
22
|
+
import {
|
|
23
|
+
ToolRegistry
|
|
24
|
+
} from "../chunk-K4BGRCSH.js";
|
|
22
25
|
import {
|
|
23
26
|
BRUSH_OPTION_CONTROLS,
|
|
24
27
|
createLayerEffectTools,
|
|
@@ -150,7 +153,11 @@ import {
|
|
|
150
153
|
stickerIdFor
|
|
151
154
|
} from "../chunk-APX63ILU.js";
|
|
152
155
|
import "../chunk-M6DNZNVY.js";
|
|
153
|
-
import
|
|
156
|
+
import {
|
|
157
|
+
bezierPathToSvg,
|
|
158
|
+
flattenChain,
|
|
159
|
+
pathToContours
|
|
160
|
+
} from "../chunk-WEIK3UYB.js";
|
|
154
161
|
import "../chunk-PWBZH3FK.js";
|
|
155
162
|
import {
|
|
156
163
|
brushColourHex
|
|
@@ -274,7 +281,8 @@ import "../chunk-VDGBHYC7.js";
|
|
|
274
281
|
import {
|
|
275
282
|
EDITOR_DOCUMENT_FORMAT,
|
|
276
283
|
createEditorAssetRegistry,
|
|
277
|
-
defineEditorDocument
|
|
284
|
+
defineEditorDocument,
|
|
285
|
+
toEditorDocumentFile
|
|
278
286
|
} from "../chunk-XBQX76CT.js";
|
|
279
287
|
import "../chunk-HF5QZS7Z.js";
|
|
280
288
|
import "../chunk-M2TPMYJE.js";
|
|
@@ -5221,6 +5229,55 @@ function engineChanges(base, next) {
|
|
|
5221
5229
|
return rest;
|
|
5222
5230
|
}
|
|
5223
5231
|
|
|
5232
|
+
// src/paint-editor/path-samples.ts
|
|
5233
|
+
var FLATTEN_TOLERANCE = 0.1;
|
|
5234
|
+
var MAX_PATH_SAMPLES = 2e4;
|
|
5235
|
+
function profileAt(profile, t) {
|
|
5236
|
+
if (profile.length === 1) return profile[0];
|
|
5237
|
+
const at = t * (profile.length - 1);
|
|
5238
|
+
const i = Math.min(profile.length - 2, Math.floor(at));
|
|
5239
|
+
return profile[i] + (profile[i + 1] - profile[i]) * (at - i);
|
|
5240
|
+
}
|
|
5241
|
+
function samplePath(d, options = {}) {
|
|
5242
|
+
const spacing = options.spacing ?? 2;
|
|
5243
|
+
if (!(spacing >= 0.25 && spacing <= 100)) throw new Error(`spacing ${spacing} is out of range; use 0.25..100 document pixels.`);
|
|
5244
|
+
const profile = options.pressure;
|
|
5245
|
+
if (profile && (!profile.length || profile.some((p) => !(p >= 0 && p <= 1)))) throw new Error("pressure needs one or more values from 0 to 1, e.g. [0.2, 1, 0.2] for a taper.");
|
|
5246
|
+
let contours;
|
|
5247
|
+
try {
|
|
5248
|
+
contours = pathToContours(d);
|
|
5249
|
+
} catch (e) {
|
|
5250
|
+
throw new Error(`path is not SVG path data (${e instanceof Error ? e.message : String(e)}); give e.g. "M 10 10 C 40 0 80 60 120 30".`);
|
|
5251
|
+
}
|
|
5252
|
+
const out = [];
|
|
5253
|
+
for (const contour of contours) {
|
|
5254
|
+
if (!contour.segments.length) continue;
|
|
5255
|
+
const line = flattenChain(contour.segments, FLATTEN_TOLERANCE);
|
|
5256
|
+
if (contour.closed && line.length) line.push(line[0]);
|
|
5257
|
+
const lengths = [0];
|
|
5258
|
+
for (let i = 1; i < line.length; i++) lengths.push(lengths[i - 1] + Math.hypot(line[i][0] - line[i - 1][0], line[i][1] - line[i - 1][1]));
|
|
5259
|
+
const total = lengths[lengths.length - 1];
|
|
5260
|
+
const n = Math.max(1, Math.ceil(total / spacing));
|
|
5261
|
+
if (n + 1 > MAX_PATH_SAMPLES) throw new Error(`The path is ${Math.round(total)} px long: too many samples at spacing ${spacing}; use a larger spacing (at least ${Math.ceil(total / (MAX_PATH_SAMPLES - 1))}).`);
|
|
5262
|
+
const points = [];
|
|
5263
|
+
let j = 0;
|
|
5264
|
+
for (let k = 0; k <= n; k++) {
|
|
5265
|
+
const at = total * k / n;
|
|
5266
|
+
while (j < line.length - 2 && lengths[j + 1] < at) j++;
|
|
5267
|
+
const span = lengths[j + 1] - lengths[j] || 1;
|
|
5268
|
+
const f = line.length > 1 ? Math.min(1, Math.max(0, (at - lengths[j]) / span)) : 0;
|
|
5269
|
+
const a = line[j];
|
|
5270
|
+
const b = line[Math.min(j + 1, line.length - 1)];
|
|
5271
|
+
const x = Math.round((a[0] + (b[0] - a[0]) * f) * 1e3) / 1e3;
|
|
5272
|
+
const y = Math.round((a[1] + (b[1] - a[1]) * f) * 1e3) / 1e3;
|
|
5273
|
+
points.push(profile ? { x, y, pressure: Math.round(profileAt(profile, k / n) * 1e3) / 1e3 } : { x, y });
|
|
5274
|
+
}
|
|
5275
|
+
out.push(points);
|
|
5276
|
+
}
|
|
5277
|
+
if (!out.length) throw new Error('path has no segments to brush; give SVG path data such as "M 10 10 L 200 80".');
|
|
5278
|
+
return out;
|
|
5279
|
+
}
|
|
5280
|
+
|
|
5224
5281
|
// src/paint-editor/live-strokes.ts
|
|
5225
5282
|
var LIVE_STROKE_ENGINES = ["dab", "particle", "bristle", "mixer"];
|
|
5226
5283
|
function liveStrokeRefusal(brush) {
|
|
@@ -7857,6 +7914,47 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7857
7914
|
else this.emit([]);
|
|
7858
7915
|
return result;
|
|
7859
7916
|
}
|
|
7917
|
+
/**
|
|
7918
|
+
* Brushes vector path data (SVG `d`, document pixels) with the active brush, or `brushId` /
|
|
7919
|
+
* `brush`, as if drawn by hand: one recorded stroke per subpath, sampled every `spacing` px
|
|
7920
|
+
* with an optional `pressure` profile along each. All of it is one undo step. A given `seed`
|
|
7921
|
+
* seeds the first subpath; the next ones take the following seeds.
|
|
7922
|
+
*/
|
|
7923
|
+
strokePath(init) {
|
|
7924
|
+
this.requireIdle("painting");
|
|
7925
|
+
if (typeof init.path !== "string" || !init.path.trim()) throw new PaintEditorError('path is empty; give SVG path data in document pixels, e.g. "M 10 10 C 40 0 80 60 120 30".');
|
|
7926
|
+
let subpaths;
|
|
7927
|
+
try {
|
|
7928
|
+
subpaths = samplePath(init.path, init);
|
|
7929
|
+
} catch (e) {
|
|
7930
|
+
throw new PaintEditorError(e instanceof Error ? e.message : String(e));
|
|
7931
|
+
}
|
|
7932
|
+
const { path: _path, spacing: _spacing, pressure: _pressure, ...stroke } = init;
|
|
7933
|
+
const before = this.doc;
|
|
7934
|
+
let doc = this.doc;
|
|
7935
|
+
let bounds = null;
|
|
7936
|
+
let dabs = 0;
|
|
7937
|
+
const strokeIds = [];
|
|
7938
|
+
let last = null;
|
|
7939
|
+
try {
|
|
7940
|
+
subpaths.forEach((points, i) => {
|
|
7941
|
+
last = this.runStroke(doc, { ...stroke, ...stroke.seed !== void 0 ? { seed: stroke.seed + i } : {}, start: points[0] }, points);
|
|
7942
|
+
doc = last.doc;
|
|
7943
|
+
bounds = unionRect(bounds, last.result.bounds);
|
|
7944
|
+
dabs += last.result.dabs;
|
|
7945
|
+
if (last.result.strokeId) strokeIds.push(last.result.strokeId);
|
|
7946
|
+
});
|
|
7947
|
+
} catch (e) {
|
|
7948
|
+
this.wetOp = null;
|
|
7949
|
+
throw e;
|
|
7950
|
+
}
|
|
7951
|
+
const run = last;
|
|
7952
|
+
if (run?.result.mixerLoad) this.keepLoad(run.brush, run.result.mixerLoad);
|
|
7953
|
+
if (doc !== before || this.wetOp) this.commit(`${run?.brush.name ?? "Brush"} path stroke`, this.withWetEvents(doc), before);
|
|
7954
|
+
else this.emit([]);
|
|
7955
|
+
const layerId = run?.result.layerId ?? stroke.layerId ?? this.doc.activeLayerId ?? "";
|
|
7956
|
+
return { layerId, bounds, dabs, strokeIds, ...run?.result.mixerLoad ? { mixerLoad: run.result.mixerLoad } : {}, ...run?.result.wet !== void 0 ? { wet: run.result.wet } : {} };
|
|
7957
|
+
}
|
|
7860
7958
|
/** Checks stroke samples (finite x and y, pressure 0..1) and returns them; throws a sentence naming the bad point. */
|
|
7861
7959
|
checkPoints(points) {
|
|
7862
7960
|
if (!Array.isArray(points) || !points.length) throw new PaintEditorError("points is empty; give at least one {x, y} in document pixels.");
|
|
@@ -11579,6 +11677,41 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11579
11677
|
return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
|
|
11580
11678
|
}
|
|
11581
11679
|
});
|
|
11680
|
+
add({
|
|
11681
|
+
name: "stroke_path",
|
|
11682
|
+
title: "Brush along a path",
|
|
11683
|
+
description: "Brushes vector path data (SVG path d in document pixels: M, L, H, V, C, S, Q, T, A and Z) on a pixel layer with a library brush or the active one, as if drawn by hand along it: the outline of a shape, a lettering guide, a curve from a vector tool. Each subpath becomes one recorded stroke, sampled every spacing px, with an optional pressure profile spread along each subpath (a taper such as [0.2, 1, 0.2]); closed subpaths end where they start. Brushes behave as in paint_stroke (grain, pick-up, wet and thick paint, guides off). One undo step for the whole path. Returns the layer id, changed bounds, dab count and the recorded strokeIds (for replay_strokes, update_stroke).",
|
|
11684
|
+
inputSchema: {
|
|
11685
|
+
type: "object",
|
|
11686
|
+
properties: {
|
|
11687
|
+
path: { type: "string", minLength: 1, description: 'SVG path data in document pixels, e.g. "M 40 200 C 120 40 280 40 360 200". Several subpaths paint several strokes.' },
|
|
11688
|
+
spacing: { type: "number", minimum: 0.25, maximum: 100, description: "Distance between samples along the path in document pixels (default 2); the brush spaces its own dabs, so this only sets how closely curves are followed." },
|
|
11689
|
+
pressure: { type: "array", minItems: 1, maxItems: 64, items: { type: "number", minimum: 0, maximum: 1 }, description: "Pen pressure along each subpath, spread evenly from start to end (default full pressure); [0.2, 1, 0.2] tapers both ends." },
|
|
11690
|
+
layerId: LAYER_ID4,
|
|
11691
|
+
brushId: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." },
|
|
11692
|
+
brush: { type: "object", description: "Facets to change for this stroke only, laid over brushId or the active brush (as set_brush takes them).", properties: BRUSH_PATCH_PROPERTIES, additionalProperties: false },
|
|
11693
|
+
color: { ...COLOR3, description: "Paint colour #rrggbb (default the paint colour)." },
|
|
11694
|
+
paint: { ...PAINT_SOURCE_JSON_SCHEMA, description: "What the strokes paint with instead of the editor's paint: a colour, or gradient paint that changes colour as each stroke goes." },
|
|
11695
|
+
seed: { type: "integer", minimum: 0, description: "Jitter seed of the first subpath, the next ones take the following seeds (default new ones); the same seed and path give the same marks." }
|
|
11696
|
+
},
|
|
11697
|
+
required: ["path"],
|
|
11698
|
+
additionalProperties: false
|
|
11699
|
+
},
|
|
11700
|
+
annotations: { title: "Brush along a path", ...WRITE5 },
|
|
11701
|
+
coordinates: { out: ["bounds"] },
|
|
11702
|
+
handler: async (a) => {
|
|
11703
|
+
await c.wetIdle();
|
|
11704
|
+
const base = a.brushId ? c.getBrush(a.brushId) : c.getState().brush;
|
|
11705
|
+
const brush = a.brush ? c.brushChanges(a.brush, a.brushId) : void 0;
|
|
11706
|
+
if (base) {
|
|
11707
|
+
const refs = brushStickerRefs(normalizePaintBrush({ ...brush ?? {}, id: base.id }, base));
|
|
11708
|
+
if (refs.length) await c.prepareStickers(refs);
|
|
11709
|
+
}
|
|
11710
|
+
const path = options.origin && typeof a.path === "string" && a.path.trim() ? bezierPathToSvg(a.path, { offset: { x: -options.origin.x, y: -options.origin.y } }) : a.path;
|
|
11711
|
+
const r = c.strokePath({ path, spacing: a.spacing, pressure: a.pressure, layerId: a.layerId, brushId: a.brushId, brush, color: a.color, paint: a.paint, seed: a.seed });
|
|
11712
|
+
return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
|
|
11713
|
+
}
|
|
11714
|
+
});
|
|
11582
11715
|
add({
|
|
11583
11716
|
name: "get_live_stroke",
|
|
11584
11717
|
title: "Get the stroke being painted",
|
|
@@ -12480,6 +12613,12 @@ function toggleDotTexture(textures, stamp) {
|
|
|
12480
12613
|
if (textures.includes(stamp)) return textures.filter((s) => s !== stamp);
|
|
12481
12614
|
return textures.length < MAX_DOT_TEXTURES ? [...textures, stamp] : [...textures];
|
|
12482
12615
|
}
|
|
12616
|
+
var PATH_STROKE_TAPERS = [
|
|
12617
|
+
{ id: "even", label: "Even" },
|
|
12618
|
+
{ id: "both", label: "Taper both ends", pressure: [0.2, 1, 0.2] },
|
|
12619
|
+
{ id: "in", label: "Fade in", pressure: [0.2, 1] },
|
|
12620
|
+
{ id: "out", label: "Fade out", pressure: [1, 0.2] }
|
|
12621
|
+
];
|
|
12483
12622
|
var STROKE_SLIDERS = [
|
|
12484
12623
|
{ path: "tip.size", label: "Size", min: 1, max: 400, step: 1 },
|
|
12485
12624
|
{ path: "tip.opacity", label: "Opacity", min: 0, max: 1, step: 0.01 },
|
|
@@ -13270,6 +13409,7 @@ function uiTables() {
|
|
|
13270
13409
|
blendMenu: BLEND_MENU_GROUPS,
|
|
13271
13410
|
layerEffects: LAYER_EFFECT_MENU.map(({ kind, label }) => ({ kind, label, controls: layerEffectControls(defaultLayerEffect(kind, kind)) })),
|
|
13272
13411
|
sliders: { paper: PAPER_SLIDERS, wet: WET_PARAM_SLIDERS, wetTilt: WET_TILT_SLIDERS, wetSpeed: WET_SPEED_SLIDER, light: LIGHT_SLIDERS, stroke: STROKE_SLIDERS, sticker: STICKER_SLIDERS },
|
|
13412
|
+
pathStrokeTapers: PATH_STROKE_TAPERS,
|
|
13273
13413
|
padSampleSizes: PAD_SAMPLE_SIZES,
|
|
13274
13414
|
wetGpu: WET_GPU_OPTIONS,
|
|
13275
13415
|
labels: {
|
|
@@ -13310,6 +13450,23 @@ function paintContracts(options) {
|
|
|
13310
13450
|
};
|
|
13311
13451
|
return files;
|
|
13312
13452
|
}
|
|
13453
|
+
var GENERATED_ID = /^(pl[0-9a-z]{9,}|[a-z]+_[0-9a-z]{8,}_[0-9a-z]+)$/;
|
|
13454
|
+
function createPaintConformanceHost(setup, options) {
|
|
13455
|
+
const controller = new PaintEditorController({ ...setup, wet: { kernel: "typescript", threads: 0, gpu: "off", ...setup.wet } });
|
|
13456
|
+
controller.setEffectsProvider(options.effects);
|
|
13457
|
+
const registry = new ToolRegistry({ validateArguments: true });
|
|
13458
|
+
registry.register(createPaintEditorToolProvider(controller));
|
|
13459
|
+
registry.register(createPaintEffectsToolProvider(controller));
|
|
13460
|
+
registry.register(createPaintBrushLibraryToolProvider(controller));
|
|
13461
|
+
registry.register(createPaintProposalToolProvider(createPaintProposals({ controller })));
|
|
13462
|
+
return {
|
|
13463
|
+
callTool: (name, args) => registry.callTool(name, args),
|
|
13464
|
+
document: () => toEditorDocumentFile(createPaintEditorDocument(controller)),
|
|
13465
|
+
pixels: ({ paper }) => controller.compositePixels({ paper }),
|
|
13466
|
+
isGeneratedId: (value) => GENERATED_ID.test(value),
|
|
13467
|
+
dispose: () => controller.dispose()
|
|
13468
|
+
};
|
|
13469
|
+
}
|
|
13313
13470
|
export {
|
|
13314
13471
|
BACKLIGHT_SPILL,
|
|
13315
13472
|
BLEND_MENU_GROUPS,
|
|
@@ -13364,6 +13521,7 @@ export {
|
|
|
13364
13521
|
MAX_DOT_TEXTURES,
|
|
13365
13522
|
MAX_IMAGE_SIDE,
|
|
13366
13523
|
MAX_LIVE_EFFECT_SOURCES,
|
|
13524
|
+
MAX_PATH_SAMPLES,
|
|
13367
13525
|
MAX_STICKER_STAMPS,
|
|
13368
13526
|
MAX_ZOOM,
|
|
13369
13527
|
MIN_CANVAS_SIDE,
|
|
@@ -13407,6 +13565,7 @@ export {
|
|
|
13407
13565
|
PARTICLE_KIND_LABELS,
|
|
13408
13566
|
PARTICLE_RENDERS,
|
|
13409
13567
|
PARTICLE_RENDER_LABELS,
|
|
13568
|
+
PATH_STROKE_TAPERS,
|
|
13410
13569
|
PaintCanvasPresenter,
|
|
13411
13570
|
PaintDabStroke,
|
|
13412
13571
|
PaintDirector,
|
|
@@ -13477,6 +13636,7 @@ export {
|
|
|
13477
13636
|
createPaintBrushLibraryToolProvider,
|
|
13478
13637
|
createPaintChangeFeed,
|
|
13479
13638
|
createPaintCompositor,
|
|
13639
|
+
createPaintConformanceHost,
|
|
13480
13640
|
createPaintDocument,
|
|
13481
13641
|
createPaintEditorDocument,
|
|
13482
13642
|
createPaintEditorToolProvider,
|
|
@@ -13617,6 +13777,7 @@ export {
|
|
|
13617
13777
|
sampleArt,
|
|
13618
13778
|
sampleEffectAt,
|
|
13619
13779
|
sampleEffectField,
|
|
13780
|
+
samplePath,
|
|
13620
13781
|
samplePressure,
|
|
13621
13782
|
scalePaintGradient,
|
|
13622
13783
|
scaleStencil,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pen samples along vector path data, so a path can be brushed like a hand-drawn stroke
|
|
3
|
+
* (`strokePath`, the `stroke_path` tool): each subpath becomes one stroke, sampled at an even
|
|
4
|
+
* spacing, with an optional pressure profile spread along its length (a taper).
|
|
5
|
+
*/
|
|
6
|
+
import type { PenPoint } from './engine.cjs';
|
|
7
|
+
/** The most samples one subpath may have (the stroke tools' point limit). */
|
|
8
|
+
export declare const MAX_PATH_SAMPLES = 20000;
|
|
9
|
+
export interface PathSampleOptions {
|
|
10
|
+
/** Distance between samples in document pixels (default 2). */
|
|
11
|
+
spacing?: number;
|
|
12
|
+
/** Pressure along each subpath, spread evenly from its start to its end (default full pressure). */
|
|
13
|
+
pressure?: readonly number[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Samples SVG path data (document pixels) into pen points, one array per subpath. Closed
|
|
17
|
+
* subpaths end where they start. Throws a sentence when the path has nothing to brush.
|
|
18
|
+
*/
|
|
19
|
+
export declare function samplePath(d: string, options?: PathSampleOptions): PenPoint[][];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pen samples along vector path data, so a path can be brushed like a hand-drawn stroke
|
|
3
|
+
* (`strokePath`, the `stroke_path` tool): each subpath becomes one stroke, sampled at an even
|
|
4
|
+
* spacing, with an optional pressure profile spread along its length (a taper).
|
|
5
|
+
*/
|
|
6
|
+
import type { PenPoint } from './engine.js';
|
|
7
|
+
/** The most samples one subpath may have (the stroke tools' point limit). */
|
|
8
|
+
export declare const MAX_PATH_SAMPLES = 20000;
|
|
9
|
+
export interface PathSampleOptions {
|
|
10
|
+
/** Distance between samples in document pixels (default 2). */
|
|
11
|
+
spacing?: number;
|
|
12
|
+
/** Pressure along each subpath, spread evenly from its start to its end (default full pressure). */
|
|
13
|
+
pressure?: readonly number[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Samples SVG path data (document pixels) into pen points, one array per subpath. Closed
|
|
17
|
+
* subpaths end where they start. Throws a sentence when the path has nothing to brush.
|
|
18
|
+
*/
|
|
19
|
+
export declare function samplePath(d: string, options?: PathSampleOptions): PenPoint[][];
|
|
@@ -250,6 +250,12 @@ export declare function toggleStamp(stamps: readonly StampKind[], stamp: StampKi
|
|
|
250
250
|
* else adds it while there are fewer than `MAX_DOT_TEXTURES`. Empty means round dots.
|
|
251
251
|
*/
|
|
252
252
|
export declare function toggleDotTexture(textures: readonly StampKind[], stamp: StampKind): StampKind[];
|
|
253
|
+
/** Pressure profiles the "Brush a path" panel offers for `strokePath` (`pressure` undefined: even). */
|
|
254
|
+
export declare const PATH_STROKE_TAPERS: ReadonlyArray<{
|
|
255
|
+
id: string;
|
|
256
|
+
label: string;
|
|
257
|
+
pressure?: readonly number[];
|
|
258
|
+
}>;
|
|
253
259
|
/** The sticker panel's sliders for the selected sticker. */
|
|
254
260
|
/** The brush sliders of a picked stroke on a stroke layer (paths into its brush; see `updateStroke`). */
|
|
255
261
|
export declare const STROKE_SLIDERS: readonly SliderControl[];
|
|
@@ -250,6 +250,12 @@ export declare function toggleStamp(stamps: readonly StampKind[], stamp: StampKi
|
|
|
250
250
|
* else adds it while there are fewer than `MAX_DOT_TEXTURES`. Empty means round dots.
|
|
251
251
|
*/
|
|
252
252
|
export declare function toggleDotTexture(textures: readonly StampKind[], stamp: StampKind): StampKind[];
|
|
253
|
+
/** Pressure profiles the "Brush a path" panel offers for `strokePath` (`pressure` undefined: even). */
|
|
254
|
+
export declare const PATH_STROKE_TAPERS: ReadonlyArray<{
|
|
255
|
+
id: string;
|
|
256
|
+
label: string;
|
|
257
|
+
pressure?: readonly number[];
|
|
258
|
+
}>;
|
|
253
259
|
/** The sticker panel's sliders for the selected sticker. */
|
|
254
260
|
/** The brush sliders of a picked stroke on a stroke layer (paths into its brush; see `updateStroke`). */
|
|
255
261
|
export declare const STROKE_SLIDERS: readonly SliderControl[];
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conformance scenarios: a recorded sequence of tool calls with what each one returned, so
|
|
3
|
+
* another implementation of an editor (the Swift port, #533) can replay the same calls and check
|
|
4
|
+
* it gets the same results, documents and pixels. `@pixerate/conformance` holds the scenarios
|
|
5
|
+
* (`domains/<domain>/fixtures/*.json`); `scripts/record-conformance.mjs` records them from this
|
|
6
|
+
* core and a core test replays them.
|
|
7
|
+
*
|
|
8
|
+
* Results are compared after {@link normalizeConformanceValue}: ids an implementation generates
|
|
9
|
+
* (from a clock, say) become `@1`, `@2`, … in the order they are first seen, and binary payloads
|
|
10
|
+
* become `"<binary>"`, so neither the id format nor the PNG encoder is part of the contract. Ids
|
|
11
|
+
* that come from counters or names (`s1`, `pad1`, `ink-pen`) stay as they are. Every implementation applies the same rules (written
|
|
12
|
+
* out in the package README).
|
|
13
|
+
*/
|
|
14
|
+
import type { McpToolResult } from './registry.cjs';
|
|
15
|
+
/** One tool call of a scenario and, once recorded, what it returned. */
|
|
16
|
+
export interface ConformanceStep {
|
|
17
|
+
/** Tool name, unprefixed (`paint_stroke`). */
|
|
18
|
+
tool: string;
|
|
19
|
+
/** Arguments. A string `@n` is replaced by the id that `@n` stands for. */
|
|
20
|
+
arguments?: Record<string, unknown>;
|
|
21
|
+
/** Also compare the saved document after this call. */
|
|
22
|
+
document?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Also compare the rendered pixels after this call (`paper: false` leaves the paper out;
|
|
25
|
+
* `tolerance` loosens what another implementation is held to for this snapshot).
|
|
26
|
+
*/
|
|
27
|
+
pixels?: boolean | {
|
|
28
|
+
paper?: boolean;
|
|
29
|
+
tolerance?: {
|
|
30
|
+
maxDelta?: number;
|
|
31
|
+
meanDelta?: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Parts of the result left out of the comparison because they describe this implementation
|
|
36
|
+
* rather than the contract (the wet engine's backend): JSON pointers like `/backend`, where a
|
|
37
|
+
* `*` segment stands for every array element or object member.
|
|
38
|
+
*/
|
|
39
|
+
ignore?: string[];
|
|
40
|
+
/** Recorded results, written by the recorder. */
|
|
41
|
+
expect?: ConformanceExpectation;
|
|
42
|
+
}
|
|
43
|
+
export interface ConformanceExpectation {
|
|
44
|
+
/** The normalized `structuredContent`, or `{ error: { code, message, details? } }` for a failed call. */
|
|
45
|
+
result: unknown;
|
|
46
|
+
isError?: true;
|
|
47
|
+
document?: unknown;
|
|
48
|
+
pixels?: ConformancePixels;
|
|
49
|
+
}
|
|
50
|
+
/** A pixel snapshot: the PNG next to the scenario plus how close another implementation must be. */
|
|
51
|
+
export interface ConformancePixels {
|
|
52
|
+
/** PNG file, relative to the scenario file. */
|
|
53
|
+
file: string;
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
/** SHA-256 of the RGBA bytes, hex. This core matches it exactly. */
|
|
57
|
+
sha256: string;
|
|
58
|
+
/** Largest difference allowed in any channel of any pixel (0–255). */
|
|
59
|
+
maxDelta: number;
|
|
60
|
+
/** Largest mean difference allowed over all channels. */
|
|
61
|
+
meanDelta: number;
|
|
62
|
+
}
|
|
63
|
+
export interface ConformanceScenario {
|
|
64
|
+
$schema?: string;
|
|
65
|
+
/** Unique kebab-case id within the domain; also the file name. */
|
|
66
|
+
id: string;
|
|
67
|
+
description: string;
|
|
68
|
+
/** Host options, such as the canvas size. */
|
|
69
|
+
setup?: Record<string, unknown>;
|
|
70
|
+
steps: ConformanceStep[];
|
|
71
|
+
/** Reasons an implementation skips the whole scenario, by implementation (`swift`). */
|
|
72
|
+
skip?: Record<string, string>;
|
|
73
|
+
}
|
|
74
|
+
/** What a scenario runs against: a tool registry plus ways to read the document and pixels. */
|
|
75
|
+
export interface ConformanceHost {
|
|
76
|
+
callTool(name: string, args: Record<string, unknown>): Promise<McpToolResult>;
|
|
77
|
+
document(): unknown;
|
|
78
|
+
pixels(options: {
|
|
79
|
+
paper?: boolean;
|
|
80
|
+
}): {
|
|
81
|
+
width: number;
|
|
82
|
+
height: number;
|
|
83
|
+
data: Uint8Array | Uint8ClampedArray;
|
|
84
|
+
};
|
|
85
|
+
/** Whether a string is an id this implementation generated in its own format (default: none). */
|
|
86
|
+
isGeneratedId?(value: string): boolean;
|
|
87
|
+
dispose?(): void;
|
|
88
|
+
}
|
|
89
|
+
/** What one step produced, normalized (pixels are raw RGBA for the caller to hash or encode). */
|
|
90
|
+
export interface ConformanceStepOutput {
|
|
91
|
+
result: unknown;
|
|
92
|
+
isError: boolean;
|
|
93
|
+
document?: unknown;
|
|
94
|
+
pixels?: {
|
|
95
|
+
width: number;
|
|
96
|
+
height: number;
|
|
97
|
+
data: Uint8Array;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Maps real ids to `@n` placeholders and back, across a whole scenario. */
|
|
101
|
+
export declare class ConformanceIds {
|
|
102
|
+
private readonly toPlaceholder;
|
|
103
|
+
private readonly fromPlaceholder;
|
|
104
|
+
/** The placeholder for `id`, assigning the next one when it is new. */
|
|
105
|
+
add(id: string): string;
|
|
106
|
+
placeholder(id: string): string | undefined;
|
|
107
|
+
/** `text` with every known id in it replaced by its placeholder (longest ids first). */
|
|
108
|
+
replaceWithin(text: string): string;
|
|
109
|
+
/** The real id a placeholder stands for. */
|
|
110
|
+
resolve(placeholder: string): string | undefined;
|
|
111
|
+
}
|
|
112
|
+
/** Strings at least this long made only of base64 characters are binary payloads. */
|
|
113
|
+
export declare const CONFORMANCE_BINARY_MIN_LENGTH = 200;
|
|
114
|
+
type IdTest = (value: string) => boolean;
|
|
115
|
+
/**
|
|
116
|
+
* A value as scenarios store it:
|
|
117
|
+
* 1. Every string value or object key the implementation generated as an id (`isGeneratedId`) is
|
|
118
|
+
* numbered `@1`, `@2`, … in the order first seen, walking depth first with object keys sorted
|
|
119
|
+
* (each key before its value); the numbering carries across the scenario's steps.
|
|
120
|
+
* 2. Every known id in a string or object key, whole or inside a longer string (an error message),
|
|
121
|
+
* becomes its placeholder; longer ids are replaced first.
|
|
122
|
+
* 3. A string starting with `data:`, or at least 200 characters of base64, becomes `"<binary>"`.
|
|
123
|
+
* 4. An ISO 8601 date-time string (`2026-11-04T01:06:52.993Z`) becomes `"<time>"`: clocks differ.
|
|
124
|
+
* 5. Object keys are sorted; `undefined` values are dropped (as JSON would).
|
|
125
|
+
*/
|
|
126
|
+
export declare function normalizeConformanceValue(value: unknown, ids: ConformanceIds, isGeneratedId?: IdTest): unknown;
|
|
127
|
+
/** `value` without the members at `paths` (`/a/b`, `*` for every element or member). */
|
|
128
|
+
export declare function omitConformancePaths(value: unknown, paths: readonly string[]): unknown;
|
|
129
|
+
/** Arguments with each `@n` placeholder string replaced by the id it stands for. */
|
|
130
|
+
export declare function resolveConformanceArguments(value: unknown, ids: ConformanceIds): unknown;
|
|
131
|
+
/** The comparable part of a tool result: `structuredContent`, or the error's code, message and details. */
|
|
132
|
+
export declare function conformanceResult(result: McpToolResult): {
|
|
133
|
+
value: unknown;
|
|
134
|
+
isError: boolean;
|
|
135
|
+
};
|
|
136
|
+
/** Runs a scenario's calls in order against a fresh host and returns what each step produced. */
|
|
137
|
+
export declare function runConformanceScenario(scenario: ConformanceScenario, host: ConformanceHost): Promise<ConformanceStepOutput[]>;
|
|
138
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conformance scenarios: a recorded sequence of tool calls with what each one returned, so
|
|
3
|
+
* another implementation of an editor (the Swift port, #533) can replay the same calls and check
|
|
4
|
+
* it gets the same results, documents and pixels. `@pixerate/conformance` holds the scenarios
|
|
5
|
+
* (`domains/<domain>/fixtures/*.json`); `scripts/record-conformance.mjs` records them from this
|
|
6
|
+
* core and a core test replays them.
|
|
7
|
+
*
|
|
8
|
+
* Results are compared after {@link normalizeConformanceValue}: ids an implementation generates
|
|
9
|
+
* (from a clock, say) become `@1`, `@2`, … in the order they are first seen, and binary payloads
|
|
10
|
+
* become `"<binary>"`, so neither the id format nor the PNG encoder is part of the contract. Ids
|
|
11
|
+
* that come from counters or names (`s1`, `pad1`, `ink-pen`) stay as they are. Every implementation applies the same rules (written
|
|
12
|
+
* out in the package README).
|
|
13
|
+
*/
|
|
14
|
+
import type { McpToolResult } from './registry.js';
|
|
15
|
+
/** One tool call of a scenario and, once recorded, what it returned. */
|
|
16
|
+
export interface ConformanceStep {
|
|
17
|
+
/** Tool name, unprefixed (`paint_stroke`). */
|
|
18
|
+
tool: string;
|
|
19
|
+
/** Arguments. A string `@n` is replaced by the id that `@n` stands for. */
|
|
20
|
+
arguments?: Record<string, unknown>;
|
|
21
|
+
/** Also compare the saved document after this call. */
|
|
22
|
+
document?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Also compare the rendered pixels after this call (`paper: false` leaves the paper out;
|
|
25
|
+
* `tolerance` loosens what another implementation is held to for this snapshot).
|
|
26
|
+
*/
|
|
27
|
+
pixels?: boolean | {
|
|
28
|
+
paper?: boolean;
|
|
29
|
+
tolerance?: {
|
|
30
|
+
maxDelta?: number;
|
|
31
|
+
meanDelta?: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Parts of the result left out of the comparison because they describe this implementation
|
|
36
|
+
* rather than the contract (the wet engine's backend): JSON pointers like `/backend`, where a
|
|
37
|
+
* `*` segment stands for every array element or object member.
|
|
38
|
+
*/
|
|
39
|
+
ignore?: string[];
|
|
40
|
+
/** Recorded results, written by the recorder. */
|
|
41
|
+
expect?: ConformanceExpectation;
|
|
42
|
+
}
|
|
43
|
+
export interface ConformanceExpectation {
|
|
44
|
+
/** The normalized `structuredContent`, or `{ error: { code, message, details? } }` for a failed call. */
|
|
45
|
+
result: unknown;
|
|
46
|
+
isError?: true;
|
|
47
|
+
document?: unknown;
|
|
48
|
+
pixels?: ConformancePixels;
|
|
49
|
+
}
|
|
50
|
+
/** A pixel snapshot: the PNG next to the scenario plus how close another implementation must be. */
|
|
51
|
+
export interface ConformancePixels {
|
|
52
|
+
/** PNG file, relative to the scenario file. */
|
|
53
|
+
file: string;
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
/** SHA-256 of the RGBA bytes, hex. This core matches it exactly. */
|
|
57
|
+
sha256: string;
|
|
58
|
+
/** Largest difference allowed in any channel of any pixel (0–255). */
|
|
59
|
+
maxDelta: number;
|
|
60
|
+
/** Largest mean difference allowed over all channels. */
|
|
61
|
+
meanDelta: number;
|
|
62
|
+
}
|
|
63
|
+
export interface ConformanceScenario {
|
|
64
|
+
$schema?: string;
|
|
65
|
+
/** Unique kebab-case id within the domain; also the file name. */
|
|
66
|
+
id: string;
|
|
67
|
+
description: string;
|
|
68
|
+
/** Host options, such as the canvas size. */
|
|
69
|
+
setup?: Record<string, unknown>;
|
|
70
|
+
steps: ConformanceStep[];
|
|
71
|
+
/** Reasons an implementation skips the whole scenario, by implementation (`swift`). */
|
|
72
|
+
skip?: Record<string, string>;
|
|
73
|
+
}
|
|
74
|
+
/** What a scenario runs against: a tool registry plus ways to read the document and pixels. */
|
|
75
|
+
export interface ConformanceHost {
|
|
76
|
+
callTool(name: string, args: Record<string, unknown>): Promise<McpToolResult>;
|
|
77
|
+
document(): unknown;
|
|
78
|
+
pixels(options: {
|
|
79
|
+
paper?: boolean;
|
|
80
|
+
}): {
|
|
81
|
+
width: number;
|
|
82
|
+
height: number;
|
|
83
|
+
data: Uint8Array | Uint8ClampedArray;
|
|
84
|
+
};
|
|
85
|
+
/** Whether a string is an id this implementation generated in its own format (default: none). */
|
|
86
|
+
isGeneratedId?(value: string): boolean;
|
|
87
|
+
dispose?(): void;
|
|
88
|
+
}
|
|
89
|
+
/** What one step produced, normalized (pixels are raw RGBA for the caller to hash or encode). */
|
|
90
|
+
export interface ConformanceStepOutput {
|
|
91
|
+
result: unknown;
|
|
92
|
+
isError: boolean;
|
|
93
|
+
document?: unknown;
|
|
94
|
+
pixels?: {
|
|
95
|
+
width: number;
|
|
96
|
+
height: number;
|
|
97
|
+
data: Uint8Array;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Maps real ids to `@n` placeholders and back, across a whole scenario. */
|
|
101
|
+
export declare class ConformanceIds {
|
|
102
|
+
private readonly toPlaceholder;
|
|
103
|
+
private readonly fromPlaceholder;
|
|
104
|
+
/** The placeholder for `id`, assigning the next one when it is new. */
|
|
105
|
+
add(id: string): string;
|
|
106
|
+
placeholder(id: string): string | undefined;
|
|
107
|
+
/** `text` with every known id in it replaced by its placeholder (longest ids first). */
|
|
108
|
+
replaceWithin(text: string): string;
|
|
109
|
+
/** The real id a placeholder stands for. */
|
|
110
|
+
resolve(placeholder: string): string | undefined;
|
|
111
|
+
}
|
|
112
|
+
/** Strings at least this long made only of base64 characters are binary payloads. */
|
|
113
|
+
export declare const CONFORMANCE_BINARY_MIN_LENGTH = 200;
|
|
114
|
+
type IdTest = (value: string) => boolean;
|
|
115
|
+
/**
|
|
116
|
+
* A value as scenarios store it:
|
|
117
|
+
* 1. Every string value or object key the implementation generated as an id (`isGeneratedId`) is
|
|
118
|
+
* numbered `@1`, `@2`, … in the order first seen, walking depth first with object keys sorted
|
|
119
|
+
* (each key before its value); the numbering carries across the scenario's steps.
|
|
120
|
+
* 2. Every known id in a string or object key, whole or inside a longer string (an error message),
|
|
121
|
+
* becomes its placeholder; longer ids are replaced first.
|
|
122
|
+
* 3. A string starting with `data:`, or at least 200 characters of base64, becomes `"<binary>"`.
|
|
123
|
+
* 4. An ISO 8601 date-time string (`2026-11-04T01:06:52.993Z`) becomes `"<time>"`: clocks differ.
|
|
124
|
+
* 5. Object keys are sorted; `undefined` values are dropped (as JSON would).
|
|
125
|
+
*/
|
|
126
|
+
export declare function normalizeConformanceValue(value: unknown, ids: ConformanceIds, isGeneratedId?: IdTest): unknown;
|
|
127
|
+
/** `value` without the members at `paths` (`/a/b`, `*` for every element or member). */
|
|
128
|
+
export declare function omitConformancePaths(value: unknown, paths: readonly string[]): unknown;
|
|
129
|
+
/** Arguments with each `@n` placeholder string replaced by the id it stands for. */
|
|
130
|
+
export declare function resolveConformanceArguments(value: unknown, ids: ConformanceIds): unknown;
|
|
131
|
+
/** The comparable part of a tool result: `structuredContent`, or the error's code, message and details. */
|
|
132
|
+
export declare function conformanceResult(result: McpToolResult): {
|
|
133
|
+
value: unknown;
|
|
134
|
+
isError: boolean;
|
|
135
|
+
};
|
|
136
|
+
/** Runs a scenario's calls in order against a fresh host and returns what each step produced. */
|
|
137
|
+
export declare function runConformanceScenario(scenario: ConformanceScenario, host: ConformanceHost): Promise<ConformanceStepOutput[]>;
|
|
138
|
+
export {};
|