Add Smart Tile prompt override
This commit is contained in:
@@ -55,6 +55,7 @@ When subdividing a masked tile with `Grid in`, the Mask mode can crop the origin
|
||||
| `TTP Smart Tile Set Preview` | Preview a tile set as a contact sheet or one selected tile. |
|
||||
| `TTP QwenVL3 Local Loader` | Load a local QwenVL tagging model from `ComfyUI/models/text_encoders`. |
|
||||
| `TTP Smart Tile QwenVL Prompt Set Builder` | Generate per-tile prompts before loop processing. |
|
||||
| `TTP Smart Tile Prompt Override` | Filter selected tiles and replace, prepend, append, or find/replace per-tile prompts before sampling. |
|
||||
| `TTP Smart Tile Semantic Rank` | Classify tiles and write recommended layer, priority, scale weight, and composite metadata. |
|
||||
| `TTP Smart Tile Loop Source` | Output one tile at a time for VAE Encode / sampler / VAE Decode. |
|
||||
| `TTP Smart Tile Loop Collect` | Collect each processed tile back into the same tile set. |
|
||||
@@ -68,6 +69,7 @@ When subdividing a masked tile with `Grid in`, the Mask mode can crop the origin
|
||||
```text
|
||||
TTP Smart Tile Interactive Crop
|
||||
-> TTP Smart Tile QwenVL Prompt Set Builder (optional)
|
||||
-> TTP Smart Tile Prompt Override (optional)
|
||||
-> TTP Smart Tile Semantic Rank (optional)
|
||||
-> TTP Smart Tile Loop Source
|
||||
-> VAE Encode / Sampler / VAE Decode
|
||||
@@ -114,6 +116,7 @@ This workflow is the recommended full Smart Tile loop. It uses `TTP Smart Tile I
|
||||
| `Interactive Crop` | `auto_detect_mode=sam3.1`, `default_pad=32`, `default_blend=32`, `auto_object_padding=64`, `auto_max_tiles=16` | Use `Auto Tile` to create an object-aware layout, then set `auto_detect_mode=none` if you want to freeze manual edits. / 用 `Auto Tile` 生成语义分块;如果之后要固定手动编辑结果,可以把 `auto_detect_mode` 改回 `none`。 |
|
||||
| `QwenVL3 Local Loader` | `qwen3vl_4b_fp8_scaled.safetensors` | Replace this with the QwenVL model installed in `ComfyUI/models/text_encoders`. / 按你本机 `models/text_encoders` 里的 QwenVL 模型替换。 |
|
||||
| `QwenVL Prompt Set Builder` | `reference_image_mode=first_message`, `prompt_preset=tile_img2img_prompt`, `output_language=chinese` | Builds prompts before the tile loop, then `Loop Source.prompt` feeds the text encoder. / 在循环前一次性生成每块提示词,然后由 `Loop Source.prompt` 送入文本编码。 |
|
||||
| `Prompt Override` | `selector_type=label`, `selector=face`, `prompt_mode=replace` | Rewrites only matched tile prompts. Use `drop_unmatched` when only selected regions should run through an edit sampler and Assemble should keep the rest from `source_image`. / 只改写匹配 tile 的提示词;局部 edit 时可用 `drop_unmatched`,其余区域由 Assemble 的 `source_image` 底图保留。 |
|
||||
| `Image Upscale Prep` | `scale=2.5`, `round_to=8`, `max_megapixels=1.5`, `use_upscale_model=true` | Enlarges each tile before img2img while keeping each tile under the megapixel cap. / 采样前放大每块 tile,同时用百万像素上限控制显存。 |
|
||||
| `Loop Source` / `Loop Collect` | `Process All Tiles` workflow | The tile count can be 4, 8, 16, or any layout count; no manual sampler duplication is needed. / 分块数量可以变化,不需要手动复制多套 sampler。 |
|
||||
| `Output Size Estimate` | `focus_weighted` | Estimates the final canvas from processed tile sizes, giving more influence to focus/detail tiles. / 根据处理后的 tile 尺寸估算最终画布,并更重视细节块。 |
|
||||
|
||||
+251
@@ -4087,6 +4087,255 @@ class TTP_Smart_Tile_QwenVL_Prompt_Set_Builder_Experimental:
|
||||
return (next_tile_set, prompt_set_json, summary)
|
||||
|
||||
|
||||
class TTP_Smart_Tile_Prompt_Override_Experimental:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
text_modes = ["keep", "replace", "prepend", "append", "find_replace", "clear"]
|
||||
return {
|
||||
"required": {
|
||||
"tile_set": ("TTP_SMART_TILE_SET", {"forceInput": True}),
|
||||
"selector_type": ([
|
||||
"all",
|
||||
"index",
|
||||
"name",
|
||||
"label",
|
||||
"prompt_tag",
|
||||
"source",
|
||||
"semantic_category",
|
||||
"semantic_role",
|
||||
"prompt_contains",
|
||||
"regex",
|
||||
], {"default": "label"}),
|
||||
"selector": ("STRING", {"default": "", "multiline": False}),
|
||||
"unmatched_mode": (["keep", "drop_unmatched"], {"default": "keep"}),
|
||||
"prompt_mode": (text_modes, {"default": "replace"}),
|
||||
"prompt_text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False}),
|
||||
"negative_mode": (text_modes, {"default": "keep"}),
|
||||
"negative_text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False}),
|
||||
"find_text": ("STRING", {"default": "", "multiline": False, "dynamicPrompts": False}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("TTP_SMART_TILE_SET", "STRING", "STRING")
|
||||
RETURN_NAMES = ("tile_set", "override_json", "summary")
|
||||
FUNCTION = "override_prompts"
|
||||
CATEGORY = "TTP/Smart Tile"
|
||||
|
||||
@staticmethod
|
||||
def _selector_terms(selector):
|
||||
return [
|
||||
part.strip()
|
||||
for part in re.split(r"[,;\n]+", str(selector or ""))
|
||||
if part.strip()
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _selector_index_set(selector, count):
|
||||
indexes = set()
|
||||
for term in TTP_Smart_Tile_Prompt_Override_Experimental._selector_terms(selector):
|
||||
cleaned = term.strip()
|
||||
t_range = re.match(r"^[tT]\s*(\d+)\s*-\s*[tT]?\s*(\d+)$", cleaned)
|
||||
zero_range = re.match(r"^(\d+)\s*-\s*(\d+)$", cleaned)
|
||||
t_single = re.match(r"^[tT]\s*(\d+)$", cleaned)
|
||||
zero_single = re.match(r"^\d+$", cleaned)
|
||||
if t_range:
|
||||
start = int(t_range.group(1)) - 1
|
||||
end = int(t_range.group(2)) - 1
|
||||
elif zero_range:
|
||||
start = int(zero_range.group(1))
|
||||
end = int(zero_range.group(2))
|
||||
elif t_single:
|
||||
start = end = int(t_single.group(1)) - 1
|
||||
elif zero_single:
|
||||
start = end = int(cleaned)
|
||||
else:
|
||||
continue
|
||||
if end < start:
|
||||
start, end = end, start
|
||||
for index in range(start, end + 1):
|
||||
if 0 <= index < count:
|
||||
indexes.add(index)
|
||||
return indexes
|
||||
|
||||
@staticmethod
|
||||
def _match_tile(tile, index, count, selector_type, selector):
|
||||
selector_type = str(selector_type or "label")
|
||||
if selector_type == "all":
|
||||
return True
|
||||
if selector_type == "index":
|
||||
return index in TTP_Smart_Tile_Prompt_Override_Experimental._selector_index_set(selector, count)
|
||||
|
||||
terms = [term.lower() for term in TTP_Smart_Tile_Prompt_Override_Experimental._selector_terms(selector)]
|
||||
if not terms:
|
||||
return False
|
||||
|
||||
fields = {
|
||||
"name": [tile.get("name", "")],
|
||||
"label": [tile.get("label", "")],
|
||||
"prompt_tag": [tile.get("prompt_tag", "")],
|
||||
"source": [tile.get("source", "")],
|
||||
"semantic_category": [tile.get("semantic_category", "")],
|
||||
"semantic_role": [tile.get("semantic_role", "")],
|
||||
"prompt_contains": [tile.get("prompt", ""), tile.get("negative", ""), tile.get("caption", "")],
|
||||
}
|
||||
searchable = fields.get(selector_type)
|
||||
if selector_type == "regex":
|
||||
searchable = [
|
||||
str(index),
|
||||
str(index + 1),
|
||||
tile.get("name", ""),
|
||||
tile.get("label", ""),
|
||||
tile.get("prompt_tag", ""),
|
||||
tile.get("source", ""),
|
||||
tile.get("semantic_category", ""),
|
||||
tile.get("semantic_role", ""),
|
||||
tile.get("caption", ""),
|
||||
tile.get("prompt", ""),
|
||||
tile.get("negative", ""),
|
||||
]
|
||||
text = "\n".join(str(value or "") for value in searchable)
|
||||
try:
|
||||
return bool(re.search(str(selector or ""), text, flags=re.IGNORECASE))
|
||||
except re.error as exc:
|
||||
raise ValueError(f"Invalid Smart Tile prompt override regex: {exc}") from exc
|
||||
if searchable is None:
|
||||
searchable = [
|
||||
tile.get("name", ""),
|
||||
tile.get("label", ""),
|
||||
tile.get("prompt_tag", ""),
|
||||
tile.get("source", ""),
|
||||
tile.get("caption", ""),
|
||||
tile.get("prompt", ""),
|
||||
]
|
||||
text = "\n".join(str(value or "").lower() for value in searchable)
|
||||
return any(term in text for term in terms)
|
||||
|
||||
@staticmethod
|
||||
def _join_text(prefix, suffix):
|
||||
prefix = str(prefix or "").strip()
|
||||
suffix = str(suffix or "").strip()
|
||||
if prefix and suffix:
|
||||
return f"{prefix}\n{suffix}"
|
||||
return prefix or suffix
|
||||
|
||||
@staticmethod
|
||||
def _apply_text_mode(current, mode, text, find_text):
|
||||
current = str(current or "")
|
||||
mode = str(mode or "keep")
|
||||
text = str(text or "").strip()
|
||||
find_text = str(find_text or "")
|
||||
if mode == "keep":
|
||||
return current
|
||||
if mode == "clear":
|
||||
return ""
|
||||
if mode == "replace":
|
||||
return text
|
||||
if mode == "prepend":
|
||||
return TTP_Smart_Tile_Prompt_Override_Experimental._join_text(text, current)
|
||||
if mode == "append":
|
||||
return TTP_Smart_Tile_Prompt_Override_Experimental._join_text(current, text)
|
||||
if mode == "find_replace":
|
||||
return current.replace(find_text, text) if find_text else current
|
||||
return current
|
||||
|
||||
@staticmethod
|
||||
def _position_for_tile(tile):
|
||||
box = tile.get("paste_box", tile.get("core_box", [0, 0, 1, 1]))
|
||||
x, y, w, h = [int(round(float(value))) for value in box[:4]]
|
||||
return (x, y, x + max(1, w), y + max(1, h))
|
||||
|
||||
def override_prompts(
|
||||
self,
|
||||
tile_set,
|
||||
selector_type="label",
|
||||
selector="",
|
||||
unmatched_mode="keep",
|
||||
prompt_mode="replace",
|
||||
prompt_text="",
|
||||
negative_mode="keep",
|
||||
negative_text="",
|
||||
find_text="",
|
||||
):
|
||||
tile_images, _tile_meta, tiles_info = _ttp_validate_tile_set(tile_set)
|
||||
next_tile_set = _ttp_clone_tile_set(tile_set)
|
||||
next_tiles = next_tile_set["tile_meta"]["tiles"]
|
||||
next_images = list(next_tile_set.get("tile_images", []))
|
||||
next_positions = list(next_tile_set.get("positions", []))
|
||||
|
||||
kept_tiles = []
|
||||
kept_images = []
|
||||
kept_positions = []
|
||||
records = []
|
||||
matched_count = 0
|
||||
drop_unmatched = str(unmatched_mode or "keep") == "drop_unmatched"
|
||||
count = len(tiles_info)
|
||||
|
||||
for index, tile in enumerate(next_tiles):
|
||||
matched = self._match_tile(tile, index, count, selector_type, selector)
|
||||
if not matched and drop_unmatched:
|
||||
continue
|
||||
|
||||
updated = dict(tile)
|
||||
if matched:
|
||||
matched_count += 1
|
||||
before_prompt = str(updated.get("prompt", ""))
|
||||
before_negative = str(updated.get("negative", ""))
|
||||
updated["prompt"] = self._apply_text_mode(before_prompt, prompt_mode, prompt_text, find_text)
|
||||
updated["negative"] = self._apply_text_mode(before_negative, negative_mode, negative_text, find_text)
|
||||
existing_source = str(updated.get("prompt_source", "") or "")
|
||||
updated["prompt_source"] = f"{existing_source}+override" if existing_source else "override"
|
||||
updated["prompt_override"] = True
|
||||
updated["prompt_override_selector_type"] = str(selector_type or "")
|
||||
updated["prompt_override_selector"] = str(selector or "")
|
||||
records.append({
|
||||
"index": int(index),
|
||||
"name": updated.get("name", f"tile_{index}"),
|
||||
"label": updated.get("label", ""),
|
||||
"prompt_before": before_prompt,
|
||||
"prompt_after": updated["prompt"],
|
||||
"negative_before": before_negative,
|
||||
"negative_after": updated["negative"],
|
||||
})
|
||||
|
||||
kept_tiles.append(updated)
|
||||
kept_images.append(next_images[index] if index < len(next_images) else tile_images[index])
|
||||
if index < len(next_positions):
|
||||
kept_positions.append(next_positions[index])
|
||||
else:
|
||||
kept_positions.append(self._position_for_tile(updated))
|
||||
|
||||
if not kept_tiles:
|
||||
raise ValueError("Smart Tile prompt override kept no tiles. Change selector or unmatched_mode.")
|
||||
|
||||
for new_index, tile in enumerate(kept_tiles):
|
||||
tile["id"] = new_index
|
||||
|
||||
next_tile_set["tile_meta"]["tiles"] = kept_tiles
|
||||
next_tile_set["tile_images"] = kept_images
|
||||
next_tile_set["positions"] = kept_positions
|
||||
|
||||
override_json = json.dumps({
|
||||
"type": "ttp_smart_tile_prompt_override",
|
||||
"selector_type": str(selector_type or ""),
|
||||
"selector": str(selector or ""),
|
||||
"unmatched_mode": str(unmatched_mode or "keep"),
|
||||
"prompt_mode": str(prompt_mode or "keep"),
|
||||
"negative_mode": str(negative_mode or "keep"),
|
||||
"matched": int(matched_count),
|
||||
"kept": int(len(kept_tiles)),
|
||||
"dropped": int(count - len(kept_tiles)),
|
||||
"tiles": records,
|
||||
}, ensure_ascii=False, indent=2)
|
||||
summary_lines = [
|
||||
f"matched={matched_count} kept={len(kept_tiles)} dropped={count - len(kept_tiles)} selector={selector_type}:{selector}"
|
||||
]
|
||||
summary_lines.extend(
|
||||
f'{record["index"]}:{record["name"]} -> {_ttp_compact_text(record["prompt_after"], 160)}'
|
||||
for record in records
|
||||
)
|
||||
return (next_tile_set, override_json, "\n".join(summary_lines))
|
||||
|
||||
|
||||
class TTP_Smart_Tile_Loop_Source_Experimental:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -6092,6 +6341,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"TTP_Smart_Tile_Set_Preview_Experimental": TTP_Smart_Tile_Set_Preview_Experimental,
|
||||
"TTP_QwenVL3_Local_Loader_Experimental": TTP_QwenVL3_Local_Loader_Experimental,
|
||||
"TTP_Smart_Tile_QwenVL_Prompt_Set_Builder_Experimental": TTP_Smart_Tile_QwenVL_Prompt_Set_Builder_Experimental,
|
||||
"TTP_Smart_Tile_Prompt_Override_Experimental": TTP_Smart_Tile_Prompt_Override_Experimental,
|
||||
"TTP_Smart_Tile_Loop_Source_Experimental": TTP_Smart_Tile_Loop_Source_Experimental,
|
||||
"TTP_Smart_Tile_Loop_Collect_Experimental": TTP_Smart_Tile_Loop_Collect_Experimental,
|
||||
"TTP_Smart_Tile_Image_Upscale_Prep_Experimental": TTP_Smart_Tile_Image_Upscale_Prep_Experimental,
|
||||
@@ -6117,6 +6367,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"TTP_Smart_Tile_Set_Preview_Experimental": "TTP Smart Tile Set Preview",
|
||||
"TTP_QwenVL3_Local_Loader_Experimental": "TTP QwenVL3 Local Loader",
|
||||
"TTP_Smart_Tile_QwenVL_Prompt_Set_Builder_Experimental": "TTP Smart Tile QwenVL Prompt Set Builder",
|
||||
"TTP_Smart_Tile_Prompt_Override_Experimental": "TTP Smart Tile Prompt Override",
|
||||
"TTP_Smart_Tile_Loop_Source_Experimental": "TTP Smart Tile Loop Source",
|
||||
"TTP_Smart_Tile_Loop_Collect_Experimental": "TTP Smart Tile Loop Collect",
|
||||
"TTP_Smart_Tile_Image_Upscale_Prep_Experimental": "TTP Smart Tile Image Upscale Prep",
|
||||
|
||||
@@ -257,6 +257,82 @@ assert_equal(
|
||||
ttp.TTP_Smart_Tile_QwenVL_Prompt_Set_Builder_Experimental,
|
||||
"prompt builder should be registered",
|
||||
)
|
||||
prompt_override_inputs = ttp.TTP_Smart_Tile_Prompt_Override_Experimental.INPUT_TYPES()
|
||||
assert_equal(prompt_override_inputs["required"]["tile_set"][0], "TTP_SMART_TILE_SET", "prompt override should accept Smart Tile Set")
|
||||
assert_equal(prompt_override_inputs["required"]["tile_set"][1].get("forceInput"), True, "prompt override tile_set should be a forceInput connection")
|
||||
assert_equal("regex" in prompt_override_inputs["required"]["selector_type"][0], True, "prompt override should support regex selectors")
|
||||
assert_equal(prompt_override_inputs["required"]["unmatched_mode"][0], ["keep", "drop_unmatched"], "prompt override should allow dropping unmatched tiles")
|
||||
assert_equal(prompt_override_inputs["required"]["prompt_text"][1]["dynamicPrompts"], False, "prompt override text should not use dynamic prompt parsing")
|
||||
assert_equal(
|
||||
ttp.NODE_CLASS_MAPPINGS["TTP_Smart_Tile_Prompt_Override_Experimental"],
|
||||
ttp.TTP_Smart_Tile_Prompt_Override_Experimental,
|
||||
"prompt override should be registered",
|
||||
)
|
||||
|
||||
override_tile_set = {
|
||||
"version": 1,
|
||||
"type": "ttp_smart_tile_set",
|
||||
"original_size": [100, 100],
|
||||
"tile_meta": {
|
||||
"version": 3,
|
||||
"type": "ttp_smart_tile",
|
||||
"storage": "tile_set",
|
||||
"original_size": [100, 100],
|
||||
"tiles": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "tile_1",
|
||||
"label": "face",
|
||||
"source": "paint_mask",
|
||||
"core_box": [0, 0, 50, 50],
|
||||
"sample_box": [0, 0, 50, 50],
|
||||
"paste_box": [0, 0, 50, 50],
|
||||
"prompt": "old face prompt",
|
||||
"negative": "old face negative",
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "tile_2",
|
||||
"label": "background",
|
||||
"source": "manual",
|
||||
"core_box": [50, 0, 50, 50],
|
||||
"sample_box": [50, 0, 50, 50],
|
||||
"paste_box": [50, 0, 50, 50],
|
||||
"prompt": "old background prompt",
|
||||
"negative": "old background negative",
|
||||
},
|
||||
],
|
||||
},
|
||||
"tile_images": ["face_image", "background_image"],
|
||||
"positions": [(0, 0, 50, 50), (50, 0, 100, 50)],
|
||||
}
|
||||
override_node = ttp.TTP_Smart_Tile_Prompt_Override_Experimental()
|
||||
overridden_set, override_json, override_summary = override_node.override_prompts(
|
||||
override_tile_set,
|
||||
selector_type="label",
|
||||
selector="face",
|
||||
unmatched_mode="keep",
|
||||
prompt_mode="replace",
|
||||
prompt_text="edit only the face",
|
||||
negative_mode="append",
|
||||
negative_text="avoid changing identity",
|
||||
)
|
||||
assert_equal(overridden_set["tile_meta"]["tiles"][0]["prompt"], "edit only the face", "prompt override should replace matched tile prompt")
|
||||
assert_equal(overridden_set["tile_meta"]["tiles"][0]["negative"], "old face negative\navoid changing identity", "prompt override should append matched tile negative")
|
||||
assert_equal(overridden_set["tile_meta"]["tiles"][1]["prompt"], "old background prompt", "prompt override should keep unmatched tile prompt")
|
||||
assert_equal(json.loads(override_json)["matched"], 1, "prompt override JSON should report matched tile count")
|
||||
assert_equal("matched=1" in override_summary, True, "prompt override summary should report matched count")
|
||||
dropped_set, _dropped_json, _dropped_summary = override_node.override_prompts(
|
||||
override_tile_set,
|
||||
selector_type="label",
|
||||
selector="face",
|
||||
unmatched_mode="drop_unmatched",
|
||||
prompt_mode="replace",
|
||||
prompt_text="edit only the face",
|
||||
)
|
||||
assert_equal(len(dropped_set["tile_meta"]["tiles"]), 1, "prompt override should drop unmatched tile metadata")
|
||||
assert_equal(len(dropped_set["tile_images"]), 1, "prompt override should drop unmatched tile images")
|
||||
assert_equal(dropped_set["tile_meta"]["tiles"][0]["id"], 0, "prompt override should reindex kept tile ids")
|
||||
loop_source_inputs = ttp.TTP_Smart_Tile_Loop_Source_Experimental.INPUT_TYPES()
|
||||
assert_equal(loop_source_inputs["required"]["tile_set"][0], "TTP_SMART_TILE_SET", "loop source should accept Smart Tile Set")
|
||||
assert_equal(loop_source_inputs["optional"]["clip"][0], "CLIP", "loop source should optionally encode tile prompts with CLIP")
|
||||
|
||||
@@ -76,6 +76,7 @@ assert(/auto_paint_mask/.test(source), "tile editor should sync painted masks in
|
||||
assert(/Brush/.test(source), "tile editor should expose a paint brush action");
|
||||
assert(/Erase/.test(source), "tile editor should expose an erase action");
|
||||
assert(/Mask to Tile/.test(source), "tile editor should expose a button that commits painted masks into layout tiles");
|
||||
assert(/Mask Replace/.test(source), "tile editor should expose a button that replaces the layout with painted mask tiles");
|
||||
assert(/Refresh masks/.test(source), "tile editor should expose a refresh masks action");
|
||||
assert(/refreshInheritedMasks\(node, tiles, gridMaskMode\(node\)\)/.test(source), "Refresh masks should use the current grid mask mode");
|
||||
assert(/function addPaintMaskTiles/.test(source), "tile editor should convert painted masks into persisted tiles");
|
||||
|
||||
@@ -645,7 +645,7 @@ function maskCropData(canvas, box) {
|
||||
return crop.toDataURL("image/png").split(",", 2)[1];
|
||||
}
|
||||
|
||||
function addPaintMaskTiles(node, tiles, selectedIndex) {
|
||||
function addPaintMaskTiles(node, tiles, selectedIndex, replaceExisting = false) {
|
||||
const canvas = ensurePaintMaskCanvas(node);
|
||||
const size = imageSourceSize(node);
|
||||
if (!canvas || !size || !paintMaskHasPixels(canvas)) {
|
||||
@@ -659,7 +659,7 @@ function addPaintMaskTiles(node, tiles, selectedIndex) {
|
||||
}
|
||||
const padding = Math.max(0, Math.round(Number(widgetByName(node, "auto_object_padding")?.value ?? 96)));
|
||||
const defaults = layoutDefaults(node);
|
||||
const nextTiles = [...tiles];
|
||||
const nextTiles = replaceExisting ? [] : [...tiles];
|
||||
let added = 0;
|
||||
for (const component of components) {
|
||||
if (nextTiles.length >= MAX_TILES) {
|
||||
@@ -700,10 +700,15 @@ function addPaintMaskTiles(node, tiles, selectedIndex) {
|
||||
node.ttpSmartTileStatus = `Mask has ${components.length} region(s), but max ${MAX_TILES} tiles is reached.`;
|
||||
return false;
|
||||
}
|
||||
writeLayout(node, nextTiles, Math.min(nextTiles.length - 1, Math.max(0, selectedIndex + added)));
|
||||
const nextSelected = replaceExisting
|
||||
? Math.max(0, added - 1)
|
||||
: Math.min(nextTiles.length - 1, Math.max(0, selectedIndex + added));
|
||||
writeLayout(node, nextTiles, nextSelected);
|
||||
clearPaintMask(node);
|
||||
node.ttpSmartTilePaintMode = "off";
|
||||
node.ttpSmartTileStatus = `Mask to Tile added ${added} tile(s).`;
|
||||
node.ttpSmartTileStatus = replaceExisting
|
||||
? `Mask Replace created ${added} tile(s).`
|
||||
: `Mask to Tile added ${added} tile(s).`;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1802,6 +1807,13 @@ function renderEditor(node) {
|
||||
renderEditor(node);
|
||||
}
|
||||
}, !paintMaskHasPixels(ensurePaintMaskCanvas(node)) || tiles.length >= MAX_TILES),
|
||||
createButton("Mask Replace", () => {
|
||||
if (addPaintMaskTiles(node, tiles, 0, true)) {
|
||||
renderEditor(node);
|
||||
} else {
|
||||
renderEditor(node);
|
||||
}
|
||||
}, !paintMaskHasPixels(ensurePaintMaskCanvas(node))),
|
||||
createButton("Refresh masks", async () => {
|
||||
updateGridValues();
|
||||
const refreshed = await refreshInheritedMasks(node, tiles, gridMaskMode(node)).catch((error) => {
|
||||
|
||||
Reference in New Issue
Block a user