Lineage subway tree view + viewer/modal polish
- Lineage "subway" lanes: branch columns, spine + S-curve connectors, per-node
expand/fold, header expand-all toggle, right-click Copy Path
- Lineage fixes: show family when an image is opened by file path; new-gen banner
moved off the lineage header to top-center; transparent scrollbar corner;
hover-only branch controls; stale-refetch staleness guard + edge-keyed
connectors + anomaly instrumentation
- Modal-setup persistence: 3D scene, inpaint mask, prompt, upscale dials restore
across reopen
- Re-pose modal: auto-restore last setup, Re-Apply, pan/zoom + before/after Compare
- Upscale modal: before/after Compare wipe
- Pose Studio: name-your-file scene export; $name{} block rename mirroring
- docs: README wordmark hero
This commit is contained in:
@@ -204,6 +204,23 @@ async def open_file(request):
|
||||
return web.json_response({"ok": True})
|
||||
|
||||
|
||||
@routes.get("/promptchain/file-path")
|
||||
async def file_path(request):
|
||||
"""Return the resolved absolute disk path for an image so the viewer can copy
|
||||
it to the clipboard.
|
||||
|
||||
Loopback-only: the path is a SERVER-machine filesystem path — meaningless on a
|
||||
remote client and not worth leaking. The frontend only offers Copy Path to
|
||||
local clients; this is the backstop.
|
||||
"""
|
||||
if request.remote not in ("127.0.0.1", "::1"):
|
||||
return error_response("file path is only available from the server machine", 403)
|
||||
path, err = _resolve_request_path(request)
|
||||
if err:
|
||||
return error_response(err, 404)
|
||||
return web.json_response({"path": os.path.normpath(path)})
|
||||
|
||||
|
||||
def _show_file_properties_win(path):
|
||||
"""Show the native Windows file-properties sheet for `path`.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { d as delegate, p as push, a as prop, u as user_effect, D as comment, v as first_child, i as if_block, k as append, l as pop, g as get, s as state, f as sibling, o as bind_this, e as set, t as template_effect, y as set_text, j as delegated, A as from_html, n as child } from "./disclose-version-BjTnIIw0.js";
|
||||
import { a as action } from "./actions-Ckx5huYg.js";
|
||||
import { d as delegate, p as push, a as prop, u as user_effect, D as comment, v as first_child, i as if_block, k as append, l as pop, g as get, s as state, f as sibling, o as bind_this, e as set, t as template_effect, y as set_text, j as delegated, A as from_html, n as child } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { a as action } from "./actions-WPfqiWYB.js";
|
||||
function portal(node) {
|
||||
document.body.appendChild(node);
|
||||
return { destroy() {
|
||||
@@ -87,4 +87,4 @@ export {
|
||||
ConfirmModal as C,
|
||||
portal as p
|
||||
};
|
||||
//# sourceMappingURL=ConfirmModal-DSeVjwuq.js.map
|
||||
//# sourceMappingURL=ConfirmModal-BGcVYndw.js.map
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
import { d as delegate, p as push, a as prop, u as user_effect, i as if_block, g as get, o as bind_this, t as template_effect, k as append, l as pop, m as user_derived, n as child, v as first_child, f as sibling, j as delegated, r as event, A as from_html, s as state, y as set_text, w as each, h as set_class, z as index, e as set, c as proxy, C as tick, x as set_attribute, D as comment } from "./disclose-version-BjTnIIw0.js";
|
||||
import { s as set_style } from "./style-CbOHK2KU.js";
|
||||
import { d as delegate, p as push, a as prop, u as user_effect, i as if_block, g as get, o as bind_this, t as template_effect, k as append, l as pop, m as user_derived, n as child, v as first_child, f as sibling, j as delegated, r as event, A as from_html, s as state, y as set_text, w as each, h as set_class, z as index, e as set, c as proxy, C as tick, x as set_attribute, D as comment } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { s as set_style } from "./style-Boi27oOu.js";
|
||||
import { app } from "/scripts/app.js";
|
||||
import { j as justifiedLayout } from "./justified-layout-cyVM7i96.js";
|
||||
import { a as action } from "./actions-Ckx5huYg.js";
|
||||
import { p as portal, C as ConfirmModal } from "./ConfirmModal-DSeVjwuq.js";
|
||||
import { a as action } from "./actions-WPfqiWYB.js";
|
||||
import { p as portal, C as ConfirmModal } from "./ConfirmModal-BGcVYndw.js";
|
||||
const GALLERY_ROW_HEIGHT_MIN = 40;
|
||||
const GALLERY_ROW_HEIGHT_MAX = 400;
|
||||
const ZOOM_FACTOR = 1.15;
|
||||
@@ -782,4 +782,4 @@ export {
|
||||
clampGalleryRowHeight as c,
|
||||
zoomGalleryRowHeight as z
|
||||
};
|
||||
//# sourceMappingURL=GeneratedGallery-D27T_RcN.js.map
|
||||
//# sourceMappingURL=GeneratedGallery-Dw0CC3-N.js.map
|
||||
@@ -1,10 +1,10 @@
|
||||
import { d as delegate, p as push, a as prop, u as user_effect, g as get, e as set, v as first_child, i as if_block, f as sibling, k as append, l as pop, s as state, m as user_derived, o as bind_this, j as delegated, n as child, D as comment, w as each, z as index, A as from_html, t as template_effect, h as set_class, r as event, y as set_text, x as set_attribute } from "./disclose-version-BjTnIIw0.js";
|
||||
import { o as onDestroy } from "./index-client-m0VtlDjX.js";
|
||||
import { h as html, P as PopupAnchor } from "./PopupAnchor-sxqUMRLP.js";
|
||||
import { a as action } from "./actions-Ckx5huYg.js";
|
||||
import { s as set_style } from "./style-CbOHK2KU.js";
|
||||
import { d as delegate, p as push, a as prop, u as user_effect, g as get, e as set, v as first_child, i as if_block, f as sibling, k as append, l as pop, s as state, m as user_derived, o as bind_this, j as delegated, n as child, D as comment, w as each, z as index, A as from_html, t as template_effect, h as set_class, r as event, y as set_text, x as set_attribute } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { o as onDestroy } from "./index-client-iMRCrpBY.js";
|
||||
import { h as html, P as PopupAnchor } from "./PopupAnchor-C6OWGGTI.js";
|
||||
import { a as action } from "./actions-WPfqiWYB.js";
|
||||
import { s as set_style } from "./style-Boi27oOu.js";
|
||||
import { app } from "/scripts/app.js";
|
||||
import { b as bind_value } from "./input-Bjai8x-c.js";
|
||||
import { b as bind_value } from "./input-DFQhebEz.js";
|
||||
var root_1$2 = from_html(`<div class="pcr-mode-menu-search-container"><input type="text" class="pcr-mode-menu-search" placeholder="Search options..."/></div> <div class="pcr-mode-menu-separator"></div>`, 1);
|
||||
var root_2$2 = from_html(`<div class="pcr-mode-menu-empty">No matching options</div>`);
|
||||
var root_5$1 = from_html(`<span class="pcr-mode-menu-check"></span>`);
|
||||
@@ -592,4 +592,4 @@ export {
|
||||
Menubar as M,
|
||||
SearchableList as S
|
||||
};
|
||||
//# sourceMappingURL=Menubar-PsG9jYP0.js.map
|
||||
//# sourceMappingURL=Menubar-DI-z1NzN.js.map
|
||||
@@ -1,5 +1,5 @@
|
||||
import { t as template_effect, R as active_effect, S as assign_nodes, T as get_first_child, U as remove_effect_dom, V as create_element, W as NAMESPACE_SVG, X as NAMESPACE_MATHML, b as block, Y as EFFECT_TRANSPARENT, B as BranchManager, c as proxy, d as delegate, p as push, a as prop, u as user_effect, o as bind_this, j as delegated, k as append, l as pop, A as from_html, n as child } from "./disclose-version-BjTnIIw0.js";
|
||||
import { a as onMount, o as onDestroy } from "./index-client-m0VtlDjX.js";
|
||||
import { t as template_effect, S as active_effect, T as assign_nodes, U as get_first_child, V as remove_effect_dom, W as create_element, X as NAMESPACE_SVG, Y as NAMESPACE_MATHML, b as block, Z as EFFECT_TRANSPARENT, B as BranchManager, c as proxy, d as delegate, p as push, a as prop, u as user_effect, o as bind_this, j as delegated, k as append, l as pop, A as from_html, n as child } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { a as onMount, o as onDestroy } from "./index-client-iMRCrpBY.js";
|
||||
function html(node, get_value, is_controlled = false, svg = false, mathml = false, skip_warning = false) {
|
||||
var anchor = node;
|
||||
var value = "";
|
||||
@@ -155,4 +155,4 @@ export {
|
||||
PopupAnchor as P,
|
||||
html as h
|
||||
};
|
||||
//# sourceMappingURL=PopupAnchor-sxqUMRLP.js.map
|
||||
//# sourceMappingURL=PopupAnchor-C6OWGGTI.js.map
|
||||
@@ -1,5 +1,5 @@
|
||||
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, i as if_block, g as get, f as sibling, t as template_effect, x as set_attribute, q as set_value, j as delegated, k as append, l as pop, m as user_derived, n as child, o as bind_this, w as each, h as set_class, y as set_text, z as index, A as from_html } from "./disclose-version-BjTnIIw0.js";
|
||||
import { s as set_style } from "./style-CbOHK2KU.js";
|
||||
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, i as if_block, g as get, f as sibling, t as template_effect, x as set_attribute, q as set_value, j as delegated, k as append, l as pop, m as user_derived, n as child, o as bind_this, w as each, h as set_class, y as set_text, z as index, A as from_html } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { s as set_style } from "./style-Boi27oOu.js";
|
||||
var root_2 = from_html(`<div class="pcr-slider-zone svelte-nuvs0d"></div>`);
|
||||
var root_3 = from_html(`<div class="pcr-slider-marker svelte-nuvs0d"></div>`);
|
||||
var root_4 = from_html(`<div><span class="pcr-slider-tick-label svelte-nuvs0d"> </span></div>`);
|
||||
@@ -267,4 +267,4 @@ delegate([
|
||||
export {
|
||||
SettingsSlider as S
|
||||
};
|
||||
//# sourceMappingURL=SettingsSlider-Bxw-taga.js.map
|
||||
//# sourceMappingURL=SettingsSlider-CKF_XmgB.js.map
|
||||
@@ -1,4 +1,4 @@
|
||||
import { a3 as effect, E as untrack, a8 as render_effect, a9 as deep_read_state, aa as safe_not_equal } from "./disclose-version-BjTnIIw0.js";
|
||||
import { a4 as effect, E as untrack, a9 as render_effect, aa as deep_read_state, ab as safe_not_equal } from "./disclose-version-uq4tn5Y6.js";
|
||||
function action(dom, action2, get_value) {
|
||||
effect(() => {
|
||||
var payload = untrack(() => action2(dom, get_value == null ? void 0 : get_value()) || {});
|
||||
@@ -29,4 +29,4 @@ function action(dom, action2, get_value) {
|
||||
export {
|
||||
action as a
|
||||
};
|
||||
//# sourceMappingURL=actions-Ckx5huYg.js.map
|
||||
//# sourceMappingURL=actions-WPfqiWYB.js.map
|
||||
@@ -1,4 +1,4 @@
|
||||
import { O as setContext, N as getContext } from "./disclose-version-BjTnIIw0.js";
|
||||
import { P as setContext, O as getContext } from "./disclose-version-uq4tn5Y6.js";
|
||||
const API_KEY = "pcr-api";
|
||||
function provideApi(api) {
|
||||
setContext(API_KEY, api);
|
||||
@@ -41,4 +41,4 @@ export {
|
||||
safeJson as s,
|
||||
useApi as u
|
||||
};
|
||||
//# sourceMappingURL=api-context-D3RUFV-0.js.map
|
||||
//# sourceMappingURL=api-context-BFKo1mCD.js.map
|
||||
@@ -1374,12 +1374,16 @@
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
border-bottom: 1px solid #222;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pcr-lin-count.svelte-5cciiw { white-space: nowrap; }
|
||||
.pcr-viewer-lineage-strip.svelte-5cciiw {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -1513,6 +1517,129 @@
|
||||
color: #666;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── subway lanes: the rail grows horizontally into parallel vertical lanes ── */
|
||||
.pcr-viewer-lineage-visible.lanes.svelte-5cciiw {
|
||||
width: auto;
|
||||
max-width: 62vw;
|
||||
}
|
||||
.pcr-lin-lanes.svelte-5cciiw {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 22px;
|
||||
padding: 8px 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
.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; }
|
||||
/* 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;
|
||||
}
|
||||
/* SVG overlay lives inside the scroll content, so it translates with scroll for
|
||||
free — geometry is only recomputed on expand/fold/resize/img-load. */
|
||||
.pcr-lin-connectors.svelte-5cciiw {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.pcr-lin-connectors.svelte-5cciiw path:where(.svelte-5cciiw) {
|
||||
stroke: #4a4a4a;
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
}
|
||||
.pcr-lin-expander.svelte-5cciiw {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 3px;
|
||||
box-sizing: border-box;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(120, 120, 120, 0.6);
|
||||
background: rgba(20, 20, 20, 0.82);
|
||||
color: #cdd6e0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
}
|
||||
.pcr-lin-expander.svelte-5cciiw:hover {
|
||||
background: rgba(60, 60, 60, 0.95);
|
||||
border-color: #888;
|
||||
}
|
||||
.pcr-lin-fold.svelte-5cciiw {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(120, 120, 120, 0.6);
|
||||
background: rgba(20, 20, 20, 0.82);
|
||||
color: #cdd6e0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
}
|
||||
.pcr-lin-fold.svelte-5cciiw:hover {
|
||||
background: rgba(60, 60, 60, 0.95);
|
||||
border-color: #888;
|
||||
}
|
||||
/* ⊞/⊟ are actions, not status — keep them off the thumb until the node is
|
||||
hovered so the strip isn't cluttered at rest (dot + layers badge stay). */
|
||||
.pcr-lin-expander.svelte-5cciiw,
|
||||
.pcr-lin-fold.svelte-5cciiw {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.12s;
|
||||
}
|
||||
.pcr-viewer-lineage-node.svelte-5cciiw:hover .pcr-lin-expander:where(.svelte-5cciiw),
|
||||
.pcr-viewer-lineage-node.svelte-5cciiw:hover .pcr-lin-fold:where(.svelte-5cciiw) {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.pcr-lin-toggle.svelte-5cciiw {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
background: #1a1a1a;
|
||||
color: #9bb4c7;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.pcr-lin-toggle.svelte-5cciiw:hover { background: #262626; border-color: #555; color: #cfe2f0; }
|
||||
.pcr-lin-toggle.on.svelte-5cciiw { background: rgba(79, 195, 247, 0.15); border-color: #4fc3f7; color: #4fc3f7; }
|
||||
|
||||
/* compare-picking: dropdown open → strip thumbnails are click targets */
|
||||
.pcr-viewer-lineage-node.compare-candidate.svelte-5cciiw {
|
||||
border-color: rgba(255, 221, 114, 0.6);
|
||||
@@ -1549,7 +1676,8 @@
|
||||
.pcr-viewer-newgen-banner.svelte-5cciiw {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 30;
|
||||
padding: 7px 13px;
|
||||
font-size: 12.5px;
|
||||
@@ -1562,8 +1690,8 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
@keyframes svelte-5cciiw-pcr-newgen-in {
|
||||
from { opacity: 0; transform: translateY(-8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from { opacity: 0; transform: translate(-50%, -8px); }
|
||||
to { opacity: 1; transform: translate(-50%, 0); }
|
||||
}
|
||||
@keyframes svelte-5cciiw-pcr-newgen-out {
|
||||
to { opacity: 0; }
|
||||
|
||||
+32
-31
@@ -4285,37 +4285,38 @@ export {
|
||||
autofocus as H,
|
||||
to_array as I,
|
||||
from_svg as J,
|
||||
mount as K,
|
||||
unmount as L,
|
||||
effect_root as M,
|
||||
getContext as N,
|
||||
setContext as O,
|
||||
clsx as P,
|
||||
text as Q,
|
||||
active_effect as R,
|
||||
assign_nodes as S,
|
||||
get_first_child as T,
|
||||
remove_effect_dom as U,
|
||||
create_element as V,
|
||||
NAMESPACE_SVG as W,
|
||||
NAMESPACE_MATHML as X,
|
||||
EFFECT_TRANSPARENT as Y,
|
||||
component_context as Z,
|
||||
lifecycle_outside_component as _,
|
||||
noop as K,
|
||||
mount as L,
|
||||
unmount as M,
|
||||
effect_root as N,
|
||||
getContext as O,
|
||||
setContext as P,
|
||||
clsx as Q,
|
||||
text as R,
|
||||
active_effect as S,
|
||||
assign_nodes as T,
|
||||
get_first_child as U,
|
||||
remove_effect_dom as V,
|
||||
create_element as W,
|
||||
NAMESPACE_SVG as X,
|
||||
NAMESPACE_MATHML as Y,
|
||||
EFFECT_TRANSPARENT as Z,
|
||||
component_context as _,
|
||||
prop as a,
|
||||
set_selected as a0,
|
||||
listen_to_event_and_reset_event as a1,
|
||||
current_batch as a2,
|
||||
effect as a3,
|
||||
is_array as a4,
|
||||
select_multiple_invalid_value as a5,
|
||||
is as a6,
|
||||
teardown as a7,
|
||||
render_effect as a8,
|
||||
deep_read_state as a9,
|
||||
safe_not_equal as aa,
|
||||
to_style as ab,
|
||||
queue_micro_task as ac,
|
||||
lifecycle_outside_component as a0,
|
||||
set_selected as a1,
|
||||
listen_to_event_and_reset_event as a2,
|
||||
current_batch as a3,
|
||||
effect as a4,
|
||||
is_array as a5,
|
||||
select_multiple_invalid_value as a6,
|
||||
is as a7,
|
||||
teardown as a8,
|
||||
render_effect as a9,
|
||||
deep_read_state as aa,
|
||||
safe_not_equal as ab,
|
||||
to_style as ac,
|
||||
queue_micro_task as ad,
|
||||
block as b,
|
||||
proxy as c,
|
||||
delegate as d,
|
||||
@@ -4342,4 +4343,4 @@ export {
|
||||
set_text as y,
|
||||
index as z
|
||||
};
|
||||
//# sourceMappingURL=disclose-version-BjTnIIw0.js.map
|
||||
//# sourceMappingURL=disclose-version-uq4tn5Y6.js.map
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Z as component_context, _ as lifecycle_outside_component, u as user_effect, E as untrack } from "./disclose-version-BjTnIIw0.js";
|
||||
import { _ as component_context, a0 as lifecycle_outside_component, u as user_effect, E as untrack } from "./disclose-version-uq4tn5Y6.js";
|
||||
function onMount(fn) {
|
||||
if (component_context === null) {
|
||||
lifecycle_outside_component();
|
||||
@@ -23,4 +23,4 @@ export {
|
||||
onMount as a,
|
||||
onDestroy as o
|
||||
};
|
||||
//# sourceMappingURL=index-client-m0VtlDjX.js.map
|
||||
//# sourceMappingURL=index-client-iMRCrpBY.js.map
|
||||
@@ -1,4 +1,4 @@
|
||||
import { a1 as listen_to_event_and_reset_event, a2 as current_batch, C as tick, E as untrack, a8 as render_effect, a6 as is, a7 as teardown, ac as queue_micro_task } from "./disclose-version-BjTnIIw0.js";
|
||||
import { a2 as listen_to_event_and_reset_event, a3 as current_batch, C as tick, E as untrack, a9 as render_effect, a7 as is, a8 as teardown, ad as queue_micro_task } from "./disclose-version-uq4tn5Y6.js";
|
||||
function bind_value(input, get, set = get) {
|
||||
var batches = /* @__PURE__ */ new WeakSet();
|
||||
listen_to_event_and_reset_event(input, "input", async (is_reset) => {
|
||||
@@ -150,4 +150,4 @@ export {
|
||||
bind_value as b,
|
||||
bind_group as c
|
||||
};
|
||||
//# sourceMappingURL=input-Bjai8x-c.js.map
|
||||
//# sourceMappingURL=input-DFQhebEz.js.map
|
||||
@@ -5,11 +5,11 @@ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot
|
||||
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
||||
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
||||
var _activeNodeId, _treeRoots, _openTabs, _activeWildcardTab, _compiledOutput, _compiledNegOutput, _imageUrl, _previewUrl, _progress, _isGenerating, _sidebarView, _fontSize, _renamingNodeId;
|
||||
import { d as delegate, p as push, a as prop, i as if_block, f as sibling, t as template_effect, e as set, g as get, j as delegated, k as append, l as pop, s as state, n as child, A as from_html, x as set_attribute, u as user_effect, h as set_class, r as event, m as user_derived, Q as text, y as set_text, o as bind_this, w as each, z as index, D as comment, v as first_child, c as proxy, L as unmount, K as mount } from "./disclose-version-BjTnIIw0.js";
|
||||
import { a as onMount, o as onDestroy } from "./index-client-m0VtlDjX.js";
|
||||
import { s as set_style } from "./style-CbOHK2KU.js";
|
||||
import { b as bind_value } from "./input-Bjai8x-c.js";
|
||||
import { M as Menubar, S as SearchableList } from "./Menubar-PsG9jYP0.js";
|
||||
import { d as delegate, p as push, a as prop, i as if_block, f as sibling, t as template_effect, e as set, g as get, j as delegated, k as append, l as pop, s as state, n as child, A as from_html, x as set_attribute, u as user_effect, h as set_class, r as event, m as user_derived, R as text, y as set_text, o as bind_this, w as each, z as index, D as comment, v as first_child, c as proxy, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { a as onMount, o as onDestroy } from "./index-client-iMRCrpBY.js";
|
||||
import { s as set_style } from "./style-Boi27oOu.js";
|
||||
import { b as bind_value } from "./input-DFQhebEz.js";
|
||||
import { M as Menubar, S as SearchableList } from "./Menubar-DI-z1NzN.js";
|
||||
var root_1$4 = from_html(`<img class="pcr-fs-topbar-logo svelte-93wsqh" alt="PromptChain"/>`);
|
||||
var root$5 = from_html(`<div class="pcr-fs-topbar svelte-93wsqh"><div class="pcr-fs-topbar-left svelte-93wsqh"><!> <span class="pcr-fs-topbar-title svelte-93wsqh"></span></div> <div class="pcr-fs-topbar-right svelte-93wsqh"><div class="pcr-fs-topbar-actions svelte-93wsqh"><input type="number" class="pcr-fs-batch-input svelte-93wsqh" min="1" max="99" title="Batch count"/> <span class="pcr-fs-run-btn svelte-93wsqh" title="Queue prompt (Ctrl+Enter)"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"></path></svg> <span>Run</span></span> <span class="pcr-fs-cancel-btn pcr-fs-inactive svelte-93wsqh" title="Cancel execution"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></svg></span> <span class="pcr-fs-queue-badge pcr-fs-inactive svelte-93wsqh">0 active</span></div> <div class="pcr-fs-panel-toggles svelte-93wsqh"><span class="pcr-fs-header-btn svelte-93wsqh" title="Toggle 3D Poser panel" data-fs-action="pose"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M20.5 6c-2.61.7-5.67 1-8.5 1s-5.89-.3-8.5-1L3 8c1.86.5 4 .83 6 1v13h2v-6h2v6h2V9c2-.17 4.14-.5 6-1l-.5-2zM12 6c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"></path></svg></span> <span class="pcr-fs-header-btn svelte-93wsqh" title="Toggle output panel" data-fs-action="output"><svg width="16" height="16" viewBox="0 -960 960 960" fill="currentColor"><path d="M400-280h160v-80H400v80Zm0-160h280v-80H400v80ZM280-600h400v-80H280v80Zm200 120ZM80-80v-80h102q-48-23-77.5-68T75-330q0-79 55.5-134.5T265-520v80q-45 0-77.5 32T155-330q0 39 24 69t61 38v-97h80v240H80Zm320-40v-80h360v-560H200v160h-80v-160q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H400Z"></path></svg></span> <span class="pcr-fs-header-btn svelte-93wsqh" title="Toggle image preview" data-fs-action="image"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"></path></svg></span></div> <span class="pcr-fs-close-btn svelte-93wsqh" title="Close (Escape)"></span></div></div>`);
|
||||
function Topbar($$anchor, $$props) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { L as unmount, K as mount } from "./disclose-version-BjTnIIw0.js";
|
||||
import { G as GeneratedGallery } from "./GeneratedGallery-D27T_RcN.js";
|
||||
import { M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { G as GeneratedGallery } from "./GeneratedGallery-Dw0CC3-N.js";
|
||||
function mountGallery(target, props) {
|
||||
return mount(GeneratedGallery, { target, props });
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { p as push, a as prop, D as comment, v as first_child, i as if_block, g as get, k as append, l as pop, s as state, e as set, f as sibling, t as template_effect, r as event, A as from_html, n as child, y as set_text, x as set_attribute, d as delegate, c as proxy, u as user_effect, E as untrack, o as bind_this, j as delegated, C as tick, m as user_derived, w as each, z as index, h as set_class, F as update, L as unmount, K as mount } from "./disclose-version-BjTnIIw0.js";
|
||||
import { s as set_style } from "./style-CbOHK2KU.js";
|
||||
import { b as bind_value } from "./input-Bjai8x-c.js";
|
||||
import { p as push, a as prop, D as comment, v as first_child, i as if_block, g as get, k as append, l as pop, s as state, e as set, f as sibling, t as template_effect, r as event, A as from_html, n as child, y as set_text, x as set_attribute, d as delegate, c as proxy, u as user_effect, E as untrack, o as bind_this, j as delegated, C as tick, m as user_derived, w as each, z as index, h as set_class, F as update, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { s as set_style } from "./style-Boi27oOu.js";
|
||||
import { b as bind_value } from "./input-DFQhebEz.js";
|
||||
import { e as extractPrecisions, r as resolveFilesForPrecision } from "./model-constants-WRFJ51jF.js";
|
||||
import { api } from "/scripts/api.js";
|
||||
import { s as safeJson, H as HttpError } from "./api-context-D3RUFV-0.js";
|
||||
import { s as safeJson, H as HttpError } from "./api-context-BFKo1mCD.js";
|
||||
var root_2$4 = from_html(`<div class="pcr-hover-card-version svelte-oulz7g"> </div>`);
|
||||
var root_3$4 = from_html(`<div class="pcr-hover-card-meta svelte-oulz7g"> </div>`);
|
||||
var root_4$3 = from_html(`<div class="pcr-hover-card-tags svelte-oulz7g"> </div>`);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, f as sibling, o as bind_this, t as template_effect, g as get, x as set_attribute, q as set_value, j as delegated, k as append, l as pop, m as user_derived, n as child, A as from_html, w as each, z as index, i as if_block, G as set_checked, y as set_text, h as set_class, v as first_child, D as comment, I as to_array, a0 as set_selected, Q as text, E as untrack, L as unmount, K as mount } from "./disclose-version-BjTnIIw0.js";
|
||||
import { s as set_style } from "./style-CbOHK2KU.js";
|
||||
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, f as sibling, o as bind_this, t as template_effect, g as get, x as set_attribute, q as set_value, j as delegated, k as append, l as pop, m as user_derived, n as child, A as from_html, w as each, z as index, i as if_block, G as set_checked, y as set_text, h as set_class, v as first_child, D as comment, I as to_array, a1 as set_selected, R as text, E as untrack, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { s as set_style } from "./style-Boi27oOu.js";
|
||||
import { F as FAMILIES, A as ARCHITECTURES } from "./model-constants-WRFJ51jF.js";
|
||||
import { S as SettingsSlider } from "./SettingsSlider-Bxw-taga.js";
|
||||
import { b as bind_value } from "./input-Bjai8x-c.js";
|
||||
import { b as bind_select_value } from "./select-Dgaht2aI.js";
|
||||
import { a as action } from "./actions-Ckx5huYg.js";
|
||||
import { S as SettingsSlider } from "./SettingsSlider-CKF_XmgB.js";
|
||||
import { b as bind_value } from "./input-DFQhebEz.js";
|
||||
import { b as bind_select_value } from "./select-Dp4ExMMc.js";
|
||||
import { a as action } from "./actions-WPfqiWYB.js";
|
||||
var root$7 = from_html(`<div class="pcr-slider-container svelte-1pxowmh"><div class="pcr-slider-track svelte-1pxowmh"><div class="pcr-slider-zone pcr-slider-zone-editable svelte-1pxowmh"></div> <div class="pcr-slider-thumb pcr-thumb-blue svelte-1pxowmh"></div> <div class="pcr-slider-thumb pcr-thumb-blue svelte-1pxowmh"></div></div> <input type="number" class="pcr-slider-input pcr-slider-input-half svelte-1pxowmh"/> <span class="pcr-slider-range-sep svelte-1pxowmh">–</span> <input type="number" class="pcr-slider-input pcr-slider-input-half svelte-1pxowmh"/></div>`);
|
||||
function RangeSlider($$anchor, $$props) {
|
||||
push($$props, true);
|
||||
|
||||
@@ -5,15 +5,15 @@ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot
|
||||
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
||||
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
||||
var _mode, _switchIndex, _locked, _disabled, _collapsed, _connectedCount, _hasLabels, _switchLabel, _iterateCurrent, _iterateTotal, _iterateCycle, _switchOptions, _outputPanelOpen, _imagePanelVisible, _aiAssistantOpen, _posePanelVisible, _hasPoseStudio, _compiledOutput, _compiledNegOutput, _imageUrl, _previewUrl, _progress, _isGenerating, _rollSelected;
|
||||
import { p as push, o as bind_this, k as append, l as pop, A as from_html, d as delegate, a as prop, g as get, j as delegated, m as user_derived, s as state, c as proxy, u as user_effect, i as if_block, f as sibling, r as event, e as set, C as tick, n as child, D as comment, v as first_child, w as each, t as template_effect, y as set_text, h as set_class, x as set_attribute, P as clsx, z as index, Q as text, E as untrack, L as unmount, K as mount } from "./disclose-version-BjTnIIw0.js";
|
||||
import { u as useApi, p as provideApi } from "./api-context-D3RUFV-0.js";
|
||||
import { M as Menubar } from "./Menubar-PsG9jYP0.js";
|
||||
import { o as onDestroy, a as onMount } from "./index-client-m0VtlDjX.js";
|
||||
import { s as set_style } from "./style-CbOHK2KU.js";
|
||||
import { h as html, P as PopupAnchor } from "./PopupAnchor-sxqUMRLP.js";
|
||||
import { p as push, o as bind_this, k as append, l as pop, A as from_html, d as delegate, a as prop, g as get, j as delegated, m as user_derived, s as state, c as proxy, u as user_effect, i as if_block, f as sibling, r as event, e as set, C as tick, n as child, D as comment, v as first_child, w as each, t as template_effect, y as set_text, h as set_class, x as set_attribute, Q as clsx, z as index, R as text, E as untrack, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { u as useApi, p as provideApi } from "./api-context-BFKo1mCD.js";
|
||||
import { M as Menubar } from "./Menubar-DI-z1NzN.js";
|
||||
import { o as onDestroy, a as onMount } from "./index-client-iMRCrpBY.js";
|
||||
import { s as set_style } from "./style-Boi27oOu.js";
|
||||
import { h as html, P as PopupAnchor } from "./PopupAnchor-C6OWGGTI.js";
|
||||
import { api } from "/scripts/api.js";
|
||||
import { c as clampGalleryRowHeight, z as zoomGalleryRowHeight, G as GeneratedGallery } from "./GeneratedGallery-D27T_RcN.js";
|
||||
import { b as bind_value } from "./input-Bjai8x-c.js";
|
||||
import { c as clampGalleryRowHeight, z as zoomGalleryRowHeight, G as GeneratedGallery } from "./GeneratedGallery-Dw0CC3-N.js";
|
||||
import { b as bind_value } from "./input-DFQhebEz.js";
|
||||
import { app } from "/scripts/app.js";
|
||||
import { r as readNodePrompt, c as cryptoId, e as extractNGrams, m as matchCharactersInDb, i as isStandaloneMainPromptChain, a as applyPromptText } from "./ai-patch-helpers-Bayqv0oF.js";
|
||||
var root$c = from_html(`<div class="pcr-editor-frame"></div>`);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { c as proxy, M as effect_root, u as user_effect, d as delegate, p as push, r as event, $ as $window, v as first_child, w as each, f as sibling, i as if_block, g as get, o as bind_this, t as template_effect, j as delegated, k as append, l as pop, s as state, e as set, m as user_derived, n as child, A as from_html, J as from_svg, h as set_class, y as set_text, z as index, q as set_value, x as set_attribute, a as prop, N as getContext, C as tick, D as comment, O as setContext, K as mount, L as unmount } from "./disclose-version-BjTnIIw0.js";
|
||||
import { a as onMount } from "./index-client-m0VtlDjX.js";
|
||||
import { s as set_style } from "./style-CbOHK2KU.js";
|
||||
import { c as proxy, N as effect_root, u as user_effect, d as delegate, p as push, r as event, $ as $window, v as first_child, w as each, f as sibling, i as if_block, g as get, o as bind_this, t as template_effect, j as delegated, k as append, l as pop, s as state, e as set, m as user_derived, n as child, A as from_html, J as from_svg, h as set_class, y as set_text, z as index, q as set_value, x as set_attribute, a as prop, O as getContext, C as tick, D as comment, P as setContext, L as mount, M as unmount } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { a as onMount } from "./index-client-iMRCrpBY.js";
|
||||
import { s as set_style } from "./style-Boi27oOu.js";
|
||||
import { j as justifiedLayout } from "./justified-layout-cyVM7i96.js";
|
||||
import { a as action } from "./actions-Ckx5huYg.js";
|
||||
import { b as bind_value, c as bind_group } from "./input-Bjai8x-c.js";
|
||||
import { p as portal, C as ConfirmModal } from "./ConfirmModal-DSeVjwuq.js";
|
||||
import { s as safeJson, H as HttpError } from "./api-context-D3RUFV-0.js";
|
||||
import { a as action } from "./actions-WPfqiWYB.js";
|
||||
import { b as bind_value, c as bind_group } from "./input-DFQhebEz.js";
|
||||
import { p as portal, C as ConfirmModal } from "./ConfirmModal-BGcVYndw.js";
|
||||
import { s as safeJson, H as HttpError } from "./api-context-BFKo1mCD.js";
|
||||
const VIEW_MODES = [
|
||||
{ id: "grid", label: "Grid" },
|
||||
{ id: "justified", label: "Justified" },
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { d as delegate, p as push, a as prop, w as each, z as index, g as get, f as sibling, i as if_block, k as append, l as pop, m as user_derived, n as child, h as set_class, j as delegated, A as from_html, t as template_effect, x as set_attribute, q as set_value, v as first_child, y as set_text, D as comment, s as state, c as proxy, u as user_effect, e as set, I as to_array, E as untrack, o as bind_this, G as set_checked, L as unmount, K as mount } from "./disclose-version-BjTnIIw0.js";
|
||||
import { a as onMount, o as onDestroy } from "./index-client-m0VtlDjX.js";
|
||||
import { a as action } from "./actions-Ckx5huYg.js";
|
||||
import { a as bind_checked, b as bind_value } from "./input-Bjai8x-c.js";
|
||||
import { d as delegate, p as push, a as prop, w as each, z as index, g as get, f as sibling, i as if_block, k as append, l as pop, m as user_derived, n as child, h as set_class, j as delegated, A as from_html, t as template_effect, x as set_attribute, q as set_value, v as first_child, y as set_text, D as comment, s as state, c as proxy, u as user_effect, e as set, I as to_array, E as untrack, o as bind_this, G as set_checked, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { a as onMount, o as onDestroy } from "./index-client-iMRCrpBY.js";
|
||||
import { a as action } from "./actions-WPfqiWYB.js";
|
||||
import { a as bind_checked, b as bind_value } from "./input-DFQhebEz.js";
|
||||
import { b as buildInsertText } from "./tag-builder-utils-ng134QDV.js";
|
||||
import { b as bind_select_value } from "./select-Dgaht2aI.js";
|
||||
import { b as bind_select_value } from "./select-Dp4ExMMc.js";
|
||||
const MULTI_SELECT_GROUPS = ["props"];
|
||||
const CUSTOMIZABLE_CLOTHING_GROUPS = [
|
||||
"legwear",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, f as sibling, n as child, i as if_block, g as get, t as template_effect, j as delegated, k as append, l as pop, v as first_child, h as set_class, y as set_text, A as from_html, m as user_derived, w as each, z as index, I as to_array, C as tick, o as bind_this, D as comment, x as set_attribute, q as set_value, Q as text, L as unmount, K as mount } from "./disclose-version-BjTnIIw0.js";
|
||||
import { a as onMount } from "./index-client-m0VtlDjX.js";
|
||||
import { a as action } from "./actions-Ckx5huYg.js";
|
||||
import { s as set_style } from "./style-CbOHK2KU.js";
|
||||
import { a as bind_checked, b as bind_value } from "./input-Bjai8x-c.js";
|
||||
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, f as sibling, n as child, i as if_block, g as get, t as template_effect, j as delegated, k as append, l as pop, v as first_child, h as set_class, y as set_text, A as from_html, m as user_derived, w as each, z as index, I as to_array, C as tick, o as bind_this, D as comment, x as set_attribute, q as set_value, R as text, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { a as onMount } from "./index-client-iMRCrpBY.js";
|
||||
import { a as action } from "./actions-WPfqiWYB.js";
|
||||
import { s as set_style } from "./style-Boi27oOu.js";
|
||||
import { a as bind_checked, b as bind_value } from "./input-DFQhebEz.js";
|
||||
import { b as isStructuralLine, d as isInNegativeBlock } from "./ai-patch-helpers-Bayqv0oF.js";
|
||||
import { f as formatTagsForModel } from "./tag-builder-utils-ng134QDV.js";
|
||||
var root_1$3 = from_html(`<div class="pcr-atb2-cust-loading svelte-1mwxie1">Loading…</div>`);
|
||||
|
||||
+1573
-1098
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { a1 as listen_to_event_and_reset_event, a2 as current_batch, a3 as effect, a4 as is_array, a5 as select_multiple_invalid_value, a6 as is, a7 as teardown } from "./disclose-version-BjTnIIw0.js";
|
||||
import { a2 as listen_to_event_and_reset_event, a3 as current_batch, a4 as effect, a5 as is_array, a6 as select_multiple_invalid_value, a7 as is, a8 as teardown } from "./disclose-version-uq4tn5Y6.js";
|
||||
function select_option(select, value, mounting = false) {
|
||||
if (select.multiple) {
|
||||
if (value == void 0) {
|
||||
@@ -97,4 +97,4 @@ export {
|
||||
init_select as i,
|
||||
select_option as s
|
||||
};
|
||||
//# sourceMappingURL=select-Dgaht2aI.js.map
|
||||
//# sourceMappingURL=select-Dp4ExMMc.js.map
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ab as to_style } from "./disclose-version-BjTnIIw0.js";
|
||||
import { ac as to_style } from "./disclose-version-uq4tn5Y6.js";
|
||||
function update_styles(dom, prev = {}, next, priority) {
|
||||
for (var key in next) {
|
||||
var value = next[key];
|
||||
@@ -36,4 +36,4 @@ function set_style(dom, value, prev_styles, next_styles) {
|
||||
export {
|
||||
set_style as s
|
||||
};
|
||||
//# sourceMappingURL=style-CbOHK2KU.js.map
|
||||
//# sourceMappingURL=style-Boi27oOu.js.map
|
||||
+9
-2
@@ -8240,11 +8240,17 @@ async function mountViewport(node, container) {
|
||||
// Svelte bundle's CSS isn't present.
|
||||
let exportModal = null; // built lazily on first export (after the teardown chain is in place)
|
||||
const closeExport = () => { if (exportModal) exportModal.backdrop.style.display = "none"; };
|
||||
// Remember the last name the author exported as (device-local), so the next
|
||||
// export defaults to it instead of the generic placeholder. The browser still
|
||||
// auto-suffixes " (1)" etc. on its own if that file already exists on disk.
|
||||
const EXPORT_NAME_KEY = "pcr.pose-export-name";
|
||||
const attemptExport = () => {
|
||||
const raw = exportModal.input.value.trim();
|
||||
if (!raw) return; // confirm is disabled while empty; guard the Enter path too
|
||||
closeExport();
|
||||
downloadScene(sanitizeSceneFilename(raw));
|
||||
const name = sanitizeSceneFilename(raw);
|
||||
try { localStorage.setItem(EXPORT_NAME_KEY, name); } catch { /* localStorage blocked */ }
|
||||
downloadScene(name);
|
||||
};
|
||||
const buildExportModal = () => {
|
||||
const backdrop = document.createElement("div");
|
||||
@@ -8328,7 +8334,8 @@ async function mountViewport(node, container) {
|
||||
const openExportScene = () => {
|
||||
if (!exportModal) buildExportModal();
|
||||
exportModal.backdrop.style.zIndex = String((node.properties?.pcrFullscreenZ || 10001) + 10); // above this poser's flyout menus
|
||||
const defaultName = "promptchain-pose-scene.json";
|
||||
let defaultName = "promptchain-pose-scene.json";
|
||||
try { const saved = localStorage.getItem(EXPORT_NAME_KEY); if (saved) defaultName = saved; } catch { /* localStorage blocked */ }
|
||||
exportModal.input.value = defaultName;
|
||||
exportModal.refreshConfirm();
|
||||
exportModal.backdrop.style.display = "flex";
|
||||
|
||||
@@ -6,6 +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 { buildFigureRegions, matchRegionByOverlap } from "../lib/region-binding.js";
|
||||
|
||||
let {
|
||||
@@ -247,14 +248,17 @@
|
||||
displayedHash = hash;
|
||||
}
|
||||
|
||||
let lineageDisplay = $derived((() => {
|
||||
const asNodes = (list) => list.map(item => ({ kind: "node", item }));
|
||||
if (lineageList.length < 2) return asNodes(lineageList);
|
||||
const byHash = new Map(lineageList.map(f => [f.hash, f]));
|
||||
if (!byHash.has(displayedHash)) return asNodes(lineageList);
|
||||
|
||||
// Topology primitives for the family tree, hoisted out of lineageDisplay so the
|
||||
// strip, the context-menu "expand branches" gate, and the subway lane builder
|
||||
// all read ONE source. byHash/childrenOf rebuild parent→children edges from each
|
||||
// node's parent_hash (a content sha256, so acyclic by construction). roots is the
|
||||
// true root plus any node orphaned by a deleted-and-absent parent, so the lane
|
||||
// builder can surface it as its own root rather than dropping it.
|
||||
let lineageTree = $derived((() => {
|
||||
const list = lineageList;
|
||||
const byHash = new Map(list.map(f => [f.hash, f]));
|
||||
const childrenOf = new Map();
|
||||
for (const f of lineageList) {
|
||||
for (const f of list) {
|
||||
if (!f.parent_hash || !byHash.has(f.parent_hash)) continue;
|
||||
if (!childrenOf.has(f.parent_hash)) childrenOf.set(f.parent_hash, []);
|
||||
childrenOf.get(f.parent_hash).push(f);
|
||||
@@ -262,6 +266,15 @@
|
||||
for (const kids of childrenOf.values()) {
|
||||
kids.sort((a, b) => (a.created_at || 0) - (b.created_at || 0));
|
||||
}
|
||||
const roots = list.filter(f => !f.parent_hash || !byHash.has(f.parent_hash)).map(f => f.hash);
|
||||
return { byHash, childrenOf, roots };
|
||||
})());
|
||||
|
||||
let lineageDisplay = $derived((() => {
|
||||
const asNodes = (list) => list.map(item => ({ kind: "node", item }));
|
||||
if (lineageList.length < 2) return asNodes(lineageList);
|
||||
const { byHash, childrenOf } = lineageTree;
|
||||
if (!byHash.has(displayedHash)) return asNodes(lineageList);
|
||||
|
||||
// parent_hash is a content sha256, so the family is acyclic by
|
||||
// construction — the visited set is cheap defense against malformed data
|
||||
@@ -341,6 +354,159 @@
|
||||
);
|
||||
let lineageHiddenCount = $derived(lineageList.length - lineageVisible.length);
|
||||
|
||||
// ── subway lanes (right-click "Expand branches") ──
|
||||
// brokenOut holds CHILD-SUBTREE ROOT hashes the user has peeled into their own
|
||||
// vertical column. The rail switches from the compact strip to the lane view
|
||||
// whenever anything is broken out. subwayAnchor reuses the stable lineageFocusHash
|
||||
// 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());
|
||||
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);
|
||||
|
||||
// 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).
|
||||
function primaryChildHash(item) {
|
||||
const kids = lineageTree.childrenOf.get(item?.hash) || [];
|
||||
if (!kids.length) return null;
|
||||
const a = ancestry(lineageTree, subwayAnchor);
|
||||
return (kids.find(k => a.set.has(k.hash)) || kids[0]).hash;
|
||||
}
|
||||
function canExpand(item) {
|
||||
const primary = primaryChildHash(item);
|
||||
return (lineageTree.childrenOf.get(item?.hash) || [])
|
||||
.some(k => k.hash !== primary && !brokenOut.has(k.hash));
|
||||
}
|
||||
function expandBranches(item) {
|
||||
const primary = primaryChildHash(item);
|
||||
const next = new Set(brokenOut);
|
||||
for (const k of lineageTree.childrenOf.get(item?.hash) || [])
|
||||
if (k.hash !== primary) next.add(k.hash);
|
||||
brokenOut = next;
|
||||
}
|
||||
function foldLane(childRoot) {
|
||||
// folding a column folds its whole broken-out subtree with it
|
||||
const next = new Set(brokenOut);
|
||||
const stack = [childRoot];
|
||||
while (stack.length) {
|
||||
const h = stack.pop();
|
||||
next.delete(h);
|
||||
for (const k of lineageTree.childrenOf.get(h) || []) stack.push(k.hash);
|
||||
}
|
||||
brokenOut = next;
|
||||
}
|
||||
function collapseAllLanes() { brokenOut = new Set(); }
|
||||
// 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;
|
||||
return false;
|
||||
})());
|
||||
// header "expand" affordance — fan EVERY branch out at once (the full subway):
|
||||
// add every off-primary child across the whole family to brokenOut.
|
||||
function expandAll() {
|
||||
const a = ancestry(lineageTree, subwayAnchor);
|
||||
const next = new Set(brokenOut);
|
||||
for (const kids of lineageTree.childrenOf.values()) {
|
||||
if (kids.length < 2) continue;
|
||||
const primary = (kids.find(k => a.set.has(k.hash)) || kids[0]).hash;
|
||||
for (const k of kids) if (k.hash !== primary) next.add(k.hash);
|
||||
}
|
||||
brokenOut = next;
|
||||
}
|
||||
|
||||
// Keep the displayed image materialized if it sits inside a folded branch: peel
|
||||
// the chain of off-primary roots from the anchor spine down to it. Guarded so it
|
||||
// only ever ADDS new roots (never loops).
|
||||
$effect(() => {
|
||||
if (!lanesActive) return;
|
||||
if (laneRender.columns.some(c => c.nodes.some(n => n.hash === displayedHash))) return;
|
||||
const { byHash, childrenOf } = lineageTree;
|
||||
const a = ancestry(lineageTree, subwayAnchor);
|
||||
const toBreak = [];
|
||||
let cur = displayedHash;
|
||||
const guard = new Set();
|
||||
while (cur && byHash.has(cur) && !guard.has(cur) && !a.set.has(cur)) {
|
||||
guard.add(cur);
|
||||
const parent = byHash.get(cur).parent_hash;
|
||||
if (!parent || !byHash.has(parent)) break;
|
||||
const kids = childrenOf.get(parent) || [];
|
||||
const primary = (kids.find(k => a.set.has(k.hash)) || kids[0])?.hash;
|
||||
if (cur !== primary) toBreak.push(cur);
|
||||
cur = parent;
|
||||
}
|
||||
const additions = toBreak.filter(h => !brokenOut.has(h));
|
||||
if (additions.length) {
|
||||
const next = new Set(brokenOut);
|
||||
for (const h of additions) next.add(h);
|
||||
brokenOut = next;
|
||||
}
|
||||
});
|
||||
|
||||
// SVG connector geometry — edges are DATA from laneRender; the rAF only MEASURES
|
||||
// pixel positions of known nodes (never infers topology from the DOM). The SVG
|
||||
// lives inside the scrolling wrapper so it translates with scroll for free; we
|
||||
// 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 connectorGeom = $state([]);
|
||||
let connectorBox = $state({ w: 0, h: 0 });
|
||||
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).
|
||||
const LANE_ROW_H = 86;
|
||||
// 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) {
|
||||
let x = 0, y = 0;
|
||||
for (let n = el; n && n !== root; n = n.offsetParent) { x += n.offsetLeft; y += n.offsetTop; }
|
||||
return { x, y, w: el.offsetWidth, h: el.offsetHeight };
|
||||
}
|
||||
function measureConnectors() {
|
||||
const wrap = lanesWrapEl;
|
||||
if (!wrap) return;
|
||||
const geom = [];
|
||||
for (const e of laneRender.edges) {
|
||||
const a = wrap.querySelector(`[data-hash="${e.from}"]`);
|
||||
const b = wrap.querySelector(`[data-hash="${e.to}"]`);
|
||||
if (!a || !b) continue;
|
||||
const ra = contentXY(a, wrap), rb = contentXY(b, wrap);
|
||||
const px = ra.x + ra.w / 2, pb = ra.y + ra.h; // parent bottom-center
|
||||
const cx = rb.x + rb.w / 2, ct = rb.y; // child top-center
|
||||
// key by edge identity, not array index — when the edge set changes, a
|
||||
// keyed {#each} won't reuse a stale path d-string under a shifted index
|
||||
// (that was the one-frame connector "points at the old node" jitter).
|
||||
const key = `${e.from}>${e.to}`;
|
||||
if (Math.abs(px - cx) < 1) {
|
||||
geom.push({ key, d: `M ${px} ${pb} L ${cx} ${ct}` }); // spine: straight down
|
||||
} else {
|
||||
const my = (pb + ct) / 2; // branch: smooth S-curve
|
||||
geom.push({ key, d: `M ${px} ${pb} C ${px} ${my}, ${cx} ${my}, ${cx} ${ct}` });
|
||||
}
|
||||
}
|
||||
connectorGeom = geom;
|
||||
connectorBox = { w: wrap.scrollWidth, h: wrap.scrollHeight };
|
||||
}
|
||||
function scheduleMeasure() {
|
||||
if (measureScheduled) return;
|
||||
measureScheduled = true;
|
||||
requestAnimationFrame(() => { measureScheduled = false; measureConnectors(); });
|
||||
}
|
||||
$effect(() => {
|
||||
if (!lanesActive) return;
|
||||
const cols = laneRender.columns; // read → track expand/fold/family changes
|
||||
if (cols) scheduleMeasure();
|
||||
});
|
||||
$effect(() => {
|
||||
if (!lanesWrapEl) return;
|
||||
const ro = new ResizeObserver(() => scheduleMeasure());
|
||||
ro.observe(lanesWrapEl);
|
||||
return () => ro.disconnect();
|
||||
});
|
||||
|
||||
let compareTargets = $derived(
|
||||
lineageList.filter(item => item?.hash !== displayedHash)
|
||||
);
|
||||
@@ -408,12 +574,24 @@
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(data => { imageInfo = data; })
|
||||
.catch(onMetaError("image meta"));
|
||||
fetchApi(`/promptchain/lineage/${displayedHash}`)
|
||||
// Capture the hash this fetch was issued for: displayedHash can change
|
||||
// (fast navigation / save-then-navigate) before the response lands, and a
|
||||
// late loser must NOT overwrite the current family — same staleness guard
|
||||
// the browse branch above uses. Deliberately not nulling lineageData first:
|
||||
// in-family navigation keeps the same family, so blanking it would flicker
|
||||
// the tree on every step.
|
||||
const reqHash = displayedHash;
|
||||
fetchApi(`/promptchain/lineage/${reqHash}`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(data => { lineageData = data; })
|
||||
.then(data => {
|
||||
if (displayedHash !== reqHash) return; // stale response — a newer hash is shown
|
||||
lineageData = data;
|
||||
reportLineageAnomaly(reqHash, data);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (displayedHash !== reqHash) return;
|
||||
lineageData = null;
|
||||
console.error(`[PromptChain] lineage fetch failed for ${displayedHash}:`, e);
|
||||
console.error(`[PromptChain] lineage fetch failed for ${reqHash}:`, e);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1068,6 +1246,26 @@
|
||||
} catch { /* diagnostics must never break the handback */ }
|
||||
}
|
||||
|
||||
// A healthy lineage response contains the queried node WITH its parent present
|
||||
// in the same family. If the node is absent, or its parent_hash points outside
|
||||
// the returned family (so the tree renders it as a stray root), log it via
|
||||
// comfyui.log — that is the data signature behind a node appearing
|
||||
// "mis-attributed" until a refresh. Event-driven (fires only on a landed
|
||||
// response), no timers. We couldn't capture the live case, so this catches the
|
||||
// next one. A true root (parent_hash null) is healthy and never logs.
|
||||
function reportLineageAnomaly(reqHash, data) {
|
||||
const fam = data?.family;
|
||||
if (!Array.isArray(fam) || fam.length === 0) return;
|
||||
const me = fam.find(f => f?.hash === reqHash);
|
||||
const parentOrphaned = !!(me?.parent_hash && !fam.some(f => f?.hash === me.parent_hash));
|
||||
if (me && !parentOrphaned) return; // healthy
|
||||
const fams = fam.map(f => (f?.hash || "").slice(0, 8)).join(",");
|
||||
reportHandback(
|
||||
"lineage-tree-anomaly",
|
||||
`node ${(reqHash || "").slice(0, 8)} ${me ? "orphaned: parent " + (me.parent_hash || "").slice(0, 8) + " absent" : "ABSENT"} from its own family [${fams}]`
|
||||
);
|
||||
}
|
||||
|
||||
async function handleUpscaleToEdit(doneState) {
|
||||
const ctx = upscaleEditCtx;
|
||||
const viewUrl = (img) => apiURL(`/view?filename=${encodeURIComponent(img.filename)}&subfolder=${encodeURIComponent(img.subfolder || "")}&type=${img.type || "output"}&rand=${Math.random()}`);
|
||||
@@ -1270,10 +1468,47 @@
|
||||
lineageMenu = { x: e.clientX, y: e.clientY, item };
|
||||
}
|
||||
|
||||
let copyPathTimer = null;
|
||||
let copyPathToast = $state(false);
|
||||
async function copyTextToClipboard(text) {
|
||||
try { await navigator.clipboard.writeText(text); return true; } catch {}
|
||||
// The async clipboard API can refuse after an awaited fetch (transient
|
||||
// activation expires); a hidden textarea + execCommand still copies.
|
||||
try {
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = text;
|
||||
ta.style.position = "fixed";
|
||||
ta.style.top = "-1000px";
|
||||
ta.style.opacity = "0";
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
const ok = document.execCommand("copy");
|
||||
document.body.removeChild(ta);
|
||||
return ok;
|
||||
} catch { return false; }
|
||||
}
|
||||
async function copyPath(hash) {
|
||||
try {
|
||||
const r = await fetchApi?.(`/promptchain/file-path?hash=${encodeURIComponent(hash)}`);
|
||||
const d = r && r.ok ? await r.json() : null;
|
||||
if (!d?.path) return;
|
||||
if (await copyTextToClipboard(d.path)) {
|
||||
copyPathToast = true;
|
||||
clearTimeout(copyPathTimer);
|
||||
copyPathTimer = setTimeout(() => { copyPathToast = false; }, 1800);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function lineageMenuAction(action, item) {
|
||||
const hash = item?.hash;
|
||||
if (!hash) return;
|
||||
if (action === "edit") {
|
||||
if (action === "expand-branches") {
|
||||
expandBranches(item);
|
||||
} else if (action === "copy-path") {
|
||||
copyPath(hash);
|
||||
} else if (action === "edit") {
|
||||
lineageJump(hash); // bring it on screen so editDocHash matches what we edit
|
||||
openEdit(hash);
|
||||
} else if (action === "delete") {
|
||||
@@ -1404,86 +1639,154 @@
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="pcr-viewer" bind:this={viewerEl} style="transform: translateY({viewerTransformY}px); opacity: {viewerOpacity}">
|
||||
{#if newGenBanner}
|
||||
<div class="pcr-viewer-newgen-banner">✨ {newGenBanner}</div>
|
||||
{/if}
|
||||
<!-- left: lineage panel -->
|
||||
{#snippet lineageNode(item, lane)}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="pcr-viewer-lineage-node"
|
||||
data-hash={item.hash}
|
||||
class:current={item.hash === displayedHash}
|
||||
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={() => {
|
||||
if (compareDropdownOpen) {
|
||||
if (item.hash !== displayedHash) enterCompareWith(item.hash, item.filename || item.hash.slice(0, 8));
|
||||
return;
|
||||
}
|
||||
lineageJump(item.hash); exitCompare();
|
||||
}}
|
||||
onmouseenter={(e) => showTip(e, item)}
|
||||
onmouseleave={hideTip}
|
||||
oncontextmenu={(e) => { hideTip(); openLineageMenu(e, item); }}
|
||||
>
|
||||
<img src={thumbUrl(item.hash)} alt="" draggable="false" loading="lazy" onload={scheduleMeasure} />
|
||||
<div class="pcr-viewer-lineage-dot"
|
||||
class:dot-root={item.hash === laneRender.rootHash}
|
||||
class:dot-current={item.hash === displayedHash}
|
||||
class:dot-tip={item.hash === lane.nodes[lane.nodes.length - 1].hash && item.hash !== displayedHash}
|
||||
></div>
|
||||
{#if editDocLayers[item.hash]}
|
||||
<div class="pcr-viewer-lineage-layers">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M12 2 2 7l10 5 10-5-10-5Z" /><path d="m2 17 10 5 10-5" /><path d="m2 12 10 5 10-5" />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
{#if laneRender.junctionsByNode.get(item.hash)?.hidden.length}
|
||||
<button class="pcr-lin-expander" title="Break this node's {laneRender.junctionsByNode.get(item.hash).hidden.length} other branch(es) into their own columns"
|
||||
onclick={(e) => { e.stopPropagation(); expandBranches(item); }}
|
||||
>⊞{laneRender.junctionsByNode.get(item.hash).hidden.length}</button>
|
||||
{/if}
|
||||
{#if brokenOut.has(lane.startHash) && item.hash === lane.startHash}
|
||||
<button class="pcr-lin-fold" title="Fold this column back into its branch"
|
||||
onclick={(e) => { e.stopPropagation(); foldLane(lane.startHash); }}
|
||||
>⊟</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#if lineageList.length > 1}
|
||||
<div class="pcr-viewer-lineage pcr-viewer-lineage-visible">
|
||||
<div class="pcr-viewer-lineage pcr-viewer-lineage-visible" class:lanes={lanesActive}>
|
||||
<div class="pcr-viewer-lineage-header">
|
||||
{lineageCurrentIdx + 1} / {lineageList.length}
|
||||
<span class="pcr-lin-count">{lineageCurrentIdx + 1} / {lineageList.length}</span>
|
||||
{#if hasBranches}
|
||||
<button
|
||||
class="pcr-lin-toggle"
|
||||
class:on={lanesActive}
|
||||
onclick={() => (lanesActive ? collapseAllLanes() : expandAll())}
|
||||
title={lanesActive ? "Collapse the subway back to the strip" : "Expand every branch into a subway map"}
|
||||
>{lanesActive ? "⊟" : "⊞"}</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="pcr-viewer-lineage-strip">
|
||||
{#each lineageDisplay as d, i (d.kind === "node" ? d.item.hash : `${d.kind}:${d.at}`)}
|
||||
{#if d.kind === "node"}
|
||||
{@const item = d.item}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="pcr-viewer-lineage-node"
|
||||
class:current={i === lineageDisplayCurrentIdx}
|
||||
class:ancestor={i < lineageDisplayCurrentIdx}
|
||||
class:descendant={i > lineageDisplayCurrentIdx}
|
||||
class:branch={d.branch}
|
||||
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();
|
||||
}}
|
||||
onmouseenter={(e) => showTip(e, item)}
|
||||
onmouseleave={hideTip}
|
||||
oncontextmenu={(e) => { hideTip(); openLineageMenu(e, item); }}
|
||||
>
|
||||
<img src={thumbUrl(item.hash)} alt="" draggable="false" loading="lazy" />
|
||||
<div class="pcr-viewer-lineage-dot"
|
||||
class:dot-root={item.hash === lineageList[0]?.hash}
|
||||
class:dot-current={i === lineageDisplayCurrentIdx}
|
||||
class:dot-tip={i === lineageDisplay.length - 1 && i !== lineageDisplayCurrentIdx}
|
||||
></div>
|
||||
{#if editDocLayers[item.hash]}
|
||||
<div class="pcr-viewer-lineage-layers">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M12 2 2 7l10 5 10-5-10-5Z" /><path d="m2 17 10 5 10-5" /><path d="m2 12 10 5 10-5" />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
{#if lanesActive}
|
||||
<div class="pcr-lin-lanes" bind:this={lanesWrapEl}>
|
||||
<svg class="pcr-lin-connectors" width={connectorBox.w} height={connectorBox.h} aria-hidden="true">
|
||||
{#each connectorGeom as g (g.key)}<path d={g.d} />{/each}
|
||||
</svg>
|
||||
{#each laneRender.columns as lane (lane.startHash)}
|
||||
<div class="pcr-lin-col" style="margin-top: {lane.topRow * LANE_ROW_H}px">
|
||||
{#each lane.nodes as item (item.hash)}
|
||||
{@render lineageNode(item, lane)}
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="pcr-viewer-lineage-bundle"
|
||||
class:expanded={d.kind === "collapse"}
|
||||
onclick={() => toggleBundle(d.at)}
|
||||
title={d.kind === "collapse"
|
||||
? "Collapse this branch"
|
||||
: `${d.count} other generation${d.count > 1 ? "s" : ""} branch off here — click to expand`}
|
||||
>
|
||||
{#if d.kind === "bundle"}
|
||||
<div class="pcr-viewer-lineage-bundle-stack">
|
||||
{#each d.preview as p (p.hash)}
|
||||
<img src={thumbUrl(p.hash)} alt="" draggable="false" loading="lazy" />
|
||||
{/each}
|
||||
</div>
|
||||
<span class="pcr-viewer-lineage-bundle-count">+{d.count}</span>
|
||||
{:else}
|
||||
<span class="pcr-viewer-lineage-bundle-count">▴</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if lineageHiddenCount > 0}
|
||||
<div class="pcr-viewer-lineage-hidden">{lineageHiddenCount} hidden</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="pcr-viewer-lineage-strip">
|
||||
{#each lineageDisplay as d, i (d.kind === "node" ? d.item.hash : `${d.kind}:${d.at}`)}
|
||||
{#if d.kind === "node"}
|
||||
{@const item = d.item}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="pcr-viewer-lineage-node"
|
||||
class:current={i === lineageDisplayCurrentIdx}
|
||||
class:ancestor={i < lineageDisplayCurrentIdx}
|
||||
class:descendant={i > lineageDisplayCurrentIdx}
|
||||
class:branch={d.branch}
|
||||
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();
|
||||
}}
|
||||
onmouseenter={(e) => showTip(e, item)}
|
||||
onmouseleave={hideTip}
|
||||
oncontextmenu={(e) => { hideTip(); openLineageMenu(e, item); }}
|
||||
>
|
||||
<img src={thumbUrl(item.hash)} alt="" draggable="false" loading="lazy" />
|
||||
<div class="pcr-viewer-lineage-dot"
|
||||
class:dot-root={item.hash === lineageList[0]?.hash}
|
||||
class:dot-current={i === lineageDisplayCurrentIdx}
|
||||
class:dot-tip={i === lineageDisplay.length - 1 && i !== lineageDisplayCurrentIdx}
|
||||
></div>
|
||||
{#if editDocLayers[item.hash]}
|
||||
<div class="pcr-viewer-lineage-layers">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M12 2 2 7l10 5 10-5-10-5Z" /><path d="m2 17 10 5 10-5" /><path d="m2 12 10 5 10-5" />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="pcr-viewer-lineage-bundle"
|
||||
class:expanded={d.kind === "collapse"}
|
||||
onclick={() => toggleBundle(d.at)}
|
||||
title={d.kind === "collapse"
|
||||
? "Collapse this branch"
|
||||
: `${d.count} other generation${d.count > 1 ? "s" : ""} branch off here — click to expand`}
|
||||
>
|
||||
{#if d.kind === "bundle"}
|
||||
<div class="pcr-viewer-lineage-bundle-stack">
|
||||
{#each d.preview as p (p.hash)}
|
||||
<img src={thumbUrl(p.hash)} alt="" draggable="false" loading="lazy" />
|
||||
{/each}
|
||||
</div>
|
||||
<span class="pcr-viewer-lineage-bundle-count">+{d.count}</span>
|
||||
{:else}
|
||||
<span class="pcr-viewer-lineage-bundle-count">▴</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if lineageHiddenCount > 0}
|
||||
<div class="pcr-viewer-lineage-hidden">{lineageHiddenCount} hidden</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
@@ -1511,6 +1814,15 @@
|
||||
ondblclick={handleDblClick}
|
||||
style="cursor: {isPanning ? 'grabbing' : 'grab'}"
|
||||
>
|
||||
<!-- Transient notices live over the image, not the corners: top-left is the
|
||||
lineage header and top-right is the close/counter. The native ComfyUI toast
|
||||
can't be used here — it caps at z-index 10000, one below this viewer. -->
|
||||
{#if newGenBanner}
|
||||
<div class="pcr-viewer-newgen-banner">✨ {newGenBanner}</div>
|
||||
{/if}
|
||||
{#if copyPathToast}
|
||||
<div class="pcr-viewer-newgen-banner">📋 Path copied to clipboard</div>
|
||||
{/if}
|
||||
{#if displayedHash}
|
||||
{#if !imageLoaded && !imageError}
|
||||
<div class="pcr-viewer-spinner"></div>
|
||||
@@ -2061,6 +2373,7 @@
|
||||
<LineageContextMenu
|
||||
x={lineageMenu.x}
|
||||
y={lineageMenu.y}
|
||||
canExpand={lineageMenu.item ? canExpand(lineageMenu.item) : false}
|
||||
canEdit={!!onEditSave}
|
||||
canDelete={!!onDelete}
|
||||
isLocal={isLocalClient}
|
||||
@@ -2098,12 +2411,16 @@
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
border-bottom: 1px solid #222;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pcr-lin-count { white-space: nowrap; }
|
||||
.pcr-viewer-lineage-strip {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -2237,6 +2554,129 @@
|
||||
color: #666;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── subway lanes: the rail grows horizontally into parallel vertical lanes ── */
|
||||
.pcr-viewer-lineage-visible.lanes {
|
||||
width: auto;
|
||||
max-width: 62vw;
|
||||
}
|
||||
.pcr-lin-lanes {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 22px;
|
||||
padding: 8px 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
.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; }
|
||||
/* 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;
|
||||
}
|
||||
/* SVG overlay lives inside the scroll content, so it translates with scroll for
|
||||
free — geometry is only recomputed on expand/fold/resize/img-load. */
|
||||
.pcr-lin-connectors {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.pcr-lin-connectors path {
|
||||
stroke: #4a4a4a;
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
}
|
||||
.pcr-lin-expander {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 3px;
|
||||
box-sizing: border-box;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(120, 120, 120, 0.6);
|
||||
background: rgba(20, 20, 20, 0.82);
|
||||
color: #cdd6e0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
}
|
||||
.pcr-lin-expander:hover {
|
||||
background: rgba(60, 60, 60, 0.95);
|
||||
border-color: #888;
|
||||
}
|
||||
.pcr-lin-fold {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(120, 120, 120, 0.6);
|
||||
background: rgba(20, 20, 20, 0.82);
|
||||
color: #cdd6e0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
}
|
||||
.pcr-lin-fold:hover {
|
||||
background: rgba(60, 60, 60, 0.95);
|
||||
border-color: #888;
|
||||
}
|
||||
/* ⊞/⊟ are actions, not status — keep them off the thumb until the node is
|
||||
hovered so the strip isn't cluttered at rest (dot + layers badge stay). */
|
||||
.pcr-lin-expander,
|
||||
.pcr-lin-fold {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.12s;
|
||||
}
|
||||
.pcr-viewer-lineage-node:hover .pcr-lin-expander,
|
||||
.pcr-viewer-lineage-node:hover .pcr-lin-fold {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.pcr-lin-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
background: #1a1a1a;
|
||||
color: #9bb4c7;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.pcr-lin-toggle:hover { background: #262626; border-color: #555; color: #cfe2f0; }
|
||||
.pcr-lin-toggle.on { background: rgba(79, 195, 247, 0.15); border-color: #4fc3f7; color: #4fc3f7; }
|
||||
|
||||
/* compare-picking: dropdown open → strip thumbnails are click targets */
|
||||
.pcr-viewer-lineage-node.compare-candidate {
|
||||
border-color: rgba(255, 221, 114, 0.6);
|
||||
@@ -2273,7 +2713,8 @@
|
||||
.pcr-viewer-newgen-banner {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 30;
|
||||
padding: 7px 13px;
|
||||
font-size: 12.5px;
|
||||
@@ -2286,8 +2727,8 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
@keyframes pcr-newgen-in {
|
||||
from { opacity: 0; transform: translateY(-8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from { opacity: 0; transform: translate(-50%, -8px); }
|
||||
to { opacity: 1; transform: translate(-50%, 0); }
|
||||
}
|
||||
@keyframes pcr-newgen-out {
|
||||
to { opacity: 0; }
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
let {
|
||||
x, y,
|
||||
canExpand = false,
|
||||
canEdit = false,
|
||||
canDelete = false,
|
||||
isLocal = false,
|
||||
@@ -44,6 +45,10 @@
|
||||
</script>
|
||||
|
||||
<div use:portal class="pcr-ctx" bind:this={menuEl} style="left:{pos.left}px;top:{pos.top}px;">
|
||||
{#if canExpand}
|
||||
<button class="pcr-ctx-item" onclick={() => act("expand-branches")}>Expand branches</button>
|
||||
{#if canEdit || canDelete || isLocal}<div class="pcr-ctx-sep"></div>{/if}
|
||||
{/if}
|
||||
{#if canEdit}
|
||||
<button class="pcr-ctx-item" onclick={() => act("edit")}>Edit</button>
|
||||
{/if}
|
||||
@@ -52,6 +57,7 @@
|
||||
{/if}
|
||||
{#if isLocal}
|
||||
{#if canEdit || canDelete}<div class="pcr-ctx-sep"></div>{/if}
|
||||
<button class="pcr-ctx-item" disabled={orphaned} onclick={() => act("copy-path")}>Copy Path</button>
|
||||
<button class="pcr-ctx-item" disabled={orphaned} onclick={() => act("open-file")}>Open File</button>
|
||||
<button class="pcr-ctx-item" disabled={orphaned} onclick={() => act("open-folder")}>Open Folder</button>
|
||||
{#if isWindows}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Lineage "subway" lane builder — pure topology, no Svelte. Shared by the in-rail
|
||||
// lane view (ImageViewer) and the pop-out full-graph panel. Consumes the hoisted
|
||||
// `lineageTree` ({ byHash, childrenOf, roots }) and a `brokenOut` Set of
|
||||
// child-subtree ROOT hashes (the nodes the user has peeled into their own columns).
|
||||
//
|
||||
// Mental model: the family is a pure tree (single parent_hash, acyclic by
|
||||
// content-sha256, never merges). A LANE is one vertical run following a primary
|
||||
// path; at each node the OTHER children are "junctions" that can break out into
|
||||
// their own lane. A column is the same recursive shape as column 0, so nested
|
||||
// sub-junctions have somewhere to attach.
|
||||
|
||||
// Climb parent_hash from `hash` to its root. Returns { root, set } where `set`
|
||||
// holds every node on the path (used as column 0's primary-path preference so the
|
||||
// displayed image always sits on the leftmost lane).
|
||||
export function ancestry(tree, hash) {
|
||||
const { byHash } = tree;
|
||||
const set = new Set();
|
||||
let cur = hash;
|
||||
let root = hash;
|
||||
while (cur && byHash.has(cur) && !set.has(cur)) {
|
||||
set.add(cur);
|
||||
root = cur;
|
||||
cur = byHash.get(cur).parent_hash;
|
||||
}
|
||||
return { root, set };
|
||||
}
|
||||
|
||||
// Build one lane from `startHash`: start → primary child → … → tip, recording each
|
||||
// off-primary fork as a junction. `spineSet` (optional) pulls the lane through a
|
||||
// preferred path — on column 0 it's the displayed image's ancestry, so the lane
|
||||
// flows root → displayed → tip; broken-out lanes pass no spineSet and follow the
|
||||
// oldest child (childrenOf is pre-sorted ascending by created_at).
|
||||
export function buildLane(tree, startHash, spineSet) {
|
||||
const { byHash, childrenOf } = tree;
|
||||
const nodes = [];
|
||||
const junctions = [];
|
||||
const guard = new Set();
|
||||
let cur = startHash;
|
||||
while (cur && byHash.has(cur) && !guard.has(cur)) {
|
||||
guard.add(cur);
|
||||
nodes.push(byHash.get(cur));
|
||||
const kids = childrenOf.get(cur) || [];
|
||||
let primary = spineSet ? kids.find(k => spineSet.has(k.hash)) : null;
|
||||
if (!primary) primary = kids[0] || null;
|
||||
const offPrimary = kids.filter(k => k !== primary).map(k => k.hash);
|
||||
if (offPrimary.length) junctions.push({ atHash: cur, childRoots: offPrimary });
|
||||
cur = primary ? primary.hash : null;
|
||||
}
|
||||
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)
|
||||
// junctionsByNode — Map<atHash, { shown:[], hidden:[] }> 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) {
|
||||
const a = ancestry(tree, anchorHash);
|
||||
const col0 = buildLane(tree, a.root, a.set);
|
||||
const columns = [];
|
||||
const edges = [];
|
||||
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 });
|
||||
}
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
rec.hidden.push(childRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(col0, 0);
|
||||
return { columns, edges, junctionsByNode, rootHash: col0.nodes.length ? col0.nodes[0].hash : null };
|
||||
}
|
||||
Reference in New Issue
Block a user