Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3030531275 | ||
|
|
f76b5aa0fd | ||
|
|
175a063195 | ||
|
|
da4a26c982 | ||
|
|
a5bd9624b8 | ||
|
|
0f178a55f6 |
@@ -1,5 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- Restructure adoption in Crop & Blend: after "Adopt this result" the layer is baked into the source at full quality and the editor returns to the repaint mask immediately, so you can generate again right away. The ◐ button now enters layer blending explicitly instead of auto-entering after adoption.
|
||||
- Remove the ✓ merge step entirely: right-click (or Escape) now releases the selected region and flattens any remaining layer in one action, so there is no intermediate commit button between adopting a result and picking the next region.
|
||||
- Speed up inline mask change detection: the source image is fingerprinted by size and mtime instead of hashing its full high-resolution bytes on every change/execution.
|
||||
|
||||
## 0.7.2
|
||||
|
||||
- Adopting a result no longer decodes the full-size image a second time: the already-decoded image is reused for the crop layer, cutting one download/decode round trip.
|
||||
- Show a visible message when adoption is skipped because the preview is still loading, no target is selected, or the crop/layer state changed, instead of failing silently.
|
||||
- Bump the asset versions for the crop, comparison and result-adoption modules.
|
||||
|
||||
## 0.7.1
|
||||
|
||||
- Uploading a new source image now keeps the painted mask, undo/redo history and viewport, the same way adopting a result does; only the source pixels change. A first upload still starts with an empty mask.
|
||||
- When the new image has a different size the mask cannot be kept, so the image loads fresh and a notice explains that the mask was reset instead of failing the upload.
|
||||
|
||||
## 0.7.0
|
||||
|
||||
- Add a download button next to the inline upload button: it saves the current source image as a PNG at its own resolution. It stays disabled until an image is loaded.
|
||||
- Change the pin toggle so it freezes the view exactly as it is drawn instead of re-fitting the image: panning and zooming stay available while pinned, and the pinned view survives canvas resizes.
|
||||
- Keep routing the plain wheel to the workflow canvas while pinned, as before.
|
||||
|
||||
## 0.6.9
|
||||
|
||||
- Place the Clear button directly after the swap width/height control in Crop & Blend; in Mask mode it is the only visible button of that bar and stays at the bottom left of the canvas.
|
||||
|
||||
+6
-3
@@ -333,10 +333,13 @@ class AdachiInlineMask:
|
||||
digest.update(str(value).encode("utf-8"))
|
||||
digest.update(b"\x00")
|
||||
if not source_data:
|
||||
# The source is a stable input file; hashing its size and mtime is
|
||||
# enough to detect a replacement and avoids reading a full high-res
|
||||
# PNG on every change/execution. Content-hash the bytes only when
|
||||
# the metadata cannot be read (the loader will then raise anyway).
|
||||
try:
|
||||
with _input_path(image).open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
stat = _input_path(image).stat()
|
||||
digest.update(f"{stat.st_size}:{stat.st_mtime_ns}".encode("utf-8"))
|
||||
except (ValueError, OSError):
|
||||
return float("nan")
|
||||
return digest.hexdigest()
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "adachi-inpaint-workbench"
|
||||
version = "0.6.9"
|
||||
version = "0.8.0"
|
||||
description = "Inline mask painting and WebUI-style inpainting nodes for ComfyUI"
|
||||
requires-python = ">=3.10"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
+3
-1
@@ -118,7 +118,9 @@ export class ImageCompareView {
|
||||
|
||||
async adopt() {
|
||||
this.refreshTargets();
|
||||
if (!this.ready || !this.targetSelect.value || this.busy) return;
|
||||
if (this.busy) return;
|
||||
if (!this.ready) { this.status(T("The result preview is still loading"), true); return; }
|
||||
if (!this.targetSelect.value) { this.status(T("Select an inline mask to adopt into"), true); return; }
|
||||
const target = this.targetSelect.value, result = this.results[this.index];
|
||||
this.busy = true; this.updateButton(); this.status(T("Setting the result as the new source image…"));
|
||||
try {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { ImageCompareView } from "./compare.js?v=0.5.9";
|
||||
import { ImageCompareView } from "./compare.js?v=0.7.2";
|
||||
import { appendComparison } from "./compare_history.js?v=0.5.8";
|
||||
import { routeWheelOutsideImage } from "./wheel_routing.js";
|
||||
import { T } from "./i18n.js";
|
||||
|
||||
+35
-9
@@ -95,6 +95,7 @@ export class InlineMaskEditor {
|
||||
this.root.append(head);
|
||||
const files = el("div", "dd-row dd-files");
|
||||
this.loadButton = this.button(files, "↑", T("Upload an image, or drop / paste one"), () => this.imageInput.click(), "dd-primary");
|
||||
this.downloadButton = this.button(files, "↓", T("Download the source image"), () => this.downloadSource(), "dd-primary");
|
||||
this.filename = el("span", "dd-filename", T("Drop an image onto the canvas"));
|
||||
files.append(this.filename);
|
||||
this.root.append(files);
|
||||
@@ -174,7 +175,7 @@ export class InlineMaskEditor {
|
||||
this.zoomLabel = el("span", "dd-zoom", "100%");
|
||||
footer.append(this.zoomLabel);
|
||||
this.fitButton = this.button(footer, "⛶", T("Fit the image to the canvas"), () => this.fit());
|
||||
this.keepButton = this.button(footer, "", T("Keep"), event => { event.stopPropagation(); this.setKeepFit(!this.keepFit); });
|
||||
this.keepButton = this.button(footer, "", T("Pin the current view"), event => { event.stopPropagation(); this.setKeepFit(!this.keepFit); });
|
||||
this.keepButton.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m16 3 5 5-4 1-4 4 1 4-3 3-3-6-6-3 3-3 4 1 4-4zM8 16l-6 6"/></svg>';
|
||||
// The inline button must not select/bring forward the ComfyUI node:
|
||||
// newer frontends can remeasure its DOM widget on that bubbling click.
|
||||
@@ -201,6 +202,7 @@ export class InlineMaskEditor {
|
||||
actions.addEventListener("keydown", event => this.keydown(event));
|
||||
this.loadButton.after(this.fitButton);
|
||||
this.fitButton.after(this.keepButton);
|
||||
this.loadButton.after(this.downloadButton);
|
||||
// These are the original live controls; the sampler mounts this
|
||||
// same DOM tree so brush state and callbacks are preserved.
|
||||
}
|
||||
@@ -225,14 +227,24 @@ export class InlineMaskEditor {
|
||||
status(message, error = false) {
|
||||
this.message.textContent = message;
|
||||
this.message.classList.toggle("dd-error", error);
|
||||
// Only errors stay visible; transient hints and progress lines hide.
|
||||
this.message.hidden = !error;
|
||||
}
|
||||
|
||||
notice(message) {
|
||||
// An explicit non-error notice that should remain visible.
|
||||
this.message.textContent = message;
|
||||
this.message.classList.remove("dd-error");
|
||||
this.message.hidden = false;
|
||||
}
|
||||
|
||||
async upload(file) {
|
||||
if (file.size > 64 * 1024 * 1024) { this.status(T("File is too large (max 64 MB)"), true); return; }
|
||||
try {
|
||||
this.status(T("Loading image…"));
|
||||
await this.options.onUpload?.(file);
|
||||
// Keep the painted mask when a new source replaces the current one,
|
||||
// the same way adopting a result does. A first upload starts empty.
|
||||
await this.options.onUpload?.(file, { preserveMask: !!this.image });
|
||||
} catch (error) { this.status(error.message, true); }
|
||||
}
|
||||
|
||||
@@ -285,10 +297,10 @@ export class InlineMaskEditor {
|
||||
async replaceSource(url, name = "") {
|
||||
const version = ++this.loadVersion;
|
||||
const image = await loadImage(url);
|
||||
if (version !== this.loadVersion || this.destroyed) return;
|
||||
if (image.width !== this.mask.width || image.height !== this.mask.height) {
|
||||
throw new Error(T("The result size must match the source image to preserve the mask"));
|
||||
}
|
||||
if (version !== this.loadVersion || this.destroyed) return false;
|
||||
// A different size cannot keep the painted mask; report it so the caller
|
||||
// can fall back to a fresh load instead of failing the upload.
|
||||
if (image.width !== this.mask.width || image.height !== this.mask.height) return false;
|
||||
// Replacing only the source preserves mask pixels, the base snapshot,
|
||||
// command history, redo history, active strokes and the current view.
|
||||
this.flush();
|
||||
@@ -296,6 +308,7 @@ export class InlineMaskEditor {
|
||||
this.filename.textContent = name || T("Embedded image");
|
||||
this.filename.title = this.filename.textContent;
|
||||
this.updateButtons(); this.schedule();
|
||||
return true;
|
||||
}
|
||||
|
||||
importMask(ctx, image) {
|
||||
@@ -325,6 +338,7 @@ export class InlineMaskEditor {
|
||||
updateButtons() {
|
||||
this.keepButton?.classList.toggle("dd-active", !!this.keepFit);
|
||||
this.keepButton?.setAttribute("aria-pressed", String(!!this.keepFit));
|
||||
if (this.downloadButton) this.downloadButton.disabled = !this.image;
|
||||
this.stage?.classList.toggle("dd-erasing", !!(this.altErase || this.tool === "erase" || this.stroke?.type === "erase"));
|
||||
for (const [name, button] of Object.entries(this.toolButtons || {})) {
|
||||
const active = name === (this.altErase ? "erase" : this.tool);
|
||||
@@ -341,7 +355,6 @@ export class InlineMaskEditor {
|
||||
|
||||
dimensions() {
|
||||
const r = this.canvas.getBoundingClientRect();
|
||||
if (this.keepFit) { this.zoom = 1; this.pan = { x: 0, y: 0 }; }
|
||||
if (this.viewport?.width && this.viewport?.height && r.width && r.height) {
|
||||
this.pan.x *= r.width / this.viewport.width;
|
||||
this.pan.y *= r.height / this.viewport.height;
|
||||
@@ -360,7 +373,6 @@ export class InlineMaskEditor {
|
||||
pointerDown(event) {
|
||||
if (event.button === 2) { event.preventDefault(); event.stopPropagation(); return; }
|
||||
if (!this.ready || this.pointerId != null) return;
|
||||
if (this.keepFit && (this.space || (!event.altKey && this.tool === "pan") || event.button === 1)) return;
|
||||
event.preventDefault(); event.stopPropagation(); this.root.focus({ preventScroll: true });
|
||||
this.altErase = !!event.altKey; this.updateButtons();
|
||||
this.pointerId = event.pointerId;
|
||||
@@ -556,11 +568,25 @@ export class InlineMaskEditor {
|
||||
const link = el("a"); link.download = "adachi-mask.png"; link.href = this.flush(); link.click();
|
||||
}
|
||||
|
||||
downloadSource() {
|
||||
if (!this.image) return;
|
||||
const out = makeCanvas();
|
||||
out.width = this.image.naturalWidth || this.image.width;
|
||||
out.height = this.image.naturalHeight || this.image.height;
|
||||
out.getContext("2d").drawImage(this.image, 0, 0);
|
||||
const label = String(this.filename.textContent || "").split(/[\\/]/).pop().replace(/\.[^.]+$/, "");
|
||||
const link = el("a");
|
||||
link.download = `${label || "adachi-source"}.png`;
|
||||
link.href = out.toDataURL("image/png");
|
||||
link.click();
|
||||
out.width = out.height = 0;
|
||||
}
|
||||
|
||||
fit() { this.zoom = 1; this.pan = { x: 0, y: 0 }; this.schedule(); }
|
||||
|
||||
setKeepFit(enabled, notify = true) {
|
||||
// Pin freezes the view exactly as drawn; it must not re-fit the image.
|
||||
this.keepFit = !!enabled;
|
||||
if (this.keepFit) this.fit();
|
||||
this.updateButtons(); this.schedule();
|
||||
if (notify) this.options.onKeepFit?.(this.keepFit);
|
||||
}
|
||||
|
||||
+7
-2
@@ -19,6 +19,9 @@ const ZH = {
|
||||
"This result belongs to another crop; generate a result for the current region": "该结果来自其他选区,请为当前选区重新生成",
|
||||
"Result dimensions must match the selected crop": "结果宽高必须与当前裁切区域一致",
|
||||
"The crop changed while adopting the result": "采用过程中选区已改变,请重新生成结果",
|
||||
"The result was not applied; the crop or layer state changed": "结果未应用,选区或图层状态已改变",
|
||||
"The result preview is still loading": "结果预览仍在加载中",
|
||||
"Select an inline mask to adopt into": "请选择要回填的内联蒙版",
|
||||
"Painting tools and mask settings": "绘图工具与蒙版设置",
|
||||
"Browser draft storage was full. The source was saved as an input file; the image, masks and layers are preserved.": "浏览器草稿空间不足,已将原图保存为图片文件;图像、蒙版和图层均已保留。",
|
||||
"Editing mode": "编辑模式",
|
||||
@@ -111,6 +114,8 @@ const ZH = {
|
||||
"Mask": "蒙版",
|
||||
"Fit": "适应",
|
||||
"Keep": "保持",
|
||||
"Pin the current view": "固定当前视图",
|
||||
"Download the source image": "下载原图",
|
||||
"Upload": "上传",
|
||||
"Fit the image to the canvas": "适应画布",
|
||||
"File is too large (max 64 MB)": "图片文件不能超过 64 MB",
|
||||
@@ -122,8 +127,8 @@ const ZH = {
|
||||
// inline_inpaint.js
|
||||
"The inline mask node was removed": "内联蒙版节点已移除",
|
||||
"Adopted the result as the new source image; the mask and undo history were preserved": "已采用重绘结果作为新原图,蒙版与撤销记录已保留",
|
||||
"The result size must match the source image to preserve the mask": "为保留蒙版,重绘结果尺寸必须与原图一致",
|
||||
"Wait for the source image to finish loading before adopting a result": "请等待原图加载完成后再采用结果",
|
||||
"The new image has a different size, so the mask was reset": "新图尺寸与原图不同,蒙版已重置",
|
||||
"Wait for the current image to finish loading": "请等待当前图片加载完成",
|
||||
"Drag to resize canvas height": "上下拖动调整画布高度",
|
||||
"Resize canvas height": "调整画布高度",
|
||||
"Upload failed": "上传失败",
|
||||
|
||||
+30
-21
@@ -1,7 +1,7 @@
|
||||
import { CROP_PRESETS, cropAt } from "./crop_geometry.js?v=0.6.2";
|
||||
import { T } from "./i18n.js";
|
||||
import { syncCropDimensions } from "./parameter_bridge.js?v=0.6.4";
|
||||
import { installResultAdoption } from "./result_adoption.js?v=0.6.4";
|
||||
import { installResultAdoption } from "./result_adoption.js?v=0.7.2";
|
||||
|
||||
const fields = ["mask", "baseMask", "history", "future", "lastMask", "dirty"];
|
||||
const canvas = (w, h) =>
|
||||
@@ -93,14 +93,17 @@ export class InlineCropController {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
editor.root.focus({ preventScroll: true });
|
||||
if (this.layer && this.region.locked && !editor.stroke && editor.pointerId == null) {
|
||||
// Hold-right preview only makes sense while adjusting the blend layer.
|
||||
// After adoption the layer is already baked, so right-click reselects
|
||||
// (reselect flattens any remaining layer first).
|
||||
if (this.target === "blend" && this.layer && this.region.locked && !editor.stroke && editor.pointerId == null) {
|
||||
this.previewOriginal = true;
|
||||
this.originalPreviewPointer = e.pointerId;
|
||||
editor.canvas.setPointerCapture(e.pointerId);
|
||||
editor.cursor = null;
|
||||
this.revision++;
|
||||
editor.schedule();
|
||||
} else if (!this.layer && this.region.locked) this.run(() => this.reselect());
|
||||
} else if (this.region.locked) this.run(() => this.reselect());
|
||||
return;
|
||||
}
|
||||
if (
|
||||
@@ -267,19 +270,19 @@ export class InlineCropController {
|
||||
this.blendButton = button(
|
||||
"◐",
|
||||
"Edit layer blending: brush keeps the new result; Alt restores original pixels. Hold right-click to preview the original, release to return.",
|
||||
() => {
|
||||
this.activate("blend");
|
||||
this.changed();
|
||||
},
|
||||
);
|
||||
this.commitButton = button("✓", "Merge the visible result and blend mask into the source image. After merging, right-click releases the selected region.", () =>
|
||||
this.flatten(),
|
||||
() =>
|
||||
this.run(async () => {
|
||||
// Blending is opt-in after adoption: build the blend mask on first
|
||||
// use, then switch the editor into the blend layer.
|
||||
if (!this.contexts.blend) await this.makeBlend(this.widgets.blend_data.value);
|
||||
this.activate("blend");
|
||||
this.changed();
|
||||
}),
|
||||
);
|
||||
// Move the original live button, retaining its history and disabled state.
|
||||
this.undoButton = e.undoButton;
|
||||
this.undoButton.textContent = "↶";
|
||||
this.undoButton.title = T("Undo the last stroke in the active repaint or blend mask (Ctrl+Z). Redo with Ctrl+Shift+Z.");
|
||||
this.commitButton.after(this.undoButton);
|
||||
this.custom = make("div", "dd-crop-custom");
|
||||
this.custom.hidden = true;
|
||||
for (const key of ["cropWidth", "cropHeight"]) {
|
||||
@@ -322,14 +325,13 @@ export class InlineCropController {
|
||||
this.modeSelect.value = this.mode;
|
||||
// No hover text on the painting canvas; the bottom controls carry the hints.
|
||||
this.editor.canvas.removeAttribute("title");
|
||||
for (const control of [this.preset, this.swapButton, this.maskButton, this.blendButton, this.commitButton]) {
|
||||
for (const control of [this.preset, this.swapButton, this.maskButton, this.blendButton]) {
|
||||
control.hidden = this.mode !== "crop";
|
||||
}
|
||||
this.custom.hidden = this.mode !== "crop" || this.preset.value !== "custom";
|
||||
this.undoButton.disabled = !!this.busy || !this.editor.history.length;
|
||||
this.clearButton.disabled = !!this.busy || !this.editor.ready;
|
||||
this.blendButton.disabled = !this.layer;
|
||||
this.commitButton.disabled = !this.layer;
|
||||
this.maskButton.classList.toggle("dd-active", this.target === "mask");
|
||||
this.blendButton.classList.toggle("dd-active", this.target === "blend");
|
||||
this.modeSelect.disabled = !!this.busy;
|
||||
@@ -498,14 +500,17 @@ export class InlineCropController {
|
||||
if (this.widgets.layer_image.value) {
|
||||
this.layer = await load(this.imageUrl(this.widgets.layer_image.value));
|
||||
await this.makeBlend(this.widgets.blend_data.value);
|
||||
if (this.mode === "crop") this.activate("blend");
|
||||
// Restore the state the user left: a non-empty blend mask means they were
|
||||
// still adjusting the blend layer, otherwise the adopted layer is already
|
||||
// baked and we stay on the repaint mask.
|
||||
if (this.widgets.blend_data.value && this.mode === "crop") this.activate("blend");
|
||||
}
|
||||
}
|
||||
this.revision++;
|
||||
this.sync();
|
||||
this.editor.schedule();
|
||||
}
|
||||
async accept(record, {allowBusy = false} = {}) {
|
||||
async accept(record, {allowBusy = false, image = null} = {}) {
|
||||
if (
|
||||
this.editor.destroyed ||
|
||||
this.mode !== "crop" ||
|
||||
@@ -520,7 +525,7 @@ export class InlineCropController {
|
||||
this.resultTimer = setTimeout(() => {
|
||||
const next = this.pendingResult;
|
||||
this.pendingResult = null;
|
||||
this.accept(next).catch((error) =>
|
||||
this.accept(next, {allowBusy}).catch((error) =>
|
||||
this.editor.status(error.message, true),
|
||||
);
|
||||
}, 50);
|
||||
@@ -531,7 +536,9 @@ export class InlineCropController {
|
||||
: record.image.filename;
|
||||
if (name === this.widgets.layer_image.value) return;
|
||||
const version = ++this.acceptVersion;
|
||||
const image = await load(this.imageUrl(name));
|
||||
// The adoption path already decoded this image once; reuse it instead of
|
||||
// downloading and decoding the full-size result a second time.
|
||||
const decoded = image || await load(this.imageUrl(name));
|
||||
if (
|
||||
this.editor.destroyed ||
|
||||
version !== this.acceptVersion ||
|
||||
@@ -539,11 +546,13 @@ export class InlineCropController {
|
||||
this.mode !== "crop"
|
||||
)
|
||||
return;
|
||||
this.layer = image;
|
||||
this.layer = decoded;
|
||||
this.widgets.layer_image.value = name;
|
||||
if (!this.contexts.blend && this.target !== "blend")
|
||||
await this.makeBlend(this.widgets.blend_data.value);
|
||||
this.activate("blend");
|
||||
// Adopt then return: with an empty blend mask the backend hard-composites
|
||||
// the whole crop from layer_image, so the result is immediately baked into
|
||||
// the source and the repaint mask is reused on the next generation. Blending
|
||||
// is no longer entered automatically; the ◐ button opens it on demand.
|
||||
this.activate("mask");
|
||||
this.revision++;
|
||||
this.sync();
|
||||
this.editor.schedule();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { InlineMaskEditor } from "./editor.js?v=0.6.7";
|
||||
import { InlineMaskEditor } from "./editor.js?v=0.8.0";
|
||||
import { installInpaintWorkbench } from "./inpaint_node.js?v=0.6.1";
|
||||
import { bindWidgetToNodeWidth } from "./widget_layout.js?v=0.5.5";
|
||||
import { routeWheelOutsideImage } from "./wheel_routing.js";
|
||||
import { InlineCropController } from "./inline_crop.js?v=0.6.9";
|
||||
import { InlineCropController } from "./inline_crop.js?v=0.8.1";
|
||||
import { canStoreDraft, prepareInlineDraft, syncInlineWidgets } from "./draft_guard.js?v=0.6.3";
|
||||
import { T } from "./i18n.js";
|
||||
import { removeLegacyFeedback } from "./legacy_feedback.js?v=0.6.4";
|
||||
@@ -132,7 +132,7 @@ app.registerExtension({
|
||||
onImage() { if (!restoring || !node.properties.adachi_compact_ui) fitNodeToImage(); },
|
||||
onGraphWheel: event => app.canvas?.processMouseWheel(event),
|
||||
async onUpload(file, { preserveMask = false, preserveCrop = false } = {}) {
|
||||
if (preserveMask && editor.image && !editor.ready) throw new Error(T("Wait for the source image to finish loading before adopting a result"));
|
||||
if (preserveMask && editor.image && !editor.ready) throw new Error(T("Wait for the current image to finish loading"));
|
||||
const version = ++uploadVersion;
|
||||
const body = new FormData();
|
||||
body.append("image", file, file.name);
|
||||
@@ -145,14 +145,16 @@ app.registerExtension({
|
||||
const name = uploaded.subfolder ? `${uploaded.subfolder}/${uploaded.name}` : uploaded.name;
|
||||
restoring = false;
|
||||
crop?.activate("mask");
|
||||
if (preserveMask && editor.ready) await editor.replaceSource(imageUrl(name), file.name);
|
||||
let kept = false;
|
||||
if (preserveMask && editor.ready) kept = await editor.replaceSource(imageUrl(name), file.name);
|
||||
else await editor.setImage(imageUrl(name), "", file.name);
|
||||
if (version !== uploadVersion || editor.destroyed) return;
|
||||
imageWidget.value = name;
|
||||
maskWidget.value = preserveMask ? editor.flush() : "";
|
||||
maskWidget.value = kept ? editor.flush() : "";
|
||||
sourceWidget.value = editor.embed.checked ? editor.sourceData() : "";
|
||||
if (!preserveCrop) crop?.reset();
|
||||
await beforeModeChange();
|
||||
if (preserveMask && !kept) editor.notice(T("The new image has a different size, so the mask was reset"));
|
||||
markChanged();
|
||||
},
|
||||
onEmbed(enabled) {
|
||||
|
||||
@@ -40,9 +40,12 @@ export function installResultAdoption(node, crop) {
|
||||
throw Error(T("The crop changed while adopting the result"));
|
||||
}
|
||||
const descriptor = {filename:uploaded.name, subfolder:uploaded.subfolder || "", type:"input"};
|
||||
await crop.accept({job:region.job, image:descriptor}, {allowBusy:true});
|
||||
await crop.accept({job:region.job, image:descriptor}, {allowBusy:true, image});
|
||||
const name = descriptor.subfolder ? `${descriptor.subfolder}/${descriptor.filename}` : descriptor.filename;
|
||||
if (crop.widgets.layer_image.value !== name) throw Error(T("The crop changed while adopting the result"));
|
||||
if (crop.widgets.layer_image.value !== name) {
|
||||
// accept() may have skipped silently (busy / changed job / duplicate).
|
||||
throw Error(T("The result was not applied; the crop or layer state changed"));
|
||||
}
|
||||
editor.status("");
|
||||
} finally {
|
||||
URL.revokeObjectURL(objectUrl); crop.busy = false; crop.sync();
|
||||
|
||||
Reference in New Issue
Block a user