@pixerate/editor 0.272.1 → 0.273.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/chunk-2A5IKZMU.js +557 -0
- package/dist/chunk-YH6SKC7P.cjs +557 -0
- package/dist/paint-editor/controller.d.cts +86 -0
- package/dist/paint-editor/controller.d.ts +86 -0
- package/dist/paint-editor/document.d.cts +1 -1
- package/dist/paint-editor/document.d.ts +1 -1
- package/dist/paint-editor/index.cjs +559 -166
- package/dist/paint-editor/index.d.cts +1 -0
- package/dist/paint-editor/index.d.ts +1 -0
- package/dist/paint-editor/index.js +397 -4
- package/dist/paint-editor/presence.d.cts +5 -1
- package/dist/paint-editor/presence.d.ts +5 -1
- package/dist/paint-editor/ui.d.cts +5 -2
- package/dist/paint-editor/ui.d.ts +5 -2
- package/dist/raster/fill.d.cts +85 -0
- package/dist/raster/fill.d.ts +85 -0
- package/dist/raster/index.cjs +9 -1
- package/dist/raster/index.d.cts +1 -0
- package/dist/raster/index.d.ts +1 -0
- package/dist/raster/index.js +8 -0
- package/package.json +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +19 -43
- package/skills/pixerate-paint-editor/references/evolution.md +27 -0
- package/skills/pixerate-paint-sketch-to-finish/SKILL.md +16 -5
- package/skills/pixerate-paint-sketch-to-finish/evals/flats-under-ink.json +2 -3
|
@@ -61,3 +61,4 @@ export * from './collaboration.cjs';
|
|
|
61
61
|
export * from './collaboration-tools.cjs';
|
|
62
62
|
export { samplePath, MAX_PATH_SAMPLES, type PathSampleOptions } from './path-samples.cjs';
|
|
63
63
|
export { createPaintConformanceHost, paintContracts, PAINT_CONTRACT_FORMAT_VERSION, PAINT_DOCUMENT_JSON_SCHEMA } from './contract.cjs';
|
|
64
|
+
export { squaredDistanceTransform, fillRegionMask, findUnfilledRegionsMask, type FillRegionPoint, type FillRegionSummary, type FillRegionMaskOptions, type FillRegionMaskResult, type UnfilledRegion, type FindUnfilledRegionsMaskOptions, } from '../raster/fill.cjs';
|
|
@@ -61,3 +61,4 @@ export * from './collaboration.js';
|
|
|
61
61
|
export * from './collaboration-tools.js';
|
|
62
62
|
export { samplePath, MAX_PATH_SAMPLES, type PathSampleOptions } from './path-samples.js';
|
|
63
63
|
export { createPaintConformanceHost, paintContracts, PAINT_CONTRACT_FORMAT_VERSION, PAINT_DOCUMENT_JSON_SCHEMA } from './contract.js';
|
|
64
|
+
export { squaredDistanceTransform, fillRegionMask, findUnfilledRegionsMask, type FillRegionPoint, type FillRegionSummary, type FillRegionMaskOptions, type FillRegionMaskResult, type UnfilledRegion, type FindUnfilledRegionsMaskOptions, } from '../raster/fill.js';
|
|
@@ -27,6 +27,11 @@ import {
|
|
|
27
27
|
import {
|
|
28
28
|
ToolRegistry
|
|
29
29
|
} from "../chunk-K4BGRCSH.js";
|
|
30
|
+
import {
|
|
31
|
+
fillRegionMask,
|
|
32
|
+
findUnfilledRegionsMask,
|
|
33
|
+
squaredDistanceTransform
|
|
34
|
+
} from "../chunk-2A5IKZMU.js";
|
|
30
35
|
import {
|
|
31
36
|
BRUSH_OPTION_CONTROLS,
|
|
32
37
|
createLayerEffectTools,
|
|
@@ -221,9 +226,11 @@ import {
|
|
|
221
226
|
DEFAULT_FLOW,
|
|
222
227
|
DEFAULT_WET_PARAMS,
|
|
223
228
|
MAX_WET_LANES,
|
|
229
|
+
PIGMENT_UNIT,
|
|
224
230
|
SRGB_TO_LINEAR,
|
|
225
231
|
WET_FLOW_KINDS,
|
|
226
232
|
WET_PARAM_KEYS,
|
|
233
|
+
WetMedium,
|
|
227
234
|
WetWorld,
|
|
228
235
|
linearToSrgb8,
|
|
229
236
|
normalizeFlow,
|
|
@@ -2927,7 +2934,7 @@ function deserializePaintDocument(raw) {
|
|
|
2927
2934
|
const wetEvents = Array.isArray(r.wetEvents) ? r.wetEvents.filter((e) => e && typeof e === "object" && Number.isInteger(e.clock) && typeof e.kind === "string") : [];
|
|
2928
2935
|
const state = { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) };
|
|
2929
2936
|
if (wetEvents.length) state.wetEvents = wetEvents;
|
|
2930
|
-
const paintEvents = Array.isArray(r.paintEvents) ? r.paintEvents.filter((e) => e && typeof e === "object" && Number.isInteger(e.at) && typeof e.layerId === "string" && ["fill", "clear", "flatten", "stickers"].includes(e.kind)) : [];
|
|
2937
|
+
const paintEvents = Array.isArray(r.paintEvents) ? r.paintEvents.filter((e) => e && typeof e === "object" && Number.isInteger(e.at) && typeof e.layerId === "string" && ["fill", "fill-region", "clear", "flatten", "stickers", "live-strokes"].includes(e.kind)) : [];
|
|
2931
2938
|
if (paintEvents.length) state.paintEvents = paintEvents;
|
|
2932
2939
|
const o = r.origin;
|
|
2933
2940
|
if (o && typeof o === "object" && Number.isFinite(o.x) && Number.isFinite(o.y) && (o.x || o.y)) state.origin = { x: Math.round(o.x), y: Math.round(o.y) };
|
|
@@ -4115,7 +4122,18 @@ async function replayPainting(source, target, options = {}) {
|
|
|
4115
4122
|
const e = item.event;
|
|
4116
4123
|
if (!target.getLayer(e.layerId)) continue;
|
|
4117
4124
|
if (e.kind === "fill") target.fillLayer({ layerId: e.layerId, ...e.data });
|
|
4118
|
-
else if (e.kind === "
|
|
4125
|
+
else if (e.kind === "fill-region") {
|
|
4126
|
+
const d = e.data ?? {};
|
|
4127
|
+
if (d.wet) await target.wetIdle();
|
|
4128
|
+
const pts = d.points ? d.points.map((p) => ({ x: p.x * s, y: p.y * s })) : d.x !== void 0 && d.y !== void 0 ? [{ x: d.x * s, y: d.y * s }] : [];
|
|
4129
|
+
target.fillRegion({
|
|
4130
|
+
...d,
|
|
4131
|
+
layerId: e.layerId,
|
|
4132
|
+
points: pts,
|
|
4133
|
+
...d.closeGap !== void 0 ? { closeGap: Math.min(32, d.closeGap * s) } : {},
|
|
4134
|
+
...d.expand !== void 0 ? { expand: Math.max(-16, Math.min(16, d.expand * s)) } : {}
|
|
4135
|
+
});
|
|
4136
|
+
} else if (e.kind === "clear") target.clearLayer(e.layerId);
|
|
4119
4137
|
else if (e.kind === "live-strokes") {
|
|
4120
4138
|
const d = e.data;
|
|
4121
4139
|
if (d?.rasterized) target.replaceLiveStrokes(e.layerId, null);
|
|
@@ -5502,6 +5520,252 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5502
5520
|
this.commit("Fill layer", this.withPaintEvent(this.withWetEvents({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), ...light ? { light } : {} }), { kind: "fill", layerId: l.id, data: { color, opacity, grain: grainAmount, pressure, blend } }));
|
|
5503
5521
|
return { layerId: l.id, bounds: rect };
|
|
5504
5522
|
}
|
|
5523
|
+
/**
|
|
5524
|
+
* Flood-fills one or more contiguous regions on a pixel layer (one undo step), sampling boundaries
|
|
5525
|
+
* from the layer itself (`reference: 'layer'`), all visible layers (`'all'` / `'composite'`), or a
|
|
5526
|
+
* specific reference layer id (e.g. an ink line-art layer above a flats layer). Closes line-art gaps
|
|
5527
|
+
* up to `closeGap` px and expands (`> 0`, bleeding under lines) or contracts (`< 0`) by `expand` px.
|
|
5528
|
+
* With `wet: true`, pours a watercolour wash into the region on the layer's wet layer.
|
|
5529
|
+
*/
|
|
5530
|
+
fillRegion(options) {
|
|
5531
|
+
this.requireIdle("filling");
|
|
5532
|
+
const fillTarget = this.paintTarget(options.layerId, "fill");
|
|
5533
|
+
if (this.doc.stickers?.[fillTarget.id]?.length) throw new PaintEditorError(`Layer "${fillTarget.name}" is a sticker layer; fill another layer (add_layer).`);
|
|
5534
|
+
if (this.doc.liveStrokes?.[fillTarget.id]) throw new PaintEditorError(`Layer "${fillTarget.name}" is a stroke layer (its pixels are drawn from its strokes); fill another layer (add_layer), or rasterize it first (rasterize_stroke_layer).`);
|
|
5535
|
+
const { width, height } = this.doc.canvas;
|
|
5536
|
+
const pts = [];
|
|
5537
|
+
if (options.points !== void 0) {
|
|
5538
|
+
if (!Array.isArray(options.points) || !options.points.length) {
|
|
5539
|
+
throw new PaintEditorError("points is empty; give at least one {x, y} in document pixels.");
|
|
5540
|
+
}
|
|
5541
|
+
for (let i = 0; i < options.points.length; i++) {
|
|
5542
|
+
const p = options.points[i];
|
|
5543
|
+
if (!p || !Number.isFinite(p.x) || !Number.isFinite(p.y)) {
|
|
5544
|
+
throw new PaintEditorError(`points[${i}] needs finite x and y in document pixels.`);
|
|
5545
|
+
}
|
|
5546
|
+
pts.push({ x: p.x, y: p.y });
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
if (options.x !== void 0 || options.y !== void 0) {
|
|
5550
|
+
if (!Number.isFinite(options.x) || !Number.isFinite(options.y)) {
|
|
5551
|
+
throw new PaintEditorError("x and y must be finite numbers in document pixels.");
|
|
5552
|
+
}
|
|
5553
|
+
if (!options.points) pts.push({ x: options.x, y: options.y });
|
|
5554
|
+
}
|
|
5555
|
+
if (!pts.length) {
|
|
5556
|
+
throw new PaintEditorError("Pass x and y (or points) in document pixels to fill a region.");
|
|
5557
|
+
}
|
|
5558
|
+
for (const p of pts) {
|
|
5559
|
+
const px = Math.floor(p.x);
|
|
5560
|
+
const py = Math.floor(p.y);
|
|
5561
|
+
if (px < 0 || py < 0 || px >= width || py >= height) {
|
|
5562
|
+
throw new PaintEditorError(`(${p.x}, ${p.y}) is outside the ${width}\xD7${height} canvas.`);
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
const refSpec = options.reference ?? "layer";
|
|
5566
|
+
if (refSpec !== "layer" && refSpec !== "all" && refSpec !== "composite") {
|
|
5567
|
+
this.requireLayer(refSpec);
|
|
5568
|
+
}
|
|
5569
|
+
const color = options.color === void 0 ? this.color : parseColor(options.color);
|
|
5570
|
+
const opacity = options.opacity ?? 1;
|
|
5571
|
+
if (!(Number.isFinite(opacity) && opacity >= 0 && opacity <= 1)) throw new PaintEditorError(`opacity ${opacity} is out of range; use 0..1.`);
|
|
5572
|
+
const blend = options.blend ?? options.blendMode ?? "normal";
|
|
5573
|
+
if (!isBlendMode(blend)) throw new PaintEditorError(`Unknown blend mode "${blend}".`);
|
|
5574
|
+
const tolerance = options.tolerance ?? 32;
|
|
5575
|
+
if (!(Number.isFinite(tolerance) && tolerance >= 0 && tolerance <= 255)) throw new PaintEditorError(`tolerance ${tolerance} is out of range; use 0..255.`);
|
|
5576
|
+
const closeGap = options.closeGap ?? 0;
|
|
5577
|
+
if (!(Number.isFinite(closeGap) && closeGap >= 0 && closeGap <= 32)) throw new PaintEditorError(`closeGap ${closeGap} is out of range; use 0..32 px.`);
|
|
5578
|
+
const expand2 = options.expand ?? 0;
|
|
5579
|
+
if (!(Number.isFinite(expand2) && expand2 >= -16 && expand2 <= 16)) throw new PaintEditorError(`expand ${expand2} is out of range; use -16..16 px.`);
|
|
5580
|
+
const antialias = options.antialias ?? true;
|
|
5581
|
+
if (typeof antialias !== "boolean") throw new PaintEditorError("antialias must be true or false.");
|
|
5582
|
+
const grainAmount = options.grain ?? 0;
|
|
5583
|
+
if (!(Number.isFinite(grainAmount) && grainAmount >= 0 && grainAmount <= 1)) throw new PaintEditorError(`grain ${grainAmount} is out of range; use 0..1.`);
|
|
5584
|
+
const pressure = options.pressure ?? 0.5;
|
|
5585
|
+
if (!(Number.isFinite(pressure) && pressure >= 0 && pressure <= 1)) throw new PaintEditorError(`pressure ${pressure} is out of range; use 0..1.`);
|
|
5586
|
+
const wet = options.wet === true;
|
|
5587
|
+
if (wet && this.previewing) throw new PaintEditorError("Wet paint can't be previewed; suggest a dry fill instead.");
|
|
5588
|
+
if (wet && !this.wet.wetLayerIds().includes(fillTarget.id) && this.wet.wetLayerIds().length >= MAX_WET_LANES) {
|
|
5589
|
+
throw new PaintEditorError(`At most ${MAX_WET_LANES} layers can hold wet paint at once; dry one first (dry_paint).`);
|
|
5590
|
+
}
|
|
5591
|
+
const refPixels = refSpec === "layer" ? this.compositePixels({ layerIds: [fillTarget.id], paper: false, wet: "display" }).data : refSpec === "all" || refSpec === "composite" ? this.compositePixels({ paper: false, wet: "display" }).data : this.compositePixels({ layerIds: [refSpec], paper: false, wet: "display" }).data;
|
|
5592
|
+
const filled = fillRegionMask(refPixels, width, height, {
|
|
5593
|
+
points: pts,
|
|
5594
|
+
tolerance,
|
|
5595
|
+
closeGap,
|
|
5596
|
+
expand: expand2,
|
|
5597
|
+
antialias
|
|
5598
|
+
});
|
|
5599
|
+
const eventData = {
|
|
5600
|
+
points: pts.map((p) => ({ x: p.x, y: p.y })),
|
|
5601
|
+
reference: refSpec,
|
|
5602
|
+
color,
|
|
5603
|
+
opacity,
|
|
5604
|
+
blend,
|
|
5605
|
+
tolerance,
|
|
5606
|
+
closeGap,
|
|
5607
|
+
expand: expand2,
|
|
5608
|
+
antialias,
|
|
5609
|
+
grain: grainAmount,
|
|
5610
|
+
pressure,
|
|
5611
|
+
...wet ? { wet: true } : {}
|
|
5612
|
+
};
|
|
5613
|
+
if (!filled.bounds || filled.pixelCount === 0) {
|
|
5614
|
+
return {
|
|
5615
|
+
layerId: fillTarget.id,
|
|
5616
|
+
bounds: null,
|
|
5617
|
+
pixelCount: 0,
|
|
5618
|
+
leaked: false,
|
|
5619
|
+
regions: filled.regions,
|
|
5620
|
+
...wet ? { wet: this.wet.isWet(fillTarget.id) } : {}
|
|
5621
|
+
};
|
|
5622
|
+
}
|
|
5623
|
+
const rect = filled.bounds;
|
|
5624
|
+
const depth = grainAmount * this.doc.paper.depth;
|
|
5625
|
+
const grain = depth > 0 ? paperGrain(this.doc.paper) : null;
|
|
5626
|
+
if (wet) {
|
|
5627
|
+
this.requireWetIdle("filling");
|
|
5628
|
+
const before = this.doc;
|
|
5629
|
+
let l2 = fillTarget;
|
|
5630
|
+
if (l2.offset.x || l2.offset.y) {
|
|
5631
|
+
const moved = { ...l2, surface: translateSurface(l2.surface, l2.offset.x, l2.offset.y), offset: { x: 0, y: 0 } };
|
|
5632
|
+
this.doc = { ...this.doc, layers: mapLayer(this.doc.layers, l2.id, () => moved) };
|
|
5633
|
+
l2 = moved;
|
|
5634
|
+
}
|
|
5635
|
+
this.beginWetOp();
|
|
5636
|
+
const world = this.wet.ensureLane(l2.id);
|
|
5637
|
+
const [r, g, b] = hexToRgb8(color);
|
|
5638
|
+
const medium = new WetMedium(world, l2.id, {
|
|
5639
|
+
water: 0.9 * Math.max(0.25, opacity),
|
|
5640
|
+
mass: PIGMENT_UNIT * 2.5 * opacity,
|
|
5641
|
+
body: 0,
|
|
5642
|
+
light: 0
|
|
5643
|
+
});
|
|
5644
|
+
const subCov = new Float32Array(rect.width * rect.height);
|
|
5645
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
5646
|
+
const py = rect.y + yy;
|
|
5647
|
+
const row = py * width;
|
|
5648
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
5649
|
+
const px = rect.x + xx;
|
|
5650
|
+
let v = filled.mask[row + px];
|
|
5651
|
+
if (v > 0 && grain) v *= grainCatch(grain.at(px + 0.5, py + 0.5), depth, pressure);
|
|
5652
|
+
subCov[k] = Math.round(v * 4096) / 4096;
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
medium.deposit(rect, subCov, 1, [r / 255, g / 255, b / 255], null);
|
|
5656
|
+
if (this.wet.instantDry) {
|
|
5657
|
+
this.wet.dry(l2.id);
|
|
5658
|
+
this.logWet({ kind: "dry", layerId: l2.id });
|
|
5659
|
+
} else {
|
|
5660
|
+
this.wet.syncDisplays();
|
|
5661
|
+
}
|
|
5662
|
+
const afterLayer = this.getLayer(l2.id);
|
|
5663
|
+
this.commit(
|
|
5664
|
+
"Fill region",
|
|
5665
|
+
this.withPaintEvent(this.withWetEvents({ ...this.doc, layers: mapLayer(this.doc.layers, l2.id, () => ({ ...afterLayer })) }), {
|
|
5666
|
+
kind: "fill-region",
|
|
5667
|
+
layerId: l2.id,
|
|
5668
|
+
data: eventData
|
|
5669
|
+
}),
|
|
5670
|
+
before
|
|
5671
|
+
);
|
|
5672
|
+
return {
|
|
5673
|
+
layerId: l2.id,
|
|
5674
|
+
bounds: rect,
|
|
5675
|
+
pixelCount: filled.pixelCount,
|
|
5676
|
+
leaked: filled.leaked,
|
|
5677
|
+
regions: filled.regions,
|
|
5678
|
+
wet: this.wet.isWet(l2.id)
|
|
5679
|
+
};
|
|
5680
|
+
}
|
|
5681
|
+
this.dryLayersFirst([fillTarget.id]);
|
|
5682
|
+
const l = this.paintTarget(options.layerId, "fill");
|
|
5683
|
+
const coverage = new CoverageMap();
|
|
5684
|
+
const vals = new Float32Array(rect.width * rect.height);
|
|
5685
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
5686
|
+
const py = rect.y + yy;
|
|
5687
|
+
const row = py * width;
|
|
5688
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
5689
|
+
const px = rect.x + xx;
|
|
5690
|
+
let v = filled.mask[row + px];
|
|
5691
|
+
if (v > 0 && grain) v *= grainCatch(grain.at(px + 0.5, py + 0.5), depth, pressure);
|
|
5692
|
+
vals[k] = v;
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
coverage.accumulate(rect, vals, 1);
|
|
5696
|
+
const apply = {
|
|
5697
|
+
mode: "paint",
|
|
5698
|
+
color: rgbaOf(color),
|
|
5699
|
+
background: rgbaOf(this.doc.paper.color),
|
|
5700
|
+
opacity,
|
|
5701
|
+
blend,
|
|
5702
|
+
lockTransparency: isLayerLocked(l, "transparency"),
|
|
5703
|
+
selection: null,
|
|
5704
|
+
offset: l.offset
|
|
5705
|
+
};
|
|
5706
|
+
const surface = this.applyCoverage(l, coverage, rect, apply);
|
|
5707
|
+
this.commit(
|
|
5708
|
+
"Fill region",
|
|
5709
|
+
this.withPaintEvent(this.withWetEvents({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })) }), {
|
|
5710
|
+
kind: "fill-region",
|
|
5711
|
+
layerId: l.id,
|
|
5712
|
+
data: eventData
|
|
5713
|
+
})
|
|
5714
|
+
);
|
|
5715
|
+
return {
|
|
5716
|
+
layerId: l.id,
|
|
5717
|
+
bounds: rect,
|
|
5718
|
+
pixelCount: filled.pixelCount,
|
|
5719
|
+
leaked: filled.leaked,
|
|
5720
|
+
regions: filled.regions
|
|
5721
|
+
};
|
|
5722
|
+
}
|
|
5723
|
+
/**
|
|
5724
|
+
* Finds enclosed unfilled regions from a reference layer or the visible composite (without paper),
|
|
5725
|
+
* closing line-art gaps up to `closeGap` px and returning each region's interior seed `point`,
|
|
5726
|
+
* `centroid`, `bounds`, `pixelCount`, and `touchesEdge` flag.
|
|
5727
|
+
*/
|
|
5728
|
+
findUnfilledRegions(options = {}) {
|
|
5729
|
+
if (options.layerId !== void 0) {
|
|
5730
|
+
const l = this.requireLayer(options.layerId);
|
|
5731
|
+
if (!isPixelLayer(l)) throw new PaintEditorError(`Layer "${l.name}" is a group; pass a pixel layer inside it.`);
|
|
5732
|
+
}
|
|
5733
|
+
const refSpec = options.reference ?? "all";
|
|
5734
|
+
if (refSpec !== "layer" && refSpec !== "all" && refSpec !== "composite") {
|
|
5735
|
+
this.requireLayer(refSpec);
|
|
5736
|
+
}
|
|
5737
|
+
const tolerance = options.tolerance ?? 32;
|
|
5738
|
+
if (!(Number.isFinite(tolerance) && tolerance >= 0 && tolerance <= 255)) throw new PaintEditorError(`tolerance ${tolerance} is out of range; use 0..255.`);
|
|
5739
|
+
const closeGap = options.closeGap ?? 0;
|
|
5740
|
+
if (!(Number.isFinite(closeGap) && closeGap >= 0 && closeGap <= 32)) throw new PaintEditorError(`closeGap ${closeGap} is out of range; use 0..32 px.`);
|
|
5741
|
+
const minArea = options.minArea ?? 16;
|
|
5742
|
+
if (!(Number.isInteger(minArea) && minArea >= 1 && minArea <= 1e7)) throw new PaintEditorError(`minArea ${minArea} is out of range; use a whole number of pixels, 1 or more.`);
|
|
5743
|
+
const maxRegions = options.maxRegions ?? 100;
|
|
5744
|
+
if (!(Number.isInteger(maxRegions) && maxRegions >= 1 && maxRegions <= 1e3)) throw new PaintEditorError(`maxRegions ${maxRegions} is out of range; use 1..1000.`);
|
|
5745
|
+
const includeEdge = options.includeEdge ?? false;
|
|
5746
|
+
if (typeof includeEdge !== "boolean") throw new PaintEditorError("includeEdge must be true or false.");
|
|
5747
|
+
const { width, height } = this.doc.canvas;
|
|
5748
|
+
let refPixels;
|
|
5749
|
+
if (refSpec === "all" || refSpec === "composite") {
|
|
5750
|
+
refPixels = this.compositePixels({ paper: false, wet: "display" }).data;
|
|
5751
|
+
} else if (refSpec === "layer") {
|
|
5752
|
+
const targetId = options.layerId ?? this.doc.activeLayerId;
|
|
5753
|
+
if (!targetId) throw new PaintEditorError("There is no active layer; pass layerId or reference.");
|
|
5754
|
+
refPixels = this.compositePixels({ layerIds: [targetId], paper: false, wet: "display" }).data;
|
|
5755
|
+
} else {
|
|
5756
|
+
refPixels = this.compositePixels({ layerIds: [refSpec], paper: false, wet: "display" }).data;
|
|
5757
|
+
}
|
|
5758
|
+
const targetPixels = options.layerId !== void 0 && options.layerId !== refSpec && refSpec !== "layer" ? this.compositePixels({ layerIds: [options.layerId], paper: false, wet: "display" }).data : void 0;
|
|
5759
|
+
const regions = findUnfilledRegionsMask(refPixels, width, height, {
|
|
5760
|
+
target: targetPixels,
|
|
5761
|
+
tolerance,
|
|
5762
|
+
closeGap,
|
|
5763
|
+
minArea,
|
|
5764
|
+
includeEdge,
|
|
5765
|
+
maxRegions
|
|
5766
|
+
});
|
|
5767
|
+
return { regions };
|
|
5768
|
+
}
|
|
5505
5769
|
applyCoverage(l, coverage, rect, apply) {
|
|
5506
5770
|
const base = l.surface;
|
|
5507
5771
|
const tiles = new Map(base.tiles);
|
|
@@ -8825,7 +9089,19 @@ function shiftDocument(doc, dx, dy) {
|
|
|
8825
9089
|
const list = e.kind === "stickers" ? e.data?.stickers : void 0;
|
|
8826
9090
|
if (list) return { ...e, data: { ...e.data, stickers: moveStickers(list) } };
|
|
8827
9091
|
const strokes = e.kind === "live-strokes" ? e.data?.strokes : void 0;
|
|
8828
|
-
|
|
9092
|
+
if (strokes) return { ...e, data: { ...e.data, strokes: strokes.map(moveStroke) } };
|
|
9093
|
+
if (e.kind === "fill-region" && e.data) {
|
|
9094
|
+
const d = e.data;
|
|
9095
|
+
return {
|
|
9096
|
+
...e,
|
|
9097
|
+
data: {
|
|
9098
|
+
...e.data,
|
|
9099
|
+
...typeof d.x === "number" && typeof d.y === "number" ? { x: d.x + dx, y: d.y + dy } : {},
|
|
9100
|
+
...Array.isArray(d.points) ? { points: d.points.map((p) => movePoint(p, dx, dy)) } : {}
|
|
9101
|
+
}
|
|
9102
|
+
};
|
|
9103
|
+
}
|
|
9104
|
+
return e;
|
|
8829
9105
|
});
|
|
8830
9106
|
}
|
|
8831
9107
|
if (doc.wetEvents) {
|
|
@@ -9029,6 +9305,17 @@ var PaintDirector = class {
|
|
|
9029
9305
|
}
|
|
9030
9306
|
return c.fillLayer(command.fill);
|
|
9031
9307
|
}
|
|
9308
|
+
case "fill-region": {
|
|
9309
|
+
await this.say(id, command.note, 500);
|
|
9310
|
+
const pts = command.fill.points?.length ? command.fill.points : command.fill.x !== void 0 && command.fill.y !== void 0 ? [{ x: command.fill.x, y: command.fill.y }] : [];
|
|
9311
|
+
for (const p of pts) {
|
|
9312
|
+
await this.glide(id, p, 240);
|
|
9313
|
+
this.presence.set({ id, pressed: true });
|
|
9314
|
+
await this.clock.wait(this.ms(90));
|
|
9315
|
+
this.presence.set({ id, pressed: false });
|
|
9316
|
+
}
|
|
9317
|
+
return c.fillRegion(command.fill);
|
|
9318
|
+
}
|
|
9032
9319
|
case "replay": {
|
|
9033
9320
|
await this.say(id, command.note, 500);
|
|
9034
9321
|
const source = c.getStrokes().filter((s) => command.replay.strokeIds?.includes(s.id));
|
|
@@ -9144,7 +9431,7 @@ function planPaintToolPlayback(tool, _args) {
|
|
|
9144
9431
|
if (control) steps.push({ type: "target", ref: control }, { type: "press" }, { type: "release" });
|
|
9145
9432
|
return steps.length ? steps : null;
|
|
9146
9433
|
}
|
|
9147
|
-
var DIRECTED = /* @__PURE__ */ new Set(["paint_stroke", "stroke_path", "fill_layer", "replay_strokes"]);
|
|
9434
|
+
var DIRECTED = /* @__PURE__ */ new Set(["paint_stroke", "stroke_path", "fill_layer", "fill_region", "replay_strokes"]);
|
|
9148
9435
|
function withPaintPlayback(provider, options) {
|
|
9149
9436
|
const plan = (tool) => {
|
|
9150
9437
|
if (tool.plan || tool.playback !== void 0) return tool;
|
|
@@ -10479,6 +10766,77 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
10479
10766
|
coordinates: { out: ["bounds"] },
|
|
10480
10767
|
handler: (a) => act({ kind: "fill", fill: a }, () => c.fillLayer(a))
|
|
10481
10768
|
});
|
|
10769
|
+
add({
|
|
10770
|
+
name: "fill_region",
|
|
10771
|
+
title: "Fill a region (bucket / flats)",
|
|
10772
|
+
description: 'Flood-fills one or more enclosed regions on a pixel layer from a seed point (x, y) or a batch of seed points (points). Set reference to a line-art layer id from list_layers (or "all" / "composite" for all visible layers, or "layer" for the target layer itself) to drop flat colours onto a flats layer under the ink without touching the line art. closeGap (0..32 px) bridges small gaps in the line art so flats do not leak; expand (-16..16 px, e.g. 1..2 px) bleeds the fill under anti-aliased line edges so no halo shows; wet pours a watercolour wash into the region on the wet layer. One undo step. Returns layerId, bounds, pixelCount, regions, and leaked (true when the fill touched the canvas border).',
|
|
10773
|
+
inputSchema: {
|
|
10774
|
+
type: "object",
|
|
10775
|
+
properties: {
|
|
10776
|
+
x: { type: "number", description: "Seed point x in document pixels (pass x and y, or points)." },
|
|
10777
|
+
y: { type: "number", description: "Seed point y in document pixels (pass x and y, or points)." },
|
|
10778
|
+
points: {
|
|
10779
|
+
type: "array",
|
|
10780
|
+
minItems: 1,
|
|
10781
|
+
maxItems: 500,
|
|
10782
|
+
description: "Multiple seed points in document pixels, filled together as one undo step.",
|
|
10783
|
+
items: {
|
|
10784
|
+
type: "object",
|
|
10785
|
+
properties: {
|
|
10786
|
+
x: { type: "number", description: "Document x in pixels." },
|
|
10787
|
+
y: { type: "number", description: "Document y in pixels." }
|
|
10788
|
+
},
|
|
10789
|
+
required: ["x", "y"],
|
|
10790
|
+
additionalProperties: false
|
|
10791
|
+
}
|
|
10792
|
+
},
|
|
10793
|
+
layerId: LAYER_ID4,
|
|
10794
|
+
reference: {
|
|
10795
|
+
type: "string",
|
|
10796
|
+
description: 'Which pixels stop the fill: "layer" (default, the target layer itself), "all" or "composite" (all visible layers without paper), or a layer id from list_layers (e.g. an ink line-art layer above the flats layer).'
|
|
10797
|
+
},
|
|
10798
|
+
color: { ...COLOR3, description: "Fill colour #rrggbb (default the paint colour)." },
|
|
10799
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
|
|
10800
|
+
blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal; see list_blend_modes)." },
|
|
10801
|
+
blendMode: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Alias for blend (default normal; see list_blend_modes)." },
|
|
10802
|
+
tolerance: { type: "number", minimum: 0, maximum: 255, description: "Colour and alpha similarity tolerance 0..255 (default 32)." },
|
|
10803
|
+
closeGap: { type: "number", minimum: 0, maximum: 32, description: "Close gaps in line art up to this width in pixels, 0..32 (default 0)." },
|
|
10804
|
+
expand: { type: "number", minimum: -16, maximum: 16, description: "Expand (> 0, bleed under lines to hide anti-aliased halos) or contract (< 0) the filled region in pixels, -16..16 (default 0)." },
|
|
10805
|
+
antialias: { type: "boolean", description: "Soften boundary edges (default true)." },
|
|
10806
|
+
grain: { type: "number", minimum: 0, maximum: 1, description: "Paper grain response 0..1 (default 0: flat)." },
|
|
10807
|
+
pressure: { type: "number", minimum: 0, maximum: 1, description: "With grain: how far into the valleys, 0..1 (default 0.5)." },
|
|
10808
|
+
wet: { type: "boolean", description: "Pour a wet watercolour wash into the region on the wet layer instead of dry pixels (default false)." }
|
|
10809
|
+
},
|
|
10810
|
+
additionalProperties: false
|
|
10811
|
+
},
|
|
10812
|
+
annotations: { title: "Fill a region (bucket / flats)", ...WRITE5 },
|
|
10813
|
+
coordinates: { in: ["", "points[]"], out: ["bounds", "regions[].point", "regions[].bounds"] },
|
|
10814
|
+
handler: (a) => act({ kind: "fill-region", fill: a }, () => c.fillRegion(a))
|
|
10815
|
+
});
|
|
10816
|
+
add({
|
|
10817
|
+
name: "find_unfilled_regions",
|
|
10818
|
+
title: "Find enclosed unfilled regions",
|
|
10819
|
+
description: 'Finds enclosed unfilled regions from a reference line-art layer (reference: layerId) or the visible composite ("all" / "composite", the default), closing line-art gaps up to closeGap px. When layerId is given, regions already painted on that target layer are excluded. Changes nothing. Returns regions sorted largest first, each with an interior seed point {x, y} ready for fill_region, centroid, bounds, pixelCount (area), and touchesEdge.',
|
|
10820
|
+
inputSchema: {
|
|
10821
|
+
type: "object",
|
|
10822
|
+
properties: {
|
|
10823
|
+
layerId: { type: "string", description: "Target flats layer id from list_layers; regions already painted on it are excluded (optional)." },
|
|
10824
|
+
reference: {
|
|
10825
|
+
type: "string",
|
|
10826
|
+
description: 'Which pixels define the line-art barriers: "all" or "composite" (default, all visible layers without paper), "layer" (the target or active layer), or a layer id from list_layers (e.g. an ink line-art layer).'
|
|
10827
|
+
},
|
|
10828
|
+
tolerance: { type: "number", minimum: 0, maximum: 255, description: "Alpha/colour threshold 0..255 above which a reference pixel counts as line art (default 32)." },
|
|
10829
|
+
closeGap: { type: "number", minimum: 0, maximum: 32, description: "Close small gaps in line art up to this width in pixels, 0..32 (default 0)." },
|
|
10830
|
+
minArea: { type: "integer", minimum: 1, maximum: 1e7, description: "Ignore regions smaller than this many pixels (default 16)." },
|
|
10831
|
+
includeEdge: { type: "boolean", description: "Include regions that touch the canvas border (default false: only enclosed shapes)." },
|
|
10832
|
+
maxRegions: { type: "integer", minimum: 1, maximum: 1e3, description: "Most regions to return, largest first (default 100)." }
|
|
10833
|
+
},
|
|
10834
|
+
additionalProperties: false
|
|
10835
|
+
},
|
|
10836
|
+
annotations: { title: "Find enclosed unfilled regions", ...READ6 },
|
|
10837
|
+
coordinates: { out: ["regions[].point", "regions[].centroid", "regions[].bounds"] },
|
|
10838
|
+
handler: (a) => c.findUnfilledRegions(a)
|
|
10839
|
+
});
|
|
10482
10840
|
add({
|
|
10483
10841
|
name: "clear_layer",
|
|
10484
10842
|
title: "Clear a layer",
|
|
@@ -11110,6 +11468,7 @@ function createPaintBrushLibraryToolProvider(controller, options = {}) {
|
|
|
11110
11468
|
// src/paint-editor/ui.ts
|
|
11111
11469
|
var PAINT_TOOLS = [
|
|
11112
11470
|
{ id: "brush", label: "Brush", key: "b" },
|
|
11471
|
+
{ id: "fill", label: "Fill", key: "g" },
|
|
11113
11472
|
{ id: "eyedropper", label: "Eyedropper", key: "i" },
|
|
11114
11473
|
{ id: "sticker", label: "Stickers", key: "s" },
|
|
11115
11474
|
{ id: "stroke", label: "Select stroke", key: "v" },
|
|
@@ -11124,6 +11483,8 @@ var PaintGesture = class {
|
|
|
11124
11483
|
controller;
|
|
11125
11484
|
box;
|
|
11126
11485
|
tool = "brush";
|
|
11486
|
+
/** Options the Fill tool (`tool === 'fill'`) passes to `fillRegion` (click to fill; drag horizontally to adjust tolerance). */
|
|
11487
|
+
fillOptions = { reference: "all", tolerance: 32, closeGap: 4, expand: 1, antialias: true };
|
|
11127
11488
|
/** The last refusal (a hidden or locked layer), for the editor to show. */
|
|
11128
11489
|
error = null;
|
|
11129
11490
|
/** The predicted tail of the current stroke in document pixels (draw it as a thin preview). */
|
|
@@ -11166,6 +11527,7 @@ var PaintGesture = class {
|
|
|
11166
11527
|
this.pick(e);
|
|
11167
11528
|
return false;
|
|
11168
11529
|
}
|
|
11530
|
+
if (tool === "fill") return this.fillDown(e);
|
|
11169
11531
|
if (tool === "sticker") return this.stickerDown(e);
|
|
11170
11532
|
try {
|
|
11171
11533
|
const p = this.pen(e);
|
|
@@ -11235,6 +11597,18 @@ var PaintGesture = class {
|
|
|
11235
11597
|
if (p.x < 0 || p.y < 0 || p.x >= width || p.y >= height) return;
|
|
11236
11598
|
this.controller.setColor(this.controller.pickColor(p.x, p.y).color);
|
|
11237
11599
|
}
|
|
11600
|
+
fillDown(e) {
|
|
11601
|
+
const p = this.doc(e);
|
|
11602
|
+
const baseTolerance = this.fillOptions.tolerance ?? 32;
|
|
11603
|
+
try {
|
|
11604
|
+
const filled = this.controller.fillRegion({ ...this.fillOptions, tolerance: baseTolerance, x: p.x, y: p.y });
|
|
11605
|
+
this.mode = { kind: "fill", pointerId: e.pointerId, startX: e.clientX, p, baseTolerance, lastTolerance: baseTolerance, committed: filled.bounds !== null };
|
|
11606
|
+
return true;
|
|
11607
|
+
} catch (err) {
|
|
11608
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
11609
|
+
return false;
|
|
11610
|
+
}
|
|
11611
|
+
}
|
|
11238
11612
|
move(e) {
|
|
11239
11613
|
if (e.pointerType === "pen") this.penAt = { x: e.clientX, y: e.clientY };
|
|
11240
11614
|
const m = this.mode;
|
|
@@ -11255,6 +11629,21 @@ var PaintGesture = class {
|
|
|
11255
11629
|
m.to = this.doc(e);
|
|
11256
11630
|
return;
|
|
11257
11631
|
}
|
|
11632
|
+
if (m.kind === "fill") {
|
|
11633
|
+
const dx = e.clientX - m.startX;
|
|
11634
|
+
if (Math.abs(dx) < 4) return;
|
|
11635
|
+
const nextTol = Math.max(0, Math.min(255, Math.round(m.baseTolerance + dx * 0.5)));
|
|
11636
|
+
if (nextTol === m.lastTolerance) return;
|
|
11637
|
+
m.lastTolerance = nextTol;
|
|
11638
|
+
try {
|
|
11639
|
+
if (m.committed) this.controller.undo();
|
|
11640
|
+
m.committed = false;
|
|
11641
|
+
m.committed = this.controller.fillRegion({ ...this.fillOptions, tolerance: nextTol, x: m.p.x, y: m.p.y }).bounds !== null;
|
|
11642
|
+
} catch (err) {
|
|
11643
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
11644
|
+
}
|
|
11645
|
+
return;
|
|
11646
|
+
}
|
|
11258
11647
|
if (m.kind === "sticker") {
|
|
11259
11648
|
const p = this.doc(e);
|
|
11260
11649
|
m.at = { x: round2(p.x + m.dx), y: round2(p.y + m.dy) };
|
|
@@ -11316,6 +11705,7 @@ var PaintGesture = class {
|
|
|
11316
11705
|
this.penDown = false;
|
|
11317
11706
|
this.predicted = [];
|
|
11318
11707
|
if (this.mode?.kind === "stroke") this.controller.activeStroke?.cancel();
|
|
11708
|
+
if (this.mode?.kind === "fill" && this.mode.committed) this.controller.undo();
|
|
11319
11709
|
if (this.mode?.kind === "sticker") this.controller.cancelStickerPreview();
|
|
11320
11710
|
this.mode = null;
|
|
11321
11711
|
}
|
|
@@ -12686,6 +13076,8 @@ export {
|
|
|
12686
13076
|
emptiedLayers,
|
|
12687
13077
|
emptyThickChannels,
|
|
12688
13078
|
engineBrushSourceIds,
|
|
13079
|
+
fillRegionMask,
|
|
13080
|
+
findUnfilledRegionsMask,
|
|
12689
13081
|
fitZoom,
|
|
12690
13082
|
grainCatch,
|
|
12691
13083
|
grainHeights,
|
|
@@ -12809,6 +13201,7 @@ export {
|
|
|
12809
13201
|
serializePaintDocument,
|
|
12810
13202
|
serializePaintDocumentExternal,
|
|
12811
13203
|
solidPaint,
|
|
13204
|
+
squaredDistanceTransform,
|
|
12812
13205
|
stampCoverage,
|
|
12813
13206
|
standardBlendMode,
|
|
12814
13207
|
startWetClock,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PresenceStore, type PresenceActor, type PresenceActorInfo, type PresenceClock, type PresenceState } from '../animation/presence.cjs';
|
|
2
2
|
import type { MixerLoad } from '../paint/mixer.cjs';
|
|
3
3
|
import type { RasterRect } from '../raster/surface.cjs';
|
|
4
|
-
import { type FillLayerOptions, type PaintEditorController, type PaintStrokeInit, type ReplayOptions } from './controller.cjs';
|
|
4
|
+
import { type FillLayerOptions, type FillRegionOptions, type PaintEditorController, type PaintStrokeInit, type ReplayOptions } from './controller.cjs';
|
|
5
5
|
import type { PenPoint } from './engine.cjs';
|
|
6
6
|
export type PaintPresenceActor = PresenceActor;
|
|
7
7
|
export type PaintPresenceState = PresenceState;
|
|
@@ -35,6 +35,10 @@ export type PaintAgentCommand = {
|
|
|
35
35
|
kind: 'fill';
|
|
36
36
|
fill: FillLayerOptions;
|
|
37
37
|
note?: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'fill-region';
|
|
40
|
+
fill: FillRegionOptions;
|
|
41
|
+
note?: string;
|
|
38
42
|
} | {
|
|
39
43
|
kind: 'replay';
|
|
40
44
|
replay: ReplayOptions;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PresenceStore, type PresenceActor, type PresenceActorInfo, type PresenceClock, type PresenceState } from '../animation/presence.js';
|
|
2
2
|
import type { MixerLoad } from '../paint/mixer.js';
|
|
3
3
|
import type { RasterRect } from '../raster/surface.js';
|
|
4
|
-
import { type FillLayerOptions, type PaintEditorController, type PaintStrokeInit, type ReplayOptions } from './controller.js';
|
|
4
|
+
import { type FillLayerOptions, type FillRegionOptions, type PaintEditorController, type PaintStrokeInit, type ReplayOptions } from './controller.js';
|
|
5
5
|
import type { PenPoint } from './engine.js';
|
|
6
6
|
export type PaintPresenceActor = PresenceActor;
|
|
7
7
|
export type PaintPresenceState = PresenceState;
|
|
@@ -35,6 +35,10 @@ export type PaintAgentCommand = {
|
|
|
35
35
|
kind: 'fill';
|
|
36
36
|
fill: FillLayerOptions;
|
|
37
37
|
note?: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'fill-region';
|
|
40
|
+
fill: FillRegionOptions;
|
|
41
|
+
note?: string;
|
|
38
42
|
} | {
|
|
39
43
|
kind: 'replay';
|
|
40
44
|
replay: ReplayOptions;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { MixerLoad } from '../paint/exchange.cjs';
|
|
12
12
|
import { type BlendModeInfo } from '../raster/blend.cjs';
|
|
13
13
|
import type { ImpastoTool, WetMediumKind, WetTool } from './brushes.cjs';
|
|
14
|
-
import type { PaintEditorController } from './controller.cjs';
|
|
14
|
+
import type { FillRegionOptions, PaintEditorController } from './controller.cjs';
|
|
15
15
|
import type { PenPoint } from './engine.cjs';
|
|
16
16
|
import { type Point2 } from './guides.cjs';
|
|
17
17
|
import { type StencilInput } from './stencils.cjs';
|
|
@@ -35,7 +35,7 @@ export interface PaintPointerEvent extends PointerSample {
|
|
|
35
35
|
getCoalescedEvents?(): PaintPointerEvent[];
|
|
36
36
|
getPredictedEvents?(): PaintPointerEvent[];
|
|
37
37
|
}
|
|
38
|
-
export type PaintTool = 'brush' | 'eyedropper' | 'pan' | 'rotate' | 'sticker' | 'stroke';
|
|
38
|
+
export type PaintTool = 'brush' | 'fill' | 'eyedropper' | 'pan' | 'rotate' | 'sticker' | 'stroke';
|
|
39
39
|
export declare const PAINT_TOOLS: ReadonlyArray<{
|
|
40
40
|
id: PaintTool;
|
|
41
41
|
label: string;
|
|
@@ -52,6 +52,8 @@ export declare class PaintGesture {
|
|
|
52
52
|
private readonly controller;
|
|
53
53
|
private readonly box;
|
|
54
54
|
tool: PaintTool;
|
|
55
|
+
/** Options the Fill tool (`tool === 'fill'`) passes to `fillRegion` (click to fill; drag horizontally to adjust tolerance). */
|
|
56
|
+
fillOptions: Omit<FillRegionOptions, 'x' | 'y' | 'points'>;
|
|
55
57
|
/** The last refusal (a hidden or locked layer), for the editor to show. */
|
|
56
58
|
error: string | null;
|
|
57
59
|
/** The predicted tail of the current stroke in document pixels (draw it as a thin preview). */
|
|
@@ -82,6 +84,7 @@ export declare class PaintGesture {
|
|
|
82
84
|
*/
|
|
83
85
|
private stickerDown;
|
|
84
86
|
private pick;
|
|
87
|
+
private fillDown;
|
|
85
88
|
move(e: PaintPointerEvent): void;
|
|
86
89
|
up(e: PaintPointerEvent): void;
|
|
87
90
|
cancel(): void;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { MixerLoad } from '../paint/exchange.js';
|
|
12
12
|
import { type BlendModeInfo } from '../raster/blend.js';
|
|
13
13
|
import type { ImpastoTool, WetMediumKind, WetTool } from './brushes.js';
|
|
14
|
-
import type { PaintEditorController } from './controller.js';
|
|
14
|
+
import type { FillRegionOptions, PaintEditorController } from './controller.js';
|
|
15
15
|
import type { PenPoint } from './engine.js';
|
|
16
16
|
import { type Point2 } from './guides.js';
|
|
17
17
|
import { type StencilInput } from './stencils.js';
|
|
@@ -35,7 +35,7 @@ export interface PaintPointerEvent extends PointerSample {
|
|
|
35
35
|
getCoalescedEvents?(): PaintPointerEvent[];
|
|
36
36
|
getPredictedEvents?(): PaintPointerEvent[];
|
|
37
37
|
}
|
|
38
|
-
export type PaintTool = 'brush' | 'eyedropper' | 'pan' | 'rotate' | 'sticker' | 'stroke';
|
|
38
|
+
export type PaintTool = 'brush' | 'fill' | 'eyedropper' | 'pan' | 'rotate' | 'sticker' | 'stroke';
|
|
39
39
|
export declare const PAINT_TOOLS: ReadonlyArray<{
|
|
40
40
|
id: PaintTool;
|
|
41
41
|
label: string;
|
|
@@ -52,6 +52,8 @@ export declare class PaintGesture {
|
|
|
52
52
|
private readonly controller;
|
|
53
53
|
private readonly box;
|
|
54
54
|
tool: PaintTool;
|
|
55
|
+
/** Options the Fill tool (`tool === 'fill'`) passes to `fillRegion` (click to fill; drag horizontally to adjust tolerance). */
|
|
56
|
+
fillOptions: Omit<FillRegionOptions, 'x' | 'y' | 'points'>;
|
|
55
57
|
/** The last refusal (a hidden or locked layer), for the editor to show. */
|
|
56
58
|
error: string | null;
|
|
57
59
|
/** The predicted tail of the current stroke in document pixels (draw it as a thin preview). */
|
|
@@ -82,6 +84,7 @@ export declare class PaintGesture {
|
|
|
82
84
|
*/
|
|
83
85
|
private stickerDown;
|
|
84
86
|
private pick;
|
|
87
|
+
private fillDown;
|
|
85
88
|
move(e: PaintPointerEvent): void;
|
|
86
89
|
up(e: PaintPointerEvent): void;
|
|
87
90
|
cancel(): void;
|