Polish regional editor and publish v0.3.2 assets
@@ -1,5 +1,10 @@
|
||||
# Third-party notices
|
||||
|
||||
This file records third-party code or data incorporated into BV Node Pack and the
|
||||
licenses or provenance obligations that accompany it. Projects that influenced
|
||||
design or research without contributing copied code are acknowledged separately
|
||||
below and in the README.
|
||||
|
||||
## Baseline completion tag data
|
||||
|
||||
`data/completion/bv_default_tags.csv` was sourced from the
|
||||
@@ -37,3 +42,27 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## Inspiration and research acknowledgements
|
||||
|
||||
The following projects informed product design, interoperability research or
|
||||
architecture discussions. They are not bundled runtime dependencies, and no copied
|
||||
implementation is claimed unless another section of this file says otherwise.
|
||||
|
||||
- [`rgthree-comfy`](https://github.com/rgthree/rgthree-comfy): compact workflow,
|
||||
control and seed interaction patterns.
|
||||
- [`ComfyUI_agilly1989_motorway`](https://github.com/agilly1989/ComfyUI_agilly1989_motorway):
|
||||
the original inspiration for configurable, user-named values carried and
|
||||
overridden along BV Pipe chains.
|
||||
- [`cg-use-everywhere`](https://github.com/chrisgoringe/cg-use-everywhere):
|
||||
wireless value broadcasting and routing-control concepts that informed the
|
||||
Smart Pipe wireless design.
|
||||
- [`ComfyUI-KJNodes`](https://github.com/kijai/ComfyUI-KJNodes): the Ideogram
|
||||
prompt-builder editor and detached authoring workflow.
|
||||
- [`ComfyUI_LC123_nodes`](https://github.com/lonecatone23/ComfyUI_LC123_nodes):
|
||||
painted regional authoring plus Krea 2 and Anima workflow references.
|
||||
- [`RES4LYF`](https://github.com/ClownsharkBatwing/RES4LYF): clean-room research
|
||||
reference for regional attention, overlap semantics and backend boundaries.
|
||||
- [`comfy-ex-tagcomplete`](https://github.com/jupo-ai/comfy-ex-tagcomplete):
|
||||
completion interoperability and UX research. Its bundled tag-data relationship
|
||||
is documented in the first section of this file.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="640" viewBox="0 0 1600 640" role="img" aria-labelledby="title desc">
|
||||
<title id="title">BV Node Pack for ComfyUI</title>
|
||||
<desc id="desc">A dark ComfyUI-inspired workflow showing regional prompting, structured prompts, smart pipes and subgraph controls.</desc>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b0f14"/><stop offset="1" stop-color="#151c26"/></linearGradient>
|
||||
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="0"><stop stop-color="#269ee8"/><stop offset="1" stop-color="#7667f5"/></linearGradient>
|
||||
<pattern id="grid" width="32" height="32" patternUnits="userSpaceOnUse"><path d="M32 0H0V32" fill="none" stroke="#ffffff" stroke-opacity=".045"/></pattern>
|
||||
<filter id="shadow"><feDropShadow dx="0" dy="12" stdDeviation="16" flood-color="#000" flood-opacity=".55"/></filter>
|
||||
</defs>
|
||||
<rect width="1600" height="640" rx="34" fill="url(#bg)"/>
|
||||
<rect width="1600" height="640" rx="34" fill="url(#grid)"/>
|
||||
<circle cx="1360" cy="80" r="260" fill="#269ee8" opacity=".08"/>
|
||||
<circle cx="180" cy="590" r="300" fill="#7667f5" opacity=".08"/>
|
||||
|
||||
<g font-family="Inter,Segoe UI,Arial,sans-serif">
|
||||
<g transform="translate(90 92)">
|
||||
<circle cx="34" cy="34" r="34" fill="url(#accent)"/>
|
||||
<path d="M19 35c8-18 26-18 31-5 5 14-13 27-25 17-9-7-3-23 11-21 9 2 9 13 1 15" fill="none" stroke="#fff" stroke-width="5" stroke-linecap="round"/>
|
||||
<text x="92" y="34" fill="#fff" font-size="48" font-weight="750">BV Node Pack</text>
|
||||
<text x="94" y="70" fill="#a9b8ca" font-size="22">Deep workflow tools for ComfyUI</text>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<g transform="translate(92 240)">
|
||||
<rect width="315" height="230" rx="14" fill="#222831" stroke="#4c78a8" stroke-width="2"/>
|
||||
<rect width="315" height="48" rx="14" fill="#2b3440"/><rect y="36" width="315" height="12" fill="#2b3440"/>
|
||||
<circle cx="25" cy="24" r="8" fill="#4c78a8"/><text x="45" y="31" fill="#fff" font-size="20" font-weight="650">Regional Prompt</text>
|
||||
<text x="24" y="84" fill="#93a4b7" font-size="15">GLOBAL</text><rect x="24" y="98" width="267" height="35" rx="6" fill="#151a20"/>
|
||||
<text x="24" y="164" fill="#93a4b7" font-size="15">REGIONS</text>
|
||||
<rect x="24" y="178" width="118" height="28" rx="6" fill="#e45756" opacity=".75"/><rect x="154" y="178" width="137" height="28" rx="6" fill="#4c78a8" opacity=".85"/>
|
||||
<circle cx="315" cy="82" r="7" fill="#64dc85"/>
|
||||
</g>
|
||||
|
||||
<g transform="translate(500 208)">
|
||||
<rect width="600" height="300" rx="16" fill="#181d24" stroke="#394555" stroke-width="2"/>
|
||||
<rect width="600" height="48" rx="16" fill="#232a33"/><rect y="36" width="600" height="12" fill="#232a33"/>
|
||||
<text x="24" y="31" fill="#fff" font-size="19" font-weight="650">BV Regional Editor</text>
|
||||
<rect x="22" y="68" width="140" height="204" rx="8" fill="#101419" stroke="#303946"/>
|
||||
<text x="38" y="98" fill="#b8c5d3" font-size="14">LAYERS</text>
|
||||
<rect x="38" y="116" width="108" height="30" rx="5" fill="#e45756" opacity=".72"/><rect x="38" y="156" width="108" height="30" rx="5" fill="#4c78a8" opacity=".8"/>
|
||||
<rect x="182" y="68" width="270" height="204" rx="8" fill="#0c1015" stroke="#303946"/>
|
||||
<rect x="214" y="94" width="206" height="152" rx="4" fill="#252c34"/>
|
||||
<rect x="228" y="109" width="90" height="122" fill="#e45756" opacity=".55"/><rect x="304" y="120" width="102" height="111" fill="#4c78a8" opacity=".62"/>
|
||||
<rect x="472" y="68" width="106" height="204" rx="8" fill="#101419" stroke="#303946"/>
|
||||
<text x="486" y="98" fill="#b8c5d3" font-size="14">PROMPT</text>
|
||||
<rect x="486" y="114" width="78" height="62" rx="5" fill="#191f27"/><rect x="486" y="186" width="78" height="62" rx="5" fill="#191f27"/>
|
||||
</g>
|
||||
|
||||
<g transform="translate(1195 250)">
|
||||
<rect width="310" height="188" rx="14" fill="#222831" stroke="#7667f5" stroke-width="2"/>
|
||||
<rect width="310" height="46" rx="14" fill="#2b3440"/><rect y="34" width="310" height="12" fill="#2b3440"/>
|
||||
<circle cx="24" cy="23" r="8" fill="#7667f5"/><text x="44" y="30" fill="#fff" font-size="19" font-weight="650">Anima Conditioning</text>
|
||||
<text x="22" y="83" fill="#9faec0" font-size="14">MODEL</text><circle cx="0" cy="78" r="7" fill="#b58bd8"/>
|
||||
<text x="22" y="117" fill="#9faec0" font-size="14">CLIP</text><circle cx="0" cy="112" r="7" fill="#f4df45"/>
|
||||
<text x="22" y="151" fill="#9faec0" font-size="14">BV_REGIONAL</text><circle cx="0" cy="146" r="7" fill="#64dc85"/>
|
||||
<circle cx="310" cy="82" r="7" fill="#b58bd8"/><circle cx="310" cy="116" r="7" fill="#f4a340"/><circle cx="310" cy="150" r="7" fill="#f4a340"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<path d="M407 322C450 322 455 350 500 350" fill="none" stroke="#64dc85" stroke-width="5" opacity=".9"/>
|
||||
<path d="M1100 358C1142 358 1152 396 1195 396" fill="none" stroke="#64dc85" stroke-width="5" opacity=".9"/>
|
||||
<text x="800" y="574" text-anchor="middle" fill="#d9e4ef" font-size="20">Regional prompting · Prompt AST · Smart Pipes · Subgraph UI · Local autocomplete</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,39 @@
|
||||
# Regional documentation shot list
|
||||
|
||||
This file tracks the current documentation screenshots and optional future additions.
|
||||
Capture at 100% browser scale with unrelated panels closed and no personal paths,
|
||||
usernames, seeds or model filenames visible unless they are intentional.
|
||||
|
||||
## Present assets
|
||||
|
||||
- `editor-workspace.png`, `editor-floating.png` — both editor layouts.
|
||||
- `view-controls.png` — mask and background-image display opacity.
|
||||
- `regional-prompt-node.png`, `toolbar-*.png`, `quick-edit.png` — editor entry points.
|
||||
- `region-*.png` — main rectangles, interaction rectangles and painted light arc.
|
||||
- `autocomplete-*.png` — Quick Edit, full editor and native multiline completion.
|
||||
- `completion-settings.png` — dataset selection, priority and global enable switch.
|
||||
- `anima-workflow.png` — minimal Anima regional execution graph.
|
||||
- `../../../examples/images/anima-android-dance-regional-showcase.png` — final
|
||||
metadata-bearing workflow PNG and reference result.
|
||||
|
||||
## Still needed
|
||||
|
||||
1. `minimal-native-workflow.png`
|
||||
- Regional Prompt → Native Conditioning → standard KSampler.
|
||||
2. `editor-tools.png`
|
||||
- One rectangle, one additive brush and a subtractive split; show selection handles.
|
||||
3. `image-feedback.png`
|
||||
- Preview/Save Send target selector plus the resulting editor background.
|
||||
4. `document-tools.png`
|
||||
- Deconstructor, Prompt Extract and Mask Render with representative outputs.
|
||||
5. `completion-priority.png`
|
||||
- At least two datasets so the ordering controls and priority rule are visible.
|
||||
|
||||
## Replacement quality bar
|
||||
|
||||
- Prefer 16:9 or a tight landscape crop.
|
||||
- Use English UI labels.
|
||||
- Avoid open menus unless the menu itself is the subject.
|
||||
- Show one concept per image.
|
||||
- Ensure cables do not obscure node titles or controls.
|
||||
- Use the same dark ComfyUI theme and zoom level throughout a sequence.
|
||||
|
After Width: | Height: | Size: 767 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 863 KiB |
|
After Width: | Height: | Size: 862 KiB |
|
After Width: | Height: | Size: 753 KiB |
|
After Width: | Height: | Size: 861 KiB |
|
After Width: | Height: | Size: 863 KiB |
|
After Width: | Height: | Size: 856 KiB |
|
After Width: | Height: | Size: 858 KiB |
|
After Width: | Height: | Size: 863 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 877 KiB |
|
After Width: | Height: | Size: 210 KiB |
@@ -0,0 +1,108 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1680" height="720" viewBox="0 0 1680 720" role="img" aria-labelledby="title desc">
|
||||
<title id="title">BV Node Pack for ComfyUI</title>
|
||||
<desc id="desc">A dark 21:9 banner presenting BV Node Pack and its regional prompting editor.</desc>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop stop-color="#090d12"/>
|
||||
<stop offset=".55" stop-color="#111822"/>
|
||||
<stop offset="1" stop-color="#182334"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="brand" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop stop-color="#2ca7ee"/>
|
||||
<stop offset="1" stop-color="#7567f5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cyan" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop stop-color="#46b6ff"/>
|
||||
<stop offset="1" stop-color="#4c78a8"/>
|
||||
</linearGradient>
|
||||
<pattern id="grid" width="36" height="36" patternUnits="userSpaceOnUse">
|
||||
<path d="M36 0H0V36" fill="none" stroke="#fff" stroke-opacity=".045"/>
|
||||
</pattern>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="160%">
|
||||
<feDropShadow dx="0" dy="18" stdDeviation="22" flood-color="#000" flood-opacity=".6"/>
|
||||
</filter>
|
||||
<filter id="glow" x="-60%" y="-60%" width="220%" height="220%">
|
||||
<feGaussianBlur stdDeviation="28"/>
|
||||
</filter>
|
||||
<clipPath id="editorClip"><rect x="790" y="100" width="800" height="520" rx="24"/></clipPath>
|
||||
</defs>
|
||||
|
||||
<rect width="1680" height="720" fill="url(#bg)"/>
|
||||
<rect width="1680" height="720" fill="url(#grid)"/>
|
||||
<circle cx="250" cy="630" r="340" fill="#7567f5" opacity=".09" filter="url(#glow)"/>
|
||||
<circle cx="1510" cy="60" r="300" fill="#2ca7ee" opacity=".12" filter="url(#glow)"/>
|
||||
|
||||
<g font-family="Inter,Segoe UI,Arial,sans-serif">
|
||||
<g transform="translate(92 110)">
|
||||
<circle cx="46" cy="46" r="46" fill="url(#brand)"/>
|
||||
<path d="M25 48c10-25 36-27 45-9 9 20-17 39-35 24-13-11-4-33 16-29 13 3 13 19 2 23" fill="none" stroke="#fff" stroke-width="7" stroke-linecap="round"/>
|
||||
<text x="120" y="47" fill="#fff" font-size="58" font-weight="760">BV Node Pack</text>
|
||||
<text x="122" y="88" fill="#aab9ca" font-size="24" letter-spacing=".3">Advanced workflow tools for ComfyUI</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(94 280)">
|
||||
<text fill="#fff" font-size="43" font-weight="700">
|
||||
<tspan x="0" y="0">Design prompts spatially.</tspan>
|
||||
<tspan x="0" y="56">Keep workflows modular.</tspan>
|
||||
</text>
|
||||
<text x="2" y="152" fill="#aebdcd" font-size="24">Regional prompting, structured prompts,</text>
|
||||
<text x="2" y="188" fill="#aebdcd" font-size="24">Smart Pipes and subgraph-aware controls.</text>
|
||||
|
||||
<g transform="translate(0 244)" font-size="19" font-weight="650">
|
||||
<rect width="190" height="50" rx="25" fill="#1c2c3b" stroke="#2ca7ee" stroke-opacity=".7"/>
|
||||
<text x="95" y="32" text-anchor="middle" fill="#c8eaff">Regional Editor</text>
|
||||
<rect x="208" width="164" height="50" rx="25" fill="#26243b" stroke="#7567f5" stroke-opacity=".8"/>
|
||||
<text x="290" y="32" text-anchor="middle" fill="#ded9ff">Prompt AST</text>
|
||||
<rect x="390" width="160" height="50" rx="25" fill="#1c2c3b" stroke="#64dc85" stroke-opacity=".7"/>
|
||||
<text x="470" y="32" text-anchor="middle" fill="#d2ffe0">Smart Pipes</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g filter="url(#shadow)">
|
||||
<rect x="790" y="100" width="800" height="520" rx="24" fill="#13181f" stroke="#3b4858" stroke-width="2"/>
|
||||
<g clip-path="url(#editorClip)">
|
||||
<rect x="790" y="100" width="800" height="58" fill="#202730"/>
|
||||
<circle cx="824" cy="129" r="7" fill="#2ca7ee"/>
|
||||
<text x="845" y="137" fill="#fff" font-size="22" font-weight="700">BV Regional Editor</text>
|
||||
<rect x="1378" y="115" width="92" height="30" rx="6" fill="#151b22" stroke="#465464"/>
|
||||
<text x="1424" y="136" fill="#c7d3df" font-size="14" text-anchor="middle">Workspace</text>
|
||||
|
||||
<rect x="790" y="158" width="188" height="462" fill="#101419"/>
|
||||
<text x="816" y="196" fill="#8fa0b2" font-size="15" font-weight="700">REGIONS</text>
|
||||
<rect x="808" y="216" width="150" height="67" rx="8" fill="#19232d" stroke="#4c78a8"/>
|
||||
<circle cx="826" cy="237" r="7" fill="#4c78a8"/><text x="842" y="243" fill="#e7eef6" font-size="16">Cyan subject</text>
|
||||
<rect x="808" y="297" width="150" height="67" rx="8" fill="#2a2023" stroke="#e45756"/>
|
||||
<circle cx="826" cy="318" r="7" fill="#e45756"/><text x="842" y="324" fill="#e7eef6" font-size="16">Amber subject</text>
|
||||
<rect x="808" y="378" width="150" height="67" rx="8" fill="#17271d" stroke="#59b957"/>
|
||||
<circle cx="826" cy="399" r="7" fill="#59b957"/><text x="842" y="405" fill="#e7eef6" font-size="16">Light arc</text>
|
||||
|
||||
<rect x="978" y="158" width="422" height="462" fill="#0b0f14"/>
|
||||
<rect x="1023" y="204" width="332" height="370" rx="7" fill="#252d36"/>
|
||||
<rect x="1045" y="230" width="151" height="316" fill="#4c78a8" opacity=".67"/>
|
||||
<rect x="1184" y="230" width="149" height="316" fill="#e45756" opacity=".62"/>
|
||||
<rect x="1162" y="330" width="56" height="116" fill="#9f70a0" opacity=".82"/>
|
||||
<path d="M1041 270C1124 203 1251 210 1337 279" fill="none" stroke="#59b957" stroke-width="26" stroke-linecap="round" opacity=".7"/>
|
||||
<rect x="1001" y="177" width="34" height="164" rx="14" fill="#171d24" stroke="#425061"/>
|
||||
<circle cx="1018" cy="201" r="11" fill="#2ca7ee"/>
|
||||
<rect x="1009" y="230" width="18" height="18" rx="2" fill="none" stroke="#dbe7f2" stroke-width="2"/>
|
||||
<path d="M1009 271l18-10M1009 277l18-10" stroke="#dbe7f2" stroke-width="2"/>
|
||||
|
||||
<rect x="1400" y="158" width="190" height="462" fill="#101419"/>
|
||||
<text x="1422" y="196" fill="#8fa0b2" font-size="15" font-weight="700">REGION PROMPT</text>
|
||||
<text x="1422" y="230" fill="#cbd6e1" font-size="14">Strength</text>
|
||||
<rect x="1422" y="241" width="144" height="34" rx="6" fill="#171d24" stroke="#384655"/>
|
||||
<text x="1434" y="264" fill="#fff" font-size="15">0.85</text>
|
||||
<text x="1422" y="312" fill="#cbd6e1" font-size="14">Positive</text>
|
||||
<rect x="1422" y="324" width="144" height="112" rx="6" fill="#171d24" stroke="#384655"/>
|
||||
<path d="M1436 346h102M1436 363h116M1436 380h94M1436 397h110" stroke="#8292a4" stroke-width="4" stroke-linecap="round" opacity=".7"/>
|
||||
<text x="1422" y="475" fill="#cbd6e1" font-size="14">Negative</text>
|
||||
<rect x="1422" y="487" width="144" height="75" rx="6" fill="#171d24" stroke="#384655"/>
|
||||
<path d="M1436 509h111M1436 526h87" stroke="#8292a4" stroke-width="4" stroke-linecap="round" opacity=".7"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<path d="M720 360C750 360 758 360 790 360" fill="none" stroke="url(#cyan)" stroke-width="5" opacity=".9"/>
|
||||
<circle cx="720" cy="360" r="7" fill="#64dc85"/>
|
||||
<circle cx="790" cy="360" r="7" fill="#64dc85"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 400 400" role="img" aria-labelledby="title desc">
|
||||
<title id="title">BV Node Pack</title>
|
||||
<desc id="desc">BV Node Pack spiral mark on a dark rounded-square background.</desc>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop stop-color="#0a0f15"/>
|
||||
<stop offset="1" stop-color="#182334"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="brand" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop stop-color="#2ca7ee"/>
|
||||
<stop offset="1" stop-color="#7567f5"/>
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-30%" y="-30%" width="160%" height="180%">
|
||||
<feDropShadow dx="0" dy="18" stdDeviation="20" flood-color="#000" flood-opacity=".5"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="400" height="400" rx="88" fill="url(#bg)"/>
|
||||
<circle cx="200" cy="200" r="126" fill="url(#brand)" filter="url(#shadow)"/>
|
||||
<path d="M139 207c27-72 103-80 132-27 29 56-45 112-96 72-39-31-14-96 43-84 38 8 39 54 7 68" fill="none" stroke="#fff" stroke-width="25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,72 @@
|
||||
# BV Prompt AST Guide
|
||||
|
||||
BV Prompt AST stores prompt intent as structured blocks instead of repeatedly
|
||||
parsing ad-hoc strings. The encoder emits both `BV_AST` and cleaned plain text;
|
||||
downstream nodes can filter categories without losing the original hierarchy.
|
||||
|
||||
## Syntax
|
||||
|
||||
Unmarked text belongs to the default category:
|
||||
|
||||
```text
|
||||
A beautiful landscape, sunset lighting
|
||||
```
|
||||
|
||||
Block categories remain active until the next block marker:
|
||||
|
||||
```text
|
||||
@@style
|
||||
cinematic lighting
|
||||
@@subject
|
||||
a woman in rain
|
||||
```
|
||||
|
||||
Inline categories delimit a smaller fragment:
|
||||
|
||||
```text
|
||||
a portrait with @<eye> green eyes @@ and @<hair> black hair @@
|
||||
```
|
||||
|
||||
Inline categories may be nested:
|
||||
|
||||
```text
|
||||
@<subject> a @<face> smiling woman @@ in rain @@
|
||||
```
|
||||
|
||||
Comments are removed while their line break is retained:
|
||||
|
||||
```text
|
||||
a portrait ## authoring note, not part of the prompt
|
||||
```
|
||||
|
||||
## Rules and failure cases
|
||||
|
||||
- Category names match `[a-zA-Z0-9_-]+`; spaces are not allowed.
|
||||
- Every inline category must be closed with `@@`.
|
||||
- An unmatched closing marker or unclosed category raises an error with line and column.
|
||||
|
||||
Invalid:
|
||||
|
||||
```text
|
||||
@<clothing color> white dress @@
|
||||
@<clothing> long dress,@@@@
|
||||
```
|
||||
|
||||
Valid:
|
||||
|
||||
```text
|
||||
@<clothing_color> white dress @@
|
||||
@<clothing> long dress,@@
|
||||
```
|
||||
|
||||
## Typical chain
|
||||
|
||||
1. **BV Prompt Encode** parses source text into `BV_AST`.
|
||||
2. **BV Prompt Category Switch** enables or disables categories.
|
||||
3. **BV Prompt Decode** materializes the selected plain prompt.
|
||||
4. **BV Prompt AST Debug** exposes the current tree during authoring.
|
||||
|
||||
[](../examples/images/prompt-ast-categories.png)
|
||||
|
||||
The workflow PNG contains importable ComfyUI metadata. A JSON fallback is
|
||||
available at [`examples/workflows/prompt-ast-categories.json`](../examples/workflows/prompt-ast-categories.json).
|
||||
@@ -0,0 +1,231 @@
|
||||
# BV Regional Editor Guide
|
||||
|
||||
The BV Regional Editor authors a model-neutral `BV_REGIONAL` document. Geometry,
|
||||
prompts and authoring metadata are stored once; model-specific compiler nodes
|
||||
decide how much of that contract a backend can execute.
|
||||
|
||||
[](../examples/images/anima-android-dance-regional-showcase.png)
|
||||
|
||||
The reference document behind this image uses two rectangle layers per character:
|
||||
a main body area and a smaller shared interaction area. A third, painted region
|
||||
guides the golden light arc.
|
||||
|
||||
[Import the workflow PNG](../examples/images/anima-android-dance-regional-showcase.png)
|
||||
· [Import the regional document](examples/anima-android-dance-regional-showcase.bv-regional.json)
|
||||
|
||||
## Minimal workflow
|
||||
|
||||
1. Add **BV Regional Prompt**.
|
||||
2. Open the editor from the node or the BV toolbar action.
|
||||
3. Define Global and Background prompts.
|
||||
4. Add named regions and create rectangle or brush geometry.
|
||||
5. Connect `regional` to a supported compiler.
|
||||
6. Connect the compiler outputs to a standard KSampler.
|
||||
|
||||
| Backend | Connect | Behavior |
|
||||
| --- | --- | --- |
|
||||
| Native/Krea-style | `BV Regional Native Conditioning` | Standard ComfyUI masked conditioning; no model attention patch |
|
||||
| Anima | `BV Regional Anima Conditioning` | Built-in Anima attention patch; standard KSampler |
|
||||
| External Anima fallback | `BV Regional Anima Adapter` | Compiles for the optional Sen-sou node pack |
|
||||
|
||||
## Editor layout
|
||||
|
||||
| Area | Purpose |
|
||||
| --- | --- |
|
||||
| Left panel | Regions, priority order, geometry layers, visibility, lock and selection |
|
||||
| Artboard | WYSIWYG canvas, background preview, masks, selection handles, zoom and pan |
|
||||
| Right panel | Region strength, feather, prompts and document prompts |
|
||||
| Top menu | Import/export, undo/redo, display controls, canvas settings, completion data and help |
|
||||
| Floating tool strip | Select, rectangle, additive brush and subtractive brush |
|
||||
|
||||
Panel widths, open sections, active selection, zoom/pan and window geometry are
|
||||
saved separately per `document_id`. They never alter the semantic regional document.
|
||||
|
||||
<details>
|
||||
<summary><strong>Workspace and floating layouts</strong></summary>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Floating mode remains at viewport scale while the graph is zoomed or panned.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Node and toolbar entry points</strong></summary>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## Geometry and layers
|
||||
|
||||
- Rectangles show a live preview while dragging and support move plus eight resize handles.
|
||||
- Brush layers support round/square tips, size, hardness, opacity and optional stylus pressure.
|
||||
- Subtractive brush strokes remove from the selected brush layer; they do not create white masks.
|
||||
- Regions and layers can be renamed, reordered, locked, hidden, disabled, duplicated or deleted.
|
||||
- Regions may overlap. Version 1 executes the overlap mode `joint`.
|
||||
- Canvas width and height control the authoring aspect ratio even when no background exists.
|
||||
|
||||
Mask overlay opacity and background-image opacity are display-only settings. They
|
||||
do not change exported masks, regional strength or the image returned by a sender.
|
||||
|
||||
<details>
|
||||
<summary><strong>Display controls</strong></summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Reference document geometry</strong></summary>
|
||||
|
||||
The main layers guide the two character designs. The smaller rectangles overlap
|
||||
where their hands should meet, while the brush region demonstrates freehand geometry.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## Prompts
|
||||
|
||||
Each document contains:
|
||||
|
||||
- Global positive and negative prompts;
|
||||
- Background positive and negative prompts;
|
||||
- positive and negative prompts per region;
|
||||
- per-region strength and feather;
|
||||
- negative mode, including the technical zero-out path.
|
||||
|
||||
Global prompting establishes composition and concepts across the image. Regional
|
||||
prompts should usually describe local attributes rather than duplicate the entire
|
||||
scene. A region guides where conditioning applies; it is not a hard object boundary.
|
||||
|
||||
## How strict is regional guidance?
|
||||
|
||||
Regional prompting and attention masking bias where prompt information is available;
|
||||
they do not clip a generated object to a mask. Results still depend on the model,
|
||||
seed, sampler, prompt composition, mask geometry and backend implementation.
|
||||
|
||||
- A subject may extend beyond its region or appear slightly offset from it.
|
||||
- Attributes may bleed between nearby or strongly overlapping regions.
|
||||
- A small interaction region can make both regional contexts available around a
|
||||
contact point, but it cannot guarantee joined hands, eye contact or another pose.
|
||||
- Region strength controls conditioning influence. Raising it does not turn the
|
||||
region into a stricter bounding box and can overemphasize local concepts.
|
||||
- Global prompts can still influence composition and placement across the full image.
|
||||
|
||||
Treat masks as spatial intent. Validate important layouts across several seeds and
|
||||
adjust geometry, prompt wording and backend settings together.
|
||||
|
||||
## Workspace, floating and Quick Edit
|
||||
|
||||
Workspace mode fills the available browser viewport. Floating mode is independently
|
||||
movable and resizable and remains unaffected by graph zoom or pan.
|
||||
|
||||
Quick Edit changes Global, Background or one selected region without opening the
|
||||
full artboard. It targets stable document and region IDs even though the UI shows names.
|
||||
|
||||
<details>
|
||||
<summary><strong>Quick Edit over a live workflow</strong></summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## Background feedback
|
||||
|
||||
- **BV Regional Preview Send** returns a temporary preview to a selected editor.
|
||||
- **BV Regional Save Send** saves normally and returns the saved result.
|
||||
- Multiple senders use last-completed-sender-wins semantics.
|
||||
- Images are not embedded as Base64 in the workflow document.
|
||||
|
||||
This makes it possible to redraw masks over the latest sampler, detailer or upscale
|
||||
result without creating a workflow cycle.
|
||||
|
||||
## Prompt autocomplete
|
||||
|
||||
BV Prompt Autocomplete works in the full editor, Quick Edit and ordinary ComfyUI
|
||||
multiline widgets. Tags from the default dataset are inserted with spaces while the
|
||||
canonical underscore form remains available as metadata.
|
||||
|
||||
Autocomplete is optional. Disable it under **Settings → BV Node Pack → Prompting**
|
||||
or from **Edit → Prompt Autocomplete** when another extension should own prompt fields.
|
||||
Several CSV/TSV datasets may be enabled and ordered; the topmost source wins duplicate tags.
|
||||
|
||||
<details>
|
||||
<summary><strong>Completion surfaces</strong></summary>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Dataset settings and priority</strong></summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## Document tools
|
||||
|
||||
| Node | Use |
|
||||
| --- | --- |
|
||||
| BV Regional Debug | Canonical JSON, summary and stable `document_id` |
|
||||
| BV Regional Select | Select Global, Background or a named region |
|
||||
| BV Regional Deconstructor | AST, plain text, source text and reusable selection |
|
||||
| BV Regional Prompt Extract | Extract positive/negative AST and text |
|
||||
| BV Regional Mask Render | Render mask plus pixel bounding box |
|
||||
|
||||
These seams are intended for later detailer adapters and other consumers without
|
||||
coupling them to the editor UI.
|
||||
|
||||
## Anima reference workflow
|
||||
|
||||
The built-in Anima path has been verified with two independently conditioned
|
||||
characters, joint interaction overlap, a painted light region and a standard KSampler.
|
||||
|
||||
<details>
|
||||
<summary><strong>Minimal workflow</strong></summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
Import either the metadata-bearing
|
||||
[`workflow PNG`](../examples/images/anima-android-dance-regional-showcase.png)
|
||||
or the editor-only
|
||||
[`regional document`](examples/anima-android-dance-regional-showcase.bv-regional.json).
|
||||
|
||||
## Current limits
|
||||
|
||||
- `joint` is the only executable overlap mode; normalized, exclusive and priority modes are reserved.
|
||||
- Native masked conditioning does not provide model-internal attention isolation.
|
||||
- The built-in attention backend currently targets Anima; other model families require future adapters.
|
||||
- Parent-region IDs exist in the schema, but hierarchy authoring is not exposed yet.
|
||||
- Regional negative isolation depends on backend capability.
|
||||
- A region is conditioning guidance, not a guaranteed object-sized bounding box,
|
||||
exact pose constraint or pixel-perfect interaction target.
|
||||
|
||||
For the formal schema and implementation boundaries, see
|
||||
[`docs/specs/bv-regional-v1.md`](specs/bv-regional-v1.md) and
|
||||
[`docs/regional-editor-mvp.md`](regional-editor-mvp.md).
|
||||
@@ -21,7 +21,6 @@ In Version 1 ist nur die Overlap-Semantik `joint` ausführbar. Die reservierten
|
||||
- `BV Regional Preview Send` (interne kompatible Klassenkennung `BV Regional Image Send`): sendet ein temporäres Preview-Ergebnis an den Editor eines ausgewählten `BV Regional Prompt`. Die sichtbare Auswahl verwendet Node-Titel und Node-ID, während intern die stabile `document_id` gespeichert wird. Bei mehreren ausgeführten Sendern gewinnt das zuletzt beim Frontend eingetroffene Ergebnis; entfernte Ziele werden nicht stillschweigend auf ein anderes Dokument umgebogen.
|
||||
- `BV Regional Save Send` (interne Klassenkennung `BV Regional Image Save`): speichert das Bild regulär im ComfyUI-Output, sendet dasselbe gespeicherte Ergebnis an den ausgewählten Editor und reicht das Bild am Ausgang weiter. Zielauflösung und Last-Sender-Wins-Semantik entsprechen der Preview-Variante.
|
||||
Das modellbezogene Backend wird erst beim Ausführen dieser Node importiert. Fehlende optionale Abhängigkeiten oder inkompatible ComfyUI-Patcher-Schnittstellen dürfen deshalb nicht die Registrierung des übrigen BV NodePacks verhindern.
|
||||
- `BV Regional Image Send`: IMAGE-Passthrough, das eine temporäre Preview an eine `document_id` sendet. Pro Dokument gewinnt die zuletzt ausgeführte Sender-Node.
|
||||
|
||||
## Editor-Verhalten
|
||||
|
||||
@@ -75,23 +74,23 @@ Geometrieebenen besitzen dafür rückwärtskompatible optionale Felder `enabled`
|
||||
|
||||
## Hintergrundbilder
|
||||
|
||||
`BV Regional Image Send` speichert ausschließlich eine ComfyUI-Temp-Preview. Die Browser-Session ordnet das letzte Ergebnis anhand der `document_id` zu. Das Bild wird nicht in `BV_REGIONAL` und nicht als Base64 im Workflow persistiert. Ein Workflow-Reload beginnt daher absichtlich ohne Sender-Hintergrund, bis die Sender-Node erneut ausgeführt wird.
|
||||
`BV Regional Preview Send` speichert ausschließlich eine ComfyUI-Temp-Preview. Die Browser-Session ordnet das letzte Ergebnis anhand der `document_id` zu. Das Bild wird nicht in `BV_REGIONAL` und nicht als Base64 im Workflow persistiert. Ein Workflow-Reload beginnt daher absichtlich ohne Sender-Hintergrund, bis die Sender-Node erneut ausgeführt wird.
|
||||
|
||||
## Geplante Editor-Erweiterungen
|
||||
## Prompt-Bearbeitung und Completion
|
||||
|
||||
### Prompt-Schnellbearbeitung
|
||||
|
||||
Ohne den vollständigen Editor zu öffnen, soll eine kompakte Prompt-Palette erreichbar sein. Sie bietet direkte Ziele für Global, Background und jede Region und zeigt jeweils Positive und Negative als editierbare Textfelder. Die Zielauswahl verwendet sichtbare Namen, speichert intern jedoch stabile IDs. Änderungen laufen durch dieselbe Validierungs-, AST- und Undo-Pipeline wie Änderungen im vollständigen Editor; es entsteht keine zweite Prompt-Datenquelle.
|
||||
Ohne den vollständigen Editor zu öffnen, ist eine kompakte Prompt-Palette erreichbar. Sie bietet direkte Ziele für Global, Background und jede Region und zeigt jeweils Positive und Negative als editierbare Textfelder. Die Zielauswahl verwendet sichtbare Namen, speichert intern jedoch stabile IDs. Änderungen laufen durch dieselbe Validierungs- und Dokumentpipeline wie Änderungen im vollständigen Editor; es entsteht keine zweite Prompt-Datenquelle.
|
||||
|
||||
Die Schnellbearbeitung soll sowohl über die jeweilige `BV Regional Prompt`-Node als auch über die globale Action-Bar-Oberfläche für das aktuell gewählte Dokument erreichbar sein. Bei mehreren Editor-Nodes muss vor dem Bearbeiten eindeutig sichtbar sein, welches Dokument aktiv ist.
|
||||
Die Schnellbearbeitung ist sowohl über die jeweilige `BV Regional Prompt`-Node als auch über die globale Action-Bar-Oberfläche erreichbar. Bei mehreren Editor-Nodes zeigt die Dokumentauswahl eindeutig, welches Dokument aktiv ist; das zuletzt gewählte Prompt-Ziel wird pro Dokument gespeichert.
|
||||
|
||||
### Autovervollständigung und Textassistenz
|
||||
|
||||
Die Promptfelder verwenden `BV Global Completion`, ein nodepack-weites Completion-Modul mit austauschbarer Suggestion-Provider-Schnittstelle. Dieselbe Engine integriert neben BV React-Editoren auch geeignete klassische ComfyUI- und Nodes-2.0-Multiline-Widgets, sodass Benutzer kein zweites Completion-System benötigen. Details und Integrationsverträge stehen in `docs/specs/bv-global-completion.md`.
|
||||
Die Promptfelder verwenden `BV Global Completion`, ein nodepack-weites Completion-Modul mit austauschbarer Suggestion-Provider-Schnittstelle. Dieselbe Engine integriert neben BV React-Editoren auch geeignete klassische ComfyUI- und Nodes-2.0-Multiline-Widgets. Sie verwendet lokale CSV-/TSV-Datasets, unterstützt priorisierte Quellen und kann global oder im Editor deaktiviert werden. Details und Integrationsverträge stehen in `docs/specs/bv-global-completion.md`.
|
||||
|
||||
`comfy-ex-tagcomplete` und vergleichbare Widget-Extensions können abgekoppelte React-/Floating-Textfelder nicht über einen gemeinsamen stabilen nativen Opt-in-Hook registrieren. BV emuliert deshalb weder Node-DOM noch importiert es private Frontendklassen fremder Extensions. Normale `<textarea>`-Semantik und bubbling `input`-Events bleiben für allgemeine Interoperabilität erhalten. Bridges zu einzelnen Fremdsystemen sind außerhalb des MVP als optionale Adapter denkbar, dürfen aber weder Kernfunktion noch lokales Datenformat bestimmen.
|
||||
|
||||
Das Completion-System wird von Beginn an für weitere Providerklassen vorbereitet: lokale Tagdaten, Embeddings, LoRAs, Wildcards, AST-/Kontextvorschläge sowie später Rechtschreib-, Übersetzungs- oder Sprachassistenz. Vorschläge verändern den kanonischen Prompt niemals ohne explizite Benutzerübernahme und jede Übernahme muss über den normalen Editor-Undo-Pfad rückgängig sein.
|
||||
Das Completion-System ist für weitere Providerklassen vorbereitet: Embeddings, LoRAs, Wildcards, AST-/Kontextvorschläge sowie später Rechtschreib-, Übersetzungs- oder Sprachassistenz. Vorschläge verändern den kanonischen Prompt niemals ohne explizite Benutzerübernahme.
|
||||
|
||||
Rechtschreib- oder Sprachkorrektur wird als separater optionaler Text-Assistant vorgesehen. Sie darf niemals ungefragt den kanonischen Prompt verändern und muss Vorschlag, Übernahme und Rücknahme klar trennen. Eine mögliche TextLab-Integration wird erst nach Prüfung ihrer konkreten Schnittstelle festgelegt.
|
||||
|
||||
@@ -112,7 +111,7 @@ Interaktiv verifiziert:
|
||||
- Krea 2 über `BV Regional Native Conditioning`: lokale Farb-/Musterzuordnung und Prompt-Swap funktionieren; getrennte Instanzerzeugung ohne Global-Komposition bleibt eine Grenze des nativen Backends.
|
||||
- Anima über den eingebauten `BV Regional Anima Conditioning`-Patcher: zwei getrennte Charaktere, regionale Haar-/Outfit-Zuordnung, Background-Routing und normaler KSampler funktionieren im realen GPU-Lauf. Der externe Sen-sou-Apply-Node war dabei nicht Bestandteil des Ausführungspfads.
|
||||
|
||||
Vor dem ersten Release noch interaktiv in ComfyUI prüfen:
|
||||
Interaktive Smoke-Test-Matrix für Releases:
|
||||
|
||||
1. Node anlegen, Editor über Node und Action Bar öffnen.
|
||||
2. Zwei `BV Regional Prompt`-Nodes anlegen und im Editor wechseln.
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# ComfyUI Nodes Manager tile metadata research
|
||||
|
||||
Research date: 2026-08-17
|
||||
|
||||
## Conclusion
|
||||
|
||||
The wide image at the top of a node-pack tile in the current Nodes Manager is
|
||||
the Registry **banner**, not a README image. It is configured together with a
|
||||
separate square icon in `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[tool.comfy]
|
||||
PublisherId = "blackvortex"
|
||||
DisplayName = "BV-NodePack"
|
||||
Icon = "https://raw.githubusercontent.com/BlackVortexAI/bv_nodepack/main/docs/assets/registry/bv-nodepack-icon.png"
|
||||
Banner = "https://raw.githubusercontent.com/BlackVortexAI/bv_nodepack/main/docs/assets/registry/bv-nodepack-banner.png"
|
||||
```
|
||||
|
||||
The official specification says both fields are used by the Comfy Registry and
|
||||
ComfyUI-Manager. `Icon` must be SVG, PNG, JPG, or GIF, square, and no larger
|
||||
than 400 x 400 pixels. `Banner` accepts the same file formats and must be 21:9.
|
||||
The specification does not currently state a maximum banner resolution. See
|
||||
the official [`pyproject.toml` specification](https://docs.comfy.org/registry/specifications#icon-optional).
|
||||
|
||||
For BV, create and publish both assets. A 400 x 400 PNG or SVG icon and a
|
||||
1680 x 720 (21:9) PNG banner are conservative choices. The banner should carry
|
||||
the recognizable BV visual identity and remain readable after center-cropping;
|
||||
the icon should work without small text. The URL must be publicly and directly
|
||||
fetchable. The official example uses `raw.githubusercontent.com`.
|
||||
|
||||
## Current BV repository state
|
||||
|
||||
The repository already has the main Registry metadata and an automated publish
|
||||
workflow:
|
||||
|
||||
- `[project].name = "bv_nodepack"`
|
||||
- `[project].version = "0.3.1"`
|
||||
- a detailed `[project].description`
|
||||
- `[project.urls]` for repository, issues, and documentation
|
||||
- `[tool.comfy].PublisherId = "blackvortex"`
|
||||
- `[tool.comfy].DisplayName = "BV-NodePack"`
|
||||
- `.github/workflows/publish.yml` using the official publish action
|
||||
|
||||
The missing pieces for the visual tile are that `Icon` is currently empty and
|
||||
`Banner` is absent. The existing `docs/assets/bv-nodepack-hero.svg` is a README
|
||||
hero and is not automatically discovered as Registry artwork.
|
||||
|
||||
Recommended repository layout:
|
||||
|
||||
```text
|
||||
docs/assets/registry/
|
||||
bv-nodepack-icon.svg # or .png; square, max 400 x 400
|
||||
bv-nodepack-banner.png # 21:9, e.g. 1680 x 720
|
||||
```
|
||||
|
||||
After the files exist on the public GitHub repository, add their raw URLs to
|
||||
`[tool.comfy]`. Do not point at a local path or at the normal GitHub HTML file
|
||||
page. A branch-based `main` URL is easy to maintain; an immutable commit URL is
|
||||
more reproducible but must be changed whenever the artwork changes. Image-cache
|
||||
delay when replacing content behind an unchanged URL is a practical possibility,
|
||||
not a behavior guaranteed by the official documentation.
|
||||
|
||||
## Metadata that drives the listing
|
||||
|
||||
The documented author-controlled fields are:
|
||||
|
||||
| Field | Status | Listing purpose |
|
||||
| --- | --- | --- |
|
||||
| `[project].name` | required | Globally unique Registry node ID and install name; immutable after creation |
|
||||
| `[project].version` | required | Three-part semantic version used for releases and upgrades |
|
||||
| `[project].description` | recommended | Short node-pack summary shown by Registry/Manager clients |
|
||||
| `[project.urls].Repository` | required | Source repository |
|
||||
| `[project.urls].Documentation` | recommended | Documentation destination |
|
||||
| `[project.urls]."Bug Tracker"` | recommended | Support/issues destination |
|
||||
| `[tool.comfy].PublisherId` | required | Globally unique Registry publisher identity |
|
||||
| `[tool.comfy].DisplayName` | optional | User-facing pack name; can be changed later |
|
||||
| `[tool.comfy].Icon` | optional | Square icon URL, max 400 x 400 |
|
||||
| `[tool.comfy].Banner` | optional | Wide 21:9 banner URL |
|
||||
| `[tool.comfy].requires-comfyui` | optional | Compatible ComfyUI version constraint |
|
||||
| `[project].dependencies` | optional | Python/frontend package requirements; the frontend package can express frontend-version compatibility |
|
||||
| `[project].classifiers` | recommended | OS and accelerator discovery metadata |
|
||||
|
||||
These definitions and validation rules come from the official
|
||||
[`pyproject.toml` specification](https://docs.comfy.org/registry/specifications).
|
||||
The Registry API response additionally contains tags, compatibility projections,
|
||||
download counts, ratings, repository stars, status, and publisher data. Those
|
||||
fields explain other card/filter data, but the current `pyproject.toml`
|
||||
specification does not document a free-form `[tool.comfy]` tags key. BV should
|
||||
therefore not invent one. See the official
|
||||
[`GET /nodes/{nodeId}` schema](https://docs.comfy.org/registry/api-reference/nodes/retrieve-a-specific-node-by-id).
|
||||
|
||||
The current BV description is accurate but very long for a compact card. A
|
||||
shorter first sentence would improve scanning, for example:
|
||||
|
||||
> Regional prompting, structured prompts, Smart Pipes, Subgraph controls, and
|
||||
> workflow utilities for current ComfyUI.
|
||||
|
||||
This is a content recommendation, not a Registry length requirement; the
|
||||
official specification only calls for a brief description.
|
||||
|
||||
## Publishing and update behavior
|
||||
|
||||
The Comfy Registry is the backend that powers ComfyUI-Manager. The supported
|
||||
publication flow is:
|
||||
|
||||
1. Create a publisher at the Comfy Registry. Its ID is globally unique and
|
||||
cannot be changed later.
|
||||
2. Create a Registry publishing API key.
|
||||
3. Maintain the metadata in `pyproject.toml`.
|
||||
4. Increment `[project].version` using semantic versioning.
|
||||
5. Publish with `comfy node publish`, or run the official GitHub publish action.
|
||||
|
||||
The official publishing guide explicitly shows a workflow triggered by changes
|
||||
to `pyproject.toml`, and says that pushing an updated version causes the updated
|
||||
node to appear in the Registry. See
|
||||
[`Publishing Nodes`](https://docs.comfy.org/registry/publishing#publish-to-the-registry).
|
||||
BV already has `.github/workflows/publish.yml` and therefore only needs the
|
||||
assets, metadata entries, a version increment, and a successful workflow run.
|
||||
The repository secret `REGISTRY_ACCESS_TOKEN` must exist for that action.
|
||||
|
||||
Published versions are immutable. To change packaged contents, publish a new
|
||||
version; a released version cannot be overwritten. A faulty version can be
|
||||
deprecated in the Registry UI, which warns installed users and directs them to
|
||||
a newer version. See the official
|
||||
[`Registry overview`](https://docs.comfy.org/registry/overview#frequently-asked-questions).
|
||||
|
||||
The official Registry API also exposes an update operation for node-level
|
||||
metadata, including `banner_url`, `description`, `icon`, repository, tags, and
|
||||
compatibility fields. This proves that visual metadata is node-level data, but
|
||||
the documented, repository-centered workflow remains `pyproject.toml` plus a
|
||||
new publish. See
|
||||
[`Update a specific node`](https://docs.comfy.org/api-reference/registry/update-a-specific-node).
|
||||
|
||||
## Documentation inconsistency
|
||||
|
||||
The publishing tutorial's generated example still comments that `Icon` permits
|
||||
"MAX. 800x400px". That conflicts with the dedicated current specification,
|
||||
which defines `Icon` as square and at most 400 x 400 and defines `Banner`
|
||||
separately as 21:9. Treat the dedicated specification as normative: use a
|
||||
maximum 400 x 400 square icon and a separate 21:9 banner. Compare
|
||||
[`Publishing Nodes: Add Metadata`](https://docs.comfy.org/registry/publishing#add-metadata)
|
||||
with the
|
||||
[`pyproject.toml` Icon/Banner specification](https://docs.comfy.org/registry/specifications#icon-optional).
|
||||
|
||||
## Legacy Manager boundary
|
||||
|
||||
The old Manager database is not the place to add this artwork. The official
|
||||
ComfyUI-Manager repository describes `node_db` as the JSON database powering
|
||||
the **legacy** registry system and identifies the online Custom Node Registry as
|
||||
the replacement. See the official
|
||||
[`node_db` README](https://github.com/Comfy-Org/ComfyUI-Manager/blob/main/node_db/README.md).
|
||||
|
||||
Therefore:
|
||||
|
||||
- do not add image metadata to the README and expect Manager discovery;
|
||||
- do not submit a local/legacy `custom-node-list.json` image change for this;
|
||||
- publish `Icon` and `Banner` through BV's Registry `pyproject.toml` metadata.
|
||||
|
||||
## Concrete BV implementation checklist
|
||||
|
||||
1. Design a square BV icon, at most 400 x 400, without small copy.
|
||||
2. Design a 21:9 BV banner (recommended working size: 1680 x 720) with safe
|
||||
margins and minimal text.
|
||||
3. Commit both under `docs/assets/registry/` and verify their raw GitHub URLs
|
||||
return the image directly without authentication.
|
||||
4. Set both `Icon` and `Banner` in `[tool.comfy]`.
|
||||
5. Optionally shorten the project description's opening sentence for the card.
|
||||
6. Increment the semantic version; do not try to replace release `0.3.1`.
|
||||
7. Push and confirm the publish workflow succeeds.
|
||||
8. Verify the Registry detail page and current Nodes Manager tile at normal and
|
||||
narrow widths; specifically inspect crop, contrast, transparent SVG handling,
|
||||
description truncation, and icon/banner fallback.
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "bv_nodepack"
|
||||
version = "0.3.1"
|
||||
description = "Advanced ComfyUI workflow toolkit featuring a model-neutral regional prompting editor, native and Anima conditioning backends, optional local CSV/TSV prompt completion for BV and ordinary ComfyUI multiline fields, image feedback senders, structured AST prompts, wireless Smart Pipes, Subgraph-native controls, composable UI nodes, and flexible latent utilities."
|
||||
version = "0.3.2"
|
||||
description = "Regional prompting, structured prompts, Smart Pipes, Subgraph controls, local autocomplete, and workflow utilities for current ComfyUI."
|
||||
authors = [
|
||||
{name = "BlackVortex", email = "blackvortexx@proton.me"}
|
||||
]
|
||||
@@ -18,4 +18,5 @@ Documentation = "https://github.com/BlackVortexAI/bv_nodepack/wiki"
|
||||
[tool.comfy]
|
||||
PublisherId = "blackvortex"
|
||||
DisplayName = "BV-NodePack"
|
||||
Icon = ""
|
||||
Icon = "https://raw.githubusercontent.com/BlackVortexAI/bv_nodepack/main/docs/assets/registry/bv-nodepack-icon.png"
|
||||
Banner = "https://raw.githubusercontent.com/BlackVortexAI/bv_nodepack/main/docs/assets/registry/bv-nodepack-banner.png"
|
||||
|
||||
@@ -18,16 +18,25 @@ test("editor state is isolated by stable document id", () => {
|
||||
});
|
||||
|
||||
test("invalid persisted values are normalized without entering semantic document data", () => {
|
||||
const normalized = normalizeEditorState({ mode: "garbage", panels: { left: -1, right: 9000 }, artboard: { zoom: 99 }, displayOpacity: -4 }, viewport);
|
||||
const normalized = normalizeEditorState({ mode: "garbage", panels: { left: -1, right: 9000 }, artboard: { zoom: 99 }, displayOpacity: -4, backgroundOpacity: 4 }, viewport);
|
||||
assert.equal(normalized.version, 1);
|
||||
assert.equal(normalized.mode, "workspace");
|
||||
assert.deepEqual(normalized.panels, { left: 210, right: 620 });
|
||||
assert.equal(normalized.artboard.zoom, 8);
|
||||
assert.equal(normalized.displayOpacity, .05);
|
||||
assert.equal(normalized.backgroundOpacity, 1);
|
||||
assert.equal(normalized.openMenu, null);
|
||||
assert.equal("document_id" in normalized, false);
|
||||
});
|
||||
|
||||
test("background display opacity survives save and reload per document", () => {
|
||||
const storage = memoryStorage(), state = defaultEditorState(viewport);
|
||||
state.backgroundOpacity = .35;
|
||||
saveEditorState("doc", state, storage);
|
||||
assert.equal(loadEditorState("doc", storage, viewport).backgroundOpacity, .35);
|
||||
assert.equal(loadEditorState("other-doc", storage, viewport).backgroundOpacity, 1);
|
||||
});
|
||||
|
||||
test("floating windows remain recoverable after viewport changes", () => {
|
||||
const value = clampWindowGeometry({ x: 5000, y: -300, width: 1200, height: 700 }, { width: 1000, height: 700 }, "floating");
|
||||
assert.ok(value.x <= 920);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { preserveDocumentIdentity } from "../ui/src/regional/model.ts";
|
||||
|
||||
const source = readFileSync(new URL("../ui/src/regional/RegionalEditor.tsx", import.meta.url), "utf8");
|
||||
|
||||
test("the full editor reloads the node document whenever it opens", () => {
|
||||
assert.match(source, /if \(!open \|\| !node\) return;/);
|
||||
assert.match(source, /\}, \[node, open\]\);/);
|
||||
});
|
||||
|
||||
test("replacing document content preserves the target editor identity", () => {
|
||||
const imported = { document_id: "example-document", title: "Imported example" };
|
||||
const replaced = preserveDocumentIdentity(imported, "active-editor");
|
||||
assert.equal(replaced.document_id, "active-editor");
|
||||
assert.equal(replaced.title, "Imported example");
|
||||
assert.equal(imported.document_id, "example-document");
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { regionsInPaintOrder, regionsInPriorityOrder, shouldAppendFinalBrushPoint, shouldStartSelectionMove, synchronizeRegionPriorities } from "../ui/src/regional/interaction.ts";
|
||||
import { brushAddLayerTarget, regionsInPaintOrder, regionsInPriorityOrder, shouldAppendFinalBrushPoint, shouldStartSelectionMove, synchronizeRegionPriorities } from "../ui/src/regional/interaction.ts";
|
||||
|
||||
test("select ignores pointer jitter instead of mutating a compound brush mask", () => {
|
||||
assert.equal(shouldStartSelectionMove({ x: 100, y: 100 }, { x: 101, y: 101 }), false);
|
||||
@@ -28,3 +28,15 @@ test("only drawing appends the pointer-up point to a brush stroke", () => {
|
||||
assert.equal(shouldAppendFinalBrushPoint("move"), false);
|
||||
assert.equal(shouldAppendFinalBrushPoint("resize"), false);
|
||||
});
|
||||
|
||||
test("brush add extends the selected editable brush layer", () => {
|
||||
const layer = { id: "brush-layer", enabled: true, authoring: { locked: false }, geometries: [{ type: "brush_stroke" }] };
|
||||
assert.equal(brushAddLayerTarget(layer), "brush-layer");
|
||||
});
|
||||
|
||||
test("brush add creates a layer when the selection cannot be extended", () => {
|
||||
assert.equal(brushAddLayerTarget(null), null);
|
||||
assert.equal(brushAddLayerTarget({ id: "rectangle", enabled: true, authoring: { locked: false }, geometries: [{ type: "rect" }] }), null);
|
||||
assert.equal(brushAddLayerTarget({ id: "locked", enabled: true, authoring: { locked: true }, geometries: [{ type: "brush_stroke" }] }), null);
|
||||
assert.equal(brushAddLayerTarget({ id: "disabled", enabled: false, authoring: { locked: false }, geometries: [{ type: "brush_stroke" }] }), null);
|
||||
});
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
.artboard-viewport.pan-ready:active { cursor: grabbing; }
|
||||
.artboard-stage { position: absolute; top: 0; left: 0; transform-origin: 0 0; will-change: transform; }
|
||||
.bv-regional-canvas { position: relative; background-color: #24282d; background-image: linear-gradient(45deg,#2c3137 25%,transparent 25%),linear-gradient(-45deg,#2c3137 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#2c3137 75%),linear-gradient(-45deg,transparent 75%,#2c3137 75%); background-size: 24px 24px; background-position: 0 0,0 12px,12px -12px,-12px 0; touch-action: none; user-select: none; box-shadow: 0 4px 28px #000a; }
|
||||
.bv-background-image { position: absolute; inset: 0; background-size: cover; background-repeat: no-repeat; pointer-events: none; }
|
||||
.bv-composition, .bv-selection-overlay { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||
.bv-composition { pointer-events: none; }
|
||||
.bv-selection-overlay { overflow: visible; pointer-events: none; }
|
||||
|
||||
@@ -17,6 +17,7 @@ type Props = {
|
||||
brush: BrushSettings;
|
||||
canSubtract: boolean;
|
||||
displayOpacity: number;
|
||||
backgroundOpacity: number;
|
||||
isolate: boolean;
|
||||
onPointerDown: (event: React.PointerEvent<HTMLDivElement>) => void;
|
||||
onPointerMove: (event: React.PointerEvent<HTMLDivElement>) => void;
|
||||
@@ -103,9 +104,18 @@ export default function Artboard(props: Props) {
|
||||
<ToolPalette tool={props.tool} brush={props.brush} canSubtract={props.canSubtract} canvas={document.canvas} onTool={props.onTool} onBrush={props.onBrush}/>
|
||||
<div className="artboard-stage" style={{ width: document.canvas.width, height: document.canvas.height, transform: `translate(${stageLeft}px, ${stageTop}px) scale(${zoom})` }}><div
|
||||
className={`bv-regional-canvas tool-${props.tool}`}
|
||||
style={{ width: document.canvas.width, height: document.canvas.height, backgroundImage: props.background ? `linear-gradient(#0002,#0002), url(${JSON.stringify(props.background)})` : undefined, backgroundSize: props.background ? "cover" : undefined }}
|
||||
style={{ width: document.canvas.width, height: document.canvas.height }}
|
||||
onPointerDown={props.onPointerDown} onPointerMove={props.onPointerMove} onPointerUp={props.onPointerUp} onPointerCancel={props.onPointerCancel} onPointerLeave={props.onPointerLeave}
|
||||
>
|
||||
{props.background && (
|
||||
<div
|
||||
className="bv-background-image"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(#0002,#0002), url(${JSON.stringify(props.background)})`,
|
||||
opacity: props.backgroundOpacity,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<svg className="bv-composition" viewBox={`0 0 ${document.canvas.width} ${document.canvas.height}`} preserveAspectRatio="none">{regionsInPaintOrder(document.regions.filter(region => region.enabled && region.authoring.visible)).map(region => <RegionMask key={region.id} region={region} geometries={geometriesFor(region)} canvas={document.canvas} opacity={props.displayOpacity}/>)}</svg>
|
||||
{props.selectionBounds && <svg className="bv-selection-overlay" viewBox={`0 0 ${document.canvas.width} ${document.canvas.height}`} preserveAspectRatio="none">
|
||||
<rect className="brush-bounds" x={props.selectionBounds.x * document.canvas.width} y={props.selectionBounds.y * document.canvas.height} width={props.selectionBounds.width * document.canvas.width} height={props.selectionBounds.height * document.canvas.height}/>
|
||||
|
||||
@@ -5,9 +5,11 @@ import DatasetPicker from "../completion/DatasetPicker";
|
||||
|
||||
type Props = {
|
||||
displayOpacity: number;
|
||||
backgroundOpacity: number;
|
||||
isolate: boolean;
|
||||
hasSelection: boolean;
|
||||
onDisplayOpacity: (value: number) => void;
|
||||
onBackgroundOpacity: (value: number) => void;
|
||||
onToggleIsolate: () => void;
|
||||
onExportDocument: () => void;
|
||||
onExportRegions: () => void;
|
||||
@@ -41,7 +43,7 @@ export default function EditorMenus(props: Props) {
|
||||
<button onClick={() => { regionsInput.current?.click(); props.onOpenMenu(null); }}>Add Regions</button>
|
||||
</div>}</div>
|
||||
<div className="menu-item"><button onClick={() => toggle("edit")}>Edit</button>{menu === "edit" && <div className="menu-popover completion-menu"><button disabled={!props.canUndo} onClick={props.onUndo}>Undo <kbd>Ctrl Z</kbd></button><button disabled={!props.canRedo} onClick={props.onRedo}>Redo <kbd>Ctrl Shift Z</kbd></button><hr/><button className={autocomplete ? "active" : ""} onClick={() => setCompletionEnabled(!autocomplete, true)}>{autocomplete ? "✓ " : ""}Prompt Autocomplete</button><small>Optional. Disable it when using another completion extension.</small><hr/><DatasetPicker/></div>}</div>
|
||||
<div className="menu-item"><button onClick={() => toggle("view")}>View</button>{menu === "view" && <div className="menu-popover view-menu"><label>Mask Display <output>{Math.round(props.displayOpacity * 100)} %</output><input type="range" min=".05" max="1" step=".05" value={props.displayOpacity} onChange={event => props.onDisplayOpacity(+event.target.value)}/></label><button disabled={!props.hasSelection} className={props.isolate ? "active" : ""} onClick={props.onToggleIsolate}>Isolate Selection</button><small>Affects the editor display only.</small></div>}</div>
|
||||
<div className="menu-item"><button onClick={() => toggle("view")}>View</button>{menu === "view" && <div className="menu-popover view-menu"><label>Mask Display <output>{Math.round(props.displayOpacity * 100)} %</output><input type="range" min=".05" max="1" step=".05" value={props.displayOpacity} onChange={event => props.onDisplayOpacity(+event.target.value)}/></label><label>Background Image <output>{Math.round(props.backgroundOpacity * 100)} %</output><input type="range" min="0" max="1" step=".05" value={props.backgroundOpacity} onChange={event => props.onBackgroundOpacity(+event.target.value)}/></label><button disabled={!props.hasSelection} className={props.isolate ? "active" : ""} onClick={props.onToggleIsolate}>Isolate Selection</button><small>Affects the editor display only.</small></div>}</div>
|
||||
<div className="menu-item"><button onClick={() => toggle("artboard")}>Artboard</button>{menu === "artboard" && <div className="menu-popover artboard-menu"><label>Width<input type="number" min="1" value={props.canvas.width} onChange={event => props.onCanvas({ ...props.canvas, width: Math.max(1, +event.target.value) })}/></label><label>Height<input type="number" min="1" value={props.canvas.height} onChange={event => props.onCanvas({ ...props.canvas, height: Math.max(1, +event.target.value) })}/></label></div>}</div>
|
||||
<div className="menu-item"><button onClick={() => toggle("layers")}>Layers</button>{menu === "layers" && <div className="menu-popover"><button disabled title="Planned feature">Split Disconnected Areas…</button><small>Connected-component detection will follow in a later iteration.</small></div>}</div>
|
||||
<div className="menu-item"><button className="help-button" title="Keyboard and mouse commands" onClick={() => toggle("help")}>?</button>{menu === "help" && <div className="menu-popover help-menu"><section className="help-section"><strong>Keyboard</strong><dl><dt>Ctrl Z</dt><dd>Undo</dd><dt>Ctrl Shift Z</dt><dd>Redo</dd><dt>Escape</dt><dd>Cancel action</dd><dt>Delete</dt><dd>Delete layer</dd><dt>Shift + Resize</dt><dd>Scale proportionally</dd><dt>Space + Drag</dt><dd>Pan artboard</dd></dl></section><section className="help-section"><strong>Mouse</strong><dl><dt>Click</dt><dd>Select layer</dd><dt>Drag</dt><dd>Draw or move</dd><dt>Drag handles</dt><dd>Resize</dd><dt>Mouse wheel</dt><dd>Zoom under cursor</dd><dt>Middle button + Drag</dt><dd>Pan artboard</dd><dt>Double-click name</dt><dd>Rename</dd></dl></section></div>}</div>
|
||||
|
||||
@@ -5,8 +5,8 @@ import LayerPanel from "./LayerPanel";
|
||||
import OptionsPanel from "./OptionsPanel";
|
||||
import { BrushSettings, Tool } from "./ToolPalette";
|
||||
import { Bounds, boundsOfLayer, Handle, hitTest, moveLayer, resizeLayer, setLayerBounds, simplifyPoints } from "./geometry";
|
||||
import { regionsInPriorityOrder, shouldAppendFinalBrushPoint, shouldStartSelectionMove, synchronizeRegionPriorities } from "./interaction";
|
||||
import { clone, Geometry, geometryAuthoring, geometryLayerId, geometryLayers, newRegion, parseDocument, Point, RegionalDocument, Region, uuid } from "./model";
|
||||
import { brushAddLayerTarget, regionsInPriorityOrder, shouldAppendFinalBrushPoint, shouldStartSelectionMove, synchronizeRegionPriorities } from "./interaction";
|
||||
import { clone, Geometry, geometryAuthoring, geometryLayerId, geometryLayers, newRegion, parseDocument, Point, preserveDocumentIdentity, RegionalDocument, Region, uuid } from "./model";
|
||||
import { activeWindowGeometry, applyObservedWindowSize, clampWindowGeometry, defaultEditorState, EditorViewState, loadEditorState, observedWindowSize, saveEditorState } from "./editorState";
|
||||
|
||||
type NodeRef = { id: number | string; title?: string; widgets?: Array<{ name: string; value: unknown; callback?: (value: unknown) => void }>; graph?: { setDirtyCanvas?: (a: boolean, b: boolean) => void } };
|
||||
@@ -44,7 +44,7 @@ export default function RegionalEditor({ open, nodes, initialNode, backgrounds,
|
||||
const [node, setNode] = useState<NodeRef | null>(initialNode), [documentValue, setDocumentValue] = useState<RegionalDocument | null>(null);
|
||||
const [selectedRegionId, setSelectedRegionId] = useState<string | null>(null), [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
|
||||
const [tool, setTool] = useState<Tool>("select"), [brush, setBrush] = useState<BrushSettings>({ size: .04, hardness: .75, opacity: 1, shape: "round", pressureMode: "constant" });
|
||||
const [displayOpacity, setDisplayOpacity] = useState(.5), [isolate, setIsolate] = useState(false);
|
||||
const [displayOpacity, setDisplayOpacity] = useState(.5), [backgroundOpacity, setBackgroundOpacity] = useState(1), [isolate, setIsolate] = useState(false);
|
||||
const [history, setHistory] = useState<RegionalDocument[]>([]), [future, setFuture] = useState<RegionalDocument[]>([]);
|
||||
const [draft, setDraft] = useState<Geometry[] | null>(null), [cursor, setCursor] = useState<Point | null>(null), [error, setError] = useState("");
|
||||
const [viewState, setViewState] = useState<EditorViewState>(() => defaultEditorState());
|
||||
@@ -57,20 +57,20 @@ export default function RegionalEditor({ open, nodes, initialNode, backgrounds,
|
||||
|
||||
useEffect(() => { if (initialNode) setNode(initialNode); else if (!node && nodes[0]) setNode(nodes[0]); }, [initialNode, node, nodes]);
|
||||
useEffect(() => {
|
||||
if (!node) return;
|
||||
if (!open || !node) return;
|
||||
try {
|
||||
const next = parseDocument(getWidget(node)?.value); next.regions = regionsInPriorityOrder(next.regions); syncPriorities(next.regions);
|
||||
const stored = loadEditorState(next.document_id), regionId = next.regions.some(region => region.id === stored.selectedRegionId) ? stored.selectedRegionId : next.regions[0]?.id ?? null;
|
||||
const region = next.regions.find(item => item.id === regionId), layerId = region && geometryLayers(region).some(layer => layer.id === stored.selectedLayerId) ? stored.selectedLayerId : null;
|
||||
setViewState(stored); setDocumentValue(next); setSelectedRegionId(regionId); setSelectedLayerId(layerId); setTool(stored.tool); setBrush(stored.brush); setDisplayOpacity(stored.displayOpacity); setIsolate(stored.isolate && !!layerId); setHistory([]); setFuture([]); setError("");
|
||||
setViewState(stored); setDocumentValue(next); setSelectedRegionId(regionId); setSelectedLayerId(layerId); setTool(stored.tool); setBrush(stored.brush); setDisplayOpacity(stored.displayOpacity); setBackgroundOpacity(stored.backgroundOpacity); setIsolate(stored.isolate && !!layerId); setHistory([]); setFuture([]); setError("");
|
||||
}
|
||||
catch (reason) { setError(reason instanceof Error ? reason.message : String(reason)); }
|
||||
}, [node]);
|
||||
}, [node, open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!documentValue) return;
|
||||
saveEditorState(documentValue.document_id, { ...viewState, selectedRegionId, selectedLayerId, displayOpacity, isolate, tool, brush });
|
||||
}, [brush, displayOpacity, documentValue, isolate, selectedLayerId, selectedRegionId, tool, viewState]);
|
||||
saveEditorState(documentValue.document_id, { ...viewState, selectedRegionId, selectedLayerId, displayOpacity, backgroundOpacity, isolate, tool, brush });
|
||||
}, [backgroundOpacity, brush, displayOpacity, documentValue, isolate, selectedLayerId, selectedRegionId, tool, viewState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !shell.current || viewState.mode !== "floating") return;
|
||||
@@ -156,8 +156,9 @@ export default function RegionalEditor({ open, nodes, initialNode, backgrounds,
|
||||
return;
|
||||
}
|
||||
if (tool === "brush-subtract" && (!selectedLayerId || selectedLayer?.authoring.locked)) return;
|
||||
const id = uuid(), layerId = tool === "brush-subtract" ? selectedLayerId! : id;
|
||||
const authoring = tool === "brush-subtract" ? selectedLayer?.authoring : { name: `${tool === "rect" ? "Rectangle" : "Brush"} ${geometryLayers(selectedRegion).length + 1}`, visible: true, locked: false };
|
||||
const id = uuid(), reusableBrushLayerId = tool === "brush-add" ? brushAddLayerTarget(selectedLayer) : null;
|
||||
const layerId = tool === "brush-subtract" ? selectedLayerId! : reusableBrushLayerId ?? id;
|
||||
const authoring = tool === "brush-subtract" || reusableBrushLayerId ? selectedLayer?.authoring : { name: `${tool === "rect" ? "Rectangle" : "Brush"} ${geometryLayers(selectedRegion).length + 1}`, visible: true, locked: false };
|
||||
if (tool === "rect") {
|
||||
setSelectedLayerId(layerId); setDraft([{ id, layer_id: layerId, type: "rect", operation: "add", enabled: true, authoring, x: point.x, y: point.y, width: .001, height: .001 }]); gesture.current = { mode: "draw-rect", start: point, commit: "add" };
|
||||
} else {
|
||||
@@ -190,18 +191,18 @@ export default function RegionalEditor({ open, nodes, initialNode, backgrounds,
|
||||
const duplicateLayer = (id: string) => updateLayer(id, (operations, region) => { const layerId = uuid(), copies = moveLayer(clone(operations), .02, .02, documentValue!.canvas); copies.forEach((item, index) => { item.id = uuid(); item.layer_id = layerId; item.authoring = { ...geometryAuthoring(item), name: `${geometryAuthoring(item).name} Copy`, visible: true, locked: false }; if (index) item.authoring = copies[0].authoring; }); region.geometry.push(...copies); setSelectedLayerId(layerId); });
|
||||
const deleteRegion = (id: string) => { const region = documentValue?.regions.find(item => item.id === id); if (!region || !window.confirm(`Delete region “${region.name}” with ${geometryLayers(region).length} layer(s)?`)) return; mutate(value => { value.regions = value.regions.filter(item => item.id !== id); value.regions.forEach(item => { if (item.parent_region_id === id) item.parent_region_id = null; }); syncPriorities(value.regions); }); setSelectedRegionId(documentValue?.regions.find(item => item.id !== id)?.id ?? null); setSelectedLayerId(null); };
|
||||
|
||||
const importDocument = async (file: File) => { try { const imported = parseDocument(await file.text()); imported.regions = regionsInPriorityOrder(imported.regions); syncPriorities(imported.regions); const collision = nodes.some(candidate => candidate !== node && (() => { try { return parseDocument(getWidget(candidate)?.value).document_id === imported.document_id; } catch { return false; } })()); if (collision) imported.document_id = uuid(); persist(imported); setSelectedRegionId(imported.regions[0]?.id ?? null); setSelectedLayerId(null); } catch (reason) { alert(`Import failed: ${reason}`); } };
|
||||
const importDocument = async (file: File) => { try { if (!documentValue) return; const imported = preserveDocumentIdentity(parseDocument(await file.text()), documentValue.document_id); imported.regions = regionsInPriorityOrder(imported.regions); syncPriorities(imported.regions); persist(imported); setSelectedRegionId(imported.regions[0]?.id ?? null); setSelectedLayerId(null); } catch (reason) { alert(`Import failed: ${reason}`); } };
|
||||
const importRegions = async (file: File) => { try { const data = JSON.parse(await file.text()); if (data?.schema !== "bv.regions" || data?.version !== 1 || !Array.isArray(data.regions)) throw new Error("Not a BV Regions v1 export"); mutate(value => { value.regions.push(...remapImportedRegions(data.regions)); syncPriorities(value.regions); }); } catch (reason) { alert(`Import failed: ${reason}`); } };
|
||||
|
||||
if (!open) return null;
|
||||
const windowGeometry = activeWindowGeometry(viewState, { width: window.innerWidth, height: window.innerHeight });
|
||||
return <div ref={shell} className={`bv-regional-shell ${viewState.mode}`} role="dialog" aria-modal={viewState.mode === "workspace"} style={{ inset: "auto", left: windowGeometry.x, top: windowGeometry.y, width: windowGeometry.width, height: windowGeometry.height }} onKeyDown={event => event.stopPropagation()}>
|
||||
<header onPointerDown={beginWindowDrag}><EditorMenus openMenu={viewState.openMenu} onOpenMenu={updateOpenMenu} displayOpacity={displayOpacity} isolate={isolate} hasSelection={!!selectedLayerId} onDisplayOpacity={setDisplayOpacity} onToggleIsolate={() => setIsolate(value => !value)} onExportDocument={() => documentValue && downloadJson(`${documentValue.title || "bv-regional"}.json`, documentValue)} onExportRegions={() => documentValue && downloadJson(`${documentValue.title || "bv-regions"}.regions.json`, { schema: "bv.regions", version: 1, canvas: documentValue.canvas, regions: documentValue.regions })} onImportDocument={importDocument} onImportRegions={importRegions} onUndo={undo} onRedo={redo} canUndo={!!history.length} canRedo={!!future.length} canvas={documentValue?.canvas ?? { width: 1024, height: 1024 }} onCanvas={canvas => mutate(value => { value.canvas = canvas; })}/><div className="window-drag-handle"><span aria-hidden="true">⠿</span><strong>BV Regional Editor</strong></div><select value={node?.id ?? ""} onChange={event => setNode(nodes.find(item => String(item.id) === event.target.value) ?? null)}>{nodes.map(item => <option key={item.id} value={String(item.id)}>{`${item.title || "BV Regional Prompt"} · #${item.id}`}</option>)}</select><button className="window-mode-button" title={viewState.mode === "workspace" ? "Switch to floating window" : "Switch to workspace view"} onClick={() => setViewState(current => ({ ...current, mode: current.mode === "workspace" ? "floating" : "workspace" }))}>{viewState.mode === "workspace" ? "Float" : "Workspace"}</button><button title="Close" onClick={onClose}>×</button></header>
|
||||
<header onPointerDown={beginWindowDrag}><EditorMenus openMenu={viewState.openMenu} onOpenMenu={updateOpenMenu} displayOpacity={displayOpacity} backgroundOpacity={backgroundOpacity} isolate={isolate} hasSelection={!!selectedLayerId} onDisplayOpacity={setDisplayOpacity} onBackgroundOpacity={setBackgroundOpacity} onToggleIsolate={() => setIsolate(value => !value)} onExportDocument={() => documentValue && downloadJson(`${documentValue.title || "bv-regional"}.json`, documentValue)} onExportRegions={() => documentValue && downloadJson(`${documentValue.title || "bv-regions"}.regions.json`, { schema: "bv.regions", version: 1, canvas: documentValue.canvas, regions: documentValue.regions })} onImportDocument={importDocument} onImportRegions={importRegions} onUndo={undo} onRedo={redo} canUndo={!!history.length} canRedo={!!future.length} canvas={documentValue?.canvas ?? { width: 1024, height: 1024 }} onCanvas={canvas => mutate(value => { value.canvas = canvas; })}/><div className="window-drag-handle"><span aria-hidden="true">⠿</span><strong>BV Regional Editor</strong></div><select value={node?.id ?? ""} onChange={event => setNode(nodes.find(item => String(item.id) === event.target.value) ?? null)}>{nodes.map(item => <option key={item.id} value={String(item.id)}>{`${item.title || "BV Regional Prompt"} · #${item.id}`}</option>)}</select><button className="window-mode-button" title={viewState.mode === "workspace" ? "Switch to floating window" : "Switch to workspace view"} onClick={() => setViewState(current => ({ ...current, mode: current.mode === "workspace" ? "floating" : "workspace" }))}>{viewState.mode === "workspace" ? "Float" : "Workspace"}</button><button title="Close" onClick={onClose}>×</button></header>
|
||||
{error ? <div className="bv-regional-error">{error}</div> : documentValue && <>
|
||||
<main style={{ gridTemplateColumns: `${viewState.panels.left}px 6px minmax(300px, 1fr) 6px ${viewState.panels.right}px` }}>
|
||||
<LayerPanel regions={documentValue.regions} selectedRegionId={selectedRegionId} selectedLayerId={selectedLayerId} isolate={isolate} onSelectRegion={id => { setSelectedRegionId(id); setSelectedLayerId(null); setIsolate(false); }} onSelectLayer={setSelectedLayerId} onAddRegion={() => mutate(value => { const region = newRegion(value.regions.length); value.regions.unshift(region); syncPriorities(value.regions); setSelectedRegionId(region.id); setSelectedLayerId(null); })} onRenameRegion={(id, name) => mutate(value => { value.regions.find(region => region.id === id)!.name = name; })} onRenameLayer={(id, name) => updateLayer(id, operations => operations.forEach(item => { item.authoring = { ...geometryAuthoring(item), name }; }))} onToggleRegion={(id, field) => mutate(value => { const region = value.regions.find(item => item.id === id)!; if (field === "enabled") region.enabled = !region.enabled; else region.authoring.locked = !region.authoring.locked; })} onToggleLayer={toggleLayer} onMoveRegion={moveRegion} onMoveLayer={moveLayerOrder} onDeleteRegion={deleteRegion} onDeleteLayer={deleteLayer} onDuplicateLayer={duplicateLayer} onToggleIsolate={() => setIsolate(value => !value)}/>
|
||||
<div className="panel-splitter" onPointerDown={event => beginPanelResize("left", event)} onDoubleClick={() => setViewState(current => ({ ...current, panels: { ...current.panels, left: 290 } }))}/>
|
||||
<Artboard key={documentValue.document_id} document={documentValue} background={backgrounds[documentValue.document_id]} selectedRegionId={selectedRegionId} selectedLayerId={selectedLayerId} draft={draft} selectionBounds={selectionBounds} cursor={cursor} tool={tool} brush={brush} canSubtract={!!selectedLayer && !selectedLayer.authoring.locked} displayOpacity={displayOpacity} isolate={isolate} initialView={viewState.artboard} onView={updateArtboardView} onTool={next => { setTool(next); cancelGesture(); }} onBrush={setBrush} onPointerDown={pointerDown} onPointerMove={pointerMove} onPointerUp={pointerUp} onPointerCancel={cancelGesture} onPointerLeave={() => { if (!gesture.current) setCursor(null); }}/>
|
||||
<Artboard key={documentValue.document_id} document={documentValue} background={backgrounds[documentValue.document_id]} selectedRegionId={selectedRegionId} selectedLayerId={selectedLayerId} draft={draft} selectionBounds={selectionBounds} cursor={cursor} tool={tool} brush={brush} canSubtract={!!selectedLayer && !selectedLayer.authoring.locked} displayOpacity={displayOpacity} backgroundOpacity={backgroundOpacity} isolate={isolate} initialView={viewState.artboard} onView={updateArtboardView} onTool={next => { setTool(next); cancelGesture(); }} onBrush={setBrush} onPointerDown={pointerDown} onPointerMove={pointerMove} onPointerUp={pointerUp} onPointerCancel={cancelGesture} onPointerLeave={() => { if (!gesture.current) setCursor(null); }}/>
|
||||
<div className="panel-splitter" onPointerDown={event => beginPanelResize("right", event)} onDoubleClick={() => setViewState(current => ({ ...current, panels: { ...current.panels, right: 340 } }))}/>
|
||||
<OptionsPanel region={selectedRegion} layer={selectedLayer} bounds={selectionBounds} canvas={documentValue.canvas} globalPrompts={documentValue.prompts.global} backgroundPrompts={documentValue.prompts.background} negativeMode={documentValue.negative_mode} promptSections={viewState.promptSections} onPromptSection={(section, open) => setViewState(current => ({ ...current, promptSections: { ...current.promptSections, [section]: open } }))} onNegativeMode={negativeMode => mutate(value => { value.negative_mode = negativeMode; })} onGlobalPrompts={prompts => mutate(value => { value.prompts.global = prompts; })} onBackgroundPrompts={prompts => mutate(value => { value.prompts.background = prompts; })} onRegion={updateRegion} onLayerBounds={bounds => selectedLayerId && updateLayer(selectedLayerId, (operations, region) => { const replacements = new Map(setLayerBounds(operations, bounds, documentValue.canvas).map(item => [item.id, item])); region.geometry = region.geometry.map(item => replacements.get(item.id) ?? item); })} onBrushSetting={(field, setting) => selectedLayerId && updateLayer(selectedLayerId, operations => operations.forEach(item => { if (item.type === "brush_stroke") Object.assign(item, { [field]: setting }); }))}/>
|
||||
</main>
|
||||
|
||||
@@ -13,6 +13,7 @@ export type EditorViewState = {
|
||||
selectedRegionId: string | null;
|
||||
selectedLayerId: string | null;
|
||||
displayOpacity: number;
|
||||
backgroundOpacity: number;
|
||||
isolate: boolean;
|
||||
tool: Tool;
|
||||
brush: BrushSettings;
|
||||
@@ -83,6 +84,7 @@ export function defaultEditorState(viewport = { width: window.innerWidth, height
|
||||
selectedRegionId: null,
|
||||
selectedLayerId: null,
|
||||
displayOpacity: .5,
|
||||
backgroundOpacity: 1,
|
||||
isolate: false,
|
||||
tool: "select",
|
||||
brush: { size: .04, hardness: .75, opacity: 1, shape: "round", pressureMode: "constant" },
|
||||
@@ -117,6 +119,7 @@ export function normalizeEditorState(value: unknown, viewport = { width: window.
|
||||
selectedRegionId: typeof input.selectedRegionId === "string" ? input.selectedRegionId : null,
|
||||
selectedLayerId: typeof input.selectedLayerId === "string" ? input.selectedLayerId : null,
|
||||
displayOpacity: clamp(finite(input.displayOpacity, .5), .05, 1),
|
||||
backgroundOpacity: clamp(finite(input.backgroundOpacity, 1), 0, 1),
|
||||
isolate: input.isolate === true,
|
||||
tool: tools.includes(input.tool as Tool) ? input.tool as Tool : "select",
|
||||
brush: {
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
export type ScreenPoint = { x: number; y: number };
|
||||
type BrushLayerCandidate = {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
authoring: { locked: boolean };
|
||||
geometries: Array<{ type: string }>;
|
||||
};
|
||||
|
||||
export function brushAddLayerTarget(layer: BrushLayerCandidate | null) {
|
||||
if (!layer || !layer.enabled || layer.authoring.locked) return null;
|
||||
return layer.geometries.some(geometry => geometry.type === "brush_stroke") ? layer.id : null;
|
||||
}
|
||||
|
||||
export function shouldStartSelectionMove(start: ScreenPoint, current: ScreenPoint, threshold = 3) {
|
||||
return Math.hypot(current.x - start.x, current.y - start.y) >= threshold;
|
||||
|
||||
@@ -33,6 +33,9 @@ export const newRegion = (index: number): Region => ({
|
||||
authoring: { visible: true, locked: false, color: COLORS[index % COLORS.length] },
|
||||
});
|
||||
export const clone = <T,>(value: T): T => structuredClone(value);
|
||||
export function preserveDocumentIdentity<T extends { document_id: string }>(imported: T, documentId: string): T {
|
||||
return { ...imported, document_id: documentId };
|
||||
}
|
||||
export const geometryAuthoring = (geometry: Geometry, index = 0): GeometryAuthoring => geometry.authoring ?? { name: `${geometry.type === "rect" ? "Rectangle" : "Brush"} ${index + 1}`, visible: true, locked: false };
|
||||
export const geometryLayerId = (geometry: Geometry) => geometry.layer_id ?? geometry.id;
|
||||
export function geometryLayers(region: Region): GeometryLayer[] {
|
||||
|
||||