1 Commits
Author SHA1 Message Date
meareabc 2d2a51c02c Fix guide preview dimensions and show the first frame 2026-10-28 09:59:54 +08:00
6 changed files with 219 additions and 48 deletions
+9 -5
View File
@@ -24,10 +24,12 @@ git clone https://github.com/meareabc/ComfyUI-MiniMaxH3-SpatialGuide.git
**MiniMax H3 Guide Position**
- Set `guide_width`, `guide_height`, `canvas_width`, and `canvas_height`, or connect their INT inputs.
- Drag the blue guide rectangle; its size stays fixed. Hold Shift to snap movement to 32 pixels. Center and Origin buttons provide quick alignment.
- Connect the same `image` batch used by Spatial Guide to preview its **first frame**. This also works with a single image. The actual image dimensions override the guide size fields for preview.
- Connect the target AV `latent` to use its actual canvas dimensions. This accounts for alignment performed by the empty latent node.
- Click **Preview** to run this node and its upstream dependencies without running downstream sampling. Then drag the image; coordinates and placement update immediately. Hold Shift to snap movement to 32 pixels. Center and Origin buttons provide quick alignment.
- Connect its `x` and `y` outputs to Add Spatial Guide's corresponding inputs. Coordinates save with the workflow and can also be edited numerically.
- Linked Primitive values update the preview immediately. Dimensions computed by other backend nodes synchronize after execution; before the first run the preview uses the saved dimensions. Enter sizes directly for an exact preview before running.
- After changing the image source or target size, refresh the preview. Old dimensions are invalidated instead of being combined with new input values. If the guide is actually larger than the canvas, the preview shows the overflow without resizing it.
- Without `image` or `latent`, set `guide_width`, `guide_height`, `canvas_width`, and `canvas_height` manually, or connect their INT inputs.
For a 640×480 guide on a 960×480 canvas, `x=0,y=0` leaves 320 pixels to the right; `x=160,y=0` centers it. For a 480×640 guide on a 480×960 canvas, use `x=0,y=160` to center it vertically.
@@ -35,7 +37,7 @@ For a 640×480 guide on a 960×480 canvas, `x=0,y=0` leaves 320 pixels to the ri
Video guides follow native Add Guide's valid lengths: 5, 22, 39… frames; batches below 5 frames use the first image. Nonmultiples of 32 are padded at the right/bottom with replicated edges, without scaling. This is positional conditioning, so generated pixels can change inside the guide region.
CPU tests cover native-size coordinates, mixed guides/audio/references, clone isolation, registration, and a small native H3 forward. Full pretrained outpainting quality has not been validated.
CPU tests cover native-size coordinates, mixed guides/audio/references, clone isolation, registration, first-frame selection, actual latent dimensions, partial execution and cached previews, and a small native H3 forward. Browser checks cover image display, dragging, zoom, and stale input invalidation. Full pretrained outpainting quality has not been validated.
```bash
python -m unittest discover -s custom_nodes/ComfyUI-MiniMaxH3-SpatialGuide/tests -v
@@ -43,6 +45,8 @@ python -m unittest discover -s custom_nodes/ComfyUI-MiniMaxH3-SpatialGuide/tests
## 中文使用说明
重启并刷新后搜索 **MiniMax H3 Guide Position**,输入参考宽高和目标画布宽高,在预览里拖动蓝框。输出的 `x/y` 接到 **Add Spatial Guide for MiniMax H3**;表示参考区域左上角坐标,原点在目标画布左上角。按住 Shift 拖动可按 32 像素对齐,也可点击居中或左上归零。
重启并刷新后搜索 **MiniMax H3 Guide Position**,把同一份 Guide 视频/图片接入 `image`,目标 AV latent 接入 `latent`。点击“刷新预览”即可单独执行预览及其上游准备,显示视频第一帧或所选图片和真实画布尺寸;随后拖动图片,实时调整位置。修改来源或画布尺寸后再次刷新。
输出的 `x/y` 接到 **Add Spatial Guide for MiniMax H3**,表示参考区域左上角坐标,原点在目标画布左上角。按住 Shift 拖动可按 32 像素对齐,也可点击居中或左上归零。如果参考区域确实大于目标画布,预览会如实显示越界,需要增大画布宽高。
Spatial Guide 的 **MODEL 和 positive 两个输出都要连接到采样器**,目标 latent 直接接采样器。音频可用原生 Add Guide 单独添加。
+17 -4
View File
@@ -8,7 +8,7 @@ import comfy.conds
import comfy.patcher_extension
import nodes
from comfy.ldm.minimax.model import FRAME_PER_TOKEN
from comfy_api.latest import ComfyExtension, io
from comfy_api.latest import ComfyExtension, io, ui
GUIDE_POSITION = "minimax_h3_spatial_guide"
@@ -132,7 +132,8 @@ class MiniMaxH3GuidePosition(io.ComfyNode):
node_id="MiniMaxH3GuidePosition",
display_name="MiniMax H3 Guide Position",
category="model/conditioning/minimax",
description="Drag the guide rectangle inside the target canvas. Outputs the guide's top-left x/y in target pixels.",
description="Preview the guide's first frame and drag it within the target canvas. Run this node to refresh input dimensions without sampling.",
is_output_node=True,
inputs=[
io.Int.Input("guide_width", default=640, min=1, max=nodes.MAX_RESOLUTION),
io.Int.Input("guide_height", default=480, min=1, max=nodes.MAX_RESOLUTION),
@@ -140,13 +141,25 @@ class MiniMaxH3GuidePosition(io.ComfyNode):
io.Int.Input("canvas_height", default=480, min=1, max=nodes.MAX_RESOLUTION),
io.Int.Input("x", default=0, min=-nodes.MAX_RESOLUTION, max=nodes.MAX_RESOLUTION, socketless=True),
io.Int.Input("y", default=0, min=-nodes.MAX_RESOLUTION, max=nodes.MAX_RESOLUTION, socketless=True),
io.Image.Input("image", optional=True, tooltip="Connect the same image/video batch as Spatial Guide. The first frame determines the guide preview and its actual size."),
io.Latent.Input("latent", optional=True, tooltip="Target H3 AV latent. Uses its actual canvas dimensions, including the latent's size alignment."),
],
outputs=[io.Int.Output(display_name="x"), io.Int.Output(display_name="y")],
)
@classmethod
def execute(cls, guide_width, guide_height, canvas_width, canvas_height, x, y) -> io.NodeOutput:
return io.NodeOutput(x, y, ui={"guide_size": [guide_width, guide_height], "canvas_size": [canvas_width, canvas_height]})
def execute(cls, guide_width, guide_height, canvas_width, canvas_height, x, y, image=None, latent=None) -> io.NodeOutput:
preview = {}
if image is not None:
guide_height, guide_width = image.shape[1:3]
preview["guide_preview"] = ui.PreviewImage(image[:1, ..., :3]).as_dict()["images"]
if latent is not None:
samples = latent["samples"]
if not samples.is_nested or len(samples.tensors) != 2 or samples.tensors[0].ndim != 5 or samples.tensors[0].shape[1] != 24:
raise ValueError("MiniMaxH3GuidePosition expects a MiniMax H3 AV latent")
canvas_height, canvas_width = [size * 16 for size in samples.tensors[0].shape[-2:]]
preview.update({"guide_size": [guide_width, guide_height], "canvas_size": [canvas_width, canvas_height]})
return io.NodeOutput(x, y, ui=preview)
class MiniMaxH3SpatialGuideExtension(ComfyExtension):
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 24 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "minimax-h3-spatial-guide"
version = "0.1.0"
version = "0.2.0"
description = "Native-size spatial guides and drag positioning for MiniMax H3 in ComfyUI"
readme = "README.md"
license = { file = "LICENSE" }
+56
View File
@@ -2,16 +2,19 @@ import asyncio
import importlib.util
import math
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
from PIL import Image
import torch
import comfy.cli_args
comfy.cli_args.args.cpu = True
import comfy.conds
import execution
from comfy.ldm.minimax.model import MiniMaxH3Model, PackedLayout, patchify_video
from comfy.model_patcher import ModelPatcher
from comfy.nested_tensor import NestedTensor
@@ -141,6 +144,59 @@ class SpatialGuideTests(unittest.TestCase):
self.assertEqual(result.ui, {"guide_size": [640, 480], "canvas_size": [960, 480]})
schema = guide.MiniMaxH3GuidePosition.define_schema()
self.assertEqual([output.display_name for output in schema.outputs], ["x", "y"])
self.assertTrue(schema.is_output_node)
def test_position_previews_first_frame_and_actual_canvas_size(self):
images = torch.zeros(2, 48, 65, 3)
images[0, ..., 0] = 1
images[1, ..., 1] = 1
latent = {"samples": NestedTensor([torch.zeros(1, 24, 2, 35, 16), torch.zeros(1, 32, 2, 8)])}
with tempfile.TemporaryDirectory() as directory, patch("folder_paths.get_temp_directory", return_value=directory):
result = guide.MiniMaxH3GuidePosition.execute(320, 416, 270, 560, -38, 147, image=images, latent=latent)
self.assertEqual(result.result, (-38, 147))
self.assertEqual(result.ui["guide_size"], [65, 48])
self.assertEqual(result.ui["canvas_size"], [256, 560])
self.assertEqual(len(result.ui["guide_preview"]), 1)
descriptor = result.ui["guide_preview"][0]
self.assertEqual(descriptor["type"], "temp")
with Image.open(Path(directory) / descriptor["subfolder"] / descriptor["filename"]) as image:
self.assertEqual(image.size, (65, 48))
self.assertEqual(image.getpixel((0, 0)), (255, 0, 0))
def test_position_accepts_single_image_preview(self):
images = torch.ones(1, 32, 64, 3)
with tempfile.TemporaryDirectory() as directory, patch("folder_paths.get_temp_directory", return_value=directory):
result = guide.MiniMaxH3GuidePosition.execute(1, 1, 128, 96, 32, 32, image=images)
self.assertEqual(result.ui["guide_size"], [64, 32])
self.assertEqual(result.ui["canvas_size"], [128, 96])
def test_partial_execution_refreshes_preview_without_downstream_output(self):
events = []
server = SimpleNamespace(client_id=None, last_node_id=None, send_sync=lambda event, data, client=None: events.append((event, data)))
executor = execution.PromptExecutor(server, cache_args={"ram": 0, "ram_inactive": 0})
prompt = {
"source": {"class_type": "EmptyImage", "inputs": {"width": 64, "height": 48, "batch_size": 2, "color": 0xFF0000}},
"position": {"class_type": "MiniMaxH3GuidePosition", "inputs": {
"guide_width": 320, "guide_height": 416, "canvas_width": 128, "canvas_height": 96,
"x": 32, "y": 16, "image": ["source", 0],
}},
"downstream": {"class_type": "PreviewImage", "inputs": {"images": ["position", 0]}},
}
with tempfile.TemporaryDirectory() as directory, patch("folder_paths.get_temp_directory", return_value=directory), patch.dict(
guide.nodes.NODE_CLASS_MAPPINGS, {"MiniMaxH3GuidePosition": guide.MiniMaxH3GuidePosition}
):
valid = asyncio.run(execution.validate_prompt("position-preview", prompt, ["position"]))
self.assertTrue(valid[0], valid)
self.assertEqual(set(valid[2]), {"position"})
for prompt_id in ("position-preview", "position-preview-cached"):
events.clear()
executor.execute(prompt, prompt_id, {"client_id": "position-test"}, valid[2])
self.assertTrue(executor.success, executor.status_messages)
output = executor.history_result["outputs"]["position"]
self.assertEqual(output["guide_size"], [64, 48])
self.assertEqual(len(output["guide_preview"]), 1)
self.assertNotIn("downstream", executor.history_result["outputs"])
self.assertTrue(any(event == "executed" and data["node"] == "position" for event, data in events))
if __name__ == "__main__":
+136 -38
View File
@@ -1,16 +1,26 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
const DIMENSIONS = ["guide_width", "guide_height", "canvas_width", "canvas_height"];
const HEIGHT = 290;
const HEIGHT = 310;
function widget(node, name) {
return node.widgets?.find((item) => item.name === name);
}
function inputSlot(node, name) {
return node.inputs?.findIndex((input) => input.name === name) ?? -1;
}
function connected(node, name) {
const slot = inputSlot(node, name);
return slot >= 0 && node.inputs[slot].link != null;
}
function inputValue(node, name, visited = new Set()) {
if (visited.has(node)) return undefined;
visited = new Set(visited).add(node);
const slot = node.inputs?.findIndex((input) => input.name === name) ?? -1;
const slot = inputSlot(node, name);
if (slot < 0 || node.inputs[slot].link == null) return widget(node, name)?.value;
const source = node.getInputNode(slot);
const link = node.getInputLink(slot);
@@ -18,7 +28,8 @@ function inputValue(node, name, visited = new Set()) {
const type = source.comfyClass || source.type;
if (type === "ComfySwitchNode") {
const selected = inputValue(source, "switch", visited);
return selected == null ? undefined : inputValue(source, selected ? "on_true" : "on_false", visited);
if (typeof selected !== "boolean") return undefined;
return inputValue(source, selected ? "on_true" : "on_false", visited);
}
if (type === "Reroute") return inputValue(source, source.inputs[0].name, visited);
if (["PrimitiveInt", "PrimitiveBoolean", "PrimitiveNode", "easy int"].includes(type)) {
@@ -27,17 +38,47 @@ function inputValue(node, name, visited = new Set()) {
return undefined;
}
function inputSignature(node, name, visited = new Set()) {
const slot = inputSlot(node, name);
if (slot < 0 || node.inputs[slot].link == null) return widget(node, name)?.value;
const source = node.getInputNode(slot);
const link = node.getInputLink(slot);
if (!source || !link || visited.has(source)) return node.inputs[slot].link;
visited = new Set(visited).add(source);
const type = source.comfyClass || source.type;
const identity = [node.inputs[slot].link, source.id, link.origin_slot, type];
if (type === "ComfySwitchNode") {
const selected = inputValue(source, "switch");
if (typeof selected === "boolean") {
return [...identity, selected, inputSignature(source, selected ? "on_true" : "on_false", visited)];
}
}
if (type === "EmptyMiniMaxH3LatentAV") {
return [...identity, inputSignature(source, "width", visited), inputSignature(source, "height", visited)];
}
if (type === "ImageFromBatch") {
return [...identity, inputSignature(source, "image", visited), inputSignature(source, "batch_index", visited)];
}
return [...identity,
source.widgets?.map(({ name, value }) => [name, ["string", "number", "boolean"].includes(typeof value) ? value : null]),
source.inputs?.map((input) => inputSignature(source, input.name, visited))];
}
function validDimension(value) {
return typeof value === "number" && Number.isInteger(value) && value > 0;
}
function createPositionEditor(node) {
const root = document.createElement("div");
root.className = "h3-guide-position";
root.style.cssText = "width:100%;height:290px;display:flex;flex-direction:column;gap:6px;box-sizing:border-box;padding:6px;color:#dce5ef;font:12px sans-serif;";
root.style.cssText = `width:100%;height:${HEIGHT}px;display:flex;flex-direction:column;gap:6px;box-sizing:border-box;padding:6px;color:#dce5ef;font:12px sans-serif;`;
const canvas = document.createElement("canvas");
canvas.style.cssText = "width:100%;height:218px;flex:1;min-height:100px;border:1px solid #435066;border-radius:5px;box-sizing:border-box;touch-action:none;cursor:grab;";
canvas.style.cssText = "width:100%;flex:1;min-height:100px;border:1px solid #435066;border-radius:5px;box-sizing:border-box;touch-action:none;cursor:grab;";
canvas.setAttribute("aria-label", "Drag guide position / 拖动参考区域");
const row = document.createElement("div");
row.style.cssText = "display:flex;gap:6px;align-items:center;";
row.style.cssText = "display:flex;gap:6px;align-items:center;flex-wrap:wrap;flex-shrink:0;";
const status = document.createElement("div");
status.style.cssText = "white-space:nowrap;font-size:11px;color:#a9b8cb;min-height:14px;";
status.style.cssText = "font-size:11px;line-height:15px;color:#a9b8cb;min-height:30px;flex-shrink:0;";
const coordinates = document.createElement("span");
coordinates.style.marginLeft = "auto";
const context = canvas.getContext("2d");
@@ -46,14 +87,28 @@ function createPositionEditor(node) {
let liveKey;
let frame;
let removed = false;
let executed;
let preview;
let queuedSignatures;
let previewError = "";
function dimensions() {
function signatures() {
return {
guide: JSON.stringify(connected(node, "image") ? inputSignature(node, "image") : DIMENSIONS.slice(0, 2).map((name) => inputSignature(node, name))),
canvas: JSON.stringify(connected(node, "latent") ? inputSignature(node, "latent") : DIMENSIONS.slice(2).map((name) => inputSignature(node, name))),
};
}
function dimensions(keys = signatures()) {
const guide = executed?.signatures.guide === keys.guide ? executed.guide : [];
const target = executed?.signatures.canvas === keys.canvas ? executed.canvas : [];
const values = DIMENSIONS.map((name, index) => {
const linked = inputValue(node, name);
const stored = node.properties?.h3GuideSize?.[index];
return Number(linked ?? stored ?? widget(node, name)?.value);
const actual = index < 2 ? guide[index] : target[index - 2];
if (connected(node, index < 2 ? "image" : "latent")) return actual;
const live = inputValue(node, name);
return live === undefined ? actual : live;
});
return values.every((value) => Number.isFinite(value) && value > 0) ? values : null;
return values.every(validDimension) ? values : null;
}
function position() {
@@ -84,14 +139,17 @@ function createPositionEditor(node) {
context.setTransform(dpr, 0, 0, dpr, 0, 0);
context.fillStyle = "#111923";
context.fillRect(0, 0, bounds.width, bounds.height);
const sizes = dimensions();
const keys = signatures();
const sizes = dimensions(keys);
const [x, y] = position();
coordinates.textContent = `x ${x} · y ${y}`;
if (!sizes) {
status.textContent = "请填写参考宽高和画布宽高 / Enter dimensions";
view = null;
status.textContent = previewError || "尺寸尚未同步或来源已更改,点击“刷新预览”读取实际尺寸。";
return;
}
const [gw, gh, cw, ch] = sizes;
const [x, y] = position();
if (!drag) {
if (!drag || !view) {
const left = Math.min(0, x), top = Math.min(0, y);
const right = Math.max(cw, x + gw), bottom = Math.max(ch, y + gh);
const scale = Math.min((bounds.width - 36) / (right - left), (bounds.height - 36) / (bottom - top));
@@ -118,18 +176,21 @@ function createPositionEditor(node) {
context.stroke();
}
context.restore();
context.fillStyle = "rgba(64, 164, 217, 0.35)";
context.fillRect(ox + x * scale, oy + y * scale, gw * scale, gh * scale);
if (preview?.signature === keys.guide && preview.image.complete && preview.image.naturalWidth) {
context.drawImage(preview.image, ox + x * scale, oy + y * scale, gw * scale, gh * scale);
} else {
context.fillStyle = "rgba(64, 164, 217, 0.35)";
context.fillRect(ox + x * scale, oy + y * scale, gw * scale, gh * scale);
context.fillStyle = "#d2f5ff";
context.font = "12px sans-serif";
context.textAlign = "center";
context.fillText("Guide", ox + (x + gw / 2) * scale, oy + (y + gh / 2) * scale + 4);
}
context.strokeStyle = "#65d5ff";
context.lineWidth = 2;
context.strokeRect(ox + x * scale, oy + y * scale, gw * scale, gh * scale);
context.fillStyle = "#d2f5ff";
context.font = "12px sans-serif";
context.textAlign = "center";
context.fillText("Guide", ox + (x + gw / 2) * scale, oy + (y + gh / 2) * scale + 4);
coordinates.textContent = `x ${x} · y ${y}`;
const unresolved = DIMENSIONS.some((name) => inputValue(node, name) == null);
status.textContent = `Guide ${gw}×${gh} → ${cw}×${ch}${unresolved ? " · 连线尺寸运行后同步" : ""}`;
const overflow = [gw > cw ? "宽度" : "", gh > ch ? "高度" : ""].filter(Boolean).join("和");
status.textContent = previewError || `Guide ${gw}×${gh} → 画布 ${cw}×${ch}${overflow ? ` · Guide ${overflow}超出画布` : ""}`;
}
function button(label, action) {
@@ -137,20 +198,40 @@ function createPositionEditor(node) {
element.type = "button";
element.textContent = label;
element.style.cssText = "background:#26364b;color:#e1efff;border:1px solid #506783;border-radius:4px;padding:4px 8px;cursor:pointer;";
element.addEventListener("click", () => {
element.addEventListener("click", action);
row.appendChild(element);
return element;
}
const refreshButton = button("刷新预览 / Preview", async () => {
refreshButton.disabled = true;
previewError = "";
queuedSignatures = signatures();
try {
await app.queuePrompt(0, 1, [String(node.id)]);
} catch (error) {
queuedSignatures = null;
previewError = "预览失败,请重试。";
refreshButton.title = String(error);
} finally {
refreshButton.disabled = false;
draw();
}
});
refreshButton.title = "只运行此节点及其上游,读取视频第一帧或参考图片。";
function positionButton(label, action) {
button(label, () => {
const sizes = dimensions();
if (!sizes) return;
node.graph?.beforeChange();
action(sizes);
node.graph?.afterChange();
});
row.appendChild(element);
}
button("居中 / Center", ([gw, gh, cw, ch]) => setPosition((cw - gw) / 2, (ch - gh) / 2));
button("左上 / Origin", () => setPosition(0, 0));
positionButton("居中 / Center", ([gw, gh, cw, ch]) => setPosition((cw - gw) / 2, (ch - gh) / 2));
positionButton("左上 / Origin", () => setPosition(0, 0));
row.appendChild(coordinates);
root.append(canvas, row, status);
root.title = "拖动蓝框调整左上角坐标;按住 Shift 以 32 像素对齐。";
root.title = "拖动参考图调整左上角坐标;按住 Shift 以 32 像素对齐。";
for (const type of ["pointerdown", "pointerup", "mousedown", "mouseup", "click", "dblclick"]) {
root.addEventListener(type, (event) => event.stopPropagation());
}
@@ -169,7 +250,7 @@ function createPositionEditor(node) {
canvas.style.cursor = "grabbing";
});
canvas.addEventListener("pointermove", (event) => {
if (!drag || event.pointerId !== drag.id) return;
if (!drag || event.pointerId !== drag.id || !view) return;
const step = event.shiftKey ? 32 : 1;
setPosition(Math.round((drag.x + (event.clientX - drag.px) / view.scale) / step) * step,
Math.round((drag.y + (event.clientY - drag.py) / view.scale) / step) * step);
@@ -201,11 +282,11 @@ function createPositionEditor(node) {
return result;
};
}
// Linked primitives may change without calling this node's widget callbacks.
// Linked upstream widgets and graph zoom may change without this node's callbacks.
function refresh() {
if (removed) return;
const bounds = canvas.getBoundingClientRect();
const key = JSON.stringify([dimensions(), position(), bounds.width, bounds.height]);
const key = JSON.stringify([signatures(), position(), bounds.width, bounds.height]);
if (key !== liveKey) {
liveKey = key;
draw();
@@ -216,6 +297,27 @@ function createPositionEditor(node) {
node.setSize([Math.max(node.size[0], 390), Math.max(node.size[1], node.computeSize()[1])]);
return {
draw,
executed(message) {
const keys = queuedSignatures || signatures();
queuedSignatures = null;
previewError = "";
executed = { guide: message.guide_size, canvas: message.canvas_size, signatures: keys };
preview = null;
const descriptor = message.guide_preview?.[0];
if (descriptor) {
const image = new Image();
const loaded = { image, signature: keys.guide };
preview = loaded;
image.onload = () => { if (preview === loaded) draw(); };
image.onerror = () => {
if (preview !== loaded) return;
previewError = "预览图片加载失败,请刷新预览。";
draw();
};
image.src = api.apiURL(`/view?${new URLSearchParams(descriptor)}`);
}
draw();
},
remove() {
removed = true;
cancelAnimationFrame(frame);
@@ -243,11 +345,7 @@ app.registerExtension({
const executed = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message, ...args) {
const result = executed?.call(this, message, ...args);
if (message.guide_size && message.canvas_size) {
this.properties ??= {};
this.properties.h3GuideSize = [...message.guide_size, ...message.canvas_size];
this.h3PositionEditor?.draw();
}
if (message.guide_size && message.canvas_size) this.h3PositionEditor?.executed(message);
return result;
};
const removed = nodeType.prototype.onRemoved;