Add ordered Regional LoRA operations
This commit is contained in:
@@ -199,6 +199,16 @@ Unsupported operations are absent from the UI and rejected by the backend. A
|
||||
transformer always produces a complete valid capability; half-configured runtime
|
||||
contexts are forbidden.
|
||||
|
||||
The dedicated Regional LoRA transformer may persist an ordered list of operation
|
||||
steps. Every step owns a stable ID, one global or regional target, its operation,
|
||||
and its own ordered LoRA entries. Targets are repeatable, and drag-and-drop order
|
||||
is execution order. The backend applies each step through the ordinary registered
|
||||
capability handler, so one node with `merge(region A)` followed by
|
||||
`subtract(region B)` is semantically identical to two transformer nodes in that
|
||||
order. `replace` and `clear` therefore retain their normal capability-wide effect
|
||||
at their position in the sequence. The original single-operation configuration
|
||||
remains accepted and is normalized deterministically as the Legacy fallback.
|
||||
|
||||
If a downstream configuration references a region removed upstream, that
|
||||
transformer fails before emitting an output. It reports its node, missing
|
||||
`document_id + region_id`, and provides an action to open the affected editor.
|
||||
|
||||
+25
-25
File diff suppressed because one or more lines are too long
@@ -38,6 +38,7 @@ from ..util.regional.lora_v3 import (
|
||||
build_lora_provider,
|
||||
resolve_lora_capability,
|
||||
transform_lora_capability,
|
||||
transform_lora_sequence,
|
||||
)
|
||||
from ..util.regional.sdxl_attention import compile_sdxl_attention, apply_sdxl_attention_patch
|
||||
from ..util.regional.zimage_attention import compile_zimage_attention, apply_zimage_attention_patch
|
||||
@@ -177,8 +178,11 @@ class BVRegionalLoraNode:
|
||||
|
||||
def transform(self, regional, operation, config_json, resource_provider=None):
|
||||
payload = json.loads(config_json) if isinstance(config_json, str) else config_json
|
||||
transformed = transform_lora_capability(regional, payload, registry=LORA_CAPABILITY_REGISTRY, operation=operation)
|
||||
if operation != "clear" and any(entry["source"]["kind"] == "external" for entry in payload["entries"]):
|
||||
transformed = transform_lora_sequence(
|
||||
regional, payload, registry=LORA_CAPABILITY_REGISTRY, fallback_operation=operation
|
||||
)
|
||||
result = transformed.capabilities.get(LORA_CAPABILITY)
|
||||
if result is not None and any(entry["source"]["kind"] == "external" for entry in result["entries"]):
|
||||
resolve_lora_capability(transformed, resource_provider, registry=LORA_CAPABILITY_REGISTRY)
|
||||
return (transformed.to_dict(),)
|
||||
|
||||
|
||||
@@ -188,6 +188,63 @@ def transform_lora_capability(value: Any, payload: Any, *, registry: CapabilityR
|
||||
return context.with_capability(LORA_CAPABILITY, result)
|
||||
|
||||
|
||||
def _same_lora_target(left: dict[str, Any], right: dict[str, Any]) -> bool:
|
||||
return left.get("scope") == right.get("scope") and (
|
||||
right.get("scope") == "global"
|
||||
or (left.get("document_id") == right.get("document_id") and left.get("region_id") == right.get("region_id"))
|
||||
)
|
||||
|
||||
|
||||
def transform_lora_sequence(
|
||||
value: Any,
|
||||
config: Any,
|
||||
*,
|
||||
registry: CapabilityRegistry,
|
||||
fallback_operation: str = "replace",
|
||||
) -> RegionalContext:
|
||||
"""Apply a transformer config as one or more ordinary LoRA capability operations."""
|
||||
if not isinstance(config, dict):
|
||||
raise RegionalContextError("LoRA transformer config must be an object")
|
||||
if config.get("version") == 1:
|
||||
return transform_lora_capability(value, config, registry=registry, operation=fallback_operation)
|
||||
if config.get("version") != 2 or set(config) != {"version", "collector_id", "entries", "steps"}:
|
||||
raise RegionalContextError("LoRA transformer config version 2 has unknown or missing fields")
|
||||
if config["entries"]:
|
||||
raise RegionalContextError("LoRA transformer config version 2 stores entries inside steps")
|
||||
steps = config["steps"]
|
||||
if not isinstance(steps, list):
|
||||
raise RegionalContextError("LoRA transformer steps must be an array")
|
||||
context = normalize_context(value, registry=registry)
|
||||
step_ids: set[str] = set()
|
||||
for index, step in enumerate(steps):
|
||||
path = f"LoRA transformer steps[{index}]"
|
||||
if not isinstance(step, dict) or set(step) != {"id", "operation", "target", "entries"}:
|
||||
raise RegionalContextError(f"{path} has unknown or missing fields")
|
||||
step_id = _required_uuid(step["id"], f"{path}.id")
|
||||
if step_id in step_ids:
|
||||
raise RegionalContextError(f"duplicate LoRA transformer step id: {step_id}")
|
||||
step_ids.add(step_id)
|
||||
operation = step["operation"]
|
||||
if operation not in {"replace", "merge", "subtract", "clear"}:
|
||||
raise RegionalContextError(f"{path}.operation is unsupported")
|
||||
target = step["target"]
|
||||
if not isinstance(target, dict) or target.get("scope") not in {"global", "region"}:
|
||||
raise RegionalContextError(f"{path}.target must target global or region scope")
|
||||
if target["scope"] == "global" and set(target) != {"scope"}:
|
||||
raise RegionalContextError(f"{path}.target global target has unknown fields")
|
||||
if target["scope"] == "region":
|
||||
if set(target) != {"scope", "document_id", "region_id"}:
|
||||
raise RegionalContextError(f"{path}.target region target has unknown or missing fields")
|
||||
_required_uuid(target["document_id"], f"{path}.target.document_id")
|
||||
_required_uuid(target["region_id"], f"{path}.target.region_id")
|
||||
scoped = {"version": 1, "collector_id": config["collector_id"], "entries": step["entries"]}
|
||||
validate_lora_capability(scoped)
|
||||
if any(len(entry["targets"]) != 1 or not _same_lora_target(entry["targets"][0], target) for entry in scoped["entries"]):
|
||||
raise RegionalContextError(f"{path}.entries must target only the step scope")
|
||||
context = transform_lora_capability(context, scoped, registry=registry, operation=operation)
|
||||
return context
|
||||
|
||||
|
||||
def _provider_resources(provider: Any, expected_id: str | None) -> dict[str, Any]:
|
||||
if expected_id is None:
|
||||
if provider is not None:
|
||||
|
||||
@@ -7,6 +7,7 @@ import {LoraV3ResourcePickerPanel,OptionalLoraV3ScopePicker,withoutLoraV3Target}
|
||||
import {emptyLoraV3Config,parseLoraV3Config,serializeLoraV3Config} from "../ui/src/regional/loraV3Config.ts";
|
||||
|
||||
const collectors=[{id:"collector",label:"Styles",resources:[{id:"skin",label:"Skin"}]}];
|
||||
const editorSource=readFileSync(new URL("../ui/src/regional/LoraV3EditorWindow.tsx",import.meta.url),"utf8");
|
||||
test("the production picker renders Collector / Resource from persisted ids",()=>{
|
||||
const config={version:1,collector_id:"collector",entries:[{id:"entry",source:{kind:"external",resource_id:"skin"},targets:[{scope:"global"}]}]};
|
||||
const html=renderToStaticMarkup(React.createElement(LoraV3ResourcePickerPanel,{collectors,config,resolved:true,targetOptions:[{value:"global",label:"Global",target:{scope:"global"}}],onCollector(){},onResource(){},onAddExternal(){}}));
|
||||
@@ -37,6 +38,12 @@ test("Regional Prompt easy mode renders every stack assigned to its scope",()=>{
|
||||
const config={version:1,collector_id:"collector",entries:[{id:"first",source:{kind:"external",resource_id:"skin"},targets:[{scope:"global"}]},{id:"second",source:{kind:"external",resource_id:"skin"},targets:[{scope:"global"}]}]};
|
||||
const html=renderToStaticMarkup(React.createElement(OptionalLoraV3ScopePicker,{collectors,config,target:{scope:"global"},resolved:true,onCollector(){},onResource(){},onAdd(){},onRemove(){},onClear(){}}));
|
||||
assert.match(html,/LoRA enabled/);assert.match(html,/LoRA Stack 1/);assert.match(html,/LoRA Stack 2/);assert.match(html,/Add LoRA stack/);
|
||||
assert.match(html,/aria-label="Remove LoRA Stack 1"/);assert.doesNotMatch(html,/>Remove</);
|
||||
});
|
||||
test("the LoRA editor exposes ordered repeatable scope operations and participates in window visibility",()=>{
|
||||
assert.match(editorSource,/Add step/);assert.match(editorSource,/<SortableList/);assert.match(editorSource,/onReorder=/);
|
||||
assert.match(editorSource,/label="Target"/);assert.match(editorSource,/label="Operation"/);assert.match(editorSource,/crypto\.randomUUID\(\)/);
|
||||
assert.match(editorSource,/menuVisible=\{menuVisible\}/);assert.match(editorSource,/onMenuVisible=\{visible=>setWindowMenuVisible/);
|
||||
});
|
||||
test("removing one scope preserves shared assignments and the input config",()=>{
|
||||
const original={version:1,collector_id:"collector",entries:[{id:"shared",source:{kind:"external",resource_id:"skin"},targets:[{scope:"global"},{scope:"region",document_id:"doc",region_id:"left"}]}]};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from py.util.regional.context import normalize_context
|
||||
@@ -8,6 +10,7 @@ from py.util.regional.lora_v3 import (
|
||||
register_lora_contracts,
|
||||
resolve_lora_capability,
|
||||
transform_lora_capability,
|
||||
transform_lora_sequence,
|
||||
)
|
||||
|
||||
|
||||
@@ -106,6 +109,35 @@ class RegionalLoraV3Tests(unittest.TestCase):
|
||||
self.assertNotIn(LORA_CAPABILITY, cleared.capabilities)
|
||||
self.assertEqual(cleared.core, self.context.core)
|
||||
|
||||
def test_scope_steps_match_two_sequential_transformer_nodes(self):
|
||||
with (Path(__file__).parent / "fixtures" / "regional" / "v1_hybrid_joint.json").open(encoding="utf-8") as handle:
|
||||
context = normalize_context(json.load(handle), registry=self.capabilities)
|
||||
first_region, second_region = [region["id"] for region in context.core["regions"][:2]]
|
||||
collector_id = "22222222-2222-4222-8222-222222222222"
|
||||
removed_id = "33333333-3333-4333-8333-333333333333"
|
||||
added_id = "44444444-4444-4444-8444-444444444444"
|
||||
upstream = transform_lora_capability(context, {"version": 1, "collector_id": collector_id, "entries": [{"id": removed_id, "source": {"kind": "external", "resource_id": "remove"}, "targets": [{"scope": "region", "document_id": context.core["document_id"], "region_id": second_region}]}]}, registry=self.capabilities)
|
||||
config = {"version": 2, "collector_id": collector_id, "entries": [], "steps": [
|
||||
{"id": "55555555-5555-4555-8555-555555555555", "operation": "merge", "target": {"scope": "region", "document_id": context.core["document_id"], "region_id": first_region}, "entries": [
|
||||
{"id": added_id, "source": {"kind": "external", "resource_id": "add"}, "targets": [{"scope": "region", "document_id": context.core["document_id"], "region_id": first_region}]},
|
||||
]},
|
||||
{"id": "66666666-6666-4666-8666-666666666666", "operation": "subtract", "target": {"scope": "region", "document_id": context.core["document_id"], "region_id": second_region}, "entries": [
|
||||
{"id": removed_id, "source": {"kind": "external", "resource_id": "remove"}, "targets": [{"scope": "region", "document_id": context.core["document_id"], "region_id": second_region}]},
|
||||
]},
|
||||
]}
|
||||
transformed = transform_lora_sequence(upstream, config, registry=self.capabilities)
|
||||
self.assertEqual([entry["id"] for entry in transformed.require_capability(LORA_CAPABILITY)["entries"]], [added_id])
|
||||
|
||||
def test_same_region_can_appear_twice_and_order_remains_semantic(self):
|
||||
target = {"scope": "global"}
|
||||
entry = {"id": "33333333-3333-4333-8333-333333333333", "source": {"kind": "native", "lora_name": "style.safetensors", "model_strength": 1.0, "clip_strength": 1.0}, "targets": [target]}
|
||||
config = {"version": 2, "collector_id": None, "entries": [], "steps": [
|
||||
{"id": "44444444-4444-4444-8444-444444444444", "operation": "merge", "target": target, "entries": [entry]},
|
||||
{"id": "55555555-5555-4555-8555-555555555555", "operation": "subtract", "target": target, "entries": [entry]},
|
||||
]}
|
||||
transformed = transform_lora_sequence(self.context, config, registry=self.capabilities)
|
||||
self.assertEqual(transformed.require_capability(LORA_CAPABILITY)["entries"], [])
|
||||
|
||||
def test_registered_resource_type_rejects_runtime_objects(self):
|
||||
provider = build_lora_provider("22222222-2222-4222-8222-222222222222", {})
|
||||
reference = {"provider_id": provider["provider_id"], "resource_type": LORA_RESOURCE_TYPE, "resource_id": "skin"}
|
||||
|
||||
@@ -89,7 +89,8 @@ test("toolbar and node-button activation use the same keep-mode lifecycle", () =
|
||||
|
||||
test("ComfyUI toolbar exposes a node-aware multi-column window launcher", () => {
|
||||
assert.match(uiEntry, /<ToolbarWindowLauncher\s+getColumns=\{launcherColumns\}/);
|
||||
for (const label of ["Regional Prompts", "Detailer Plan", "Detector Registry"]) assert.match(uiEntry, new RegExp(`label:\"${label}\"`));
|
||||
for (const label of ["Regional Prompts", "Regional LoRA", "Detailer Plan", "Detector Registry"]) assert.match(uiEntry, new RegExp(`label:\"${label}\"`));
|
||||
assert.match(uiEntry,/workflowNodesOfType\("BV Regional LoRA"\)/);
|
||||
assert.match(uiEntry,/secondary:\{label:`Quick edit/);
|
||||
assert.match(toolbarLauncher, /filter\(column=>column\.items\.length\)/);
|
||||
assert.match(toolbarLauncher, /\.bv-regional-action/);
|
||||
@@ -108,6 +109,7 @@ test("toolbar primary actions restore the last active node per editor type", ()
|
||||
assert.match(regionalEditor, /rememberBvWindowInstance\("regional",nextId\)/);
|
||||
assert.match(uiEntry, /rememberBvWindowInstance\("detailer",scopedNodeKey\(node\)\)/);
|
||||
assert.match(uiEntry, /rememberBvWindowInstance\("detector",scopedNodeKey\(node\)\)/);
|
||||
assert.match(uiEntry, /rememberBvWindowInstance\("lora",scopedNodeKey\(node\)\)/);
|
||||
});
|
||||
|
||||
test("detector and detailer editors use shared compact collection and grid modules", () => {
|
||||
|
||||
+1
-1
@@ -586,5 +586,5 @@
|
||||
.lg-node:has([node-type="BV M0 Fake Resource Collector"]) [node-type="BV M0 Fake Resource Collector"]:has(input[aria-label="collector_id"]),.lg-node:has([node-type="BV M0 Fake Resource Collector"]) [node-type="BV M0 Fake Resource Collector"]:has(input[aria-label="resource_a_id"]),.lg-node:has([node-type="BV M0 Fake Resource Collector"]) [node-type="BV M0 Fake Resource Collector"]:has(input[aria-label="resource_b_id"]),.lg-node:has([node-type="BV M0 Fake Resource Consumer"]) [node-type="BV M0 Fake Resource Consumer"]:has(input[aria-label="collector_id"]),.lg-node:has([node-type="BV M0 Fake Resource Consumer"]) [node-type="BV M0 Fake Resource Consumer"]:has(input[aria-label="resource_id"]){display:none}
|
||||
.lg-slot:has(circle[fill*="BV_RUNTIME_RESOURCE_PROVIDER_M0"],[style*="BV_RUNTIME_RESOURCE_PROVIDER_M0"]){display:none!important}
|
||||
.lg-slot:has(circle[fill*="BV_RUNTIME_RESOURCE_PROVIDER"],[style*="BV_RUNTIME_RESOURCE_PROVIDER"]){display:none!important}
|
||||
.bv-lora-v3-picker-host{position:relative;z-index:1;padding:6px}.bv-lora-v3-picker-panel{display:grid;gap:10px}.bv-lora-v3-target{display:grid;gap:8px;padding:9px;border:1px solid var(--bv-ui-border);border-radius:4px}.bv-lora-v3-target>h3{margin:0}.bv-optional-lora-scope{display:grid;gap:7px}.bv-lora-v3-enable-row{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:start;gap:12px}.bv-lora-v3-enable-row>.bv-control-field>.bv-control-label{display:none}.bv-lora-v3-enable-row>.bv-control-field{padding-top:20px}.bv-lora-v3-scope-picker{display:grid;gap:8px}.bv-lora-v3-stack-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 8px;align-items:end;padding:7px;border:1px solid var(--bv-ui-border);border-radius:4px}.bv-lora-v3-stack-row>strong,.bv-lora-v3-stack-row>.bv-resource-picker{grid-column:1}.bv-lora-v3-stack-row>.bv-button{grid-column:2;grid-row:2}.bv-lora-v3-window{display:grid;gap:10px;padding:10px;overflow:auto}
|
||||
.bv-lora-v3-picker-host{position:relative;z-index:1;padding:6px}.bv-lora-v3-picker-panel{display:grid;gap:10px}.bv-lora-v3-target{display:grid;gap:8px;padding:9px;border:1px solid var(--bv-ui-border);border-radius:4px}.bv-lora-v3-target>h3{margin:0}.bv-optional-lora-scope{display:grid;gap:7px}.bv-lora-v3-enable-row{display:grid;grid-template-columns:max-content minmax(0,1fr);align-items:start;gap:12px}.bv-lora-v3-enable-row>.bv-control-field>.bv-control-label{display:none}.bv-lora-v3-enable-row>.bv-control-field{padding-top:20px}.bv-lora-v3-enable-row>.bv-control-field .bv-toggle{white-space:nowrap}.bv-lora-v3-scope-picker{display:grid;gap:8px}.bv-lora-v3-stack-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 8px;align-items:end;padding:7px;border:1px solid var(--bv-ui-border);border-radius:4px}.bv-lora-v3-stack-row>strong,.bv-lora-v3-stack-row>.bv-resource-picker{grid-column:1}.bv-lora-v3-stack-row>.bv-button{grid-column:2;grid-row:2}.bv-trash-icon{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.bv-lora-v3-window{display:grid;gap:10px;padding:10px;overflow:auto}
|
||||
.lg-node:has([node-type="BV M0 Fake Multi Resource Consumer"]) [node-type="BV M0 Fake Multi Resource Consumer"]:has(input[aria-label="resource_bindings"]){display:none}.bv-m0-multi-picker{max-height:520px;overflow:auto}.bv-m0-binding{display:grid;gap:5px;padding:6px;border:1px solid var(--bv-ui-border);border-radius:4px}.bv-m0-binding>header{display:flex;align-items:center;justify-content:space-between}.bv-m0-binding>header button,.bv-m0-add-binding{color:var(--bv-ui-text);background:var(--bv-ui-input);border:1px solid var(--bv-ui-border);border-radius:4px}.bv-m0-binding>header button{width:24px;height:24px}.bv-m0-add-binding{min-height:28px}
|
||||
|
||||
+8
-6
@@ -24,7 +24,7 @@ import { visibleExternalDetectorSlots } from "./regional/detectorExternalInputs"
|
||||
import { DETAILER_UI_NODES, detailerUiLabel } from "./regional/detailerLoopUi";
|
||||
import { upgradeRemoteLLMProvider } from "./remoteLLM";
|
||||
import { installM0ResourceSpike } from "./regional/m0ResourceSpike";
|
||||
import { hideLoraV3Widget, installLoraV3ConsumerSlot, installLoraV3Ui, OPEN_LORA_V3_EDITOR_EVENT } from "./regional/loraV3Ui";
|
||||
import { hideLoraV3Widget, installLoraV3ConsumerSlot, installLoraV3Ui, LORA_V3_INVENTORY_CHANGED_EVENT, OPEN_LORA_V3_EDITOR_EVENT } from "./regional/loraV3Ui";
|
||||
import LoraV3EditorWindow from "./regional/LoraV3EditorWindow";
|
||||
import { installM0CanvasVisibility } from "./regional/m0VisualProjection";
|
||||
import { applyReducedEffects, applyUiPreferences, applyUiSize, bindWindowSwitchModePersistence, getWindowSwitchMode, setWindowSwitchMode, UI_REDUCED_EFFECTS_SETTING_ID, UI_SIZE_SETTING_ID, UI_WINDOW_SWITCH_MODE_SETTING_ID } from "./ui/preferences";
|
||||
@@ -350,7 +350,7 @@ const regionalNodes = (): RegionalNode[] => {
|
||||
return found;
|
||||
};
|
||||
const refreshBvToolbarCapabilities=()=>{
|
||||
const control=workflowNodesOfType("BV Control Center").length>0,quick=regionalNodes().length>0,full=quick||["BV Regional Detailer Plan","BV Detector Registry","BV Smart Pipe","BV Smart Pipe Merge"].some(type=>workflowNodesOfType(type).length>0),overview=full||quick;
|
||||
const control=workflowNodesOfType("BV Control Center").length>0,quick=regionalNodes().length>0,full=quick||["BV Regional LoRA","BV Regional Detailer Plan","BV Detector Registry","BV Smart Pipe","BV Smart Pipe Merge"].some(type=>workflowNodesOfType(type).length>0),overview=full||quick;
|
||||
document.body.classList.toggle("bv-toolbar-no-control",!control);document.body.classList.toggle("bv-toolbar-no-editor",!full);document.body.classList.toggle("bv-toolbar-no-quick",!quick);document.body.classList.toggle("bv-toolbar-no-overview",!overview);
|
||||
window.dispatchEvent(new CustomEvent("bv-toolbar-capabilities-changed",{detail:{control,quick,full,overview}}));
|
||||
};
|
||||
@@ -365,9 +365,9 @@ const activateManagedLauncherNode = (type:"detailer"|"detector", prefix:string,
|
||||
else openTarget();
|
||||
rememberBvWindowInstance(type,targetId);
|
||||
};
|
||||
const openFullEditorNode=(type:string,node:any)=>{if(type==="regional")window.dispatchEvent(new CustomEvent(OPEN_REGIONAL_EDITOR_EVENT,{detail:{node}}));else if(type==="detailer")activateManagedLauncherNode("detailer","detailer-plan","configure_detailer_plan",node);else if(type==="detector")activateManagedLauncherNode("detector","detector-registry","configure_detector_registry",node);else window.dispatchEvent(new CustomEvent("bv-open-smart-pipe-editor",{detail:{node,kind:type}}))};
|
||||
const openFullEditorNode=(type:string,node:any)=>{if(type==="regional")window.dispatchEvent(new CustomEvent(OPEN_REGIONAL_EDITOR_EVENT,{detail:{node}}));else if(type==="lora")window.dispatchEvent(new CustomEvent(OPEN_LORA_V3_EDITOR_EVENT,{detail:{node}}));else if(type==="detailer")activateManagedLauncherNode("detailer","detailer-plan","configure_detailer_plan",node);else if(type==="detector")activateManagedLauncherNode("detector","detector-registry","configure_detector_registry",node);else window.dispatchEvent(new CustomEvent("bv-open-smart-pipe-editor",{detail:{node,kind:type}}))};
|
||||
const openLastFullBvEditor=()=>{
|
||||
const definitions=[{type:"regional",nodes:regionalNodes()},{type:"detailer",nodes:workflowNodesOfType("BV Regional Detailer Plan")},{type:"detector",nodes:workflowNodesOfType("BV Detector Registry")},{type:"pipe",nodes:workflowNodesOfType("BV Smart Pipe")},{type:"merge",nodes:workflowNodesOfType("BV Smart Pipe Merge")}],lastType=lastBvFullWindowType(),lastId=lastType&&lastBvWindowInstance(lastType),last=definitions.find(item=>item.type===lastType)?.nodes.find(node=>scopedNodeKey(node)===lastId);
|
||||
const definitions=[{type:"regional",nodes:regionalNodes()},{type:"lora",nodes:workflowNodesOfType("BV Regional LoRA")},{type:"detailer",nodes:workflowNodesOfType("BV Regional Detailer Plan")},{type:"detector",nodes:workflowNodesOfType("BV Detector Registry")},{type:"pipe",nodes:workflowNodesOfType("BV Smart Pipe")},{type:"merge",nodes:workflowNodesOfType("BV Smart Pipe Merge")}],lastType=lastBvFullWindowType(),lastId=lastType&&lastBvWindowInstance(lastType),last=definitions.find(item=>item.type===lastType)?.nodes.find(node=>scopedNodeKey(node)===lastId);
|
||||
if(last){openFullEditorNode(lastType!,last);return}
|
||||
const fallback=definitions.flatMap(item=>item.nodes.map(node=>({type:item.type,node}))).sort((left,right)=>(Number(left.node.id)||Number.MAX_SAFE_INTEGER)-(Number(right.node.id)||Number.MAX_SAFE_INTEGER))[0];
|
||||
if(!fallback)return;openFullEditorNode(fallback.type,fallback.node);if(lastType)showBvToast({title:"Previous editor unavailable",message:"Opened the first available BV editor instead.",tone:"warning",duration:4000});
|
||||
@@ -468,11 +468,12 @@ function BVRoot() {
|
||||
const [loraStacks, setLoraStacks] = useState<NamedLoraStack[]>([]);
|
||||
const [loraV3Node,setLoraV3Node]=useState<any|null>(null);
|
||||
const launcherColumns=useCallback((includeHidden=false):ToolbarLauncherColumn[]=>{
|
||||
const regional=regionalNodes(),detailers=workflowNodesOfType("BV Regional Detailer Plan"),detectors=workflowNodesOfType("BV Detector Registry"),pipes=[...workflowNodesOfType("BV Smart Pipe"),...workflowNodesOfType("BV Smart Pipe Merge")];
|
||||
const regional=regionalNodes(),loras=workflowNodesOfType("BV Regional LoRA"),detailers=workflowNodesOfType("BV Regional Detailer Plan"),detectors=workflowNodesOfType("BV Detector Registry"),pipes=[...workflowNodesOfType("BV Smart Pipe"),...workflowNodesOfType("BV Smart Pipe Merge")];
|
||||
const decorate=(node:any,item:any)=>{const hidden=!windowMenuVisible(node);return hidden&&!includeHidden?null:{...item,hidden,onReveal:hidden?()=>setWindowMenuVisible(node,true):undefined}};
|
||||
const regionalItems=regional.map(node=>{const copy=nodeLauncherLabel(node,"BV Regional Prompt"),last=lastBvWindowInstance("regional")===scopedNodeKey(node);return decorate(node,{id:scopedNodeKey(node),...copy,meta:`${copy.meta}${last?" · Last active":""}`,onSelect:()=>window.dispatchEvent(new CustomEvent(OPEN_REGIONAL_EDITOR_EVENT,{detail:{node}})),secondary:{label:`Quick edit ${copy.label}`,onSelect:()=>window.dispatchEvent(new CustomEvent(OPEN_REGIONAL_QUICK_EDIT_EVENT,{detail:{node}}))}})}).filter(Boolean) as any[];
|
||||
return [
|
||||
{id:"regional",label:"Regional Prompts",items:regionalItems},
|
||||
{id:"lora",label:"Regional LoRA",items:loras.map(node=>{const copy=nodeLauncherLabel(node,"BV Regional LoRA"),last=lastBvWindowInstance("lora")===scopedNodeKey(node);return decorate(node,{id:scopedNodeKey(node),...copy,meta:`${copy.meta}${last?" · Last active":""}`,onSelect:()=>{rememberBvWindowInstance("lora",scopedNodeKey(node));window.dispatchEvent(new CustomEvent(OPEN_LORA_V3_EDITOR_EVENT,{detail:{node}}));}})}).filter(Boolean) as any[]},
|
||||
{id:"detailer",label:"Detailer Plan",items:detailers.map(node=>{const copy=nodeLauncherLabel(node,"BV Regional Detailer Plan"),action=node.widgets?.find((widget:any)=>widget.name==="configure_detailer_plan"),last=lastBvWindowInstance("detailer")===scopedNodeKey(node);return decorate(node,{id:scopedNodeKey(node),...copy,disabled:!action||action.disabled,meta:`${copy.meta}${action?.disabled?" · Connect Regional Prompt":last?" · Last active":""}`,onSelect:()=>activateManagedLauncherNode("detailer","detailer-plan","configure_detailer_plan",node)})}).filter(Boolean) as any[]},
|
||||
{id:"detector",label:"Detector Registry",items:detectors.map(node=>{const copy=nodeLauncherLabel(node,"BV Detector Registry"),last=lastBvWindowInstance("detector")===scopedNodeKey(node);return decorate(node,{id:scopedNodeKey(node),...copy,meta:`${copy.meta}${last?" · Last active":""}`,onSelect:()=>activateManagedLauncherNode("detector","detector-registry","configure_detector_registry",node)})}).filter(Boolean) as any[]},
|
||||
{id:"smart-pipe",label:"Smart Pipes",items:pipes.map(node=>{const kind=String(node.type??node.comfyClass)==="BV Smart Pipe Merge"?"merge":"pipe",copy=nodeLauncherLabel(node,kind==="merge"?"Smart Pipe Merge":"Smart Pipe");return decorate(node,{id:scopedNodeKey(node),...copy,meta:`${copy.meta} · ${kind==="merge"?"Merge":"Pipe"}`,onSelect:()=>window.dispatchEvent(new CustomEvent("bv-open-smart-pipe-editor",{detail:{node,kind}}))})}).filter(Boolean) as any[]},
|
||||
@@ -492,8 +493,9 @@ function BVRoot() {
|
||||
window.addEventListener(OPEN_CONTROL_RACK_EVENT, open);
|
||||
return () => window.removeEventListener(OPEN_CONTROL_RACK_EVENT, open);
|
||||
}, []);
|
||||
useEffect(()=>{const open=(event:Event)=>setLoraV3Node((event as CustomEvent<{node?:any}>).detail?.node??null);window.addEventListener(OPEN_LORA_V3_EDITOR_EVENT,open);return()=>window.removeEventListener(OPEN_LORA_V3_EDITOR_EVENT,open)},[]);
|
||||
useEffect(()=>{const open=(event:Event)=>{const node=(event as CustomEvent<{node?:any}>).detail?.node??null;if(node)rememberBvWindowInstance("lora",scopedNodeKey(node));setLoraV3Node(node)};window.addEventListener(OPEN_LORA_V3_EDITOR_EVENT,open);return()=>window.removeEventListener(OPEN_LORA_V3_EDITOR_EVENT,open)},[]);
|
||||
useEffect(()=>{const update=(event:Event)=>setHasControlNodes(Boolean((event as CustomEvent).detail?.control));window.addEventListener("bv-toolbar-capabilities-changed",update);refreshBvToolbarCapabilities();return()=>window.removeEventListener("bv-toolbar-capabilities-changed",update)},[]);
|
||||
useEffect(()=>{const refresh=()=>refreshBvToolbarCapabilities();window.addEventListener(LORA_V3_INVENTORY_CHANGED_EVENT,refresh);return()=>window.removeEventListener(LORA_V3_INVENTORY_CHANGED_EVENT,refresh)},[]);
|
||||
|
||||
useEffect(() => {
|
||||
const open = (event: Event) => {
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { BvSelect } from "../ui/controls";
|
||||
import { Button, ResourcePicker, SelectField, SortableList, type SortableItem } from "../ui/components";
|
||||
import { BvManagedWindow } from "../ui/window";
|
||||
import { LoraV3ResourcePickerPanel, type LoraV3Config } from "./LoraV3ResourcePickerPanel";
|
||||
import { addLoraV3TargetEntry, loraV3Catalog, loraV3Resolved, loraV3TargetOptions, readNodeLoraV3Config, removeLoraV3TargetEntry, setLoraV3Collector, setLoraV3EntryResource } from "./loraV3Ui";
|
||||
import { setWindowMenuVisible, useWindowMenuVisibility } from "../ui/windowRegistry";
|
||||
import { type LoraV3Config, type LoraV3Operation, type LoraV3Step, loraV3TargetValue } from "./LoraV3ResourcePickerPanel";
|
||||
import { loraV3Catalog, loraV3Resolved, loraV3TargetOptions, readNodeLoraV3Config, setLoraV3Collector, setLoraV3EntryResource, writeNodeLoraV3Config } from "./loraV3Ui";
|
||||
|
||||
const widget=(node:any,name:string)=>node?.widgets?.find((item:any)=>item.name===name);
|
||||
const operations=["replace","merge","subtract","clear"].map(value=>({value,label:value[0].toUpperCase()+value.slice(1)}));
|
||||
|
||||
export default function LoraV3EditorWindow({open,node,onClose}:{open:boolean;node:any|null;onClose:()=>void}){
|
||||
const [config,setConfig]=useState<LoraV3Config>(()=>readNodeLoraV3Config(node));
|
||||
const [config,setConfig]=useState<LoraV3Config>(()=>readNodeLoraV3Config(node)),menuVisible=useWindowMenuVisibility(node);
|
||||
useEffect(()=>{if(open&&node)setConfig(readNodeLoraV3Config(node));},[node,open]);
|
||||
if(!node)return null;
|
||||
const collectors=loraV3Catalog(node).filter(item=>item.resources.length>0),resolved=loraV3Resolved(node,config),targetOptions=loraV3TargetOptions(node);
|
||||
return <BvManagedWindow open={open} title="BV Regional LoRA Editor" initialGeometry={{width:620,height:520}} minSize={{width:420,height:320}} onClose={onClose}>
|
||||
<div className="bv-ui bv-lora-v3-window">
|
||||
<BvSelect label="Operation" value={String(widget(node,"operation")?.value??"replace")} onChange={value=>{const item=widget(node,"operation");if(item){item.value=value;item.callback?.(value);node.setDirtyCanvas?.(true,true);}}}><option value="replace">Replace</option><option value="merge">Merge</option><option value="subtract">Subtract</option><option value="clear">Clear</option></BvSelect>
|
||||
<LoraV3ResourcePickerPanel collectors={collectors} config={config} resolved={resolved} targetOptions={targetOptions} onCollector={id=>setConfig(setLoraV3Collector(node,config,id))} onResource={(entryId,id)=>setConfig(setLoraV3EntryResource(node,config,entryId,id))} onAddExternal={target=>setConfig(addLoraV3TargetEntry(node,config,target))} onRemove={(entryId,target)=>setConfig(removeLoraV3TargetEntry(node,config,entryId,target))}/>
|
||||
</div>
|
||||
</BvManagedWindow>;
|
||||
const collectors=loraV3Catalog(node).filter(item=>item.resources.length>0),targets=loraV3TargetOptions(node),resolved=loraV3Resolved(node,config);
|
||||
const legacyOperation=String(widget(node,"operation")?.value??"replace") as LoraV3Operation;
|
||||
const steps:LoraV3Step[]=config.version===2?(config.steps??[]):config.entries.flatMap(entry=>entry.targets.map(target=>({id:crypto.randomUUID(),operation:legacyOperation,target:target as any,entries:[{...entry,targets:[target]}]})));
|
||||
const commit=(nextSteps:LoraV3Step[],collectorId=config.collector_id)=>{const next:LoraV3Config={version:2,collector_id:collectorId,entries:[],steps:nextSteps};writeNodeLoraV3Config(node,next);setConfig(next);};
|
||||
const items:SortableItem[]=steps.map((step,index)=>{
|
||||
const targetValue=loraV3TargetValue(step.target),targetLabel=targets.find(item=>item.value===targetValue)?.label??"Missing region";
|
||||
return {id:step.id,title:`Step ${index+1}: ${targetLabel}`,description:step.operation,content:<div className="bv-ui-stack">
|
||||
<SelectField label="Target" value={targetValue} options={targets} onValue={value=>{const target=targets.find(item=>item.value===value)?.target;if(target)commit(steps.map(item=>item.id===step.id?{...item,target,entries:item.entries.map(entry=>({...entry,targets:[target]}))}:item));}}/>
|
||||
<SelectField label="Operation" value={step.operation} options={operations} onValue={value=>commit(steps.map(item=>item.id===step.id?{...item,operation:value as LoraV3Operation}:item))}/>
|
||||
{step.operation!=="clear"&&step.entries.map((entry,entryIndex)=>entry.source.kind==="external"?<section className="bv-lora-v3-stack-row" key={entry.id}><strong>LoRA Stack {entryIndex+1}</strong><ResourcePicker collectors={collectors} collectorId={config.collector_id??""} resourceId={entry.source.resource_id} resolved={resolved} onCollector={id=>setConfig(setLoraV3Collector(node,config,id))} onResource={id=>setConfig(setLoraV3EntryResource(node,config,entry.id,id))}/><Button intent="ghost" iconOnly aria-label={`Remove LoRA Stack ${entryIndex+1}`} onClick={()=>commit(steps.map(item=>item.id===step.id?{...item,entries:item.entries.filter(candidate=>candidate.id!==entry.id)}:item))}>×</Button></section>:null)}
|
||||
{step.operation!=="clear"&&<Button intent="secondary" disabled={!collectors.length} onClick={()=>{const choice=collectors.find(item=>item.id===config.collector_id)??collectors[0],resource=choice?.resources[0];if(resource)commit(steps.map(item=>item.id===step.id?{...item,entries:[...item.entries,{id:crypto.randomUUID(),source:{kind:"external",resource_id:resource.id},targets:[item.target]}]}:item),choice.id);}}>Add LoRA stack</Button>}
|
||||
</div>};
|
||||
});
|
||||
return <BvManagedWindow open={open} title="BV Regional LoRA Editor" shortTitle="Regional LoRA" menuVisible={menuVisible} onMenuVisible={visible=>setWindowMenuVisible(node,visible)} initialGeometry={{width:620,height:520}} minSize={{width:420,height:320}} onClose={onClose}><div className="bv-ui bv-lora-v3-window"><Button intent="secondary" disabled={!targets.length} onClick={()=>commit([...steps,{id:crypto.randomUUID(),operation:"merge",target:targets[0].target,entries:[]}])}>Add step</Button><SortableList items={items} initialExpanded={items.map(item=>item.id)} onReorder={ordered=>{const byId=new Map(steps.map(step=>[step.id,step]));commit(ordered.map(item=>byId.get(item.id)!).filter(Boolean));}} onRemove={id=>commit(steps.filter(step=>step.id!==id))}/></div></BvManagedWindow>;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ import React, { useEffect, useState } from "react";
|
||||
import { Button, ResourcePicker, ResourcePickerCollector, ToggleField, type SelectOption } from "../ui/components";
|
||||
|
||||
export type LoraV3Entry={id:string;source:{kind:"external";resource_id:string}|{kind:"native";lora_name:string;model_strength:number;clip_strength:number};targets:Record<string,unknown>[]};
|
||||
export type LoraV3Config={version:1;collector_id:string|null;entries:LoraV3Entry[]};
|
||||
export type LoraV3Operation="replace"|"merge"|"subtract"|"clear";
|
||||
export type LoraV3Step={id:string;operation:LoraV3Operation;target:LoraV3Target;entries:LoraV3Entry[]};
|
||||
export type LoraV3Config={version:1|2;collector_id:string|null;entries:LoraV3Entry[];steps?:LoraV3Step[]};
|
||||
|
||||
export type LoraV3Target={scope:"global"}|{scope:"region";document_id:string;region_id:string};
|
||||
export type LoraV3TargetOption=SelectOption&{target:LoraV3Target};
|
||||
@@ -17,7 +19,7 @@ const externalFor=(config:LoraV3Config,target:LoraV3Target)=>config.entries.filt
|
||||
|
||||
export function LoraV3ScopePicker({collectors,config,target,resolved,onCollector,onResource,onAdd,onRemove}:ScopeProps){
|
||||
const entries=externalFor(config,target);
|
||||
return <div className="bv-lora-v3-scope-picker">{entries.map((entry,index)=><section className="bv-lora-v3-stack-row" key={entry.id}><strong>LoRA Stack {index+1}</strong><ResourcePicker collectors={collectors} collectorId={config.collector_id??""} resourceId={entry.source.resource_id} resolved={resolved} onCollector={onCollector} onResource={id=>onResource(entry.id,id)}/><Button density="compact" intent="ghost" onClick={()=>onRemove(entry.id,target)}>Remove</Button></section>)}<Button intent="secondary" disabled={!collectors.some(collector=>collector.resources.length>0)} onClick={()=>onAdd(target)}>Add LoRA stack</Button></div>;
|
||||
return <div className="bv-lora-v3-scope-picker">{entries.map((entry,index)=><section className="bv-lora-v3-stack-row" key={entry.id}><strong>LoRA Stack {index+1}</strong><ResourcePicker collectors={collectors} collectorId={config.collector_id??""} resourceId={entry.source.resource_id} resolved={resolved} onCollector={onCollector} onResource={id=>onResource(entry.id,id)}/><Button density="compact" intent="ghost" iconOnly aria-label={`Remove LoRA Stack ${index+1}`} title="Remove LoRA stack" onClick={()=>onRemove(entry.id,target)}><svg className="bv-trash-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13M10 11v5M14 11v5"/></svg></Button></section>)}<Button intent="secondary" disabled={!collectors.some(collector=>collector.resources.length>0)} onClick={()=>onAdd(target)}>Add LoRA stack</Button></div>;
|
||||
}
|
||||
|
||||
export function OptionalLoraV3ScopePicker(props:ScopeProps&{onClear:(target:LoraV3Target)=>void}){
|
||||
|
||||
@@ -4,7 +4,7 @@ export const emptyLoraV3Config=():LoraV3Config=>({version:1,collector_id:null,en
|
||||
|
||||
export function parseLoraV3Config(value:unknown):LoraV3Config{
|
||||
const candidate=typeof value==="string"?JSON.parse(value):value;
|
||||
if(!candidate||typeof candidate!=="object"||(candidate as any).version!==1||!Array.isArray((candidate as any).entries))throw new Error("Invalid BV Regional LoRA v3 configuration");
|
||||
if(!candidate||typeof candidate!=="object"||![1,2].includes((candidate as any).version)||!Array.isArray((candidate as any).entries)||((candidate as any).version===2&&!Array.isArray((candidate as any).steps)))throw new Error("Invalid BV Regional LoRA v3 configuration");
|
||||
return structuredClone(candidate as LoraV3Config);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { connectLoraConsumerTree, ensureLoraCollectorOutput, ensureLoraConsumerI
|
||||
import { installM0CanvasVisibility } from "./m0VisualProjection";
|
||||
|
||||
export const OPEN_LORA_V3_EDITOR_EVENT="bv-open-regional-lora-editor";
|
||||
export const LORA_V3_INVENTORY_CHANGED_EVENT="bv-regional-lora-inventory-changed";
|
||||
const widget=(node:any,name:string)=>node.widgets?.find((item:any)=>item.name===name);
|
||||
const nodeClass=(node:any)=>String(node?.comfyClass??node?.type??"");
|
||||
const graphLink=(graph:any,id:any)=>id==null?null:(graph?._links?.get?.(id)??graph?.links?.get?.(id)??graph?.links?.[id]??null);
|
||||
@@ -25,7 +26,8 @@ export function readNodeLoraV3Config(node:any,name=nodeClass(node)==="BV Regiona
|
||||
try{
|
||||
let config=parseLoraV3Config(widget(node,name)?.value),linked=linkedLocalLoraCollector(node);
|
||||
const collectorRemap=linked?.__bvLoraCollectorIdRemap?.[String(config.collector_id??"")],resourceRemaps=Object.assign({},...upstreamNamedStacks(linked).map((item:any)=>item.node?.__bvLoraResourceIdRemap??{}));
|
||||
if(collectorRemap){config={...config,collector_id:collectorRemap,entries:config.entries.map(entry=>entry.source.kind==="external"&&resourceRemaps[entry.source.resource_id]?{...entry,source:{...entry.source,resource_id:resourceRemaps[entry.source.resource_id]}}:entry)};writeNodeLoraV3Config(node,config,name);}
|
||||
const remap=(entry:any)=>entry.source.kind==="external"&&resourceRemaps[entry.source.resource_id]?{...entry,source:{...entry.source,resource_id:resourceRemaps[entry.source.resource_id]}}:entry;
|
||||
if(collectorRemap){config={...config,collector_id:collectorRemap,entries:config.entries.map(remap),steps:config.steps?.map(step=>({...step,entries:step.entries.map(remap)}))};writeNodeLoraV3Config(node,config,name);}
|
||||
return config;
|
||||
}catch{return emptyLoraV3Config();}
|
||||
}
|
||||
@@ -40,8 +42,8 @@ export function loraV3Resolved(node:any,config:LoraV3Config){const linked=linked
|
||||
function commitLoraV3Config(node:any,next:LoraV3Config){
|
||||
const choice=loraV3Catalog(node).find(item=>item.id===next.collector_id);connectLoraConsumerTree(node,choice?.node??null);writeNodeLoraV3Config(node,next);return next;
|
||||
}
|
||||
export function setLoraV3Collector(node:any,config:LoraV3Config,collectorId:string){const choice=loraV3Catalog(node).find(item=>item.id===collectorId),resourceId=choice?.resources[0]?.id??"";return commitLoraV3Config(node,{...config,collector_id:collectorId||null,entries:config.entries.map(entry=>entry.source.kind==="external"?{...entry,source:{...entry.source,resource_id:resourceId}}:entry)});}
|
||||
export function setLoraV3EntryResource(node:any,config:LoraV3Config,entryId:string,resourceId:string){return commitLoraV3Config(node,{...config,entries:config.entries.map(entry=>entry.id===entryId&&entry.source.kind==="external"?{...entry,source:{...entry.source,resource_id:resourceId}}:entry)});}
|
||||
export function setLoraV3Collector(node:any,config:LoraV3Config,collectorId:string){const choice=loraV3Catalog(node).find(item=>item.id===collectorId),resourceId=choice?.resources[0]?.id??"",map=(entry:any)=>entry.source.kind==="external"?{...entry,source:{...entry.source,resource_id:resourceId}}:entry;return commitLoraV3Config(node,{...config,collector_id:collectorId||null,entries:config.entries.map(map),steps:config.steps?.map(step=>({...step,entries:step.entries.map(map)}))});}
|
||||
export function setLoraV3EntryResource(node:any,config:LoraV3Config,entryId:string,resourceId:string){const map=(entry:any)=>entry.id===entryId&&entry.source.kind==="external"?{...entry,source:{...entry.source,resource_id:resourceId}}:entry;return commitLoraV3Config(node,{...config,entries:config.entries.map(map),steps:config.steps?.map(step=>({...step,entries:step.entries.map(map)}))});}
|
||||
export function addLoraV3TargetEntry(node:any,config:LoraV3Config,target:LoraV3Target){const choice=loraV3Catalog(node).find(item=>item.id===config.collector_id)??loraV3Catalog(node)[0],resource=choice?.resources[0];if(!choice||!resource)return config;return commitLoraV3Config(node,{...config,collector_id:choice.id,entries:[...config.entries,{id:crypto.randomUUID(),source:{kind:"external",resource_id:resource.id},targets:[target]}]});}
|
||||
export function removeLoraV3TargetEntry(node:any,config:LoraV3Config,entryId:string,target:LoraV3Target){return commitLoraV3Config(node,withoutLoraV3Target(config,target,entryId));}
|
||||
export function clearLoraV3Target(node:any,config:LoraV3Config,target:LoraV3Target){return commitLoraV3Config(node,withoutLoraV3Target(config,target));}
|
||||
@@ -52,6 +54,6 @@ function installTransformer(node:any){installLoraV3ConsumerSlot(node);hideLoraV3
|
||||
|
||||
export function installLoraV3Ui(nodeType:any,nodeData:any){
|
||||
const collector=nodeData.name==="BV LoRA Stack Collector",transformer=nodeData.name==="BV Regional LoRA",consumer=["BV Regional Native Conditioning","BV Regional Krea 2 Attention","BV Regional Anima Conditioning"].includes(nodeData.name);if(!collector&&!transformer&&!consumer)return false;
|
||||
const created=nodeType.prototype.onNodeCreated,configured=nodeType.prototype.onConfigure,changed=nodeType.prototype.onConnectionsChange,upgrade=function(this:any){if(collector)installCollector(this);if(transformer)installTransformer(this);if(consumer)installLoraV3ConsumerSlot(this);};
|
||||
nodeType.prototype.onNodeCreated=function(){const result=created?.apply(this,arguments);queueMicrotask(()=>upgrade.call(this));return result;};nodeType.prototype.onConfigure=function(){const result=configured?.apply(this,arguments);queueMicrotask(()=>upgrade.call(this));return result;};nodeType.prototype.onConnectionsChange=function(){const result=changed?.apply(this,arguments);queueMicrotask(()=>upgrade.call(this));return result;};return true;
|
||||
const created=nodeType.prototype.onNodeCreated,configured=nodeType.prototype.onConfigure,changed=nodeType.prototype.onConnectionsChange,removed=nodeType.prototype.onRemoved,upgrade=function(this:any){if(collector)installCollector(this);if(transformer)installTransformer(this);if(consumer)installLoraV3ConsumerSlot(this);if(transformer)window.dispatchEvent(new CustomEvent(LORA_V3_INVENTORY_CHANGED_EVENT));};
|
||||
nodeType.prototype.onNodeCreated=function(){const result=created?.apply(this,arguments);queueMicrotask(()=>upgrade.call(this));return result;};nodeType.prototype.onConfigure=function(){const result=configured?.apply(this,arguments);queueMicrotask(()=>upgrade.call(this));return result;};nodeType.prototype.onConnectionsChange=function(){const result=changed?.apply(this,arguments);queueMicrotask(()=>upgrade.call(this));return result;};nodeType.prototype.onRemoved=function(){const result=removed?.apply(this,arguments);if(transformer)queueMicrotask(()=>window.dispatchEvent(new CustomEvent(LORA_V3_INVENTORY_CHANGED_EVENT)));return result;};return true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type BvWindowType = "regional" | "quick" | "detailer" | "detector" | "pipe" | "merge";
|
||||
export type BvWindowType = "regional" | "quick" | "lora" | "detailer" | "detector" | "pipe" | "merge";
|
||||
|
||||
const lastActive = new Map<BvWindowType, string>();
|
||||
let lastFull:BvWindowType|undefined;
|
||||
|
||||
Reference in New Issue
Block a user