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(`
No matching models
`); 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(``); +var root_11$3 = from_html(`
`, 1); +var root_8$5 = from_html(`
`); +var root_13$4 = from_html(``); 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(`
Engine
`); -var root_20$4 = from_html(``); +var root_20$3 = from_html(``); var root_21$3 = from_html(`
Depth from
3D pose
This image
`); var root_19$4 = from_html(`
Mode
`); 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(``); +var root_51$2 = from_html(``); var root_53$2 = from_html(``); var root_54$2 = from_html(``); -var root_52$1 = from_html(` `, 1); +var root_52$2 = from_html(` `, 1); var root_56$2 = from_html(``); var root_55$2 = from_html(` `, 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(``); +var root_13$3 = from_html(``); 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(`this
 var root_25$2 = from_html(`<option> </option>`);
 var root_24$2 = from_html(`<label class=Weight type
Start
End
`, 1); var root_26$2 = from_html(`
Fidelity
Start
End
`, 1); -var root_20$3 = from_html(`
Strength
`, 1); -var root_18$3 = from_html(`
Condition
`); +var root_20$2 = from_html(`
Strength
`, 1); +var root_18$2 = from_html(`
Condition
`); var root_28$1 = from_html(``); var root_27$2 = from_html(`
Mode
`); 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 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
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
drag handles · drag inside to move · Shift locks ratio · Enter / double-click applies · Esc resets`, 1); +var root_18$1 = from_html(`
Ratio
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
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(``); var root_50$1 = from_html(``); -var root_51 = from_html(`
`); +var root_51$1 = from_html(`
`); 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(``); var root_64$1 = from_html(``); 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.
`); -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(`
Editing:
Navigator
`); 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(`preview
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(``); var root_15$1 = from_html(``); 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(``); -var root_18$1 = from_html(` `, 1); +var root_18 = from_html(` `, 1); var root_19$1 = from_html(` `, 1); -var root_20$1 = from_html(` `, 1); +var root_20 = from_html(` `, 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(` `, 1); -var root_3$1 = from_html(``); -var root_4$1 = from_html(``); -var root_6$1 = from_html(`
`); -var root_7$1 = from_html(``); -var root_5$1 = from_html(` `, 1); -var root = from_html(`
`); +var root_1$1 = from_html(``); +var root_2$1 = from_html(``); +var root_3$1 = from_html(`
`); +var root_4$1 = from_html(``); +var root_5$1 = from_html(``); +var root_7$1 = from_html(`
`); +var root_8$1 = from_html(``); +var root_6$1 = from_html(` `, 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(`Compare`); -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(``); +var root_11 = from_html(`
`); +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(`Compare`); +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(`
Compare
`); -var root_54 = from_html(` `); -var root_53 = from_html(`
`); -var root_55 = from_html(`
`); -var root_56 = from_html(`
Open Folder
`); -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(`
Global:

`); -var root_64 = from_html(` `, 1); -var root_67 = from_html(`
Positive Prompt:

`); -var root_68 = from_html(`
Negative Prompt:

`); -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(`
Compare
`); +var root_63 = from_html(` `); +var root_62 = from_html(`
`); +var root_64 = from_html(`
`); +var root_65 = from_html(`
Open Folder
`); +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(`
Global:

`); +var root_73 = from_html(` `, 1); +var root_76 = from_html(`
Positive Prompt:

`); +var root_77 = from_html(`
Negative Prompt:

`); +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} - + {#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} - + {/if} + {#if brokenOut.has(item.hash)} + {/if}
{/snippet} {#if lineageList.length > 1} -
+
{lineageCurrentIdx + 1} / {lineageList.length} {#if hasBranches} @@ -1701,18 +1957,47 @@ >{lanesActive ? "⊟" : "⊞"} {/if}
+ {#if focusRootHash} + + {/if} + {#if selectedHashes.size} +
+ {selectedHashes.size} selected + + +
+ {/if} {#if lanesActive} -
- - {#each laneRender.columns as lane (lane.startHash)} -
- {#each lane.nodes as item (item.hash)} - {@render lineageNode(item, lane)} - {/each} -
- {/each} +
+
+ + {#each laneRender.nodes as node (node.hash)} +
+ {#if node.kind === "bundle"} + + + {:else} + {@render lineageNode(node.item, node)} + {/if} +
+ {/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} - {#if canEdit || canDelete || isLocal}
{/if} {/if} + {#if canFocus} + + {/if} + {#if (canExpand || canFocus) && (canEdit || canDelete || isLocal)}
{/if} {#if canEdit} {/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 }; }