diff --git a/__init__.py b/__init__.py
index ab6d639..6e41740 100644
--- a/__init__.py
+++ b/__init__.py
@@ -13,6 +13,8 @@ from .nodes.attention_couple import AttentionCoupleNode
from .nodes.regional_detailer import RegionalDetailerNode, MaskedDetailNode
from .nodes.regional_conditioning import RegionalConditioningNode
from .nodes.defocus_mask import DefocusMaskNode
+from .nodes.ideogram_sampler import IdeogramSamplerNode
+from .nodes.ideogram_caption import IdeogramCaptionNode
# Timestamps on every PromptChain log line. ComfyUI's default formatter
@@ -220,7 +222,7 @@ _threading.Thread(target=_preload, daemon=True).start()
class PromptChainExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
- return [PromptChainNode, PoseStudioNode, AttentionCoupleNode, RegionalDetailerNode, MaskedDetailNode, RegionalConditioningNode, DefocusMaskNode]
+ return [PromptChainNode, PoseStudioNode, AttentionCoupleNode, RegionalDetailerNode, MaskedDetailNode, RegionalConditioningNode, DefocusMaskNode, IdeogramSamplerNode, IdeogramCaptionNode]
async def comfy_entrypoint() -> PromptChainExtension:
diff --git a/core/fingerprint.py b/core/fingerprint.py
index 6acced2..c877819 100644
--- a/core/fingerprint.py
+++ b/core/fingerprint.py
@@ -158,8 +158,13 @@ def _detect_safetensor_arch(filepath: str) -> str:
header = _read_safetensor_header(filepath)
if not header:
return "unknown"
- tensor_names = [k for k in list(header.keys())[:50] if k != "__metadata__"]
- names_str = " ".join(tensor_names)
+ all_names = [k for k in header.keys() if k != "__metadata__"]
+ # Ideogram 4's marker tensor sits ~200 keys deep in the fp8 checkpoints (leading
+ # keys are .weight_scale quant tensors), past the first-50 window below — so check
+ # the full key set, mirroring ComfyUI's detect_unet_config.
+ if any("embed_image_indicator." in k for k in all_names):
+ return "ideogram"
+ names_str = " ".join(all_names[:50])
for pattern, arch in ARCHITECTURE_PATTERNS:
if pattern in names_str:
if arch == "flux":
@@ -366,6 +371,11 @@ def scan_models() -> int:
cached_mtime = info.get("mtime", 0)
cached_size = info.get("size", -1)
if st.st_mtime == cached_mtime and st.st_size == cached_size:
+ # Re-detect a previously-unknown arch (cheap header read, no
+ # re-hash) so fingerprint improvements reclassify old files
+ # without a full cache wipe.
+ if info.get("architecture") == "unknown":
+ info = {**info, "architecture": detect_architecture(filepath)}
new_by_hash[h] = info
new_by_filename[filename.lower()] = h
new_by_path[filepath] = h
diff --git a/data/models/ideogram_4.json b/data/models/ideogram_4.json
new file mode 100644
index 0000000..3d63d5e
--- /dev/null
+++ b/data/models/ideogram_4.json
@@ -0,0 +1,162 @@
+{
+ "display_name": "Ideogram 4.0",
+ "model_name": "Ideogram 4.0",
+ "version": "Base",
+ "architecture": "ideogram",
+ "family": "ideogram4",
+ "author": "Ideogram",
+ "description": "Ideogram 4.0 open-weight text-to-image model (9.3B single-stream DiT) run locally in ComfyUI. Best-in-class in-image text rendering and typography. Uses a Qwen3-VL-8B text encoder and the Flux 2 VAE, plus a second 'unconditional' transformer that drives dual-branch CFG (there is no user negative prompt). Authored in natural language. Weights are non-commercial; generated images may be used commercially.",
+ "license": "Ideogram Non-Commercial Model Agreement (inference code Apache-2.0)",
+ "tags": [
+ "ideogram",
+ "ideogram4",
+ "text-rendering",
+ "typography",
+ "open-weight",
+ "consumer"
+ ],
+ "url": "https://huggingface.co/Comfy-Org/Ideogram-4",
+ "release_date": "2026-06-03",
+ "tag_sources": [],
+ "tag_format": "spaces",
+ "prompt_style": "natural",
+ "trigger": "",
+ "default_prompt_id": "ideogram-default",
+ "negative": "",
+ "notes": "REQUIRES a ComfyUI version with native Ideogram 4 support (the UNETLoader 'ideogram4' arch, CLIPLoader 'ideogram4' type, Ideogram4Scheduler, DualModelGuider, CFGOverride). Weights from the UN-GATED Comfy-Org/Ideogram-4 mirror (no HF token needed). Two transformers load via two UNETLoaders; the unconditional one feeds DualModelGuider.model_negative. Recommended: Default = 20 steps, Turbo = 12, Quality = 48; DualModelGuider guidance 7; CFGOverride [3, 0.7, 1]; scheduler mu 0.5 / std 1.75; sampler euler. Native output up to 2048px. On a 32 GB card (RTX 5090) full fp8 fits with text-encoder offload; drop the unconditional transformer or encoder to nvfp4 if VRAM is tight. NON-COMMERCIAL weights: fine for personal/local use; commercial deployment of the model needs Ideogram's paid license (generated images themselves are fine to use commercially, except training competing models).",
+ "files": [
+ {
+ "label": "Diffusion Model",
+ "folder": "diffusion_models",
+ "variants": [
+ {
+ "precision": "fp8",
+ "filename": "ideogram4_fp8_scaled.safetensors",
+ "quick_hash": "e9380be7fe0165c7",
+ "size_bytes": 9280741285,
+ "source": {
+ "provider": "huggingface",
+ "repo": "Comfy-Org/Ideogram-4",
+ "path": "diffusion_models/ideogram4_fp8_scaled.safetensors"
+ },
+ "sha256": null
+ },
+ {
+ "precision": "nvfp4",
+ "filename": "ideogram4_nvfp4_mixed.safetensors",
+ "size_bytes": 5490000000,
+ "source": {
+ "provider": "huggingface",
+ "repo": "Comfy-Org/Ideogram-4",
+ "path": "diffusion_models/ideogram4_nvfp4_mixed.safetensors"
+ },
+ "sha256": null
+ }
+ ]
+ },
+ {
+ "label": "Unconditional Transformer",
+ "folder": "diffusion_models",
+ "variants": [
+ {
+ "precision": "fp8",
+ "filename": "ideogram4_unconditional_fp8_scaled.safetensors",
+ "quick_hash": "76383548b85c3b56",
+ "size_bytes": 9280741293,
+ "source": {
+ "provider": "huggingface",
+ "repo": "Comfy-Org/Ideogram-4",
+ "path": "diffusion_models/ideogram4_unconditional_fp8_scaled.safetensors"
+ },
+ "sha256": null
+ },
+ {
+ "precision": "nvfp4",
+ "filename": "ideogram4_unconditional_nvfp4_mixed.safetensors",
+ "size_bytes": 5490000000,
+ "source": {
+ "provider": "huggingface",
+ "repo": "Comfy-Org/Ideogram-4",
+ "path": "diffusion_models/ideogram4_unconditional_nvfp4_mixed.safetensors"
+ },
+ "sha256": null
+ }
+ ]
+ },
+ {
+ "label": "Text Encoder (Qwen3-VL-8B)",
+ "folder": "text_encoders",
+ "variants": [
+ {
+ "precision": "fp8",
+ "filename": "qwen3vl_8b_fp8_scaled.safetensors",
+ "size_bytes": 10588637512,
+ "source": {
+ "provider": "huggingface",
+ "repo": "Comfy-Org/Ideogram-4",
+ "path": "text_encoders/qwen3vl_8b_fp8_scaled.safetensors"
+ },
+ "sha256": null
+ },
+ {
+ "precision": "nvfp4",
+ "filename": "qwen3vl_8b_nvfp4.safetensors",
+ "size_bytes": 6310000000,
+ "source": {
+ "provider": "huggingface",
+ "repo": "Comfy-Org/Ideogram-4",
+ "path": "text_encoders/qwen3vl_8b_nvfp4.safetensors"
+ },
+ "sha256": null
+ }
+ ]
+ },
+ {
+ "label": "VAE",
+ "folder": "vae",
+ "filename": "flux2-vae.safetensors",
+ "size_bytes": 336213556,
+ "source": {
+ "provider": "huggingface",
+ "repo": "Comfy-Org/Ideogram-4",
+ "path": "vae/flux2-vae.safetensors"
+ },
+ "sha256": null
+ }
+ ],
+ "nodes": {
+ "Ideogram4Scheduler": {
+ "steps": 20,
+ "steps_range": [
+ 12,
+ 48
+ ]
+ },
+ "EmptyFlux2LatentImage": {
+ "width": 1024,
+ "height": 1024,
+ "_resolutions": [
+ {
+ "width": 1024,
+ "height": 1024
+ },
+ {
+ "width": 896,
+ "height": 1152
+ },
+ {
+ "width": 1152,
+ "height": 896
+ },
+ {
+ "width": 832,
+ "height": 1216
+ },
+ {
+ "width": 1216,
+ "height": 832
+ }
+ ]
+ }
+ }
+}
diff --git a/data/prompts/ideogram.json b/data/prompts/ideogram.json
new file mode 100644
index 0000000..da3ef4a
--- /dev/null
+++ b/data/prompts/ideogram.json
@@ -0,0 +1,385 @@
+{
+ "name": "Ideogram 4 Prompts",
+ "prompts": [
+ {
+ "id": "ideogram-default",
+ "name": "Default",
+ "category": "Photographic",
+ "text": "// Your description\n{cursor}\n\n// Photographic - Default\nA candid natural-light photograph, soft daylight, true-to-life color and texture, sharp focus with a gently blurred background, relaxed off-center framing.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-photographic-cinematic",
+ "name": "Cinematic",
+ "category": "Photographic",
+ "text": "// Your description\n{cursor}\n\n// Photographic - Cinematic\nA cinematic film still, moody low-key lighting, soft teal-and-amber tones, atmospheric depth, a wide dramatic composition, a contemplative mood.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-photographic-studio_portrait",
+ "name": "Studio Portrait",
+ "category": "Photographic",
+ "text": "// Your description\n{cursor}\n\n// Photographic - Studio Portrait\nA clean studio portrait on a seamless grey backdrop, soft key light with a gentle rim light, natural skin detail, a softly blurred background, refined neutral color.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-photographic-product_shot",
+ "name": "Product Shot",
+ "category": "Photographic",
+ "text": "// Your description\n{cursor}\n\n// Photographic - Product Shot\nA clean studio product photo, the subject centered on a soft seamless gradient, gentle rim light and a subtle reflection beneath, crisp detail and true material color, generous negative space.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-photographic-editorial_fashion",
+ "name": "Editorial Fashion",
+ "category": "Photographic",
+ "text": "// Your description\n{cursor}\n\n// Photographic - Editorial Fashion\nA high-fashion editorial photo, dramatic studio lighting with crisp shadows, confident styling, a refined palette, a clean set, a polished glossy finish.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-design-poster",
+ "name": "Poster",
+ "category": "Design",
+ "text": "// Your description\n{cursor}\n\n// Design - Poster\nA bold graphic poster, large title 'YOUR TITLE' across the top and 'your subtitle here' below, a striking central subject, screen-print texture with flat high-contrast color blocking, strong typographic hierarchy, confident limited palette (color palette #1B1035, #FF2E97, #00E5FF). Portrait 2:3.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-design-logo_wordmark",
+ "name": "Logo / Wordmark",
+ "category": "Design",
+ "text": "// Your description\n{cursor}\n\n// Design - Logo / Wordmark\nA minimalist flat-vector logo, the wordmark 'BRAND NAME' in clean bold sans-serif with a small simple iconic mark above it, two-tone monochrome palette, perfectly centered with generous whitespace on a plain white background, sharp crisp edges, balanced professional brand mark. 1:1.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-design-badge_emblem",
+ "name": "Badge / Emblem",
+ "category": "Design",
+ "text": "// Your description\n{cursor}\n\n// Design - Badge / Emblem\nA vintage badge emblem with 'EST. 2026' curved along the top and 'YOUR GUILD' curved along the bottom, a central engraving-style icon, distressed letterpress texture, two-tone cream and dark navy, symmetrical centered composition. 1:1.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-design-album_cover",
+ "name": "Album Cover",
+ "category": "Design",
+ "text": "// Your description\n{cursor}\n\n// Design - Album Cover\nA square album cover, bold display title 'ALBUM NAME' centered in glowing chrome 3D type, dark gradient background with film grain, dramatic studio lighting and high contrast, color palette #0A0A0A, #00E5FF, #FF2E97. 1:1.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-design-book_cover",
+ "name": "Book Cover",
+ "category": "Design",
+ "text": "// Your description\n{cursor}\n\n// Design - Book Cover\nA book cover on a deep navy background (#0D1B2A), a bold centered two-line white title 'THE TITLE' then 'SECOND LINE' with the second line larger, a smaller grey subtitle beneath 'your subtitle', a central minimalist gold geometric illustration, elegant serif typography. Portrait 2:3.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-design-magazine_cover",
+ "name": "Magazine Cover",
+ "category": "Design",
+ "text": "// Your description\n{cursor}\n\n// Design - Magazine Cover\nA premium magazine cover, large bold masthead 'TITLE' across the top and a smaller tagline 'your tagline' beneath, a striking close-up editorial portrait, dramatic studio lighting with crisp shadows, refined sophisticated palette, clean grid with generous margins, glossy print finish. Portrait 3:4.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-design-packaging",
+ "name": "Packaging",
+ "category": "Design",
+ "text": "// Your description\n{cursor}\n\n// Design - Packaging\nA product package design, kraft-paper texture background, a centered bold vintage serif name 'PRODUCT NAME' with a small-caps subline 'your descriptor' below, a central hand-drawn engraving-style illustration, warm earthy palette, balanced label layout. 4:5.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-design-infographic",
+ "name": "Infographic",
+ "category": "Design",
+ "text": "// Your description\n{cursor}\n\n// Design - Infographic\nA clean modern infographic, title 'HOW IT WORKS' with three numbered step labels '1 Create', '2 Convert', '3 Export', simple flat icons, generous white space, a single accent color (color palette #00D1B2, #111111, #FFFFFF), tidy aligned grid. 4:5.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-design-sticker",
+ "name": "Sticker",
+ "category": "Design",
+ "text": "// Your description\n{cursor}\n\n// Design - Sticker\nA die-cut sticker with a thick clean white border, bold flat cartoon shapes and cheerful saturated colors, a single centered subject, simple chunky linework, an optional all-caps word 'YOUR WORD' in bold condensed type, soft even lighting, glossy vinyl finish. 1:1.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-design-signage",
+ "name": "Signage",
+ "category": "Design",
+ "text": "// Your description\n{cursor}\n\n// Design - Signage\nA modern signage design, the word 'WELCOME' large and centered with a smaller line 'your second line' beneath, high-contrast lettering legible at a glance, clean museum-style layout, simple solid background. 16:9.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-design-greeting_card",
+ "name": "Greeting Card",
+ "category": "Design",
+ "text": "// Your description\n{cursor}\n\n// Design - Greeting Card\nA charming greeting card, a friendly central illustration with a hand-lettered message 'YOUR MESSAGE' above it, soft warm palette, plenty of breathing room, gentle decorative accents, balanced symmetrical layout. Portrait 4:5.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-typography-bold_headline",
+ "name": "Bold Headline",
+ "category": "Typography",
+ "text": "// Your description\n{cursor}\n\n// Typography - Bold Headline\nLarge bold uppercase sans-serif headline reading 'YOUR TEXT', clean crisp letterforms with tight kerning, high-contrast color on a simple flat background, strong centered composition, sharp edges, modern editorial type treatment.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-typography-3d_inflated_type",
+ "name": "3D Inflated Type",
+ "category": "Typography",
+ "text": "// Your description\n{cursor}\n\n// Typography - 3D Inflated Type\nMassive puffy inflated 3D balloon letters spelling 'YOUR TEXT', a soft glossy plastic surface with smooth highlights and gentle shadows, playful rounded forms filling the frame, a bright cheerful palette, soft studio lighting.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-typography-hand_lettered_script",
+ "name": "Hand-Lettered Script",
+ "category": "Typography",
+ "text": "// Your description\n{cursor}\n\n// Typography - Hand-Lettered Script\nElegant hand-lettered script spelling 'YOUR TEXT', flowing brush-pen strokes with graceful flourishes and natural ink texture, a warm cream paper background, refined calligraphic composition, an artisanal feel.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-typography-neon_sign",
+ "name": "Neon Sign",
+ "category": "Typography",
+ "text": "// Your description\n{cursor}\n\n// Typography - Neon Sign\nA glowing neon sign spelling 'YOUR TEXT' in vibrant cursive tube lighting, a rich magenta-and-cyan glow reflecting on a dark brick wall, soft atmospheric haze, high-contrast night mood, realistic light bloom.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-typography-vintage_letterpress",
+ "name": "Vintage Letterpress",
+ "category": "Typography",
+ "text": "// Your description\n{cursor}\n\n// Typography - Vintage Letterpress\nDistressed vintage letterpress type spelling 'YOUR TEXT', a bold condensed serif with ink texture and a subtle paper deboss, a muted two-tone palette, a classic centered poster layout, a weathered handcrafted print feel.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-typography-chrome_display",
+ "name": "Chrome Display",
+ "category": "Typography",
+ "text": "// Your description\n{cursor}\n\n// Typography - Chrome Display\nSleek liquid-chrome 3D display type spelling 'YOUR TEXT', mirror-polished metal with sharp specular highlights and reflections, a dark gradient background, dramatic rim lighting, a high-gloss futuristic finish.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-illustration-flat_vector",
+ "name": "Flat Vector",
+ "category": "Illustration",
+ "text": "// Your description\n{cursor}\n\n// Illustration - Flat Vector\nDesigned as flat vector illustration with bold clean outlines, simple geometric shapes and large solid color fills, generous negative space, no gradients or texture, even flat lighting, a minimal modern editorial layout and a confident limited palette.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-illustration-anime_manga",
+ "name": "Anime / Manga",
+ "category": "Illustration",
+ "text": "// Your description\n{cursor}\n\n// Illustration - Anime / Manga\nIllustrated in clean cel-shaded anime style with confident crisp linework, large expressive eyes and flat vibrant color fills, soft cinematic rim lighting and gentle gradient shadows, Studio Ghibli-inspired warmth, a dynamic three-quarter composition.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-illustration-comic_graphic_novel",
+ "name": "Comic / Graphic Novel",
+ "category": "Illustration",
+ "text": "// Your description\n{cursor}\n\n// Illustration - Comic / Graphic Novel\nDrawn in bold comic-book style with heavy black ink outlines, dramatic cross-hatching and crisp cel shading, vivid flat colors with halftone dot texture, a high-contrast dynamic composition and strong directional lighting, graphic-novel ink-and-color energy.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-illustration-watercolor",
+ "name": "Watercolor",
+ "category": "Illustration",
+ "text": "// Your description\n{cursor}\n\n// Illustration - Watercolor\nPainted in ink and watercolor with fine crisp linework, soft translucent washes that bleed and pool, visible paper texture and granulating pigment, gentle natural light, loose painterly edges, a delicate romantic mood and an airy pastel palette.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-illustration-oil_painting",
+ "name": "Oil Painting",
+ "category": "Illustration",
+ "text": "// Your description\n{cursor}\n\n// Illustration - Oil Painting\nRendered as a textured oil painting with thick visible impasto brushstrokes and rich blended color, warm directional studio light raking across the canvas, deep saturated darks and luminous highlights, a painterly classical composition, an old-master gallery atmosphere.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-illustration-children_s_book",
+ "name": "Children's Book",
+ "category": "Illustration",
+ "text": "// Your description\n{cursor}\n\n// Illustration - Children's Book\nIllustrated in warm storybook style with soft hand-painted gouache textures, rounded friendly shapes and gentle cozy color, soft diffuse daylight, a charming whimsical composition with plenty of breathing room, a tender heartwarming picture-book mood.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-illustration-concept_art",
+ "name": "Concept Art",
+ "category": "Illustration",
+ "text": "// Your description\n{cursor}\n\n// Illustration - Concept Art\nRendered as painterly digital concept art with loose confident brushwork and soft atmospheric edges, tonal value blocking, dramatic cinematic lighting and deep aerial perspective, an epic widescreen establishing composition, a moody evocative atmosphere.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-illustration-fantasy_art",
+ "name": "Fantasy Art",
+ "category": "Illustration",
+ "text": "// Your description\n{cursor}\n\n// Illustration - Fantasy Art\nPainted as epic fantasy illustration with luminous golden-hour light and glowing magical accents, a rich jewel-toned palette and intricate ornate detail, a sweeping dramatic vista with atmospheric depth, a heroic awe-inspiring storybook mood.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-3d_render-3d_render",
+ "name": "3D Render",
+ "category": "3D & Render",
+ "text": "// Your description\n{cursor}\n\n// 3D & Render - 3D Render\nRendered as a polished 3D scene with smooth subsurface-scattered surfaces, soft global illumination and gentle ambient occlusion, glossy and matte material contrast, a dramatic studio key light on a clean gradient backdrop, crisp reflections, high-contrast stylized realism.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-3d_render-isometric",
+ "name": "Isometric",
+ "category": "3D & Render",
+ "text": "// Your description\n{cursor}\n\n// 3D & Render - Isometric\nComposed as a clean isometric illustration at a true 30-degree angle, a soft pastel palette and smooth vector shading with subtle clay-render volume, even diffuse lighting and gentle long shadows, a tidy miniature-diorama layout on a plain white background.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-3d_render-pixel_art",
+ "name": "Pixel Art",
+ "category": "3D & Render",
+ "text": "// Your description\n{cursor}\n\n// 3D & Render - Pixel Art\nRendered as crisp 16-bit pixel art on a tight grid, a limited indexed color palette with dithered shading, clean hard-edged sprites and chunky readable forms, flat retro-game lighting, a centered composition against a simple background, a nostalgic arcade mood.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-3d_render-clay_figurine",
+ "name": "Clay Figurine",
+ "category": "3D & Render",
+ "text": "// Your description\n{cursor}\n\n// 3D & Render - Clay Figurine\nRendered as a cute claymation figurine sculpted from smooth matte polymer clay, soft rounded handmade forms with gentle fingerprint texture, soft studio lighting on a simple backdrop, a miniature diorama feel, a charming stop-motion look.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-aesthetic-cyberpunk",
+ "name": "Cyberpunk",
+ "category": "Aesthetic",
+ "text": "// Your description\n{cursor}\n\n// Aesthetic - Cyberpunk\nRendered in neon-soaked cyberpunk style with glowing magenta-and-cyan signage reflecting on wet surfaces, dense atmospheric haze and volumetric light, high-contrast darkness pierced by holographic glow, a gritty futuristic city composition, an electric dystopian mood.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-aesthetic-minimalist",
+ "name": "Minimalist",
+ "category": "Aesthetic",
+ "text": "// Your description\n{cursor}\n\n// Aesthetic - Minimalist\nComposed as a clean minimalist design with a single subject and abundant negative space, a soft gradient or solid background, a restrained two-or-three-color palette, flat even lighting, precise balanced placement, an elegant calm uncluttered mood.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-aesthetic-synthwave_retro",
+ "name": "Synthwave / Retro",
+ "category": "Aesthetic",
+ "text": "// Your description\n{cursor}\n\n// Aesthetic - Synthwave / Retro\nRendered in 1980s synthwave style with a glowing neon grid horizon and a setting-sun gradient, chrome accents and purple-and-pink haze, bold retro-futuristic composition, scanline texture, a nostalgic outrun mood. Color palette #1B1035, #FF2E97, #00E5FF.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ },
+ {
+ "id": "ideogram-aesthetic-risograph_screenprint",
+ "name": "Risograph / Screenprint",
+ "category": "Aesthetic",
+ "text": "// Your description\n{cursor}\n\n// Aesthetic - Risograph / Screenprint\nRendered as a risograph screen-print with a limited 2-3 ink palette, grainy halftone texture and slight misregistration, bold flat shapes with overprint blends, matte paper feel, a handmade indie-poster aesthetic.",
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/data/templates/ideogram-t2i.json b/data/templates/ideogram-t2i.json
new file mode 100644
index 0000000..7e8b1dd
--- /dev/null
+++ b/data/templates/ideogram-t2i.json
@@ -0,0 +1,139 @@
+{
+ "name": "Text-to-Image",
+ "category": "Generation",
+ "order": 0,
+ "scope": {
+ "type": "architecture",
+ "architecture": "ideogram"
+ },
+ "created_at": 1781740800,
+ "id": "ideogram-t2i",
+ "prompt_id": "ideogram-default",
+ "nodes": [
+ {
+ "type": "CLIPTextEncode",
+ "offset": [900, 0],
+ "size": [225, 116],
+ "widgets_values": [""],
+ "properties": { "Node name for S&R": "CLIPTextEncode" }
+ },
+ {
+ "type": "ConditioningZeroOut",
+ "offset": [1180, 0],
+ "size": [240, 48],
+ "widgets_values": [],
+ "properties": { "Node name for S&R": "ConditioningZeroOut" }
+ },
+ {
+ "type": "CFGOverride",
+ "offset": [1180, 120],
+ "size": [280, 106],
+ "widgets_values": [3.0, 0.7, 1.0],
+ "properties": { "Node name for S&R": "CFGOverride" }
+ },
+ {
+ "type": "DualModelGuider",
+ "offset": [1500, 120],
+ "size": [266, 150],
+ "widgets_values": [7.0],
+ "properties": { "Node name for S&R": "DualModelGuider" }
+ },
+ {
+ "type": "PromptChain_IdeogramSampler",
+ "offset": [1840, 360],
+ "size": [310, 220],
+ "widgets_values": [4, 0.08],
+ "properties": { "Node name for S&R": "PromptChain_IdeogramSampler" }
+ },
+ {
+ "type": "SaveImage",
+ "offset": [2440, 0],
+ "size": [400, 460],
+ "widgets_values": ["Ideogram4/%date:yyyy-MM-dd%"],
+ "properties": {}
+ },
+ {
+ "type": "UNETLoader",
+ "offset": [820, 200],
+ "size": [320, 82],
+ "widgets_values": ["ideogram4_fp8_scaled.safetensors", "default"],
+ "properties": { "Node name for S&R": "UNETLoader" }
+ },
+ {
+ "type": "UNETLoader",
+ "offset": [820, 320],
+ "size": [320, 82],
+ "widgets_values": ["ideogram4_unconditional_fp8_scaled.safetensors", "default"],
+ "properties": { "Node name for S&R": "UNETLoader" }
+ },
+ {
+ "type": "CLIPLoader",
+ "offset": [560, 0],
+ "size": [320, 110],
+ "widgets_values": ["qwen3vl_8b_fp8_scaled.safetensors", "ideogram4", "default"],
+ "properties": { "Node name for S&R": "CLIPLoader" }
+ },
+ {
+ "type": "VAELoader",
+ "offset": [1500, 300],
+ "size": [246, 58],
+ "widgets_values": ["flux2-vae.safetensors"],
+ "properties": { "Node name for S&R": "VAELoader" }
+ },
+ {
+ "type": "RandomNoise",
+ "offset": [1500, 400],
+ "size": [260, 82],
+ "widgets_values": [0, "randomize"],
+ "properties": { "Node name for S&R": "RandomNoise" }
+ },
+ {
+ "type": "KSamplerSelect",
+ "offset": [1500, 520],
+ "size": [240, 58],
+ "widgets_values": ["euler"],
+ "properties": { "Node name for S&R": "KSamplerSelect" }
+ },
+ {
+ "type": "Ideogram4Scheduler",
+ "offset": [1500, 620],
+ "size": [300, 154],
+ "widgets_values": [20, 1024, 1024, 0.5, 1.75],
+ "properties": { "Node name for S&R": "Ideogram4Scheduler" }
+ },
+ {
+ "type": "EmptyFlux2LatentImage",
+ "offset": [1500, 800],
+ "size": [290, 106],
+ "widgets_values": [1024, 1024, 1],
+ "properties": { "Node name for S&R": "EmptyFlux2LatentImage" }
+ },
+ {
+ "type": "PromptChain_IdeogramCaption",
+ "offset": [600, 180],
+ "size": [250, 82],
+ "widgets_values": ["envelope"],
+ "properties": { "Node name for S&R": "PromptChain_IdeogramCaption" }
+ }
+ ],
+ "connections": [
+ { "from_node_idx": 8, "from_slot": 0, "to_node_idx": 0, "to_slot": 0 },
+ { "from_node_idx": 14, "from_slot": 0, "to_node_idx": 0, "to_slot": 1 },
+ { "from_node_idx": 0, "from_slot": 0, "to_node_idx": 3, "to_slot": 1 },
+ { "from_node_idx": 0, "from_slot": 0, "to_node_idx": 1, "to_slot": 0 },
+ { "from_node_idx": 1, "from_slot": 0, "to_node_idx": 3, "to_slot": 3 },
+ { "from_node_idx": 6, "from_slot": 0, "to_node_idx": 2, "to_slot": 0 },
+ { "from_node_idx": 2, "from_slot": 0, "to_node_idx": 3, "to_slot": 0 },
+ { "from_node_idx": 7, "from_slot": 0, "to_node_idx": 3, "to_slot": 2 },
+ { "from_node_idx": 3, "from_slot": 0, "to_node_idx": 4, "to_slot": 1 },
+ { "from_node_idx": 10, "from_slot": 0, "to_node_idx": 4, "to_slot": 0 },
+ { "from_node_idx": 11, "from_slot": 0, "to_node_idx": 4, "to_slot": 2 },
+ { "from_node_idx": 12, "from_slot": 0, "to_node_idx": 4, "to_slot": 3 },
+ { "from_node_idx": 13, "from_slot": 0, "to_node_idx": 4, "to_slot": 4 },
+ { "from_node_idx": 9, "from_slot": 0, "to_node_idx": 4, "to_slot": 5 },
+ { "from_node_idx": 4, "from_slot": 0, "to_node_idx": 5, "to_slot": 0 }
+ ],
+ "anchorConnections": [
+ { "from_slot": 1, "to_node_idx": 14, "to_slot": 0 }
+ ]
+}
diff --git a/js/lib/editor-context-menu.js b/js/lib/editor-context-menu.js
index 3816236..47ee0ef 100644
--- a/js/lib/editor-context-menu.js
+++ b/js/lib/editor-context-menu.js
@@ -37,7 +37,7 @@ function closeMenu() {
// ── graph traversal ──────────────────────────────────────────────
-const KSAMPLER_TYPES = new Set(["KSampler", "KSamplerAdvanced", "PromptChain_RegionalDetailer", "SamplerCustomAdvanced", "UltimateSDUpscale"]);
+const KSAMPLER_TYPES = new Set(["KSampler", "KSamplerAdvanced", "PromptChain_RegionalDetailer", "SamplerCustomAdvanced", "UltimateSDUpscale", "PromptChain_IdeogramSampler"]);
const LOAD_IMAGE_TYPES = new Set(["LoadImage", "LoadImageOutput"]);
function _findDownstream(startNode, targetTypes, graph) {
const visited = new Set();
diff --git a/js/lib/fullscreen-bridge.js b/js/lib/fullscreen-bridge.js
index e9ea2ea..0bda687 100644
--- a/js/lib/fullscreen-bridge.js
+++ b/js/lib/fullscreen-bridge.js
@@ -55,6 +55,7 @@ let focusedGroup = null;
// PromptChain feeding them. Mirrored from main.js — the canonical set.
const KSAMPLER_TYPES = new Set([
"KSampler", "KSamplerAdvanced", "PromptChain_RegionalDetailer", "SamplerCustomAdvanced", "UltimateSDUpscale",
+ "PromptChain_IdeogramSampler",
]);
// Workflow-level fullscreen state stash. The preview panels are
diff --git a/js/lib/model-indicator-bridge.js b/js/lib/model-indicator-bridge.js
index c0fd1c3..7c4a358 100644
--- a/js/lib/model-indicator-bridge.js
+++ b/js/lib/model-indicator-bridge.js
@@ -39,6 +39,7 @@ api.addEventListener("promptchain_recognition_done", () => {
const KSAMPLER_TYPES = new Set([
"KSampler", "KSamplerAdvanced",
"PromptChain_RegionalDetailer", "SamplerCustomAdvanced", "UltimateSDUpscale",
+ "PromptChain_IdeogramSampler",
]);
const MODEL_WIDGET_NAMES = ["ckpt_name", "unet_name", "model_name"];
diff --git a/js/lib/model-settings-bridge.js b/js/lib/model-settings-bridge.js
index 29c6beb..7dda018 100644
--- a/js/lib/model-settings-bridge.js
+++ b/js/lib/model-settings-bridge.js
@@ -19,6 +19,7 @@ export const ARCHITECTURES = [
{ id: "wan22", label: "Wan 2.2" }, { id: "ltx", label: "LTX Video" },
{ id: "hunyuan_video", label: "HunyuanVideo" }, { id: "hidream", label: "HiDream" },
{ id: "ernie", label: "ERNIE Image" },
+ { id: "ideogram", label: "Ideogram" },
];
export const FAMILIES = {
@@ -35,6 +36,7 @@ export const FAMILIES = {
hunyuan_video: [{ id: "hunyuan_video_15", label: "HunyuanVideo 1.5" }],
hidream: [{ id: "hidream", label: "HiDream-I1" }],
ernie: [{ id: "ernie_base", label: "ERNIE Image" }, { id: "ernie_turbo", label: "ERNIE Image Turbo" }],
+ ideogram: [{ id: "ideogram4", label: "Ideogram 4" }],
};
// ── supported node types + widgets ─────────────────────────────────
diff --git a/js/lib/svelte/assets/promptchain-svelte.css b/js/lib/svelte/assets/promptchain-svelte.css
index 6465b71..cb25f1b 100644
--- a/js/lib/svelte/assets/promptchain-svelte.css
+++ b/js/lib/svelte/assets/promptchain-svelte.css
@@ -1411,11 +1411,82 @@
.pcr-viewer-lineage-node.current.svelte-5cciiw {
border-color: #4fc3f7;
background: rgba(79, 195, 247, 0.15);
+ box-shadow: 0 0 0 1px rgba(79, 195, 247, 0.5), 0 0 10px 1px rgba(79, 195, 247, 0.45);
}
.pcr-viewer-lineage-node.svelte-5cciiw:hover:not(.current) {
background: rgba(255, 255, 255, 0.1);
border-color: #555;
}
+ /* multi-select for batch delete */
+ .pcr-viewer-lineage-node.selected.svelte-5cciiw {
+ border-color: #ffb84d;
+ box-shadow: 0 0 0 1px rgba(255, 184, 77, 0.6);
+ }
+ /* centered over the thumb so it never collides with the corner badges (dot,
+ layers, fork pip / expander, opbadge) — the .selected ring already frames it */
+ .pcr-lin-selcheck.svelte-5cciiw {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 22px;
+ height: 22px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 13px;
+ font-weight: 700;
+ line-height: 1;
+ border-radius: 50%;
+ background: rgba(255, 184, 77, 0.92);
+ color: #1a1208;
+ pointer-events: none;
+ z-index: 3;
+ }
+ .pcr-lin-focuscrumb.svelte-5cciiw {
+ margin: 0 6px 4px;
+ padding: 3px 6px;
+ font-size: 10px;
+ border-radius: 4px;
+ border: 1px solid rgba(120, 200, 255, 0.4);
+ background: rgba(40, 70, 95, 0.5);
+ color: #bfe3ff;
+ cursor: pointer;
+ white-space: nowrap;
+ }
+ .pcr-lin-focuscrumb.svelte-5cciiw:hover { background: rgba(50, 90, 120, 0.7); }
+ .pcr-lin-focusx.svelte-5cciiw { opacity: 0.7; margin-left: 2px; }
+ .pcr-lin-selbar.svelte-5cciiw {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 5px;
+ margin: 0 6px 4px;
+ padding: 3px 6px;
+ font-size: 10px;
+ border-radius: 4px;
+ background: rgba(95, 65, 20, 0.5);
+ color: #ffd9a3;
+ }
+ .pcr-lin-seldel.svelte-5cciiw {
+ padding: 2px 6px;
+ border-radius: 3px;
+ border: none;
+ background: #e0863a;
+ color: #1a1208;
+ font-weight: 700;
+ cursor: pointer;
+ }
+ .pcr-lin-seldel.svelte-5cciiw:hover { background: #f09a4d; }
+ .pcr-lin-selclear.svelte-5cciiw {
+ padding: 2px 5px;
+ border-radius: 3px;
+ border: none;
+ background: transparent;
+ color: #ffd9a3;
+ cursor: pointer;
+ }
+ .pcr-lin-selclear.svelte-5cciiw:hover { color: #fff; }
.pcr-viewer-lineage-node.svelte-5cciiw img:where(.svelte-5cciiw) {
width: 100%;
height: 100%;
@@ -1521,29 +1592,36 @@
/* ── subway lanes: the rail grows horizontally into parallel vertical lanes ── */
.pcr-viewer-lineage-visible.lanes.svelte-5cciiw {
width: auto;
- max-width: 62vw;
+ max-width: 45vw;
}
- .pcr-lin-lanes.svelte-5cciiw {
- position: relative;
+ /* scroll viewport — fills the rail; the sized tree inside scrolls both axes */
+ .pcr-lin-treescroll.svelte-5cciiw {
flex: 1;
- display: flex;
- flex-direction: row;
- align-items: flex-start;
- gap: 22px;
- padding: 8px 12px;
overflow: auto;
+ padding: 8px 12px;
+ position: relative; /* offset origin for centering the current node (contentXY) */
}
- .pcr-lin-lanes.svelte-5cciiw::-webkit-scrollbar { width: 7px; height: 7px; }
- .pcr-lin-lanes.svelte-5cciiw::-webkit-scrollbar-track { background: transparent; }
- .pcr-lin-lanes.svelte-5cciiw::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
+ .pcr-lin-treescroll.svelte-5cciiw::-webkit-scrollbar { width: 7px; height: 7px; }
+ .pcr-lin-treescroll.svelte-5cciiw::-webkit-scrollbar-track { background: transparent; }
+ .pcr-lin-treescroll.svelte-5cciiw::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
/* both axes scroll here, so kill the default white corner where they meet */
- .pcr-lin-lanes.svelte-5cciiw::-webkit-scrollbar-corner { background: transparent; }
- .pcr-lin-col.svelte-5cciiw {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 22px;
- flex: 0 0 auto;
+ .pcr-lin-treescroll.svelte-5cciiw::-webkit-scrollbar-corner { background: transparent; }
+ /* sized to the tree's extent; nodes are absolutely positioned within it and the
+ SVG connector layer measures geometry relative to this element */
+ .pcr-lin-tree.svelte-5cciiw {
+ position: relative;
+ }
+ .pcr-lin-treenode.svelte-5cciiw {
+ position: absolute;
+ transition: opacity 0.12s ease;
+ }
+ /* fade everything off the displayed image's branch so the active lineage reads
+ at a glance; hovering a faded node brings it back to inspect it */
+ .pcr-lin-treenode.dimmed.svelte-5cciiw {
+ opacity: 0.7;
+ }
+ .pcr-lin-treenode.dimmed.svelte-5cciiw:hover {
+ opacity: 1;
}
/* SVG overlay lives inside the scroll content, so it translates with scroll for
free — geometry is only recomputed on expand/fold/resize/img-load. */
@@ -1559,6 +1637,66 @@
stroke-width: 2;
fill: none;
}
+ /* the displayed image's lineage path — drawn last so it sits over dim edges */
+ .pcr-lin-connectors.svelte-5cciiw path.active:where(.svelte-5cciiw) {
+ stroke: #3b9eff;
+ stroke-width: 3;
+ }
+ /* overflow bundle sitting in a tree row (oldest siblings folded away) */
+ .pcr-lin-treebundle.svelte-5cciiw {
+ width: 64px;
+ height: 44px;
+ }
+ /* faint blue tie between active-branch thumbs and the bright path connecting them */
+ .pcr-lin-treenode.svelte-5cciiw:not(.dimmed) .pcr-viewer-lineage-node:where(.svelte-5cciiw):not(.current) {
+ border-color: rgba(59, 158, 255, 0.45);
+ }
+ /* upscale provenance badge — bottom-left, clears the layers badge (top-left) and dot (bottom-right) */
+ .pcr-lin-opbadge.svelte-5cciiw {
+ position: absolute;
+ bottom: 3px;
+ left: 3px;
+ padding: 0 3px;
+ height: 14px;
+ display: flex;
+ align-items: center;
+ font-size: 9px;
+ font-weight: 700;
+ line-height: 1;
+ border-radius: 3px;
+ background: rgba(20, 20, 20, 0.82);
+ color: #8fd0ff;
+ box-shadow: 0 0 0 1px rgba(59, 158, 255, 0.5);
+ pointer-events: none;
+ }
+ /* at-rest "this node hides N branches" marker; swapped for the ⊞ button on hover */
+ .pcr-lin-forkpip.svelte-5cciiw {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ min-width: 16px;
+ height: 16px;
+ padding: 0 3px;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ font-weight: 700;
+ line-height: 1;
+ border-radius: 4px;
+ background: rgba(20, 20, 20, 0.7);
+ color: #c0a3ff;
+ box-shadow: 0 0 0 1px rgba(150, 120, 255, 0.5);
+ pointer-events: none;
+ }
+ .pcr-viewer-lineage-node.svelte-5cciiw:hover .pcr-lin-forkpip:where(.svelte-5cciiw),
+ .pcr-viewer-lineage-node.svelte-5cciiw:hover .pcr-lin-opbadge:where(.svelte-5cciiw) {
+ display: none;
+ }
+ .pcr-viewer-tip-op.svelte-5cciiw {
+ color: #8fd0ff;
+ }
.pcr-lin-expander.svelte-5cciiw {
position: absolute;
top: 2px;
diff --git a/js/lib/svelte/promptchain-viewer.js b/js/lib/svelte/promptchain-viewer.js
index 8ddc34e..065112e 100644
--- a/js/lib/svelte/promptchain-viewer.js
+++ b/js/lib/svelte/promptchain-viewer.js
@@ -360,7 +360,7 @@ function SavePathInput($$anchor, $$props) {
delegate(["click", "input", "keydown", "mousedown"]);
var root_3$7 = from_html(`
`);
var root_6$6 = from_html(`
`);
-var root_8$5 = from_html(``);
+var root_8$6 = from_html(``);
var root_7$6 = from_html(`
`);
var root_5$6 = from_html(` `, 1);
var root_2$7 = from_html(` `, 1);
@@ -516,7 +516,7 @@ function SearchableSelect($$anchor, $$props) {
var node_5 = sibling(span_2, 2);
{
var consequent_2 = ($$anchor7) => {
- var span_3 = root_8$5();
+ var span_3 = root_8$6();
span_3.textContent = "✓";
append($$anchor7, span_3);
};
@@ -577,15 +577,15 @@ var root_4$6 = from_html(` `);
var root_10$4 = from_html(`
`);
var root_12$4 = from_html(`
`);
-var root_11$2 = from_html(`
`, 1);
-var root_8$4 = from_html(`
`);
-var root_13$3 = from_html(`↩ Restore last setup `);
+var root_11$3 = from_html(`
`, 1);
+var root_8$5 = from_html(`
`);
+var root_13$4 = from_html(`↩ Restore last setup `);
var root_14$4 = from_html(`This image has no usable prompt metadata — a plain model upscale (ESRGAN) will be used.
`);
var root_16$4 = from_html(` `);
var root_17$4 = from_html(`re-renders the whole frame at ~2MP from your instruction — composition can shift slightly; the climb model below pushes to the target size `);
-var root_18$4 = from_html(`deterministic enlargement with the climb model below — add a Restore step for degraded sources (webcam, jpeg, blur) `);
+var root_18$3 = from_html(`deterministic enlargement with the climb model below — add a Restore step for degraded sources (webcam, jpeg, blur) `);
var root_15$4 = from_html(``);
-var root_20$4 = from_html(` `);
+var root_20$3 = from_html(` `);
var root_21$3 = from_html(``);
var root_19$4 = from_html(``);
var root_23$2 = from_html(` `);
@@ -614,10 +614,10 @@ var root_48$2 = from_html(`
`);
var root_41$3 = from_html(`▶ Advanced
`, 1);
var root_49$2 = from_html(`Save to
`);
-var root_51$1 = from_html(`Cancel `);
+var root_51$2 = from_html(`Cancel `);
var root_53$2 = from_html(`Add to Edit `);
var root_54$2 = from_html(`View result `);
-var root_52$1 = from_html(`Close `, 1);
+var root_52$2 = from_html(`Close `, 1);
var root_56$2 = from_html(`Create Workflow `);
var root_55$2 = from_html(`Cancel Apply `, 1);
var root_1$6 = from_html(``);
@@ -1482,7 +1482,7 @@ function UpscaleOptionsModal($$anchor, $$props) {
var node_7 = sibling(div_5, 2);
{
var consequent_9 = ($$anchor3) => {
- var div_11 = root_8$4();
+ var div_11 = root_8$5();
var node_8 = child(div_11);
{
var consequent_6 = ($$anchor4) => {
@@ -1498,7 +1498,7 @@ function UpscaleOptionsModal($$anchor, $$props) {
append($$anchor4, div_13);
};
var alternate = ($$anchor4) => {
- var fragment_3 = root_11$2();
+ var fragment_3 = root_11$3();
var div_14 = first_child(fragment_3);
var text_5 = child(div_14);
var div_15 = sibling(div_14, 2);
@@ -1540,7 +1540,7 @@ function UpscaleOptionsModal($$anchor, $$props) {
var node_10 = child(div_18);
{
var consequent_10 = ($$anchor3) => {
- var button_3 = root_13$3();
+ var button_3 = root_13$4();
delegated("click", button_3, applySavedSetup);
append($$anchor3, button_3);
};
@@ -1592,7 +1592,7 @@ function UpscaleOptionsModal($$anchor, $$props) {
append($$anchor4, span_3);
};
var consequent_14 = ($$anchor4) => {
- var span_4 = root_18$4();
+ var span_4 = root_18$3();
append($$anchor4, span_4);
};
if_block(node_14, ($$render) => {
@@ -1613,7 +1613,7 @@ function UpscaleOptionsModal($$anchor, $$props) {
var div_21 = root_19$4();
var select = sibling(child(div_21), 2);
each(select, 21, () => MODE_ROWS, index, ($$anchor4, row) => {
- var option = root_20$4();
+ var option = root_20$3();
var text_8 = child(option);
var option_value = {};
template_effect(() => {
@@ -2243,7 +2243,7 @@ function UpscaleOptionsModal($$anchor, $$props) {
var node_45 = first_child(fragment_8);
{
var consequent_38 = ($$anchor4) => {
- var button_4 = root_51$1();
+ var button_4 = root_51$2();
delegated("click", button_4, function(...$$args) {
var _a;
(_a = onCancelRun()) == null ? void 0 : _a.apply(this, $$args);
@@ -2251,7 +2251,7 @@ function UpscaleOptionsModal($$anchor, $$props) {
append($$anchor4, button_4);
};
var alternate_2 = ($$anchor4) => {
- var fragment_9 = root_52$1();
+ var fragment_9 = root_52$2();
var button_5 = first_child(fragment_9);
var button_6 = sibling(button_5, 2);
var text_28 = child(button_6);
@@ -2359,12 +2359,12 @@ var root_4$5 = from_html(`
`);
var root_7$4 = from_html(``);
var root_9$3 = from_html(` Before
After
`, 1);
-var root_8$3 = from_html(` `, 1);
+var root_8$4 = from_html(` `, 1);
var root_10$3 = from_html(``);
var root_12$3 = from_html(` `);
-var root_11$1 = from_html(``);
+var root_11$2 = from_html(``);
var root_6$4 = from_html(` `, 1);
-var root_13$2 = from_html(`↩ Restore last setup `);
+var root_13$3 = from_html(`↩ Restore last setup `);
var root_15$3 = from_html(`renders the painted region with this checkpoint — no render metadata needed, and PuLID / Style Reference work on any source
`);
var root_16$3 = from_html(`re-renders the painted region with this FLUX.1 model at the denoise below — describe the content, strong on faces and eyes
`);
var root_17$3 = from_html(`follows your instruction inside the mask only — unmasked pixels stay untouched; the region renders at full model resolution
`);
@@ -2375,8 +2375,8 @@ var root_22$2 = from_html(` Weight type Start
End
`, 1);
var root_26$2 = from_html(`Fidelity
Start
End
`, 1);
-var root_20$3 = from_html(`Choose image
Strength
`, 1);
-var root_18$3 = from_html(`Condition
Inpaint Inpaint + PuLID Inpaint + Style Reference `);
+var root_20$2 = from_html(`Choose image
Strength
`, 1);
+var root_18$2 = from_html(`Condition
Inpaint Inpaint + PuLID Inpaint + Style Reference `);
var root_28$1 = from_html(` `);
var root_27$2 = from_html(``);
var root_30$2 = from_html(` `);
@@ -3427,7 +3427,7 @@ function InpaintModal($$anchor, $$props) {
append($$anchor4, div_15);
};
var consequent_5 = ($$anchor4) => {
- var fragment_3 = root_8$3();
+ var fragment_3 = root_8$4();
var div_16 = first_child(fragment_3);
var img_3 = child(div_16);
var node_7 = sibling(div_16, 2);
@@ -3481,7 +3481,7 @@ function InpaintModal($$anchor, $$props) {
var node_8 = sibling(node_6, 2);
{
var consequent_7 = ($$anchor4) => {
- var div_22 = root_11$1();
+ var div_22 = root_11$2();
var div_23 = child(div_22);
var div_24 = child(div_23);
let classes_7;
@@ -3518,7 +3518,7 @@ function InpaintModal($$anchor, $$props) {
var node_10 = child(div_25);
{
var consequent_9 = ($$anchor3) => {
- var button_7 = root_13$2();
+ var button_7 = root_13$3();
delegated("click", button_7, applySavedSetup);
append($$anchor3, button_7);
};
@@ -3578,7 +3578,7 @@ function InpaintModal($$anchor, $$props) {
var node_14 = sibling(node_11, 2);
{
var consequent_19 = ($$anchor3) => {
- var div_30 = root_18$3();
+ var div_30 = root_18$2();
var select = sibling(child(div_30), 2);
var option = child(select);
option.value = option.__value = "none";
@@ -3595,7 +3595,7 @@ function InpaintModal($$anchor, $$props) {
append($$anchor4, div_31);
};
var consequent_18 = ($$anchor4) => {
- var fragment_5 = root_20$3();
+ var fragment_5 = root_20$2();
var div_32 = first_child(fragment_5);
var button_8 = child(div_32);
var node_16 = sibling(button_8, 2);
@@ -4992,20 +4992,20 @@ var root_4$3 = from_svg(`
`);
var root_6$3 = from_html(`Transform ⇆ Flip H ⇅ Flip V Reset ✓ Apply drag a corner to scale · the knob above rotates (Shift = 15° steps) · drag inside to move · Shift on a corner keeps proportions · Enter applies · Esc cancels `, 1);
var root_7$3 = from_html(`Tolerance
Contiguous click to flood-fill with the foreground color · constrained to the selection · Alt/Ctrl+Backspace fills `, 1);
-var root_8$2 = from_html(`Blur
Feather
lasso a region — it blurs and feather-blends on release `, 1);
+var root_8$3 = from_html(`Blur
Feather
lasso a region — it blurs and feather-blends on release `, 1);
var root_9$2 = from_html(`Size
Hardness
Opacity
Flow
`, 1);
-var root_11 = from_html(`Opacity
Flow
`, 1);
+var root_11$1 = from_html(`Opacity
Flow
`, 1);
var root_12$2 = from_html(`paint over a blemish — it heals from the surroundings on release `);
-var root_13$1 = from_html(` `);
+var root_13$2 = from_html(` `);
var root_14$2 = from_html(` `);
var root_10$2 = from_html(`Size
Hardness
`, 1);
var root_16$2 = from_html(` `);
var root_17$2 = from_html(`Rate
`);
var root_15$2 = from_html(`
Size
Density
Pressure
`, 1);
var root_19$2 = from_html(` `);
-var root_18$2 = from_html(`Ratio ⇄
✓ Apply Reset drag handles · drag inside to move · Shift locks ratio · Enter / double-click applies · Esc resets `, 1);
+var root_18$1 = from_html(`Ratio ⇄
✓ Apply Reset drag handles · drag inside to move · Shift locks ratio · Enter / double-click applies · Esc resets `, 1);
var root_21$1 = from_html(` `);
-var root_20$2 = from_html(`Feather
Opacity
Ctrl+J copy / Ctrl+Shift+J cut to a new layer · Ctrl+C/V · Esc deselects · Feather softens the edge `, 1);
+var root_20$1 = from_html(`Feather
Opacity
Ctrl+J copy / Ctrl+Shift+J cut to a new layer · Ctrl+C/V · Esc deselects · Feather softens the edge `, 1);
var root_23$1 = from_html(` `);
var root_22$1 = from_html(` `, 1);
var root_24$1 = from_html(`Sample Point 3×3 average 5×5 average
click the image to pick a color · hold to preview · Screen pick grabs from anywhere `, 1);
@@ -5033,9 +5033,9 @@ var root_46$1 = from_html(`drag /
var root_47$1 = from_html(`
`);
var root_49$1 = from_html(`
✦ Inpaint `);
var root_50$1 = from_html(`
✦ Upscale `);
-var root_51 = from_html(`
✦ Re-pose
`);
+var root_51$1 = from_html(`
✦ Re-pose
`);
var root_48$1 = from_html(`
`);
-var root_52 = from_html(`
+ ⬇ ▣ 🗑 `);
+var root_52$1 = from_html(`
+ ⬇ ▣ 🗑 `);
var root_54$1 = from_html(`
`);
var root_56$1 = from_html(`
`);
var root_58$1 = from_html(`
▦ `);
@@ -5043,8 +5043,8 @@ var root_59$1 = from_html(`
<
var root_57$1 = from_html(` `);
var root_55$1 = from_html(`⠿
`);
var root_53$1 = from_html(`
`, 1);
-var root_61$1 = from_html(`
`);
-var root_60$1 = from_html(`
`);
+var root_61 = from_html(`
`);
+var root_60 = from_html(`
`);
var root_63$1 = from_html(`Merge Layers `);
var root_64$1 = from_html(`Merge Down `);
var root_66$1 = from_html(` `);
@@ -5059,8 +5059,8 @@ var root_73$1 = from_html(`
`);
var root_74$1 = from_html(`
`);
var root_75$1 = from_html(`⏳ Selecting subject…
`);
var root_76$1 = from_html(` Alt-click a clean area of the image to define a source point, then paint.
OK
`);
-var root_77 = from_html(``);
-var root_78 = from_html(`✓ Edits saved `);
+var root_77$1 = from_html(``);
+var root_78$1 = from_html(`✓ Edits saved `);
var root_1$3 = from_html(``);
function EditModal($$anchor, $$props) {
push($$props, true);
@@ -9725,7 +9725,7 @@ function EditModal($$anchor, $$props) {
append($$anchor3, fragment_2);
};
var consequent_5 = ($$anchor3) => {
- var fragment_3 = root_8$2();
+ var fragment_3 = root_8$3();
var div_9 = first_child(fragment_3);
var node_6 = sibling(child(div_9), 2);
SettingsSlider(node_6, {
@@ -9855,7 +9855,7 @@ function EditModal($$anchor, $$props) {
var node_14 = sibling(div_16, 2);
{
var consequent_7 = ($$anchor4) => {
- var fragment_6 = root_11();
+ var fragment_6 = root_11$1();
var div_17 = first_child(fragment_6);
var node_15 = sibling(child(div_17), 2);
SettingsSlider(node_15, {
@@ -9897,7 +9897,7 @@ function EditModal($$anchor, $$props) {
append($$anchor4, span_5);
};
var consequent_9 = ($$anchor4) => {
- var span_6 = root_13$1();
+ var span_6 = root_13$2();
var text_4 = child(span_6);
template_effect(() => set_text(text_4, get(healSource) ? "paint over the flaw — texture from the source, lighting from the spot · Alt-click to re-sample" : "Alt-click a clean area to set the sample source"));
append($$anchor4, span_6);
@@ -10019,7 +10019,7 @@ function EditModal($$anchor, $$props) {
append($$anchor3, fragment_7);
};
var consequent_13 = ($$anchor3) => {
- var fragment_8 = root_18$2();
+ var fragment_8 = root_18$1();
var div_24 = first_child(fragment_8);
var select = sibling(child(div_24), 2);
each(select, 21, () => CROP_RATIOS, index, ($$anchor4, cr) => {
@@ -10049,7 +10049,7 @@ function EditModal($$anchor, $$props) {
append($$anchor3, fragment_8);
};
var consequent_14 = ($$anchor3) => {
- var fragment_9 = root_20$2();
+ var fragment_9 = root_20$1();
var div_25 = first_child(fragment_9);
var node_23 = sibling(child(div_25), 2);
SettingsSlider(node_23, {
@@ -10599,7 +10599,7 @@ function EditModal($$anchor, $$props) {
var node_53 = sibling(div_67, 2);
{
var consequent_37 = ($$anchor4) => {
- var div_68 = root_51();
+ var div_68 = root_51$1();
var button_29 = child(div_68);
template_effect(() => button_29.disabled = !!get(handoffBusy));
delegated("click", button_29, openReposeHandoff);
@@ -10626,7 +10626,7 @@ function EditModal($$anchor, $$props) {
var node_54 = sibling(span_15, 2);
{
var consequent_39 = ($$anchor3) => {
- var span_16 = root_52();
+ var span_16 = root_52$1();
var button_32 = child(span_16);
var button_33 = sibling(button_32, 2);
var button_34 = sibling(button_33, 2);
@@ -10810,9 +10810,9 @@ function EditModal($$anchor, $$props) {
append($$anchor3, fragment_16);
};
var alternate_4 = ($$anchor3) => {
- var div_73 = root_60$1();
+ var div_73 = root_60();
each(div_73, 21, () => get(history2), index, ($$anchor4, h, i) => {
- var div_74 = root_61$1();
+ var div_74 = root_61();
let classes_13;
var text_23 = child(div_74);
template_effect(() => {
@@ -11101,7 +11101,7 @@ function EditModal($$anchor, $$props) {
var node_72 = sibling(node_71, 2);
{
var consequent_58 = ($$anchor3) => {
- var div_84 = root_77();
+ var div_84 = root_77$1();
var div_85 = child(div_84);
var div_86 = child(div_85);
var text_31 = child(div_86);
@@ -11137,7 +11137,7 @@ function EditModal($$anchor, $$props) {
var node_74 = sibling(div_89, 2);
{
var consequent_59 = ($$anchor3) => {
- var span_22 = root_78();
+ var span_22 = root_78$1();
append($$anchor3, span_22);
};
if_block(node_74, ($$render) => {
@@ -11290,18 +11290,18 @@ var root_4$2 = from_html(` Before
After
`, 1);
var root_5$2 = from_html(` `, 1);
var root_7$2 = from_html(``);
-var root_8$1 = from_html(` `);
+var root_8$2 = from_html(` `);
var root_9$1 = from_html(`
`);
var root_10$1 = from_html(` `);
var root_12$1 = from_html(`
`);
-var root_13 = from_html(``);
+var root_13$1 = from_html(``);
var root_14$1 = from_html(`Input scale (MP) `);
var root_15$1 = from_html(`Seed `);
var root_16$1 = from_html(`Depth-locked: the poser outputs a depth map; output follows the pose frame.
`);
var root_17$1 = from_html(`Cancel `);
-var root_18$1 = from_html(`Close Re-Apply Add to Edit `, 1);
+var root_18 = from_html(`Close Re-Apply Add to Edit `, 1);
var root_19$1 = from_html(`Close Retry `, 1);
-var root_20$1 = from_html(`Cancel Run `, 1);
+var root_20 = from_html(`Cancel Run `, 1);
var root_1$2 = from_html(``);
function RePoseModal($$anchor, $$props) {
push($$props, true);
@@ -11851,7 +11851,7 @@ function RePoseModal($$anchor, $$props) {
var div_18 = child(div_17);
var select = sibling(child(div_18), 2);
each(select, 21, () => get(recipes), index, ($$anchor3, r) => {
- var option = root_8$1();
+ var option = root_8$2();
var text_4 = child(option);
var option_value = {};
template_effect(() => {
@@ -11906,7 +11906,7 @@ function RePoseModal($$anchor, $$props) {
append($$anchor4, div_22);
};
var alternate = ($$anchor4) => {
- var textarea = root_13();
+ var textarea = root_13$1();
bind_value(textarea, () => get(promptDoc), ($$value) => set(promptDoc, $$value));
append($$anchor4, textarea);
};
@@ -11973,7 +11973,7 @@ function RePoseModal($$anchor, $$props) {
append($$anchor3, button_3);
};
var consequent_12 = ($$anchor3) => {
- var fragment_4 = root_18$1();
+ var fragment_4 = root_18();
var button_4 = first_child(fragment_4);
var button_5 = sibling(button_4, 2);
var button_6 = sibling(button_5, 2);
@@ -11999,7 +11999,7 @@ function RePoseModal($$anchor, $$props) {
append($$anchor3, fragment_5);
};
var alternate_1 = ($$anchor3) => {
- var fragment_6 = root_20$1();
+ var fragment_6 = root_20();
var button_9 = first_child(fragment_6);
var button_10 = sibling(button_9, 2);
template_effect(() => button_10.disabled = !get(canRun));
@@ -12062,17 +12062,18 @@ delegate([
"contextmenu",
"change"
]);
-var root_2$1 = from_html(`
`);
-var root_1$1 = from_html(`Expand branches `, 1);
-var root_3$1 = from_html(`Edit `);
-var root_4$1 = from_html(`Delete `);
-var root_6$1 = from_html(`
`);
-var root_7$1 = from_html(`Properties `);
-var root_5$1 = from_html(` Copy Path Open File Open Folder `, 1);
-var root = from_html(`
`);
+var root_1$1 = from_html(`Expand branches `);
+var root_2$1 = from_html(`Focus this branch `);
+var root_3$1 = from_html(`
`);
+var root_4$1 = from_html(`Edit `);
+var root_5$1 = from_html(`Delete `);
+var root_7$1 = from_html(`
`);
+var root_8$1 = from_html(`Properties `);
+var root_6$1 = from_html(` Copy Path Open File Open Folder `, 1);
+var root = from_html(`
`);
function LineageContextMenu($$anchor, $$props) {
push($$props, true);
- let canExpand = prop($$props, "canExpand", 3, false), canEdit = prop($$props, "canEdit", 3, false), canDelete = prop($$props, "canDelete", 3, false), isLocal = prop($$props, "isLocal", 3, false), isWindows = prop($$props, "isWindows", 3, false), orphaned = prop($$props, "orphaned", 3, false);
+ let canExpand = prop($$props, "canExpand", 3, false), canFocus = prop($$props, "canFocus", 3, false), canEdit = prop($$props, "canEdit", 3, false), canDelete = prop($$props, "canDelete", 3, false), isLocal = prop($$props, "isLocal", 3, false), isWindows = prop($$props, "isWindows", 3, false), orphaned = prop($$props, "orphaned", 3, false);
let menuEl;
function act(action2) {
var _a, _b;
@@ -12106,89 +12107,99 @@ function LineageContextMenu($$anchor, $$props) {
var div = root();
var node = child(div);
{
- var consequent_1 = ($$anchor2) => {
- var fragment = root_1$1();
- var button = first_child(fragment);
- var node_1 = sibling(button, 2);
- {
- var consequent = ($$anchor3) => {
- var div_1 = root_2$1();
- append($$anchor3, div_1);
- };
- if_block(node_1, ($$render) => {
- if (canEdit() || canDelete() || isLocal()) $$render(consequent);
- });
- }
+ var consequent = ($$anchor2) => {
+ var button = root_1$1();
delegated("click", button, () => act("expand-branches"));
- append($$anchor2, fragment);
+ append($$anchor2, button);
};
if_block(node, ($$render) => {
- if (canExpand()) $$render(consequent_1);
+ if (canExpand()) $$render(consequent);
});
}
- var node_2 = sibling(node, 2);
+ var node_1 = sibling(node, 2);
{
- var consequent_2 = ($$anchor2) => {
- var button_1 = root_3$1();
- delegated("click", button_1, () => act("edit"));
+ var consequent_1 = ($$anchor2) => {
+ var button_1 = root_2$1();
+ delegated("click", button_1, () => act("focus-branch"));
append($$anchor2, button_1);
};
+ if_block(node_1, ($$render) => {
+ if (canFocus()) $$render(consequent_1);
+ });
+ }
+ var node_2 = sibling(node_1, 2);
+ {
+ var consequent_2 = ($$anchor2) => {
+ var div_1 = root_3$1();
+ append($$anchor2, div_1);
+ };
if_block(node_2, ($$render) => {
- if (canEdit()) $$render(consequent_2);
+ if ((canExpand() || canFocus()) && (canEdit() || canDelete() || isLocal())) $$render(consequent_2);
});
}
var node_3 = sibling(node_2, 2);
{
var consequent_3 = ($$anchor2) => {
var button_2 = root_4$1();
- delegated("click", button_2, () => act("delete"));
+ delegated("click", button_2, () => act("edit"));
append($$anchor2, button_2);
};
if_block(node_3, ($$render) => {
- if (canDelete()) $$render(consequent_3);
+ if (canEdit()) $$render(consequent_3);
});
}
var node_4 = sibling(node_3, 2);
{
- var consequent_6 = ($$anchor2) => {
- var fragment_1 = root_5$1();
- var node_5 = first_child(fragment_1);
- {
- var consequent_4 = ($$anchor3) => {
- var div_2 = root_6$1();
- append($$anchor3, div_2);
- };
- if_block(node_5, ($$render) => {
- if (canEdit() || canDelete()) $$render(consequent_4);
- });
- }
- var button_3 = sibling(node_5, 2);
- var button_4 = sibling(button_3, 2);
- var button_5 = sibling(button_4, 2);
- var node_6 = sibling(button_5, 2);
+ var consequent_4 = ($$anchor2) => {
+ var button_3 = root_5$1();
+ delegated("click", button_3, () => act("delete"));
+ append($$anchor2, button_3);
+ };
+ if_block(node_4, ($$render) => {
+ if (canDelete()) $$render(consequent_4);
+ });
+ }
+ var node_5 = sibling(node_4, 2);
+ {
+ var consequent_7 = ($$anchor2) => {
+ var fragment = root_6$1();
+ var node_6 = first_child(fragment);
{
var consequent_5 = ($$anchor3) => {
- var button_6 = root_7$1();
- template_effect(() => button_6.disabled = orphaned());
- delegated("click", button_6, () => act("properties"));
- append($$anchor3, button_6);
+ var div_2 = root_7$1();
+ append($$anchor3, div_2);
};
if_block(node_6, ($$render) => {
- if (isWindows()) $$render(consequent_5);
+ if (canEdit() || canDelete()) $$render(consequent_5);
+ });
+ }
+ var button_4 = sibling(node_6, 2);
+ var button_5 = sibling(button_4, 2);
+ var button_6 = sibling(button_5, 2);
+ var node_7 = sibling(button_6, 2);
+ {
+ var consequent_6 = ($$anchor3) => {
+ var button_7 = root_8$1();
+ template_effect(() => button_7.disabled = orphaned());
+ delegated("click", button_7, () => act("properties"));
+ append($$anchor3, button_7);
+ };
+ if_block(node_7, ($$render) => {
+ if (isWindows()) $$render(consequent_6);
});
}
template_effect(() => {
- button_3.disabled = orphaned();
button_4.disabled = orphaned();
button_5.disabled = orphaned();
+ button_6.disabled = orphaned();
});
- delegated("click", button_3, () => act("copy-path"));
- delegated("click", button_4, () => act("open-file"));
- delegated("click", button_5, () => act("open-folder"));
- append($$anchor2, fragment_1);
+ delegated("click", button_4, () => act("copy-path"));
+ delegated("click", button_5, () => act("open-file"));
+ delegated("click", button_6, () => act("open-folder"));
+ append($$anchor2, fragment);
};
- if_block(node_4, ($$render) => {
- if (isLocal()) $$render(consequent_6);
+ if_block(node_5, ($$render) => {
+ if (isLocal()) $$render(consequent_7);
});
}
action(div, ($$node) => {
@@ -12231,21 +12242,22 @@ function buildLane(tree, startHash, spineSet) {
}
return { startHash, nodes, junctions };
}
-function buildRender(tree, anchorHash, brokenOut) {
+const CHILDREN_PER_ROW_CAP = 5;
+const CHILDREN_REVEAL_STEP = 4;
+const DEFAULT_REVEAL = CHILDREN_PER_ROW_CAP - 1;
+function buildRender(tree, anchorHash, brokenOut, opts = {}) {
+ var _a;
+ const { byHash, childrenOf } = tree;
+ const { displayedHash, revealed = /* @__PURE__ */ new Map(), rootHash: rootOverride } = opts;
const a = ancestry(tree, anchorHash);
- const col0 = buildLane(tree, a.root, a.set);
- const columns = [];
- const edges = [];
+ const laneStart = rootOverride && byHash.has(rootOverride) ? rootOverride : a.root;
+ const col0 = buildLane(tree, laneStart, a.set);
+ const shown = /* @__PURE__ */ new Set();
const started = /* @__PURE__ */ new Set();
const junctionsByNode = /* @__PURE__ */ new Map();
- const visit = (lane, topRow) => {
- lane.topRow = topRow;
- columns.push(lane);
- for (let i = 0; i + 1 < lane.nodes.length; i++) {
- edges.push({ from: lane.nodes[i].hash, to: lane.nodes[i + 1].hash });
- }
+ const visit = (lane) => {
+ for (const n of lane.nodes) shown.add(n.hash);
for (const j of lane.junctions) {
- const forkRow = lane.nodes.findIndex((n) => n.hash === j.atHash);
let rec = junctionsByNode.get(j.atHash);
if (!rec) {
rec = { shown: [], hidden: [] };
@@ -12256,8 +12268,7 @@ function buildRender(tree, anchorHash, brokenOut) {
rec.shown.push(childRoot);
if (!started.has(childRoot)) {
started.add(childRoot);
- edges.push({ from: j.atHash, to: childRoot });
- visit(buildLane(tree, childRoot), topRow + forkRow + 1);
+ visit(buildLane(tree, childRoot));
}
} else {
rec.hidden.push(childRoot);
@@ -12265,8 +12276,86 @@ function buildRender(tree, anchorHash, brokenOut) {
}
}
};
- visit(col0, 0);
- return { columns, edges, junctionsByNode, rootHash: col0.nodes.length ? col0.nodes[0].hash : null };
+ visit(col0);
+ const activePath = ancestry(tree, displayedHash || anchorHash).set;
+ const childrenShown = (h) => (childrenOf.get(h) || []).filter((k) => shown.has(k.hash));
+ const hasShownSubtree = (h) => childrenShown(h).length > 0;
+ const xOf = /* @__PURE__ */ new Map();
+ const depthOf = /* @__PURE__ */ new Map();
+ const placed = /* @__PURE__ */ new Set();
+ const foldedAway = /* @__PURE__ */ new Set();
+ const bundles = [];
+ let cursor = 0;
+ const place = (h, depth) => {
+ depthOf.set(h, depth);
+ placed.add(h);
+ const kids = childrenShown(h);
+ if (!kids.length) {
+ xOf.set(h, cursor++);
+ return;
+ }
+ let bundleKids = [];
+ let reals = kids;
+ if (kids.length > CHILDREN_PER_ROW_CAP) {
+ const optional = kids.filter((k) => !activePath.has(k.hash) && !hasShownSubtree(k.hash));
+ const budget = revealed.get(h) ?? DEFAULT_REVEAL;
+ const forced = kids.length - optional.length;
+ const keepOptional = Math.max(0, budget - forced);
+ const keep = new Set(optional.slice(Math.max(0, optional.length - keepOptional)).map((k) => k.hash));
+ bundleKids = optional.filter((k) => !keep.has(k.hash));
+ if (bundleKids.length <= 1) bundleKids = [];
+ const folded = new Set(bundleKids.map((k) => k.hash));
+ reals = kids.filter((k) => !folded.has(k.hash));
+ }
+ const xs = [];
+ if (bundleKids.length) {
+ const id = `bundle:${h}`;
+ depthOf.set(id, depth + 1);
+ xOf.set(id, cursor++);
+ xs.push(xOf.get(id));
+ bundles.push({
+ id,
+ parent: h,
+ depth: depth + 1,
+ hidden: bundleKids.map((k) => k.hash),
+ preview: bundleKids.slice(0, 2).map((k) => k.hash),
+ count: bundleKids.length
+ });
+ for (const k of bundleKids) foldedAway.add(k.hash);
+ }
+ for (const k of reals) {
+ place(k.hash, depth + 1);
+ xs.push(xOf.get(k.hash));
+ }
+ xOf.set(h, (xs[0] + xs[xs.length - 1]) / 2);
+ };
+ const rootHash = col0.nodes.length ? col0.nodes[0].hash : null;
+ if (rootHash && shown.has(rootHash)) place(rootHash, 0);
+ for (const h of shown) if (!placed.has(h) && !foldedAway.has(h)) place(h, 0);
+ const edges = [];
+ for (const h of placed) {
+ if (h === rootHash) continue;
+ const p = (_a = byHash.get(h)) == null ? void 0 : _a.parent_hash;
+ if (p && placed.has(p)) edges.push({ from: p, to: h });
+ }
+ for (const b of bundles) edges.push({ from: b.parent, to: b.id });
+ let maxX = 0, maxDepth = 0;
+ const nodes = [];
+ const note = (x, depth) => {
+ if (x > maxX) maxX = x;
+ if (depth > maxDepth) maxDepth = depth;
+ };
+ for (const h of placed) {
+ const x = xOf.get(h), depth = depthOf.get(h);
+ note(x, depth);
+ nodes.push({ kind: "node", hash: h, item: byHash.get(h), x, depth, isTip: childrenShown(h).length === 0 });
+ }
+ for (const b of bundles) {
+ const x = xOf.get(b.id);
+ note(x, b.depth);
+ nodes.push({ kind: "bundle", hash: b.id, parent: b.parent, hidden: b.hidden, preview: b.preview, count: b.count, x, depth: b.depth });
+ }
+ return { nodes, edges, junctionsByNode, rootHash, width: maxX + 1, height: maxDepth + 1 };
}
function regionFigureIndices(regionList, poseJson) {
let names = [];
@@ -12360,82 +12449,91 @@ function buildFigureRegions({ workflow, regionsRaw, poseFilesOk, apiURL }) {
return [];
}
}
-var root_3 = from_html(``);
-var root_4 = from_html(` `);
-var root_5 = from_html(`⊟ `);
-var root_2 = from_html(``);
-var root_7 = from_html(` `);
-var root_9 = from_svg(` `);
-var root_10 = from_html(`
`);
-var root_8 = from_html(`
`);
-var root_15 = from_html(``);
-var root_14 = from_html(``);
-var root_18 = from_html(` `);
-var root_17 = from_html(`
`, 1);
-var root_19 = from_html(`▴ `);
-var root_16 = from_html(`
`);
-var root_20 = from_html(`
`);
-var root_12 = from_html(`
`, 1);
-var root_6 = from_html(`
`);
-var root_21 = from_html(`
`);
-var root_23 = from_html(`
`);
-var root_24 = from_html(`
`);
-var root_25 = from_html(`
`);
-var root_22 = from_html(``);
-var root_26 = from_html(`
`);
-var root_27 = from_html(`📋 Path copied to clipboard
`);
-var root_29 = from_html(`
`);
-var root_30 = from_html(` `);
-var root_33 = from_html(`
`);
-var root_32 = from_html(
+var root_3 = from_html(`✓
`);
+var root_4 = from_html(``);
+var root_5 = from_html(`
`);
+var root_6 = from_html(`
`, 1);
+var root_7 = from_html(`⊟ `);
+var root_2 = from_html(``);
+var root_9 = from_html(` `);
+var root_10 = from_html(`⊙ Focused ✕ `);
+var root_11 = from_html(` Delete ✕
`);
+var root_13 = from_svg(` `);
+var root_14 = from_svg(` `);
+var root_17 = from_html(` `);
+var root_16 = from_html(`
`);
+var root_15 = from_html(`
`);
+var root_12 = from_html(``);
+var root_22 = from_html(`✓
`);
+var root_23 = from_html(``);
+var root_21 = from_html(``);
+var root_26 = from_html(` `);
+var root_25 = from_html(`
`, 1);
+var root_27 = from_html(`▴ `);
+var root_24 = from_html(`
`);
+var root_28 = from_html(`
`);
+var root_19 = from_html(`
`, 1);
+var root_8 = from_html(`
`);
+var root_29 = from_html(`
`);
+var root_31 = from_html(`
`);
+var root_32 = from_html(`
`);
+var root_33 = from_html(`
`);
+var root_34 = from_html(`
`);
+var root_30 = from_html(``);
+var root_35 = from_html(`
`);
+var root_36 = from_html(`📋 Path copied to clipboard
`);
+var root_38 = from_html(`
`);
+var root_39 = from_html(` `);
+var root_42 = from_html(`
`);
+var root_41 = from_html(
`This image's record points to a file that no longer exists. If the
file was moved or renamed, locate it — it's verified against the
image's content hash, so the wrong file can't be attached.
`,
1
);
-var root_31 = from_html(``);
-var root_35 = from_html(`
`);
-var root_36 = from_html(`
`);
-var root_34 = from_html(`
`, 1);
-var root_28 = from_html(` `, 1);
-var root_37 = from_html(``);
-var root_38 = from_html(``);
-var root_40 = from_html(``);
-var root_41 = from_svg(` `);
-var root_42 = from_svg(` `);
-var root_44 = from_html(``);
-var root_43 = from_html(`
`);
-var root_39 = from_html(``);
-var root_46 = from_html(`
`);
-var root_45 = from_html(`
`);
-var root_49 = from_html(`
`);
-var root_50 = from_html(`
Clear
`, 1);
-var root_48 = from_html(`
`);
-var root_47 = from_html(``);
-var root_54 = from_html(` `);
-var root_53 = from_html(``);
-var root_55 = from_html(``);
-var root_56 = from_html(``);
-var root_58 = from_html(`Dimensions
`);
-var root_59 = from_html(`Size
`);
-var root_60 = from_html(`Generated
`);
-var root_61 = from_html(`Filename
`);
-var root_62 = from_html(`Status Source deleted
`);
-var root_65 = from_html(``);
-var root_66 = from_html(``);
-var root_64 = from_html(` `, 1);
-var root_67 = from_html(``);
-var root_68 = from_html(``);
-var root_63 = from_html(`Prompt
`, 1);
-var root_70 = from_html(`Model
`);
-var root_71 = from_html(`Seed
`);
-var root_72 = from_html(`Steps
`);
-var root_73 = from_html(`CFG
`);
-var root_74 = from_html(`Sampler
`);
-var root_75 = from_html(`Denoise
`);
-var root_69 = from_html(`Generation Settings
`, 1);
-var root_57 = from_html(`Image Info
`, 1);
-var root_76 = from_html(`Loading metadata...
`);
+var root_40 = from_html(``);
+var root_44 = from_html(`
`);
+var root_45 = from_html(`
`);
+var root_43 = from_html(`
`, 1);
+var root_37 = from_html(` `, 1);
+var root_46 = from_html(``);
+var root_47 = from_html(``);
+var root_49 = from_html(``);
+var root_50 = from_svg(` `);
+var root_51 = from_svg(` `);
+var root_53 = from_html(``);
+var root_52 = from_html(`
`);
+var root_48 = from_html(``);
+var root_55 = from_html(`
`);
+var root_54 = from_html(`
`);
+var root_58 = from_html(`
`);
+var root_59 = from_html(`
Clear
`, 1);
+var root_57 = from_html(`
`);
+var root_56 = from_html(``);
+var root_63 = from_html(` `);
+var root_62 = from_html(``);
+var root_64 = from_html(``);
+var root_65 = from_html(``);
+var root_67 = from_html(`Dimensions
`);
+var root_68 = from_html(`Size
`);
+var root_69 = from_html(`Generated
`);
+var root_70 = from_html(`Filename
`);
+var root_71 = from_html(`Status Source deleted
`);
+var root_74 = from_html(``);
+var root_75 = from_html(``);
+var root_73 = from_html(` `, 1);
+var root_76 = from_html(``);
+var root_77 = from_html(``);
+var root_72 = from_html(`Prompt
`, 1);
+var root_79 = from_html(`Model
`);
+var root_80 = from_html(`Seed
`);
+var root_81 = from_html(`Steps
`);
+var root_82 = from_html(`CFG
`);
+var root_83 = from_html(`Sampler
`);
+var root_84 = from_html(`Denoise
`);
+var root_78 = from_html(`Generation Settings
`, 1);
+var root_66 = from_html(`Image Info
`, 1);
+var root_85 = from_html(`Loading metadata...
`);
var root_1 = from_html(` `, 1);
function ImageViewer($$anchor, $$props) {
var _a;
@@ -12635,6 +12733,26 @@ function ImageViewer($$anchor, $$props) {
const roots = list.filter((f) => !f.parent_hash || !byHash.has(f.parent_hash)).map((f) => f.hash);
return { byHash, childrenOf, roots };
})());
+ let lineageOps = user_derived(() => {
+ const { byHash } = get(lineageTree);
+ const ops = /* @__PURE__ */ new Map();
+ for (const item of get(lineageList)) {
+ const p = (item == null ? void 0 : item.parent_hash) ? byHash.get(item.parent_hash) : null;
+ if (!p || !item.width || !item.height || !p.width || !p.height) continue;
+ if (item.width > p.width * 1.2 && item.height > p.height * 1.2) {
+ const wf = item.width / p.width, hf = item.height / p.height;
+ const uniform = Math.abs(wf - hf) < 0.05;
+ ops.set(item.hash, {
+ kind: "upscale",
+ factor: wf,
+ uniform,
+ from: { w: p.width, h: p.height }
+ });
+ }
+ }
+ return ops;
+ });
+ const fmtFactor = (f) => Math.abs(f - Math.round(f)) < 0.05 ? `${Math.round(f)}×` : `${f.toFixed(1)}×`;
let lineageDisplay = user_derived(() => (() => {
const asNodes = (list) => list.map((item) => ({ kind: "node", item }));
if (get(lineageList).length < 2) return asNodes(get(lineageList));
@@ -12714,9 +12832,68 @@ function ImageViewer($$anchor, $$props) {
let lineageDisplayCurrentIdx = user_derived(() => get(lineageDisplay).findIndex((d) => d.kind === "node" && d.item.hash === get(displayedHash)));
let lineageHiddenCount = user_derived(() => get(lineageList).length - get(lineageVisible).length);
let brokenOut = state(proxy(/* @__PURE__ */ new Set()));
+ let lineageRevealed = state(proxy(/* @__PURE__ */ new Map()));
let subwayAnchor = user_derived(() => get(lineageFocusHash) && get(lineageTree).byHash.has(get(lineageFocusHash)) ? get(lineageFocusHash) : get(displayedHash));
- let laneRender = user_derived(() => buildRender(get(lineageTree), get(subwayAnchor), get(brokenOut)));
- let lanesActive = user_derived(() => get(brokenOut).size > 0 && get(lineageList).length > 1);
+ let focusRootHash = state(null);
+ let laneRender = user_derived(() => buildRender(get(lineageTree), get(subwayAnchor), get(brokenOut), {
+ displayedHash: get(displayedHash),
+ revealed: get(lineageRevealed),
+ rootHash: get(focusRootHash)
+ }));
+ let lanesActive = user_derived(() => (get(brokenOut).size > 0 || !!get(focusRootHash)) && get(lineageList).length > 1);
+ function isInSubtree(desc, rootHash) {
+ const { byHash } = get(lineageTree);
+ const seen = /* @__PURE__ */ new Set();
+ let cur = desc;
+ while (cur && byHash.has(cur) && !seen.has(cur)) {
+ seen.add(cur);
+ if (cur === rootHash) return true;
+ cur = byHash.get(cur).parent_hash;
+ }
+ return false;
+ }
+ user_effect(() => {
+ if (!get(focusRootHash)) return;
+ if (!get(lineageTree).byHash.has(get(focusRootHash)) || !isInSubtree(get(displayedHash), get(focusRootHash))) set(focusRootHash, null);
+ });
+ function focusBranch(hash) {
+ if (!hash || !get(lineageTree).byHash.has(hash)) return;
+ if (!isInSubtree(get(displayedHash), hash)) lineageJump(hash);
+ set(focusRootHash, hash, true);
+ }
+ function clearFocus() {
+ set(focusRootHash, null);
+ }
+ const LANE_PREFS_KEY = "pcr.viewer.lanePrefs.v1";
+ let familyRootHash = user_derived(() => get(lineageTree).byHash.has(get(displayedHash)) ? ancestry(get(lineageTree), get(displayedHash)).root : null);
+ let hydratedRoot = null;
+ function readLanePrefs() {
+ try {
+ return JSON.parse(localStorage.getItem(LANE_PREFS_KEY) || "{}") || {};
+ } catch {
+ return {};
+ }
+ }
+ user_effect(() => {
+ const root2 = get(familyRootHash);
+ if (!root2 || root2 === hydratedRoot) return;
+ hydratedRoot = root2;
+ const prefs = readLanePrefs()[root2];
+ set(brokenOut, new Set(((prefs == null ? void 0 : prefs.brokenOut) || []).filter((h) => get(lineageTree).byHash.has(h))), true);
+ set(lineageRevealed, new Map(((prefs == null ? void 0 : prefs.revealed) || []).filter(([k]) => get(lineageTree).byHash.has(k))), true);
+ });
+ user_effect(() => {
+ const root2 = get(familyRootHash);
+ const bo = [...get(brokenOut)], rv = [...get(lineageRevealed)];
+ if (!root2 || root2 !== hydratedRoot) return;
+ try {
+ const all = readLanePrefs();
+ if (bo.length || rv.length) all[root2] = { brokenOut: bo, revealed: rv };
+ else delete all[root2];
+ localStorage.setItem(LANE_PREFS_KEY, JSON.stringify(all));
+ } catch {
+ }
+ });
function primaryChildHash(item) {
const kids = get(lineageTree).childrenOf.get(item == null ? void 0 : item.hash) || [];
if (!kids.length) return null;
@@ -12745,6 +12922,42 @@ function ImageViewer($$anchor, $$props) {
}
function collapseAllLanes() {
set(brokenOut, /* @__PURE__ */ new Set(), true);
+ set(lineageRevealed, /* @__PURE__ */ new Map(), true);
+ set(focusRootHash, null);
+ }
+ let selectedHashes = state(proxy(/* @__PURE__ */ new Set()));
+ user_effect(() => {
+ if (!get(selectedHashes).size) return;
+ const { byHash } = get(lineageTree);
+ const kept = [...get(selectedHashes)].filter((h) => byHash.has(h));
+ if (kept.length !== get(selectedHashes).size) set(selectedHashes, new Set(kept), true);
+ });
+ function toggleSelect(hash) {
+ const next = new Set(get(selectedHashes));
+ if (next.has(hash)) next.delete(hash);
+ else next.add(hash);
+ set(selectedHashes, next, true);
+ }
+ function clearSelection() {
+ if (get(selectedHashes).size) set(selectedHashes, /* @__PURE__ */ new Set(), true);
+ }
+ function handleNodeClick(e, item) {
+ if (get(compareDropdownOpen)) {
+ if (item.hash !== get(displayedHash)) enterCompareWith(item.hash, item.filename || item.hash.slice(0, 8));
+ return;
+ }
+ if (e.ctrlKey || e.metaKey || e.shiftKey) {
+ toggleSelect(item.hash);
+ return;
+ }
+ clearSelection();
+ lineageJump(item.hash);
+ exitCompare();
+ }
+ function revealMore(parentHash) {
+ const next = new Map(get(lineageRevealed));
+ next.set(parentHash, (next.get(parentHash) ?? CHILDREN_PER_ROW_CAP - 1) + CHILDREN_REVEAL_STEP);
+ set(lineageRevealed, next, true);
}
let hasBranches = user_derived(() => (() => {
for (const kids of get(lineageTree).childrenOf.values()) if (kids.length > 1) return true;
@@ -12763,7 +12976,7 @@ function ImageViewer($$anchor, $$props) {
user_effect(() => {
var _a2;
if (!get(lanesActive)) return;
- if (get(laneRender).columns.some((c) => c.nodes.some((n) => n.hash === get(displayedHash)))) return;
+ if (get(laneRender).nodes.some((n) => n.hash === get(displayedHash))) return;
const { byHash, childrenOf } = get(lineageTree);
const a = ancestry(get(lineageTree), get(subwayAnchor));
const toBreak = [];
@@ -12786,10 +12999,28 @@ function ImageViewer($$anchor, $$props) {
}
});
let lanesWrapEl = state(null);
+ let treeScrollEl = state(null);
let connectorGeom = state(proxy([]));
let connectorBox = state(proxy({ w: 0, h: 0 }));
+ let activePath = user_derived(() => {
+ const nodes = /* @__PURE__ */ new Set(), edges = /* @__PURE__ */ new Set();
+ const { byHash } = get(lineageTree);
+ const seen = /* @__PURE__ */ new Set();
+ let cur = get(displayedHash);
+ while (cur && byHash.has(cur) && !seen.has(cur)) {
+ seen.add(cur);
+ nodes.add(cur);
+ const p = byHash.get(cur).parent_hash;
+ if (p && byHash.has(p)) edges.add(`${p}>${cur}`);
+ cur = p;
+ }
+ return { nodes, edges };
+ });
+ let connectorsBase = user_derived(() => get(connectorGeom).filter((g) => !get(activePath).edges.has(g.key)));
+ let connectorsActive = user_derived(() => get(connectorGeom).filter((g) => get(activePath).edges.has(g.key)));
let measureScheduled = false;
const LANE_ROW_H = 86;
+ const LANE_COL_W = 88;
function contentXY(el, root2) {
let x = 0, y = 0;
for (let n = el; n && n !== root2; n = n.offsetParent) {
@@ -12833,11 +13064,11 @@ function ImageViewer($$anchor, $$props) {
}
user_effect(() => {
if (!get(lanesActive)) return;
- const cols = get(
+ const ns = get(
laneRender
// read → track expand/fold/family changes
- ).columns;
- if (cols) scheduleMeasure();
+ ).nodes;
+ if (ns) scheduleMeasure();
});
user_effect(() => {
if (!get(lanesWrapEl)) return;
@@ -12845,6 +13076,33 @@ function ImageViewer($$anchor, $$props) {
ro.observe(get(lanesWrapEl));
return () => ro.disconnect();
});
+ function centerCurrentNode() {
+ const scroller = get(treeScrollEl), wrap = get(lanesWrapEl);
+ if (!scroller || !wrap) return;
+ const el = wrap.querySelector(`[data-hash="${get(displayedHash)}"]`);
+ if (!el) return;
+ const p = contentXY(el, scroller);
+ const mX = Math.min(LANE_COL_W, Math.max(8, (scroller.clientWidth - p.w) / 2 - 1));
+ const mY = Math.min(LANE_COL_W, Math.max(8, (scroller.clientHeight - p.h) / 2 - 1));
+ const offL = p.x - scroller.scrollLeft, offT = p.y - scroller.scrollTop;
+ const outH = offL < mX || offL + p.w > scroller.clientWidth - mX;
+ const outV = offT < mY || offT + p.h > scroller.clientHeight - mY;
+ if (!outH && !outV) return;
+ scroller.scrollTo({
+ left: Math.max(0, p.x + p.w / 2 - scroller.clientWidth / 2),
+ top: Math.max(0, p.y + p.h / 2 - scroller.clientHeight / 2),
+ behavior: "smooth"
+ });
+ }
+ user_effect(() => {
+ if (!get(lanesActive)) return;
+ get(
+ displayedHash
+ // track → recenter on navigation/expand
+ );
+ get(laneRender).nodes;
+ requestAnimationFrame(centerCurrentNode);
+ });
let compareTargets = user_derived(() => get(lineageList).filter((item) => (item == null ? void 0 : item.hash) !== get(displayedHash)));
let hasCompareTargets = user_derived(() => get(compareTargets).length > 0);
let compareImageUrl = user_derived(() => get(compareTargetHash) ? imageUrl(get(compareTargetHash)) : null);
@@ -12924,13 +13182,14 @@ function ImageViewer($$anchor, $$props) {
user_effect(() => {
console.trace("[PCR][viewer] displayedHash ->", get(displayedHash), "currentIndex", get(currentIndex));
});
+ let imageByHash = user_derived(() => new Map((images() || []).map((i) => [i.hash, i])));
function imageUrl(hash) {
- const img = images().find((i) => i.hash === hash);
+ const img = get(imageByHash).get(hash);
if (img == null ? void 0 : img._directUrl) return img._directUrl;
return apiURL()(`/promptchain/image/${hash}`);
}
function thumbUrl(hash) {
- const img = images().find((i) => i.hash === hash);
+ const img = get(imageByHash).get(hash);
if (img == null ? void 0 : img._directUrl) return img._directUrl;
return apiURL()(`/promptchain/thumb/${hash}`);
}
@@ -13584,6 +13843,10 @@ ${negative}` : ""));
set(compareDropdownOpen, false);
return;
}
+ if (get(selectedHashes).size) {
+ clearSelection();
+ return;
+ }
if (get(historyExpanded)) {
set(historyExpanded, false);
return;
@@ -13595,6 +13858,13 @@ ${negative}` : ""));
onClose()();
return;
}
+ if ((e.key === "Delete" || e.key === "Backspace") && get(selectedHashes).size && onDelete()) {
+ const t = e.target;
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
+ e.preventDefault();
+ requestDeleteBatch();
+ return;
+ }
if ((e.key === "c" || e.key === "C") && !e.ctrlKey && !e.metaKey && get(hasCompareTargets)) {
e.preventDefault();
if (get(compareMode)) exitCompare();
@@ -13710,14 +13980,22 @@ ${negative}` : ""));
let deleting = state(false);
let confirmDeleteOpen = state(false);
let deleteTargetHash = state(null);
+ let deleteBatch = state(null);
function requestDelete() {
if (!onDelete() || get(deleting)) return;
set(deleteTargetHash, null);
+ set(deleteBatch, null);
+ set(confirmDeleteOpen, true);
+ }
+ function requestDeleteBatch() {
+ if (!onDelete() || get(deleting) || !get(selectedHashes).size) return;
+ set(deleteBatch, [...get(selectedHashes)], true);
set(confirmDeleteOpen, true);
}
function cancelDelete() {
set(confirmDeleteOpen, false);
set(deleteTargetHash, null);
+ set(deleteBatch, null);
}
const isWindows = /windows/i.test(navigator.userAgent);
let lineageMenu = state(
@@ -13778,6 +14056,8 @@ ${negative}` : ""));
if (!hash) return;
if (action2 === "expand-branches") {
expandBranches(item);
+ } else if (action2 === "focus-branch") {
+ focusBranch(hash);
} else if (action2 === "copy-path") {
copyPath(hash);
} else if (action2 === "edit") {
@@ -13797,17 +14077,66 @@ ${negative}` : ""));
});
}
}
+ async function handleDeleteBatch(hashes) {
+ set(deleteBatch, null);
+ set(deleting, true);
+ const gone = /* @__PURE__ */ new Set();
+ try {
+ for (const h of hashes) {
+ try {
+ await onDelete()(h);
+ gone.add(h);
+ } catch (err) {
+ console.error("[PromptChain] batch delete failed for", h, err);
+ }
+ }
+ if (gone.size) {
+ set(selectedHashes, new Set([...get(
+ selectedHashes
+ // keep any that failed
+ )].filter((h) => !gone.has(h))), true);
+ const survivorsOrdered = get(lineageVisible).map((i) => i == null ? void 0 : i.hash).filter((h) => h && !gone.has(h));
+ const landHash = get(displayedHash) && !gone.has(get(displayedHash)) ? get(displayedHash) : survivorsOrdered[0] || null;
+ images(images().filter((i) => !gone.has(i.hash)));
+ const survivors = get(lineageList).filter((item) => (item == null ? void 0 : item.hash) && !gone.has(item.hash));
+ if (!images().length) {
+ if (survivors.length) {
+ const next = survivors.find((s) => s.hash === landHash) || survivors[0];
+ images(survivors);
+ set(currentIndex, Math.max(0, survivors.findIndex((s) => s.hash === next.hash)), true);
+ set(displayedHash, next.hash, true);
+ } else {
+ onClose()();
+ }
+ } else if (landHash && images().some((i) => i.hash === landHash)) {
+ set(currentIndex, images().findIndex((i) => i.hash === landHash), true);
+ set(displayedHash, landHash, true);
+ } else {
+ const idx = Math.min(get(currentIndex), images().length - 1);
+ set(currentIndex, idx, true);
+ set(displayedHash, images()[idx].hash, true);
+ }
+ }
+ } catch (e) {
+ console.error("[PromptChain] viewer batch delete failed", e);
+ }
+ set(deleting, false);
+ }
async function handleDelete() {
- var _a2, _b;
+ var _a2, _b, _c;
set(confirmDeleteOpen, false);
if (!onDelete() || get(deleting)) return;
+ if ((_a2 = get(deleteBatch)) == null ? void 0 : _a2.length) {
+ await handleDeleteBatch(get(deleteBatch));
+ return;
+ }
const hash = get(deleteTargetHash) || get(displayedHash);
set(deleteTargetHash, null);
const linIdx = get(lineageVisible).findIndex((item) => (item == null ? void 0 : item.hash) === hash);
let landHash = null;
if (linIdx >= 0) {
- const prev = (_a2 = get(lineageVisible)[linIdx - 1]) == null ? void 0 : _a2.hash;
- const next = (_b = get(lineageVisible)[linIdx + 1]) == null ? void 0 : _b.hash;
+ const prev = (_b = get(lineageVisible)[linIdx - 1]) == null ? void 0 : _b.hash;
+ const next = (_c = get(lineageVisible)[linIdx + 1]) == null ? void 0 : _c.hash;
landHash = prev && prev !== hash ? prev : next && next !== hash ? next : null;
}
set(deleting, true);
@@ -13899,337 +14228,469 @@ ${negative}` : ""));
});
var div = first_child(fragment);
{
- const lineageNode = ($$anchor2, item = noop, lane = noop) => {
+ const lineageNode = ($$anchor2, item = noop, meta = noop) => {
var div_1 = root_2();
let classes;
var img_1 = child(div_1);
- var div_2 = sibling(img_1, 2);
- let classes_1;
- var node = sibling(div_2, 2);
+ var node_1 = sibling(img_1, 2);
{
var consequent = ($$anchor3) => {
- var div_3 = root_3();
- append($$anchor3, div_3);
+ var div_2 = root_3();
+ append($$anchor3, div_2);
};
- if_block(node, ($$render) => {
- if (get(editDocLayers)[item().hash]) $$render(consequent);
+ var d_1 = user_derived(() => get(selectedHashes).has(item().hash));
+ if_block(node_1, ($$render) => {
+ if (get(d_1)) $$render(consequent);
});
}
- var node_1 = sibling(node, 2);
+ var div_3 = sibling(node_1, 2);
+ let classes_1;
+ var node_2 = sibling(div_3, 2);
{
var consequent_1 = ($$anchor3) => {
- var button = root_4();
- var text_1 = child(button);
+ var div_4 = root_4();
+ append($$anchor3, div_4);
+ };
+ if_block(node_2, ($$render) => {
+ if (get(editDocLayers)[item().hash]) $$render(consequent_1);
+ });
+ }
+ var node_3 = sibling(node_2, 2);
+ {
+ var consequent_2 = ($$anchor3) => {
+ const op = user_derived(() => get(lineageOps).get(item().hash));
+ var div_5 = root_5();
+ var text_1 = child(div_5);
template_effect(
($0, $1) => {
- set_attribute(button, "title", `Break this node's ${$0 ?? ""} other branch(es) into their own columns`);
- set_text(text_1, `⊞${$1 ?? ""}`);
+ set_attribute(div_5, "title", `Upscaled ${$0 ?? ""}from ${get(op).from.w ?? ""}×${get(op).from.h ?? ""}`);
+ set_text(text_1, `▲${$1 ?? ""}`);
},
[
- () => get(laneRender).junctionsByNode.get(item().hash).hidden.length,
- () => get(laneRender).junctionsByNode.get(item().hash).hidden.length
+ () => get(op).uniform ? fmtFactor(get(op).factor) + " " : "",
+ () => get(op).uniform ? fmtFactor(get(op).factor) : ""
]
);
+ append($$anchor3, div_5);
+ };
+ var d_2 = user_derived(() => {
+ var _a2;
+ return ((_a2 = get(lineageOps).get(item().hash)) == null ? void 0 : _a2.kind) === "upscale";
+ });
+ if_block(node_3, ($$render) => {
+ if (get(d_2)) $$render(consequent_2);
+ });
+ }
+ var node_4 = sibling(node_3, 2);
+ {
+ var consequent_3 = ($$anchor3) => {
+ const n = user_derived(() => get(laneRender).junctionsByNode.get(item().hash).hidden.length);
+ var fragment_1 = root_6();
+ var div_6 = first_child(fragment_1);
+ var text_2 = child(div_6);
+ var button = sibling(div_6, 2);
+ var text_3 = child(button);
+ template_effect(() => {
+ set_text(text_2, `⋔${get(n) ?? ""}`);
+ set_attribute(button, "title", `Show this node's ${get(n) ?? ""} other branch(es)`);
+ set_text(text_3, `⊞${get(n) ?? ""}`);
+ });
delegated("click", button, (e) => {
e.stopPropagation();
expandBranches(item());
});
- append($$anchor3, button);
+ append($$anchor3, fragment_1);
};
- var d_1 = user_derived(() => {
+ var d_3 = user_derived(() => {
var _a2;
return (_a2 = get(laneRender).junctionsByNode.get(item().hash)) == null ? void 0 : _a2.hidden.length;
});
- if_block(node_1, ($$render) => {
- if (get(d_1)) $$render(consequent_1);
+ if_block(node_4, ($$render) => {
+ if (get(d_3)) $$render(consequent_3);
});
}
- var node_2 = sibling(node_1, 2);
+ var node_5 = sibling(node_4, 2);
{
- var consequent_2 = ($$anchor3) => {
- var button_1 = root_5();
+ var consequent_4 = ($$anchor3) => {
+ var button_1 = root_7();
delegated("click", button_1, (e) => {
e.stopPropagation();
- foldLane(lane().startHash);
+ foldLane(item().hash);
});
append($$anchor3, button_1);
};
- var d_2 = user_derived(() => get(brokenOut).has(lane().startHash) && item().hash === lane().startHash);
- if_block(node_2, ($$render) => {
- if (get(d_2)) $$render(consequent_2);
+ var d_4 = user_derived(() => get(brokenOut).has(item().hash));
+ if_block(node_5, ($$render) => {
+ if (get(d_4)) $$render(consequent_4);
});
}
template_effect(
- ($0) => {
- classes = set_class(div_1, 1, "pcr-viewer-lineage-node svelte-5cciiw", null, classes, {
+ ($0, $1) => {
+ var _a2;
+ classes = set_class(div_1, 1, "pcr-viewer-lineage-node svelte-5cciiw", null, classes, $0);
+ set_attribute(div_1, "data-hash", item().hash);
+ set_attribute(img_1, "src", $1);
+ classes_1 = set_class(div_3, 1, "pcr-viewer-lineage-dot svelte-5cciiw", null, classes_1, {
+ "dot-root": item().hash === get(laneRender).rootHash,
+ "dot-current": item().hash === get(displayedHash),
+ "dot-tip": ((_a2 = meta()) == null ? void 0 : _a2.isTip) && item().hash !== get(displayedHash)
+ });
+ },
+ [
+ () => ({
current: item().hash === get(displayedHash),
+ selected: get(selectedHashes).has(item().hash),
orphaned: item().orphaned === 1,
"compare-candidate": get(compareDropdownOpen) && item().hash !== get(displayedHash),
"compare-left": get(compareMode) && item().hash === get(displayedHash),
"compare-right": get(compareMode) && item().hash === get(compareTargetHash)
- });
- set_attribute(div_1, "data-hash", item().hash);
- set_attribute(img_1, "src", $0);
- classes_1 = set_class(div_2, 1, "pcr-viewer-lineage-dot svelte-5cciiw", null, classes_1, {
- "dot-root": item().hash === get(laneRender).rootHash,
- "dot-current": item().hash === get(displayedHash),
- "dot-tip": item().hash === lane().nodes[lane().nodes.length - 1].hash && item().hash !== get(displayedHash)
- });
- },
- [() => thumbUrl(item().hash)]
+ }),
+ () => thumbUrl(item().hash)
+ ]
);
- delegated("click", div_1, () => {
- if (get(compareDropdownOpen)) {
- if (item().hash !== get(displayedHash)) enterCompareWith(item().hash, item().filename || item().hash.slice(0, 8));
- return;
- }
- lineageJump(item().hash);
- exitCompare();
- });
+ delegated("click", div_1, (e) => handleNodeClick(e, item()));
event("mouseenter", div_1, (e) => showTip(e, item()));
event("mouseleave", div_1, hideTip);
delegated("contextmenu", div_1, (e) => {
hideTip();
openLineageMenu(e, item());
});
- event("load", img_1, scheduleMeasure);
append($$anchor2, div_1);
};
- var node_3 = child(div);
+ var node_6 = child(div);
{
- var consequent_9 = ($$anchor2) => {
- var div_4 = root_6();
+ var consequent_15 = ($$anchor2) => {
+ var div_7 = root_8();
let classes_2;
- var div_5 = child(div_4);
- var span = child(div_5);
- var text_2 = child(span);
- var node_4 = sibling(span, 2);
+ var div_8 = child(div_7);
+ var span = child(div_8);
+ var text_4 = child(span);
+ var node_7 = sibling(span, 2);
{
- var consequent_3 = ($$anchor3) => {
- var button_2 = root_7();
+ var consequent_5 = ($$anchor3) => {
+ var button_2 = root_9();
let classes_3;
- var text_3 = child(button_2);
+ var text_5 = child(button_2);
template_effect(() => {
classes_3 = set_class(button_2, 1, "pcr-lin-toggle svelte-5cciiw", null, classes_3, { on: get(lanesActive) });
set_attribute(button_2, "title", get(lanesActive) ? "Collapse the subway back to the strip" : "Expand every branch into a subway map");
- set_text(text_3, get(lanesActive) ? "⊟" : "⊞");
+ set_text(text_5, get(lanesActive) ? "⊟" : "⊞");
});
delegated("click", button_2, () => get(lanesActive) ? collapseAllLanes() : expandAll());
append($$anchor3, button_2);
};
- if_block(node_4, ($$render) => {
- if (get(hasBranches)) $$render(consequent_3);
+ if_block(node_7, ($$render) => {
+ if (get(hasBranches)) $$render(consequent_5);
});
}
- var node_5 = sibling(div_5, 2);
+ var node_8 = sibling(div_8, 2);
{
- var consequent_4 = ($$anchor3) => {
- var div_6 = root_8();
- var svg = child(div_6);
- each(svg, 21, () => get(connectorGeom), (g) => g.key, ($$anchor4, g) => {
- var path_1 = root_9();
+ var consequent_6 = ($$anchor3) => {
+ var button_3 = root_10();
+ delegated("click", button_3, clearFocus);
+ append($$anchor3, button_3);
+ };
+ if_block(node_8, ($$render) => {
+ if (get(focusRootHash)) $$render(consequent_6);
+ });
+ }
+ var node_9 = sibling(node_8, 2);
+ {
+ var consequent_7 = ($$anchor3) => {
+ var div_9 = root_11();
+ var text_6 = child(div_9);
+ var button_4 = sibling(text_6);
+ var button_5 = sibling(button_4, 2);
+ template_effect(() => set_text(text_6, `${get(selectedHashes).size ?? ""} selected `));
+ delegated("click", button_4, requestDeleteBatch);
+ delegated("click", button_5, clearSelection);
+ append($$anchor3, div_9);
+ };
+ if_block(node_9, ($$render) => {
+ if (get(selectedHashes).size) $$render(consequent_7);
+ });
+ }
+ var node_10 = sibling(node_9, 2);
+ {
+ var consequent_9 = ($$anchor3) => {
+ var div_10 = root_12();
+ var div_11 = child(div_10);
+ var svg = child(div_11);
+ var node_11 = child(svg);
+ each(node_11, 17, () => get(connectorsBase), (g) => g.key, ($$anchor4, g) => {
+ var path_1 = root_13();
template_effect(() => set_attribute(path_1, "d", get(g).d));
append($$anchor4, path_1);
});
- var node_6 = sibling(svg, 2);
- each(node_6, 17, () => get(laneRender).columns, (lane) => lane.startHash, ($$anchor4, lane) => {
- var div_7 = root_10();
- each(div_7, 21, () => get(lane).nodes, (item) => item.hash, ($$anchor5, item) => {
- lineageNode($$anchor5, () => get(item), () => get(lane));
- });
- template_effect(() => set_style(div_7, `margin-top: ${get(lane).topRow * LANE_ROW_H}px`));
- append($$anchor4, div_7);
+ var node_12 = sibling(node_11);
+ each(node_12, 17, () => get(connectorsActive), (g) => g.key, ($$anchor4, g) => {
+ var path_2 = root_14();
+ template_effect(() => set_attribute(path_2, "d", get(g).d));
+ append($$anchor4, path_2);
});
- bind_this(div_6, ($$value) => set(lanesWrapEl, $$value), () => get(lanesWrapEl));
+ var node_13 = sibling(svg, 2);
+ each(node_13, 17, () => get(laneRender).nodes, (node) => node.hash, ($$anchor4, node) => {
+ var div_12 = root_15();
+ let classes_4;
+ var node_14 = child(div_12);
+ {
+ var consequent_8 = ($$anchor5) => {
+ var button_6 = root_16();
+ var div_13 = child(button_6);
+ each(div_13, 20, () => get(node).preview, (p) => p, ($$anchor6, p) => {
+ var img_2 = root_17();
+ template_effect(($0) => set_attribute(img_2, "src", $0), [() => thumbUrl(p)]);
+ append($$anchor6, img_2);
+ });
+ var span_1 = sibling(div_13, 2);
+ var text_7 = child(span_1);
+ template_effect(() => {
+ set_attribute(button_6, "data-hash", get(node).hash);
+ set_attribute(button_6, "title", `${get(node).count ?? ""} older generation${get(node).count > 1 ? "s" : ""} — click to show more`);
+ set_text(text_7, `+${get(node).count ?? ""}`);
+ });
+ delegated("click", button_6, () => revealMore(get(node).parent));
+ append($$anchor5, button_6);
+ };
+ var alternate = ($$anchor5) => {
+ lineageNode($$anchor5, () => get(node).item, () => get(node));
+ };
+ if_block(node_14, ($$render) => {
+ if (get(node).kind === "bundle") $$render(consequent_8);
+ else $$render(alternate, -1);
+ });
+ }
+ template_effect(
+ ($0) => {
+ classes_4 = set_class(div_12, 1, "pcr-lin-treenode svelte-5cciiw", null, classes_4, $0);
+ set_style(div_12, `left: ${get(node).x * LANE_COL_W}px; top: ${get(node).depth * LANE_ROW_H}px`);
+ },
+ [
+ () => ({ dimmed: !get(activePath).nodes.has(get(node).hash) })
+ ]
+ );
+ append($$anchor4, div_12);
+ });
+ bind_this(div_11, ($$value) => set(lanesWrapEl, $$value), () => get(lanesWrapEl));
+ bind_this(div_10, ($$value) => set(treeScrollEl, $$value), () => get(treeScrollEl));
template_effect(() => {
+ set_style(div_11, `width: ${get(laneRender).width * LANE_COL_W}px; height: ${get(laneRender).height * LANE_ROW_H}px`);
set_attribute(svg, "width", get(connectorBox).w);
set_attribute(svg, "height", get(connectorBox).h);
});
- append($$anchor3, div_6);
+ append($$anchor3, div_10);
};
- var alternate_2 = ($$anchor3) => {
- var fragment_2 = root_12();
- var div_8 = first_child(fragment_2);
- each(div_8, 23, () => get(lineageDisplay), (d) => d.kind === "node" ? d.item.hash : `${d.kind}:${d.at}`, ($$anchor4, d, i) => {
- var fragment_3 = comment();
- var node_7 = first_child(fragment_3);
+ var alternate_3 = ($$anchor3) => {
+ var fragment_3 = root_19();
+ var div_14 = first_child(fragment_3);
+ each(div_14, 23, () => get(lineageDisplay), (d) => d.kind === "node" ? d.item.hash : `${d.kind}:${d.at}`, ($$anchor4, d, i) => {
+ var fragment_4 = comment();
+ var node_15 = first_child(fragment_4);
{
- var consequent_6 = ($$anchor5) => {
+ var consequent_12 = ($$anchor5) => {
const item = user_derived(() => get(d).item);
- var div_9 = root_14();
- let classes_4;
- var img_2 = child(div_9);
- var div_10 = sibling(img_2, 2);
+ var div_15 = root_21();
let classes_5;
- var node_8 = sibling(div_10, 2);
+ var img_3 = child(div_15);
+ var node_16 = sibling(img_3, 2);
{
- var consequent_5 = ($$anchor6) => {
- var div_11 = root_15();
- append($$anchor6, div_11);
+ var consequent_10 = ($$anchor6) => {
+ var div_16 = root_22();
+ append($$anchor6, div_16);
};
- if_block(node_8, ($$render) => {
- if (get(editDocLayers)[get(item).hash]) $$render(consequent_5);
+ var d_5 = user_derived(() => get(selectedHashes).has(get(item).hash));
+ if_block(node_16, ($$render) => {
+ if (get(d_5)) $$render(consequent_10);
+ });
+ }
+ var div_17 = sibling(node_16, 2);
+ let classes_6;
+ var node_17 = sibling(div_17, 2);
+ {
+ var consequent_11 = ($$anchor6) => {
+ var div_18 = root_23();
+ append($$anchor6, div_18);
+ };
+ if_block(node_17, ($$render) => {
+ if (get(editDocLayers)[get(item).hash]) $$render(consequent_11);
});
}
template_effect(
- ($0) => {
+ ($0, $1) => {
var _a2;
- classes_4 = set_class(div_9, 1, "pcr-viewer-lineage-node svelte-5cciiw", null, classes_4, {
- current: get(i) === get(lineageDisplayCurrentIdx),
- ancestor: get(i) < get(lineageDisplayCurrentIdx),
- descendant: get(i) > get(lineageDisplayCurrentIdx),
- branch: get(d).branch,
- orphaned: get(item).orphaned === 1,
- "compare-candidate": get(compareDropdownOpen) && get(item).hash !== get(displayedHash),
- "compare-left": get(compareMode) && get(item).hash === get(displayedHash),
- "compare-right": get(compareMode) && get(item).hash === get(compareTargetHash)
- });
- set_attribute(img_2, "src", $0);
- classes_5 = set_class(div_10, 1, "pcr-viewer-lineage-dot svelte-5cciiw", null, classes_5, {
+ classes_5 = set_class(div_15, 1, "pcr-viewer-lineage-node svelte-5cciiw", null, classes_5, $0);
+ set_attribute(img_3, "src", $1);
+ classes_6 = set_class(div_17, 1, "pcr-viewer-lineage-dot svelte-5cciiw", null, classes_6, {
"dot-root": get(item).hash === ((_a2 = get(lineageList)[0]) == null ? void 0 : _a2.hash),
"dot-current": get(i) === get(lineageDisplayCurrentIdx),
"dot-tip": get(i) === get(lineageDisplay).length - 1 && get(i) !== get(lineageDisplayCurrentIdx)
});
},
- [() => thumbUrl(get(item).hash)]
+ [
+ () => ({
+ current: get(i) === get(lineageDisplayCurrentIdx),
+ ancestor: get(i) < get(lineageDisplayCurrentIdx),
+ descendant: get(i) > get(lineageDisplayCurrentIdx),
+ branch: get(d).branch,
+ selected: get(selectedHashes).has(get(item).hash),
+ orphaned: get(item).orphaned === 1,
+ "compare-candidate": get(compareDropdownOpen) && get(item).hash !== get(displayedHash),
+ "compare-left": get(compareMode) && get(item).hash === get(displayedHash),
+ "compare-right": get(compareMode) && get(item).hash === get(compareTargetHash)
+ }),
+ () => thumbUrl(get(item).hash)
+ ]
);
- delegated("click", div_9, () => {
- if (get(compareDropdownOpen)) {
- if (get(item).hash !== get(displayedHash)) enterCompareWith(get(item).hash, get(item).filename || get(item).hash.slice(0, 8));
- return;
- }
- lineageJump(get(item).hash);
- exitCompare();
- });
- event("mouseenter", div_9, (e) => showTip(e, get(item)));
- event("mouseleave", div_9, hideTip);
- delegated("contextmenu", div_9, (e) => {
+ delegated("click", div_15, (e) => handleNodeClick(e, get(item)));
+ event("mouseenter", div_15, (e) => showTip(e, get(item)));
+ event("mouseleave", div_15, hideTip);
+ delegated("contextmenu", div_15, (e) => {
hideTip();
openLineageMenu(e, get(item));
});
- append($$anchor5, div_9);
+ append($$anchor5, div_15);
};
- var alternate_1 = ($$anchor5) => {
- var div_12 = root_16();
- let classes_6;
- var node_9 = child(div_12);
+ var alternate_2 = ($$anchor5) => {
+ var div_19 = root_24();
+ let classes_7;
+ var node_18 = child(div_19);
{
- var consequent_7 = ($$anchor6) => {
- var fragment_4 = root_17();
- var div_13 = first_child(fragment_4);
- each(div_13, 21, () => get(d).preview, (p) => p.hash, ($$anchor7, p) => {
- var img_3 = root_18();
- template_effect(($0) => set_attribute(img_3, "src", $0), [() => thumbUrl(get(p).hash)]);
- append($$anchor7, img_3);
+ var consequent_13 = ($$anchor6) => {
+ var fragment_5 = root_25();
+ var div_20 = first_child(fragment_5);
+ each(div_20, 21, () => get(d).preview, (p) => p.hash, ($$anchor7, p) => {
+ var img_4 = root_26();
+ template_effect(($0) => set_attribute(img_4, "src", $0), [() => thumbUrl(get(p).hash)]);
+ append($$anchor7, img_4);
});
- var span_1 = sibling(div_13, 2);
- var text_4 = child(span_1);
- template_effect(() => set_text(text_4, `+${get(d).count ?? ""}`));
- append($$anchor6, fragment_4);
+ var span_2 = sibling(div_20, 2);
+ var text_8 = child(span_2);
+ template_effect(() => set_text(text_8, `+${get(d).count ?? ""}`));
+ append($$anchor6, fragment_5);
};
- var alternate = ($$anchor6) => {
- var span_2 = root_19();
- append($$anchor6, span_2);
+ var alternate_1 = ($$anchor6) => {
+ var span_3 = root_27();
+ append($$anchor6, span_3);
};
- if_block(node_9, ($$render) => {
- if (get(d).kind === "bundle") $$render(consequent_7);
- else $$render(alternate, -1);
+ if_block(node_18, ($$render) => {
+ if (get(d).kind === "bundle") $$render(consequent_13);
+ else $$render(alternate_1, -1);
});
}
template_effect(() => {
- classes_6 = set_class(div_12, 1, "pcr-viewer-lineage-bundle svelte-5cciiw", null, classes_6, { expanded: get(d).kind === "collapse" });
- set_attribute(div_12, "title", get(d).kind === "collapse" ? "Collapse this branch" : `${get(d).count} other generation${get(d).count > 1 ? "s" : ""} branch off here — click to expand`);
+ classes_7 = set_class(div_19, 1, "pcr-viewer-lineage-bundle svelte-5cciiw", null, classes_7, { expanded: get(d).kind === "collapse" });
+ set_attribute(div_19, "title", get(d).kind === "collapse" ? "Collapse this branch" : `${get(d).count} other generation${get(d).count > 1 ? "s" : ""} branch off here — click to expand`);
});
- delegated("click", div_12, () => toggleBundle(get(d).at));
- append($$anchor5, div_12);
+ delegated("click", div_19, () => toggleBundle(get(d).at));
+ append($$anchor5, div_19);
};
- if_block(node_7, ($$render) => {
- if (get(d).kind === "node") $$render(consequent_6);
- else $$render(alternate_1, -1);
+ if_block(node_15, ($$render) => {
+ if (get(d).kind === "node") $$render(consequent_12);
+ else $$render(alternate_2, -1);
});
}
- append($$anchor4, fragment_3);
+ append($$anchor4, fragment_4);
});
- var node_10 = sibling(div_8, 2);
+ var node_19 = sibling(div_14, 2);
{
- var consequent_8 = ($$anchor4) => {
- var div_14 = root_20();
- var text_5 = child(div_14);
- template_effect(() => set_text(text_5, `${get(lineageHiddenCount) ?? ""} hidden`));
- append($$anchor4, div_14);
+ var consequent_14 = ($$anchor4) => {
+ var div_21 = root_28();
+ var text_9 = child(div_21);
+ template_effect(() => set_text(text_9, `${get(lineageHiddenCount) ?? ""} hidden`));
+ append($$anchor4, div_21);
};
- if_block(node_10, ($$render) => {
- if (get(lineageHiddenCount) > 0) $$render(consequent_8);
+ if_block(node_19, ($$render) => {
+ if (get(lineageHiddenCount) > 0) $$render(consequent_14);
});
}
- append($$anchor3, fragment_2);
+ append($$anchor3, fragment_3);
};
- if_block(node_5, ($$render) => {
- if (get(lanesActive)) $$render(consequent_4);
- else $$render(alternate_2, -1);
+ if_block(node_10, ($$render) => {
+ if (get(lanesActive)) $$render(consequent_9);
+ else $$render(alternate_3, -1);
});
}
template_effect(() => {
- classes_2 = set_class(div_4, 1, "pcr-viewer-lineage pcr-viewer-lineage-visible svelte-5cciiw", null, classes_2, { lanes: get(lanesActive) });
- set_text(text_2, `${get(lineageCurrentIdx) + 1} / ${get(lineageList).length ?? ""}`);
+ classes_2 = set_class(div_7, 1, "pcr-viewer-lineage pcr-viewer-lineage-visible svelte-5cciiw", null, classes_2, { lanes: get(lanesActive) });
+ set_style(div_7, get(lanesActive) ? `width: ${get(laneRender).width * LANE_COL_W + 24}px` : "");
+ set_text(text_4, `${get(lineageCurrentIdx) + 1} / ${get(lineageList).length ?? ""}`);
});
- append($$anchor2, div_4);
+ append($$anchor2, div_7);
};
- var alternate_3 = ($$anchor2) => {
- var div_15 = root_21();
- append($$anchor2, div_15);
+ var alternate_4 = ($$anchor2) => {
+ var div_22 = root_29();
+ append($$anchor2, div_22);
};
- if_block(node_3, ($$render) => {
- if (get(lineageList).length > 1) $$render(consequent_9);
- else $$render(alternate_3, -1);
+ if_block(node_6, ($$render) => {
+ if (get(lineageList).length > 1) $$render(consequent_15);
+ else $$render(alternate_4, -1);
});
}
- var node_11 = sibling(node_3, 2);
+ var node_20 = sibling(node_6, 2);
{
- var consequent_13 = ($$anchor2) => {
- var div_16 = root_22();
- var div_17 = child(div_16);
- var text_6 = child(div_17);
- var node_12 = sibling(div_17, 2);
+ var consequent_20 = ($$anchor2) => {
+ var div_23 = root_30();
+ var div_24 = child(div_23);
+ var text_10 = child(div_24);
+ var node_21 = sibling(div_24, 2);
{
- var consequent_10 = ($$anchor3) => {
- var div_18 = root_23();
- var text_7 = child(div_18);
- template_effect(($0) => set_text(text_7, $0), [() => timeAgo(get(hoverTip).item.created_at)]);
- append($$anchor3, div_18);
+ var consequent_16 = ($$anchor3) => {
+ var div_25 = root_31();
+ var text_11 = child(div_25);
+ template_effect(($0) => set_text(text_11, $0), [() => timeAgo(get(hoverTip).item.created_at)]);
+ append($$anchor3, div_25);
};
- if_block(node_12, ($$render) => {
- if (get(hoverTip).item.created_at) $$render(consequent_10);
+ if_block(node_21, ($$render) => {
+ if (get(hoverTip).item.created_at) $$render(consequent_16);
});
}
- var node_13 = sibling(node_12, 2);
+ var node_22 = sibling(node_21, 2);
{
- var consequent_11 = ($$anchor3) => {
- var div_19 = root_24();
- var text_8 = child(div_19);
- template_effect(() => set_text(text_8, `${get(hoverTip).item.width ?? ""}×${get(hoverTip).item.height ?? ""}`));
- append($$anchor3, div_19);
+ var consequent_17 = ($$anchor3) => {
+ var div_26 = root_32();
+ var text_12 = child(div_26);
+ template_effect(() => set_text(text_12, `${get(hoverTip).item.width ?? ""}×${get(hoverTip).item.height ?? ""}`));
+ append($$anchor3, div_26);
};
- if_block(node_13, ($$render) => {
- if (get(hoverTip).item.width && get(hoverTip).item.height) $$render(consequent_11);
+ if_block(node_22, ($$render) => {
+ if (get(hoverTip).item.width && get(hoverTip).item.height) $$render(consequent_17);
});
}
- var node_14 = sibling(node_13, 2);
+ var node_23 = sibling(node_22, 2);
{
- var consequent_12 = ($$anchor3) => {
- var div_20 = root_25();
- var text_9 = child(div_20);
- template_effect(() => set_text(text_9, `▦ ${get(editDocLayers)[get(hoverTip).item.hash] ?? ""} layers — opens layered in Edit`));
- append($$anchor3, div_20);
+ var consequent_18 = ($$anchor3) => {
+ const op = user_derived(() => get(lineageOps).get(get(hoverTip).item.hash));
+ var div_27 = root_33();
+ var text_13 = child(div_27);
+ template_effect(($0) => set_text(text_13, `▲ upscaled ${$0 ?? ""}from ${get(op).from.w ?? ""}×${get(op).from.h ?? ""}`), [
+ () => get(op).uniform ? fmtFactor(get(op).factor) + " " : ""
+ ]);
+ append($$anchor3, div_27);
};
- if_block(node_14, ($$render) => {
- if (get(editDocLayers)[get(hoverTip).item.hash]) $$render(consequent_12);
+ var d_6 = user_derived(() => {
+ var _a2;
+ return ((_a2 = get(lineageOps).get(get(hoverTip).item.hash)) == null ? void 0 : _a2.kind) === "upscale";
+ });
+ if_block(node_23, ($$render) => {
+ if (get(d_6)) $$render(consequent_18);
+ });
+ }
+ var node_24 = sibling(node_23, 2);
+ {
+ var consequent_19 = ($$anchor3) => {
+ var div_28 = root_34();
+ var text_14 = child(div_28);
+ template_effect(() => set_text(text_14, `▦ ${get(editDocLayers)[get(hoverTip).item.hash] ?? ""} layers — opens layered in Edit`));
+ append($$anchor3, div_28);
+ };
+ if_block(node_24, ($$render) => {
+ if (get(editDocLayers)[get(hoverTip).item.hash]) $$render(consequent_19);
});
}
template_effect(
($0) => {
- set_style(div_16, `top:${get(hoverTip).top ?? ""}px; left:${get(hoverTip).left ?? ""}px;`);
- set_text(text_6, $0);
+ set_style(div_23, `top:${get(hoverTip).top ?? ""}px; left:${get(hoverTip).left ?? ""}px;`);
+ set_text(text_10, $0);
},
[
() => {
@@ -14238,721 +14699,721 @@ ${negative}` : ""));
}
]
);
- append($$anchor2, div_16);
- };
- if_block(node_11, ($$render) => {
- if (get(hoverTip)) $$render(consequent_13);
- });
- }
- var div_21 = sibling(node_11, 2);
- var node_15 = child(div_21);
- {
- var consequent_14 = ($$anchor2) => {
- var div_22 = root_26();
- var text_10 = child(div_22);
- template_effect(() => set_text(text_10, `✨ ${get(newGenBanner) ?? ""}`));
- append($$anchor2, div_22);
- };
- if_block(node_15, ($$render) => {
- if (get(newGenBanner)) $$render(consequent_14);
- });
- }
- var node_16 = sibling(node_15, 2);
- {
- var consequent_15 = ($$anchor2) => {
- var div_23 = root_27();
append($$anchor2, div_23);
};
- if_block(node_16, ($$render) => {
- if (get(copyPathToast)) $$render(consequent_15);
+ if_block(node_20, ($$render) => {
+ if (get(hoverTip)) $$render(consequent_20);
});
}
- var node_17 = sibling(node_16, 2);
+ var div_29 = sibling(node_20, 2);
+ var node_25 = child(div_29);
{
- var consequent_24 = ($$anchor2) => {
- var fragment_5 = root_28();
- var node_18 = first_child(fragment_5);
- {
- var consequent_16 = ($$anchor3) => {
- var div_24 = root_29();
- append($$anchor3, div_24);
- };
- if_block(node_18, ($$render) => {
- if (!get(imageLoaded) && !get(imageError)) $$render(consequent_16);
- });
- }
- var img_4 = sibling(node_18, 2);
- let classes_7;
- bind_this(img_4, ($$value) => set(mainImageEl, $$value), () => get(mainImageEl));
- var node_19 = sibling(img_4, 2);
- {
- var consequent_17 = ($$anchor3) => {
- var img_5 = root_30();
- template_effect(() => {
- set_attribute(img_5, "src", get(compareImageUrl));
- set_style(img_5, `transform: translate(${get(panX) ?? ""}px, ${get(panY) ?? ""}px) scale(${get(zoom) ?? ""}); clip-path: inset(0 0 0 ${get(compareClipPercent) ?? ""}%);`);
- });
- append($$anchor3, img_5);
- };
- if_block(node_19, ($$render) => {
- if (get(compareMode) && get(compareImageUrl)) $$render(consequent_17);
- });
- }
- var node_20 = sibling(node_19, 2);
- {
- var consequent_20 = ($$anchor3) => {
- var div_25 = root_31();
- var div_26 = child(div_25);
- var text_11 = child(div_26);
- var node_21 = sibling(div_26, 2);
- {
- var consequent_19 = ($$anchor4) => {
- var fragment_6 = root_32();
- var button_3 = sibling(first_child(fragment_6), 2);
- var text_12 = child(button_3);
- var node_22 = sibling(button_3, 2);
- {
- var consequent_18 = ($$anchor5) => {
- var div_27 = root_33();
- var text_13 = child(div_27);
- template_effect(() => set_text(text_13, get(reattachMsg)));
- append($$anchor5, div_27);
- };
- if_block(node_22, ($$render) => {
- if (get(reattachMsg)) $$render(consequent_18);
- });
- }
- var input = sibling(node_22, 2);
- bind_this(input, ($$value) => set(reattachInputEl, $$value), () => get(reattachInputEl));
- template_effect(() => {
- button_3.disabled = get(reattaching);
- set_text(text_12, get(reattaching) ? "Verifying…" : "Locate file…");
- });
- delegated("click", button_3, () => {
- var _a2;
- return (_a2 = get(reattachInputEl)) == null ? void 0 : _a2.click();
- });
- delegated("change", input, handleReattachPick);
- append($$anchor4, fragment_6);
- };
- if_block(node_21, ($$render) => {
- if (get(canReattach)) $$render(consequent_19);
- });
- }
- template_effect(() => set_text(text_11, get(isOrphaned) ? "Source file deleted" : "Image not found"));
- append($$anchor3, div_25);
- };
- if_block(node_20, ($$render) => {
- if (get(imageError)) $$render(consequent_20);
- });
- }
- var node_23 = sibling(node_20, 2);
- {
- var consequent_23 = ($$anchor3) => {
- var fragment_7 = root_34();
- var div_28 = first_child(fragment_7);
- var div_29 = sibling(div_28, 2);
- var text_14 = child(div_29);
- var node_24 = sibling(text_14);
- {
- var consequent_21 = ($$anchor4) => {
- var div_30 = root_35();
- var text_15 = child(div_30);
- template_effect(($0) => set_text(text_15, $0), [() => timeAgo(get(imageInfo).created_at)]);
- append($$anchor4, div_30);
- };
- if_block(node_24, ($$render) => {
- var _a2;
- if ((_a2 = get(imageInfo)) == null ? void 0 : _a2.created_at) $$render(consequent_21);
- });
- }
- var div_31 = sibling(div_29, 2);
- var text_16 = child(div_31);
- var node_25 = sibling(text_16);
- {
- var consequent_22 = ($$anchor4) => {
- var div_32 = root_36();
- var text_17 = child(div_32);
- template_effect(($0) => set_text(text_17, $0), [() => timeAgo(get(compareTargetItem).created_at)]);
- append($$anchor4, div_32);
- };
- if_block(node_25, ($$render) => {
- var _a2;
- if ((_a2 = get(compareTargetItem)) == null ? void 0 : _a2.created_at) $$render(consequent_22);
- });
- }
- template_effect(() => {
- var _a2;
- set_style(div_28, `left: ${get(compareSliderPos) ?? ""}%;`);
- set_text(text_14, `${(((_a2 = get(imageInfo)) == null ? void 0 : _a2.filename) || "Current") ?? ""} `);
- set_text(text_16, `${(get(compareTargetLabel) || "Compare") ?? ""} `);
- });
- delegated("mousedown", div_28, handleSliderDown);
- append($$anchor3, fragment_7);
- };
- if_block(node_23, ($$render) => {
- if (get(compareMode)) $$render(consequent_23);
- });
- }
- template_effect(
- ($0) => {
- set_attribute(img_4, "src", $0);
- classes_7 = set_class(img_4, 1, "pcr-viewer-image svelte-5cciiw", null, classes_7, { hidden: !get(imageLoaded) });
- set_style(img_4, `transform: translate(${get(panX) ?? ""}px, ${get(panY) ?? ""}px) scale(${get(zoom) ?? ""})`);
- },
- [() => mainImageSrc(get(displayedHash))]
- );
- event("load", img_4, () => {
- set(imageLoaded, true);
- });
- event("error", img_4, () => {
- set(imageError, true);
- set(imageLoaded, false);
- });
- append($$anchor2, fragment_5);
+ var consequent_21 = ($$anchor2) => {
+ var div_30 = root_35();
+ var text_15 = child(div_30);
+ template_effect(() => set_text(text_15, `✨ ${get(newGenBanner) ?? ""}`));
+ append($$anchor2, div_30);
};
- if_block(node_17, ($$render) => {
- if (get(displayedHash)) $$render(consequent_24);
+ if_block(node_25, ($$render) => {
+ if (get(newGenBanner)) $$render(consequent_21);
});
}
- var node_26 = sibling(node_17, 2);
+ var node_26 = sibling(node_25, 2);
{
- var consequent_25 = ($$anchor2) => {
- var div_33 = root_37();
- delegated("pointerdown", div_33, (e) => e.stopPropagation());
- delegated("click", div_33, (e) => {
- e.stopPropagation();
- navigate(-1);
- });
- append($$anchor2, div_33);
+ var consequent_22 = ($$anchor2) => {
+ var div_31 = root_36();
+ append($$anchor2, div_31);
};
if_block(node_26, ($$render) => {
- if (get(currentIndex) > 0) $$render(consequent_25);
+ if (get(copyPathToast)) $$render(consequent_22);
});
}
var node_27 = sibling(node_26, 2);
{
- var consequent_26 = ($$anchor2) => {
- var div_34 = root_38();
- delegated("pointerdown", div_34, (e) => e.stopPropagation());
- delegated("click", div_34, (e) => {
+ var consequent_31 = ($$anchor2) => {
+ var fragment_6 = root_37();
+ var node_28 = first_child(fragment_6);
+ {
+ var consequent_23 = ($$anchor3) => {
+ var div_32 = root_38();
+ append($$anchor3, div_32);
+ };
+ if_block(node_28, ($$render) => {
+ if (!get(imageLoaded) && !get(imageError)) $$render(consequent_23);
+ });
+ }
+ var img_5 = sibling(node_28, 2);
+ let classes_8;
+ bind_this(img_5, ($$value) => set(mainImageEl, $$value), () => get(mainImageEl));
+ var node_29 = sibling(img_5, 2);
+ {
+ var consequent_24 = ($$anchor3) => {
+ var img_6 = root_39();
+ template_effect(() => {
+ set_attribute(img_6, "src", get(compareImageUrl));
+ set_style(img_6, `transform: translate(${get(panX) ?? ""}px, ${get(panY) ?? ""}px) scale(${get(zoom) ?? ""}); clip-path: inset(0 0 0 ${get(compareClipPercent) ?? ""}%);`);
+ });
+ append($$anchor3, img_6);
+ };
+ if_block(node_29, ($$render) => {
+ if (get(compareMode) && get(compareImageUrl)) $$render(consequent_24);
+ });
+ }
+ var node_30 = sibling(node_29, 2);
+ {
+ var consequent_27 = ($$anchor3) => {
+ var div_33 = root_40();
+ var div_34 = child(div_33);
+ var text_16 = child(div_34);
+ var node_31 = sibling(div_34, 2);
+ {
+ var consequent_26 = ($$anchor4) => {
+ var fragment_7 = root_41();
+ var button_7 = sibling(first_child(fragment_7), 2);
+ var text_17 = child(button_7);
+ var node_32 = sibling(button_7, 2);
+ {
+ var consequent_25 = ($$anchor5) => {
+ var div_35 = root_42();
+ var text_18 = child(div_35);
+ template_effect(() => set_text(text_18, get(reattachMsg)));
+ append($$anchor5, div_35);
+ };
+ if_block(node_32, ($$render) => {
+ if (get(reattachMsg)) $$render(consequent_25);
+ });
+ }
+ var input = sibling(node_32, 2);
+ bind_this(input, ($$value) => set(reattachInputEl, $$value), () => get(reattachInputEl));
+ template_effect(() => {
+ button_7.disabled = get(reattaching);
+ set_text(text_17, get(reattaching) ? "Verifying…" : "Locate file…");
+ });
+ delegated("click", button_7, () => {
+ var _a2;
+ return (_a2 = get(reattachInputEl)) == null ? void 0 : _a2.click();
+ });
+ delegated("change", input, handleReattachPick);
+ append($$anchor4, fragment_7);
+ };
+ if_block(node_31, ($$render) => {
+ if (get(canReattach)) $$render(consequent_26);
+ });
+ }
+ template_effect(() => set_text(text_16, get(isOrphaned) ? "Source file deleted" : "Image not found"));
+ append($$anchor3, div_33);
+ };
+ if_block(node_30, ($$render) => {
+ if (get(imageError)) $$render(consequent_27);
+ });
+ }
+ var node_33 = sibling(node_30, 2);
+ {
+ var consequent_30 = ($$anchor3) => {
+ var fragment_8 = root_43();
+ var div_36 = first_child(fragment_8);
+ var div_37 = sibling(div_36, 2);
+ var text_19 = child(div_37);
+ var node_34 = sibling(text_19);
+ {
+ var consequent_28 = ($$anchor4) => {
+ var div_38 = root_44();
+ var text_20 = child(div_38);
+ template_effect(($0) => set_text(text_20, $0), [() => timeAgo(get(imageInfo).created_at)]);
+ append($$anchor4, div_38);
+ };
+ if_block(node_34, ($$render) => {
+ var _a2;
+ if ((_a2 = get(imageInfo)) == null ? void 0 : _a2.created_at) $$render(consequent_28);
+ });
+ }
+ var div_39 = sibling(div_37, 2);
+ var text_21 = child(div_39);
+ var node_35 = sibling(text_21);
+ {
+ var consequent_29 = ($$anchor4) => {
+ var div_40 = root_45();
+ var text_22 = child(div_40);
+ template_effect(($0) => set_text(text_22, $0), [() => timeAgo(get(compareTargetItem).created_at)]);
+ append($$anchor4, div_40);
+ };
+ if_block(node_35, ($$render) => {
+ var _a2;
+ if ((_a2 = get(compareTargetItem)) == null ? void 0 : _a2.created_at) $$render(consequent_29);
+ });
+ }
+ template_effect(() => {
+ var _a2;
+ set_style(div_36, `left: ${get(compareSliderPos) ?? ""}%;`);
+ set_text(text_19, `${(((_a2 = get(imageInfo)) == null ? void 0 : _a2.filename) || "Current") ?? ""} `);
+ set_text(text_21, `${(get(compareTargetLabel) || "Compare") ?? ""} `);
+ });
+ delegated("mousedown", div_36, handleSliderDown);
+ append($$anchor3, fragment_8);
+ };
+ if_block(node_33, ($$render) => {
+ if (get(compareMode)) $$render(consequent_30);
+ });
+ }
+ template_effect(
+ ($0) => {
+ set_attribute(img_5, "src", $0);
+ classes_8 = set_class(img_5, 1, "pcr-viewer-image svelte-5cciiw", null, classes_8, { hidden: !get(imageLoaded) });
+ set_style(img_5, `transform: translate(${get(panX) ?? ""}px, ${get(panY) ?? ""}px) scale(${get(zoom) ?? ""})`);
+ },
+ [() => mainImageSrc(get(displayedHash))]
+ );
+ event("load", img_5, () => {
+ set(imageLoaded, true);
+ });
+ event("error", img_5, () => {
+ set(imageError, true);
+ set(imageLoaded, false);
+ });
+ append($$anchor2, fragment_6);
+ };
+ if_block(node_27, ($$render) => {
+ if (get(displayedHash)) $$render(consequent_31);
+ });
+ }
+ var node_36 = sibling(node_27, 2);
+ {
+ var consequent_32 = ($$anchor2) => {
+ var div_41 = root_46();
+ delegated("pointerdown", div_41, (e) => e.stopPropagation());
+ delegated("click", div_41, (e) => {
+ e.stopPropagation();
+ navigate(-1);
+ });
+ append($$anchor2, div_41);
+ };
+ if_block(node_36, ($$render) => {
+ if (get(currentIndex) > 0) $$render(consequent_32);
+ });
+ }
+ var node_37 = sibling(node_36, 2);
+ {
+ var consequent_33 = ($$anchor2) => {
+ var div_42 = root_47();
+ delegated("pointerdown", div_42, (e) => e.stopPropagation());
+ delegated("click", div_42, (e) => {
e.stopPropagation();
navigate(1);
});
- append($$anchor2, div_34);
+ append($$anchor2, div_42);
};
- if_block(node_27, ($$render) => {
- if (get(currentIndex) < images().length - 1) $$render(consequent_26);
+ if_block(node_37, ($$render) => {
+ if (get(currentIndex) < images().length - 1) $$render(consequent_33);
});
}
- bind_this(div_21, ($$value) => containerEl = $$value, () => containerEl);
- var div_35 = sibling(div_21, 2);
- var div_36 = child(div_35);
- var span_3 = child(div_36);
- var text_18 = child(span_3);
- var div_37 = sibling(span_3, 2);
- var node_28 = sibling(div_36, 2);
+ bind_this(div_29, ($$value) => containerEl = $$value, () => containerEl);
+ var div_43 = sibling(div_29, 2);
+ var div_44 = child(div_43);
+ var span_4 = child(div_44);
+ var text_23 = child(span_4);
+ var div_45 = sibling(span_4, 2);
+ var node_38 = sibling(div_44, 2);
{
- var consequent_29 = ($$anchor2) => {
- var div_38 = root_39();
- let classes_8;
- var div_39 = child(div_38);
- var div_40 = child(div_39);
- var node_29 = child(div_40);
- each(node_29, 17, () => get(visibleThumbs), (thumb) => thumb.hash, ($$anchor3, thumb) => {
- var div_41 = root_40();
- let classes_9;
- var img_6 = child(div_41);
+ var consequent_36 = ($$anchor2) => {
+ var div_46 = root_48();
+ let classes_9;
+ var div_47 = child(div_46);
+ var div_48 = child(div_47);
+ var node_39 = child(div_48);
+ each(node_39, 17, () => get(visibleThumbs), (thumb) => thumb.hash, ($$anchor3, thumb) => {
+ var div_49 = root_49();
+ let classes_10;
+ var img_7 = child(div_49);
template_effect(
($0) => {
- classes_9 = set_class(div_41, 1, "pcr-viewer-history-thumb svelte-5cciiw", null, classes_9, { current: get(thumb).globalIndex === get(currentIndex) });
- set_attribute(img_6, "src", $0);
+ classes_10 = set_class(div_49, 1, "pcr-viewer-history-thumb svelte-5cciiw", null, classes_10, { current: get(thumb).globalIndex === get(currentIndex) });
+ set_attribute(img_7, "src", $0);
},
[() => thumbUrl(get(thumb).hash)]
);
- delegated("click", div_41, () => jumpTo(get(thumb).globalIndex));
- append($$anchor3, div_41);
+ delegated("click", div_49, () => jumpTo(get(thumb).globalIndex));
+ append($$anchor3, div_49);
});
- var div_42 = sibling(div_40, 2);
- var svg_1 = child(div_42);
- var node_30 = child(svg_1);
+ var div_50 = sibling(div_48, 2);
+ var svg_1 = child(div_50);
+ var node_40 = child(svg_1);
{
- var consequent_27 = ($$anchor3) => {
- var path_2 = root_41();
- append($$anchor3, path_2);
- };
- var alternate_4 = ($$anchor3) => {
- var path_3 = root_42();
+ var consequent_34 = ($$anchor3) => {
+ var path_3 = root_50();
append($$anchor3, path_3);
};
- if_block(node_30, ($$render) => {
- if (get(historyExpanded)) $$render(consequent_27);
- else $$render(alternate_4, -1);
+ var alternate_5 = ($$anchor3) => {
+ var path_4 = root_51();
+ append($$anchor3, path_4);
+ };
+ if_block(node_40, ($$render) => {
+ if (get(historyExpanded)) $$render(consequent_34);
+ else $$render(alternate_5, -1);
});
}
- var div_43 = sibling(div_39, 2);
- var div_44 = child(div_43);
- var div_45 = sibling(div_44, 2);
- var node_31 = sibling(div_43, 2);
+ var div_51 = sibling(div_47, 2);
+ var div_52 = child(div_51);
+ var div_53 = sibling(div_52, 2);
+ var node_41 = sibling(div_51, 2);
{
- var consequent_28 = ($$anchor3) => {
- var div_46 = root_43();
- each(div_46, 23, images, (img) => img.hash, ($$anchor4, img, idx) => {
- var div_47 = root_44();
- let classes_10;
- var img_7 = child(div_47);
+ var consequent_35 = ($$anchor3) => {
+ var div_54 = root_52();
+ each(div_54, 23, images, (img) => img.hash, ($$anchor4, img, idx) => {
+ var div_55 = root_53();
+ let classes_11;
+ var img_8 = child(div_55);
template_effect(
($0) => {
- classes_10 = set_class(div_47, 1, "pcr-viewer-grid-thumb svelte-5cciiw", null, classes_10, { current: get(idx) === get(currentIndex) });
- set_attribute(img_7, "src", $0);
+ classes_11 = set_class(div_55, 1, "pcr-viewer-grid-thumb svelte-5cciiw", null, classes_11, { current: get(idx) === get(currentIndex) });
+ set_attribute(img_8, "src", $0);
},
[() => thumbUrl(get(img).hash)]
);
- delegated("click", div_47, () => jumpTo(get(idx)));
- append($$anchor4, div_47);
+ delegated("click", div_55, () => jumpTo(get(idx)));
+ append($$anchor4, div_55);
});
- bind_this(div_46, ($$value) => set(historyGridEl, $$value), () => get(historyGridEl));
- append($$anchor3, div_46);
+ bind_this(div_54, ($$value) => set(historyGridEl, $$value), () => get(historyGridEl));
+ append($$anchor3, div_54);
};
- if_block(node_31, ($$render) => {
- if (get(historyExpanded)) $$render(consequent_28);
+ if_block(node_41, ($$render) => {
+ if (get(historyExpanded)) $$render(consequent_35);
});
}
template_effect(() => {
- classes_8 = set_class(div_38, 1, "pcr-viewer-history svelte-5cciiw", null, classes_8, { expanded: get(historyExpanded) });
- set_attribute(div_42, "title", get(historyExpanded) ? "Collapse" : "Expand");
- set_style(div_44, `width: ${get(scrubberPercent) ?? ""}%`);
- set_style(div_45, `left: ${get(scrubberPercent) ?? ""}%`);
+ classes_9 = set_class(div_46, 1, "pcr-viewer-history svelte-5cciiw", null, classes_9, { expanded: get(historyExpanded) });
+ set_attribute(div_50, "title", get(historyExpanded) ? "Collapse" : "Expand");
+ set_style(div_52, `width: ${get(scrubberPercent) ?? ""}%`);
+ set_style(div_53, `left: ${get(scrubberPercent) ?? ""}%`);
});
- delegated("click", div_42, toggleGrid);
- delegated("mousedown", div_43, handleScrubberTrackDown);
- delegated("mousedown", div_45, handleScrubberHandleDown);
- append($$anchor2, div_38);
+ delegated("click", div_50, toggleGrid);
+ delegated("mousedown", div_51, handleScrubberTrackDown);
+ delegated("mousedown", div_53, handleScrubberHandleDown);
+ append($$anchor2, div_46);
};
- if_block(node_28, ($$render) => {
- if (images().length > 1) $$render(consequent_29);
+ if_block(node_38, ($$render) => {
+ if (images().length > 1) $$render(consequent_36);
});
}
- var div_48 = sibling(node_28, 2);
- var div_49 = child(div_48);
- var div_50 = child(div_49);
- var div_51 = child(div_50);
- var span_4 = sibling(child(div_51), 2);
- var text_19 = child(span_4);
- var node_32 = sibling(div_51, 2);
+ var div_56 = sibling(node_38, 2);
+ var div_57 = child(div_56);
+ var div_58 = child(div_57);
+ var div_59 = child(div_58);
+ var span_5 = sibling(child(div_59), 2);
+ var text_24 = child(span_5);
+ var node_42 = sibling(div_59, 2);
{
- var consequent_30 = ($$anchor2) => {
- var div_52 = root_45();
- each(div_52, 21, () => ZOOM_PRESETS, index, ($$anchor3, level) => {
- var div_53 = root_46();
- var text_20 = child(div_53);
- template_effect(() => set_text(text_20, `${get(level) ?? ""}%`));
- delegated("click", div_53, () => setZoomLevel(get(level)));
- append($$anchor3, div_53);
+ var consequent_37 = ($$anchor2) => {
+ var div_60 = root_54();
+ each(div_60, 21, () => ZOOM_PRESETS, index, ($$anchor3, level) => {
+ var div_61 = root_55();
+ var text_25 = child(div_61);
+ template_effect(() => set_text(text_25, `${get(level) ?? ""}%`));
+ delegated("click", div_61, () => setZoomLevel(get(level)));
+ append($$anchor3, div_61);
});
- append($$anchor2, div_52);
+ append($$anchor2, div_60);
};
- if_block(node_32, ($$render) => {
- if (get(zoomDropdownOpen)) $$render(consequent_30);
+ if_block(node_42, ($$render) => {
+ if (get(zoomDropdownOpen)) $$render(consequent_37);
});
}
- var input_1 = sibling(div_50, 2);
- var node_33 = sibling(div_49, 2);
+ var input_1 = sibling(div_58, 2);
+ var node_43 = sibling(div_57, 2);
{
- var consequent_33 = ($$anchor2) => {
- var div_54 = root_47();
- var div_55 = child(div_54);
- let classes_11;
- var node_34 = sibling(div_55, 2);
+ var consequent_40 = ($$anchor2) => {
+ var div_62 = root_56();
+ var div_63 = child(div_62);
+ let classes_12;
+ var node_44 = sibling(div_63, 2);
{
- var consequent_32 = ($$anchor3) => {
- var div_56 = root_48();
- var node_35 = child(div_56);
- each(node_35, 17, () => get(compareTargets), index, ($$anchor4, target) => {
- var div_57 = root_49();
- let classes_12;
- var text_21 = child(div_57);
+ var consequent_39 = ($$anchor3) => {
+ var div_64 = root_57();
+ var node_45 = child(div_64);
+ each(node_45, 17, () => get(compareTargets), index, ($$anchor4, target) => {
+ var div_65 = root_58();
+ let classes_13;
+ var text_26 = child(div_65);
template_effect(
($0) => {
- classes_12 = set_class(div_57, 1, "pcr-viewer-compare-dropdown-item svelte-5cciiw", null, classes_12, { active: get(compareTargetHash) === get(target).hash });
- set_text(text_21, $0);
+ classes_13 = set_class(div_65, 1, "pcr-viewer-compare-dropdown-item svelte-5cciiw", null, classes_13, { active: get(compareTargetHash) === get(target).hash });
+ set_text(text_26, $0);
},
[
() => get(target).filename || get(target).hash.slice(0, 8)
]
);
- delegated("click", div_57, () => enterCompareWith(get(target).hash, get(target).filename || get(target).hash.slice(0, 8)));
- append($$anchor4, div_57);
+ delegated("click", div_65, () => enterCompareWith(get(target).hash, get(target).filename || get(target).hash.slice(0, 8)));
+ append($$anchor4, div_65);
});
- var node_36 = sibling(node_35, 2);
+ var node_46 = sibling(node_45, 2);
{
- var consequent_31 = ($$anchor4) => {
- var fragment_8 = root_50();
- var div_58 = sibling(first_child(fragment_8), 2);
- delegated("click", div_58, exitCompare);
- append($$anchor4, fragment_8);
+ var consequent_38 = ($$anchor4) => {
+ var fragment_9 = root_59();
+ var div_66 = sibling(first_child(fragment_9), 2);
+ delegated("click", div_66, exitCompare);
+ append($$anchor4, fragment_9);
};
- if_block(node_36, ($$render) => {
- if (get(compareMode)) $$render(consequent_31);
+ if_block(node_46, ($$render) => {
+ if (get(compareMode)) $$render(consequent_38);
});
}
- append($$anchor3, div_56);
- };
- if_block(node_34, ($$render) => {
- if (get(compareDropdownOpen)) $$render(consequent_32);
- });
- }
- template_effect(() => classes_11 = set_class(div_55, 1, "pcr-viewer-toolbar-btn svelte-5cciiw", null, classes_11, { active: get(compareMode) || get(compareDropdownOpen) }));
- delegated("click", div_55, () => {
- set(compareDropdownOpen, !get(compareDropdownOpen));
- });
- append($$anchor2, div_54);
- };
- if_block(node_33, ($$render) => {
- if (get(hasCompareTargets)) $$render(consequent_33);
- });
- }
- var node_37 = sibling(node_33, 2);
- {
- if_block(node_37, ($$render) => {
- });
- }
- var node_38 = sibling(node_37, 2);
- {
- if_block(node_38, ($$render) => {
- });
- }
- var node_39 = sibling(node_38, 2);
- {
- var consequent_37 = ($$anchor2) => {
- var div_63 = root_53();
- var div_64 = child(div_63);
- var span_7 = sibling(child(div_64), 2);
- var text_24 = child(span_7);
- var node_40 = sibling(span_7, 2);
- {
- var consequent_36 = ($$anchor3) => {
- var span_8 = root_54();
- var text_25 = sibling(child(span_8));
- template_effect(() => {
- set_attribute(span_8, "title", `This image has ${get(editDocLayers)[get(displayedHash)] ?? ""} saved layers — Edit opens it layered, un-flattened`);
- set_text(text_25, ` ${get(editDocLayers)[get(displayedHash)] ?? ""}`);
- });
- append($$anchor3, span_8);
- };
- if_block(node_40, ($$render) => {
- if (get(editDocLayers)[get(displayedHash)]) $$render(consequent_36);
- });
- }
- template_effect(() => set_text(text_24, get(editPreparing) ? "Reading..." : "Edit"));
- delegated("click", div_64, () => {
- if (!get(editPreparing)) openEdit();
- });
- append($$anchor2, div_63);
- };
- if_block(node_39, ($$render) => {
- if (onEditSave()) $$render(consequent_37);
- });
- }
- var div_65 = sibling(node_39, 2);
- var node_41 = child(div_65);
- {
- var consequent_38 = ($$anchor2) => {
- var div_66 = root_55();
- var span_9 = sibling(child(div_66), 2);
- var text_26 = child(span_9);
- template_effect(() => set_text(text_26, get(deleting) ? "Deleting..." : "Delete"));
- delegated("click", div_66, requestDelete);
- append($$anchor2, div_66);
- };
- if_block(node_41, ($$render) => {
- if (onDelete()) $$render(consequent_38);
- });
- }
- var node_42 = sibling(node_41, 2);
- {
- var consequent_39 = ($$anchor2) => {
- var div_67 = root_56();
- delegated("click", div_67, openFolder);
- append($$anchor2, div_67);
- };
- if_block(node_42, ($$render) => {
- if (isLocalClient) $$render(consequent_39);
- });
- }
- var div_68 = sibling(node_42, 2);
- let classes_13;
- var span_10 = sibling(child(div_68), 2);
- var text_27 = child(span_10);
- var div_69 = sibling(div_48, 2);
- var node_43 = child(div_69);
- {
- var consequent_57 = ($$anchor2) => {
- var fragment_9 = root_57();
- var div_70 = sibling(first_child(fragment_9), 2);
- var div_71 = child(div_70);
- var node_44 = child(div_71);
- {
- var consequent_40 = ($$anchor3) => {
- var div_72 = root_58();
- var span_11 = sibling(child(div_72), 2);
- var text_28 = child(span_11);
- template_effect(() => set_text(text_28, `${get(imageInfo).width ?? ""} × ${get(imageInfo).height ?? ""}`));
- append($$anchor3, div_72);
+ append($$anchor3, div_64);
};
if_block(node_44, ($$render) => {
- if (get(imageInfo).width && get(imageInfo).height) $$render(consequent_40);
+ if (get(compareDropdownOpen)) $$render(consequent_39);
});
}
- var node_45 = sibling(node_44, 2);
- {
- var consequent_41 = ($$anchor3) => {
- var div_73 = root_59();
- var span_12 = sibling(child(div_73), 2);
- var text_29 = child(span_12);
- template_effect(($0) => set_text(text_29, $0), [
- () => get(imageInfo).file_size > 1048576 ? (get(imageInfo).file_size / 1048576).toFixed(1) + " MB" : Math.round(get(imageInfo).file_size / 1024) + " KB"
- ]);
- append($$anchor3, div_73);
- };
- if_block(node_45, ($$render) => {
- if (get(imageInfo).file_size) $$render(consequent_41);
- });
- }
- var node_46 = sibling(div_71, 2);
- {
- var consequent_42 = ($$anchor3) => {
- var div_74 = root_60();
- var span_13 = sibling(child(div_74), 2);
- var text_30 = child(span_13);
- template_effect(($0) => set_text(text_30, $0), [
- () => new Date(get(imageInfo).created_at * 1e3).toLocaleDateString()
- ]);
- append($$anchor3, div_74);
- };
- if_block(node_46, ($$render) => {
- if (get(imageInfo).created_at) $$render(consequent_42);
- });
- }
- var node_47 = sibling(node_46, 2);
- {
- var consequent_43 = ($$anchor3) => {
- var div_75 = root_61();
- var span_14 = sibling(child(div_75), 2);
- var text_31 = child(span_14);
- template_effect(() => set_text(text_31, get(imageInfo).filename));
- append($$anchor3, div_75);
- };
- if_block(node_47, ($$render) => {
- if (get(imageInfo).filename) $$render(consequent_43);
- });
- }
- var node_48 = sibling(node_47, 2);
- {
- var consequent_44 = ($$anchor3) => {
- var div_76 = root_62();
- append($$anchor3, div_76);
- };
- if_block(node_48, ($$render) => {
- if (get(isOrphaned)) $$render(consequent_44);
- });
- }
- var node_49 = sibling(div_70, 2);
- {
- var consequent_49 = ($$anchor3) => {
- const regionData = user_derived(() => parseRegions(get(imageInfo).regions));
- var fragment_10 = root_63();
- var div_77 = sibling(first_child(fragment_10), 2);
- var node_50 = child(div_77);
- {
- var consequent_46 = ($$anchor4) => {
- var fragment_11 = root_64();
- var node_51 = first_child(fragment_11);
- each(node_51, 17, () => get(regionData).regions, (region) => region.id, ($$anchor5, region) => {
- var div_78 = root_65();
- var span_15 = child(div_78);
- var text_32 = child(span_15);
- var p_1 = sibling(span_15, 2);
- var text_33 = child(p_1);
- template_effect(() => {
- set_text(text_32, `$${get(region).name ?? ""}`);
- set_text(text_33, get(region).text);
- });
- append($$anchor5, div_78);
- });
- var node_52 = sibling(node_51, 2);
- {
- var consequent_45 = ($$anchor5) => {
- var div_79 = root_66();
- var p_2 = sibling(child(div_79), 2);
- var text_34 = child(p_2);
- template_effect(() => set_text(text_34, get(regionData).global));
- append($$anchor5, div_79);
- };
- if_block(node_52, ($$render) => {
- if (get(regionData).global) $$render(consequent_45);
- });
- }
- append($$anchor4, fragment_11);
- };
- var consequent_47 = ($$anchor4) => {
- var div_80 = root_67();
- var p_3 = sibling(child(div_80), 2);
- var text_35 = child(p_3);
- template_effect(() => set_text(text_35, get(imageInfo).prompt));
- append($$anchor4, div_80);
- };
- if_block(node_50, ($$render) => {
- if (get(regionData)) $$render(consequent_46);
- else if (get(imageInfo).prompt) $$render(consequent_47, 1);
- });
- }
- var node_53 = sibling(node_50, 2);
- {
- var consequent_48 = ($$anchor4) => {
- var div_81 = root_68();
- var p_4 = sibling(child(div_81), 2);
- var text_36 = child(p_4);
- template_effect(() => set_text(text_36, get(imageInfo).negative));
- append($$anchor4, div_81);
- };
- if_block(node_53, ($$render) => {
- if (get(imageInfo).negative) $$render(consequent_48);
- });
- }
- append($$anchor3, fragment_10);
- };
- if_block(node_49, ($$render) => {
- if (get(imageInfo).prompt || get(imageInfo).negative) $$render(consequent_49);
- });
- }
- var node_54 = sibling(node_49, 2);
- {
- var consequent_56 = ($$anchor3) => {
- var fragment_12 = root_69();
- var div_82 = sibling(first_child(fragment_12), 2);
- var node_55 = child(div_82);
- {
- var consequent_50 = ($$anchor4) => {
- var div_83 = root_70();
- var span_16 = sibling(child(div_83), 2);
- var text_37 = child(span_16);
- template_effect(() => set_text(text_37, get(imageInfo).model));
- append($$anchor4, div_83);
- };
- if_block(node_55, ($$render) => {
- if (get(imageInfo).model) $$render(consequent_50);
- });
- }
- var node_56 = sibling(node_55, 2);
- {
- var consequent_51 = ($$anchor4) => {
- var div_84 = root_71();
- var span_17 = sibling(child(div_84), 2);
- var text_38 = child(span_17);
- template_effect(($0) => set_text(text_38, $0), [() => formatSeed(get(imageInfo).seed)]);
- delegated("click", div_84, copySeed);
- append($$anchor4, div_84);
- };
- if_block(node_56, ($$render) => {
- if (get(imageInfo).seed != null) $$render(consequent_51);
- });
- }
- var div_85 = sibling(node_56, 2);
- var node_57 = child(div_85);
- {
- var consequent_52 = ($$anchor4) => {
- var div_86 = root_72();
- var span_18 = sibling(child(div_86), 2);
- var text_39 = child(span_18);
- template_effect(() => set_text(text_39, get(imageInfo).steps));
- append($$anchor4, div_86);
- };
- if_block(node_57, ($$render) => {
- if (get(imageInfo).steps) $$render(consequent_52);
- });
- }
- var node_58 = sibling(node_57, 2);
- {
- var consequent_53 = ($$anchor4) => {
- var div_87 = root_73();
- var span_19 = sibling(child(div_87), 2);
- var text_40 = child(span_19);
- template_effect(() => set_text(text_40, get(imageInfo).cfg));
- append($$anchor4, div_87);
- };
- if_block(node_58, ($$render) => {
- if (get(imageInfo).cfg) $$render(consequent_53);
- });
- }
- var node_59 = sibling(node_58, 2);
- {
- var consequent_54 = ($$anchor4) => {
- var div_88 = root_74();
- var span_20 = sibling(child(div_88), 2);
- var text_41 = child(span_20);
- template_effect(() => set_text(text_41, get(imageInfo).sampler));
- append($$anchor4, div_88);
- };
- if_block(node_59, ($$render) => {
- if (get(imageInfo).sampler) $$render(consequent_54);
- });
- }
- var node_60 = sibling(node_59, 2);
- {
- var consequent_55 = ($$anchor4) => {
- var div_89 = root_75();
- var span_21 = sibling(child(div_89), 2);
- var text_42 = child(span_21);
- template_effect(() => set_text(text_42, get(imageInfo).denoise));
- append($$anchor4, div_89);
- };
- if_block(node_60, ($$render) => {
- if (get(imageInfo).denoise != null) $$render(consequent_55);
- });
- }
- append($$anchor3, fragment_12);
- };
- if_block(node_54, ($$render) => {
- if (get(imageInfo).model || get(imageInfo).seed != null || get(imageInfo).steps) $$render(consequent_56);
- });
- }
- append($$anchor2, fragment_9);
- };
- var consequent_58 = ($$anchor2) => {
- var div_90 = root_76();
- append($$anchor2, div_90);
+ template_effect(() => classes_12 = set_class(div_63, 1, "pcr-viewer-toolbar-btn svelte-5cciiw", null, classes_12, { active: get(compareMode) || get(compareDropdownOpen) }));
+ delegated("click", div_63, () => {
+ set(compareDropdownOpen, !get(compareDropdownOpen));
+ });
+ append($$anchor2, div_62);
};
if_block(node_43, ($$render) => {
- if (get(imageInfo)) $$render(consequent_57);
- else if (get(currentImage)) $$render(consequent_58, 1);
+ if (get(hasCompareTargets)) $$render(consequent_40);
+ });
+ }
+ var node_47 = sibling(node_43, 2);
+ {
+ if_block(node_47, ($$render) => {
+ });
+ }
+ var node_48 = sibling(node_47, 2);
+ {
+ if_block(node_48, ($$render) => {
+ });
+ }
+ var node_49 = sibling(node_48, 2);
+ {
+ var consequent_44 = ($$anchor2) => {
+ var div_71 = root_62();
+ var div_72 = child(div_71);
+ var span_8 = sibling(child(div_72), 2);
+ var text_29 = child(span_8);
+ var node_50 = sibling(span_8, 2);
+ {
+ var consequent_43 = ($$anchor3) => {
+ var span_9 = root_63();
+ var text_30 = sibling(child(span_9));
+ template_effect(() => {
+ set_attribute(span_9, "title", `This image has ${get(editDocLayers)[get(displayedHash)] ?? ""} saved layers — Edit opens it layered, un-flattened`);
+ set_text(text_30, ` ${get(editDocLayers)[get(displayedHash)] ?? ""}`);
+ });
+ append($$anchor3, span_9);
+ };
+ if_block(node_50, ($$render) => {
+ if (get(editDocLayers)[get(displayedHash)]) $$render(consequent_43);
+ });
+ }
+ template_effect(() => set_text(text_29, get(editPreparing) ? "Reading..." : "Edit"));
+ delegated("click", div_72, () => {
+ if (!get(editPreparing)) openEdit();
+ });
+ append($$anchor2, div_71);
+ };
+ if_block(node_49, ($$render) => {
+ if (onEditSave()) $$render(consequent_44);
+ });
+ }
+ var div_73 = sibling(node_49, 2);
+ var node_51 = child(div_73);
+ {
+ var consequent_45 = ($$anchor2) => {
+ var div_74 = root_64();
+ var span_10 = sibling(child(div_74), 2);
+ var text_31 = child(span_10);
+ template_effect(() => set_text(text_31, get(deleting) ? "Deleting..." : "Delete"));
+ delegated("click", div_74, requestDelete);
+ append($$anchor2, div_74);
+ };
+ if_block(node_51, ($$render) => {
+ if (onDelete()) $$render(consequent_45);
+ });
+ }
+ var node_52 = sibling(node_51, 2);
+ {
+ var consequent_46 = ($$anchor2) => {
+ var div_75 = root_65();
+ delegated("click", div_75, openFolder);
+ append($$anchor2, div_75);
+ };
+ if_block(node_52, ($$render) => {
+ if (isLocalClient) $$render(consequent_46);
+ });
+ }
+ var div_76 = sibling(node_52, 2);
+ let classes_14;
+ var span_11 = sibling(child(div_76), 2);
+ var text_32 = child(span_11);
+ var div_77 = sibling(div_56, 2);
+ var node_53 = child(div_77);
+ {
+ var consequent_64 = ($$anchor2) => {
+ var fragment_10 = root_66();
+ var div_78 = sibling(first_child(fragment_10), 2);
+ var div_79 = child(div_78);
+ var node_54 = child(div_79);
+ {
+ var consequent_47 = ($$anchor3) => {
+ var div_80 = root_67();
+ var span_12 = sibling(child(div_80), 2);
+ var text_33 = child(span_12);
+ template_effect(() => set_text(text_33, `${get(imageInfo).width ?? ""} × ${get(imageInfo).height ?? ""}`));
+ append($$anchor3, div_80);
+ };
+ if_block(node_54, ($$render) => {
+ if (get(imageInfo).width && get(imageInfo).height) $$render(consequent_47);
+ });
+ }
+ var node_55 = sibling(node_54, 2);
+ {
+ var consequent_48 = ($$anchor3) => {
+ var div_81 = root_68();
+ var span_13 = sibling(child(div_81), 2);
+ var text_34 = child(span_13);
+ template_effect(($0) => set_text(text_34, $0), [
+ () => get(imageInfo).file_size > 1048576 ? (get(imageInfo).file_size / 1048576).toFixed(1) + " MB" : Math.round(get(imageInfo).file_size / 1024) + " KB"
+ ]);
+ append($$anchor3, div_81);
+ };
+ if_block(node_55, ($$render) => {
+ if (get(imageInfo).file_size) $$render(consequent_48);
+ });
+ }
+ var node_56 = sibling(div_79, 2);
+ {
+ var consequent_49 = ($$anchor3) => {
+ var div_82 = root_69();
+ var span_14 = sibling(child(div_82), 2);
+ var text_35 = child(span_14);
+ template_effect(($0) => set_text(text_35, $0), [
+ () => new Date(get(imageInfo).created_at * 1e3).toLocaleDateString()
+ ]);
+ append($$anchor3, div_82);
+ };
+ if_block(node_56, ($$render) => {
+ if (get(imageInfo).created_at) $$render(consequent_49);
+ });
+ }
+ var node_57 = sibling(node_56, 2);
+ {
+ var consequent_50 = ($$anchor3) => {
+ var div_83 = root_70();
+ var span_15 = sibling(child(div_83), 2);
+ var text_36 = child(span_15);
+ template_effect(() => set_text(text_36, get(imageInfo).filename));
+ append($$anchor3, div_83);
+ };
+ if_block(node_57, ($$render) => {
+ if (get(imageInfo).filename) $$render(consequent_50);
+ });
+ }
+ var node_58 = sibling(node_57, 2);
+ {
+ var consequent_51 = ($$anchor3) => {
+ var div_84 = root_71();
+ append($$anchor3, div_84);
+ };
+ if_block(node_58, ($$render) => {
+ if (get(isOrphaned)) $$render(consequent_51);
+ });
+ }
+ var node_59 = sibling(div_78, 2);
+ {
+ var consequent_56 = ($$anchor3) => {
+ const regionData = user_derived(() => parseRegions(get(imageInfo).regions));
+ var fragment_11 = root_72();
+ var div_85 = sibling(first_child(fragment_11), 2);
+ var node_60 = child(div_85);
+ {
+ var consequent_53 = ($$anchor4) => {
+ var fragment_12 = root_73();
+ var node_61 = first_child(fragment_12);
+ each(node_61, 17, () => get(regionData).regions, (region) => region.id, ($$anchor5, region) => {
+ var div_86 = root_74();
+ var span_16 = child(div_86);
+ var text_37 = child(span_16);
+ var p_1 = sibling(span_16, 2);
+ var text_38 = child(p_1);
+ template_effect(() => {
+ set_text(text_37, `$${get(region).name ?? ""}`);
+ set_text(text_38, get(region).text);
+ });
+ append($$anchor5, div_86);
+ });
+ var node_62 = sibling(node_61, 2);
+ {
+ var consequent_52 = ($$anchor5) => {
+ var div_87 = root_75();
+ var p_2 = sibling(child(div_87), 2);
+ var text_39 = child(p_2);
+ template_effect(() => set_text(text_39, get(regionData).global));
+ append($$anchor5, div_87);
+ };
+ if_block(node_62, ($$render) => {
+ if (get(regionData).global) $$render(consequent_52);
+ });
+ }
+ append($$anchor4, fragment_12);
+ };
+ var consequent_54 = ($$anchor4) => {
+ var div_88 = root_76();
+ var p_3 = sibling(child(div_88), 2);
+ var text_40 = child(p_3);
+ template_effect(() => set_text(text_40, get(imageInfo).prompt));
+ append($$anchor4, div_88);
+ };
+ if_block(node_60, ($$render) => {
+ if (get(regionData)) $$render(consequent_53);
+ else if (get(imageInfo).prompt) $$render(consequent_54, 1);
+ });
+ }
+ var node_63 = sibling(node_60, 2);
+ {
+ var consequent_55 = ($$anchor4) => {
+ var div_89 = root_77();
+ var p_4 = sibling(child(div_89), 2);
+ var text_41 = child(p_4);
+ template_effect(() => set_text(text_41, get(imageInfo).negative));
+ append($$anchor4, div_89);
+ };
+ if_block(node_63, ($$render) => {
+ if (get(imageInfo).negative) $$render(consequent_55);
+ });
+ }
+ append($$anchor3, fragment_11);
+ };
+ if_block(node_59, ($$render) => {
+ if (get(imageInfo).prompt || get(imageInfo).negative) $$render(consequent_56);
+ });
+ }
+ var node_64 = sibling(node_59, 2);
+ {
+ var consequent_63 = ($$anchor3) => {
+ var fragment_13 = root_78();
+ var div_90 = sibling(first_child(fragment_13), 2);
+ var node_65 = child(div_90);
+ {
+ var consequent_57 = ($$anchor4) => {
+ var div_91 = root_79();
+ var span_17 = sibling(child(div_91), 2);
+ var text_42 = child(span_17);
+ template_effect(() => set_text(text_42, get(imageInfo).model));
+ append($$anchor4, div_91);
+ };
+ if_block(node_65, ($$render) => {
+ if (get(imageInfo).model) $$render(consequent_57);
+ });
+ }
+ var node_66 = sibling(node_65, 2);
+ {
+ var consequent_58 = ($$anchor4) => {
+ var div_92 = root_80();
+ var span_18 = sibling(child(div_92), 2);
+ var text_43 = child(span_18);
+ template_effect(($0) => set_text(text_43, $0), [() => formatSeed(get(imageInfo).seed)]);
+ delegated("click", div_92, copySeed);
+ append($$anchor4, div_92);
+ };
+ if_block(node_66, ($$render) => {
+ if (get(imageInfo).seed != null) $$render(consequent_58);
+ });
+ }
+ var div_93 = sibling(node_66, 2);
+ var node_67 = child(div_93);
+ {
+ var consequent_59 = ($$anchor4) => {
+ var div_94 = root_81();
+ var span_19 = sibling(child(div_94), 2);
+ var text_44 = child(span_19);
+ template_effect(() => set_text(text_44, get(imageInfo).steps));
+ append($$anchor4, div_94);
+ };
+ if_block(node_67, ($$render) => {
+ if (get(imageInfo).steps) $$render(consequent_59);
+ });
+ }
+ var node_68 = sibling(node_67, 2);
+ {
+ var consequent_60 = ($$anchor4) => {
+ var div_95 = root_82();
+ var span_20 = sibling(child(div_95), 2);
+ var text_45 = child(span_20);
+ template_effect(() => set_text(text_45, get(imageInfo).cfg));
+ append($$anchor4, div_95);
+ };
+ if_block(node_68, ($$render) => {
+ if (get(imageInfo).cfg) $$render(consequent_60);
+ });
+ }
+ var node_69 = sibling(node_68, 2);
+ {
+ var consequent_61 = ($$anchor4) => {
+ var div_96 = root_83();
+ var span_21 = sibling(child(div_96), 2);
+ var text_46 = child(span_21);
+ template_effect(() => set_text(text_46, get(imageInfo).sampler));
+ append($$anchor4, div_96);
+ };
+ if_block(node_69, ($$render) => {
+ if (get(imageInfo).sampler) $$render(consequent_61);
+ });
+ }
+ var node_70 = sibling(node_69, 2);
+ {
+ var consequent_62 = ($$anchor4) => {
+ var div_97 = root_84();
+ var span_22 = sibling(child(div_97), 2);
+ var text_47 = child(span_22);
+ template_effect(() => set_text(text_47, get(imageInfo).denoise));
+ append($$anchor4, div_97);
+ };
+ if_block(node_70, ($$render) => {
+ if (get(imageInfo).denoise != null) $$render(consequent_62);
+ });
+ }
+ append($$anchor3, fragment_13);
+ };
+ if_block(node_64, ($$render) => {
+ if (get(imageInfo).model || get(imageInfo).seed != null || get(imageInfo).steps) $$render(consequent_63);
+ });
+ }
+ append($$anchor2, fragment_10);
+ };
+ var consequent_65 = ($$anchor2) => {
+ var div_98 = root_85();
+ append($$anchor2, div_98);
+ };
+ if_block(node_53, ($$render) => {
+ if (get(imageInfo)) $$render(consequent_64);
+ else if (get(currentImage)) $$render(consequent_65, 1);
});
}
bind_this(div, ($$value) => viewerEl = $$value, () => viewerEl);
template_effect(() => {
- set_style(div_21, `cursor: ${get(isPanning) ? "grabbing" : "grab"}`);
- set_text(text_18, `${get(currentIndex) + 1} / ${images().length ?? ""}`);
- set_text(text_19, get(zoomDisplayText));
+ set_style(div_29, `cursor: ${get(isPanning) ? "grabbing" : "grab"}`);
+ set_text(text_23, `${get(currentIndex) + 1} / ${images().length ?? ""}`);
+ set_text(text_24, get(zoomDisplayText));
set_value(input_1, get(zoomSliderValue));
- classes_13 = set_class(div_68, 1, "pcr-viewer-toolbar-btn svelte-5cciiw", null, classes_13, { active: get(linkCopied) });
- set_text(text_27, get(linkCopied) ? "Copied!" : "Copy Link");
+ classes_14 = set_class(div_76, 1, "pcr-viewer-toolbar-btn svelte-5cciiw", null, classes_14, { active: get(linkCopied) });
+ set_text(text_32, get(linkCopied) ? "Copied!" : "Copy Link");
});
- event("wheel", div_21, handleWheel);
- delegated("pointerdown", div_21, handlePointerDown);
- delegated("pointermove", div_21, handlePointerMove);
- delegated("pointerup", div_21, handlePointerUp);
- delegated("dblclick", div_21, handleDblClick);
- delegated("click", div_37, function(...$$args) {
+ event("wheel", div_29, handleWheel);
+ delegated("pointerdown", div_29, handlePointerDown);
+ delegated("pointermove", div_29, handlePointerMove);
+ delegated("pointerup", div_29, handlePointerUp);
+ delegated("dblclick", div_29, handleDblClick);
+ delegated("click", div_45, function(...$$args) {
var _a2;
(_a2 = onClose()) == null ? void 0 : _a2.apply(this, $$args);
});
- delegated("click", div_51, () => {
+ delegated("click", div_59, () => {
set(zoomDropdownOpen, !get(zoomDropdownOpen));
});
delegated("input", input_1, handleZoomSlider);
- delegated("click", div_68, copyLink);
+ delegated("click", div_76, copyLink);
}
- var node_61 = sibling(div, 2);
+ var node_71 = sibling(div, 2);
{
let $0 = user_derived(() => get(displayedHash) || "");
let $1 = user_derived(() => {
@@ -14980,7 +15441,7 @@ ${negative}` : ""));
var _a2;
return get(upscaleFromEdit) ? ((_a2 = get(upscaleEditCtx)) == null ? void 0 : _a2.docHeight) || 0 : 0;
});
- UpscaleOptionsModal(node_61, {
+ UpscaleOptionsModal(node_71, {
get open() {
return get(upscaleModalOpen);
},
@@ -15032,7 +15493,7 @@ ${negative}` : ""));
}
});
}
- var node_62 = sibling(node_61, 2);
+ var node_72 = sibling(node_71, 2);
{
let $0 = user_derived(() => get(displayedHash) || "");
let $1 = user_derived(() => {
@@ -15073,7 +15534,7 @@ ${get(imageInfo).negative}` : "") : "");
let $9 = user_derived(() => get(inpaintFromEdit) ? get(inpaintMovedContent) : false);
let $10 = user_derived(() => get(inpaintFromEdit) ? get(inpaintEditMask) : null);
let $11 = user_derived(() => get(inpaintFromEdit) ? handleInpaintToEdit : null);
- InpaintModal(node_62, {
+ InpaintModal(node_72, {
get open() {
return get(inpaintModalOpen);
},
@@ -15152,7 +15613,7 @@ ${get(imageInfo).negative}` : "") : "");
}
});
}
- var node_63 = sibling(node_62, 2);
+ var node_73 = sibling(node_72, 2);
{
let $0 = user_derived(() => {
var _a2;
@@ -15180,7 +15641,7 @@ ${get(imageInfo).negative}` : "") : "");
let $8 = user_derived(() => onReposeCaps() && onReposeRun() && onMountPoser() && onInpaintUploadReference() ? openReposeFromEdit : null);
let $9 = user_derived(() => get(inpaintFromEdit) && get(inpaintModalOpen) || get(upscaleFromEdit) && get(upscaleModalOpen) || get(reposeModalOpen));
bind_this(
- EditModal(node_63, {
+ EditModal(node_73, {
get open() {
return get(editModalOpen);
},
@@ -15235,7 +15696,7 @@ ${get(imageInfo).negative}` : "") : "");
() => get(editModalRef)
);
}
- var node_64 = sibling(node_63, 2);
+ var node_74 = sibling(node_73, 2);
{
let $0 = user_derived(() => get(displayedHash) || "");
let $1 = user_derived(() => {
@@ -15254,7 +15715,7 @@ ${get(imageInfo).negative}` : "") : "");
var _a2;
return (_a2 = get(reposePrepared)) == null ? void 0 : _a2.caps;
});
- RePoseModal(node_64, {
+ RePoseModal(node_74, {
get open() {
return get(reposeModalOpen);
},
@@ -15297,25 +15758,49 @@ ${get(imageInfo).negative}` : "") : "");
}
});
}
- var node_65 = sibling(node_64, 2);
- ConfirmModal(node_65, {
- get open() {
- return get(confirmDeleteOpen);
- },
- title: "Delete image",
- message: "Permanently delete this image? This removes the file from disk and can't be undone.",
- confirmLabel: "Delete",
- onConfirm: handleDelete,
- onCancel: cancelDelete
- });
- var node_66 = sibling(node_65, 2);
+ var node_75 = sibling(node_74, 2);
{
- var consequent_59 = ($$anchor2) => {
+ let $0 = user_derived(() => {
+ var _a2;
+ return ((_a2 = get(deleteBatch)) == null ? void 0 : _a2.length) ? `Delete ${get(deleteBatch).length} images` : "Delete image";
+ });
+ let $1 = user_derived(() => {
+ var _a2;
+ return ((_a2 = get(deleteBatch)) == null ? void 0 : _a2.length) ? `Permanently delete these ${get(deleteBatch).length} selected images? This removes the files from disk and can't be undone.` : "Permanently delete this image? This removes the file from disk and can't be undone.";
+ });
+ let $2 = user_derived(() => {
+ var _a2;
+ return ((_a2 = get(deleteBatch)) == null ? void 0 : _a2.length) ? `Delete ${get(deleteBatch).length}` : "Delete";
+ });
+ ConfirmModal(node_75, {
+ get open() {
+ return get(confirmDeleteOpen);
+ },
+ get title() {
+ return get($0);
+ },
+ get message() {
+ return get($1);
+ },
+ get confirmLabel() {
+ return get($2);
+ },
+ onConfirm: handleDelete,
+ onCancel: cancelDelete
+ });
+ }
+ var node_76 = sibling(node_75, 2);
+ {
+ var consequent_66 = ($$anchor2) => {
{
let $0 = user_derived(() => get(lineageMenu).item ? canExpand(get(lineageMenu).item) : false);
- let $1 = user_derived(() => !!onEditSave());
- let $2 = user_derived(() => !!onDelete());
- let $3 = user_derived(() => {
+ let $1 = user_derived(() => {
+ var _a2;
+ return get(lineageMenu).item ? ((_a2 = get(lineageTree).childrenOf.get(get(lineageMenu).item.hash)) == null ? void 0 : _a2.length) > 0 && get(lineageMenu).item.hash !== get(focusRootHash) : false;
+ });
+ let $2 = user_derived(() => !!onEditSave());
+ let $3 = user_derived(() => !!onDelete());
+ let $4 = user_derived(() => {
var _a2;
return ((_a2 = get(lineageMenu).item) == null ? void 0 : _a2.orphaned) === 1;
});
@@ -15329,12 +15814,15 @@ ${get(imageInfo).negative}` : "") : "");
get canExpand() {
return get($0);
},
- get canEdit() {
+ get canFocus() {
return get($1);
},
- get canDelete() {
+ get canEdit() {
return get($2);
},
+ get canDelete() {
+ return get($3);
+ },
get isLocal() {
return isLocalClient;
},
@@ -15342,7 +15830,7 @@ ${get(imageInfo).negative}` : "") : "");
return isWindows;
},
get orphaned() {
- return get($3);
+ return get($4);
},
onAction: (action2) => lineageMenuAction(action2, get(lineageMenu).item),
onClose: () => {
@@ -15351,8 +15839,8 @@ ${get(imageInfo).negative}` : "") : "");
});
}
};
- if_block(node_66, ($$render) => {
- if (get(lineageMenu)) $$render(consequent_59);
+ if_block(node_76, ($$render) => {
+ if (get(lineageMenu)) $$render(consequent_66);
});
}
template_effect(() => set_style(div, `transform: translateY(${get(viewerTransformY) ?? ""}px); opacity: ${get(viewerOpacity) ?? ""}`));
diff --git a/js/main.js b/js/main.js
index 29e3800..567972b 100644
--- a/js/main.js
+++ b/js/main.js
@@ -140,7 +140,7 @@ let _executionWorkflowId = null;
let _lastPreviewUrl = null;
const OUTPUT_NODE_TYPES = new Set(["SaveImage", "PreviewImage"]);
-const KSAMPLER_TYPES = new Set(["KSampler", "KSamplerAdvanced", "PromptChain_RegionalDetailer", "SamplerCustomAdvanced", "UltimateSDUpscale"]);
+const KSAMPLER_TYPES = new Set(["KSampler", "KSamplerAdvanced", "PromptChain_RegionalDetailer", "SamplerCustomAdvanced", "UltimateSDUpscale", "PromptChain_IdeogramSampler"]);
const IMAGE_LATENT_TYPES = new Set(["IMAGE", "LATENT"]);
const LOAD_IMAGE_TYPES = new Set(["LoadImage"]);
diff --git a/nodes/ideogram_caption.py b/nodes/ideogram_caption.py
new file mode 100644
index 0000000..68c5df3
--- /dev/null
+++ b/nodes/ideogram_caption.py
@@ -0,0 +1,67 @@
+from __future__ import annotations
+
+import json
+import logging
+
+from comfy_api.latest import io
+
+logger = logging.getLogger("promptchain.ideogram_caption")
+
+
+def _envelope(text: str) -> str:
+ """Wrap plain text in Ideogram's minimal JSON caption envelope.
+
+ Ideogram 4 was trained on structured JSON captions; its docs state the
+ 'Image blocked by safety filter' false-positive rate is HIGHER for non-JSON
+ prompts. Measured on a benign prompt: plain text blocked 4/5 seeds, this
+ mechanical envelope (coherent text) blocked 1/5. The remaining ~20% is
+ cleared by the auto-retry sampler. (A real per-element deconstruction via an
+ LLM gets to 0/5 — that's the optional upgrade when a local LLM is set up.)
+ """
+ t = (text or "").strip()
+ return json.dumps(
+ {"high_level_description": t,
+ "compositional_deconstruction": {"background": t, "elements": []}},
+ ensure_ascii=False)
+
+
+class IdeogramCaptionNode(io.ComfyNode):
+ """Turn a PromptChain prompt into an Ideogram-friendly JSON caption.
+
+ Sits between PromptChain's `positive` output and CLIPTextEncode in the
+ Ideogram template. JSON-shaped prompts trip Ideogram's stochastic safety
+ refusal far less often than plain text. If the user already wrote valid JSON,
+ it's passed through untouched.
+ """
+
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="PromptChain_IdeogramCaption",
+ display_name="Prompt Chain Ideogram Caption",
+ category="promptchain",
+ inputs=[
+ io.String.Input("text", default="", force_input=True,
+ tooltip="Wire to Prompt Chain's 'positive' output."),
+ io.Combo.Input("mode", options=["envelope", "off"], default="envelope",
+ tooltip="envelope = wrap plain text in Ideogram's JSON caption "
+ "(reduces the model's false safety blocks); off = passthrough."),
+ ],
+ outputs=[
+ io.String.Output("text"),
+ ],
+ )
+
+ @classmethod
+ def execute(cls, text: str = "", mode: str = "envelope") -> io.NodeOutput:
+ s = (text or "").strip()
+ if mode == "off" or not s:
+ return io.NodeOutput(text)
+ # Already valid JSON (user hand-wrote a caption) — leave it alone.
+ if s.startswith("{") and s.endswith("}"):
+ try:
+ json.loads(s)
+ return io.NodeOutput(text)
+ except json.JSONDecodeError:
+ pass
+ return io.NodeOutput(_envelope(text))
diff --git a/nodes/ideogram_sampler.py b/nodes/ideogram_sampler.py
new file mode 100644
index 0000000..9306e08
--- /dev/null
+++ b/nodes/ideogram_sampler.py
@@ -0,0 +1,97 @@
+from __future__ import annotations
+
+import logging
+
+import torch
+
+import comfy.sample
+import comfy.model_management
+import comfy.utils
+import latent_preview
+from comfy_api.latest import io
+
+logger = logging.getLogger("promptchain.ideogram_sampler")
+
+
+class IdeogramSamplerNode(io.ComfyNode):
+ """Ideogram 4 sampler with automatic retry past the model's stochastic
+ 'Image blocked by safety filter' refusal.
+
+ Ideogram 4's open weights emit a near-uniform gray refusal frame on a
+ fraction of seeds — an officially-acknowledged false-positive (higher for
+ non-JSON prompts), baked into the weights and not disableable. It's
+ seed-dependent, so re-rolling the noise seed escapes it. This node samples,
+ VAE-decodes, and if the decoded image is a near-flat refusal frame (pixel
+ std below `block_std`), it re-samples with the next seed up to `max_retries`
+ times. Measured separation: refusal frames ~0.04 std, real images ~0.15-0.33
+ (0-1 scale), so the default 0.08 sits cleanly between.
+
+ Drop-in replacement for SamplerCustomAdvanced + VAEDecode (returns IMAGE).
+ """
+
+ @classmethod
+ def define_schema(cls):
+ return io.Schema(
+ node_id="PromptChain_IdeogramSampler",
+ display_name="Prompt Chain Ideogram Sampler (auto-retry)",
+ category="promptchain",
+ inputs=[
+ io.Noise.Input("noise"),
+ io.Guider.Input("guider"),
+ io.Sampler.Input("sampler"),
+ io.Sigmas.Input("sigmas"),
+ io.Latent.Input("latent_image"),
+ io.Vae.Input("vae"),
+ io.Int.Input("max_retries", default=4, min=0, max=16,
+ tooltip="Extra re-rolls if the model returns its gray "
+ "'Image blocked by safety filter' frame."),
+ io.Float.Input("block_std", default=0.08, min=0.0, max=1.0, step=0.005,
+ tooltip="Pixel-std threshold below which a decoded image is "
+ "treated as a blocked/refusal frame (0-1 scale)."),
+ ],
+ outputs=[
+ io.Image.Output("IMAGE"),
+ ],
+ )
+
+ @classmethod
+ def execute(cls, noise, guider, sampler, sigmas, latent_image,
+ vae, max_retries: int = 4, block_std: float = 0.08) -> io.NodeOutput:
+ base = latent_image
+ samples_in = comfy.sample.fix_empty_latent_channels(
+ guider.model_patcher, base["samples"],
+ base.get("downscale_ratio_spacial", None),
+ base.get("downscale_ratio_temporal", None))
+ noise_mask = base.get("noise_mask", None)
+ batch_inds = base.get("batch_index", None)
+ base_seed = int(getattr(noise, "seed", 0))
+ disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED
+
+ last_image = None
+ for attempt in range(max_retries + 1):
+ seed = base_seed + attempt # attempt 0 keeps the user's seed
+ noise_tensor = comfy.sample.prepare_noise(samples_in, seed, batch_inds)
+ x0_output = {}
+ callback = latent_preview.prepare_callback(
+ guider.model_patcher, sigmas.shape[-1] - 1, x0_output)
+ samples = guider.sample(noise_tensor, samples_in, sampler, sigmas,
+ denoise_mask=noise_mask, callback=callback,
+ disable_pbar=disable_pbar, seed=seed)
+ samples = samples.to(comfy.model_management.intermediate_device())
+
+ image = vae.decode(samples)
+ if len(image.shape) == 5: # video VAE — flatten batches
+ image = image.reshape(-1, image.shape[-3], image.shape[-2], image.shape[-1])
+ last_image = image
+
+ std = float(image.float().std().item())
+ blocked = std < block_std
+ logger.info("[IdeogramSampler] attempt %d/%d seed=%d std=%.4f -> %s",
+ attempt, max_retries, seed, std,
+ "BLOCKED (retry)" if blocked else "ok")
+ if not blocked:
+ return io.NodeOutput(image)
+
+ logger.warning("[IdeogramSampler] still blocked after %d attempts; "
+ "returning last frame", max_retries + 1)
+ return io.NodeOutput(last_image)
diff --git a/svelte-src/components/ImageViewer.svelte b/svelte-src/components/ImageViewer.svelte
index b1c0c72..e869968 100644
--- a/svelte-src/components/ImageViewer.svelte
+++ b/svelte-src/components/ImageViewer.svelte
@@ -6,7 +6,7 @@
import RePoseModal from "./RePoseModal.svelte";
import ConfirmModal from "./sidebar/ConfirmModal.svelte";
import LineageContextMenu from "./LineageContextMenu.svelte";
- import { buildRender, ancestry } from "../lib/lineage-lanes.js";
+ import { buildRender, ancestry, CHILDREN_PER_ROW_CAP, CHILDREN_REVEAL_STEP } from "../lib/lineage-lanes.js";
import { buildFigureRegions, matchRegionByOverlap } from "../lib/region-binding.js";
let {
@@ -270,6 +270,29 @@
return { byHash, childrenOf, roots };
})());
+ // What operation produced a node, inferred ONLY from data we hold per family node
+ // (dimensions vs its parent). Upscale is the one transform we can call with high
+ // confidence here; re-roll/variation/repose would need a per-node op field from the
+ // server (no seed/prompt on lineage records) — left unlabeled rather than guessed.
+ // Edits are already surfaced separately via the editDocLayers (▦) badge.
+ let lineageOps = $derived.by(() => {
+ const { byHash } = lineageTree;
+ const ops = new Map();
+ for (const item of lineageList) {
+ const p = item?.parent_hash ? byHash.get(item.parent_hash) : null;
+ if (!p || !item.width || !item.height || !p.width || !p.height) continue;
+ if (item.width > p.width * 1.2 && item.height > p.height * 1.2) {
+ const wf = item.width / p.width, hf = item.height / p.height;
+ // only assert a single multiplier when both axes scaled together; otherwise
+ // it's a non-uniform grow — show the badge without an overstated factor
+ const uniform = Math.abs(wf - hf) < 0.05;
+ ops.set(item.hash, { kind: "upscale", factor: wf, uniform, from: { w: p.width, h: p.height } });
+ }
+ }
+ return ops;
+ });
+ const fmtFactor = (f) => (Math.abs(f - Math.round(f)) < 0.05 ? `${Math.round(f)}×` : `${f.toFixed(1)}×`);
+
let lineageDisplay = $derived((() => {
const asNodes = (list) => list.map(item => ({ kind: "node", item }));
if (lineageList.length < 2) return asNodes(lineageList);
@@ -361,11 +384,83 @@
// so navigating between nodes never re-partitions the lanes, and the displayed
// image is always on column 0 (anchored on the anchor's root→tip spine).
let brokenOut = $state(new Set());
+ // Per-parent override of how many siblings render individually before the rest
+ // fold into a "+N" bundle chip; bumped by CHILDREN_REVEAL_STEP on a chip click.
+ let lineageRevealed = $state(new Map());
let subwayAnchor = $derived(
lineageFocusHash && lineageTree.byHash.has(lineageFocusHash) ? lineageFocusHash : displayedHash
);
- let laneRender = $derived(buildRender(lineageTree, subwayAnchor, brokenOut));
- let lanesActive = $derived(brokenOut.size > 0 && lineageList.length > 1);
+ // "Focus this branch": a VIEW-ONLY re-root. buildRender renders only this hash's
+ // subtree (no parent_hash mutation, no effect on which image is displayed/loaded).
+ let focusRootHash = $state(null);
+ let laneRender = $derived(buildRender(lineageTree, subwayAnchor, brokenOut,
+ { displayedHash, revealed: lineageRevealed, rootHash: focusRootHash }));
+ let lanesActive = $derived((brokenOut.size > 0 || !!focusRootHash) && lineageList.length > 1);
+ // is `desc` the focus root or a descendant of it? (guarded parent_hash climb)
+ function isInSubtree(desc, rootHash) {
+ const { byHash } = lineageTree;
+ const seen = new Set();
+ let cur = desc;
+ while (cur && byHash.has(cur) && !seen.has(cur)) {
+ seen.add(cur);
+ if (cur === rootHash) return true;
+ cur = byHash.get(cur).parent_hash;
+ }
+ return false;
+ }
+ // Drop a stale focus when it leaves the family OR when navigation (arrows / strip /
+ // timeline) moves the displayed image out of the focused subtree — otherwise the
+ // current image would have no node in the focus-only tree and nothing would mark
+ // "you are here". Writes focusRootHash only in the clear branch, so it can't loop.
+ $effect(() => {
+ if (!focusRootHash) return;
+ if (!lineageTree.byHash.has(focusRootHash) || !isInSubtree(displayedHash, focusRootHash)) focusRootHash = null;
+ });
+ function focusBranch(hash) {
+ if (!hash || !lineageTree.byHash.has(hash)) return;
+ // land on the branch root first if the displayed image is outside the subtree,
+ // so the invalidation effect doesn't immediately clear the focus we just set.
+ if (!isInSubtree(displayedHash, hash)) lineageJump(hash);
+ focusRootHash = hash;
+ }
+ function clearFocus() { focusRootHash = null; }
+
+ // ── per-family expansion persistence ──
+ // Remembers which branches the user expanded, keyed by the IMMUTABLE family root
+ // (a content sha256), so reopening a family restores its tree shape. This is a
+ // pure UI overlay: it stores ONLY brokenOut/revealed and never writes
+ // displayedHash or touches how/which image loads — the family you land on is
+ // always decided by the invoke point, and the materialize effect still guarantees
+ // the displayed image is shown regardless of what was restored.
+ const LANE_PREFS_KEY = "pcr.viewer.lanePrefs.v1";
+ let familyRootHash = $derived(
+ lineageTree.byHash.has(displayedHash) ? ancestry(lineageTree, displayedHash).root : null
+ );
+ let hydratedRoot = null; // plain let: writing it must not re-trigger the hydrate effect
+ function readLanePrefs() {
+ try { return JSON.parse(localStorage.getItem(LANE_PREFS_KEY) || "{}") || {}; } catch { return {}; }
+ }
+ // hydrate once when we enter a family; restore only branches that still exist
+ $effect(() => {
+ const root = familyRootHash;
+ if (!root || root === hydratedRoot) return;
+ hydratedRoot = root;
+ const prefs = readLanePrefs()[root];
+ brokenOut = new Set((prefs?.brokenOut || []).filter(h => lineageTree.byHash.has(h)));
+ lineageRevealed = new Map((prefs?.revealed || []).filter(([k]) => lineageTree.byHash.has(k)));
+ });
+ // persist after hydration; an empty overlay drops the entry rather than storing {}
+ $effect(() => {
+ const root = familyRootHash;
+ const bo = [...brokenOut], rv = [...lineageRevealed];
+ if (!root || root !== hydratedRoot) return;
+ try {
+ const all = readLanePrefs();
+ if (bo.length || rv.length) all[root] = { brokenOut: bo, revealed: rv };
+ else delete all[root];
+ localStorage.setItem(LANE_PREFS_KEY, JSON.stringify(all));
+ } catch {}
+ });
// The child that CONTINUES a node's lane — "expand branches" peels the SIBLINGS,
// never this. Matches buildLane's primary rule (spine-ward child, else oldest).
@@ -398,7 +493,43 @@
}
brokenOut = next;
}
- function collapseAllLanes() { brokenOut = new Set(); }
+ function collapseAllLanes() { brokenOut = new Set(); lineageRevealed = new Map(); focusRootHash = null; }
+
+ // ── multi-select (ctrl/⌘/shift-click thumbs) → batch delete ──
+ let selectedHashes = $state(new Set());
+ // Selection is scoped to the visible family: drop any selected hash that isn't in
+ // the current family tree (navigating to another family via arrows/timeline/gallery
+ // must never leave a stale selection that a later [Del] would silently tombstone).
+ // Reads selectedHashes but only writes a strictly smaller set, so it can't loop.
+ $effect(() => {
+ if (!selectedHashes.size) return;
+ const { byHash } = lineageTree;
+ const kept = [...selectedHashes].filter(h => byHash.has(h));
+ if (kept.length !== selectedHashes.size) selectedHashes = new Set(kept);
+ });
+ function toggleSelect(hash) {
+ const next = new Set(selectedHashes);
+ if (next.has(hash)) next.delete(hash); else next.add(hash);
+ selectedHashes = next;
+ }
+ function clearSelection() { if (selectedHashes.size) selectedHashes = new Set(); }
+ // Shared click for every lineage thumb (strip + tree): modifier-click multi-selects;
+ // a plain click clears any selection and navigates.
+ function handleNodeClick(e, item) {
+ if (compareDropdownOpen) {
+ if (item.hash !== displayedHash) enterCompareWith(item.hash, item.filename || item.hash.slice(0, 8));
+ return;
+ }
+ if (e.ctrlKey || e.metaKey || e.shiftKey) { toggleSelect(item.hash); return; }
+ clearSelection();
+ lineageJump(item.hash); exitCompare();
+ }
+ // Page more of a folded row's oldest siblings back into individual columns.
+ function revealMore(parentHash) {
+ const next = new Map(lineageRevealed);
+ next.set(parentHash, (next.get(parentHash) ?? CHILDREN_PER_ROW_CAP - 1) + CHILDREN_REVEAL_STEP);
+ lineageRevealed = next;
+ }
// the family has at least one fork worth fanning out
let hasBranches = $derived((() => {
for (const kids of lineageTree.childrenOf.values()) if (kids.length > 1) return true;
@@ -422,7 +553,7 @@
// only ever ADDS new roots (never loops).
$effect(() => {
if (!lanesActive) return;
- if (laneRender.columns.some(c => c.nodes.some(n => n.hash === displayedHash))) return;
+ if (laneRender.nodes.some(n => n.hash === displayedHash)) return;
const { byHash, childrenOf } = lineageTree;
const a = ancestry(lineageTree, subwayAnchor);
const toBreak = [];
@@ -451,13 +582,36 @@
// recompute only on expand/fold/resize/thumbnail-load — never on scroll, never on
// a timer (event/observer-driven per CLAUDE.md).
let lanesWrapEl = $state(null);
+ let treeScrollEl = $state(null);
let connectorGeom = $state([]);
let connectorBox = $state({ w: 0, h: 0 });
+ // The displayed image's root→self lineage — the active branch. One guarded walk
+ // yields both the edge keys (`parent>child`, for the blue connector highlight)
+ // and the node set (for dimming everything off the active branch). Derived (not
+ // measured) so it re-resolves on navigation without recomputing geometry.
+ let activePath = $derived.by(() => {
+ const nodes = new Set(), edges = new Set();
+ const { byHash } = lineageTree;
+ const seen = new Set(); // cheap defense against malformed data looping the browser
+ let cur = displayedHash;
+ while (cur && byHash.has(cur) && !seen.has(cur)) {
+ seen.add(cur);
+ nodes.add(cur);
+ const p = byHash.get(cur).parent_hash;
+ if (p && byHash.has(p)) edges.add(`${p}>${cur}`);
+ cur = p;
+ }
+ return { nodes, edges };
+ });
+ // split so the highlighted paths paint over the dim ones
+ let connectorsBase = $derived(connectorGeom.filter(g => !activePath.edges.has(g.key)));
+ let connectorsActive = $derived(connectorGeom.filter(g => activePath.edges.has(g.key)));
let measureScheduled = false;
- // Row pitch for vertically offsetting broken-out columns so children sit one
- // generation below their parent. MUST match .pcr-viewer-lineage-node height (64px)
- // + .pcr-lin-col gap (22px).
+ // Tree layout pitch: a row is one generation (node 64px + 22px gap), a column
+ // one sibling slot (node 64px + 24px gap). Each lineage node is placed at
+ // left = x*LANE_COL_W, top = depth*LANE_ROW_H.
const LANE_ROW_H = 86;
+ const LANE_COL_W = 88;
// Layout coords relative to a content root — independent of scroll AND of CSS
// transform, so the same measurement serves the P3 zoom panel unchanged.
function contentXY(el, root) {
@@ -497,8 +651,8 @@
}
$effect(() => {
if (!lanesActive) return;
- const cols = laneRender.columns; // read → track expand/fold/family changes
- if (cols) scheduleMeasure();
+ const ns = laneRender.nodes; // read → track expand/fold/family changes
+ if (ns) scheduleMeasure();
});
$effect(() => {
if (!lanesWrapEl) return;
@@ -506,6 +660,36 @@
ro.observe(lanesWrapEl);
return () => ro.disconnect();
});
+ // "You are here": after expand/fold/navigation, bring the displayed image into
+ // view — but only if it has drifted out of (or near the edge of) the viewport, so
+ // we never yank a node the user can already see. Reads displayedHash + nodes so it
+ // re-runs on every relevant change; the rAF lets layout settle first (no timer).
+ function centerCurrentNode() {
+ const scroller = treeScrollEl, wrap = lanesWrapEl;
+ if (!scroller || !wrap) return;
+ const el = wrap.querySelector(`[data-hash="${displayedHash}"]`);
+ if (!el) return;
+ const p = contentXY(el, scroller);
+ // Edge margin clamped per-axis so the "already in view" range stays satisfiable
+ // even on a narrow/linear panel — otherwise a perfectly centered node reads as
+ // out-of-view and re-fires scrollTo on every navigation (jitter).
+ const mX = Math.min(LANE_COL_W, Math.max(8, (scroller.clientWidth - p.w) / 2 - 1));
+ const mY = Math.min(LANE_COL_W, Math.max(8, (scroller.clientHeight - p.h) / 2 - 1));
+ const offL = p.x - scroller.scrollLeft, offT = p.y - scroller.scrollTop;
+ const outH = offL < mX || offL + p.w > scroller.clientWidth - mX;
+ const outV = offT < mY || offT + p.h > scroller.clientHeight - mY;
+ if (!outH && !outV) return;
+ scroller.scrollTo({
+ left: Math.max(0, p.x + p.w / 2 - scroller.clientWidth / 2),
+ top: Math.max(0, p.y + p.h / 2 - scroller.clientHeight / 2),
+ behavior: "smooth",
+ });
+ }
+ $effect(() => {
+ if (!lanesActive) return;
+ displayedHash; laneRender.nodes; // track → recenter on navigation/expand
+ requestAnimationFrame(centerCurrentNode);
+ });
let compareTargets = $derived(
lineageList.filter(item => item?.hash !== displayedHash)
@@ -638,14 +822,18 @@
console.trace("[PCR][viewer] displayedHash ->", displayedHash, "currentIndex", currentIndex);
});
+ // hash → gallery entry, so per-thumb url lookups are O(1) instead of scanning the
+ // whole gallery array on every reactive render (every lane thumb + bundle preview
+ // hit images.find() before this).
+ let imageByHash = $derived(new Map((images || []).map(i => [i.hash, i])));
function imageUrl(hash) {
// support direct URLs for images without DB hash (browse preview)
- const img = images.find(i => i.hash === hash);
+ const img = imageByHash.get(hash);
if (img?._directUrl) return img._directUrl;
return apiURL(`/promptchain/image/${hash}`);
}
function thumbUrl(hash) {
- const img = images.find(i => i.hash === hash);
+ const img = imageByHash.get(hash);
if (img?._directUrl) return img._directUrl;
return apiURL(`/promptchain/thumb/${hash}`);
}
@@ -1321,11 +1509,19 @@
if (upscaleModalOpen || inpaintModalOpen || editModalOpen || confirmDeleteOpen) return; // a modal owns the keyboard while open
if (e.key === "Escape") {
if (compareDropdownOpen) { compareDropdownOpen = false; return; }
+ if (selectedHashes.size) { clearSelection(); return; }
if (historyExpanded) { historyExpanded = false; return; }
if (compareMode) { exitCompare(); return; }
onClose();
return;
}
+ if ((e.key === "Delete" || e.key === "Backspace") && selectedHashes.size && onDelete) {
+ const t = e.target;
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return; // let editors keep Backspace
+ e.preventDefault();
+ requestDeleteBatch();
+ return;
+ }
if ((e.key === "c" || e.key === "C") && !e.ctrlKey && !e.metaKey && hasCompareTargets) {
e.preventDefault();
if (compareMode) exitCompare();
@@ -1443,15 +1639,26 @@
// specific lineage node (right-click menu), which may not be the one on screen.
let deleteTargetHash = $state(null);
+ // null = single delete; an array = a multi-select batch (drives the modal copy).
+ let deleteBatch = $state(null);
+
function requestDelete() {
if (!onDelete || deleting) return;
deleteTargetHash = null;
+ deleteBatch = null;
+ confirmDeleteOpen = true;
+ }
+
+ function requestDeleteBatch() {
+ if (!onDelete || deleting || !selectedHashes.size) return;
+ deleteBatch = [...selectedHashes];
confirmDeleteOpen = true;
}
function cancelDelete() {
confirmDeleteOpen = false;
deleteTargetHash = null;
+ deleteBatch = null;
}
// ── lineage strip right-click menu ──
@@ -1506,6 +1713,8 @@
if (!hash) return;
if (action === "expand-branches") {
expandBranches(item);
+ } else if (action === "focus-branch") {
+ focusBranch(hash);
} else if (action === "copy-path") {
copyPath(hash);
} else if (action === "edit") {
@@ -1523,9 +1732,52 @@
}
}
+ async function handleDeleteBatch(hashes) {
+ deleteBatch = null;
+ deleting = true;
+ // Only remove what truly deleted: onDelete throws by contract on failure (file
+ // locked, non-OK, reattach mismatch). Dropping the whole batch optimistically
+ // would hide a still-on-disk image until the next refetch — match the single
+ // delete, which filters only after a successful await.
+ const gone = new Set();
+ try {
+ for (const h of hashes) {
+ try { await onDelete(h); gone.add(h); } catch (err) { console.error("[PromptChain] batch delete failed for", h, err); }
+ }
+ if (gone.size) {
+ selectedHashes = new Set([...selectedHashes].filter(h => !gone.has(h))); // keep any that failed
+ const survivorsOrdered = lineageVisible.map(i => i?.hash).filter(h => h && !gone.has(h));
+ const landHash = (displayedHash && !gone.has(displayedHash)) ? displayedHash : (survivorsOrdered[0] || null);
+ images = images.filter(i => !gone.has(i.hash));
+ const survivors = lineageList.filter(item => item?.hash && !gone.has(item.hash));
+ if (!images.length) {
+ if (survivors.length) {
+ const next = survivors.find(s => s.hash === landHash) || survivors[0];
+ images = survivors;
+ currentIndex = Math.max(0, survivors.findIndex(s => s.hash === next.hash));
+ displayedHash = next.hash;
+ } else {
+ onClose();
+ }
+ } else if (landHash && images.some(i => i.hash === landHash)) {
+ currentIndex = images.findIndex(i => i.hash === landHash);
+ displayedHash = landHash;
+ } else {
+ const idx = Math.min(currentIndex, images.length - 1);
+ currentIndex = idx;
+ displayedHash = images[idx].hash;
+ }
+ }
+ } catch (e) {
+ console.error("[PromptChain] viewer batch delete failed", e);
+ }
+ deleting = false;
+ }
+
async function handleDelete() {
confirmDeleteOpen = false;
if (!onDelete || deleting) return;
+ if (deleteBatch?.length) { await handleDeleteBatch(deleteBatch); return; }
const hash = deleteTargetHash || displayedHash;
deleteTargetHash = null;
// Land on the PREVIOUS member of the same lineage chain (its parent/ancestor)
@@ -1640,33 +1892,29 @@
- {#snippet lineageNode(item, lane)}
+ {#snippet lineageNode(item, meta)}
{
- if (compareDropdownOpen) {
- if (item.hash !== displayedHash) enterCompareWith(item.hash, item.filename || item.hash.slice(0, 8));
- return;
- }
- lineageJump(item.hash); exitCompare();
- }}
+ onclick={(e) => handleNodeClick(e, item)}
onmouseenter={(e) => showTip(e, item)}
onmouseleave={hideTip}
oncontextmenu={(e) => { hideTip(); openLineageMenu(e, item); }}
>
-
+
+ {#if selectedHashes.has(item.hash)}
✓
{/if}
{#if editDocLayers[item.hash]}
@@ -1675,21 +1923,29 @@
{/if}
- {#if laneRender.junctionsByNode.get(item.hash)?.hidden.length}
-
{ e.stopPropagation(); expandBranches(item); }}
- >⊞{laneRender.junctionsByNode.get(item.hash).hidden.length}
+ {#if lineageOps.get(item.hash)?.kind === "upscale"}
+ {@const op = lineageOps.get(item.hash)}
+
▲{op.uniform ? fmtFactor(op.factor) : ""}
{/if}
- {#if brokenOut.has(lane.startHash) && item.hash === lane.startHash}
-
{ e.stopPropagation(); foldLane(lane.startHash); }}
+ {#if laneRender.junctionsByNode.get(item.hash)?.hidden.length}
+ {@const n = laneRender.junctionsByNode.get(item.hash).hidden.length}
+
+ ⋔{n}
+ { e.stopPropagation(); expandBranches(item); }}
+ >⊞{n}
+ {/if}
+ {#if brokenOut.has(item.hash)}
+ { e.stopPropagation(); foldLane(item.hash); }}
>⊟
{/if}
{/snippet}
{#if lineageList.length > 1}
-
+
+ {#if focusRootHash}
+
+ ⊙ Focused ✕
+
+ {/if}
+ {#if selectedHashes.size}
+
+ {selectedHashes.size} selected
+ Delete
+ ✕
+
+ {/if}
{#if lanesActive}
-
-
- {#each connectorGeom as g (g.key)} {/each}
-
- {#each laneRender.columns as lane (lane.startHash)}
-
- {#each lane.nodes as item (item.hash)}
- {@render lineageNode(item, lane)}
- {/each}
-
- {/each}
+
{:else}
@@ -1727,25 +2012,18 @@
class:ancestor={i < lineageDisplayCurrentIdx}
class:descendant={i > lineageDisplayCurrentIdx}
class:branch={d.branch}
+ class:selected={selectedHashes.has(item.hash)}
class:orphaned={item.orphaned === 1}
class:compare-candidate={compareDropdownOpen && item.hash !== displayedHash}
class:compare-left={compareMode && item.hash === displayedHash}
class:compare-right={compareMode && item.hash === compareTargetHash}
- onclick={() => {
- // With the compare dropdown open, the strip doubles as the
- // target picker — clicking a thumbnail compares against it
- // instead of navigating away.
- if (compareDropdownOpen) {
- if (item.hash !== displayedHash) enterCompareWith(item.hash, item.filename || item.hash.slice(0, 8));
- return;
- }
- lineageJump(item.hash); exitCompare();
- }}
+ onclick={(e) => handleNodeClick(e, item)}
onmouseenter={(e) => showTip(e, item)}
onmouseleave={hideTip}
oncontextmenu={(e) => { hideTip(); openLineageMenu(e, item); }}
>
+ {#if selectedHashes.has(item.hash)}
✓
{/if}
{hoverTip.item.filename || hoverTip.item.hash?.slice(0, 12)}
{#if hoverTip.item.created_at}
{timeAgo(hoverTip.item.created_at)}
{/if}
{#if hoverTip.item.width && hoverTip.item.height}
{hoverTip.item.width}×{hoverTip.item.height}
{/if}
+ {#if lineageOps.get(hoverTip.item.hash)?.kind === "upscale"}
+ {@const op = lineageOps.get(hoverTip.item.hash)}
+
▲ upscaled {op.uniform ? fmtFactor(op.factor) + " " : ""}from {op.from.w}×{op.from.h}
+ {/if}
{#if editDocLayers[hoverTip.item.hash]}
▦ {editDocLayers[hoverTip.item.hash]} layers — opens layered in Edit
{/if}
{/if}
@@ -2362,9 +2644,11 @@
@@ -2374,6 +2658,7 @@
x={lineageMenu.x}
y={lineageMenu.y}
canExpand={lineageMenu.item ? canExpand(lineageMenu.item) : false}
+ canFocus={lineageMenu.item ? (lineageTree.childrenOf.get(lineageMenu.item.hash)?.length > 0 && lineageMenu.item.hash !== focusRootHash) : false}
canEdit={!!onEditSave}
canDelete={!!onDelete}
isLocal={isLocalClient}
@@ -2448,11 +2733,82 @@
.pcr-viewer-lineage-node.current {
border-color: #4fc3f7;
background: rgba(79, 195, 247, 0.15);
+ box-shadow: 0 0 0 1px rgba(79, 195, 247, 0.5), 0 0 10px 1px rgba(79, 195, 247, 0.45);
}
.pcr-viewer-lineage-node:hover:not(.current) {
background: rgba(255, 255, 255, 0.1);
border-color: #555;
}
+ /* multi-select for batch delete */
+ .pcr-viewer-lineage-node.selected {
+ border-color: #ffb84d;
+ box-shadow: 0 0 0 1px rgba(255, 184, 77, 0.6);
+ }
+ /* centered over the thumb so it never collides with the corner badges (dot,
+ layers, fork pip / expander, opbadge) — the .selected ring already frames it */
+ .pcr-lin-selcheck {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 22px;
+ height: 22px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 13px;
+ font-weight: 700;
+ line-height: 1;
+ border-radius: 50%;
+ background: rgba(255, 184, 77, 0.92);
+ color: #1a1208;
+ pointer-events: none;
+ z-index: 3;
+ }
+ .pcr-lin-focuscrumb {
+ margin: 0 6px 4px;
+ padding: 3px 6px;
+ font-size: 10px;
+ border-radius: 4px;
+ border: 1px solid rgba(120, 200, 255, 0.4);
+ background: rgba(40, 70, 95, 0.5);
+ color: #bfe3ff;
+ cursor: pointer;
+ white-space: nowrap;
+ }
+ .pcr-lin-focuscrumb:hover { background: rgba(50, 90, 120, 0.7); }
+ .pcr-lin-focusx { opacity: 0.7; margin-left: 2px; }
+ .pcr-lin-selbar {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 5px;
+ margin: 0 6px 4px;
+ padding: 3px 6px;
+ font-size: 10px;
+ border-radius: 4px;
+ background: rgba(95, 65, 20, 0.5);
+ color: #ffd9a3;
+ }
+ .pcr-lin-seldel {
+ padding: 2px 6px;
+ border-radius: 3px;
+ border: none;
+ background: #e0863a;
+ color: #1a1208;
+ font-weight: 700;
+ cursor: pointer;
+ }
+ .pcr-lin-seldel:hover { background: #f09a4d; }
+ .pcr-lin-selclear {
+ padding: 2px 5px;
+ border-radius: 3px;
+ border: none;
+ background: transparent;
+ color: #ffd9a3;
+ cursor: pointer;
+ }
+ .pcr-lin-selclear:hover { color: #fff; }
.pcr-viewer-lineage-node img {
width: 100%;
height: 100%;
@@ -2558,29 +2914,36 @@
/* ── subway lanes: the rail grows horizontally into parallel vertical lanes ── */
.pcr-viewer-lineage-visible.lanes {
width: auto;
- max-width: 62vw;
+ max-width: 45vw;
}
- .pcr-lin-lanes {
- position: relative;
+ /* scroll viewport — fills the rail; the sized tree inside scrolls both axes */
+ .pcr-lin-treescroll {
flex: 1;
- display: flex;
- flex-direction: row;
- align-items: flex-start;
- gap: 22px;
- padding: 8px 12px;
overflow: auto;
+ padding: 8px 12px;
+ position: relative; /* offset origin for centering the current node (contentXY) */
}
- .pcr-lin-lanes::-webkit-scrollbar { width: 7px; height: 7px; }
- .pcr-lin-lanes::-webkit-scrollbar-track { background: transparent; }
- .pcr-lin-lanes::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
+ .pcr-lin-treescroll::-webkit-scrollbar { width: 7px; height: 7px; }
+ .pcr-lin-treescroll::-webkit-scrollbar-track { background: transparent; }
+ .pcr-lin-treescroll::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
/* both axes scroll here, so kill the default white corner where they meet */
- .pcr-lin-lanes::-webkit-scrollbar-corner { background: transparent; }
- .pcr-lin-col {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 22px;
- flex: 0 0 auto;
+ .pcr-lin-treescroll::-webkit-scrollbar-corner { background: transparent; }
+ /* sized to the tree's extent; nodes are absolutely positioned within it and the
+ SVG connector layer measures geometry relative to this element */
+ .pcr-lin-tree {
+ position: relative;
+ }
+ .pcr-lin-treenode {
+ position: absolute;
+ transition: opacity 0.12s ease;
+ }
+ /* fade everything off the displayed image's branch so the active lineage reads
+ at a glance; hovering a faded node brings it back to inspect it */
+ .pcr-lin-treenode.dimmed {
+ opacity: 0.7;
+ }
+ .pcr-lin-treenode.dimmed:hover {
+ opacity: 1;
}
/* SVG overlay lives inside the scroll content, so it translates with scroll for
free — geometry is only recomputed on expand/fold/resize/img-load. */
@@ -2596,6 +2959,66 @@
stroke-width: 2;
fill: none;
}
+ /* the displayed image's lineage path — drawn last so it sits over dim edges */
+ .pcr-lin-connectors path.active {
+ stroke: #3b9eff;
+ stroke-width: 3;
+ }
+ /* overflow bundle sitting in a tree row (oldest siblings folded away) */
+ .pcr-lin-treebundle {
+ width: 64px;
+ height: 44px;
+ }
+ /* faint blue tie between active-branch thumbs and the bright path connecting them */
+ .pcr-lin-treenode:not(.dimmed) .pcr-viewer-lineage-node:not(.current) {
+ border-color: rgba(59, 158, 255, 0.45);
+ }
+ /* upscale provenance badge — bottom-left, clears the layers badge (top-left) and dot (bottom-right) */
+ .pcr-lin-opbadge {
+ position: absolute;
+ bottom: 3px;
+ left: 3px;
+ padding: 0 3px;
+ height: 14px;
+ display: flex;
+ align-items: center;
+ font-size: 9px;
+ font-weight: 700;
+ line-height: 1;
+ border-radius: 3px;
+ background: rgba(20, 20, 20, 0.82);
+ color: #8fd0ff;
+ box-shadow: 0 0 0 1px rgba(59, 158, 255, 0.5);
+ pointer-events: none;
+ }
+ /* at-rest "this node hides N branches" marker; swapped for the ⊞ button on hover */
+ .pcr-lin-forkpip {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ min-width: 16px;
+ height: 16px;
+ padding: 0 3px;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ font-weight: 700;
+ line-height: 1;
+ border-radius: 4px;
+ background: rgba(20, 20, 20, 0.7);
+ color: #c0a3ff;
+ box-shadow: 0 0 0 1px rgba(150, 120, 255, 0.5);
+ pointer-events: none;
+ }
+ .pcr-viewer-lineage-node:hover .pcr-lin-forkpip,
+ .pcr-viewer-lineage-node:hover .pcr-lin-opbadge {
+ display: none;
+ }
+ .pcr-viewer-tip-op {
+ color: #8fd0ff;
+ }
.pcr-lin-expander {
position: absolute;
top: 2px;
diff --git a/svelte-src/components/LineageContextMenu.svelte b/svelte-src/components/LineageContextMenu.svelte
index 9d65b52..2dbb748 100644
--- a/svelte-src/components/LineageContextMenu.svelte
+++ b/svelte-src/components/LineageContextMenu.svelte
@@ -4,6 +4,7 @@
let {
x, y,
canExpand = false,
+ canFocus = false,
canEdit = false,
canDelete = false,
isLocal = false,
@@ -47,8 +48,11 @@
{#if canExpand}
act("expand-branches")}>Expand branches
- {#if canEdit || canDelete || isLocal}
{/if}
{/if}
+ {#if canFocus}
+
act("focus-branch")}>Focus this branch
+ {/if}
+ {#if (canExpand || canFocus) && (canEdit || canDelete || isLocal)}
{/if}
{#if canEdit}
act("edit")}>Edit
{/if}
diff --git a/svelte-src/lib/lineage-lanes.js b/svelte-src/lib/lineage-lanes.js
index 8c94e84..b2dfad5 100644
--- a/svelte-src/lib/lineage-lanes.js
+++ b/svelte-src/lib/lineage-lanes.js
@@ -49,54 +49,151 @@ export function buildLane(tree, startHash, spineSet) {
return { startHash, nodes, junctions };
}
-// Full render: column 0 anchored on `anchorHash`'s spine, then DFS the broken-out
-// lanes (each appears just right of the junction it forks from). Returns:
-// columns — lanes in left-to-right DFS order (column 0 first)
-// edges — { from: forkHash, to: childRootHash } per broken-out lane,
-// consumed as DATA by the SVG connector layer (geometry is
-// measured from the DOM; topology is never inferred from it)
+// A row stays readable up to this many columns; past it, the OLDEST overflow
+// siblings collapse into a single "+N" bundle chip that takes the last column
+// slot (so the visible width is bounded). CHILDREN_REVEAL_STEP is how many more
+// individual siblings each bundle click pages in.
+export const CHILDREN_PER_ROW_CAP = 5;
+export const CHILDREN_REVEAL_STEP = 4;
+const DEFAULT_REVEAL = CHILDREN_PER_ROW_CAP - 1; // reals shown alongside the chip
+
+// Full render. The breakout walk (col0 spine + DFS through broken-out junctions)
+// decides WHICH nodes are shown and collects the per-node junction record; then a
+// tidy top-down tree positions them, collapsing wide sibling rows into bundles.
+//
+// opts.displayedHash — the image currently on screen; its whole root→self chain
+// is protected from bundling so the active branch is always visible.
+// opts.revealed — Map
: how many individual children
+// to show for a parent before bundling the rest (paged up by bundle clicks).
+//
+// Returns:
+// nodes — for every placed item: real nodes { kind:"node", hash, item,
+// x, depth, isTip } and bundle chips { kind:"bundle", hash,
+// parent, hidden:[…], preview:[…], count, x, depth }.
+// depth = generation (row); x = column (may be fractional),
+// assigned post-order so each leaf takes the next slot and each
+// parent centers over its children. Absolutely positioned
+// (left = x*COL_W, top = depth*ROW_H).
+// edges — { from, to } parent→child among PLACED nodes (a bundle's
+// `to` is its synthetic id), consumed as DATA by the SVG
+// connector layer (geometry is measured from the DOM).
// junctionsByNode — Map powering the ⊞ expander
-// badge and the context-menu "Expand branches" gate
-// rootHash — column 0's first node (for the dot-root badge)
-// topRow is the column's vertical offset measured in ROWS from the top of the
-// whole lane area. A broken-out child column starts one generation BELOW its fork
-// (topRow = parentTopRow + forkRowInParent + 1), i.e. the SAME row as the sibling
-// that stayed inline in the parent column. So a parent sits a row above all its
-// children, children of children another row down — a proper top-down hierarchy
-// instead of every column top-aligned (which made parents look like peers).
-export function buildRender(tree, anchorHash, brokenOut) {
+// badge and the context-menu "Expand branches" gate.
+// rootHash — the family root (for the dot-root badge).
+// width, height — extent in columns / rows, for sizing the scroll content.
+export function buildRender(tree, anchorHash, brokenOut, opts = {}) {
+ const { byHash, childrenOf } = tree;
+ const { displayedHash, revealed = new Map(), rootHash: rootOverride } = opts;
const a = ancestry(tree, anchorHash);
- const col0 = buildLane(tree, a.root, a.set);
- const columns = [];
- const edges = [];
+ // "Focus this branch" renders only the subtree from rootOverride down; otherwise
+ // the lane starts at the true family root.
+ const laneStart = (rootOverride && byHash.has(rootOverride)) ? rootOverride : a.root;
+ const col0 = buildLane(tree, laneStart, a.set);
+ const shown = new Set();
const started = new Set();
const junctionsByNode = new Map();
- const visit = (lane, topRow) => {
- lane.topRow = topRow;
- columns.push(lane);
- // spine edges: every consecutive pair in a column is a parent→primary-child
- // link, drawn as a straight vertical connector (same column = same x).
- for (let i = 0; i + 1 < lane.nodes.length; i++) {
- edges.push({ from: lane.nodes[i].hash, to: lane.nodes[i + 1].hash });
- }
+ const visit = (lane) => {
+ for (const n of lane.nodes) shown.add(n.hash);
for (const j of lane.junctions) {
- const forkRow = lane.nodes.findIndex(n => n.hash === j.atHash);
let rec = junctionsByNode.get(j.atHash);
if (!rec) { rec = { shown: [], hidden: [] }; junctionsByNode.set(j.atHash, rec); }
for (const childRoot of j.childRoots) {
if (brokenOut.has(childRoot)) {
rec.shown.push(childRoot);
- if (!started.has(childRoot)) {
- started.add(childRoot);
- edges.push({ from: j.atHash, to: childRoot });
- visit(buildLane(tree, childRoot), topRow + forkRow + 1);
- }
+ if (!started.has(childRoot)) { started.add(childRoot); visit(buildLane(tree, childRoot)); }
} else {
rec.hidden.push(childRoot);
}
}
}
};
- visit(col0, 0);
- return { columns, edges, junctionsByNode, rootHash: col0.nodes.length ? col0.nodes[0].hash : null };
+ visit(col0);
+
+ // The root→displayed chain is protected so the active branch never bundles away.
+ const activePath = ancestry(tree, displayedHash || anchorHash).set;
+ const childrenShown = (h) => (childrenOf.get(h) || []).filter(k => shown.has(k.hash));
+ const hasShownSubtree = (h) => childrenShown(h).length > 0;
+
+ // Tidy layout. childrenShown is in created_at order (oldest→newest). A parent
+ // with more than the cap collapses its oldest non-protected leaf siblings into
+ // one bundle chip (placed leftmost, where the old generations would sit); the
+ // remaining reals keep their column each. Sibling subtrees occupy disjoint
+ // column ranges, so same-row nodes never overlap and a parent always sits above
+ // its own lineage.
+ const xOf = new Map();
+ const depthOf = new Map();
+ const placed = new Set();
+ const foldedAway = new Set(); // children collapsed into a chip — never placed as reals
+ const bundles = [];
+ let cursor = 0;
+ const place = (h, depth) => {
+ depthOf.set(h, depth);
+ placed.add(h);
+ const kids = childrenShown(h);
+ if (!kids.length) { xOf.set(h, cursor++); return; }
+
+ let bundleKids = [];
+ let reals = kids;
+ if (kids.length > CHILDREN_PER_ROW_CAP) {
+ // Candidates to fold: leaf siblings that aren't on the active path.
+ const optional = kids.filter(k => !activePath.has(k.hash) && !hasShownSubtree(k.hash));
+ const budget = revealed.get(h) ?? DEFAULT_REVEAL;
+ const forced = kids.length - optional.length; // protected reals
+ const keepOptional = Math.max(0, budget - forced); // newest optional kept as reals
+ const keep = new Set(optional.slice(Math.max(0, optional.length - keepOptional)).map(k => k.hash));
+ bundleKids = optional.filter(k => !keep.has(k.hash)); // the oldest leftover
+ if (bundleKids.length <= 1) bundleKids = []; // a 1-item chip saves nothing
+ const folded = new Set(bundleKids.map(k => k.hash));
+ reals = kids.filter(k => !folded.has(k.hash));
+ }
+
+ const xs = [];
+ if (bundleKids.length) {
+ const id = `bundle:${h}`;
+ depthOf.set(id, depth + 1);
+ xOf.set(id, cursor++);
+ xs.push(xOf.get(id));
+ bundles.push({
+ id, parent: h, depth: depth + 1,
+ hidden: bundleKids.map(k => k.hash),
+ preview: bundleKids.slice(0, 2).map(k => k.hash),
+ count: bundleKids.length,
+ });
+ for (const k of bundleKids) foldedAway.add(k.hash);
+ }
+ for (const k of reals) { place(k.hash, depth + 1); xs.push(xOf.get(k.hash)); }
+ xOf.set(h, (xs[0] + xs[xs.length - 1]) / 2);
+ };
+ const rootHash = col0.nodes.length ? col0.nodes[0].hash : null;
+ if (rootHash && shown.has(rootHash)) place(rootHash, 0);
+ // Stragglers (defensive): shown nodes the spine walk never reached. Folded
+ // children are intentionally unplaced (they live in a chip) — skip them here or
+ // they'd be re-placed as duplicate real nodes at row 0, defeating the bundle.
+ for (const h of shown) if (!placed.has(h) && !foldedAway.has(h)) place(h, 0);
+
+ // Edges derive from parent_hash over the placed reals (reproducing every spine
+ // and broken-out junction edge), plus one parent→chip edge per bundle. Folded
+ // children aren't placed, so no connector dangles at a missing node.
+ const edges = [];
+ for (const h of placed) {
+ if (h === rootHash) continue;
+ const p = byHash.get(h)?.parent_hash;
+ if (p && placed.has(p)) edges.push({ from: p, to: h });
+ }
+ for (const b of bundles) edges.push({ from: b.parent, to: b.id });
+
+ let maxX = 0, maxDepth = 0;
+ const nodes = [];
+ const note = (x, depth) => { if (x > maxX) maxX = x; if (depth > maxDepth) maxDepth = depth; };
+ for (const h of placed) {
+ const x = xOf.get(h), depth = depthOf.get(h);
+ note(x, depth);
+ nodes.push({ kind: "node", hash: h, item: byHash.get(h), x, depth, isTip: childrenShown(h).length === 0 });
+ }
+ for (const b of bundles) {
+ const x = xOf.get(b.id);
+ note(x, b.depth);
+ nodes.push({ kind: "bundle", hash: b.id, parent: b.parent, hidden: b.hidden, preview: b.preview, count: b.count, x, depth: b.depth });
+ }
+ return { nodes, edges, junctionsByNode, rootHash, width: maxX + 1, height: maxDepth + 1 };
}