diff --git a/.comfyignore b/.comfyignore
index 74d2368..8a37e42 100644
--- a/.comfyignore
+++ b/.comfyignore
@@ -7,6 +7,10 @@
# dev test harnesses
tests/
+# GitHub repo metadata (PR template etc.) — only meaningful on GitHub, not in an
+# installed pack. CONTRIBUTING.md is kept so the contributor terms ship with it.
+.github/
+
# comfy-node-publish build artifact
node.zip
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..d143135
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,15 @@
+> **By opening this pull request, I agree to the terms in
+> [CONTRIBUTING.md](../CONTRIBUTING.md)** — including the grant that lets the
+> maintainer relicense my contribution (for example, under the MIT License).
+
+
+
+## What does this PR do?
+
+
+## Related issues
+
+
+## Checklist
+
+- [ ] I have read and agree to [CONTRIBUTING.md](../CONTRIBUTING.md).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..34e3839
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,41 @@
+# Contributing to PromptChain
+
+Thanks for your interest in contributing! PromptChain is licensed under the
+[GNU AGPL-3.0](LICENSE), and it stays that way. The terms below let the project
+keep that license today while preserving the maintainer's option to relicense
+later (for example, to a permissive license such as MIT). Please read them
+before opening a pull request.
+
+## Contributor terms
+
+By submitting a contribution to this project — a pull request, patch, or any
+other change — you agree that:
+
+1. **License.** Your contribution is licensed to the project and its users
+ under the project's current license, the GNU AGPL-3.0.
+
+2. **Relicensing grant.** In consideration of the project reviewing, merging,
+ and maintaining your contribution, you also grant the project maintainer
+ (mobcat40) and their successors and assigns a perpetual, worldwide,
+ irrevocable, non-exclusive, royalty-free, sublicensable right to use,
+ reproduce, modify, prepare derivative works of, publicly display and perform,
+ distribute, and relicense your contribution under any license — including
+ permissive licenses such as the MIT License — as part of this project or any
+ future version or derivative of it. This grant does not revoke the AGPL-3.0
+ license in point 1; it adds the maintainer's ability to also distribute your
+ contribution under other terms.
+
+3. **Right to contribute.** The contribution is your own original work (or you
+ otherwise have the right to submit it), and you have the legal right to make
+ the contribution and to grant the rights above. If your employer has any
+ rights to your contribution, you confirm you have permission to submit it
+ under these terms (or your employer has waived those rights).
+
+4. **No warranty.** Unless required by law or agreed in writing, you provide
+ your contribution on an "AS IS" basis, without warranties or conditions of
+ any kind, express or implied.
+
+If you do not agree to these terms, please do not submit a contribution.
+
+These terms apply to contributions submitted after these terms were added to
+the project. Opening a pull request is taken as your agreement to them.
diff --git a/README.md b/README.md
index 690c026..d37a3fa 100644
--- a/README.md
+++ b/README.md
@@ -112,6 +112,10 @@ Add the **Prompt Chain** node from the node menu, and onboarding takes it from t
| Prompt Chain Background Mask (Depth) | Depth-based background mask for bokeh-preserving upscales |
| Prompt Chain OpenPose / Depth Anything / Canny / Tile / Luminance / Scribble / Soft Edge / Line Art | Bundled, license-clean ControlNet image preprocessors |
+## Contributing
+
+Contributions are welcome. By opening a pull request you agree to the terms in [CONTRIBUTING.md](CONTRIBUTING.md), which keep PromptChain on AGPL-3.0 today while preserving the maintainer's option to relicense contributions (for example, to MIT) in the future.
+
## License
[AGPL-3.0](LICENSE). © 2026 mobcat40. If you run a modified PromptChain as part of a hosted service, you must offer your users the modified source.
diff --git a/data/templates/krea2-turbo-t2i-3d-regional.json b/data/templates/krea2-turbo-t2i-3d-regional.json
deleted file mode 100644
index 35681d9..0000000
--- a/data/templates/krea2-turbo-t2i-3d-regional.json
+++ /dev/null
@@ -1,270 +0,0 @@
-{
- "name": "Text-to-Image 3D Regional",
- "scope": {
- "type": "family",
- "architecture": "krea2",
- "family": "krea2_turbo"
- },
- "created_at": 1781920000,
- "nodes": [
- {
- "type": "PromptChain_RegionalConditioning",
- "offset": [
- 820,
- 120
- ],
- "size": [
- 300,
- 170
- ],
- "widgets_values": [
- 22
- ],
- "properties": {
- "Node name for S&R": "PromptChain_RegionalConditioning"
- }
- },
- {
- "type": "KSampler",
- "offset": [
- 1830,
- 60
- ],
- "size": [
- 344.15625,
- 565.328125
- ],
- "widgets_values": [
- 674787291728667,
- "randomize",
- 8,
- 1,
- "euler",
- "simple",
- 1
- ],
- "properties": {
- "Node name for S&R": "KSampler"
- }
- },
- {
- "type": "VAEDecode",
- "offset": [
- 2230,
- 60
- ],
- "size": [
- 225,
- 72
- ],
- "widgets_values": [],
- "properties": {
- "Node name for S&R": "VAEDecode"
- }
- },
- {
- "type": "SaveImage",
- "offset": [
- 2230,
- 200
- ],
- "size": [
- 247.25,
- 419.046875
- ],
- "widgets_values": [
- "Krea2/%date:yyyy-MM-dd_hhmmss%"
- ],
- "properties": {}
- },
- {
- "type": "CLIPLoader",
- "offset": [
- 820,
- 320
- ],
- "size": [
- 346.78125,
- 117.328125
- ],
- "widgets_values": [
- "qwen3vl_4b_fp8_scaled.safetensors",
- "krea2",
- "default"
- ],
- "properties": {
- "Node name for S&R": "CLIPLoader"
- }
- },
- {
- "type": "UNETLoader",
- "offset": [
- 820,
- 520
- ],
- "size": [
- 304.953125,
- 82.65625
- ],
- "widgets_values": [
- "krea2_turbo_fp8_scaled.safetensors",
- "default"
- ],
- "properties": {
- "Node name for S&R": "UNETLoader"
- }
- },
- {
- "type": "EmptySD3LatentImage",
- "offset": [
- 1180,
- 320
- ],
- "size": [
- 270,
- 144
- ],
- "widgets_values": [
- 1024,
- 1536,
- 1
- ],
- "properties": {
- "Node name for S&R": "EmptySD3LatentImage"
- }
- },
- {
- "type": "VAELoader",
- "offset": [
- 820,
- 680
- ],
- "size": [
- 246,
- 83
- ],
- "widgets_values": [
- "qwen_image_vae.safetensors"
- ],
- "properties": {
- "Node name for S&R": "VAELoader"
- }
- },
- {
- "type": "PromptChain_PoseStudio",
- "offset": [
- -1350,
- 0
- ],
- "size": [
- 360,
- 470
- ],
- "widgets_values": [
- "",
- "{\"version\":3,\"camera\":{\"position\":[-0.11421765907369465,2.2679667675054747,3.7640721683793985],\"target\":[-0.08116832790578885,0.9626632788387957,0.003294416573047413]},\"activeIndex\":1,\"figures\":[{\"uid\":4,\"body\":\"custom\",\"root\":[-0.6544460683107087,1.83557839158723e-7,-0.1296069154697062],\"rootQuat\":[0,0,0,1],\"rootScale\":[1.0847089667745293,1.0847089667745293,1.0847089667745293],\"joints\":{\"root\":[0.530589759349823,0,0,0.8476287722587585],\"shoulderL\":[-0.008142700418829918,0.012219684198498726,-0.33881765604019165,0.9407374858856201],\"elbowL\":[0.3669451177120209,0.0018381300615146756,0.0031449354719370604,0.9302355051040649],\"wristL\":[0.11023613810539246,0.41108429431915283,-0.016057291999459267,0.9047650694847107],\"shoulderR\":[-0.008142700418829918,-0.012219684198498726,0.33881765604019165,0.9407374858856201],\"elbowR\":[0.3669451177120209,-0.0018381300615146756,-0.0031449354719370604,0.9302355051040649],\"wristR\":[0.11023613810539246,-0.41108429431915283,0.016057291999459267,0.9047650694847107],\"neck\":[0.16243939101696014,0,0,0.9867185354232788],\"head\":[-0.20577044785022736,0,0,0.978600263595581],\"hipL\":[-0.9580745100975037,-0.09174012392759323,0.21390390396118164,0.16709932684898376],\"kneeL\":[0.058844611048698425,0.23217718303203583,-0.008530011400580406,0.9708544611930847],\"ankleL\":[-0.5353211164474487,-0.0010657769162207842,0.0007613753550685942,0.8446476459503174],\"hipR\":[-0.9580745100975037,0.09174012392759323,-0.21390390396118164,0.16709932684898376],\"kneeR\":[0.058844611048698425,-0.23217718303203583,0.008530011400580406,0.9708544611930847],\"ankleR\":[-0.5353211164474487,0.0010657769162207842,-0.0007613753550685942,0.8446476459503174],\"chest\":[0.008510730229318142,0,0,0.9999638199806213],\"spine\":[0.3344581127166748,0,0,0.9424106478691101],\"back\":[0,0,0,1]},\"jointPos\":{\"Root\":[0,-0.000562518835067749,6.100162863731384e-8],\"pelvis\":[0,-0.015393643795202175,0.8765072291716933],\"spine_01\":[0,0.08837844353790084,-0.0000035875321676330854],\"spine_02\":[0,0.06657692409741382,-0.0015471302564643939],\"spine_03\":[0,0.07465957864769734,-0.005422550541500943],\"spine_04\":[0,0.1554920266911116,0.008057116608445842],\"clavicle_l\":[0.022795276308897883,0.08298090375804652,0.026856951939407736],\"upperarm_l\":[-0.002402820275165144,0.14708170919523886,-0.0024412409402430058],\"lowerarm_l\":[-0.0036035171942785382,0.2335675825791744,0.007550966273508189],\"hand_l\":[-0.0022912098793312925,0.23001713307652003,0.00286084097691249],\"index_01_l\":[-0.014275551244888145,0.10155431967965949,0.00913081387989223],\"index_02_l\":[-0.00032961389198741625,0.024996880173906295,-0.00044333142189619445],\"index_03_l\":[-0.00028979239747665514,0.02228745664691208,-0.000271643739930975],\"middle_01_l\":[0.0011213888258983697,0.0983148121922568,-0.010979589074850082],\"middle_02_l\":[-0.0004448858817340806,0.032625457237979084,-0.00045865285210311413],\"middle_03_l\":[-0.0003890491255636637,0.026699312448423978,-0.00020532777125481538],\"pinky_01_l\":[0.030837264833583806,0.08091115868228371,-0.0306820534557725],\"pinky_02_l\":[-0.0004330708179622891,0.020605956754783012,-0.0003710941079286083],\"pinky_03_l\":[-0.0002436345233339428,0.015057439972830858,0.000014291528107908793],\"ring_01_l\":[0.014671692488870274,0.09115619229805816,-0.023141342176434897],\"ring_02_l\":[-0.00039252417445823095,0.028744175334092386,-0.0004921690172826243],\"ring_03_l\":[-0.00043109167184714653,0.02398470316741926,-0.0001844997920367557],\"thumb_01_l\":[-0.014337413944303988,0.037947991551694336,0.014606909942813218],\"thumb_02_l\":[0.0002165158900121842,0.029646236808427297,-0.0005050097582473731],\"thumb_03_l\":[0.0001837790356754948,0.035048878259355355,-0.0005737583730175779],\"clavicle_r\":[-0.022795276308897883,0.08298090375804652,0.026856951939407736],\"upperarm_r\":[0.002402820275165144,0.14708170919523886,-0.0024412409402430058],\"lowerarm_r\":[0.0036035171942785382,0.2335675825791744,0.007550966273508189],\"hand_r\":[0.0022912098793312925,0.23001713307652003,0.00286084097691249],\"index_01_r\":[0.014275551244888145,0.10155431967965949,0.00913081387989223],\"index_02_r\":[0.00032961389198741625,0.024996880173906295,-0.00044333142189619445],\"index_03_r\":[0.00028979239747665514,0.02228745664691208,-0.000271643739930975],\"middle_01_r\":[-0.0011213888258983697,0.0983148121922568,-0.010979589074850082],\"middle_02_r\":[0.0004448858817340806,0.032625457237979084,-0.00045865285210311413],\"middle_03_r\":[0.0003890491255636637,0.026699312448423978,-0.00020532777125481538],\"pinky_01_r\":[-0.030837264833583806,0.08091115868228371,-0.0306820534557725],\"pinky_02_r\":[0.0004330708179622891,0.020605956754783012,-0.0003710941079286083],\"pinky_03_r\":[0.0002436345233339428,0.015057439972830858,0.000014291528107908793],\"ring_01_r\":[-0.014671692488870274,0.09115619229805816,-0.023141342176434897],\"ring_02_r\":[0.00039252417445823095,0.028744175334092386,-0.0004921690172826243],\"ring_03_r\":[0.00043109167184714653,0.02398470316741926,-0.0001844997920367557],\"thumb_01_r\":[0.014337413944303988,0.037947991551694336,0.014606909942813218],\"thumb_02_r\":[-0.0002165158900121842,0.029646236808427297,-0.0005050097582473731],\"thumb_03_r\":[-0.0001837790356754948,0.035048878259355355,-0.0005737583730175779],\"neck_01\":[0,0.1554920266911116,0.008057112766740222],\"head\":[0,0.09913267713272944,-0.0024949000217020546],\"thigh_l\":[0.10406423709112762,-0.01035807409789413,-0.015486810940274154],\"calf_l\":[-0.0018221052984396628,0.38333685158674297,0.0019527052451546005],\"foot_l\":[0.0012040060549528171,0.41225113686717424,0.0060748945009739454],\"ball_l\":[-0.004144214297411963,0.1319912777786764,0.0008641794556751847],\"thigh_r\":[-0.10406423709112762,-0.01035807409789413,-0.015486810940274154],\"calf_r\":[0.0018221052984396628,0.38333685158674297,0.0019527052451546005],\"foot_r\":[-0.0012040060549528171,0.41225113686717424,0.0060748945009739454],\"ball_r\":[0.004144214297411963,0.1319912777786764,0.0008641794556751847]},\"digits\":{\"index_01_l\":[0.11009154468774796,0.47919291257858276,-0.0033358719665557146,0.8707714676856995],\"index_02_l\":[0.10867490619421005,0.0028905405197292566,0.006149682216346264,0.9940541386604309],\"index_03_l\":[0.06272030621767044,0.0006448967615142465,0.018671324476599693,0.9978562593460083],\"middle_01_l\":[0.09819573163986206,0.41311776638031006,-0.15238040685653687,0.8924525380134583],\"middle_02_l\":[0.04684380069375038,0.0005204545450396836,0.03666960448026657,0.9982287883758545],\"middle_03_l\":[0.06767940521240234,0.0009195368620567024,-0.005850149318575859,0.9976895451545715],\"pinky_01_l\":[0.04672148823738098,0.29210352897644043,-0.3033210337162018,0.9058084487915039],\"pinky_02_l\":[0.08093646168708801,0.0011062186677008867,0.024598531424999237,0.9964150786399841],\"pinky_03_l\":[-0.008727755397558212,-0.00023478623188566417,-0.01132167037576437,0.9998977780342102],\"ring_01_l\":[0.05214594677090645,0.3895569145679474,-0.2304530292749405,0.890178382396698],\"ring_02_l\":[0.059518661350011826,0.00033943928428925574,0.04314512386918068,0.9972943067550659],\"ring_03_l\":[0.05962633714079857,0.00068375637056306,-0.02453024499118328,0.9979191422462463],\"thumb_01_l\":[0.2744263708591461,0.731201171875,0.43757393956184387,0.44560524821281433],\"thumb_02_l\":[0.06898587942123413,-0.017102878540754318,-0.18289531767368317,0.9805599451065063],\"thumb_03_l\":[0.14205099642276764,-0.006924070883542299,-0.032810747623443604,0.9892911911010742],\"index_01_r\":[0.11009154468774796,-0.47919291257858276,0.0033358719665557146,0.8707714676856995],\"index_02_r\":[0.10867490619421005,-0.0028905405197292566,-0.006149682216346264,0.9940541386604309],\"index_03_r\":[0.06272030621767044,-0.0006448967615142465,-0.018671324476599693,0.9978562593460083],\"middle_01_r\":[0.09819573163986206,-0.41311776638031006,0.15238040685653687,0.8924525380134583],\"middle_02_r\":[0.04684380069375038,-0.0005204545450396836,-0.03666960448026657,0.9982287883758545],\"middle_03_r\":[0.06767940521240234,-0.0009195368620567024,0.005850149318575859,0.9976895451545715],\"pinky_01_r\":[0.04672148823738098,-0.29210352897644043,0.3033210337162018,0.9058084487915039],\"pinky_02_r\":[0.08093646168708801,-0.0011062186677008867,-0.024598531424999237,0.9964150786399841],\"pinky_03_r\":[-0.008727755397558212,0.00023478623188566417,0.01132167037576437,0.9998977780342102],\"ring_01_r\":[0.05214594677090645,-0.3895569145679474,0.2304530292749405,0.890178382396698],\"ring_02_r\":[0.059518661350011826,-0.00033943928428925574,-0.04314512386918068,0.9972943067550659],\"ring_03_r\":[0.05962633714079857,-0.00068375637056306,0.02453024499118328,0.9979191422462463],\"thumb_01_r\":[0.2744263708591461,-0.731201171875,-0.43757393956184387,0.44560524821281433],\"thumb_02_r\":[0.06898587942123413,0.017102878540754318,0.18289531767368317,0.9805599451065063],\"thumb_03_r\":[0.14205099642276764,0.006924070883542299,0.032810747623443604,0.9892911911010742],\"ball_l\":[-0.26220664381980896,0.0072789317928254604,0.029169803485274315,0.964543342590332],\"ball_r\":[-0.26220664381980896,-0.0072789317928254604,-0.029169803485274315,0.964543342590332]},\"spine\":{\"spine_01\":[0.3344581127166748,0,0,0.9424106478691101],\"spine_02\":[-0.10261907428503036,0,0,0.9947206974029541],\"spine_03\":[0.008510730229318142,0,0,0.9999638199806213],\"spine_04\":[0,0,0,1],\"neck_01\":[0.16243939101696014,0,0,0.9867185354232788],\"head\":[-0.20577044785022736,0,0,0.978600263595581]},\"morphs\":{},\"skeletalAmount\":{\"__custom\":1},\"footPins\":{},\"shoes\":false,\"sliders\":{\"gender\":0.5,\"age\":0.5,\"muscle\":0.5,\"weight\":0.5,\"height\":0.5,\"proportions\":0.5,\"african\":0.3333333333333333,\"asian\":0.3333333333333333,\"caucasian\":0.3333333333333333,\"cupsize\":0.5,\"firmness\":0.5,\"breast-trans\":0,\"breast-dist\":0,\"breast-point\":0,\"breast-volume-vert\":0,\"nipple-size\":0,\"nipple-point\":0,\"fem-apple\":0,\"fem-diamond\":0,\"fem-full-hourglass\":0,\"fem-neat-hourglass\":0,\"fem-invert-triangle\":0,\"fem-lean-column\":0,\"fem-rectangle\":0,\"fem-triangle\":0,\"fem-adrenal\":0,\"fem-liver\":0,\"fem-ovary\":0,\"fem-thyroid\":0,\"man-apple\":0,\"man-rectangle\":0,\"man-lean-column\":0,\"man-triangle\":0,\"man-invert-triangle\":0,\"man-trapezoid\":0,\"man-adrenal\":0,\"man-liver\":0,\"man-gonadal\":0,\"man-thyroid\":0,\"torso-scale-depth\":0,\"torso-scale-horiz\":0,\"torso-scale-vert\":0,\"torso-trans-horiz\":0,\"torso-trans-vert\":0,\"torso-trans-depth\":0,\"torso-vshape\":0,\"torso-dorsi\":0,\"torso-pectoral\":0,\"fingers-distance\":0,\"fingers-diameter\":0,\"fingers-length\":0,\"hand-scale\":0,\"hand-position\":0,\"hips\":0,\"glutes\":0,\"stomach\":0,\"neck-circ\":0,\"neck-height\":0,\"head-age\":0,\"head-fat\":0,\"head-angle\":0,\"head-oval\":0,\"head-round\":0,\"head-rectangular\":0,\"head-square\":0,\"head-triangular\":0,\"head-invtriangular\":0,\"head-diamond\":0,\"head-parietal\":0}},{\"uid\":5,\"body\":\"custom\",\"root\":[0.5167425402036215,1.83557839158723e-7,-0.1296069154697062],\"rootQuat\":[0,0,0,1],\"rootScale\":[1.0847089667745293,1.0847089667745293,1.0847089667745293],\"joints\":{\"root\":[0.530589759349823,0,0,0.8476287722587585],\"shoulderL\":[-0.008142700418829918,0.012219684198498726,-0.33881765604019165,0.9407374858856201],\"elbowL\":[0.3669451177120209,0.0018381300615146756,0.0031449354719370604,0.9302355051040649],\"wristL\":[0.11023613810539246,0.41108429431915283,-0.016057291999459267,0.9047650694847107],\"shoulderR\":[-0.008142700418829918,-0.012219684198498726,0.33881765604019165,0.9407374858856201],\"elbowR\":[0.3669451177120209,-0.0018381300615146756,-0.0031449354719370604,0.9302355051040649],\"wristR\":[0.11023613810539246,-0.41108429431915283,0.016057291999459267,0.9047650694847107],\"neck\":[0.16243939101696014,0,0,0.9867185354232788],\"head\":[-0.20577044785022736,0,0,0.978600263595581],\"hipL\":[-0.9580745100975037,-0.09174012392759323,0.21390390396118164,0.16709932684898376],\"kneeL\":[0.058844611048698425,0.23217718303203583,-0.008530011400580406,0.9708544611930847],\"ankleL\":[-0.5353211164474487,-0.0010657769162207842,0.0007613753550685942,0.8446476459503174],\"hipR\":[-0.9580745100975037,0.09174012392759323,-0.21390390396118164,0.16709932684898376],\"kneeR\":[0.058844611048698425,-0.23217718303203583,0.008530011400580406,0.9708544611930847],\"ankleR\":[-0.5353211164474487,0.0010657769162207842,-0.0007613753550685942,0.8446476459503174],\"chest\":[0.008510730229318142,0,0,0.9999638199806213],\"spine\":[0.3344581127166748,0,0,0.9424106478691101],\"back\":[0,0,0,1]},\"jointPos\":{\"Root\":[0,-0.000562518835067749,6.100162863731384e-8],\"pelvis\":[0,-0.015393643795202175,0.8765072291716933],\"spine_01\":[0,0.08837844353790084,-0.0000035875321676330854],\"spine_02\":[0,0.06657692409741382,-0.0015471302564643939],\"spine_03\":[0,0.07465957864769734,-0.005422550541500943],\"spine_04\":[0,0.1554920266911116,0.008057116608445842],\"clavicle_l\":[0.022795276308897883,0.08298090375804652,0.026856951939407736],\"upperarm_l\":[-0.002402820275165144,0.14708170919523886,-0.0024412409402430058],\"lowerarm_l\":[-0.0036035171942785382,0.2335675825791744,0.007550966273508189],\"hand_l\":[-0.0022912098793312925,0.23001713307652003,0.00286084097691249],\"index_01_l\":[-0.014275551244888145,0.10155431967965949,0.00913081387989223],\"index_02_l\":[-0.00032961389198741625,0.024996880173906295,-0.00044333142189619445],\"index_03_l\":[-0.00028979239747665514,0.02228745664691208,-0.000271643739930975],\"middle_01_l\":[0.0011213888258983697,0.0983148121922568,-0.010979589074850082],\"middle_02_l\":[-0.0004448858817340806,0.032625457237979084,-0.00045865285210311413],\"middle_03_l\":[-0.0003890491255636637,0.026699312448423978,-0.00020532777125481538],\"pinky_01_l\":[0.030837264833583806,0.08091115868228371,-0.0306820534557725],\"pinky_02_l\":[-0.0004330708179622891,0.020605956754783012,-0.0003710941079286083],\"pinky_03_l\":[-0.0002436345233339428,0.015057439972830858,0.000014291528107908793],\"ring_01_l\":[0.014671692488870274,0.09115619229805816,-0.023141342176434897],\"ring_02_l\":[-0.00039252417445823095,0.028744175334092386,-0.0004921690172826243],\"ring_03_l\":[-0.00043109167184714653,0.02398470316741926,-0.0001844997920367557],\"thumb_01_l\":[-0.014337413944303988,0.037947991551694336,0.014606909942813218],\"thumb_02_l\":[0.0002165158900121842,0.029646236808427297,-0.0005050097582473731],\"thumb_03_l\":[0.0001837790356754948,0.035048878259355355,-0.0005737583730175779],\"clavicle_r\":[-0.022795276308897883,0.08298090375804652,0.026856951939407736],\"upperarm_r\":[0.002402820275165144,0.14708170919523886,-0.0024412409402430058],\"lowerarm_r\":[0.0036035171942785382,0.2335675825791744,0.007550966273508189],\"hand_r\":[0.0022912098793312925,0.23001713307652003,0.00286084097691249],\"index_01_r\":[0.014275551244888145,0.10155431967965949,0.00913081387989223],\"index_02_r\":[0.00032961389198741625,0.024996880173906295,-0.00044333142189619445],\"index_03_r\":[0.00028979239747665514,0.02228745664691208,-0.000271643739930975],\"middle_01_r\":[-0.0011213888258983697,0.0983148121922568,-0.010979589074850082],\"middle_02_r\":[0.0004448858817340806,0.032625457237979084,-0.00045865285210311413],\"middle_03_r\":[0.0003890491255636637,0.026699312448423978,-0.00020532777125481538],\"pinky_01_r\":[-0.030837264833583806,0.08091115868228371,-0.0306820534557725],\"pinky_02_r\":[0.0004330708179622891,0.020605956754783012,-0.0003710941079286083],\"pinky_03_r\":[0.0002436345233339428,0.015057439972830858,0.000014291528107908793],\"ring_01_r\":[-0.014671692488870274,0.09115619229805816,-0.023141342176434897],\"ring_02_r\":[0.00039252417445823095,0.028744175334092386,-0.0004921690172826243],\"ring_03_r\":[0.00043109167184714653,0.02398470316741926,-0.0001844997920367557],\"thumb_01_r\":[0.014337413944303988,0.037947991551694336,0.014606909942813218],\"thumb_02_r\":[-0.0002165158900121842,0.029646236808427297,-0.0005050097582473731],\"thumb_03_r\":[-0.0001837790356754948,0.035048878259355355,-0.0005737583730175779],\"neck_01\":[0,0.1554920266911116,0.008057112766740222],\"head\":[0,0.09913267713272944,-0.0024949000217020546],\"thigh_l\":[0.10406423709112762,-0.01035807409789413,-0.015486810940274154],\"calf_l\":[-0.0018221052984396628,0.38333685158674297,0.0019527052451546005],\"foot_l\":[0.0012040060549528171,0.41225113686717424,0.0060748945009739454],\"ball_l\":[-0.004144214297411963,0.1319912777786764,0.0008641794556751847],\"thigh_r\":[-0.10406423709112762,-0.01035807409789413,-0.015486810940274154],\"calf_r\":[0.0018221052984396628,0.38333685158674297,0.0019527052451546005],\"foot_r\":[-0.0012040060549528171,0.41225113686717424,0.0060748945009739454],\"ball_r\":[0.004144214297411963,0.1319912777786764,0.0008641794556751847]},\"digits\":{\"index_01_l\":[0.11009154468774796,0.47919291257858276,-0.0033358719665557146,0.8707714676856995],\"index_02_l\":[0.10867490619421005,0.0028905405197292566,0.006149682216346264,0.9940541386604309],\"index_03_l\":[0.06272030621767044,0.0006448967615142465,0.018671324476599693,0.9978562593460083],\"middle_01_l\":[0.09819573163986206,0.41311776638031006,-0.15238040685653687,0.8924525380134583],\"middle_02_l\":[0.04684380069375038,0.0005204545450396836,0.03666960448026657,0.9982287883758545],\"middle_03_l\":[0.06767940521240234,0.0009195368620567024,-0.005850149318575859,0.9976895451545715],\"pinky_01_l\":[0.04672148823738098,0.29210352897644043,-0.3033210337162018,0.9058084487915039],\"pinky_02_l\":[0.08093646168708801,0.0011062186677008867,0.024598531424999237,0.9964150786399841],\"pinky_03_l\":[-0.008727755397558212,-0.00023478623188566417,-0.01132167037576437,0.9998977780342102],\"ring_01_l\":[0.05214594677090645,0.3895569145679474,-0.2304530292749405,0.890178382396698],\"ring_02_l\":[0.059518661350011826,0.00033943928428925574,0.04314512386918068,0.9972943067550659],\"ring_03_l\":[0.05962633714079857,0.00068375637056306,-0.02453024499118328,0.9979191422462463],\"thumb_01_l\":[0.2744263708591461,0.731201171875,0.43757393956184387,0.44560524821281433],\"thumb_02_l\":[0.06898587942123413,-0.017102878540754318,-0.18289531767368317,0.9805599451065063],\"thumb_03_l\":[0.14205099642276764,-0.006924070883542299,-0.032810747623443604,0.9892911911010742],\"index_01_r\":[0.11009154468774796,-0.47919291257858276,0.0033358719665557146,0.8707714676856995],\"index_02_r\":[0.10867490619421005,-0.0028905405197292566,-0.006149682216346264,0.9940541386604309],\"index_03_r\":[0.06272030621767044,-0.0006448967615142465,-0.018671324476599693,0.9978562593460083],\"middle_01_r\":[0.09819573163986206,-0.41311776638031006,0.15238040685653687,0.8924525380134583],\"middle_02_r\":[0.04684380069375038,-0.0005204545450396836,-0.03666960448026657,0.9982287883758545],\"middle_03_r\":[0.06767940521240234,-0.0009195368620567024,0.005850149318575859,0.9976895451545715],\"pinky_01_r\":[0.04672148823738098,-0.29210352897644043,0.3033210337162018,0.9058084487915039],\"pinky_02_r\":[0.08093646168708801,-0.0011062186677008867,-0.024598531424999237,0.9964150786399841],\"pinky_03_r\":[-0.008727755397558212,0.00023478623188566417,0.01132167037576437,0.9998977780342102],\"ring_01_r\":[0.05214594677090645,-0.3895569145679474,0.2304530292749405,0.890178382396698],\"ring_02_r\":[0.059518661350011826,-0.00033943928428925574,-0.04314512386918068,0.9972943067550659],\"ring_03_r\":[0.05962633714079857,-0.00068375637056306,0.02453024499118328,0.9979191422462463],\"thumb_01_r\":[0.2744263708591461,-0.731201171875,-0.43757393956184387,0.44560524821281433],\"thumb_02_r\":[0.06898587942123413,0.017102878540754318,0.18289531767368317,0.9805599451065063],\"thumb_03_r\":[0.14205099642276764,0.006924070883542299,0.032810747623443604,0.9892911911010742],\"ball_l\":[-0.26220664381980896,0.0072789317928254604,0.029169803485274315,0.964543342590332],\"ball_r\":[-0.26220664381980896,-0.0072789317928254604,-0.029169803485274315,0.964543342590332]},\"spine\":{\"spine_01\":[0.3344581127166748,0,0,0.9424106478691101],\"spine_02\":[-0.10261907428503036,0,0,0.9947206974029541],\"spine_03\":[0.008510730229318142,0,0,0.9999638199806213],\"spine_04\":[0,0,0,1],\"neck_01\":[0.16243939101696014,0,0,0.9867185354232788],\"head\":[-0.20577044785022736,0,0,0.978600263595581]},\"morphs\":{},\"skeletalAmount\":{\"__custom\":1},\"footPins\":{},\"shoes\":false,\"sliders\":{\"gender\":0.5,\"age\":0.5,\"muscle\":0.5,\"weight\":0.5,\"height\":0.5,\"proportions\":0.5,\"african\":0.3333333333333333,\"asian\":0.3333333333333333,\"caucasian\":0.3333333333333333,\"cupsize\":0.5,\"firmness\":0.5,\"breast-trans\":0,\"breast-dist\":0,\"breast-point\":0,\"breast-volume-vert\":0,\"nipple-size\":0,\"nipple-point\":0,\"fem-apple\":0,\"fem-diamond\":0,\"fem-full-hourglass\":0,\"fem-neat-hourglass\":0,\"fem-invert-triangle\":0,\"fem-lean-column\":0,\"fem-rectangle\":0,\"fem-triangle\":0,\"fem-adrenal\":0,\"fem-liver\":0,\"fem-ovary\":0,\"fem-thyroid\":0,\"man-apple\":0,\"man-rectangle\":0,\"man-lean-column\":0,\"man-triangle\":0,\"man-invert-triangle\":0,\"man-trapezoid\":0,\"man-adrenal\":0,\"man-liver\":0,\"man-gonadal\":0,\"man-thyroid\":0,\"torso-scale-depth\":0,\"torso-scale-horiz\":0,\"torso-scale-vert\":0,\"torso-trans-horiz\":0,\"torso-trans-vert\":0,\"torso-trans-depth\":0,\"torso-vshape\":0,\"torso-dorsi\":0,\"torso-pectoral\":0,\"fingers-distance\":0,\"fingers-diameter\":0,\"fingers-length\":0,\"hand-scale\":0,\"hand-position\":0,\"hips\":0,\"glutes\":0,\"stomach\":0,\"neck-circ\":0,\"neck-height\":0,\"head-age\":0,\"head-fat\":0,\"head-angle\":0,\"head-oval\":0,\"head-round\":0,\"head-rectangular\":0,\"head-square\":0,\"head-triangular\":0,\"head-invtriangular\":0,\"head-diamond\":0,\"head-parietal\":0}}],\"props\":[],\"attachments\":[],\"heads\":[{\"box\":[0.1916,0.2178,0.3413,0.3343],\"facing\":0.979},{\"box\":[0.6677,0.2175,0.817,0.3336],\"facing\":0.976}],\"regionEntities\":[{\"kind\":\"figure\",\"name\":\"mannequin1\"},{\"kind\":\"figure\",\"name\":\"mannequin2\"}]}",
- 832,
- 1216,
- ""
- ],
- "properties": {
- "Node name for S&R": "PromptChain_PoseStudio",
- "pcrOutputMode": "depth",
- "pcrTrueDepth": true
- }
- },
- {
- "type": "PreviewImage",
- "offset": [
- -945,
- 0
- ],
- "size": [
- 250,
- 360
- ],
- "widgets_values": [],
- "properties": {
- "Node name for S&R": "PreviewImage"
- }
- }
- ],
- "connections": [
- {
- "from_node_idx": 4,
- "from_slot": 0,
- "to_node_idx": 0,
- "to_slot": 0
- },
- {
- "from_node_idx": 8,
- "from_slot": 2,
- "to_node_idx": 0,
- "to_slot": 2
- },
- {
- "from_node_idx": 8,
- "from_slot": 1,
- "to_node_idx": 0,
- "to_slot": 3
- },
- {
- "from_node_idx": 0,
- "from_slot": 0,
- "to_node_idx": 1,
- "to_slot": 1
- },
- {
- "from_node_idx": 0,
- "from_slot": 1,
- "to_node_idx": 1,
- "to_slot": 2
- },
- {
- "from_node_idx": 5,
- "from_slot": 0,
- "to_node_idx": 1,
- "to_slot": 0
- },
- {
- "from_node_idx": 6,
- "from_slot": 0,
- "to_node_idx": 1,
- "to_slot": 3
- },
- {
- "from_node_idx": 1,
- "from_slot": 0,
- "to_node_idx": 2,
- "to_slot": 0
- },
- {
- "from_node_idx": 7,
- "from_slot": 0,
- "to_node_idx": 2,
- "to_slot": 1
- },
- {
- "from_node_idx": 2,
- "from_slot": 0,
- "to_node_idx": 3,
- "to_slot": 0
- },
- {
- "from_node_idx": 8,
- "from_slot": 0,
- "to_node_idx": 9,
- "to_slot": 0
- }
- ],
- "anchorConnections": [
- {
- "from_slot": 3,
- "to_node_idx": 0,
- "to_slot": 1
- }
- ],
- "id": "krea2-turbo-t2i-3d-regional",
- "category": "Generation",
- "order": 2
-}
\ No newline at end of file
diff --git a/docs/RELICENSING.md b/docs/RELICENSING.md
new file mode 100644
index 0000000..8906f95
--- /dev/null
+++ b/docs/RELICENSING.md
@@ -0,0 +1,43 @@
+# Relicensing checklist (maintainer)
+
+PromptChain ships under [AGPL-3.0](../LICENSE). This note records what must be
+true to relicense the project — or a future version — under a permissive license
+such as MIT. It is a maintainer reference, not part of the contributor terms.
+
+Two independent things must **both** hold. The contributor grant only handles
+the first; the second is on you regardless of any contributor terms.
+
+## 1. Every contributor's code is relicensable
+
+- Outside contributions are covered by the relicensing grant in
+ [CONTRIBUTING.md](../CONTRIBUTING.md) **only if** they were submitted after
+ that file existed and the contributor accepted the terms (by opening a PR).
+- **Invariant:** no outside contribution may predate the CONTRIBUTING.md commit.
+ History is currently 100% single-author (mobcat40), so this holds. If it ever
+ stops holding, get an explicit written grant from that author before
+ relicensing — the grant cannot reach backwards.
+- The grant is a *license*, not an assignment: contributors keep copyright, and
+ the AGPL availability of already-published versions cannot be withdrawn. That
+ does not block offering *future* versions under MIT.
+- Route all outside contributions through PRs so the assent trigger always
+ fires. A DCO ("Signed-off-by") is **not** a substitute — it certifies origin,
+ not a relicensing right.
+- Strongest hardening, cheapest before outside PRs arrive: enable **CLA
+ Assistant** as a required status check, so each contributor's grant is backed
+ by a recorded, identity-bound signature rather than only the PR-template line.
+
+## 2. Every bundled / vendored third-party component is MIT-compatible
+
+The contributor grant covers only what contributors submit — it does nothing for
+third-party code the project incorporates. A whole-project MIT relicense
+additionally requires that **every** copyrightable component be MIT-compatible.
+
+- Copyleft third-party code (GPL / AGPL / LGPL source) **cannot** be relicensed
+ to MIT. Keep it as an *external dependency* loaded at runtime, not vendored
+ into this tree. Vendoring a copyleft node pack's source would block a clean
+ MIT relicense — watch this when doing full-install / vendoring work.
+- Permissive third-party code (MIT / BSD / Apache-2.0) may be included, but its
+ own license notices and attribution must be retained. The result is then
+ "MIT project + retained third-party notices," not a single bare MIT file.
+- Before relicensing, inventory `bundled_packs/`, `vendor/`, and
+ `requirements.txt` and confirm each component's license.
diff --git a/js/lib/asset-drop.js b/js/lib/asset-drop.js
index 2639237..0b46986 100644
--- a/js/lib/asset-drop.js
+++ b/js/lib/asset-drop.js
@@ -108,12 +108,15 @@ async function handleDrop(e) {
if (!fetchResponse.ok) throw new Error(`Failed to fetch: ${fetchResponse.status}`);
const blob = await fetchResponse.blob();
- // upload to ComfyUI's input folder
+ // Upload into the input ROOT, not a subfolder. Stock LoadImage's INPUT_TYPES
+ // only enumerates top-level input files, so a "subfolder/name" combo value is
+ // never in its option list — the node loads flagged/errored even though the
+ // backend's VALIDATE_INPUTS (exists_annotated_filepath) still finds the file
+ // and the graph runs. A bare filename keeps the combo valid.
const filename = assetData.path.split("/").pop();
const file = new File([blob], filename, { type: blob.type });
const formData = new FormData();
formData.append("image", file);
- formData.append("subfolder", "promptchain_assets");
formData.append("type", "input");
formData.append("overwrite", "true");
const uploadResponse = await api.fetchApi("/upload/image", { method: "POST", body: formData });
diff --git a/js/lib/model-settings-bridge.js b/js/lib/model-settings-bridge.js
index c3af9f3..bf5da04 100644
--- a/js/lib/model-settings-bridge.js
+++ b/js/lib/model-settings-bridge.js
@@ -1816,9 +1816,10 @@ function injectControlNet(pcNode, controlType) {
if (!graph) return false;
const LG = window.LiteGraph || graph.constructor?.LiteGraph;
if (!LG) return false;
- // "depth-pose" is the depth branch with a Pose Studio figure as the image
- // source instead of a LoadImage — the rendered figure goes THROUGH
- // DepthAnything just like an uploaded photo would.
+ // "depth-pose" is the depth branch sourced from a 3D Poser figure instead of a
+ // LoadImage. The Poser renders its OWN true geometric depth, so it wires straight
+ // into Apply ControlNet — no DepthAnything estimator (that exists only for the
+ // uploaded-image branches, which have no scene geometry to read).
const usePose = controlType === "depth-pose";
const cfg = CONTROLNET_TYPES[usePose ? "depth" : controlType];
if (!cfg) return false;
@@ -1838,12 +1839,16 @@ function injectControlNet(pcNode, controlType) {
if (!posSrc?.node || !negSrc?.node) return false;
// Image source: a shared LoadImage (uploaded reference) reused across stacked
- // branches — or, for "depth + 3D pose", a Pose Studio node whose rendered figure
- // feeds the depth preprocessor. Pose nodes aren't shared (one figure per branch).
+ // branches — or, for "depth + 3D pose", a Pose Studio node whose true-depth
+ // render IS the control map. Pose nodes aren't shared (one figure per branch).
let loadImage = null, newLoadImage = false, poseNode = null;
if (usePose) {
poseNode = LG.createNode("PromptChain_PoseStudio");
if (!poseNode) return false;
+ // Switch the Poser to true geometric depth before its mount seeds the first
+ // capture (read live from pcrOutputMode) — the default disparity curve matches
+ // what the depth ControlNets were trained on.
+ poseNode.properties = { ...(poseNode.properties || {}), pcrOutputMode: "depth", pcrTrueDepth: true };
} else {
loadImage = graph._nodes?.find(n => n.comfyClass === "LoadImage" && n.title === "ControlNet Reference");
newLoadImage = !loadImage;
@@ -1851,12 +1856,13 @@ function injectControlNet(pcNode, controlType) {
}
const sourceNode = poseNode || loadImage;
- const preNode = LG.createNode(resolvePre(cfg.pre));
+ // Pose-direct depth needs no preprocessor — the Poser's IMAGE already IS the map.
+ const preNode = usePose ? null : LG.createNode(resolvePre(cfg.pre));
const loaderNode = LG.createNode("ControlNetLoader");
const applyNode = LG.createNode("ControlNetApplyAdvanced");
const setUnion = cfg.unionType ? LG.createNode("SetUnionControlNetType") : null;
const preview = LG.createNode("PreviewImage");
- if (!preNode || !loaderNode || !applyNode) return false;
+ if ((!usePose && !preNode) || !loaderNode || !applyNode) return false;
if (cfg.unionType && !setUnion) return false;
// Layout mirrors the hand-arranged reference (janku-depth-3): the cluster
@@ -1869,7 +1875,7 @@ function injectControlNet(pcNode, controlType) {
// Loader sits just below the preprocessor, whose height varies a lot by type
// (pose tallest, scribble shortest) — derive from the live node height so one
// rule fits all controls. SetUnion is 150 below the loader.
- const preH = preNode.size?.[1] || 130;
+ const preH = preNode?.size?.[1] || 130;
const loaderY = preH + 65;
const setUnionY = loaderY + 150;
if (poseNode) {
@@ -1882,12 +1888,12 @@ function injectControlNet(pcNode, controlType) {
graph.add(loadImage);
loadImage.size = [250, 340];
}
- preNode.pos = [baseX - 643, baseY + dy];
+ if (preNode) preNode.pos = [baseX - 643, baseY + dy];
loaderNode.pos = [baseX - 633, baseY + loaderY + dy];
if (setUnion) setUnion.pos = [baseX - 633, baseY + setUnionY + dy];
applyNode.pos = [baseX - 313, baseY + dy];
if (preview) preview.pos = [baseX - 933, baseY + dy];
- graph.add(preNode);
+ if (preNode) graph.add(preNode);
graph.add(loaderNode);
if (setUnion) graph.add(setUnion);
graph.add(applyNode);
@@ -1895,7 +1901,7 @@ function injectControlNet(pcNode, controlType) {
// Sizes set after add so the litegraph size setter runs on a registered node.
// Preprocessor: fix width, keep its auto height (varies by widget count —
// canny has more rows than depth).
- preNode.size = [291, preNode.size?.[1] ?? 130];
+ if (preNode) preNode.size = [291, preNode.size?.[1] ?? 130];
loaderNode.size = [270, 83];
if (setUnion) setUnion.size = [270, 83];
applyNode.size = [270, 240];
@@ -1917,15 +1923,22 @@ function injectControlNet(pcNode, controlType) {
else if (w.name === "start_percent") { w.value = 0.0; w.callback?.(0.0); }
else if (w.name === "end_percent") { w.value = cfg.end; w.callback?.(cfg.end); }
}
- for (const w of preNode.widgets || []) {
+ for (const w of preNode?.widgets || []) {
if (w.name === "resolution") { w.value = 1024; w.callback?.(1024); }
// DWPose ships a toggle specifically for xinsir ControlNet stick scaling.
else if (w.name === "scale_stick_for_xinsr_cn") { w.value = "enable"; w.callback?.("enable"); }
}
- sourceNode.connect(0, preNode, preNode.inputs?.findIndex(i => i.name === "image") ?? 0);
- preNode.connect(0, applyNode, applyNode.inputs?.findIndex(i => i.name === "image") ?? 3);
- if (preview) preNode.connect(0, preview, 0); // show the preprocessed map
+ const applyImageIdx = applyNode.inputs?.findIndex(i => i.name === "image") ?? 3;
+ if (preNode) {
+ sourceNode.connect(0, preNode, preNode.inputs?.findIndex(i => i.name === "image") ?? 0);
+ preNode.connect(0, applyNode, applyImageIdx);
+ if (preview) preNode.connect(0, preview, 0); // show the preprocessed map
+ } else {
+ // Pose-direct: the Poser's true-depth IMAGE is the control map itself.
+ sourceNode.connect(0, applyNode, applyImageIdx);
+ if (preview) sourceNode.connect(0, preview, 0); // the depth-map flash
+ }
if (setUnion) {
loaderNode.connect(0, setUnion, 0);
setUnion.connect(0, applyNode, applyNode.inputs?.findIndex(i => i.name === "control_net") ?? 2);
@@ -2344,7 +2357,11 @@ function injectFluxControlNet(pcNode, controlType) {
if (!graph) return false;
const LG = window.LiteGraph || graph.constructor?.LiteGraph;
if (!LG) return false;
- const cfg = FLUX_CONTROLNET_TYPES[controlType];
+ // "depth-pose" = the depth branch sourced from a 3D Poser figure (true geometric
+ // depth) instead of a LoadImage — wired straight into Apply ControlNet.
+ const usePose = controlType === "depth-pose";
+ const baseControl = usePose ? "depth" : controlType;
+ const cfg = FLUX_CONTROLNET_TYPES[baseControl];
if (!cfg) return false;
const downstream = findDownstreamNodes(pcNode);
@@ -2373,47 +2390,62 @@ function injectFluxControlNet(pcNode, controlType) {
}
}
- // Reuse one shared reference image across stacked branches.
- let loadImage = graph._nodes?.find(n => n.comfyClass === "LoadImage" && n.title === "ControlNet Reference");
- const newLoadImage = !loadImage;
- if (!loadImage) loadImage = LG.createNode("LoadImage");
+ // Image source: a 3D Poser figure (depth + 3D pose) whose true-depth render IS
+ // the control map — or a shared LoadImage reference reused across stacked branches.
+ let loadImage = null, newLoadImage = false, poseNode = null;
+ if (usePose) {
+ poseNode = LG.createNode("PromptChain_PoseStudio");
+ if (!poseNode) return false;
+ // True geometric depth, set before its mount seeds the first capture.
+ poseNode.properties = { ...(poseNode.properties || {}), pcrOutputMode: "depth", pcrTrueDepth: true };
+ } else {
+ loadImage = graph._nodes?.find(n => n.comfyClass === "LoadImage" && n.title === "ControlNet Reference");
+ newLoadImage = !loadImage;
+ if (!loadImage) loadImage = LG.createNode("LoadImage");
+ }
+ const sourceNode = poseNode || loadImage;
- const preNode = LG.createNode(resolvePre(cfg.pre));
+ // Pose-direct depth needs no preprocessor — the Poser's IMAGE already IS the map.
+ const preNode = usePose ? null : LG.createNode(resolvePre(cfg.pre));
const loaderNode = LG.createNode("ControlNetLoader");
const applyNode = LG.createNode("ControlNetApplyAdvanced");
const preview = LG.createNode("PreviewImage");
- if (!preNode || !loaderNode || !applyNode) return false;
+ if ((!usePose && !preNode) || !loaderNode || !applyNode) return false;
// Same left-of-node layout as the SDXL cluster, minus the SetUnion node.
const existingApplies = downstream.filter(n => n.comfyClass === "ControlNetApplyAdvanced").length;
const baseX = pcNode.pos?.[0] ?? 0;
const baseY = pcNode.pos?.[1] ?? 0;
const dy = existingApplies * 660;
- const preH = preNode.size?.[1] || 130;
+ const preH = preNode?.size?.[1] || 130;
const loaderY = preH + 65;
- if (newLoadImage) {
+ if (poseNode) {
+ poseNode.pos = [baseX - 1383, baseY + dy];
+ graph.add(poseNode);
+ poseNode.size = [360, 470];
+ } else if (newLoadImage) {
loadImage.title = "ControlNet Reference";
loadImage.pos = [baseX - 1223, baseY];
graph.add(loadImage);
loadImage.size = [250, 340];
}
- preNode.pos = [baseX - 643, baseY + dy];
+ if (preNode) preNode.pos = [baseX - 643, baseY + dy];
loaderNode.pos = [baseX - 633, baseY + loaderY + dy];
applyNode.pos = [baseX - 313, baseY + dy];
if (preview) preview.pos = [baseX - 933, baseY + dy];
- graph.add(preNode);
+ if (preNode) graph.add(preNode);
graph.add(loaderNode);
graph.add(applyNode);
if (preview) graph.add(preview);
- preNode.size = [291, preNode.size?.[1] ?? 130];
+ if (preNode) preNode.size = [291, preNode.size?.[1] ?? 130];
loaderNode.size = [270, 83];
applyNode.size = [270, 240];
if (preview) preview.size = [250, 360];
// Flux branches carry a "flux-" prefixed control type so their settings
// sections + recommended markers stay distinct from the SDXL defaults.
- const settingsType = `flux-${controlType}`;
- applyNode.title = `ControlNet — ${CN_LABELS[settingsType] || controlType}`;
+ const settingsType = `flux-${baseControl}`;
+ applyNode.title = `ControlNet — ${usePose ? "Depth (pose)" : (CN_LABELS[settingsType] || controlType)}`;
applyNode.properties = { ...(applyNode.properties || {}), pcrControlType: settingsType };
setControlNetModel(loaderNode, "fluxunion");
@@ -2422,13 +2454,20 @@ function injectFluxControlNet(pcNode, controlType) {
else if (w.name === "start_percent") { w.value = 0.0; w.callback?.(0.0); }
else if (w.name === "end_percent") { w.value = cfg.end; w.callback?.(cfg.end); }
}
- for (const w of preNode.widgets || []) {
+ for (const w of preNode?.widgets || []) {
if (w.name === "resolution") { w.value = 1024; w.callback?.(1024); }
}
- loadImage.connect(0, preNode, preNode.inputs?.findIndex(i => i.name === "image") ?? 0);
- preNode.connect(0, applyNode, applyNode.inputs?.findIndex(i => i.name === "image") ?? 3);
- if (preview) preNode.connect(0, preview, 0);
+ const applyImageIdx = applyNode.inputs?.findIndex(i => i.name === "image") ?? 3;
+ if (preNode) {
+ sourceNode.connect(0, preNode, preNode.inputs?.findIndex(i => i.name === "image") ?? 0);
+ preNode.connect(0, applyNode, applyImageIdx);
+ if (preview) preNode.connect(0, preview, 0);
+ } else {
+ // Pose-direct: the Poser's true-depth IMAGE is the control map itself.
+ sourceNode.connect(0, applyNode, applyImageIdx);
+ if (preview) sourceNode.connect(0, preview, 0);
+ }
loaderNode.connect(0, applyNode, applyNode.inputs?.findIndex(i => i.name === "control_net") ?? 2);
if (vaeSrc?.node) {
const vaeIdx = applyNode.inputs?.findIndex(i => i.name === "vae") ?? -1;
@@ -2452,9 +2491,10 @@ function injectZImageControlNet(pcNode, controlType, isBase = false) {
if (!graph) return false;
const LG = window.LiteGraph || graph.constructor?.LiteGraph;
if (!LG) return false;
- // "depth-pose" = the depth branch driven by a 3D Poser figure instead of a
- // LoadImage — the rendered figure goes THROUGH the depth preprocessor, and its
- // MASKS/POSE outputs are what a follow-up Regional inject binds to.
+ // "depth-pose" = the depth branch sourced from a 3D Poser figure instead of a
+ // LoadImage. The Poser emits its own true geometric depth, so it wires straight
+ // into ZImageFunControlnet (no DepthAnything); its MASKS/POSE outputs are what a
+ // follow-up Regional inject binds to.
const usePose = controlType === "depth-pose";
const baseControl = usePose ? "depth" : controlType;
const cfg = ZIMAGE_CONTROLNET_TYPES[baseControl];
@@ -2487,6 +2527,9 @@ function injectZImageControlNet(pcNode, controlType, isBase = false) {
if (usePose) {
poseNode = LG.createNode("PromptChain_PoseStudio");
if (!poseNode) return false;
+ // Poser emits its own true geometric depth → straight into ZImageFunControlnet,
+ // no DepthAnything re-estimation. Set before its mount seeds the first capture.
+ poseNode.properties = { ...(poseNode.properties || {}), pcrOutputMode: "depth", pcrTrueDepth: true };
} else {
loadImage = graph._nodes?.find(n => n.comfyClass === "LoadImage" && n.title === "ControlNet Reference");
newLoadImage = !loadImage;
@@ -2494,17 +2537,18 @@ function injectZImageControlNet(pcNode, controlType, isBase = false) {
}
const sourceNode = poseNode || loadImage;
- const preNode = LG.createNode(resolvePre(cfg.pre));
+ // Pose-direct depth needs no preprocessor — the Poser's IMAGE already IS the map.
+ const preNode = usePose ? null : LG.createNode(resolvePre(cfg.pre));
const patchLoader = LG.createNode("ModelPatchLoader");
const zcnNode = LG.createNode("ZImageFunControlnet");
const preview = LG.createNode("PreviewImage");
- if (!preNode || !patchLoader || !zcnNode) return false;
+ if ((!usePose && !preNode) || !patchLoader || !zcnNode) return false;
const existing = downstream.filter(n => n.comfyClass === "ZImageFunControlnet").length;
const baseX = pcNode.pos?.[0] ?? 0;
const baseY = pcNode.pos?.[1] ?? 0;
const dy = existing * 660;
- const preH = preNode.size?.[1] || 130;
+ const preH = preNode?.size?.[1] || 130;
const loaderY = preH + 65;
if (poseNode) {
poseNode.pos = [baseX - 1383, baseY + dy];
@@ -2516,15 +2560,15 @@ function injectZImageControlNet(pcNode, controlType, isBase = false) {
graph.add(loadImage);
loadImage.size = [250, 340];
}
- preNode.pos = [baseX - 643, baseY + dy];
+ if (preNode) preNode.pos = [baseX - 643, baseY + dy];
patchLoader.pos = [baseX - 633, baseY + loaderY + dy];
zcnNode.pos = [baseX - 313, baseY + dy];
if (preview) preview.pos = [baseX - 933, baseY + dy];
- graph.add(preNode);
+ if (preNode) graph.add(preNode);
graph.add(patchLoader);
graph.add(zcnNode);
if (preview) graph.add(preview);
- preNode.size = [291, preNode.size?.[1] ?? 130];
+ if (preNode) preNode.size = [291, preNode.size?.[1] ?? 130];
patchLoader.size = [270, 58];
zcnNode.size = [270, 130];
if (preview) preview.size = [250, 360];
@@ -2549,13 +2593,20 @@ function injectZImageControlNet(pcNode, controlType, isBase = false) {
for (const w of zcnNode.widgets || []) {
if (w.name === "strength") { w.value = cfg.strength; w.callback?.(cfg.strength); }
}
- for (const w of preNode.widgets || []) {
+ for (const w of preNode?.widgets || []) {
if (w.name === "resolution") { w.value = 1024; w.callback?.(1024); }
}
- sourceNode.connect(0, preNode, preNode.inputs?.findIndex(i => i.name === "image") ?? 0);
- preNode.connect(0, zcnNode, zcnNode.inputs?.findIndex(i => i.name === "image") ?? -1);
- if (preview) preNode.connect(0, preview, 0);
+ const zcnImageIdx = zcnNode.inputs?.findIndex(i => i.name === "image") ?? -1;
+ if (preNode) {
+ sourceNode.connect(0, preNode, preNode.inputs?.findIndex(i => i.name === "image") ?? 0);
+ preNode.connect(0, zcnNode, zcnImageIdx);
+ if (preview) preNode.connect(0, preview, 0);
+ } else {
+ // Pose-direct: the Poser's true-depth IMAGE feeds the Z-Image control patch.
+ sourceNode.connect(0, zcnNode, zcnImageIdx);
+ if (preview) sourceNode.connect(0, preview, 0);
+ }
patchLoader.connect(0, zcnNode, zcnNode.inputs?.findIndex(i => i.name === "model_patch") ?? 1);
if (vaeSrc?.node) vaeSrc.node.connect(vaeSrc.slot, zcnNode, zcnNode.inputs?.findIndex(i => i.name === "vae") ?? 2);
// Model line: source → ZImageFunControlnet → sampler.
@@ -3049,6 +3100,7 @@ export async function showModelModal(pcNode, modelInfo, anchorEl, { tab = null }
label: "ControlNet",
children: [
{ label: "Depth", value: "FluxControlNet:depth" },
+ { label: "Depth + 3D pose", value: "FluxControlNet:depth-pose" },
{ label: "Canny", value: "FluxControlNet:canny" },
{ label: "Pose", value: "FluxControlNet:pose" },
{ label: "Soft Edge", value: "FluxControlNet:softedge" },
diff --git a/js/lib/svelte/ConfirmModal-BGcVYndw.js b/js/lib/svelte/ConfirmModal-CtR7InHn.js
similarity index 95%
rename from js/lib/svelte/ConfirmModal-BGcVYndw.js
rename to js/lib/svelte/ConfirmModal-CtR7InHn.js
index 31bdd11..b830b89 100644
--- a/js/lib/svelte/ConfirmModal-BGcVYndw.js
+++ b/js/lib/svelte/ConfirmModal-CtR7InHn.js
@@ -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-uq4tn5Y6.js";
-import { a as action } from "./actions-WPfqiWYB.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-et9wt-4m.js";
+import { a as action } from "./actions-zjmT0mOr.js";
function portal(node) {
document.body.appendChild(node);
return { destroy() {
@@ -87,4 +87,4 @@ export {
ConfirmModal as C,
portal as p
};
-//# sourceMappingURL=ConfirmModal-BGcVYndw.js.map
+//# sourceMappingURL=ConfirmModal-CtR7InHn.js.map
diff --git a/js/lib/svelte/GeneratedGallery-D5XbkMk7.js b/js/lib/svelte/GeneratedGallery-DgtlJz3l.js
similarity index 99%
rename from js/lib/svelte/GeneratedGallery-D5XbkMk7.js
rename to js/lib/svelte/GeneratedGallery-DgtlJz3l.js
index f4b7a53..6cefc3f 100644
--- a/js/lib/svelte/GeneratedGallery-D5XbkMk7.js
+++ b/js/lib/svelte/GeneratedGallery-DgtlJz3l.js
@@ -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-uq4tn5Y6.js";
-import { s as set_style } from "./style-Boi27oOu.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-et9wt-4m.js";
+import { s as set_style } from "./style-B3hsaAru.js";
import { app } from "/scripts/app.js";
import { j as justifiedLayout } from "./justified-layout-cyVM7i96.js";
-import { a as action } from "./actions-WPfqiWYB.js";
-import { p as portal, C as ConfirmModal } from "./ConfirmModal-BGcVYndw.js";
+import { a as action } from "./actions-zjmT0mOr.js";
+import { p as portal, C as ConfirmModal } from "./ConfirmModal-CtR7InHn.js";
const GALLERY_ROW_HEIGHT_MIN = 40;
const GALLERY_ROW_HEIGHT_MAX = 400;
const ZOOM_FACTOR = 1.15;
@@ -801,4 +801,4 @@ export {
clampGalleryRowHeight as c,
zoomGalleryRowHeight as z
};
-//# sourceMappingURL=GeneratedGallery-D5XbkMk7.js.map
+//# sourceMappingURL=GeneratedGallery-DgtlJz3l.js.map
diff --git a/js/lib/svelte/Menubar-DC12j8gE.js b/js/lib/svelte/Menubar-CMm15KuK.js
similarity index 98%
rename from js/lib/svelte/Menubar-DC12j8gE.js
rename to js/lib/svelte/Menubar-CMm15KuK.js
index 301dc86..247c27b 100644
--- a/js/lib/svelte/Menubar-DC12j8gE.js
+++ b/js/lib/svelte/Menubar-CMm15KuK.js
@@ -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-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 { 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-et9wt-4m.js";
+import { o as onDestroy } from "./index-client-6amB1qrM.js";
+import { h as html, P as PopupAnchor } from "./PopupAnchor-D5Lvfjom.js";
+import { a as action } from "./actions-zjmT0mOr.js";
+import { s as set_style } from "./style-B3hsaAru.js";
import { app } from "/scripts/app.js";
-import { b as bind_value } from "./input-DFQhebEz.js";
+import { b as bind_value } from "./input-B9kD0bWJ.js";
var root_1$2 = from_html(`
`, 1);
var root_2$2 = from_html(``);
var root_5$1 = from_html(``);
@@ -606,4 +606,4 @@ export {
Menubar as M,
SearchableList as S
};
-//# sourceMappingURL=Menubar-DC12j8gE.js.map
+//# sourceMappingURL=Menubar-CMm15KuK.js.map
diff --git a/js/lib/svelte/PopupAnchor-C6OWGGTI.js b/js/lib/svelte/PopupAnchor-D5Lvfjom.js
similarity index 96%
rename from js/lib/svelte/PopupAnchor-C6OWGGTI.js
rename to js/lib/svelte/PopupAnchor-D5Lvfjom.js
index 6feb55c..31a9bb8 100644
--- a/js/lib/svelte/PopupAnchor-C6OWGGTI.js
+++ b/js/lib/svelte/PopupAnchor-D5Lvfjom.js
@@ -1,5 +1,5 @@
-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";
+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-et9wt-4m.js";
+import { a as onMount, o as onDestroy } from "./index-client-6amB1qrM.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-C6OWGGTI.js.map
+//# sourceMappingURL=PopupAnchor-D5Lvfjom.js.map
diff --git a/js/lib/svelte/SettingsSlider-CKF_XmgB.js b/js/lib/svelte/SettingsSlider-C9Ps16J6.js
similarity index 98%
rename from js/lib/svelte/SettingsSlider-CKF_XmgB.js
rename to js/lib/svelte/SettingsSlider-C9Ps16J6.js
index d68903d..ea362fa 100644
--- a/js/lib/svelte/SettingsSlider-CKF_XmgB.js
+++ b/js/lib/svelte/SettingsSlider-C9Ps16J6.js
@@ -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-uq4tn5Y6.js";
-import { s as set_style } from "./style-Boi27oOu.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-et9wt-4m.js";
+import { s as set_style } from "./style-B3hsaAru.js";
var root_2 = from_html(`
`);
var root_3 = from_html(`
`);
var root_4 = from_html(`
`);
@@ -267,4 +267,4 @@ delegate([
export {
SettingsSlider as S
};
-//# sourceMappingURL=SettingsSlider-CKF_XmgB.js.map
+//# sourceMappingURL=SettingsSlider-C9Ps16J6.js.map
diff --git a/js/lib/svelte/actions-WPfqiWYB.js b/js/lib/svelte/actions-zjmT0mOr.js
similarity index 93%
rename from js/lib/svelte/actions-WPfqiWYB.js
rename to js/lib/svelte/actions-zjmT0mOr.js
index d398fc4..dd3da02 100644
--- a/js/lib/svelte/actions-WPfqiWYB.js
+++ b/js/lib/svelte/actions-zjmT0mOr.js
@@ -1,4 +1,4 @@
-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";
+import { a4 as effect, E as untrack, a9 as render_effect, aa as deep_read_state, ab as safe_not_equal } from "./disclose-version-et9wt-4m.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-WPfqiWYB.js.map
+//# sourceMappingURL=actions-zjmT0mOr.js.map
diff --git a/js/lib/svelte/api-context-BFKo1mCD.js b/js/lib/svelte/api-context-BNqvELYR.js
similarity index 93%
rename from js/lib/svelte/api-context-BFKo1mCD.js
rename to js/lib/svelte/api-context-BNqvELYR.js
index a652f8a..b8f97a6 100644
--- a/js/lib/svelte/api-context-BFKo1mCD.js
+++ b/js/lib/svelte/api-context-BNqvELYR.js
@@ -1,4 +1,4 @@
-import { P as setContext, O as getContext } from "./disclose-version-uq4tn5Y6.js";
+import { P as setContext, O as getContext } from "./disclose-version-et9wt-4m.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-BFKo1mCD.js.map
+//# sourceMappingURL=api-context-BNqvELYR.js.map
diff --git a/js/lib/svelte/assets/promptchain-svelte.css b/js/lib/svelte/assets/promptchain-svelte.css
index 85ff5b5..eafcde9 100644
--- a/js/lib/svelte/assets/promptchain-svelte.css
+++ b/js/lib/svelte/assets/promptchain-svelte.css
@@ -902,6 +902,7 @@
display: flex; align-items: center; gap: 8px;
padding: 5px 7px; border-radius: 4px; cursor: pointer;
background: #1c1c1c; border: 1px solid #2e2e2e;
+ -webkit-user-select: none; user-select: none; /* the name text must not hijack a press-drag */
}
.pcr-ed-layer-row.svelte-1ozzqkm:hover { border-color: #3d3d3d; }
.pcr-ed-layer-row.selected.svelte-1ozzqkm { background: rgba(200, 89, 9, 0.10); border-color: #5a4630; }
@@ -919,6 +920,20 @@
}
.pcr-ed-layer-menu.svelte-1ozzqkm button:where(.svelte-1ozzqkm):hover { background: #c85909; color: #fff; }
.pcr-ed-layer-menu.svelte-1ozzqkm button.danger:where(.svelte-1ozzqkm):hover { background: #b23b3b; }
+ .pcr-ed-menu-sep.svelte-1ozzqkm { height: 1px; margin: 4px 6px; background: #3a3a3a; }
+ /* Mask actions collapse into a hover flyout (CSS-only, no timers). The flyout is
+ a DOM descendant of .pcr-ed-layer-menu so the outside-click dismiss still works. */
+ .pcr-ed-submenu-host.svelte-1ozzqkm { position: relative; display: flex; flex-direction: column; }
+ .pcr-ed-submenu-trigger.svelte-1ozzqkm { display: flex; align-items: center; justify-content: space-between; }
+ .pcr-ed-submenu-caret.svelte-1ozzqkm { color: #888; font-size: 11px; margin-left: 8px; }
+ .pcr-ed-submenu.svelte-1ozzqkm {
+ position: absolute; left: 100%; top: -5px; min-width: 160px;
+ background: #232323; border: 1px solid #444; border-radius: 6px; padding: 4px;
+ box-shadow: 0 6px 22px rgba(0, 0, 0, 0.5); display: none; flex-direction: column; z-index: 1;
+ }
+ .pcr-ed-submenu-host.flip.svelte-1ozzqkm .pcr-ed-submenu:where(.svelte-1ozzqkm) { left: auto; right: 100%; }
+ .pcr-ed-submenu-host.svelte-1ozzqkm:hover .pcr-ed-submenu:where(.svelte-1ozzqkm), .pcr-ed-submenu.svelte-1ozzqkm:hover { display: flex; }
+ .pcr-ed-submenu-host.svelte-1ozzqkm:hover .pcr-ed-submenu-trigger:where(.svelte-1ozzqkm) { background: #c85909; color: #fff; }
/* (unused) .pcr-ed-layer-bg { cursor: default; opacity: 0.85; }*/
.pcr-ed-layer-actions.svelte-1ozzqkm { display: flex; gap: 4px; }
.pcr-ed-layer-btn.svelte-1ozzqkm {
@@ -928,8 +943,8 @@
.pcr-ed-layer-btn.svelte-1ozzqkm:hover:not(:disabled) { color: #fff; border-color: #555; }
.pcr-ed-layer-btn.svelte-1ozzqkm:disabled { opacity: 0.4; cursor: default; }
.pcr-ed-layer-grip.svelte-1ozzqkm {
- flex: none; width: 12px; text-align: center;
- font-size: 11px; line-height: 1; color: #555;
+ flex: none; width: 16px; text-align: center;
+ font-size: 12px; line-height: 1; color: #666;
cursor: grab; user-select: none;
}
.pcr-ed-layer-grip.svelte-1ozzqkm:hover { color: #aaa; }
@@ -940,11 +955,19 @@
user-select: none; color: #ccc;
}
.pcr-ed-eye.off.svelte-1ozzqkm { color: #666; }
+ /* Photoshop-style layer-mask thumbnail (shows the mask shape; white glow = active paint target). */
+ .pcr-ed-mask-thumb.svelte-1ozzqkm {
+ flex: none; width: 18px; height: 18px; border-radius: 3px;
+ object-fit: cover; background: #000; border: 1px solid #555; cursor: pointer;
+ }
+ .pcr-ed-mask-thumb.svelte-1ozzqkm:hover { border-color: #888; }
+ .pcr-ed-mask-thumb.editing.svelte-1ozzqkm { border-color: #fff; box-shadow: 0 0 0 1px #fff, 0 0 0 3px rgba(255, 255, 255, 0.30); }
.pcr-ed-layer-name.svelte-1ozzqkm { flex: 1; font-size: 12px; color: #ddd; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pcr-ed-layer-blendtag.svelte-1ozzqkm { margin-left: 5px; font-size: 10px; color: #8b96a5; }
.pcr-ed-layer-rename.svelte-1ozzqkm {
flex: 1; min-width: 0; font-size: 12px; color: #eee;
background: #1b1b1b; border: 1px solid #c85909; border-radius: 3px; padding: 1px 4px;
+ -webkit-user-select: text; user-select: text; /* stays editable under the row's user-select:none */
}
.pcr-ed-blend-select.svelte-1ozzqkm {
width: 100%; margin: 0 0 4px; padding: 3px 6px; font-size: 12px;
@@ -952,6 +975,13 @@
}
.pcr-ed-blend-select.svelte-1ozzqkm:disabled { opacity: 0.45; }
.pcr-ed-layer-opacity.svelte-1ozzqkm { width: 70px; accent-color: #c85909; }
+ .pcr-ed-group-row.svelte-1ozzqkm { display: flex; align-items: center; gap: 4px; padding: 3px 5px; border: 1px solid #333; border-radius: 4px;
+ background: rgba(120, 130, 150, 0.12); -webkit-user-select: none; user-select: none; }
+ .pcr-ed-group-row.svelte-1ozzqkm .pcr-ed-eye:where(.svelte-1ozzqkm) { cursor: pointer; }
+ .pcr-ed-group-name.svelte-1ozzqkm { flex: 1; font-size: 12px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: default; }
+ .pcr-ed-group-x.svelte-1ozzqkm { background: none; border: none; color: #999; cursor: pointer; font-size: 13px; padding: 0 2px; }
+ .pcr-ed-group-x.svelte-1ozzqkm:hover { color: #fff; }
+ .pcr-ed-layer-row.in-group.svelte-1ozzqkm { margin-left: 14px; }
.pcr-ed-hist.svelte-1ozzqkm {
display: flex; flex-direction: column;
max-height: 170px; overflow-y: auto;
diff --git a/js/lib/svelte/disclose-version-uq4tn5Y6.js b/js/lib/svelte/disclose-version-et9wt-4m.js
similarity index 99%
rename from js/lib/svelte/disclose-version-uq4tn5Y6.js
rename to js/lib/svelte/disclose-version-et9wt-4m.js
index 05f1fb5..dd678cc 100644
--- a/js/lib/svelte/disclose-version-uq4tn5Y6.js
+++ b/js/lib/svelte/disclose-version-et9wt-4m.js
@@ -4282,9 +4282,9 @@ export {
untrack as E,
update as F,
set_checked as G,
- autofocus as H,
- to_array as I,
- from_svg as J,
+ to_array as H,
+ from_svg as I,
+ autofocus as J,
noop as K,
mount as L,
unmount as M,
@@ -4343,4 +4343,4 @@ export {
set_text as y,
index as z
};
-//# sourceMappingURL=disclose-version-uq4tn5Y6.js.map
+//# sourceMappingURL=disclose-version-et9wt-4m.js.map
diff --git a/js/lib/svelte/index-client-iMRCrpBY.js b/js/lib/svelte/index-client-6amB1qrM.js
similarity index 80%
rename from js/lib/svelte/index-client-iMRCrpBY.js
rename to js/lib/svelte/index-client-6amB1qrM.js
index 7f40ebc..68b525d 100644
--- a/js/lib/svelte/index-client-iMRCrpBY.js
+++ b/js/lib/svelte/index-client-6amB1qrM.js
@@ -1,4 +1,4 @@
-import { _ as component_context, a0 as lifecycle_outside_component, u as user_effect, E as untrack } from "./disclose-version-uq4tn5Y6.js";
+import { _ as component_context, a0 as lifecycle_outside_component, u as user_effect, E as untrack } from "./disclose-version-et9wt-4m.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-iMRCrpBY.js.map
+//# sourceMappingURL=index-client-6amB1qrM.js.map
diff --git a/js/lib/svelte/input-DFQhebEz.js b/js/lib/svelte/input-B9kD0bWJ.js
similarity index 97%
rename from js/lib/svelte/input-DFQhebEz.js
rename to js/lib/svelte/input-B9kD0bWJ.js
index 53cd115..dc54803 100644
--- a/js/lib/svelte/input-DFQhebEz.js
+++ b/js/lib/svelte/input-B9kD0bWJ.js
@@ -1,4 +1,4 @@
-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";
+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-et9wt-4m.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-DFQhebEz.js.map
+//# sourceMappingURL=input-B9kD0bWJ.js.map
diff --git a/js/lib/svelte/promptchain-fullscreen.js b/js/lib/svelte/promptchain-fullscreen.js
index 32c57b5..53d4ae8 100644
--- a/js/lib/svelte/promptchain-fullscreen.js
+++ b/js/lib/svelte/promptchain-fullscreen.js
@@ -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, 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-DC12j8gE.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-et9wt-4m.js";
+import { a as onMount, o as onDestroy } from "./index-client-6amB1qrM.js";
+import { s as set_style } from "./style-B3hsaAru.js";
+import { b as bind_value } from "./input-B9kD0bWJ.js";
+import { M as Menubar, S as SearchableList } from "./Menubar-CMm15KuK.js";
var root_1$4 = from_html(` `);
var root$5 = from_html(``);
function Topbar($$anchor, $$props) {
diff --git a/js/lib/svelte/promptchain-gallery.js b/js/lib/svelte/promptchain-gallery.js
index 70c9827..4d3bba0 100644
--- a/js/lib/svelte/promptchain-gallery.js
+++ b/js/lib/svelte/promptchain-gallery.js
@@ -1,5 +1,5 @@
-import { M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
-import { G as GeneratedGallery } from "./GeneratedGallery-D5XbkMk7.js";
+import { M as unmount, L as mount } from "./disclose-version-et9wt-4m.js";
+import { G as GeneratedGallery } from "./GeneratedGallery-DgtlJz3l.js";
function mountGallery(target, props) {
return mount(GeneratedGallery, { target, props });
}
diff --git a/js/lib/svelte/promptchain-model-indicator.js b/js/lib/svelte/promptchain-model-indicator.js
index d1e1c60..1f06df8 100644
--- a/js/lib/svelte/promptchain-model-indicator.js
+++ b/js/lib/svelte/promptchain-model-indicator.js
@@ -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, 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 { 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-et9wt-4m.js";
+import { s as set_style } from "./style-B3hsaAru.js";
+import { b as bind_value } from "./input-B9kD0bWJ.js";
import { e as extractPrecisions, r as resolveFilesForPrecision } from "./model-constants-cVxhUf51.js";
import { api } from "/scripts/api.js";
-import { s as safeJson, H as HttpError } from "./api-context-BFKo1mCD.js";
+import { s as safeJson, H as HttpError } from "./api-context-BNqvELYR.js";
var root_2$4 = from_html(`
`);
var root_3$4 = from_html(`
`);
var root_4$3 = from_html(`
`);
diff --git a/js/lib/svelte/promptchain-model-settings.js b/js/lib/svelte/promptchain-model-settings.js
index af80e05..9653bce 100644
--- a/js/lib/svelte/promptchain-model-settings.js
+++ b/js/lib/svelte/promptchain-model-settings.js
@@ -1,11 +1,11 @@
-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, a1 as set_selected, I as to_array, 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 { 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, a1 as set_selected, H as to_array, R as text, E as untrack, M as unmount, L as mount } from "./disclose-version-et9wt-4m.js";
+import { s as set_style } from "./style-B3hsaAru.js";
import { F as FAMILIES, A as ARCHITECTURES } from "./model-constants-cVxhUf51.js";
import { api } from "/scripts/api.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";
+import { S as SettingsSlider } from "./SettingsSlider-C9Ps16J6.js";
+import { b as bind_value } from "./input-B9kD0bWJ.js";
+import { b as bind_select_value } from "./select-Bp4FHWwK.js";
+import { a as action } from "./actions-zjmT0mOr.js";
var root$7 = from_html(``);
function RangeSlider($$anchor, $$props) {
push($$props, true);
diff --git a/js/lib/svelte/promptchain-node.js b/js/lib/svelte/promptchain-node.js
index 158460d..101875a 100644
--- a/js/lib/svelte/promptchain-node.js
+++ b/js/lib/svelte/promptchain-node.js
@@ -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, _regionPanelVisible, _hasPoseStudio, _hasRegionBox, _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, 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-DC12j8gE.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 { 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-et9wt-4m.js";
+import { u as useApi, p as provideApi } from "./api-context-BNqvELYR.js";
+import { M as Menubar } from "./Menubar-CMm15KuK.js";
+import { o as onDestroy, a as onMount } from "./index-client-6amB1qrM.js";
+import { s as set_style } from "./style-B3hsaAru.js";
+import { h as html, P as PopupAnchor } from "./PopupAnchor-D5Lvfjom.js";
import { api } from "/scripts/api.js";
-import { c as clampGalleryRowHeight, z as zoomGalleryRowHeight, G as GeneratedGallery } from "./GeneratedGallery-D5XbkMk7.js";
-import { b as bind_value } from "./input-DFQhebEz.js";
+import { c as clampGalleryRowHeight, z as zoomGalleryRowHeight, G as GeneratedGallery } from "./GeneratedGallery-DgtlJz3l.js";
+import { b as bind_value } from "./input-B9kD0bWJ.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$d = from_html(`
`);
diff --git a/js/lib/svelte/promptchain-sidebar.js b/js/lib/svelte/promptchain-sidebar.js
index c77b542..ab1b9f8 100644
--- a/js/lib/svelte/promptchain-sidebar.js
+++ b/js/lib/svelte/promptchain-sidebar.js
@@ -1,11 +1,11 @@
-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 { 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, I 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-et9wt-4m.js";
+import { a as onMount } from "./index-client-6amB1qrM.js";
+import { s as set_style } from "./style-B3hsaAru.js";
import { j as justifiedLayout } from "./justified-layout-cyVM7i96.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";
+import { a as action } from "./actions-zjmT0mOr.js";
+import { b as bind_value, c as bind_group } from "./input-B9kD0bWJ.js";
+import { p as portal, C as ConfirmModal } from "./ConfirmModal-CtR7InHn.js";
+import { s as safeJson, H as HttpError } from "./api-context-BNqvELYR.js";
const VIEW_MODES = [
{ id: "grid", label: "Grid" },
{ id: "justified", label: "Justified" },
@@ -3011,7 +3011,10 @@ function AssetBrowser($$anchor, $$props) {
const item = (_c = get(ctxMenu)) == null ? void 0 : _c.item;
if (item) {
const root2 = scopeRoots[nav.scope] || "";
- const full = root2 ? `${root2}/${item.path}` : item.path;
+ let full = root2 ? `${root2}/${item.path}` : item.path;
+ if (/^[a-zA-Z]:[/\\]/.test(full) || full.startsWith("//")) {
+ full = full.replace(/\//g, "\\");
+ }
await navigator.clipboard.writeText(full);
(_d = $$props.toast) == null ? void 0 : _d.call($$props, "info", "Path copied to clipboard");
}
diff --git a/js/lib/svelte/promptchain-tag-builder.js b/js/lib/svelte/promptchain-tag-builder.js
index 8467c36..2552cac 100644
--- a/js/lib/svelte/promptchain-tag-builder.js
+++ b/js/lib/svelte/promptchain-tag-builder.js
@@ -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, 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 { 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, H as to_array, E as untrack, o as bind_this, G as set_checked, M as unmount, L as mount } from "./disclose-version-et9wt-4m.js";
+import { a as onMount, o as onDestroy } from "./index-client-6amB1qrM.js";
+import { a as action } from "./actions-zjmT0mOr.js";
+import { a as bind_checked, b as bind_value } from "./input-B9kD0bWJ.js";
import { b as buildInsertText } from "./tag-builder-utils-ng134QDV.js";
-import { b as bind_select_value } from "./select-Dp4ExMMc.js";
+import { b as bind_select_value } from "./select-Bp4FHWwK.js";
const MULTI_SELECT_GROUPS = ["props"];
const CUSTOMIZABLE_CLOTHING_GROUPS = [
"legwear",
diff --git a/js/lib/svelte/promptchain-tag-builder2.js b/js/lib/svelte/promptchain-tag-builder2.js
index d679572..2e59672 100644
--- a/js/lib/svelte/promptchain-tag-builder2.js
+++ b/js/lib/svelte/promptchain-tag-builder2.js
@@ -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, 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 { 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, H 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-et9wt-4m.js";
+import { a as onMount } from "./index-client-6amB1qrM.js";
+import { a as action } from "./actions-zjmT0mOr.js";
+import { s as set_style } from "./style-B3hsaAru.js";
+import { a as bind_checked, b as bind_value } from "./input-B9kD0bWJ.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(`Loading…
`);
diff --git a/js/lib/svelte/promptchain-viewer.js b/js/lib/svelte/promptchain-viewer.js
index 3c01e4d..ad8a9b6 100644
--- a/js/lib/svelte/promptchain-viewer.js
+++ b/js/lib/svelte/promptchain-viewer.js
@@ -1,12 +1,12 @@
-import { b as block, B as BranchManager, d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, g as get, e as set, i as if_block, f as sibling, t as template_effect, h as set_class, j as delegated, k as append, l as pop, m as user_derived, n as child, o as bind_this, q as set_value, r as event, v as first_child, w as each, x as set_attribute, y as set_text, z as index, A as from_html, C as tick, D as comment, E as untrack, F as update, G as set_checked, H as autofocus, I as to_array, J as from_svg, $ as $window, K as noop, L as mount, M as unmount } from "./disclose-version-uq4tn5Y6.js";
-import { o as onDestroy } from "./index-client-iMRCrpBY.js";
-import { s as set_style } from "./style-Boi27oOu.js";
-import { a as action } from "./actions-WPfqiWYB.js";
-import { b as bind_value, a as bind_checked } from "./input-DFQhebEz.js";
-import { i as init_select, s as select_option, b as bind_select_value } from "./select-Dp4ExMMc.js";
-import { p as portal, C as ConfirmModal } from "./ConfirmModal-BGcVYndw.js";
-import { S as SettingsSlider } from "./SettingsSlider-CKF_XmgB.js";
-import { P as PopupAnchor, h as html } from "./PopupAnchor-C6OWGGTI.js";
+import { b as block, B as BranchManager, d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, g as get, e as set, i as if_block, f as sibling, t as template_effect, h as set_class, j as delegated, k as append, l as pop, m as user_derived, n as child, o as bind_this, q as set_value, r as event, v as first_child, w as each, x as set_attribute, y as set_text, z as index, A as from_html, C as tick, D as comment, E as untrack, F as update, G as set_checked, H as to_array, I as from_svg, J as autofocus, $ as $window, K as noop, L as mount, M as unmount } from "./disclose-version-et9wt-4m.js";
+import { o as onDestroy } from "./index-client-6amB1qrM.js";
+import { s as set_style } from "./style-B3hsaAru.js";
+import { a as action } from "./actions-zjmT0mOr.js";
+import { b as bind_value, a as bind_checked } from "./input-B9kD0bWJ.js";
+import { i as init_select, s as select_option, b as bind_select_value } from "./select-Bp4FHWwK.js";
+import { p as portal, C as ConfirmModal } from "./ConfirmModal-CtR7InHn.js";
+import { S as SettingsSlider } from "./SettingsSlider-C9Ps16J6.js";
+import { P as PopupAnchor, h as html } from "./PopupAnchor-D5Lvfjom.js";
const NAN = Symbol("NaN");
function key(node, get_key, render_fn) {
var branches = new BranchManager(node);
@@ -592,8 +592,8 @@ var root_20$4 = from_html(`M
var root_24$4 = from_html(`
`);
var root_25$3 = from_html(`
`);
var root_23$4 = from_html(`
Character lock
None keeps the character from drifting while tiles re-detail — the image itself is the reference
`);
-var root_28$3 = from_html(`
`);
-var root_29$4 = from_html(`
`);
+var root_28$4 = from_html(`
`);
+var root_29$3 = from_html(`
`);
var root_30$3 = from_html(`
Workflow prompt (reference) `, 1);
var root_26$4 = from_html(`
`);
var root_32$3 = from_html(`
This quality is bigger than the region's footprint — how should it land?
The whole document scales up and the region lands pixel-for-pixel — full quality kept. The UltraSharp-enlarged image becomes the new Background, so erasing the upscale layer reveals it.
The render is rescaled back into the region — re-detailed, but softer than 1:1.
`);
@@ -619,7 +619,7 @@ var root_52$2 = from_html(`
Ca
var root_54$2 = from_html(`Add to Edit `);
var root_55$2 = from_html(`View result `);
var root_53$2 = from_html(`Close `, 1);
-var root_57$2 = from_html(`Create Workflow `);
+var root_57$1 = from_html(`Create Workflow `);
var root_56$2 = from_html(`Cancel Apply `, 1);
var root_1$9 = from_html(``);
function UpscaleOptionsModal($$anchor, $$props) {
@@ -1795,12 +1795,12 @@ function UpscaleOptionsModal($$anchor, $$props) {
var node_23 = first_child(fragment_4);
{
var consequent_22 = ($$anchor5) => {
- var div_33 = root_28$3();
+ var div_33 = root_28$4();
action(div_33, ($$node) => promptEditor == null ? void 0 : promptEditor($$node));
append($$anchor5, div_33);
};
var alternate_1 = ($$anchor5) => {
- var textarea = root_29$4();
+ var textarea = root_29$3();
bind_value(textarea, () => get(prompt), ($$value) => set(prompt, $$value));
append($$anchor5, textarea);
};
@@ -2346,7 +2346,7 @@ function UpscaleOptionsModal($$anchor, $$props) {
var node_48 = sibling(button_9, 2);
{
var consequent_43 = ($$anchor4) => {
- var button_10 = root_57$2();
+ var button_10 = root_57$1();
delegated("click", button_10, () => confirm("workflow"));
append($$anchor4, button_10);
};
@@ -2428,11 +2428,11 @@ var root_23$3 = from_html(` this image (default) `, 1);
var root_27$3 = from_html(` `);
var root_26$3 = from_html(`Weight type Start
End
`, 1);
-var root_28$2 = from_html(`Fidelity
Start
End
`, 1);
+var root_28$3 = from_html(`Fidelity
Start
End
`, 1);
var root_22$2 = from_html(`Choose image
Strength
`, 1);
var root_20$3 = from_html(`Condition
Inpaint Inpaint + PuLID Inpaint + Style Reference `);
var root_30$2 = from_html(` `);
-var root_29$3 = from_html(``);
+var root_29$2 = from_html(``);
var root_32$2 = from_html(` `);
var root_33$2 = from_html(` `);
var root_31$2 = from_html(`
`, 1);
@@ -3815,7 +3815,7 @@ function InpaintModal($$anchor, $$props) {
append($$anchor6, fragment_8);
};
var alternate_3 = ($$anchor6) => {
- var fragment_9 = root_28$2();
+ var fragment_9 = root_28$3();
var div_38 = first_child(fragment_9);
var node_23 = sibling(child(div_38), 2);
SettingsSlider(node_23, {
@@ -3919,7 +3919,7 @@ function InpaintModal($$anchor, $$props) {
var node_26 = sibling(node_15, 2);
{
var consequent_22 = ($$anchor3) => {
- var div_41 = root_29$3();
+ var div_41 = root_29$2();
var select_2 = sibling(child(div_41), 2);
each(select_2, 21, () => MODE_ROWS, index, ($$anchor4, row) => {
var option_4 = root_30$2();
@@ -4383,8 +4383,8 @@ var root_22$1 = from_html(`Steps
CFG
On the source model, 0 = inherit the image's own steps/cfg.
`, 1);
var root_26$2 = from_html(`
`);
var root_27$2 = from_html(`
Stop `);
-var root_29$2 = from_html(`
Add to Edit `);
-var root_28$1 = from_html(`
Cancel `, 1);
+var root_29$1 = from_html(`
Add to Edit `);
+var root_28$2 = from_html(`
Cancel `, 1);
var root_1$7 = from_html(`
`);
function I2IModal($$anchor, $$props) {
push($$props, true);
@@ -5213,14 +5213,14 @@ function I2IModal($$anchor, $$props) {
append($$anchor3, button_3);
};
var alternate_3 = ($$anchor3) => {
- var fragment_7 = root_28$1();
+ var fragment_7 = root_28$2();
var button_4 = first_child(fragment_7);
var button_5 = sibling(button_4, 2);
var text_8 = child(button_5);
var node_23 = sibling(button_5, 2);
{
var consequent_20 = ($$anchor4) => {
- var button_6 = root_29$2();
+ var button_6 = root_29$1();
template_effect(() => button_6.disabled = !get(doneState));
delegated("click", button_6, () => onUseInEdit()(get(doneState)));
append($$anchor4, button_6);
@@ -5301,7 +5301,7 @@ function AdjustModal($$anchor, $$props) {
"sourceCanvas",
3,
null
- // flattened composite to adjust
+ // the active layer's pixels to adjust (caller scopes this)
), filename = prop($$props, "filename", 3, ""), onApply = prop(
$$props,
"onApply",
@@ -5650,6 +5650,7 @@ function AdjustModal($$anchor, $$props) {
if (cH && y > 0.75) add += cH * (1 - Math.cos(Math.PI * (y - 0.75) / 0.25)) / 2;
lut[i] = Math.max(0, Math.min(1, y + 0.25 * add));
}
+ for (let i = 1; i < N; i++) if (lut[i] < lut[i - 1]) lut[i] = lut[i - 1];
return lut;
}
function buildChannelLUTs(s) {
@@ -6095,61 +6096,69 @@ var root_21$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 = from_html(` `, 1);
-var root_24$1 = from_html(`Sample Point 3×3 average 5×5 average
click the image to pick a color · hold to preview · Screen pick grabs from anywhere `, 1);
+var root_24$1 = from_html(`Sample Point 3×3 average 5×5 average
From All layers Current layer
click the image to pick a color · hold to preview · Screen pick grabs from anywhere `, 1);
var root_25$1 = from_html(` `);
var root_27$1 = from_html(`Screen pick `);
var root_26$1 = from_html(`Color
click the image to pick a color
Cancel OK
`);
-var root_29$1 = from_html(` `);
-var root_30$1 = from_html(`
`);
-var root_31$1 = from_html(`
`);
-var root_32$1 = from_svg(` `);
-var root_33$1 = from_html(`
`);
+var root_28$1 = from_html(`✎ Editing layer mask — paint white to reveal, black to hide Done
`);
+var root_30$1 = from_html(` `);
+var root_31$1 = from_html(`
`);
+var root_32$1 = from_html(`
`);
+var root_33$1 = from_svg(` `);
var root_34$1 = from_html(`
`);
-var root_35$1 = from_svg(` `);
-var root_37$1 = from_html(`
`, 1);
-var root_38$1 = from_html(`
`);
-var root_36$1 = from_html(`
`, 1);
-var root_40$1 = from_html(`
`);
-var root_41$1 = from_html(`
`, 1);
-var root_39$1 = from_html(``);
-var root_42$1 = from_html(`
`);
-var root_43$1 = from_html(`
`);
-var root_44$1 = from_html(`
`);
-var root_45$1 = from_html(`
`);
-var root_46$1 = from_html(`drag / stretch · Enter applies · Esc cancels
`);
-var root_47$1 = from_html(`
`);
-var root_49$1 = from_html(`✦ Inpaint `);
-var root_50$1 = from_html(`✦ i2i Blend `);
-var root_51$1 = from_html(`✦ Upscale `);
-var root_52$1 = from_html(`✦ Re-pose `);
-var root_48$1 = from_html(``);
-var root_53$1 = from_html(`+ ⬇ ▣ 🗑 `);
-var root_55$1 = from_html(` `);
-var root_57$1 = from_html(` `);
-var root_59$1 = from_html(`▦ `);
-var root_60$1 = from_html(` `);
-var root_58$1 = from_html(` `);
-var root_56$1 = from_html(`⠿
`);
-var root_54$1 = from_html(`
`, 1);
-var root_62$1 = from_html(`
`);
-var root_61$1 = from_html(`
`);
-var root_64$1 = from_html(`Merge Layers `);
-var root_65$1 = from_html(`Merge Down `);
-var root_67$1 = from_html(` `);
-var root_68 = from_html(`Apply Layer Mask Delete Layer Mask `, 1);
-var root_69 = from_html(`Match Colors to Below `);
-var root_70$1 = from_html(` `);
-var root_63$1 = from_html(``);
-var root_72$1 = from_html(`Deselect Select Inverse `, 1);
-var root_73$1 = from_html(`Layer Via Copy Layer Via Cut `, 1);
-var root_71$1 = from_html(``);
-var root_74$1 = from_html(`
`);
-var root_75$1 = from_html(`
`);
-var root_76$1 = from_html(`⏳ Selecting subject…
`);
-var root_77$1 = from_html(` Alt-click a clean area of the image to define a source point, then paint.
OK
`);
-var root_78$1 = from_html(``);
-var root_79$1 = from_html(`✓ Edits saved `);
-var root_1$5 = from_html(``);
+var root_35$1 = from_html(`
`);
+var root_36$1 = from_svg(` `);
+var root_38$1 = from_html(`
`, 1);
+var root_39$1 = from_html(`
`);
+var root_37$1 = from_html(`
`, 1);
+var root_41$1 = from_html(`
`);
+var root_42$1 = from_html(`
`, 1);
+var root_40$1 = from_html(``);
+var root_43$1 = from_html(`
`);
+var root_44$1 = from_html(`
`);
+var root_45$1 = from_html(`
`);
+var root_46$1 = from_html(`
`);
+var root_47$1 = from_html(`drag / stretch · Enter applies · Esc cancels
`);
+var root_48$1 = from_html(`
`);
+var root_50$1 = from_html(`✦ Inpaint `);
+var root_51$1 = from_html(`✦ i2i Blend `);
+var root_52$1 = from_html(`✦ Upscale `);
+var root_53$1 = from_html(`✦ Re-pose `);
+var root_49$1 = from_html(``);
+var root_54$1 = from_html(`+ ⬇ ▣ 🗑 `);
+var root_56$1 = from_html(` `);
+var root_59$1 = from_html(` `);
+var root_60$1 = from_html(` `);
+var root_58$1 = from_html(` ⊟
`);
+var root_62$1 = from_html(` `);
+var root_63$1 = from_html(` `);
+var root_65$1 = from_html(` `);
+var root_64$1 = from_html(` `);
+var root_61$1 = from_html(`⠿
`);
+var root_55$1 = from_html(`
`, 1);
+var root_67$1 = from_html(`
`);
+var root_66$1 = from_html(`
`);
+var root_69 = from_html(`Merge Layers `);
+var root_70$1 = from_html(`Merge Down `);
+var root_71$1 = from_html(`Match Colors to Below `);
+var root_73$1 = from_html(` `);
+var root_74$1 = from_html(`Ungroup `);
+var root_72$1 = from_html(` `, 1);
+var root_76$1 = from_html(` `);
+var root_77$1 = from_html(`
`);
+var root_75$1 = from_html(` `, 1);
+var root_78$1 = from_html(` `);
+var root_68 = from_html(``);
+var root_80$1 = from_html(`Deselect Select Inverse `, 1);
+var root_81$1 = from_html(`Layer Via Copy Layer Via Cut `, 1);
+var root_79$1 = from_html(``);
+var root_82$1 = from_html(`
`);
+var root_83$1 = from_html(`
`);
+var root_84$1 = from_html(`⏳ Selecting subject…
`);
+var root_85$1 = from_html(` Alt-click a clean area of the image to define a source point, then paint.
OK
`);
+var root_86$1 = from_html(``);
+var root_87$1 = from_html(`✓ Edits saved `);
+var root_1$5 = from_html(``);
function EditModal($$anchor, $$props) {
push($$props, true);
let srcUrlProp = prop($$props, "sourceUrl", 3, ""), widthProp = prop($$props, "width", 3, 0), heightProp = prop($$props, "height", 3, 0), filename = prop($$props, "filename", 3, ""), caps = prop($$props, "caps", 3, null), fetchApi = prop($$props, "fetchApi", 3, null);
@@ -6339,6 +6348,14 @@ function EditModal($$anchor, $$props) {
1
// Pick: 1 | 3 | 5 px average
);
+ let sampleMerged = state(
+ true
+ // Pick from the flattened composite (true) or the active layer only (false)
+ );
+ let maskEdit = state(
+ false
+ // editing the active layer's MASK (paintCanvas shows it as grayscale) vs its pixels
+ );
let fillTolerance = state(
32
// Paint bucket: color-match threshold
@@ -6368,11 +6385,18 @@ function EditModal($$anchor, $$props) {
// per-inactive-layer refs (by index) — ABOVE active only
));
let belowCanvasEl;
+ let customEl;
let nextLayerNum = 0;
let selectedIds = state(proxy(
[]
// multi-selected layer ids (for merge); always includes the active layer
));
+ let groups = state(proxy(
+ []
+ // lightweight layer groups: { id, name, collapsed, visible, opacity }
+ ));
+ let nextGroupNum = 0;
+ let renamingGroupId = state(null);
let lhTab = state(
"layers"
// Layers/History share one tabbed panel (PS-style)
@@ -6412,7 +6436,7 @@ function EditModal($$anchor, $$props) {
let panning = false;
let lastPt = null;
let strokeResidual = 0;
- const HIST_MAX = 12;
+ const HIST_MAX = 30;
let history2 = state(proxy([{ label: "Open", snap: null }]));
let histIndex = state(0);
function commitAction(label) {
@@ -6426,8 +6450,10 @@ function EditModal($$anchor, $$props) {
head.push({
label,
layers: [...get(layers)],
+ groups: [...get(groups)],
activeIndex: get(activeIndex),
selectedIds: [...get(selectedIds)],
+ sel: selSnapshot(),
url: get(sourceUrl),
w: get(width),
h: get(height)
@@ -6458,12 +6484,21 @@ function EditModal($$anchor, $$props) {
const restore = () => {
var _a;
set(layers, [...h.layers], true);
+ set(
+ groups,
+ h.groups ? h.groups.map((g) => {
+ const cur = get(groups).find((x) => x.id === g.id);
+ return cur ? { ...g, collapsed: cur.collapsed } : g;
+ }) : [],
+ true
+ );
set(activeIndex, Math.min(h.activeIndex ?? 0, get(layers).length - 1), true);
set(
selectedIds,
h.selectedIds ? [...h.selectedIds] : [(_a = get(layers)[get(activeIndex)]) == null ? void 0 : _a.id].filter((x) => x != null),
true
);
+ restoreSelection(h.sel);
loadActiveBitmap();
if (dimsChanged) resetView();
};
@@ -6614,6 +6649,33 @@ function EditModal($$anchor, $$props) {
recomputeSelBBox();
update(selVersion);
}
+ let _selSnapCache = { version: -1, snap: null };
+ function selSnapshot() {
+ if (!get(selActive) || !selMaskCanvas) return null;
+ if (_selSnapCache.version === get(selVersion) && _selSnapCache.snap) return _selSnapCache.snap;
+ const snap = {
+ mask: selMaskCanvas.getContext("2d").getImageData(0, 0, get(width), get(height)),
+ bbox: selBBox ? { ...selBBox } : null
+ };
+ _selSnapCache = { version: get(selVersion), snap };
+ return snap;
+ }
+ function restoreSelection(sel) {
+ if (!sel || !sel.mask) {
+ selBBox = null;
+ set(selActive, false);
+ update(selVersion);
+ if (selMaskCanvas) selMaskCanvas.getContext("2d").clearRect(0, 0, get(width), get(height));
+ return;
+ }
+ ensureSelMask();
+ const ctx = selMaskCanvas.getContext("2d");
+ ctx.clearRect(0, 0, get(width), get(height));
+ ctx.putImageData(sel.mask, 0, 0);
+ selBBox = sel.bbox ? { ...sel.bbox } : null;
+ set(selActive, true);
+ update(selVersion);
+ }
function selectAllRegion() {
const ctx = ensureSelMask().getContext("2d");
ctx.clearRect(0, 0, get(width), get(height));
@@ -6637,7 +6699,7 @@ function EditModal($$anchor, $$props) {
}
let flatBlobCache = null;
function flatKey() {
- return `${get(histIndex)}|${get(activeIndex)}|${get(layers).map((L) => `${L.id}:${L.visible}:${L.opacity}:${L.blend || ""}:${L.ox || 0}:${L.oy || 0}`).join(",")}`;
+ return `${get(histIndex)}|${get(activeIndex)}|${get(layers).map((L) => `${L.id}:${L.visible}:${L.opacity}:${L.blend || ""}:${L.ox || 0}:${L.oy || 0}:${L.groupId || ""}`).join(",")}|${get(groups).map((g) => `${g.id}:${g.visible}:${g.opacity}`).join(",")}`;
}
async function flattenedBlobCached() {
const key2 = flatKey();
@@ -6836,14 +6898,15 @@ function EditModal($$anchor, $$props) {
if (x < 0 || y < 0 || x >= get(width) || y >= get(height)) return false;
return selMaskCanvas.getContext("2d").getImageData(x, y, 1, 1).data[3] > 127;
}
- function captureSelection() {
+ function captureSelection(merged = false) {
if (!get(selActive) || !selBBox) return null;
const bb = selBBox;
const c = document.createElement("canvas");
c.width = bb.w;
c.height = bb.h;
const cx = c.getContext("2d");
- cx.drawImage(paintCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
+ if (merged) drawFlattened(cx, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
+ else cx.drawImage(paintCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
const mask = document.createElement("canvas");
mask.width = bb.w;
mask.height = bb.h;
@@ -6891,6 +6954,7 @@ function EditModal($$anchor, $$props) {
commitAction("Delete");
}
function newLayerFromCanvas(srcCanvas, x, y, label, sourceRect = null) {
+ var _a;
flushActive();
const lc = document.createElement("canvas");
lc.width = get(width);
@@ -6907,6 +6971,7 @@ function EditModal($$anchor, $$props) {
visible: true,
opacity: 1,
sourceRect,
+ groupId: (_a = get(layers)[get(activeIndex)]) == null ? void 0 : _a.groupId,
bitmap: lc.getContext("2d").getImageData(0, 0, get(width), get(height))
},
...get(layers).slice(get(activeIndex) + 1)
@@ -6934,8 +6999,8 @@ function EditModal($$anchor, $$props) {
if (cut) eraseSelectionFromActive(cap);
newLayerFromCanvas(cap.canvas, cap.x, cap.y, cut ? "Layer via Cut" : "Layer via Copy", capSourceRect(cap));
}
- function copySelection(cut) {
- const cap = captureSelection();
+ function copySelection(cut, merged = false) {
+ const cap = captureSelection(merged);
if (!cap) return;
clipboard = {
canvas: cap.canvas,
@@ -7062,6 +7127,10 @@ function EditModal($$anchor, $$props) {
return p;
});
let mods = state(proxy({ shift: false, alt: false, ctrl: false }));
+ let spaceDown = state(
+ false
+ // hold Space → temporary Hand tool (drag to pan)
+ );
let isSelTool = user_derived(() => get(tool) === "select" || get(tool) === "lasso" || get(tool) === "objsel");
let hiddenEditBlocked = user_derived(() => {
var _a;
@@ -7084,9 +7153,11 @@ function EditModal($$anchor, $$props) {
},
true
);
+ if (e.code === "Space") set(spaceDown, e.type === "keydown");
};
const clear = () => {
set(mods, { shift: false, alt: false, ctrl: false }, true);
+ set(spaceDown, false);
};
window.addEventListener("keydown", upd, true);
window.addEventListener("keyup", upd, true);
@@ -7480,8 +7551,8 @@ function EditModal($$anchor, $$props) {
const uctx = under.getContext("2d");
for (let j = 0; j < i; j++) {
const Lj = get(layers)[j];
- if (!Lj.visible) continue;
- uctx.globalAlpha = Lj.opacity ?? 1;
+ if (!layerShown(Lj)) continue;
+ uctx.globalAlpha = layerEffOpacity(Lj);
if (Lj.isBackground && !Lj.bitmap && baseImg) {
uctx.drawImage(baseImg, 0, 0, get(width), get(height));
} else {
@@ -7618,6 +7689,7 @@ function EditModal($$anchor, $$props) {
return true;
}
function insertResultLayer(c, maskCanvas, label, underCanvas = null) {
+ var _a, _b;
let fm = null;
if (maskCanvas) {
fm = document.createElement("canvas");
@@ -7646,6 +7718,7 @@ function EditModal($$anchor, $$props) {
name: `UltraSharp ${uid}`,
visible: true,
opacity: 1,
+ groupId: (_a = get(layers)[get(activeIndex)]) == null ? void 0 : _a.groupId,
bitmap: underCanvas.getContext("2d").getImageData(0, 0, get(width), get(height))
});
}
@@ -7655,6 +7728,7 @@ function EditModal($$anchor, $$props) {
name: `${label} ${id}`,
visible: true,
opacity: 1,
+ groupId: (_b = get(layers)[get(activeIndex)]) == null ? void 0 : _b.groupId,
bitmap: c.getContext("2d").getImageData(0, 0, get(width), get(height))
});
set(
@@ -7966,6 +8040,19 @@ function EditModal($$anchor, $$props) {
icon: ` `
}
];
+ const TOOL_KEYS = {
+ v: "move",
+ m: "select",
+ l: "lasso",
+ b: "brush",
+ e: "eraser",
+ g: "bucket",
+ w: "objsel",
+ j: "spot",
+ s: "stamp",
+ c: "crop",
+ i: "eyedrop"
+ };
const ACTION_ICONS = {
undo: ` `,
redo: ` `,
@@ -8035,16 +8122,27 @@ function EditModal($$anchor, $$props) {
set(patch, null);
}
function cancelPatch() {
- var _a;
+ var _a, _b;
if (((_a = get(patch)) == null ? void 0 : _a.restore) && paintCanvas) {
const ctx = paintCanvas.getContext("2d");
ctx.clearRect(0, 0, get(width), get(height));
ctx.drawImage(get(patch).restore, 0, 0);
}
+ if ((_b = get(patch)) == null ? void 0 : _b.maskRestore) {
+ get(layers)[get(activeIndex)] = {
+ ...get(layers)[get(activeIndex)],
+ mask: get(patch).maskRestore.mask,
+ maskUrl: get(patch).maskRestore.maskUrl
+ };
+ }
set(patch, null);
}
function beginTransform() {
if (get(patch)) return true;
+ if (get(
+ maskEdit
+ // transform operates on the layer, not its mask
+ )) exitMaskEdit();
if (blockHiddenEdit("transform")) return false;
if (!paintCanvas || !get(layers)[get(activeIndex)]) return false;
flushActive();
@@ -8105,6 +8203,18 @@ function EditModal($$anchor, $$props) {
set(errorMsg, "This layer's content is off-canvas — move it back to transform.");
return false;
}
+ let maskRestore = null;
+ if (L.mask) {
+ const mc = document.createElement("canvas");
+ mc.width = get(width);
+ mc.height = get(height);
+ mc.getContext("2d").putImageData(L.mask, 0, 0);
+ pctx.globalCompositeOperation = "destination-in";
+ pctx.drawImage(mc, ox, oy);
+ pctx.globalCompositeOperation = "source-over";
+ maskRestore = { mask: L.mask, maskUrl: L.maskUrl };
+ get(layers)[get(activeIndex)] = { ...L, mask: void 0, maskUrl: void 0 };
+ }
const c = document.createElement("canvas");
c.width = dw;
c.height = dh;
@@ -8127,6 +8237,7 @@ function EditModal($$anchor, $$props) {
flipV: false,
transform: true,
restore,
+ maskRestore,
home: { x: dx0, y: dy0, w: dw, h: dh }
},
true
@@ -8276,7 +8387,6 @@ function EditModal($$anchor, $$props) {
src.height = bh;
const sctx = src.getContext("2d");
try {
- drawBackdrop(sctx, bx, by, bw, bh, 0, 0, bw, bh);
sctx.drawImage(paintCanvas, bx, by, bw, bh, 0, 0, bw, bh);
} catch (e) {
console.warn("[PromptChain] smooth lift failed", e);
@@ -8347,7 +8457,6 @@ function EditModal($$anchor, $$props) {
c.height = get(height);
const ctx = c.getContext("2d", { willReadFrequently: true });
try {
- drawBackdrop(ctx, 0, 0, get(width), get(height), 0, 0, get(width), get(height));
ctx.drawImage(paintCanvas, 0, 0);
} catch (e) {
console.warn("[PromptChain] liquify session failed", e);
@@ -8558,6 +8667,7 @@ function EditModal($$anchor, $$props) {
o[idx] = s[j];
o[idx + 1] = s[j + 1];
o[idx + 2] = s[j + 2];
+ o[idx + 3] = s[j + 3];
} else {
const fx = Math.min(maxX, Math.max(0, X + dx));
const fy = Math.min(maxY, Math.max(0, Y + dy));
@@ -8570,11 +8680,15 @@ function EditModal($$anchor, $$props) {
o[idx] = s[i00] * w00 + s[i10] * w10 + s[i01] * w01 + s[i11] * w11;
o[idx + 1] = s[i00 + 1] * w00 + s[i10 + 1] * w10 + s[i01 + 1] * w01 + s[i11 + 1] * w11;
o[idx + 2] = s[i00 + 2] * w00 + s[i10 + 2] * w10 + s[i01 + 2] * w01 + s[i11 + 2] * w11;
+ o[idx + 3] = s[i00 + 3] * w00 + s[i10 + 3] * w10 + s[i01 + 3] * w01 + s[i11 + 3] * w11;
}
- o[idx + 3] = 255;
}
}
paintCanvas.getContext("2d").putImageData(img, x0, y0);
+ if (get(
+ hasCustomBlend
+ // refresh the single-canvas custom-blend composite mid-stroke
+ )) update(customTick);
}
let cropBox = state(
null
@@ -8604,6 +8718,10 @@ function EditModal($$anchor, $$props) {
}
function setTool(id) {
commitPatch();
+ if (get(
+ maskEdit
+ // these don't operate on a mask
+ ) && (id === "adjust" || id === "crop" || id === "liquify")) exitMaskEdit();
if (id === "adjust") {
openAdjust();
return;
@@ -8623,7 +8741,6 @@ function EditModal($$anchor, $$props) {
c.height = get(height);
const ctx = c.getContext("2d", { willReadFrequently: true });
try {
- drawBackdrop(ctx, 0, 0, get(width), get(height), 0, 0, get(width), get(height));
ctx.drawImage(paintCanvas, 0, 0);
} catch (e) {
console.warn("[PromptChain] adjust open failed", e);
@@ -8633,7 +8750,22 @@ function EditModal($$anchor, $$props) {
set(adjustOpen, true);
}
function onAdjustApply(imageData) {
- paintCanvas.getContext("2d").putImageData(imageData, 0, 0);
+ const ctx = paintCanvas.getContext("2d");
+ if (get(selActive) && selMaskCanvas) {
+ const orig = ctx.getImageData(0, 0, get(width), get(height)).data;
+ const sel = selMaskCanvas.getContext("2d").getImageData(0, 0, get(width), get(height)).data;
+ const a = imageData.data;
+ for (let i = 0; i < a.length; i += 4) {
+ const t = sel[i + 3] / 255;
+ if (t >= 1) continue;
+ const it = 1 - t;
+ a[i] = a[i] * t + orig[i] * it;
+ a[i + 1] = a[i + 1] * t + orig[i + 1] * it;
+ a[i + 2] = a[i + 2] * t + orig[i + 2] * it;
+ a[i + 3] = a[i + 3] * t + orig[i + 3] * it;
+ }
+ }
+ ctx.putImageData(imageData, 0, 0);
commitAction("Camera Raw");
set(adjustOpen, false);
set(adjustSrcCanvas, null);
@@ -8739,6 +8871,10 @@ function EditModal($$anchor, $$props) {
const ch = Math.min(get(height) - cy, Math.round(get(cropBox).h));
if (cw < 4 || ch < 4) return;
flushActive();
+ if (get(
+ selActive
+ // selection mask is in old-document coordinates — invalid after crop
+ )) deselect();
const sc = document.createElement("canvas");
sc.width = cw;
sc.height = ch;
@@ -8750,27 +8886,34 @@ function EditModal($$anchor, $$props) {
}
const newUrl = sc.toDataURL("image/png");
const ow = get(width), oh = get(height);
- const cropPlane = (data) => {
+ const cropPlane = (data, lox, loy) => {
if (!data) return void 0;
const t = document.createElement("canvas");
t.width = ow;
t.height = oh;
t.getContext("2d").putImageData(data, 0, 0);
+ const doc = document.createElement("canvas");
+ doc.width = ow;
+ doc.height = oh;
+ doc.getContext("2d").drawImage(t, lox, loy);
const cc = document.createElement("canvas");
cc.width = cw;
cc.height = ch;
- cc.getContext("2d").drawImage(t, cx, cy, cw, ch, 0, 0, cw, ch);
+ cc.getContext("2d").drawImage(doc, cx, cy, cw, ch, 0, 0, cw, ch);
return cc;
};
const newLayers = get(layers).map((L) => {
const next = { ...L };
- const bc = cropPlane(L.bitmap);
+ const lox = L.ox || 0, loy = L.oy || 0;
+ const bc = cropPlane(L.bitmap, lox, loy);
if (bc) next.bitmap = bc.getContext("2d").getImageData(0, 0, cw, ch);
- const mc = cropPlane(L.mask);
+ const mc = cropPlane(L.mask, lox, loy);
if (mc) {
next.mask = mc.getContext("2d").getImageData(0, 0, cw, ch);
next.maskUrl = mc.toDataURL();
}
+ next.ox = 0;
+ next.oy = 0;
return next;
});
set(sourceUrl, newUrl, true);
@@ -8858,6 +9001,9 @@ function EditModal($$anchor, $$props) {
set(saving, false);
set(errorMsg, "");
set(tool, "brush");
+ set(maskEdit, false);
+ set(groups, [], true);
+ set(renamingGroupId, null);
set(sourceUrl, srcUrlProp());
set(width, widthProp());
set(height, heightProp());
@@ -8874,6 +9020,7 @@ function EditModal($$anchor, $$props) {
user_effect(() => {
if (!$$props.open) return;
const onKey = (e) => {
+ var _a;
if (get(
adjustOpen
// Camera Raw / an elevated handoff modal owns the keyboard
@@ -8906,10 +9053,21 @@ function EditModal($$anchor, $$props) {
e.preventDefault();
e.stopPropagation();
redo();
- } else if ((e.ctrlKey || e.metaKey) && (e.key === "j" || e.key === "J") && get(selActive)) {
+ } else if ((e.ctrlKey || e.metaKey) && (e.key === "j" || e.key === "J")) {
e.preventDefault();
e.stopPropagation();
- layerViaCopy(e.shiftKey);
+ if (get(
+ selActive
+ // Ctrl+J copy / Ctrl+Shift+J cut → new layer
+ )) layerViaCopy(e.shiftKey);
+ else if (!e.shiftKey) duplicateLayer(get(
+ activeIndex
+ // no selection → duplicate the whole layer (PS)
+ ));
+ } else if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === "c" || e.key === "C") && get(selActive)) {
+ e.preventDefault();
+ e.stopPropagation();
+ copySelection(false, true);
} else if ((e.ctrlKey || e.metaKey) && (e.key === "c" || e.key === "C") && get(selActive)) {
e.preventDefault();
e.stopPropagation();
@@ -8926,14 +9084,25 @@ function EditModal($$anchor, $$props) {
e.preventDefault();
e.stopPropagation();
addLayer();
+ } else if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === "g" || e.key === "G")) {
+ e.preventDefault();
+ e.stopPropagation();
+ const g = groupById((_a = get(layers)[get(activeIndex)]) == null ? void 0 : _a.groupId);
+ if (g) ungroup(g.id);
+ } else if ((e.ctrlKey || e.metaKey) && (e.key === "g" || e.key === "G")) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (get(selectedIds).length) groupSelected();
} else if ((e.ctrlKey || e.metaKey) && (e.key === "a" || e.key === "A")) {
e.preventDefault();
e.stopPropagation();
selectAllRegion();
+ commitAction("Select All");
} else if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === "i" || e.key === "I") && get(selActive)) {
e.preventDefault();
e.stopPropagation();
selectInverse();
+ commitAction("Select Inverse");
} else if (e.key === "Backspace" && (e.altKey || e.ctrlKey || e.metaKey)) {
e.preventDefault();
e.stopPropagation();
@@ -8953,6 +9122,57 @@ function EditModal($$anchor, $$props) {
e.preventDefault();
e.stopPropagation();
deselect();
+ commitAction("Deselect");
+ } else if ((e.ctrlKey || e.metaKey) && e.key === "0") {
+ e.preventDefault();
+ e.stopPropagation();
+ resetView();
+ } else if ((e.ctrlKey || e.metaKey) && (e.key === "=" || e.key === "+")) {
+ e.preventDefault();
+ e.stopPropagation();
+ zoomBy(1.25);
+ } else if ((e.ctrlKey || e.metaKey) && (e.key === "-" || e.key === "_")) {
+ e.preventDefault();
+ e.stopPropagation();
+ zoomBy(1 / 1.25);
+ } else if (!e.ctrlKey && !e.metaKey && !e.altKey && (e.key === "[" || e.key === "]")) {
+ e.preventDefault();
+ e.stopPropagation();
+ const dir = e.key === "]" ? 1 : -1;
+ const step = get(brushSize) < 10 ? 1 : get(brushSize) < 50 ? 2 : get(brushSize) < 100 ? 5 : 10;
+ set(brushSize, Math.max(1, Math.min(1024, get(brushSize) + dir * step)), true);
+ } else if (!e.ctrlKey && !e.metaKey && !e.altKey && !get(colorPopout) && (e.key === "x" || e.key === "X")) {
+ e.preventDefault();
+ e.stopPropagation();
+ const fg = get(
+ rgb
+ // swap foreground/background
+ );
+ (($$value) => {
+ var $$array = to_array($$value, 3);
+ set(hue, $$array[0], true);
+ set(sat, $$array[1], true);
+ set(val, $$array[2], true);
+ })(rgbToHsv(get(bgRgb)[0], get(bgRgb)[1], get(bgRgb)[2]));
+ set(bgRgb, fg, true);
+ } else if (!e.ctrlKey && !e.metaKey && !e.altKey && !get(colorPopout) && (e.key === "d" || e.key === "D")) {
+ e.preventDefault();
+ e.stopPropagation();
+ set(
+ hue,
+ 0
+ // default colors: black foreground / white background
+ );
+ set(sat, 0);
+ set(val, 0);
+ set(bgRgb, [255, 255, 255], true);
+ } else if (!e.ctrlKey && !e.metaKey && !e.altKey && !get(patch) && !get(colorPopout) && TOOL_KEYS[(e.key || "").toLowerCase()]) {
+ e.preventDefault();
+ e.stopPropagation();
+ setTool(TOOL_KEYS[e.key.toLowerCase()]);
+ } else if (e.key === " ") {
+ e.preventDefault();
+ e.stopPropagation();
} else if ((e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight") && !e.ctrlKey && !e.metaKey && !e.altKey && !get(patch) && !get(colorPopout) && !get(healDialog) && get(tool) !== "crop") {
e.preventDefault();
e.stopPropagation();
@@ -8991,8 +9211,10 @@ function EditModal($$anchor, $$props) {
else if (get(
selActive
// first Esc drops the selection
- )) deselect();
- else if (get(tool) === "crop" && get(cropBox) && !cropBoxIsFull()) resetCropBox();
+ )) {
+ deselect();
+ commitAction("Deselect");
+ } else if (get(tool) === "crop" && get(cropBox) && !cropBoxIsFull()) resetCropBox();
else close();
}
};
@@ -9024,10 +9246,10 @@ function EditModal($$anchor, $$props) {
}
function setColorFromRgb(r, g, b) {
(($$value) => {
- var $$array = to_array($$value, 3);
- set(hue, $$array[0], true);
- set(sat, $$array[1], true);
- set(val, $$array[2], true);
+ var $$array_1 = to_array($$value, 3);
+ set(hue, $$array_1[0], true);
+ set(sat, $$array_1[1], true);
+ set(val, $$array_1[2], true);
})(rgbToHsv(r, g, b));
}
const NAV_W = 256;
@@ -9178,6 +9400,16 @@ function EditModal($$anchor, $$props) {
set(panX, (rect.width - get(width) * get(zoom)) / 2);
set(panY, (rect.height - get(height) * get(zoom)) / 2);
}
+ function zoomBy(factor) {
+ if (!viewportEl) return;
+ const rect = viewportEl.getBoundingClientRect();
+ const cx = rect.width / 2, cy = rect.height / 2;
+ const newZoom = Math.max(0.05, Math.min(12, get(zoom) * factor));
+ set(panX, cx - (cx - get(panX)) / get(zoom) * newZoom);
+ set(panY, cy - (cy - get(panY)) / get(zoom) * newZoom);
+ set(zoom, newZoom, true);
+ set(fitMode, false);
+ }
function toImageCoords(e) {
const rect = viewportEl.getBoundingClientRect();
return {
@@ -9360,7 +9592,8 @@ function EditModal($$anchor, $$props) {
for (let x = 0; x < bw; x++) {
const i = y * bw + x;
const gi = (y0 + y) * get(width) + x0 + x;
- const cov = Math.min(1, strokeCov[gi]);
+ let cov = Math.min(1, strokeCov[gi]);
+ if (selStrokeData) cov *= selStrokeData[gi * 4 + 3] / 255;
const o = i * 4, j = gi * 4;
if (cov <= 0) {
px[o] = pd[j];
@@ -9375,7 +9608,7 @@ function EditModal($$anchor, $$props) {
const target = cov * healed + (1 - cov) * dest[i * 3 + ch];
px[o + ch] = Math.max(0, Math.min(255, Math.round(target + d)));
}
- px[o + 3] = 255;
+ px[o + 3] = pd[j + 3];
}
}
paintCanvas.getContext("2d").putImageData(img, x0, y0);
@@ -9476,7 +9709,6 @@ function EditModal($$anchor, $$props) {
const img = new ImageData(bw, bh);
const px = img.data;
const pd = preStroke.data;
- const sd = sourceData.data;
for (let y = 0; y < bh; y++) {
const row = (y0 + y) * get(width) + x0;
for (let x = 0; x < bw; x++) {
@@ -9496,15 +9728,34 @@ function EditModal($$anchor, $$props) {
const d = dither(x0 + x, y0 + y);
const sj = stampMode ? (Math.max(0, Math.min(get(height) - 1, y0 + y + ody)) * get(width) + Math.max(0, Math.min(get(width) - 1, x0 + x + odx))) * 4 : 0;
const spa = stampMode ? pd[sj + 3] / 255 : 0;
+ const se = spa * cov;
+ const na = healMode ? pa : (
+ // heal preview re-textures; alpha unchanged
+ erase ? pa * (1 - cov) : (
+ // eraser = destination-out on own alpha
+ stampMode ? se + pa * (1 - se) : (
+ // clone source-over
+ cov + pa * (1 - cov)
+ )
+ )
+ );
+ const naInv = na > 0 ? 1 / na : 0;
for (let ch = 0; ch < 3; ch++) {
- const comp = pd[j + ch] * pa + sd[j + ch] * (1 - pa);
- const target = healMode ? comp * (1 - cov * 0.4) + 200 * cov * 0.4 : stampMode ? (pd[sj + ch] * spa + sd[sj + ch] * (1 - spa)) * cov + comp * (1 - cov) : erase ? comp * (1 - cov) + sd[j + ch] * cov : (ch === 0 ? cr : ch === 1 ? cg : cb) * cov + comp * (1 - cov);
- px[i + ch] = Math.max(0, Math.min(255, Math.round(target + d)));
+ const lc = pd[j + ch];
+ const target = healMode ? lc * (1 - cov * 0.4) + 200 * cov * 0.4 : erase ? lc : (
+ // RGB stays; alpha carries the erase
+ stampMode ? (pd[sj + ch] * se + lc * pa * (1 - se)) * naInv : ((ch === 0 ? cr : ch === 1 ? cg : cb) * cov + lc * pa * (1 - cov)) * naInv
+ );
+ px[i + ch] = erase ? lc : Math.max(0, Math.min(255, Math.round(target + d)));
}
- px[i + 3] = 255;
+ px[i + 3] = Math.max(0, Math.min(255, Math.round(na * 255)));
}
}
paintCanvas.getContext("2d").putImageData(img, x0, y0);
+ if (get(
+ hasCustomBlend
+ // refresh the single-canvas custom-blend composite mid-stroke
+ )) update(customTick);
}
const AIRBRUSH_DABS_PER_SEC = 30;
let airRafId = 0;
@@ -9780,6 +10031,110 @@ function EditModal($$anchor, $$props) {
}
}
const activeLayer = () => get(layers)[get(activeIndex)] || get(layers)[0];
+ const groupById = (gid) => gid ? get(groups).find((g) => g.id === gid) : null;
+ const layerShown = (L) => {
+ var _a;
+ return !!(L == null ? void 0 : L.visible) && ((_a = groupById(L == null ? void 0 : L.groupId)) == null ? void 0 : _a.visible) !== false;
+ };
+ const layerEffOpacity = (L) => {
+ var _a;
+ return ((L == null ? void 0 : L.opacity) ?? 1) * (((_a = groupById(L == null ? void 0 : L.groupId)) == null ? void 0 : _a.opacity) ?? 1);
+ };
+ let panelRows = user_derived(() => {
+ const rows = [];
+ const seen = /* @__PURE__ */ new Set();
+ for (let i = get(layers).length - 1; i >= 0; i--) {
+ const L = get(layers)[i];
+ const g = groupById(L.groupId);
+ if (g && !seen.has(g.id)) {
+ seen.add(g.id);
+ rows.push({ type: "group", group: g });
+ }
+ if (g && g.collapsed) continue;
+ rows.push({ type: "layer", L, i, inGroup: !!g });
+ }
+ return rows;
+ });
+ function groupSelected() {
+ var _a;
+ const ids = get(selectedIds).filter((id) => {
+ const L = get(layers).find((x) => x.id === id);
+ return L && !L.isBackground;
+ });
+ if (!ids.length) return;
+ flushActive();
+ const gid = ++nextGroupNum;
+ const members = get(layers).filter((L) => ids.includes(L.id)).map((L) => ({ ...L, groupId: gid }));
+ const rest = get(layers).filter((L) => !ids.includes(L.id));
+ const topSelIdx = Math.max(...get(layers).map((L, i) => ids.includes(L.id) ? i : -1));
+ let insertAt = 0;
+ for (let i = 0; i < topSelIdx; i++) if (!ids.includes(get(layers)[i].id)) insertAt++;
+ const activeId = (_a = get(layers)[get(activeIndex)]) == null ? void 0 : _a.id;
+ set(
+ groups,
+ [
+ ...get(groups),
+ {
+ id: gid,
+ name: `Group ${gid}`,
+ collapsed: false,
+ visible: true,
+ opacity: 1
+ }
+ ],
+ true
+ );
+ set(
+ layers,
+ [
+ ...rest.slice(0, insertAt),
+ ...members,
+ ...rest.slice(insertAt)
+ ],
+ true
+ );
+ set(activeIndex, Math.max(0, get(layers).findIndex((L) => L.id === activeId)), true);
+ set(selectedIds, members.map((L) => L.id), true);
+ loadActiveBitmap();
+ commitAction("Group Layers");
+ }
+ function ungroup(gid) {
+ if (!get(groups).some((g) => g.id === gid)) return;
+ flushActive();
+ set(layers, get(layers).map((L) => L.groupId === gid ? { ...L, groupId: void 0 } : L), true);
+ set(groups, get(groups).filter((g) => g.id !== gid), true);
+ commitAction("Ungroup");
+ }
+ function toggleGroupCollapsed(gid) {
+ set(groups, get(
+ groups
+ // view-only, no history
+ ).map((g) => g.id === gid ? { ...g, collapsed: !g.collapsed } : g), true);
+ }
+ function setGroupVisible(gid, v) {
+ set(groups, get(groups).map((g) => g.id === gid ? { ...g, visible: v } : g), true);
+ commitAction("Group Visibility");
+ }
+ function setGroupOpacity(gid, o) {
+ set(groups, get(
+ groups
+ // commit on release (slider onchange)
+ ).map((g) => g.id === gid ? { ...g, opacity: o } : g), true);
+ }
+ function startGroupRename(g) {
+ set(renamingGroupId, g.id, true);
+ set(renameText, g.name, true);
+ }
+ function commitGroupRename() {
+ if (get(renamingGroupId) == null) return;
+ const gid = get(renamingGroupId);
+ set(renamingGroupId, null);
+ const name = get(renameText).trim();
+ if (name) {
+ set(groups, get(groups).map((g) => g.id === gid ? { ...g, name } : g), true);
+ commitAction("Rename Group");
+ }
+ }
let hiddenEditDialog = state(
null
// PS "target layer is hidden" modal text, or null
@@ -9807,6 +10162,7 @@ function EditModal($$anchor, $$props) {
}
function activeMaskedCanvas() {
const L = activeLayer();
+ if (get(maskEdit) && (L == null ? void 0 : L.mask)) return maskedRenderCanvas(L);
if (!(L == null ? void 0 : L.mask)) return paintCanvas;
const t = document.createElement("canvas");
t.width = get(width);
@@ -9875,6 +10231,18 @@ function EditModal($$anchor, $$props) {
if (!paintCanvas || !get(layers)[get(activeIndex)]) return;
const L = get(layers)[get(activeIndex)];
const ox = L.ox || 0, oy = L.oy || 0;
+ if (get(maskEdit)) {
+ if (!L.mask) return;
+ const gray = document.createElement("canvas");
+ gray.width = get(width);
+ gray.height = get(height);
+ const gx = gray.getContext("2d");
+ gx.drawImage(maskToGrayCanvas(L.mask), 0, 0);
+ gx.clearRect(-ox, -oy, get(width), get(height));
+ gx.drawImage(paintCanvas, -ox, -oy);
+ writeLayerMask(get(activeIndex), grayToMaskImageData(gx.getImageData(0, 0, get(width), get(height))));
+ return;
+ }
if (!ox && !oy) {
get(layers)[get(activeIndex)] = {
...L,
@@ -9897,22 +10265,46 @@ function EditModal($$anchor, $$props) {
function loadActiveBitmap() {
const ctx = paintCanvas == null ? void 0 : paintCanvas.getContext("2d");
if (!ctx) return;
- ctx.clearRect(0, 0, get(width), get(height));
const L = get(layers)[get(activeIndex)];
- if (L) drawLayerRaw(ctx, L, 0, 0, get(width), get(height), 0, 0, get(width), get(height));
+ if (get(
+ maskEdit
+ // active layer has no mask → leave mask-edit (safety net)
+ ) && !(L == null ? void 0 : L.mask)) set(maskEdit, false);
+ ctx.clearRect(0, 0, get(width), get(height));
+ if (!L) return;
+ if (get(maskEdit) && L.mask) {
+ ctx.drawImage(maskToGrayCanvas(L.mask), L.ox || 0, L.oy || 0);
+ return;
+ }
+ drawLayerRaw(ctx, L, 0, 0, get(width), get(height), 0, 0, get(width), get(height));
}
function selectLayer(i) {
if (i < 0 || i >= get(layers).length || painting) return;
- if (i !== get(activeIndex)) {
- flushActive();
- set(activeIndex, i, true);
- loadActiveBitmap();
- }
+ if (i === get(activeIndex)) return;
+ if (get(
+ maskEdit
+ // clicking another layer targets its pixels, not a mask
+ )) exitMaskEdit();
+ commitPatch();
+ flushActive();
+ set(activeIndex, i, true);
+ loadActiveBitmap();
}
function clickLayer(i, e) {
+ var _a;
const id = get(layers)[i].id;
if ((e.ctrlKey || e.metaKey) && get(selectedIds).length) {
- set(selectedIds, get(selectedIds).includes(id) ? get(selectedIds) : [...get(selectedIds), id], true);
+ if (get(selectedIds).includes(id)) {
+ if (get(selectedIds).length > 1) {
+ set(selectedIds, get(selectedIds).filter((x) => x !== id), true);
+ if (id === ((_a = get(layers)[get(activeIndex)]) == null ? void 0 : _a.id)) {
+ const s = get(layers).findIndex((L) => get(selectedIds).includes(L.id));
+ if (s >= 0) selectLayer(s);
+ }
+ }
+ return;
+ }
+ set(selectedIds, [...get(selectedIds), id], true);
} else if (e.shiftKey && get(selectedIds).length) {
const lo = Math.min(get(activeIndex), i), hi = Math.max(get(activeIndex), i);
set(selectedIds, get(layers).slice(lo, hi + 1).map((L) => L.id), true);
@@ -9923,6 +10315,7 @@ function EditModal($$anchor, $$props) {
if (!get(selectedIds).includes(id)) set(selectedIds, [...get(selectedIds), id], true);
}
function addLayer(at = get(activeIndex)) {
+ var _a;
if (painting) return;
flushActive();
const id = ++nextLayerNum;
@@ -9935,7 +10328,8 @@ function EditModal($$anchor, $$props) {
name: `Layer ${id}`,
visible: true,
opacity: 1,
- bitmap: null
+ bitmap: null,
+ groupId: (_a = get(layers)[at]) == null ? void 0 : _a.groupId
},
...get(layers).slice(at + 1)
],
@@ -9944,6 +10338,7 @@ function EditModal($$anchor, $$props) {
set(activeIndex, at + 1);
set(selectedIds, [id], true);
loadActiveBitmap();
+ commitAction("New Layer");
}
function canDeleteSelected() {
return get(layers).length > 1 && get(layers).some((L) => get(selectedIds).includes(L.id) && !L.isBackground);
@@ -9962,6 +10357,7 @@ function EditModal($$anchor, $$props) {
); k++) if (!doomedIds.has(get(layers)[k].id)) na++;
const survivors = get(layers).filter((L) => !doomedIds.has(L.id));
set(layers, survivors, true);
+ set(groups, get(groups).filter((g) => get(layers).some((L) => L.groupId === g.id)), true);
set(activeIndex, Math.min(na, survivors.length - 1), true);
set(selectedIds, [(_b = get(layers)[get(activeIndex)]) == null ? void 0 : _b.id].filter((x) => x != null), true);
loadActiveBitmap();
@@ -9995,6 +10391,7 @@ function EditModal($$anchor, $$props) {
mask: src.mask,
maskUrl: src.maskUrl,
sourceRect: src.sourceRect,
+ groupId: src.groupId,
bitmap
},
...get(layers).slice(i + 1)
@@ -10024,16 +10421,78 @@ function EditModal($$anchor, $$props) {
["lighten", "Lighten"],
["color-dodge", "Color Dodge"],
["color-burn", "Color Burn"],
+ ["linear-dodge", "Linear Dodge (Add)"],
+ ["subtract", "Subtract"],
+ ["divide", "Divide"],
["hard-light", "Hard Light"],
["soft-light", "Soft Light"],
+ ["vivid-light", "Vivid Light"],
+ ["linear-light", "Linear Light"],
+ ["pin-light", "Pin Light"],
+ ["hard-mix", "Hard Mix"],
["difference", "Difference"],
["exclusion", "Exclusion"],
["hue", "Hue"],
["saturation", "Saturation"],
["color", "Color"],
- ["luminosity", "Luminosity"]
+ ["luminosity", "Luminosity"],
+ ["darker-color", "Darker Color"],
+ ["lighter-color", "Lighter Color"]
];
- const layerBlendCss = (L) => (L == null ? void 0 : L.blend) && L.blend !== "normal" ? L.blend : null;
+ const CUSTOM_BLENDS = /* @__PURE__ */ new Set([
+ "linear-dodge",
+ "subtract",
+ "divide",
+ "vivid-light",
+ "linear-light",
+ "pin-light",
+ "hard-mix",
+ "darker-color",
+ "lighter-color"
+ ]);
+ const isCustomBlend = (b) => CUSTOM_BLENDS.has(b);
+ let hasCustomBlend = user_derived(() => get(layers).some((L) => layerShown(L) && isCustomBlend(L.blend)));
+ let customTick = state(
+ 0
+ // bumped after a paint dab so the single-canvas composite refreshes live
+ );
+ function blendChannel(mode, b, s) {
+ switch (mode) {
+ case "linear-dodge":
+ return Math.min(1, b + s);
+ case "subtract":
+ return Math.max(0, b - s);
+ case "divide":
+ return s <= 0 ? 1 : Math.min(1, b / s);
+ case "linear-light":
+ return Math.max(0, Math.min(1, b + 2 * s - 1));
+ case "pin-light":
+ return s <= 0.5 ? Math.min(b, 2 * s) : Math.max(b, 2 * s - 1);
+ case "vivid-light":
+ if (s <= 0.5) {
+ const d = 2 * s;
+ return d <= 0 ? 0 : Math.max(0, 1 - (1 - b) / d);
+ }
+ {
+ const d = 2 * (1 - s);
+ return d <= 0 ? 1 : Math.min(1, b / d);
+ }
+ case "hard-mix": {
+ let v;
+ if (s <= 0.5) {
+ const d = 2 * s;
+ v = d <= 0 ? 0 : Math.max(0, 1 - (1 - b) / d);
+ } else {
+ const d = 2 * (1 - s);
+ v = d <= 0 ? 1 : Math.min(1, b / d);
+ }
+ return v < 0.5 ? 0 : 1;
+ }
+ default:
+ return s;
+ }
+ }
+ const layerBlendCss = (L) => (L == null ? void 0 : L.blend) && L.blend !== "normal" && !isCustomBlend(L.blend) ? L.blend : null;
function setLayerBlend(i, blend) {
get(layers)[i] = {
...get(layers)[i],
@@ -10105,9 +10564,107 @@ function EditModal($$anchor, $$props) {
function deleteLayerMask(i) {
var _a;
if (!((_a = get(layers)[i]) == null ? void 0 : _a.mask)) return;
+ if (get(maskEdit) && i === get(activeIndex)) set(maskEdit, false);
get(layers)[i] = { ...get(layers)[i], mask: void 0, maskUrl: void 0 };
+ if (i === get(activeIndex)) loadActiveBitmap();
commitAction("Delete Layer Mask");
}
+ function maskToGrayCanvas(mask) {
+ const c = document.createElement("canvas");
+ c.width = get(width);
+ c.height = get(height);
+ const ctx = c.getContext("2d");
+ if (mask) {
+ const g = new ImageData(get(width), get(height)), d = g.data, m = mask.data;
+ for (let i = 0; i < d.length; i += 4) {
+ const v = m[i + 3];
+ d[i] = v;
+ d[i + 1] = v;
+ d[i + 2] = v;
+ d[i + 3] = 255;
+ }
+ ctx.putImageData(g, 0, 0);
+ } else {
+ ctx.fillStyle = "#fff";
+ ctx.fillRect(0, 0, get(width), get(height));
+ }
+ return c;
+ }
+ function grayToMaskImageData(gray) {
+ const s = gray.data, out = new ImageData(get(width), get(height)), d = out.data;
+ for (let i = 0; i < s.length; i += 4) {
+ const lum = s[i] * 0.299 + s[i + 1] * 0.587 + s[i + 2] * 0.114;
+ d[i] = 255;
+ d[i + 1] = 255;
+ d[i + 2] = 255;
+ d[i + 3] = Math.round(lum * (s[i + 3] / 255));
+ }
+ return out;
+ }
+ function writeLayerMask(i, maskId) {
+ const mc = document.createElement("canvas");
+ mc.width = get(width);
+ mc.height = get(height);
+ mc.getContext("2d").putImageData(maskId, 0, 0);
+ get(layers)[i] = { ...get(layers)[i], mask: maskId, maskUrl: mc.toDataURL() };
+ }
+ function enterMaskEdit(i = get(activeIndex)) {
+ var _a;
+ if (i !== get(activeIndex)) selectLayer(i);
+ if (!((_a = get(layers)[get(activeIndex)]) == null ? void 0 : _a.mask) || get(maskEdit) || painting) return;
+ flushActive();
+ set(maskEdit, true);
+ set(
+ hue,
+ 0
+ // default to white = reveal (X/D still work)
+ );
+ set(sat, 0);
+ set(val, 1);
+ loadActiveBitmap();
+ }
+ function exitMaskEdit() {
+ if (!get(maskEdit)) return;
+ flushActive();
+ set(maskEdit, false);
+ loadActiveBitmap();
+ }
+ function toggleMaskEdit(i) {
+ if (get(maskEdit) && i === get(activeIndex)) exitMaskEdit();
+ else enterMaskEdit(i);
+ }
+ function invertLayerMask(i) {
+ var _a;
+ if (!((_a = get(layers)[i]) == null ? void 0 : _a.mask)) return;
+ if (get(
+ maskEdit
+ // pull live paint into the mask first
+ ) && i === get(activeIndex)) flushActive();
+ const m = get(layers)[i].mask, inv = new ImageData(get(width), get(height)), d = inv.data, s = m.data;
+ for (let k = 0; k < d.length; k += 4) {
+ d[k] = 255;
+ d[k + 1] = 255;
+ d[k + 2] = 255;
+ d[k + 3] = 255 - s[k + 3];
+ }
+ writeLayerMask(i, inv);
+ if (get(maskEdit) && i === get(activeIndex)) loadActiveBitmap();
+ commitAction("Invert Mask");
+ }
+ function fillLayerMask(i, vis) {
+ var _a;
+ if (!((_a = get(layers)[i]) == null ? void 0 : _a.mask)) return;
+ const fill = new ImageData(get(width), get(height)), d = fill.data;
+ for (let k = 0; k < d.length; k += 4) {
+ d[k] = 255;
+ d[k + 1] = 255;
+ d[k + 2] = 255;
+ d[k + 3] = vis;
+ }
+ writeLayerMask(i, fill);
+ if (get(maskEdit) && i === get(activeIndex)) loadActiveBitmap();
+ commitAction(vis ? "Reveal All" : "Hide All");
+ }
function startRename(L) {
set(renamingId, L.id, true);
set(renameText, L.name, true);
@@ -10122,6 +10679,7 @@ function EditModal($$anchor, $$props) {
set(renamingId, null);
}
function dropLayer(targetId) {
+ var _a;
set(dragOverId, null);
if (dragId == null || dragId === targetId) {
dragId = null;
@@ -10138,12 +10696,53 @@ function EditModal($$anchor, $$props) {
const ti = rest.findIndex((L) => L.id === targetId);
let at = ti < 0 ? rest.length : ti + 1;
if (at < 1) at = 1;
- rest.splice(at, 0, moved);
+ const targetGroupId = (_a = rest[ti]) == null ? void 0 : _a.groupId;
+ rest.splice(at, 0, { ...moved, groupId: targetGroupId });
set(layers, rest, true);
+ set(groups, get(groups).filter((g) => get(layers).some((L) => L.groupId === g.id)), true);
set(activeIndex, get(layers).findIndex((L) => L.id === activeId), true);
dragId = null;
commitAction("Reorder Layers");
}
+ function blendLayerFull(ctx, srcCanvas, mode, eff) {
+ const baseImg = ctx.getImageData(0, 0, get(width), get(height));
+ const srcImg = srcCanvas.getContext("2d").getImageData(0, 0, get(width), get(height));
+ const bd = baseImg.data, sd = srcImg.data;
+ const sep = mode !== "darker-color" && mode !== "lighter-color";
+ for (let p = 0; p < bd.length; p += 4) {
+ const sa = sd[p + 3] / 255 * eff;
+ if (sa <= 0) continue;
+ const br = bd[p] / 255, bg = bd[p + 1] / 255, bb = bd[p + 2] / 255;
+ const sr = sd[p] / 255, sg = sd[p + 1] / 255, sbb = sd[p + 2] / 255;
+ let rr, rg, rb;
+ if (sep) {
+ rr = blendChannel(mode, br, sr);
+ rg = blendChannel(mode, bg, sg);
+ rb = blendChannel(mode, bb, sbb);
+ } else {
+ const lb = 0.299 * br + 0.587 * bg + 0.114 * bb, ls = 0.299 * sr + 0.587 * sg + 0.114 * sbb;
+ const keepBase = mode === "darker-color" ? lb <= ls : lb >= ls;
+ rr = keepBase ? br : sr;
+ rg = keepBase ? bg : sg;
+ rb = keepBase ? bb : sbb;
+ }
+ const ba = bd[p + 3] / 255, oa = sa + ba * (1 - sa), inv = oa > 0 ? 1 / oa : 0;
+ const cr = (1 - ba) * sr + ba * rr, cg = (1 - ba) * sg + ba * rg, cb2 = (1 - ba) * sbb + ba * rb;
+ bd[p] = Math.round((cr * sa + br * ba * (1 - sa)) * inv * 255);
+ bd[p + 1] = Math.round((cg * sa + bg * ba * (1 - sa)) * inv * 255);
+ bd[p + 2] = Math.round((cb2 * sa + bb * ba * (1 - sa)) * inv * 255);
+ bd[p + 3] = Math.round(oa * 255);
+ }
+ ctx.putImageData(baseImg, 0, 0);
+ }
+ function layerFullCanvas(L, isActive) {
+ const tmp = document.createElement("canvas");
+ tmp.width = get(width);
+ tmp.height = get(height);
+ if (isActive) tmp.getContext("2d").drawImage(activeMaskedCanvas(), 0, 0);
+ else drawLayerContent(tmp.getContext("2d"), L, 0, 0, get(width), get(height), 0, 0, get(width), get(height));
+ return tmp;
+ }
function compositeLayers(idxs) {
const c = document.createElement("canvas");
c.width = get(width);
@@ -10151,8 +10750,13 @@ function EditModal($$anchor, $$props) {
const ctx = c.getContext("2d");
for (const i of idxs) {
const L = get(layers)[i];
- if (!L.visible) continue;
- ctx.globalAlpha = L.opacity;
+ if (!layerShown(L)) continue;
+ const eff = layerEffOpacity(L);
+ if (isCustomBlend(L.blend)) {
+ blendLayerFull(ctx, layerFullCanvas(L, i === get(activeIndex)), L.blend, eff);
+ continue;
+ }
+ ctx.globalAlpha = eff;
ctx.globalCompositeOperation = layerBlendCss(L) || "source-over";
if (i === get(activeIndex)) ctx.drawImage(activeMaskedCanvas(), 0, 0);
else drawLayerContent(ctx, L, 0, 0, get(width), get(height), 0, 0, get(width), get(height));
@@ -10171,18 +10775,21 @@ function EditModal($$anchor, $$props) {
const lo = idxs[0];
const hasBg = idxs.some((i) => get(layers)[i].isBackground);
const bitmap = compositeLayers(idxs);
+ const grp = !hasBg && get(layers)[lo].groupId && idxs.every((i) => get(layers)[i].groupId === get(layers)[lo].groupId) ? get(layers)[lo].groupId : void 0;
const merged = {
id: hasBg ? 0 : ++nextLayerNum,
name: hasBg ? "Background" : get(layers)[lo].name,
visible: true,
opacity: 1,
bitmap,
- isBackground: hasBg || void 0
+ isBackground: hasBg || void 0,
+ groupId: grp
};
const keptBelow = get(layers).filter((L, i) => i < lo && !idxs.includes(i)).length;
const rest = get(layers).filter((_, i) => !idxs.includes(i));
rest.splice(keptBelow, 0, merged);
set(layers, rest, true);
+ set(groups, get(groups).filter((g) => get(layers).some((L) => L.groupId === g.id)), true);
set(activeIndex, get(layers).findIndex((L) => L.id === merged.id), true);
set(selectedIds, [merged.id], true);
loadActiveBitmap();
@@ -10214,6 +10821,7 @@ function EditModal($$anchor, $$props) {
],
true
);
+ set(groups, [], true);
set(activeIndex, 0);
set(selectedIds, [0], true);
loadActiveBitmap();
@@ -10234,8 +10842,8 @@ function EditModal($$anchor, $$props) {
function drawBackdrop(ctx, sx, sy, sw, sh, dx, dy, dw, dh) {
for (let k = 0; k < get(activeIndex); k++) {
const L = get(layers)[k];
- if (!L.visible) continue;
- ctx.globalAlpha = L.opacity;
+ if (!layerShown(L)) continue;
+ ctx.globalAlpha = layerEffOpacity(L);
ctx.globalCompositeOperation = layerBlendCss(L) || "source-over";
drawLayerContent(ctx, L, sx, sy, sw, sh, dx, dy, dw, dh);
ctx.globalAlpha = 1;
@@ -10243,10 +10851,24 @@ function EditModal($$anchor, $$props) {
}
}
function drawFlattened(ctx, sx, sy, sw, sh, dx, dy, dw, dh) {
+ const fullFrame = sx === 0 && sy === 0 && dx === 0 && dy === 0 && sw === get(width) && sh === get(height) && dw === get(width) && dh === get(height);
+ if (!fullFrame && get(layers).some((L) => layerShown(L) && isCustomBlend(L.blend))) {
+ const full = document.createElement("canvas");
+ full.width = get(width);
+ full.height = get(height);
+ drawFlattened(full.getContext("2d"), 0, 0, get(width), get(height), 0, 0, get(width), get(height));
+ ctx.drawImage(full, sx, sy, sw, sh, dx, dy, dw, dh);
+ return;
+ }
for (let k = 0; k < get(layers).length; k++) {
const L = get(layers)[k];
- if (!L.visible) continue;
- ctx.globalAlpha = L.opacity;
+ if (!layerShown(L)) continue;
+ const eff = layerEffOpacity(L);
+ if (fullFrame && isCustomBlend(L.blend)) {
+ blendLayerFull(ctx, layerFullCanvas(L, k === get(activeIndex)), L.blend, eff);
+ continue;
+ }
+ ctx.globalAlpha = eff;
ctx.globalCompositeOperation = layerBlendCss(L) || "source-over";
if (k === get(activeIndex)) ctx.drawImage(activeMaskedCanvas(), sx, sy, sw, sh, dx, dy, dw, dh);
else drawLayerContent(ctx, L, sx, sy, sw, sh, dx, dy, dw, dh);
@@ -10283,6 +10905,16 @@ function EditModal($$anchor, $$props) {
drawLayerContent(ctx, get(layers)[i], 0, 0, get(width), get(height), 0, 0, get(width), get(height));
}
});
+ user_effect(() => {
+ get(customTick);
+ get(imgLoadTick);
+ if (!get(hasCustomBlend) || !customEl) return;
+ if (customEl.width !== get(width)) customEl.width = get(width);
+ if (customEl.height !== get(height)) customEl.height = get(height);
+ const ctx = customEl.getContext("2d");
+ ctx.clearRect(0, 0, get(width), get(height));
+ drawFlattened(ctx, 0, 0, get(width), get(height), 0, 0, get(width), get(height));
+ });
function clearAll() {
if (get(histIndex) === 0 && get(history2).length === 1) return;
set(patch, null);
@@ -10303,7 +10935,9 @@ function EditModal($$anchor, $$props) {
}
try {
sampleCtx.clearRect(0, 0, s, s);
- drawFlattened(sampleCtx, x, y, s, s, 0, 0, s, s);
+ if (get(sampleMerged) && get(hasCustomBlend) && customEl) sampleCtx.drawImage(customEl, x, y, s, s, 0, 0, s, s);
+ else if (get(sampleMerged)) drawFlattened(sampleCtx, x, y, s, s, 0, 0, s, s);
+ else sampleCtx.drawImage(paintCanvas, x, y, s, s, 0, 0, s, s);
const d = sampleCtx.getImageData(0, 0, s, s).data;
const sum = [0, 0, 0];
for (let i = 0; i < s * s; i++) {
@@ -10345,10 +10979,10 @@ function EditModal($$anchor, $$props) {
function onPointerDown(e) {
var _a;
if (e.button > 2) return;
- if (e.button === 1 || e.button === 2 || e.shiftKey && !get(
- isSelTool
- // Shift adds to a selection / snaps a transform rotation — doesn't pan
- ) && !get(transforming)) {
+ if (e.button === 1 || e.button === 2 || get(
+ spaceDown
+ // middle/right or Space-drag pans; Shift adds to a selection / snaps a transform rotation
+ ) && e.button === 0 || e.shiftKey && !get(isSelTool) && !get(transforming)) {
panning = true;
lastPt = { x: e.clientX, y: e.clientY };
e.preventDefault();
@@ -10615,6 +11249,7 @@ function EditModal($$anchor, $$props) {
}
if (selecting) {
selecting = false;
+ const had = get(selActive);
if (get(selRect) && get(selRect).w >= 1 && get(selRect).h >= 1) {
applySelectionShape(
{
@@ -10626,8 +11261,10 @@ function EditModal($$anchor, $$props) {
},
selOp
);
+ commitAction("Select");
} else if (selOp === "replace") {
deselect();
+ if (had) commitAction("Deselect");
}
set(selRect, null);
return;
@@ -10635,8 +11272,14 @@ function EditModal($$anchor, $$props) {
if (lassoing) {
lassoing = false;
if (get(tool) === "lasso") {
- if (get(lassoPts) && get(lassoPts).length >= 3) applySelectionShape({ type: "lasso", pts: get(lassoPts) }, selOp);
- else if (selOp === "replace") deselect();
+ const had = get(selActive);
+ if (get(lassoPts) && get(lassoPts).length >= 3) {
+ applySelectionShape({ type: "lasso", pts: get(lassoPts) }, selOp);
+ commitAction("Select");
+ } else if (selOp === "replace") {
+ deselect();
+ if (had) commitAction("Deselect");
+ }
} else if (get(lassoPts) && get(lassoPts).length >= 3) {
applySmooth(get(
lassoPts
@@ -10765,7 +11408,7 @@ function EditModal($$anchor, $$props) {
var fragment = comment();
var node = first_child(fragment);
{
- var consequent_61 = ($$anchor2) => {
+ var consequent_67 = ($$anchor2) => {
var div = root_1$5();
var div_1 = child(div);
let classes;
@@ -11070,9 +11713,9 @@ function EditModal($$anchor, $$props) {
],
index,
($$anchor4, $$item) => {
- var $$array_1 = user_derived(() => to_array($$item, 2));
- let id = () => get($$array_1)[0];
- let label = () => get($$array_1)[1];
+ var $$array_2 = user_derived(() => to_array($$item, 2));
+ let id = () => get($$array_2)[0];
+ let label = () => get($$array_2)[1];
var button_8 = root_16$3();
let classes_3;
var text_6 = child(button_8);
@@ -11276,7 +11919,14 @@ function EditModal($$anchor, $$props) {
option_2.value = option_2.__value = 3;
var option_3 = sibling(option_2);
option_3.value = option_3.__value = 5;
+ var div_28 = sibling(div_27, 2);
+ var select_2 = sibling(child(div_28), 2);
+ var option_4 = child(select_2);
+ option_4.value = option_4.__value = true;
+ var option_5 = sibling(option_4);
+ option_5.value = option_5.__value = false;
bind_select_value(select_1, () => get(sampleSize), ($$value) => set(sampleSize, $$value));
+ bind_select_value(select_2, () => get(sampleMerged), ($$value) => set(sampleMerged, $$value));
append($$anchor3, fragment_11);
};
if_block(node_4, ($$render) => {
@@ -11292,9 +11942,9 @@ function EditModal($$anchor, $$props) {
else $$render(alternate_2, -1);
});
}
- var div_28 = sibling(div_7, 2);
- var div_29 = child(div_28);
- var node_27 = child(div_29);
+ var div_29 = sibling(div_7, 2);
+ var div_30 = child(div_29);
+ var node_27 = child(div_30);
each(node_27, 17, () => TOOLS, index, ($$anchor3, t) => {
var button_16 = root_25$1();
let classes_4;
@@ -11323,13 +11973,13 @@ function EditModal($$anchor, $$props) {
var node_28 = sibling(button_21, 2);
{
var consequent_17 = ($$anchor3) => {
- var div_30 = root_26$1();
- var div_31 = sibling(child(div_30), 2);
- var div_32 = sibling(child(div_31), 4);
- var div_33 = sibling(div_31, 2);
- var div_34 = child(div_33);
- var div_35 = sibling(div_33, 2);
- var span_11 = child(div_35);
+ var div_31 = root_26$1();
+ var div_32 = sibling(child(div_31), 2);
+ var div_33 = sibling(child(div_32), 4);
+ var div_34 = sibling(div_32, 2);
+ var div_35 = child(div_34);
+ var div_36 = sibling(div_34, 2);
+ var span_11 = child(div_36);
var span_12 = sibling(span_11, 2);
var text_15 = child(span_12);
var node_29 = sibling(span_12, 2);
@@ -11343,50 +11993,64 @@ function EditModal($$anchor, $$props) {
if (typeof window !== "undefined" && window.EyeDropper) $$render(consequent_16);
});
}
- var div_36 = sibling(div_35, 4);
- var button_23 = child(div_36);
+ var div_37 = sibling(div_36, 4);
+ var button_23 = child(div_37);
var button_24 = sibling(button_23, 2);
- bind_this(div_30, ($$value) => set(popoutEl, $$value), () => get(popoutEl));
+ bind_this(div_31, ($$value) => set(popoutEl, $$value), () => get(popoutEl));
template_effect(() => {
var _a;
- set_style(div_30, `top: ${((_a = get(colorChipEl)) == null ? void 0 : _a.offsetTop) ?? 0 ?? ""}px;`);
- set_style(div_31, `background-color: hsl(${get(hue) ?? ""}, 100%, 50%);`);
- set_style(div_32, `left: ${get(sat) * 100}%; top: ${(1 - get(val)) * 100}%;`);
- set_style(div_34, `left: ${get(hue) / 360 * 100}%;`);
+ set_style(div_31, `top: ${((_a = get(colorChipEl)) == null ? void 0 : _a.offsetTop) ?? 0 ?? ""}px;`);
+ set_style(div_32, `background-color: hsl(${get(hue) ?? ""}, 100%, 50%);`);
+ set_style(div_33, `left: ${get(sat) * 100}%; top: ${(1 - get(val)) * 100}%;`);
+ set_style(div_35, `left: ${get(hue) / 360 * 100}%;`);
set_style(span_11, `background: ${get(hexColor) ?? ""};`);
set_text(text_15, get(hexColor));
});
- delegated("pointerdown", div_31, svDrag);
- delegated("pointerdown", div_33, hueDrag);
+ delegated("pointerdown", div_32, svDrag);
+ delegated("pointerdown", div_34, hueDrag);
delegated("click", button_23, cancelColorPop);
delegated("click", button_24, okColorPop);
- append($$anchor3, div_30);
+ append($$anchor3, div_31);
};
if_block(node_28, ($$render) => {
if (get(colorPopout)) $$render(consequent_17);
});
}
- var div_37 = sibling(div_29, 2);
- var div_38 = child(div_37);
+ var div_38 = sibling(div_30, 2);
+ var div_39 = child(div_38);
let classes_6;
let styles;
- var div_39 = child(div_38);
- var img_1 = child(div_39);
+ var node_30 = child(div_39);
+ {
+ var consequent_18 = ($$anchor3) => {
+ var div_40 = root_28$1();
+ var button_25 = sibling(child(div_40), 2);
+ delegated("pointerdown", div_40, (e) => e.stopPropagation());
+ delegated("click", button_25, exitMaskEdit);
+ append($$anchor3, div_40);
+ };
+ if_block(node_30, ($$render) => {
+ if (get(maskEdit)) $$render(consequent_18);
+ });
+ }
+ var div_41 = sibling(node_30, 2);
+ var img_1 = child(div_41);
set_style(img_1, "", {}, { display: "none" });
bind_this(img_1, ($$value) => imgEl = $$value, () => imgEl);
var canvas = sibling(img_1, 2);
+ let styles_1;
bind_this(canvas, ($$value) => belowCanvasEl = $$value, () => belowCanvasEl);
var canvas_1 = sibling(canvas, 2);
- let styles_1;
+ let styles_2;
bind_this(canvas_1, ($$value) => paintCanvas = $$value, () => paintCanvas);
- var node_30 = sibling(canvas_1, 2);
- each(node_30, 19, () => get(layers), (L) => L.id, ($$anchor3, L, i) => {
+ var node_31 = sibling(canvas_1, 2);
+ each(node_31, 19, () => get(layers), (L) => L.id, ($$anchor3, L, i) => {
var fragment_12 = comment();
- var node_31 = first_child(fragment_12);
+ var node_32 = first_child(fragment_12);
{
- var consequent_18 = ($$anchor4) => {
- var canvas_2 = root_29$1();
- let styles_2;
+ var consequent_19 = ($$anchor4) => {
+ var canvas_2 = root_30$1();
+ let styles_3;
bind_this(canvas_2, ($$value, i2) => get(layerCanvasEls)[i2] = $$value, (i2) => {
var _a;
return (_a = get(layerCanvasEls)) == null ? void 0 : _a[i2];
@@ -11395,51 +12059,54 @@ function EditModal($$anchor, $$props) {
($0) => {
set_attribute(canvas_2, "width", get(width));
set_attribute(canvas_2, "height", get(height));
- styles_2 = set_style(canvas_2, "", styles_2, $0);
+ styles_3 = set_style(canvas_2, "", styles_3, $0);
},
[
() => ({
- display: get(L).visible ? null : "none",
- opacity: get(L).opacity,
+ display: get(hasCustomBlend) || !layerShown(get(L)) ? "none" : null,
+ opacity: layerEffOpacity(get(L)),
"mix-blend-mode": layerBlendCss(get(L))
})
]
);
append($$anchor4, canvas_2);
};
- if_block(node_31, ($$render) => {
- if (get(i) > get(activeIndex)) $$render(consequent_18);
+ if_block(node_32, ($$render) => {
+ if (get(i) > get(activeIndex)) $$render(consequent_19);
});
}
append($$anchor3, fragment_12);
});
- var node_32 = sibling(node_30, 2);
- {
- var consequent_19 = ($$anchor3) => {
- var div_40 = root_30$1();
- template_effect(() => set_style(div_40, `left:${get(healSource).x ?? ""}px; top:${get(healSource).y ?? ""}px; width:${18 / get(zoom)}px; height:${18 / get(zoom)}px; border-width:${1.5 / get(zoom)}px;`));
- append($$anchor3, div_40);
- };
- if_block(node_32, ($$render) => {
- if ((get(tool) === "heal" || get(tool) === "stamp") && get(healSource) && !get(healPaintingUi)) $$render(consequent_19);
- });
- }
- var node_33 = sibling(node_32, 2);
+ var canvas_3 = sibling(node_31, 2);
+ let styles_4;
+ bind_this(canvas_3, ($$value) => customEl = $$value, () => customEl);
+ var node_33 = sibling(canvas_3, 2);
{
var consequent_20 = ($$anchor3) => {
- var div_41 = root_31$1();
- template_effect(() => set_style(div_41, `left:${get(selRect).x ?? ""}px; top:${get(selRect).y ?? ""}px; width:${get(selRect).w ?? ""}px; height:${get(selRect).h ?? ""}px; --ant-w:${1.5 / get(zoom)}px; --ant-step:${8 / get(zoom)}px;`));
- append($$anchor3, div_41);
+ var div_42 = root_31$1();
+ template_effect(() => set_style(div_42, `left:${get(healSource).x ?? ""}px; top:${get(healSource).y ?? ""}px; width:${18 / get(zoom)}px; height:${18 / get(zoom)}px; border-width:${1.5 / get(zoom)}px;`));
+ append($$anchor3, div_42);
};
if_block(node_33, ($$render) => {
- if (get(selRect)) $$render(consequent_20);
+ if ((get(tool) === "heal" || get(tool) === "stamp") && get(healSource) && !get(healPaintingUi)) $$render(consequent_20);
});
}
var node_34 = sibling(node_33, 2);
{
var consequent_21 = ($$anchor3) => {
+ var div_43 = root_32$1();
+ template_effect(() => set_style(div_43, `left:${get(selRect).x ?? ""}px; top:${get(selRect).y ?? ""}px; width:${get(selRect).w ?? ""}px; height:${get(selRect).h ?? ""}px; --ant-w:${1.5 / get(zoom)}px; --ant-step:${8 / get(zoom)}px;`));
+ append($$anchor3, div_43);
+ };
+ if_block(node_34, ($$render) => {
+ if (get(selRect)) $$render(consequent_21);
+ });
+ }
+ var node_35 = sibling(node_34, 2);
+ {
+ var consequent_22 = ($$anchor3) => {
const pts = user_derived(() => get(lassoPts).map((p) => `${p.x},${p.y}`).join(" "));
- var svg_7 = root_32$1();
+ var svg_7 = root_33$1();
var polyline = child(svg_7);
var polyline_1 = sibling(polyline);
template_effect(() => {
@@ -11453,36 +12120,36 @@ function EditModal($$anchor, $$props) {
});
append($$anchor3, svg_7);
};
- if_block(node_34, ($$render) => {
- if (get(lassoPts) && get(lassoPts).length > 1) $$render(consequent_21);
- });
- }
- var node_35 = sibling(node_34, 2);
- {
- var consequent_22 = ($$anchor3) => {
- var div_42 = root_33$1();
- template_effect(() => set_style(div_42, `left:0; top:0; width:${1 / get(zoom)}px; height:${get(height) ?? ""}px; transform: translateX(${get(snapGuides).v ?? ""}px);`));
- append($$anchor3, div_42);
- };
if_block(node_35, ($$render) => {
- if (get(snapGuides).v != null) $$render(consequent_22);
+ if (get(lassoPts) && get(lassoPts).length > 1) $$render(consequent_22);
});
}
var node_36 = sibling(node_35, 2);
{
var consequent_23 = ($$anchor3) => {
- var div_43 = root_34$1();
- template_effect(() => set_style(div_43, `left:0; top:0; width:${get(width) ?? ""}px; height:${1 / get(zoom)}px; transform: translateY(${get(snapGuides).h ?? ""}px);`));
- append($$anchor3, div_43);
+ var div_44 = root_34$1();
+ template_effect(() => set_style(div_44, `left:0; top:0; width:${1 / get(zoom)}px; height:${get(height) ?? ""}px; transform: translateX(${get(snapGuides).v ?? ""}px);`));
+ append($$anchor3, div_44);
};
if_block(node_36, ($$render) => {
- if (get(snapGuides).h != null) $$render(consequent_23);
+ if (get(snapGuides).v != null) $$render(consequent_23);
});
}
var node_37 = sibling(node_36, 2);
{
var consequent_24 = ($$anchor3) => {
- var svg_8 = root_35$1();
+ var div_45 = root_35$1();
+ template_effect(() => set_style(div_45, `left:0; top:0; width:${get(width) ?? ""}px; height:${1 / get(zoom)}px; transform: translateY(${get(snapGuides).h ?? ""}px);`));
+ append($$anchor3, div_45);
+ };
+ if_block(node_37, ($$render) => {
+ if (get(snapGuides).h != null) $$render(consequent_24);
+ });
+ }
+ var node_38 = sibling(node_37, 2);
+ {
+ var consequent_25 = ($$anchor3) => {
+ var svg_8 = root_36$1();
var g_1 = child(svg_8);
var path = child(g_1);
var path_1 = sibling(path);
@@ -11496,49 +12163,49 @@ function EditModal($$anchor, $$props) {
});
append($$anchor3, svg_8);
};
- if_block(node_37, ($$render) => {
- if (get(selContour)) $$render(consequent_24);
+ if_block(node_38, ($$render) => {
+ if (get(selContour)) $$render(consequent_25);
});
}
- var node_38 = sibling(node_37, 2);
+ var node_39 = sibling(node_38, 2);
{
- var consequent_26 = ($$anchor3) => {
- var fragment_13 = root_36$1();
+ var consequent_27 = ($$anchor3) => {
+ var fragment_13 = root_37$1();
var svg_9 = first_child(fragment_13);
var path_2 = child(svg_9);
- var div_44 = sibling(svg_9, 2);
+ var div_46 = sibling(svg_9, 2);
let classes_7;
- var node_39 = child(div_44);
+ var node_40 = child(div_46);
{
- var consequent_25 = ($$anchor4) => {
- var fragment_14 = root_37$1();
- var div_45 = first_child(fragment_14);
- var div_46 = sibling(div_45, 2);
- var div_47 = sibling(div_46, 2);
+ var consequent_26 = ($$anchor4) => {
+ var fragment_14 = root_38$1();
+ var div_47 = first_child(fragment_14);
var div_48 = sibling(div_47, 2);
+ var div_49 = sibling(div_48, 2);
+ var div_50 = sibling(div_49, 2);
template_effect(() => {
- set_style(div_45, `left: 33.333%; width:${1 / get(zoom)}px;`);
- set_style(div_46, `left: 66.666%; width:${1 / get(zoom)}px;`);
- set_style(div_47, `top: 33.333%; height:${1 / get(zoom)}px;`);
- set_style(div_48, `top: 66.666%; height:${1 / get(zoom)}px;`);
+ set_style(div_47, `left: 33.333%; width:${1 / get(zoom)}px;`);
+ set_style(div_48, `left: 66.666%; width:${1 / get(zoom)}px;`);
+ set_style(div_49, `top: 33.333%; height:${1 / get(zoom)}px;`);
+ set_style(div_50, `top: 66.666%; height:${1 / get(zoom)}px;`);
});
append($$anchor4, fragment_14);
};
- if_block(node_39, ($$render) => {
- if (get(cropDragging)) $$render(consequent_25);
+ if_block(node_40, ($$render) => {
+ if (get(cropDragging)) $$render(consequent_26);
});
}
- var node_40 = sibling(div_44, 2);
- each(node_40, 17, () => HANDLES, index, ($$anchor4, dir) => {
- var div_49 = root_38$1();
- template_effect(($0) => set_style(div_49, $0), [
+ var node_41 = sibling(div_46, 2);
+ each(node_41, 17, () => HANDLES, index, ($$anchor4, dir) => {
+ var div_51 = root_39$1();
+ template_effect(($0) => set_style(div_51, $0), [
() => cropHandleStyle(get(dir), get(cropBox), get(zoom))
]);
- delegated("pointerdown", div_49, (e) => cropPointerDown(e, get(dir)));
- delegated("pointermove", div_49, cropPointerMove);
- delegated("pointerup", div_49, cropPointerUp);
- event("pointercancel", div_49, cropPointerUp);
- append($$anchor4, div_49);
+ delegated("pointerdown", div_51, (e) => cropPointerDown(e, get(dir)));
+ delegated("pointermove", div_51, cropPointerMove);
+ delegated("pointerup", div_51, cropPointerUp);
+ event("pointercancel", div_51, cropPointerUp);
+ append($$anchor4, div_51);
});
template_effect(
($0) => {
@@ -11546,409 +12213,503 @@ function EditModal($$anchor, $$props) {
set_attribute(svg_9, "height", get(height));
set_attribute(svg_9, "viewBox", `0 0 ${get(width) ?? ""} ${get(height) ?? ""}`);
set_attribute(path_2, "d", `M0 0H${get(width) ?? ""}V${get(height) ?? ""}H0Z M${get(cropBox).x ?? ""} ${get(cropBox).y ?? ""}h${get(cropBox).w ?? ""}v${get(cropBox).h ?? ""}h-${get(cropBox).w ?? ""}Z`);
- classes_7 = set_class(div_44, 1, "pcr-ed-crop-border svelte-1ozzqkm", null, classes_7, $0);
- set_style(div_44, `left:${get(cropBox).x ?? ""}px; top:${get(cropBox).y ?? ""}px; width:${get(cropBox).w ?? ""}px; height:${get(cropBox).h ?? ""}px; border-width:${1 / get(zoom)}px;`);
+ classes_7 = set_class(div_46, 1, "pcr-ed-crop-border svelte-1ozzqkm", null, classes_7, $0);
+ set_style(div_46, `left:${get(cropBox).x ?? ""}px; top:${get(cropBox).y ?? ""}px; width:${get(cropBox).w ?? ""}px; height:${get(cropBox).h ?? ""}px; border-width:${1 / get(zoom)}px;`);
},
[() => ({ fresh: cropBoxIsFull() })]
);
- delegated("pointerdown", div_44, (e) => cropPointerDown(e, "move"));
- delegated("pointermove", div_44, cropPointerMove);
- delegated("pointerup", div_44, cropPointerUp);
- event("pointercancel", div_44, cropPointerUp);
+ delegated("pointerdown", div_46, (e) => cropPointerDown(e, "move"));
+ delegated("pointermove", div_46, cropPointerMove);
+ delegated("pointerup", div_46, cropPointerUp);
+ event("pointercancel", div_46, cropPointerUp);
append($$anchor3, fragment_13);
};
- if_block(node_38, ($$render) => {
- if (get(tool) === "crop" && get(cropBox)) $$render(consequent_26);
+ if_block(node_39, ($$render) => {
+ if (get(tool) === "crop" && get(cropBox)) $$render(consequent_27);
});
}
- var node_41 = sibling(node_38, 2);
+ var node_42 = sibling(node_39, 2);
{
- var consequent_28 = ($$anchor3) => {
+ var consequent_29 = ($$anchor3) => {
const hs = user_derived(() => 11 / get(zoom));
- var div_50 = root_39$1();
- var canvas_3 = child(div_50);
- bind_this(canvas_3, ($$value) => set(patchEl, $$value), () => get(patchEl));
- var div_51 = sibling(canvas_3, 2);
- var node_42 = sibling(div_51, 2);
- each(node_42, 17, () => HANDLES, index, ($$anchor4, dir) => {
- var div_52 = root_40$1();
- template_effect(($0) => set_style(div_52, $0), [() => tHandleStyle(get(dir), get(patch), get(zoom))]);
- delegated("pointerdown", div_52, (e) => patchPointerDown(e, get(dir)));
- delegated("pointermove", div_52, patchPointerMove);
- delegated("pointerup", div_52, patchPointerUp);
- event("pointercancel", div_52, patchPointerUp);
- append($$anchor4, div_52);
+ var div_52 = root_40$1();
+ var canvas_4 = child(div_52);
+ bind_this(canvas_4, ($$value) => set(patchEl, $$value), () => get(patchEl));
+ var div_53 = sibling(canvas_4, 2);
+ var node_43 = sibling(div_53, 2);
+ each(node_43, 17, () => HANDLES, index, ($$anchor4, dir) => {
+ var div_54 = root_41$1();
+ template_effect(($0) => set_style(div_54, $0), [() => tHandleStyle(get(dir), get(patch), get(zoom))]);
+ delegated("pointerdown", div_54, (e) => patchPointerDown(e, get(dir)));
+ delegated("pointermove", div_54, patchPointerMove);
+ delegated("pointerup", div_54, patchPointerUp);
+ event("pointercancel", div_54, patchPointerUp);
+ append($$anchor4, div_54);
});
- var node_43 = sibling(node_42, 2);
+ var node_44 = sibling(node_43, 2);
{
- var consequent_27 = ($$anchor4) => {
- var fragment_15 = root_41$1();
- var div_53 = first_child(fragment_15);
- var div_54 = sibling(div_53, 2);
+ var consequent_28 = ($$anchor4) => {
+ var fragment_15 = root_42$1();
+ var div_55 = first_child(fragment_15);
+ var div_56 = sibling(div_55, 2);
template_effect(() => {
- set_style(div_53, `left:50%; top:${-22 / get(zoom)}px; width:${1 / get(zoom)}px; height:${22 / get(zoom)}px;`);
- set_style(div_54, `left:calc(50% - ${get(hs) / 2}px); top:${-22 / get(zoom) - get(hs) / 2}px; width:${get(hs) ?? ""}px; height:${get(hs) ?? ""}px;`);
+ set_style(div_55, `left:50%; top:${-22 / get(zoom)}px; width:${1 / get(zoom)}px; height:${22 / get(zoom)}px;`);
+ set_style(div_56, `left:calc(50% - ${get(hs) / 2}px); top:${-22 / get(zoom) - get(hs) / 2}px; width:${get(hs) ?? ""}px; height:${get(hs) ?? ""}px;`);
});
- delegated("pointerdown", div_54, (e) => patchPointerDown(e, "rot"));
- delegated("pointermove", div_54, patchPointerMove);
- delegated("pointerup", div_54, patchPointerUp);
- event("pointercancel", div_54, patchPointerUp);
+ delegated("pointerdown", div_56, (e) => patchPointerDown(e, "rot"));
+ delegated("pointermove", div_56, patchPointerMove);
+ delegated("pointerup", div_56, patchPointerUp);
+ event("pointercancel", div_56, patchPointerUp);
append($$anchor4, fragment_15);
};
- if_block(node_43, ($$render) => {
- if (get(patch).transform) $$render(consequent_27);
+ if_block(node_44, ($$render) => {
+ if (get(patch).transform) $$render(consequent_28);
});
}
template_effect(() => {
- set_style(div_50, `left:${get(patch).x ?? ""}px; top:${get(patch).y ?? ""}px; width:${get(patch).w ?? ""}px; height:${get(patch).h ?? ""}px; transform: rotate(${(get(patch).rot || 0) ?? ""}rad);`);
- set_style(canvas_3, `left:0; top:0; width:100%; height:100%; opacity:${get(patchOpacity) ?? ""}; transform: scale(${get(patch).flipH ? -1 : 1}, ${get(patch).flipV ? -1 : 1});`);
- set_style(div_51, `inset:0; border-width:${1 / get(zoom)}px;`);
+ set_style(div_52, `left:${get(patch).x ?? ""}px; top:${get(patch).y ?? ""}px; width:${get(patch).w ?? ""}px; height:${get(patch).h ?? ""}px; transform: rotate(${(get(patch).rot || 0) ?? ""}rad);`);
+ set_style(canvas_4, `left:0; top:0; width:100%; height:100%; opacity:${get(patchOpacity) ?? ""}; transform: scale(${get(patch).flipH ? -1 : 1}, ${get(patch).flipV ? -1 : 1});`);
+ set_style(div_53, `inset:0; border-width:${1 / get(zoom)}px;`);
});
- delegated("pointerdown", canvas_3, (e) => patchPointerDown(e, "move"));
- delegated("pointermove", canvas_3, patchPointerMove);
- delegated("pointerup", canvas_3, patchPointerUp);
- event("pointercancel", canvas_3, patchPointerUp);
- append($$anchor3, div_50);
+ delegated("pointerdown", canvas_4, (e) => patchPointerDown(e, "move"));
+ delegated("pointermove", canvas_4, patchPointerMove);
+ delegated("pointerup", canvas_4, patchPointerUp);
+ event("pointercancel", canvas_4, patchPointerUp);
+ append($$anchor3, div_52);
};
- if_block(node_41, ($$render) => {
- if (get(patch)) $$render(consequent_28);
+ if_block(node_42, ($$render) => {
+ if (get(patch)) $$render(consequent_29);
});
}
- var node_44 = sibling(div_39, 2);
- {
- var consequent_29 = ($$anchor3) => {
- var div_55 = root_42$1();
- template_effect(() => set_style(div_55, `left: ${get(cursorPos).x ?? ""}px; top: ${get(cursorPos).y ?? ""}px; width: ${(get(tool) === "liquify" ? get(liqSize) : get(brushSize)) * get(zoom)}px; height: ${(get(tool) === "liquify" ? get(liqSize) : get(brushSize)) * get(zoom)}px;`));
- append($$anchor3, div_55);
- };
- if_block(node_44, ($$render) => {
- if (get(cursorPos) && !get(colorPopout) && !get(transforming) && !get(hiddenEditBlocked) && (get(tool) === "brush" || get(tool) === "eraser" || get(tool) === "spot" || get(tool) === "heal" || get(tool) === "stamp" || get(tool) === "liquify")) $$render(consequent_29);
- });
- }
- var node_45 = sibling(node_44, 2);
+ var node_45 = sibling(div_41, 2);
{
var consequent_30 = ($$anchor3) => {
- var div_56 = root_43$1();
- var text_16 = child(div_56);
- template_effect(() => {
- set_style(div_56, `left: ${get(cursorPos).x + 12}px; top: ${get(cursorPos).y - 16}px;`);
- set_text(text_16, get(selBadge));
- });
- append($$anchor3, div_56);
+ var div_57 = root_43$1();
+ template_effect(() => set_style(div_57, `left: ${get(cursorPos).x ?? ""}px; top: ${get(cursorPos).y ?? ""}px; width: ${(get(tool) === "liquify" ? get(liqSize) : get(brushSize)) * get(zoom)}px; height: ${(get(tool) === "liquify" ? get(liqSize) : get(brushSize)) * get(zoom)}px;`));
+ append($$anchor3, div_57);
};
if_block(node_45, ($$render) => {
- if (get(selBadge) && get(cursorPos)) $$render(consequent_30);
+ if (get(cursorPos) && !get(colorPopout) && !get(transforming) && !get(hiddenEditBlocked) && (get(tool) === "brush" || get(tool) === "eraser" || get(tool) === "spot" || get(tool) === "heal" || get(tool) === "stamp" || get(tool) === "liquify")) $$render(consequent_30);
});
}
var node_46 = sibling(node_45, 2);
{
var consequent_31 = ($$anchor3) => {
- var div_57 = root_44$1();
- template_effect(() => set_style(div_57, `left: ${get(cursorPos).x + get(healStrokeOffset).dx * get(zoom)}px; top: ${get(cursorPos).y + get(healStrokeOffset).dy * get(zoom)}px;`));
- append($$anchor3, div_57);
+ var div_58 = root_44$1();
+ var text_16 = child(div_58);
+ template_effect(() => {
+ set_style(div_58, `left: ${get(cursorPos).x + 12}px; top: ${get(cursorPos).y - 16}px;`);
+ set_text(text_16, get(selBadge));
+ });
+ append($$anchor3, div_58);
};
if_block(node_46, ($$render) => {
- if ((get(tool) === "heal" || get(tool) === "stamp") && get(healPaintingUi) && get(healStrokeOffset) && get(cursorPos)) $$render(consequent_31);
+ if (get(selBadge) && get(cursorPos)) $$render(consequent_31);
});
}
var node_47 = sibling(node_46, 2);
{
var consequent_32 = ($$anchor3) => {
- var div_58 = root_45$1();
- template_effect(() => set_style(div_58, `left: ${get(pickPreview).x ?? ""}px; top: ${get(pickPreview).y ?? ""}px; background: ${get(pickPreview).hex ?? ""};`));
- append($$anchor3, div_58);
+ var div_59 = root_45$1();
+ template_effect(() => set_style(div_59, `left: ${get(cursorPos).x + get(healStrokeOffset).dx * get(zoom)}px; top: ${get(cursorPos).y + get(healStrokeOffset).dy * get(zoom)}px;`));
+ append($$anchor3, div_59);
};
if_block(node_47, ($$render) => {
- if (get(pickPreview)) $$render(consequent_32);
+ if ((get(tool) === "heal" || get(tool) === "stamp") && get(healPaintingUi) && get(healStrokeOffset) && get(cursorPos)) $$render(consequent_32);
});
}
var node_48 = sibling(node_47, 2);
{
var consequent_33 = ($$anchor3) => {
- var div_59 = root_46$1();
- append($$anchor3, div_59);
+ var div_60 = root_46$1();
+ template_effect(() => set_style(div_60, `left: ${get(pickPreview).x ?? ""}px; top: ${get(pickPreview).y ?? ""}px; background: ${get(pickPreview).hex ?? ""};`));
+ append($$anchor3, div_60);
};
if_block(node_48, ($$render) => {
- if (get(patch)) $$render(consequent_33);
+ if (get(pickPreview)) $$render(consequent_33);
});
}
- bind_this(div_38, ($$value) => viewportEl = $$value, () => viewportEl);
- var div_60 = sibling(div_37, 2);
- var div_61 = child(div_60);
- var div_62 = child(div_61);
- var span_13 = sibling(child(div_62), 2);
- var text_17 = child(span_13);
- var div_63 = sibling(div_62, 2);
- var img_2 = child(div_63);
- var node_49 = sibling(img_2, 2);
+ var node_49 = sibling(node_48, 2);
{
var consequent_34 = ($$anchor3) => {
- var div_64 = root_47$1();
- template_effect(() => set_style(div_64, `left: ${get(navRect).x ?? ""}px; top: ${get(navRect).y ?? ""}px; width: ${get(navRect).w ?? ""}px; height: ${get(navRect).h ?? ""}px;`));
- append($$anchor3, div_64);
+ var div_61 = root_47$1();
+ append($$anchor3, div_61);
};
if_block(node_49, ($$render) => {
- if (get(navRect)) $$render(consequent_34);
+ if (get(patch)) $$render(consequent_34);
});
}
- var div_65 = sibling(div_63, 2);
- var button_25 = child(div_65);
- var input = sibling(button_25, 2);
+ bind_this(div_39, ($$value) => viewportEl = $$value, () => viewportEl);
+ var div_62 = sibling(div_38, 2);
+ var div_63 = child(div_62);
+ var div_64 = child(div_63);
+ var span_13 = sibling(child(div_64), 2);
+ var text_17 = child(span_13);
+ var div_65 = sibling(div_64, 2);
+ var img_2 = child(div_65);
+ var node_50 = sibling(img_2, 2);
+ {
+ var consequent_35 = ($$anchor3) => {
+ var div_66 = root_48$1();
+ template_effect(() => set_style(div_66, `left: ${get(navRect).x ?? ""}px; top: ${get(navRect).y ?? ""}px; width: ${get(navRect).w ?? ""}px; height: ${get(navRect).h ?? ""}px;`));
+ append($$anchor3, div_66);
+ };
+ if_block(node_50, ($$render) => {
+ if (get(navRect)) $$render(consequent_35);
+ });
+ }
+ var div_67 = sibling(div_65, 2);
+ var button_26 = child(div_67);
+ var input = sibling(button_26, 2);
set_attribute(input, "min", Math.log(0.05));
set_attribute(input, "max", Math.log(12));
- var button_26 = sibling(input, 2);
- var node_50 = sibling(div_61, 2);
+ var button_27 = sibling(input, 2);
+ var node_51 = sibling(div_63, 2);
{
- var consequent_39 = ($$anchor3) => {
- var div_66 = root_48$1();
- var span_14 = child(div_66);
+ var consequent_40 = ($$anchor3) => {
+ var div_68 = root_49$1();
+ var span_14 = child(div_68);
var text_18 = child(span_14);
- var div_67 = sibling(span_14, 2);
- var node_51 = child(div_67);
- {
- var consequent_35 = ($$anchor4) => {
- var button_27 = root_49$1();
- template_effect(() => {
- button_27.disabled = !!get(handoffBusy);
- set_attribute(button_27, "title", get(selActive) ? "Inpaint the selected region — it's pre-masked; the result returns as a new layer." : "Inpaint — an alpha-bounded layer pre-masks its silhouette, else paint the mask in the Inpaint editor; the result returns as a new layer.");
- });
- delegated("click", button_27, openInpaintHandoff);
- append($$anchor4, button_27);
- };
- if_block(node_51, ($$render) => {
- if (onOpenInpaint()) $$render(consequent_35);
- });
- }
- var node_52 = sibling(node_51, 2);
+ var div_69 = sibling(span_14, 2);
+ var node_52 = child(div_69);
{
var consequent_36 = ($$anchor4) => {
var button_28 = root_50$1();
- template_effect(() => button_28.disabled = !!get(handoffBusy));
- delegated("click", button_28, openI2iHandoff);
+ template_effect(() => {
+ button_28.disabled = !!get(handoffBusy);
+ set_attribute(button_28, "title", get(selActive) ? "Inpaint the selected region — it's pre-masked; the result returns as a new layer." : "Inpaint — an alpha-bounded layer pre-masks its silhouette, else paint the mask in the Inpaint editor; the result returns as a new layer.");
+ });
+ delegated("click", button_28, openInpaintHandoff);
append($$anchor4, button_28);
};
if_block(node_52, ($$render) => {
- if (onOpenI2i()) $$render(consequent_36);
+ if (onOpenInpaint()) $$render(consequent_36);
});
}
var node_53 = sibling(node_52, 2);
{
var consequent_37 = ($$anchor4) => {
var button_29 = root_51$1();
- template_effect(() => {
- button_29.disabled = !!get(handoffBusy);
- set_attribute(button_29, "title", get(selActive) ? "Upscale the selected region at the quality you pick." : "Upscale the active layer's content at the quality you pick — grow the canvas to keep it 1:1, or squeeze it back in place.");
- });
- delegated("click", button_29, openUpscaleHandoff);
+ template_effect(() => button_29.disabled = !!get(handoffBusy));
+ delegated("click", button_29, openI2iHandoff);
append($$anchor4, button_29);
};
if_block(node_53, ($$render) => {
- if (onOpenUpscale()) $$render(consequent_37);
+ if (onOpenI2i()) $$render(consequent_37);
});
}
var node_54 = sibling(node_53, 2);
{
var consequent_38 = ($$anchor4) => {
var button_30 = root_52$1();
- template_effect(() => button_30.disabled = !!get(handoffBusy));
- delegated("click", button_30, openReposeHandoff);
+ template_effect(() => {
+ button_30.disabled = !!get(handoffBusy);
+ set_attribute(button_30, "title", get(selActive) ? "Upscale the selected region at the quality you pick." : "Upscale the active layer's content at the quality you pick — grow the canvas to keep it 1:1, or squeeze it back in place.");
+ });
+ delegated("click", button_30, openUpscaleHandoff);
append($$anchor4, button_30);
};
if_block(node_54, ($$render) => {
- if (onOpenRepose()) $$render(consequent_38);
+ if (onOpenUpscale()) $$render(consequent_38);
+ });
+ }
+ var node_55 = sibling(node_54, 2);
+ {
+ var consequent_39 = ($$anchor4) => {
+ var button_31 = root_53$1();
+ template_effect(() => button_31.disabled = !!get(handoffBusy));
+ delegated("click", button_31, openReposeHandoff);
+ append($$anchor4, button_31);
+ };
+ if_block(node_55, ($$render) => {
+ if (onOpenRepose()) $$render(consequent_39);
});
}
template_effect(() => set_text(text_18, `Render ${get(selActive) ? "selection" : "layer"}`));
- append($$anchor3, div_66);
+ append($$anchor3, div_68);
};
- if_block(node_50, ($$render) => {
- if (onOpenInpaint() || onOpenUpscale() || onOpenRepose() || onOpenI2i()) $$render(consequent_39);
+ if_block(node_51, ($$render) => {
+ if (onOpenInpaint() || onOpenUpscale() || onOpenRepose() || onOpenI2i()) $$render(consequent_40);
});
}
- var div_68 = sibling(node_50, 2);
- var div_69 = child(div_68);
- var span_15 = child(div_69);
- var button_31 = child(span_15);
+ var div_70 = sibling(node_51, 2);
+ var div_71 = child(div_70);
+ var span_15 = child(div_71);
+ var button_32 = child(span_15);
let classes_8;
- var button_32 = sibling(button_31, 2);
+ var button_33 = sibling(button_32, 2);
let classes_9;
- var node_55 = sibling(span_15, 2);
+ var node_56 = sibling(span_15, 2);
{
- var consequent_40 = ($$anchor3) => {
- var span_16 = root_53$1();
- var button_33 = child(span_16);
- var button_34 = sibling(button_33, 2);
+ var consequent_41 = ($$anchor3) => {
+ var span_16 = root_54$1();
+ var button_34 = child(span_16);
var button_35 = sibling(button_34, 2);
var button_36 = sibling(button_35, 2);
+ var button_37 = sibling(button_36, 2);
template_effect(
($0) => {
- button_34.disabled = get(selectedIds).length < 2;
- button_35.disabled = get(layers).length <= 1;
- set_attribute(button_36, "title", get(selectedIds).length > 1 ? "Delete selected layers" : "Delete layer");
- button_36.disabled = $0;
+ button_35.disabled = get(selectedIds).length < 2;
+ button_36.disabled = get(layers).length <= 1;
+ set_attribute(button_37, "title", get(selectedIds).length > 1 ? "Delete selected layers" : "Delete layer");
+ button_37.disabled = $0;
},
[() => !canDeleteSelected()]
);
- delegated("click", button_33, () => addLayer());
- delegated("click", button_34, mergeSelected);
- delegated("click", button_35, flattenImage);
- delegated("click", button_36, deleteSelectedLayers);
+ delegated("click", button_34, () => addLayer());
+ delegated("click", button_35, mergeSelected);
+ delegated("click", button_36, flattenImage);
+ delegated("click", button_37, deleteSelectedLayers);
append($$anchor3, span_16);
};
- if_block(node_55, ($$render) => {
- if (get(lhTab) === "layers") $$render(consequent_40);
+ if_block(node_56, ($$render) => {
+ if (get(lhTab) === "layers") $$render(consequent_41);
});
}
- var node_56 = sibling(div_69, 2);
+ var node_57 = sibling(div_71, 2);
{
- var consequent_44 = ($$anchor3) => {
- var fragment_16 = root_54$1();
- var select_2 = first_child(fragment_16);
- each(select_2, 21, () => BLEND_MODES, ([v, label]) => v, ($$anchor4, $$item) => {
- var $$array_2 = user_derived(() => to_array(get($$item), 2));
- let v = () => get($$array_2)[0];
- let label = () => get($$array_2)[1];
- var option_4 = root_55$1();
- var text_19 = child(option_4);
- var option_4_value = {};
+ var consequent_47 = ($$anchor3) => {
+ var fragment_16 = root_55$1();
+ var select_3 = first_child(fragment_16);
+ each(select_3, 21, () => BLEND_MODES, ([v, label]) => v, ($$anchor4, $$item) => {
+ var $$array_3 = user_derived(() => to_array(get($$item), 2));
+ let v = () => get($$array_3)[0];
+ let label = () => get($$array_3)[1];
+ var option_6 = root_56$1();
+ var text_19 = child(option_6);
+ var option_6_value = {};
template_effect(() => {
set_text(text_19, label());
- if (option_4_value !== (option_4_value = v())) {
- option_4.value = (option_4.__value = v()) ?? "";
+ if (option_6_value !== (option_6_value = v())) {
+ option_6.value = (option_6.__value = v()) ?? "";
}
});
- append($$anchor4, option_4);
+ append($$anchor4, option_6);
});
- var select_2_value;
- init_select(select_2);
- var div_70 = sibling(select_2, 2);
- each(div_70, 21, () => [...get(layers)].reverse(), (L) => L.id, ($$anchor4, L) => {
- const i = user_derived(() => get(layers).indexOf(get(L)));
- var div_71 = root_56$1();
- let classes_10;
- var span_17 = child(div_71);
- let classes_11;
- var span_18 = sibling(span_17, 2);
- let classes_12;
- var text_20 = child(span_18);
- var node_57 = sibling(span_18, 2);
+ var select_3_value;
+ init_select(select_3);
+ var div_72 = sibling(select_3, 2);
+ each(div_72, 21, () => get(panelRows), (row) => row.type === "group" ? "g" + row.group.id : row.L.id, ($$anchor4, row) => {
+ var fragment_17 = comment();
+ var node_58 = first_child(fragment_17);
{
- var consequent_41 = ($$anchor5) => {
- var input_1 = root_57$1();
- autofocus(input_1);
- delegated("click", input_1, (e) => e.stopPropagation());
- delegated("keydown", input_1, (e) => {
- e.stopPropagation();
- if (e.key === "Enter") commitRename();
- else if (e.key === "Escape") set(renamingId, null);
- });
- event("blur", input_1, commitRename);
- bind_value(input_1, () => get(renameText), ($$value) => set(renameText, $$value));
- append($$anchor5, input_1);
- };
- var alternate_3 = ($$anchor5) => {
- var span_19 = root_58$1();
- var text_21 = child(span_19);
- var node_58 = sibling(text_21);
+ var consequent_43 = ($$anchor5) => {
+ const g = user_derived(() => get(row).group);
+ var div_73 = root_58$1();
+ var span_17 = child(div_73);
+ var text_20 = child(span_17);
+ var span_18 = sibling(span_17, 2);
+ let classes_10;
+ var text_21 = child(span_18);
+ var node_59 = sibling(span_18, 2);
{
var consequent_42 = ($$anchor6) => {
- var span_20 = root_59$1();
- append($$anchor6, span_20);
+ var input_1 = root_59$1();
+ autofocus(input_1);
+ delegated("click", input_1, (e) => e.stopPropagation());
+ delegated("keydown", input_1, (e) => {
+ e.stopPropagation();
+ if (e.key === "Enter") commitGroupRename();
+ else if (e.key === "Escape") set(renamingGroupId, null);
+ });
+ event("blur", input_1, commitGroupRename);
+ bind_value(input_1, () => get(renameText), ($$value) => set(renameText, $$value));
+ append($$anchor6, input_1);
};
- if_block(node_58, ($$render) => {
- if (get(L).mask) $$render(consequent_42);
- });
- }
- var node_59 = sibling(node_58);
- {
- var consequent_43 = ($$anchor6) => {
- var span_21 = root_60$1();
- var text_22 = child(span_21);
- template_effect(() => set_text(text_22, get(L).blend));
- append($$anchor6, span_21);
+ var alternate_3 = ($$anchor6) => {
+ var span_19 = root_60$1();
+ var text_22 = child(span_19);
+ template_effect(() => set_text(text_22, `📁 ${get(g).name ?? ""}`));
+ delegated("dblclick", span_19, (e) => {
+ e.stopPropagation();
+ startGroupRename(get(g));
+ });
+ append($$anchor6, span_19);
};
- var d_1 = user_derived(() => layerBlendCss(get(L)));
if_block(node_59, ($$render) => {
- if (get(d_1)) $$render(consequent_43);
+ if (get(renamingGroupId) === get(g).id) $$render(consequent_42);
+ else $$render(alternate_3, -1);
});
}
- template_effect(() => set_text(text_21, get(L).name));
- delegated("dblclick", span_19, (e) => {
- e.stopPropagation();
- startRename(get(L));
+ var input_2 = sibling(node_59, 2);
+ var button_38 = sibling(input_2, 2);
+ template_effect(() => {
+ set_text(text_20, get(g).collapsed ? "▸" : "▾");
+ classes_10 = set_class(span_18, 1, "pcr-ed-eye svelte-1ozzqkm", null, classes_10, { off: !get(g).visible });
+ set_text(text_21, get(g).visible ? "👁" : "—");
+ set_value(input_2, get(g).opacity);
});
- append($$anchor5, span_19);
+ delegated("click", span_17, () => toggleGroupCollapsed(get(g).id));
+ delegated("click", span_18, () => setGroupVisible(get(g).id, !get(g).visible));
+ delegated("pointerdown", input_2, () => {
+ opacityDragStart = get(g).opacity;
+ });
+ delegated("input", input_2, (e) => setGroupOpacity(get(g).id, +e.currentTarget.value));
+ delegated("change", input_2, (e) => {
+ if (opacityDragStart === null || +e.currentTarget.value !== opacityDragStart) commitAction("Group Opacity");
+ opacityDragStart = null;
+ });
+ delegated("click", input_2, (e) => e.stopPropagation());
+ delegated("click", button_38, () => ungroup(get(g).id));
+ append($$anchor5, div_73);
};
- if_block(node_57, ($$render) => {
- if (get(renamingId) === get(L).id) $$render(consequent_41);
- else $$render(alternate_3, -1);
+ var alternate_5 = ($$anchor5) => {
+ const L = user_derived(() => get(row).L);
+ const i = user_derived(() => get(row).i);
+ var div_74 = root_61$1();
+ let classes_11;
+ var span_20 = child(div_74);
+ let classes_12;
+ var span_21 = sibling(span_20, 2);
+ let classes_13;
+ var text_23 = child(span_21);
+ var node_60 = sibling(span_21, 2);
+ {
+ var consequent_44 = ($$anchor6) => {
+ var img_3 = root_62$1();
+ let classes_14;
+ template_effect(() => {
+ classes_14 = set_class(img_3, 1, "pcr-ed-mask-thumb svelte-1ozzqkm", null, classes_14, { editing: get(maskEdit) && get(i) === get(activeIndex) });
+ set_attribute(img_3, "src", get(L).maskUrl);
+ set_attribute(img_3, "title", get(maskEdit) && get(i) === get(activeIndex) ? "Editing layer mask — click the layer to edit pixels" : "Click to edit layer mask");
+ });
+ delegated("click", img_3, (e) => {
+ e.stopPropagation();
+ toggleMaskEdit(get(i));
+ });
+ append($$anchor6, img_3);
+ };
+ if_block(node_60, ($$render) => {
+ if (get(L).mask) $$render(consequent_44);
+ });
+ }
+ var node_61 = sibling(node_60, 2);
+ {
+ var consequent_45 = ($$anchor6) => {
+ var input_3 = root_63$1();
+ autofocus(input_3);
+ delegated("click", input_3, (e) => e.stopPropagation());
+ delegated("keydown", input_3, (e) => {
+ e.stopPropagation();
+ if (e.key === "Enter") commitRename();
+ else if (e.key === "Escape") set(renamingId, null);
+ });
+ event("blur", input_3, commitRename);
+ bind_value(input_3, () => get(renameText), ($$value) => set(renameText, $$value));
+ append($$anchor6, input_3);
+ };
+ var alternate_4 = ($$anchor6) => {
+ var span_22 = root_64$1();
+ var text_24 = child(span_22);
+ var node_62 = sibling(text_24);
+ {
+ var consequent_46 = ($$anchor7) => {
+ var span_23 = root_65$1();
+ var text_25 = child(span_23);
+ template_effect(() => {
+ set_attribute(span_23, "title", `Blend: ${get(L).blend}`);
+ set_text(text_25, get(L).blend);
+ });
+ append($$anchor7, span_23);
+ };
+ if_block(node_62, ($$render) => {
+ if (get(L).blend && get(L).blend !== "normal") $$render(consequent_46);
+ });
+ }
+ template_effect(() => set_text(text_24, get(L).name));
+ delegated("dblclick", span_22, (e) => {
+ e.stopPropagation();
+ startRename(get(L));
+ });
+ append($$anchor6, span_22);
+ };
+ if_block(node_61, ($$render) => {
+ if (get(renamingId) === get(L).id) $$render(consequent_45);
+ else $$render(alternate_4, -1);
+ });
+ }
+ var input_4 = sibling(node_61, 2);
+ template_effect(
+ ($0) => {
+ classes_11 = set_class(div_74, 1, "pcr-ed-layer-row svelte-1ozzqkm", null, classes_11, $0);
+ classes_12 = set_class(span_20, 1, "pcr-ed-layer-grip svelte-1ozzqkm", null, classes_12, { hidden: get(L).isBackground });
+ set_attribute(span_20, "draggable", !get(L).isBackground);
+ classes_13 = set_class(span_21, 1, "pcr-ed-eye svelte-1ozzqkm", null, classes_13, { off: !get(L).visible });
+ set_text(text_23, get(L).visible ? "👁" : "—");
+ set_value(input_4, get(L).opacity);
+ },
+ [
+ () => ({
+ active: get(i) === get(activeIndex),
+ selected: get(selectedIds).includes(get(L).id),
+ dragover: get(dragOverId) === get(L).id,
+ "in-group": get(row).inGroup
+ })
+ ]
+ );
+ delegated("click", div_74, (e) => clickLayer(get(i), e));
+ delegated("contextmenu", div_74, (e) => {
+ e.preventDefault();
+ if (!get(selectedIds).includes(get(L).id)) clickLayer(get(i), e);
+ set(
+ layerMenu,
+ {
+ x: e.clientX,
+ y: e.clientY,
+ i: get(i),
+ flip: e.clientX > window.innerWidth - 340
+ },
+ true
+ );
+ });
+ event("dragover", div_74, (e) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = "move";
+ set(dragOverId, get(L).id, true);
+ });
+ event("dragleave", div_74, () => {
+ if (get(dragOverId) === get(L).id) set(dragOverId, null);
+ });
+ event("drop", div_74, (e) => {
+ e.preventDefault();
+ dropLayer(get(L).id);
+ });
+ event("dragstart", span_20, (e) => {
+ dragId = get(L).id;
+ e.dataTransfer.effectAllowed = "move";
+ e.dataTransfer.setData("text/plain", String(get(L).id));
+ });
+ event("dragend", span_20, () => {
+ dragId = null;
+ set(dragOverId, null);
+ });
+ delegated("click", span_21, (e) => {
+ e.stopPropagation();
+ setLayerVisible(get(i), !get(L).visible);
+ });
+ delegated("pointerdown", input_4, () => {
+ opacityDragStart = get(L).opacity;
+ });
+ delegated("input", input_4, (e) => setLayerOpacity(get(i), +e.currentTarget.value));
+ delegated("change", input_4, (e) => {
+ if (opacityDragStart === null || +e.currentTarget.value !== opacityDragStart) commitAction("Opacity");
+ opacityDragStart = null;
+ });
+ delegated("click", input_4, (e) => e.stopPropagation());
+ append($$anchor5, div_74);
+ };
+ if_block(node_58, ($$render) => {
+ if (get(row).type === "group") $$render(consequent_43);
+ else $$render(alternate_5, -1);
});
}
- var input_2 = sibling(node_57, 2);
- template_effect(
- ($0) => {
- classes_10 = set_class(div_71, 1, "pcr-ed-layer-row svelte-1ozzqkm", null, classes_10, $0);
- classes_11 = set_class(span_17, 1, "pcr-ed-layer-grip svelte-1ozzqkm", null, classes_11, { hidden: get(L).isBackground });
- set_attribute(span_17, "draggable", !get(L).isBackground);
- classes_12 = set_class(span_18, 1, "pcr-ed-eye svelte-1ozzqkm", null, classes_12, { off: !get(L).visible });
- set_text(text_20, get(L).visible ? "👁" : "—");
- set_value(input_2, get(L).opacity);
- },
- [
- () => ({
- active: get(i) === get(activeIndex),
- selected: get(selectedIds).includes(get(L).id),
- dragover: get(dragOverId) === get(L).id
- })
- ]
- );
- delegated("click", div_71, (e) => clickLayer(get(i), e));
- delegated("contextmenu", div_71, (e) => {
- e.preventDefault();
- if (!get(selectedIds).includes(get(L).id)) clickLayer(get(i), e);
- set(layerMenu, { x: e.clientX, y: e.clientY, i: get(i) }, true);
- });
- event("dragover", div_71, (e) => {
- e.preventDefault();
- e.dataTransfer.dropEffect = "move";
- set(dragOverId, get(L).id, true);
- });
- event("dragleave", div_71, () => {
- if (get(dragOverId) === get(L).id) set(dragOverId, null);
- });
- event("drop", div_71, (e) => {
- e.preventDefault();
- dropLayer(get(L).id);
- });
- event("dragstart", span_17, (e) => {
- dragId = get(L).id;
- e.dataTransfer.effectAllowed = "move";
- e.dataTransfer.setData("text/plain", String(get(L).id));
- });
- event("dragend", span_17, () => {
- dragId = null;
- set(dragOverId, null);
- });
- delegated("click", span_18, (e) => {
- e.stopPropagation();
- setLayerVisible(get(i), !get(L).visible);
- });
- delegated("pointerdown", input_2, () => {
- opacityDragStart = get(L).opacity;
- });
- delegated("input", input_2, (e) => setLayerOpacity(get(i), +e.currentTarget.value));
- delegated("change", input_2, (e) => {
- if (opacityDragStart === null || +e.currentTarget.value !== opacityDragStart) commitAction("Opacity");
- opacityDragStart = null;
- });
- delegated("click", input_2, (e) => e.stopPropagation());
- append($$anchor4, div_71);
+ append($$anchor4, fragment_17);
});
template_effect(
($0, $1) => {
- select_2.disabled = $0;
- if (select_2_value !== (select_2_value = $1)) {
- select_2.value = (select_2.__value = $1) ?? "", select_option(select_2, $1);
+ select_3.disabled = $0;
+ if (select_3_value !== (select_3_value = $1)) {
+ select_3.value = (select_3.__value = $1) ?? "", select_option(select_3, $1);
}
},
[
@@ -11962,140 +12723,215 @@ function EditModal($$anchor, $$props) {
}
]
);
- delegated("change", select_2, (e) => setLayerBlend(get(activeIndex), e.currentTarget.value));
+ delegated("change", select_3, (e) => setLayerBlend(get(activeIndex), e.currentTarget.value));
append($$anchor3, fragment_16);
};
- var alternate_4 = ($$anchor3) => {
- var div_72 = root_61$1();
- each(div_72, 21, () => get(history2), index, ($$anchor4, h, i) => {
- var div_73 = root_62$1();
- let classes_13;
- var text_23 = child(div_73);
+ var alternate_6 = ($$anchor3) => {
+ var div_75 = root_66$1();
+ each(div_75, 21, () => get(history2), index, ($$anchor4, h, i) => {
+ var div_76 = root_67$1();
+ let classes_15;
+ var text_26 = child(div_76);
template_effect(() => {
- classes_13 = set_class(div_73, 1, "pcr-ed-hist-row svelte-1ozzqkm", null, classes_13, { active: i === get(histIndex), future: i > get(histIndex) });
- set_text(text_23, get(h).label);
+ classes_15 = set_class(div_76, 1, "pcr-ed-hist-row svelte-1ozzqkm", null, classes_15, { active: i === get(histIndex), future: i > get(histIndex) });
+ set_text(text_26, get(h).label);
});
- delegated("click", div_73, () => jumpTo(i));
- append($$anchor4, div_73);
+ delegated("click", div_76, () => jumpTo(i));
+ append($$anchor4, div_76);
});
- bind_this(div_72, ($$value) => histEl = $$value, () => histEl);
- append($$anchor3, div_72);
+ bind_this(div_75, ($$value) => histEl = $$value, () => histEl);
+ append($$anchor3, div_75);
};
- if_block(node_56, ($$render) => {
- if (get(lhTab) === "layers") $$render(consequent_44);
- else $$render(alternate_4, -1);
+ if_block(node_57, ($$render) => {
+ if (get(lhTab) === "layers") $$render(consequent_47);
+ else $$render(alternate_6, -1);
});
}
- var node_60 = sibling(div_68, 2);
+ var node_63 = sibling(div_70, 2);
{
- var consequent_51 = ($$anchor3) => {
- var div_74 = root_63$1();
- var button_37 = child(div_74);
- var node_61 = sibling(button_37, 2);
+ var consequent_57 = ($$anchor3) => {
+ const Lm = user_derived(() => get(layers)[get(layerMenu).i]);
+ const multi = user_derived(() => get(selectedIds).length > 1);
+ const groupable = user_derived(() => get(selectedIds).filter((id) => {
+ var _a;
+ return !((_a = get(layers).find((ly) => ly.id === id)) == null ? void 0 : _a.isBackground);
+ }).length);
+ var div_77 = root_68();
+ var button_39 = child(div_77);
+ var button_40 = sibling(button_39, 2);
+ var node_64 = sibling(button_40, 2);
{
- var consequent_45 = ($$anchor4) => {
- var button_38 = root_64$1();
- delegated("click", button_38, () => {
+ var consequent_48 = ($$anchor4) => {
+ var button_41 = root_69();
+ delegated("click", button_41, () => {
mergeSelected();
set(layerMenu, null);
});
- append($$anchor4, button_38);
+ append($$anchor4, button_41);
};
- var consequent_46 = ($$anchor4) => {
- var button_39 = root_65$1();
- delegated("click", button_39, () => {
+ var consequent_49 = ($$anchor4) => {
+ var button_42 = root_70$1();
+ delegated("click", button_42, () => {
mergeDown(get(layerMenu).i);
set(layerMenu, null);
});
- append($$anchor4, button_39);
+ append($$anchor4, button_42);
};
- if_block(node_61, ($$render) => {
- if (get(selectedIds).length > 1) $$render(consequent_45);
- else if (get(layerMenu).i > 0) $$render(consequent_46, 1);
+ if_block(node_64, ($$render) => {
+ if (get(multi)) $$render(consequent_48);
+ else if (get(layerMenu).i > 0) $$render(consequent_49, 1);
});
}
- var button_40 = sibling(node_61, 2);
- var node_62 = sibling(button_40, 2);
+ var node_65 = sibling(node_64, 2);
{
- var consequent_48 = ($$anchor4) => {
- var fragment_17 = comment();
- var node_63 = first_child(fragment_17);
- {
- var consequent_47 = ($$anchor5) => {
- var button_41 = root_67$1();
- var text_24 = child(button_41);
- template_effect(() => set_text(text_24, `Add Layer Mask${get(selActive) ? " (Reveal Selection)" : ""}`));
- delegated("click", button_41, () => {
- addLayerMask(get(layerMenu).i);
- set(layerMenu, null);
- });
- append($$anchor5, button_41);
- };
- var alternate_5 = ($$anchor5) => {
- var fragment_18 = root_68();
- var button_42 = first_child(fragment_18);
- var button_43 = sibling(button_42, 2);
- delegated("click", button_42, () => {
- applyLayerMask(get(layerMenu).i);
- set(layerMenu, null);
- });
- delegated("click", button_43, () => {
- deleteLayerMask(get(layerMenu).i);
- set(layerMenu, null);
- });
- append($$anchor5, fragment_18);
- };
- if_block(node_63, ($$render) => {
- var _a;
- if (!((_a = get(layers)[get(layerMenu).i]) == null ? void 0 : _a.mask)) $$render(consequent_47);
- else $$render(alternate_5, -1);
- });
- }
- append($$anchor4, fragment_17);
- };
- if_block(node_62, ($$render) => {
- var _a;
- if (!((_a = get(layers)[get(layerMenu).i]) == null ? void 0 : _a.isBackground)) $$render(consequent_48);
- });
- }
- var node_64 = sibling(node_62, 2);
- {
- var consequent_49 = ($$anchor4) => {
- var button_44 = root_69();
- delegated("click", button_44, () => {
+ var consequent_50 = ($$anchor4) => {
+ var button_43 = root_71$1();
+ delegated("click", button_43, () => {
const i = get(layerMenu).i;
set(layerMenu, null);
if (i === get(activeIndex)) flushActive();
if (matchLayerColorsToBelow(i)) commitAction("Match Colors");
else set(errorMsg, "No usable edge band — the layer's feathered edge must overlap solid content below.");
});
- append($$anchor4, button_44);
+ append($$anchor4, button_43);
};
- if_block(node_64, ($$render) => {
+ if_block(node_65, ($$render) => {
var _a;
- if (get(layerMenu).i > 0 && ((_a = get(layers)[get(layerMenu).i]) == null ? void 0 : _a.bitmap)) $$render(consequent_49);
+ if (get(layerMenu).i > 0 && ((_a = get(Lm)) == null ? void 0 : _a.bitmap)) $$render(consequent_50);
});
}
- var button_45 = sibling(node_64, 2);
- var node_65 = sibling(button_45, 2);
+ var node_66 = sibling(node_65, 2);
{
- var consequent_50 = ($$anchor4) => {
- var button_46 = root_70$1();
- var text_25 = child(button_46);
- template_effect(() => set_text(text_25, get(selectedIds).length > 1 ? "Delete Layers" : "Delete Layer"));
- delegated("click", button_46, () => {
+ var consequent_53 = ($$anchor4) => {
+ var fragment_18 = root_72$1();
+ var node_67 = sibling(first_child(fragment_18), 2);
+ {
+ var consequent_51 = ($$anchor5) => {
+ var button_44 = root_73$1();
+ var text_27 = child(button_44);
+ template_effect(() => set_text(text_27, `Group ${get(multi) ? "Layers" : "Layer"}`));
+ delegated("click", button_44, () => {
+ groupSelected();
+ set(layerMenu, null);
+ });
+ append($$anchor5, button_44);
+ };
+ if_block(node_67, ($$render) => {
+ if (get(groupable)) $$render(consequent_51);
+ });
+ }
+ var node_68 = sibling(node_67, 2);
+ {
+ var consequent_52 = ($$anchor5) => {
+ var button_45 = root_74$1();
+ delegated("click", button_45, () => {
+ ungroup(get(Lm).groupId);
+ set(layerMenu, null);
+ });
+ append($$anchor5, button_45);
+ };
+ if_block(node_68, ($$render) => {
+ var _a;
+ if ((_a = get(Lm)) == null ? void 0 : _a.groupId) $$render(consequent_52);
+ });
+ }
+ append($$anchor4, fragment_18);
+ };
+ if_block(node_66, ($$render) => {
+ var _a;
+ if (get(groupable) || ((_a = get(Lm)) == null ? void 0 : _a.groupId)) $$render(consequent_53);
+ });
+ }
+ var node_69 = sibling(node_66, 2);
+ {
+ var consequent_55 = ($$anchor4) => {
+ var fragment_19 = root_75$1();
+ var node_70 = sibling(first_child(fragment_19), 2);
+ {
+ var consequent_54 = ($$anchor5) => {
+ var button_46 = root_76$1();
+ var text_28 = child(button_46);
+ template_effect(() => set_text(text_28, `Add Layer Mask${get(selActive) ? " (Reveal Selection)" : ""}`));
+ delegated("click", button_46, () => {
+ addLayerMask(get(layerMenu).i);
+ set(layerMenu, null);
+ });
+ append($$anchor5, button_46);
+ };
+ var alternate_7 = ($$anchor5) => {
+ var div_78 = root_77$1();
+ let classes_16;
+ var div_79 = sibling(child(div_78), 2);
+ var button_47 = child(div_79);
+ var text_29 = child(button_47);
+ var button_48 = sibling(button_47, 2);
+ var button_49 = sibling(button_48, 2);
+ var button_50 = sibling(button_49, 2);
+ var button_51 = sibling(button_50, 4);
+ var button_52 = sibling(button_51, 2);
+ template_effect(() => {
+ classes_16 = set_class(div_78, 1, "pcr-ed-submenu-host svelte-1ozzqkm", null, classes_16, { flip: get(layerMenu).flip });
+ set_text(text_29, get(maskEdit) && get(layerMenu).i === get(activeIndex) ? "Done Editing Mask" : "Edit Mask (paint)");
+ });
+ delegated("click", button_47, () => {
+ toggleMaskEdit(get(layerMenu).i);
+ set(layerMenu, null);
+ });
+ delegated("click", button_48, () => {
+ invertLayerMask(get(layerMenu).i);
+ set(layerMenu, null);
+ });
+ delegated("click", button_49, () => {
+ fillLayerMask(get(layerMenu).i, 255);
+ set(layerMenu, null);
+ });
+ delegated("click", button_50, () => {
+ fillLayerMask(get(layerMenu).i, 0);
+ set(layerMenu, null);
+ });
+ delegated("click", button_51, () => {
+ applyLayerMask(get(layerMenu).i);
+ set(layerMenu, null);
+ });
+ delegated("click", button_52, () => {
+ deleteLayerMask(get(layerMenu).i);
+ set(layerMenu, null);
+ });
+ append($$anchor5, div_78);
+ };
+ if_block(node_70, ($$render) => {
+ var _a;
+ if (!((_a = get(Lm)) == null ? void 0 : _a.mask)) $$render(consequent_54);
+ else $$render(alternate_7, -1);
+ });
+ }
+ append($$anchor4, fragment_19);
+ };
+ if_block(node_69, ($$render) => {
+ var _a;
+ if (!((_a = get(Lm)) == null ? void 0 : _a.isBackground)) $$render(consequent_55);
+ });
+ }
+ var button_53 = sibling(node_69, 4);
+ var node_71 = sibling(button_53, 2);
+ {
+ var consequent_56 = ($$anchor4) => {
+ var button_54 = root_78$1();
+ var text_30 = child(button_54);
+ template_effect(() => set_text(text_30, get(multi) ? "Delete Layers" : "Delete Layer"));
+ delegated("click", button_54, () => {
deleteSelectedLayers();
set(layerMenu, null);
});
- append($$anchor4, button_46);
+ append($$anchor4, button_54);
};
- var d_2 = user_derived(() => canDeleteSelected());
- if_block(node_65, ($$render) => {
- if (get(d_2)) $$render(consequent_50);
+ var d_1 = user_derived(() => canDeleteSelected());
+ if_block(node_71, ($$render) => {
+ if (get(d_1)) $$render(consequent_56);
});
}
- template_effect(() => set_style(div_74, `left:${get(layerMenu).x ?? ""}px; top:${get(layerMenu).y ?? ""}px;`));
- delegated("click", button_37, () => {
+ template_effect(() => set_style(div_77, `left:${get(layerMenu).x ?? ""}px; top:${get(layerMenu).y ?? ""}px;`));
+ delegated("click", button_39, () => {
addLayer(get(layerMenu).i);
set(layerMenu, null);
});
@@ -12103,120 +12939,123 @@ function EditModal($$anchor, $$props) {
duplicateLayer(get(layerMenu).i);
set(layerMenu, null);
});
- delegated("click", button_45, () => {
+ delegated("click", button_53, () => {
flattenImage();
set(layerMenu, null);
});
- append($$anchor3, div_74);
+ append($$anchor3, div_77);
};
- if_block(node_60, ($$render) => {
- if (get(layerMenu)) $$render(consequent_51);
+ if_block(node_63, ($$render) => {
+ if (get(layerMenu)) $$render(consequent_57);
});
}
- var node_66 = sibling(node_60, 2);
+ var node_72 = sibling(node_63, 2);
{
- var consequent_54 = ($$anchor3) => {
- var div_75 = root_71$1();
- var node_67 = child(div_75);
+ var consequent_60 = ($$anchor3) => {
+ var div_80 = root_79$1();
+ var node_73 = child(div_80);
{
- var consequent_52 = ($$anchor4) => {
- var fragment_19 = root_72$1();
- var button_47 = first_child(fragment_19);
- var button_48 = sibling(button_47, 2);
- delegated("click", button_47, () => {
+ var consequent_58 = ($$anchor4) => {
+ var fragment_20 = root_80$1();
+ var button_55 = first_child(fragment_20);
+ var button_56 = sibling(button_55, 2);
+ delegated("click", button_55, () => {
deselect();
+ commitAction("Deselect");
set(canvasMenu, null);
});
- delegated("click", button_48, () => {
+ delegated("click", button_56, () => {
selectInverse();
+ commitAction("Select Inverse");
set(canvasMenu, null);
});
- append($$anchor4, fragment_19);
- };
- if_block(node_67, ($$render) => {
- if (get(selActive)) $$render(consequent_52);
- });
- }
- var button_49 = sibling(node_67, 2);
- var text_26 = child(button_49);
- var button_50 = sibling(button_49, 2);
- var button_51 = sibling(button_50, 2);
- var text_27 = child(button_51);
- var node_68 = sibling(button_51, 2);
- {
- var consequent_53 = ($$anchor4) => {
- var fragment_20 = root_73$1();
- var button_52 = first_child(fragment_20);
- var button_53 = sibling(button_52, 2);
- delegated("click", button_52, () => {
- set(canvasMenu, null);
- layerViaCopy(false);
- });
- delegated("click", button_53, () => {
- set(canvasMenu, null);
- layerViaCopy(true);
- });
append($$anchor4, fragment_20);
};
- if_block(node_68, ($$render) => {
- if (get(selActive)) $$render(consequent_53);
+ if_block(node_73, ($$render) => {
+ if (get(selActive)) $$render(consequent_58);
+ });
+ }
+ var button_57 = sibling(node_73, 2);
+ var text_31 = child(button_57);
+ var button_58 = sibling(button_57, 2);
+ var button_59 = sibling(button_58, 2);
+ var text_32 = child(button_59);
+ var node_74 = sibling(button_59, 2);
+ {
+ var consequent_59 = ($$anchor4) => {
+ var fragment_21 = root_81$1();
+ var button_60 = first_child(fragment_21);
+ var button_61 = sibling(button_60, 2);
+ delegated("click", button_60, () => {
+ set(canvasMenu, null);
+ layerViaCopy(false);
+ });
+ delegated("click", button_61, () => {
+ set(canvasMenu, null);
+ layerViaCopy(true);
+ });
+ append($$anchor4, fragment_21);
+ };
+ if_block(node_74, ($$render) => {
+ if (get(selActive)) $$render(consequent_59);
});
}
template_effect(() => {
- set_style(div_75, `left:${get(canvasMenu).x ?? ""}px; top:${get(canvasMenu).y ?? ""}px;`);
- set_text(text_26, get(selActive) ? "Free Transform Selection" : "Free Transform Layer");
- button_51.disabled = get(subjectBusy);
- set_text(text_27, get(subjectBusy) ? "Selecting Subject…" : "Select Subject");
+ set_style(div_80, `left:${get(canvasMenu).x ?? ""}px; top:${get(canvasMenu).y ?? ""}px;`);
+ set_text(text_31, get(selActive) ? "Free Transform Selection" : "Free Transform Layer");
+ button_59.disabled = get(subjectBusy);
+ set_text(text_32, get(subjectBusy) ? "Selecting Subject…" : "Select Subject");
});
- delegated("click", button_49, () => {
+ delegated("click", button_57, () => {
set(canvasMenu, null);
beginTransform();
});
- delegated("click", button_50, () => {
+ delegated("click", button_58, () => {
selectAllRegion();
+ commitAction("Select All");
set(canvasMenu, null);
});
- delegated("click", button_51, () => {
+ delegated("click", button_59, () => {
set(canvasMenu, null);
selectSubject();
});
- append($$anchor3, div_75);
+ append($$anchor3, div_80);
};
- if_block(node_66, ($$render) => {
- if (get(canvasMenu)) $$render(consequent_54);
+ if_block(node_72, ($$render) => {
+ if (get(canvasMenu)) $$render(consequent_60);
});
}
- var node_69 = sibling(div_6, 2);
+ var node_75 = sibling(div_6, 2);
{
- var consequent_55 = ($$anchor3) => {
- var div_76 = root_74$1();
- var text_28 = child(div_76);
- template_effect(() => set_text(text_28, `⚠ ${get(errorMsg) ?? ""}`));
- append($$anchor3, div_76);
+ var consequent_61 = ($$anchor3) => {
+ var div_81 = root_82$1();
+ var text_33 = child(div_81);
+ template_effect(() => set_text(text_33, `⚠ ${get(errorMsg) ?? ""}`));
+ append($$anchor3, div_81);
};
- if_block(node_69, ($$render) => {
- if (get(errorMsg)) $$render(consequent_55);
+ if_block(node_75, ($$render) => {
+ if (get(errorMsg)) $$render(consequent_61);
});
}
- var node_70 = sibling(node_69, 2);
+ var node_76 = sibling(node_75, 2);
{
- var consequent_56 = ($$anchor3) => {
- var div_77 = root_75$1();
- var text_29 = child(div_77);
- template_effect(() => set_text(text_29, `⏳ ${get(selSetupMsg) ?? ""}`));
- append($$anchor3, div_77);
+ var consequent_62 = ($$anchor3) => {
+ var div_82 = root_83$1();
+ var text_34 = child(div_82);
+ template_effect(() => set_text(text_34, `⏳ ${get(selSetupMsg) ?? ""}`));
+ append($$anchor3, div_82);
};
- var consequent_57 = ($$anchor3) => {
- var div_78 = root_76$1();
- append($$anchor3, div_78);
+ var consequent_63 = ($$anchor3) => {
+ var div_83 = root_84$1();
+ append($$anchor3, div_83);
};
- if_block(node_70, ($$render) => {
- if (get(selSetupMsg)) $$render(consequent_56);
- else if (get(subjectBusy)) $$render(consequent_57, 1);
+ if_block(node_76, ($$render) => {
+ if (get(selSetupMsg)) $$render(consequent_62);
+ else if (get(subjectBusy)) $$render(consequent_63, 1);
});
}
- var node_71 = sibling(node_70, 2);
- AdjustModal(node_71, {
+ var node_77 = sibling(node_76, 2);
+ AdjustModal(node_77, {
get open() {
return get(adjustOpen);
},
@@ -12232,54 +13071,54 @@ function EditModal($$anchor, $$props) {
set(adjustSrcCanvas, null);
}
});
- var node_72 = sibling(node_71, 2);
+ var node_78 = sibling(node_77, 2);
{
- var consequent_58 = ($$anchor3) => {
- var div_79 = root_77$1();
- var div_80 = child(div_79);
- var div_81 = child(div_80);
- var text_30 = child(div_81);
- var div_82 = sibling(div_81, 2);
- var button_54 = child(div_82);
- template_effect(() => set_text(text_30, `Could not use the ${get(tool) === "stamp" ? "clone stamp" : "healing brush"} because the area to sample from has not been defined. `));
- delegated("click", div_79, () => {
- set(healDialog, false);
- });
- delegated("click", button_54, () => {
- set(healDialog, false);
- });
- append($$anchor3, div_79);
- };
- if_block(node_72, ($$render) => {
- if (get(healDialog)) $$render(consequent_58);
- });
- }
- var node_73 = sibling(node_72, 2);
- {
- var consequent_59 = ($$anchor3) => {
- var div_83 = root_78$1();
- var div_84 = child(div_83);
+ var consequent_64 = ($$anchor3) => {
+ var div_84 = root_85$1();
var div_85 = child(div_84);
- var text_31 = child(div_85);
- var div_86 = sibling(div_85, 2);
- var button_55 = child(div_86);
- template_effect(() => set_text(text_31, get(hiddenEditDialog)));
- delegated("click", div_83, () => {
- set(hiddenEditDialog, null);
+ var div_86 = child(div_85);
+ var text_35 = child(div_86);
+ var div_87 = sibling(div_86, 2);
+ var button_62 = child(div_87);
+ template_effect(() => set_text(text_35, `Could not use the ${get(tool) === "stamp" ? "clone stamp" : "healing brush"} because the area to sample from has not been defined. `));
+ delegated("click", div_84, () => {
+ set(healDialog, false);
});
- delegated("click", button_55, () => {
- set(hiddenEditDialog, null);
+ delegated("click", button_62, () => {
+ set(healDialog, false);
});
- append($$anchor3, div_83);
+ append($$anchor3, div_84);
};
- if_block(node_73, ($$render) => {
- if (get(hiddenEditDialog)) $$render(consequent_59);
+ if_block(node_78, ($$render) => {
+ if (get(healDialog)) $$render(consequent_64);
});
}
- var div_87 = sibling(node_73, 2);
- var div_88 = child(div_87);
- var node_74 = sibling(child(div_88), 2);
- SavePathInput(node_74, {
+ var node_79 = sibling(node_78, 2);
+ {
+ var consequent_65 = ($$anchor3) => {
+ var div_88 = root_86$1();
+ var div_89 = child(div_88);
+ var div_90 = child(div_89);
+ var text_36 = child(div_90);
+ var div_91 = sibling(div_90, 2);
+ var button_63 = child(div_91);
+ template_effect(() => set_text(text_36, get(hiddenEditDialog)));
+ delegated("click", div_88, () => {
+ set(hiddenEditDialog, null);
+ });
+ delegated("click", button_63, () => {
+ set(hiddenEditDialog, null);
+ });
+ append($$anchor3, div_88);
+ };
+ if_block(node_79, ($$render) => {
+ if (get(hiddenEditDialog)) $$render(consequent_65);
+ });
+ }
+ var div_92 = sibling(node_79, 2);
+ var div_93 = child(div_92);
+ var node_80 = sibling(child(div_93), 2);
+ SavePathInput(node_80, {
get value() {
return get(savePrefix);
},
@@ -12290,21 +13129,21 @@ function EditModal($$anchor, $$props) {
return fetchApi();
}
});
- var node_75 = sibling(div_88, 2);
+ var node_81 = sibling(div_93, 2);
{
- var consequent_60 = ($$anchor3) => {
- var span_22 = root_79$1();
- append($$anchor3, span_22);
+ var consequent_66 = ($$anchor3) => {
+ var span_24 = root_87$1();
+ append($$anchor3, span_24);
};
- if_block(node_75, ($$render) => {
- if (get(editsSaved)) $$render(consequent_60);
+ if_block(node_81, ($$render) => {
+ if (get(editsSaved)) $$render(consequent_66);
});
}
- var button_56 = sibling(node_75, 2);
- var button_57 = sibling(button_56, 2);
- var text_32 = child(button_57);
- var button_58 = sibling(button_57, 2);
- var text_33 = child(button_58);
+ var button_64 = sibling(node_81, 2);
+ var button_65 = sibling(button_64, 2);
+ var text_37 = child(button_65);
+ var button_66 = sibling(button_65, 2);
+ var text_38 = child(button_66);
action(div, ($$node) => {
var _a;
return (_a = portal) == null ? void 0 : _a($$node);
@@ -12321,7 +13160,7 @@ function EditModal($$anchor, $$props) {
classes_5 = set_class(button_20, 1, "pcr-ed-strip-color svelte-1ozzqkm", null, classes_5, { open: get(colorPopout) });
set_style(button_20, `background: ${get(hexColor) ?? ""};`);
set_style(button_21, `background: ${$0 ?? ""};`);
- classes_6 = set_class(div_38, 1, "pcr-ed-viewport svelte-1ozzqkm", null, classes_6, {
+ classes_6 = set_class(div_39, 1, "pcr-ed-viewport svelte-1ozzqkm", null, classes_6, {
eyedrop: get(tool) === "eyedrop" || get(colorPopout),
"select-mode": get(tool) === "select" || get(tool) === "lasso" || get(tool) === "objsel" || get(tool) === "smooth" || get(tool) === "crop" || get(tool) === "bucket",
"move-mode": get(tool) === "move" || get(mods).ctrl && !get(mods).shift && !get(mods).alt,
@@ -12329,43 +13168,47 @@ function EditModal($$anchor, $$props) {
"no-edit": get(hiddenEditBlocked),
"drag-over": get(dragActive)
});
- styles = set_style(div_38, "", styles, {
- cursor: get(transforming) && get(overRotateZone) ? ROTATE_CURSOR : null
+ styles = set_style(div_39, "", styles, {
+ cursor: get(spaceDown) ? "grab" : get(transforming) && get(overRotateZone) ? ROTATE_CURSOR : null
});
- set_style(div_39, `transform: translate(${get(panX) ?? ""}px, ${get(panY) ?? ""}px) scale(${get(zoom) ?? ""}); width:${get(width) ?? ""}px; height:${get(height) ?? ""}px;`);
+ set_style(div_41, `transform: translate(${get(panX) ?? ""}px, ${get(panY) ?? ""}px) scale(${get(zoom) ?? ""}); width:${get(width) ?? ""}px; height:${get(height) ?? ""}px;`);
set_attribute(img_1, "src", get(sourceUrl));
set_attribute(img_1, "width", get(width));
set_attribute(img_1, "height", get(height));
set_attribute(canvas, "width", get(width));
set_attribute(canvas, "height", get(height));
+ styles_1 = set_style(canvas, "", styles_1, { display: get(hasCustomBlend) ? "none" : null });
set_attribute(canvas_1, "width", get(width));
set_attribute(canvas_1, "height", get(height));
- styles_1 = set_style(canvas_1, "", styles_1, $1);
+ styles_2 = set_style(canvas_1, "", styles_2, $1);
+ set_attribute(canvas_3, "width", get(width));
+ set_attribute(canvas_3, "height", get(height));
+ styles_4 = set_style(canvas_3, "", styles_4, { display: get(hasCustomBlend) ? null : "none" });
set_text(text_17, `${$2 ?? ""}%`);
- set_style(div_63, `width: ${get(navTw) ?? ""}px; height: ${get(navTh) ?? ""}px;`);
+ set_style(div_65, `width: ${get(navTw) ?? ""}px; height: ${get(navTh) ?? ""}px;`);
set_attribute(img_2, "src", get(sourceUrl));
set_style(img_2, `width: ${get(navTw) ?? ""}px; height: ${get(navTh) ?? ""}px;`);
set_value(input, $3);
- classes_8 = set_class(button_31, 1, "pcr-ed-lh-tab svelte-1ozzqkm", null, classes_8, { active: get(lhTab) === "layers" });
- classes_9 = set_class(button_32, 1, "pcr-ed-lh-tab svelte-1ozzqkm", null, classes_9, { active: get(lhTab) === "history" });
- button_57.disabled = !editDocHash() || !get(histIndex) || get(savingEdits) || get(saving);
- set_attribute(button_57, "title", editDocHash() ? "Save your layers back onto this image — no new file, keeps the thumbnail" : "This image can't store edits in place — use Save new image");
- set_text(text_32, get(savingEdits) ? "Saving…" : "Save edits");
- button_58.disabled = !get(histIndex) || get(saving) || get(savingEdits);
- set_attribute(button_58, "title", get(histIndex) ? "Export the result as a new image (keeps the original)" : "Paint something first");
- set_text(text_33, get(saving) ? "Saving…" : "Save new image");
+ classes_8 = set_class(button_32, 1, "pcr-ed-lh-tab svelte-1ozzqkm", null, classes_8, { active: get(lhTab) === "layers" });
+ classes_9 = set_class(button_33, 1, "pcr-ed-lh-tab svelte-1ozzqkm", null, classes_9, { active: get(lhTab) === "history" });
+ button_65.disabled = !editDocHash() || !get(histIndex) || get(savingEdits) || get(saving);
+ set_attribute(button_65, "title", editDocHash() ? "Save your layers back onto this image — no new file, keeps the thumbnail" : "This image can't store edits in place — use Save new image");
+ set_text(text_37, get(savingEdits) ? "Saving…" : "Save edits");
+ button_66.disabled = !get(histIndex) || get(saving) || get(savingEdits);
+ set_attribute(button_66, "title", get(histIndex) ? "Export the result as a new image (keeps the original)" : "Paint something first");
+ set_text(text_38, get(saving) ? "Saving…" : "Save new image");
},
[
() => rgbCss(get(bgRgb)),
() => {
- var _a, _b, _c, _d, _e;
+ var _a, _b, _c;
return {
- display: ((_a = activeLayer()) == null ? void 0 : _a.visible) ? null : "none",
- opacity: ((_b = activeLayer()) == null ? void 0 : _b.opacity) ?? 1,
- "mix-blend-mode": layerBlendCss(activeLayer()),
- "mask-image": ((_c = activeLayer()) == null ? void 0 : _c.maskUrl) ? `url(${activeLayer().maskUrl})` : null,
+ display: get(hasCustomBlend) || !layerShown(activeLayer()) ? "none" : null,
+ opacity: layerEffOpacity(activeLayer()),
+ "mix-blend-mode": get(maskEdit) ? null : layerBlendCss(activeLayer()),
+ "mask-image": !get(maskEdit) && ((_a = activeLayer()) == null ? void 0 : _a.maskUrl) ? `url(${activeLayer().maskUrl})` : null,
"mask-repeat": "no-repeat",
- "mask-position": `${((_d = activeLayer()) == null ? void 0 : _d.ox) || 0}px ${((_e = activeLayer()) == null ? void 0 : _e.oy) || 0}px`
+ "mask-position": `${((_b = activeLayer()) == null ? void 0 : _b.ox) || 0}px ${((_c = activeLayer()) == null ? void 0 : _c.oy) || 0}px`
};
},
() => Math.round(get(zoom) * 100),
@@ -12383,47 +13226,47 @@ function EditModal($$anchor, $$props) {
delegated("click", button_21, () => {
set(bgRgb, [...get(rgb)], true);
});
- event("dragover", div_38, onCanvasDragOver);
- event("dragleave", div_38, () => {
+ event("dragover", div_39, onCanvasDragOver);
+ event("dragleave", div_39, () => {
set(dragActive, false);
});
- event("drop", div_38, onCanvasDrop);
- delegated("pointerdown", div_38, onPointerDown);
- delegated("pointermove", div_38, onPointerMove);
- delegated("pointerup", div_38, onPointerUp);
- event("pointercancel", div_38, onPointerUp);
- event("pointerleave", div_38, () => {
+ event("drop", div_39, onCanvasDrop);
+ delegated("pointerdown", div_39, onPointerDown);
+ delegated("pointermove", div_39, onPointerMove);
+ delegated("pointerup", div_39, onPointerUp);
+ event("pointercancel", div_39, onPointerUp);
+ event("pointerleave", div_39, () => {
set(cursorPos, null);
});
- delegated("dblclick", div_38, () => {
+ delegated("dblclick", div_39, () => {
if (get(tool) === "crop" && get(cropBox)) commitCrop();
});
- event("wheel", div_38, onWheel);
- delegated("contextmenu", div_38, (e) => e.preventDefault());
- event("dragstart", div_38, (e) => e.preventDefault());
- event("selectstart", div_38, (e) => e.preventDefault());
+ event("wheel", div_39, onWheel);
+ delegated("contextmenu", div_39, (e) => e.preventDefault());
+ event("dragstart", div_39, (e) => e.preventDefault());
+ event("selectstart", div_39, (e) => e.preventDefault());
event("load", img_1, () => {
var _a, _b;
update(imgLoadTick);
if (((_a = activeLayer()) == null ? void 0 : _a.isBackground) && !((_b = activeLayer()) == null ? void 0 : _b.bitmap)) loadActiveBitmap();
});
- delegated("pointerdown", div_63, navDrag);
- delegated("click", button_25, () => zoomStep(-1));
+ delegated("pointerdown", div_65, navDrag);
+ delegated("click", button_26, () => zoomStep(-1));
delegated("input", input, (e) => setZoomCentered(Math.exp(parseFloat(e.target.value))));
- delegated("click", button_26, () => zoomStep(1));
- delegated("click", button_31, () => {
+ delegated("click", button_27, () => zoomStep(1));
+ delegated("click", button_32, () => {
set(lhTab, "layers");
});
- delegated("click", button_32, () => {
+ delegated("click", button_33, () => {
set(lhTab, "history");
});
- delegated("click", button_56, close);
- delegated("click", button_57, saveEdits);
- delegated("click", button_58, save);
+ delegated("click", button_64, close);
+ delegated("click", button_65, saveEdits);
+ delegated("click", button_66, save);
append($$anchor2, div);
};
if_block(node, ($$render) => {
- if ($$props.open) $$render(consequent_61);
+ if ($$props.open) $$render(consequent_67);
});
}
append($$anchor, fragment);
diff --git a/js/lib/svelte/select-Dp4ExMMc.js b/js/lib/svelte/select-Bp4FHWwK.js
similarity index 95%
rename from js/lib/svelte/select-Dp4ExMMc.js
rename to js/lib/svelte/select-Bp4FHWwK.js
index 9b587f8..e5a0a40 100644
--- a/js/lib/svelte/select-Dp4ExMMc.js
+++ b/js/lib/svelte/select-Bp4FHWwK.js
@@ -1,4 +1,4 @@
-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";
+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-et9wt-4m.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-Dp4ExMMc.js.map
+//# sourceMappingURL=select-Bp4FHWwK.js.map
diff --git a/js/lib/svelte/style-Boi27oOu.js b/js/lib/svelte/style-B3hsaAru.js
similarity index 90%
rename from js/lib/svelte/style-Boi27oOu.js
rename to js/lib/svelte/style-B3hsaAru.js
index 7c0e249..3a24d23 100644
--- a/js/lib/svelte/style-Boi27oOu.js
+++ b/js/lib/svelte/style-B3hsaAru.js
@@ -1,4 +1,4 @@
-import { ac as to_style } from "./disclose-version-uq4tn5Y6.js";
+import { ac as to_style } from "./disclose-version-et9wt-4m.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-Boi27oOu.js.map
+//# sourceMappingURL=style-B3hsaAru.js.map
diff --git a/js/lib/upscale-from-image.js b/js/lib/upscale-from-image.js
index a26b37a..055b89c 100644
--- a/js/lib/upscale-from-image.js
+++ b/js/lib/upscale-from-image.js
@@ -813,32 +813,57 @@ function ensureDepthBranch(graph, LG, usNode, depthSource, loadNode) {
const poser = graph._nodes.find((n) => n.comfyClass === "PromptChain_PoseStudio");
const sourceNode = depthSource === "pose" && poser ? poser : loadNode;
const sourceSlot = sourceNode === poser ? (poser.outputs?.findIndex((o) => o.name === "IMAGE") ?? 0) : 0;
+ // The Poser feeds its IMAGE straight into Apply ControlNet when it ALREADY renders
+ // true depth — its uploaded control map is the depth map itself, so DepthAnything
+ // is skipped. A legacy clay-mode Poser keeps the estimator: nothing re-captures a
+ // depth render at queue time, so pointing a clay render at a depth net would be wrong.
+ const poseDirect = sourceNode === poser &&
+ (poser.properties?.pcrOutputMode === "depth" || poser.properties?.pcrTrueDepth === true);
const existing = findDepthApply(graph, usNode);
if (existing) {
- if (existing.pre) {
+ if (poseDirect) {
+ // Hint the apply straight from the Poser; retire any DepthAnything in between.
+ sourceNode.connect(sourceSlot, existing.apply, inputIdx(existing.apply, "image", 3));
+ if (existing.pre) existing.pre.mode = 4;
+ } else if (existing.pre) {
const cur = traceInput(graph, existing.pre, "image");
if (cur?.node !== sourceNode) {
sourceNode.connect(sourceSlot, existing.pre, inputIdx(existing.pre, "image", 0));
}
+ } else {
+ // The apply was built pose-direct (no preprocessor) but the new source isn't
+ // a true-depth render — a clay-mode Poser or an image. Its raw pixels aren't a
+ // depth map, so splice a DepthAnything estimator back in between source and apply.
+ const pre = LG.createNode(resolvePre(CONTROLNET_TYPES.depth.pre));
+ if (pre) {
+ pre.pos = [existing.apply.pos[0] - 340, existing.apply.pos[1]];
+ graph.add(pre);
+ pre.size = [291, pre.size?.[1] ?? 130];
+ const resW = pre.widgets?.find((w) => w.name === "resolution");
+ if (resW) { resW.value = 1024; resW.callback?.(1024); }
+ sourceNode.connect(sourceSlot, pre, inputIdx(pre, "image", 0));
+ pre.connect(0, existing.apply, inputIdx(existing.apply, "image", 3));
+ }
}
return true;
}
const cfg = CONTROLNET_TYPES.depth;
- const pre = LG.createNode(resolvePre(cfg.pre));
+ const pre = poseDirect ? null : LG.createNode(resolvePre(cfg.pre));
const loader = LG.createNode("ControlNetLoader");
const setUnion = LG.createNode("SetUnionControlNetType");
const applyNode = LG.createNode("ControlNetApplyAdvanced");
- if (!pre || !loader || !setUnion || !applyNode) return false;
+ if ((!poseDirect && !pre) || !loader || !setUnion || !applyNode) return false;
const ax = usNode.pos[0];
const ay = usNode.pos[1];
- pre.pos = [ax - 700, ay + 420];
+ if (pre) pre.pos = [ax - 700, ay + 420];
loader.pos = [ax - 700, ay + 640];
setUnion.pos = [ax - 700, ay + 780];
applyNode.pos = [ax - 360, ay + 420];
- graph.add(pre); graph.add(loader); graph.add(setUnion); graph.add(applyNode);
+ if (pre) graph.add(pre);
+ graph.add(loader); graph.add(setUnion); graph.add(applyNode);
applyNode.title = `ControlNet — Depth${sourceNode === poser ? " (pose)" : ""}`;
applyNode.properties = { ...(applyNode.properties || {}), pcrControlType: "depth" };
@@ -850,15 +875,20 @@ function ensureDepthBranch(graph, LG, usNode, depthSource, loadNode) {
else if (w.name === "start_percent") { w.value = 0.0; w.callback?.(0.0); }
else if (w.name === "end_percent") { w.value = cfg.end; w.callback?.(cfg.end); }
}
- const resW = pre.widgets?.find((w) => w.name === "resolution");
+ const resW = pre?.widgets?.find((w) => w.name === "resolution");
if (resW) { resW.value = 1024; resW.callback?.(1024); }
const posSrc = traceInput(graph, usNode, "positive");
const negSrc = traceInput(graph, usNode, "negative");
if (!posSrc?.node || !negSrc?.node) return false;
- sourceNode.connect(sourceSlot, pre, inputIdx(pre, "image", 0));
- pre.connect(0, applyNode, inputIdx(applyNode, "image", 3));
+ if (pre) {
+ sourceNode.connect(sourceSlot, pre, inputIdx(pre, "image", 0));
+ pre.connect(0, applyNode, inputIdx(applyNode, "image", 3));
+ } else {
+ // Pose-direct: the Poser's true-depth IMAGE is the control map itself.
+ sourceNode.connect(sourceSlot, applyNode, inputIdx(applyNode, "image", 3));
+ }
loader.connect(0, setUnion, 0);
setUnion.connect(0, applyNode, inputIdx(applyNode, "control_net", 2));
posSrc.node.connect(posSrc.slot, applyNode, inputIdx(applyNode, "positive", 0));
diff --git a/js/lib/viewer-bridge.js b/js/lib/viewer-bridge.js
index 2eea896..8b4c0ea 100644
--- a/js/lib/viewer-bridge.js
+++ b/js/lib/viewer-bridge.js
@@ -306,7 +306,15 @@ export async function openViewer(images, startIndex = 0, workflowId = "", onDele
onInpaintInstallPack: (injectable) => installInpaintPack(injectable),
onMountPromptEditor: mountPromptEditor,
onEditPrepare: (entry) => prepareEdit(entry),
- onEditSave: (prepared, blob, prefix) => saveEditedImage(prepared, blob, prefix),
+ onEditSave: async (prepared, blob, prefix) => {
+ const entry = await saveEditedImage(prepared, blob, prefix);
+ // Opened from a node (e.g. LoadImage) that wants the result wired back.
+ if (entry && typeof opts.onAfterEditSave === "function") {
+ try { await opts.onAfterEditSave(entry); }
+ catch (e) { console.error("[PromptChain] onAfterEditSave failed", e); }
+ }
+ return entry;
+ },
onInpaintRegion: (data, args) => inpaintRegion(data, args),
onReposeCaps: () => fetchReposeCaps(),
onReposeRun: (opts) => runReposeInBackground(opts),
diff --git a/js/loadimage-editor.js b/js/loadimage-editor.js
new file mode 100644
index 0000000..a3f7e8c
--- /dev/null
+++ b/js/loadimage-editor.js
@@ -0,0 +1,123 @@
+// Right-click a LoadImage node -> "Open in PromptChain editor": opens the
+// node's currently-loaded input image in the PromptChain editor. A saved edit
+// becomes a lineage child (parent = the loaded image) and re-points THIS node
+// to the edited result — a NEW input file, not an overwrite of the source.
+//
+// A SEPARATE extension (own registerExtension) so a JS error here can never
+// take down the prompt editor / poser / region box. The menu item rides
+// getExtraMenuOptions, which fires in both the legacy and Vue node renderers —
+// the only trigger that works uniformly (a bare click on the on-node image is
+// not interceptable in Vue mode), mirroring core's "Open in MaskEditor".
+
+import { app } from "../../scripts/app.js";
+import { api } from "../../scripts/api.js";
+import { openViewer } from "./lib/viewer-bridge.js";
+
+const TARGET_NODES = new Set(["LoadImage"]);
+
+// The loaded file's identity. node.imgs[i].src is the authoritative
+// /view?filename=&subfolder=&type= URL; the 'image' widget value is the
+// fallback (it can carry a trailing " [input]" annotation, which we strip).
+function readLoadedImage(node) {
+ const src = node.imgs?.[node.imageIndex ?? 0]?.src;
+ if (src) {
+ try {
+ const u = new URL(src, window.location.origin);
+ const filename = u.searchParams.get("filename") || "";
+ if (filename) {
+ return {
+ filename,
+ subfolder: u.searchParams.get("subfolder") || "",
+ type: u.searchParams.get("type") || "input",
+ };
+ }
+ } catch {}
+ }
+ const widget = node.widgets?.find((w) => w.name === "image");
+ const raw = typeof widget?.value === "string" ? widget.value : "";
+ const val = raw.replace(/\s*\[(input|output|temp)\]\s*$/, "");
+ if (!val) return null;
+ const parts = val.split("/");
+ const filename = parts.pop();
+ return { filename, subfolder: parts.join("/"), type: "input" };
+}
+
+function findImageWidget(node) {
+ return node.widgets?.find(
+ (w) => w.options?.image_upload === true || (w.type === "combo" && w.name === "image")
+ ) || null;
+}
+
+// The edit is saved to the OUTPUT tree, but LoadImage reads the INPUT root.
+// image-workflow?hash= stages a content-addressed promptchain_source_
+// copy into input/ and hands back input_ref — the same primitive prepareEdit
+// uses for the source — which is then a valid LoadImage combo value.
+async function attachEditedToNode(node, entry) {
+ if (!entry?.hash) return;
+ let inputRef = "";
+ try {
+ const res = await api.fetchApi(`/promptchain/image-workflow?hash=${encodeURIComponent(entry.hash)}`);
+ if (res.ok) inputRef = (await res.json())?.input_ref || "";
+ } catch (e) {
+ console.error("[PromptChain] LoadImage attach-back staging failed", e);
+ }
+ if (!inputRef) return;
+
+ const widget = findImageWidget(node);
+ if (!widget) return;
+ // The full re-point: add the option, set the value, fire the combo callback
+ // (Comfy's installed refresh: node.imgs=undefined -> reload -> setDirtyCanvas).
+ // Setting .value alone does NOT reload the preview.
+ if (widget.options?.values && !widget.options.values.includes(inputRef)) {
+ widget.options.values.push(inputRef);
+ }
+ widget.value = inputRef;
+ widget.callback?.(inputRef);
+ node.setDirtyCanvas(true, true);
+}
+
+async function openLoadImageInEditor(node) {
+ const img = readLoadedImage(node);
+ if (!img?.filename) {
+ app.extensionManager?.toast?.add?.({
+ severity: "warn",
+ summary: "PromptChain",
+ detail: "This node has no loaded image to edit.",
+ life: 5000,
+ });
+ return;
+ }
+ // A browse-style entry (no DB hash) — identical shape to the sidebar's, so
+ // prepareEdit resolves the input file as the lineage parent.
+ const scope = img.type === "output" ? "output" : "input";
+ const path = img.subfolder ? `${img.subfolder}/${img.filename}` : img.filename;
+ const entry = {
+ hash: path,
+ filename: img.filename,
+ subfolder: "",
+ _directUrl: api.apiURL(`/promptchain/browse/preview?scope=${scope}&path=${encodeURIComponent(path)}`),
+ _browseScope: scope,
+ _browsePath: path,
+ };
+ openViewer([entry], 0, "", null, {
+ autoEdit: true,
+ onAfterEditSave: (saved) => attachEditedToNode(node, saved),
+ });
+}
+
+app.registerExtension({
+ name: "PromptChain.LoadImageEditor",
+ async beforeRegisterNodeDef(nodeType, nodeData) {
+ if (!TARGET_NODES.has(nodeData?.name)) return;
+ const original = nodeType.prototype.getExtraMenuOptions;
+ nodeType.prototype.getExtraMenuOptions = function (canvas, options) {
+ const result = original?.apply(this, arguments);
+ const node = this;
+ options.push({
+ content: "Open in PromptChain editor",
+ callback: () => openLoadImageInEditor(node),
+ });
+ return result;
+ };
+ },
+});
diff --git a/js/main.js b/js/main.js
index 1ff2bce..8dd96f2 100644
--- a/js/main.js
+++ b/js/main.js
@@ -367,20 +367,6 @@ function setupImagePreviewListeners() {
// here describe the user's active graph, not that prompt's.
if (detail.prompt_id && externallyTrackedPrompts.has(detail.prompt_id)) return;
const nodeId = Number(detail.node);
- let pcNodes = _outputToPC.get(nodeId);
- let wfId = _executionWorkflowId;
- if (!pcNodes?.length) {
- // The SaveImage didn't map to a PromptChain node via the execution_start
- // graph scan (trace miss / subgraph / workflow-id drift). Don't silently
- // drop the generation — diagnose, then attribute it to the active graph's
- // PromptChain node so the generation panel still catches it.
- if (!wfId) wfId = getWorkflowId();
- const fallbackPC = (app.graph?._nodes || []).find((n) => n.comfyClass === NODE_TYPE);
- console.warn(`[PromptChain][record] unmapped output node=${detail.node}->${nodeId} mapKeys=[${[..._outputToPC.keys()]}] wfId=${wfId} graphNodes=${app.graph?._nodes?.length} rootGraph=${!!app.rootGraph} fallbackPC=${fallbackPC?.id ?? "none"}`);
- if (!fallbackPC || !wfId) return;
- pcNodes = [fallbackPC];
- }
-
const img = detail.output.images[detail.output.images.length - 1];
const params = new URLSearchParams({
filename: img.filename,
@@ -389,6 +375,39 @@ function setupImagePreviewListeners() {
});
const imageUrl = api.apiURL(`/view?${params}&rand=${Math.random()}`);
+ // A PreviewImage emits type "temp" — a transient guide map (e.g. the 3D
+ // Poser depth control map), not a saved render. It still flashes in the
+ // node panel to guide the user, but must never be recorded: the Generated
+ // gallery holds saved outputs (SaveImage → "output") only.
+ const isSavedOutput = (img.type || "output") === "output";
+
+ let pcNodes = _outputToPC.get(nodeId);
+ let wfId = _executionWorkflowId;
+ if (!pcNodes?.length) {
+ // The SaveImage didn't map to a PromptChain node via the execution_start
+ // graph scan (trace miss / subgraph / workflow-id drift). Don't silently
+ // drop the generation — diagnose, then attribute it to the active graph's
+ // PromptChain node so the generation panel still catches it. A side-branch
+ // preview (the pose depth map) is EXPECTED to be unmapped — don't warn.
+ if (!wfId) wfId = getWorkflowId();
+ const fallbackPC = (app.graph?._nodes || []).find((n) => n.comfyClass === NODE_TYPE);
+ if (isSavedOutput) {
+ console.warn(`[PromptChain][record] unmapped output node=${detail.node}->${nodeId} mapKeys=[${[..._outputToPC.keys()]}] wfId=${wfId} graphNodes=${app.graph?._nodes?.length} rootGraph=${!!app.rootGraph} fallbackPC=${fallbackPC?.id ?? "none"}`);
+ }
+ if (!fallbackPC || (isSavedOutput && !wfId)) return;
+ pcNodes = [fallbackPC];
+ }
+
+ // Transient preview (PreviewImage / non-"output"): flash in the panel to
+ // guide the user, then stop — no metadata, no gallery record.
+ if (!isSavedOutput) {
+ for (const pc of pcNodes) {
+ pc._pcrImagePanel?.updateImage(imageUrl, null);
+ pc._pcrImagePanel?.hideProgress();
+ }
+ return;
+ }
+
// metadata from graph-determined nearest KSampler
const entry = _saveNodeGraph.get(nodeId);
let meta = entry?.ksampleId != null ? { ..._executionMeta.get(entry.ksampleId) } : {};
diff --git a/js/pose-studio.js b/js/pose-studio.js
index 125b3e0..94fb5a4 100644
--- a/js/pose-studio.js
+++ b/js/pose-studio.js
@@ -113,9 +113,13 @@ function installPoseStyles() {
// Stop pointer/key events from bubbling to LiteGraph (which would drag the node
// or pop its context menu), but only in the BUBBLE phase — OrbitControls listens
// on the canvas in the target phase, which runs first, so it still gets to orbit.
+// `mousedown` is isolated too: when the viewport is docked inside the PromptChain
+// fullscreen overlay, that overlay preventDefaults stray mousedowns to keep focus
+// in the editor — which silently suppresses the popup of every native
+// here (foot/hand pose, body pose, shoe pickers), making them open then snap shut.
function isolateViewportEvents(container) {
const stop = (event) => event.stopPropagation();
- for (const type of ["pointerdown", "pointerup", "pointermove", "keydown", "keyup", "contextmenu"]) {
+ for (const type of ["pointerdown", "mousedown", "pointerup", "pointermove", "keydown", "keyup", "contextmenu"]) {
container.addEventListener(type, stop);
}
// LiteGraph captures the pointer on its canvas; release it so a drag here
diff --git a/pyproject.toml b/pyproject.toml
index 013ec33..675c868 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@
[project]
name = "comfyui-promptchain"
-version = "0.1.8"
+version = "0.1.9"
description = "Prompt-authoring and image-iteration suite for ComfyUI: structured prompts, 3D posing, model awareness, regional control, and layered editing."
license = { file = "LICENSE" }
requires-python = ">=3.10"
diff --git a/svelte-src/components/AdjustModal.svelte b/svelte-src/components/AdjustModal.svelte
index 3fdda0d..a67d490 100644
--- a/svelte-src/components/AdjustModal.svelte
+++ b/svelte-src/components/AdjustModal.svelte
@@ -18,7 +18,7 @@
let {
open,
- sourceCanvas = null, // flattened composite to adjust
+ sourceCanvas = null, // the active layer's pixels to adjust (caller scopes this)
filename = "",
onApply = null, // (ImageData) => void
onCancel,
@@ -269,6 +269,11 @@
if (cH && y > 0.75) add += cH * (1 - Math.cos(Math.PI * (y - 0.75) / 0.25)) / 2;
lut[i] = Math.max(0, Math.min(1, y + 0.25 * add));
}
+ // Enforce monotonicity: large/opposing parametric bumps can fold the curve back
+ // on itself (output decreasing as input rises), which inverts/solarizes tones.
+ // Clamp any fold to a flat plateau instead — graceful, and keeps full slider
+ // strength wherever the curve is still rising.
+ for (let i = 1; i < N; i++) if (lut[i] < lut[i - 1]) lut[i] = lut[i - 1];
return lut;
}
diff --git a/svelte-src/components/EditModal.svelte b/svelte-src/components/EditModal.svelte
index d321ee6..3608403 100644
--- a/svelte-src/components/EditModal.svelte
+++ b/svelte-src/components/EditModal.svelte
@@ -119,6 +119,8 @@
let airbrush = $state(true); // timed dabs while the button is held
let featherPx = $state(0); // Select: patch-commit edge feather
let sampleSize = $state(1); // Pick: 1 | 3 | 5 px average
+ let sampleMerged = $state(true); // Pick from the flattened composite (true) or the active layer only (false)
+ let maskEdit = $state(false); // editing the active layer's MASK (paintCanvas shows it as grayscale) vs its pixels
let fillTolerance = $state(32); // Paint bucket: color-match threshold
let fillContiguous = $state(true); // Paint bucket: flood from click vs all matching pixels
let hue = $state(0); // HSV foreground color
@@ -139,8 +141,12 @@
let activeIndex = $state(0);
let layerCanvasEls = $state([]); // per-inactive-layer refs (by index) — ABOVE active only
let belowCanvasEl; // everything BELOW active, pre-composited into one buffer
+ let customEl; // single-canvas composite used when a custom (non-CSS) blend mode is in play
let nextLayerNum = 0;
let selectedIds = $state([]); // multi-selected layer ids (for merge); always includes the active layer
+ let groups = $state([]); // lightweight layer groups: { id, name, collapsed, visible, opacity }
+ let nextGroupNum = 0;
+ let renamingGroupId = $state(null);
let lhTab = $state("layers"); // Layers/History share one tabbed panel (PS-style)
let layerMenu = $state(null); // { x, y } right-click context menu
let renamingId = $state(null); // layer id being renamed (dblclick)
@@ -161,7 +167,7 @@
// ── history (Photoshop-style): snapshot AFTER each action, click to jump ──
// history[0] = "Open" (blank paint layer); undo/redo just move histIndex.
// Painting while back in time truncates the forward entries (PS semantics).
- const HIST_MAX = 12;
+ const HIST_MAX = 30; // entries share unchanged layer bitmaps by reference, so a deeper stack is cheap
let history = $state([{ label: "Open", snap: null }]);
let histIndex = $state(0);
let blankSnap = null;
@@ -176,7 +182,7 @@
editsSaved = false; // a fresh change since the last "Save edits"
flushActive(); // sync paintCanvas → the active layer's FULL bitmap (offset-aware)
const head = history.slice(0, histIndex + 1);
- head.push({ label, layers: [...layers], activeIndex, selectedIds: [...selectedIds], url: sourceUrl, w: width, h: height });
+ head.push({ label, layers: [...layers], groups: [...groups], activeIndex, selectedIds: [...selectedIds], sel: selSnapshot(), url: sourceUrl, w: width, h: height });
while (head.length > HIST_MAX + 1) head.splice(1, 1); // evict oldest, keep "Open"
history = head;
histIndex = history.length - 1;
@@ -196,8 +202,11 @@
}
const restore = () => {
layers = [...h.layers];
+ // collapse is view-only: keep the current expand/collapse state across undo/redo
+ groups = h.groups ? h.groups.map((g) => { const cur = groups.find((x) => x.id === g.id); return cur ? { ...g, collapsed: cur.collapsed } : g; }) : [];
activeIndex = Math.min(h.activeIndex ?? 0, layers.length - 1);
selectedIds = h.selectedIds ? [...h.selectedIds] : [layers[activeIndex]?.id].filter((x) => x != null);
+ restoreSelection(h.sel); // bring back the pixel selection that existed at this step
loadActiveBitmap();
if (dimsChanged) resetView();
};
@@ -317,6 +326,31 @@
recomputeSelBBox();
selVersion++;
}
+ // ── selection in history ──
+ // Snapshot the selection mask into each history entry so undo/redo restores the
+ // selection that existed at that step (and a bare select/deselect is undoable).
+ // Cached by selVersion so consecutive edits sharing one selection reference the
+ // SAME ImageData (cheap, like the layer-bitmap sharing) instead of copying it.
+ let _selSnapCache = { version: -1, snap: null };
+ function selSnapshot() {
+ if (!selActive || !selMaskCanvas) return null;
+ if (_selSnapCache.version === selVersion && _selSnapCache.snap) return _selSnapCache.snap;
+ const snap = {
+ mask: selMaskCanvas.getContext("2d").getImageData(0, 0, width, height),
+ bbox: selBBox ? { ...selBBox } : null,
+ };
+ _selSnapCache = { version: selVersion, snap };
+ return snap;
+ }
+ function restoreSelection(sel) {
+ if (!sel || !sel.mask) { selBBox = null; selActive = false; selVersion++; if (selMaskCanvas) selMaskCanvas.getContext("2d").clearRect(0, 0, width, height); return; }
+ ensureSelMask();
+ const ctx = selMaskCanvas.getContext("2d");
+ ctx.clearRect(0, 0, width, height);
+ ctx.putImageData(sel.mask, 0, 0);
+ selBBox = sel.bbox ? { ...sel.bbox } : null;
+ selActive = true; selVersion++;
+ }
function selectAllRegion() {
const ctx = ensureSelMask().getContext("2d");
ctx.clearRect(0, 0, width, height);
@@ -343,7 +377,7 @@
// histIndex covers committed edits; the per-layer suffix also keys on
// visibility/opacity/blend/offset so the cache stays correct mid-drag,
// before an opacity release commits.
- return `${histIndex}|${activeIndex}|${layers.map((L) => `${L.id}:${L.visible}:${L.opacity}:${L.blend || ""}:${L.ox || 0}:${L.oy || 0}`).join(",")}`;
+ return `${histIndex}|${activeIndex}|${layers.map((L) => `${L.id}:${L.visible}:${L.opacity}:${L.blend || ""}:${L.ox || 0}:${L.oy || 0}:${L.groupId || ""}`).join(",")}|${groups.map((g) => `${g.id}:${g.visible}:${g.opacity}`).join(",")}`;
}
async function flattenedBlobCached() {
const key = flatKey();
@@ -539,13 +573,15 @@
}
function selectionBBox() { return selBBox; }
// Grab the selected pixels of the ACTIVE layer (clipped to the mask).
- function captureSelection() {
+ function captureSelection(merged = false) {
if (!selActive || !selBBox) return null;
const bb = selBBox;
const c = document.createElement("canvas");
c.width = bb.w; c.height = bb.h;
const cx = c.getContext("2d");
- cx.drawImage(paintCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
+ // merged = the flattened VISIBLE composite (Copy Merged); else the active layer alone.
+ if (merged) drawFlattened(cx, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
+ else cx.drawImage(paintCanvas, bb.x, bb.y, bb.w, bb.h, 0, 0, bb.w, bb.h);
const mask = document.createElement("canvas");
mask.width = bb.w; mask.height = bb.h;
const mctx = mask.getContext("2d");
@@ -609,7 +645,7 @@
lc.getContext("2d").drawImage(srcCanvas, x, y);
const id = ++nextLayerNum;
layers = [...layers.slice(0, activeIndex + 1),
- { id, name: `Layer ${id}`, visible: true, opacity: 1, sourceRect,
+ { id, name: `Layer ${id}`, visible: true, opacity: 1, sourceRect, groupId: layers[activeIndex]?.groupId,
bitmap: lc.getContext("2d").getImageData(0, 0, width, height) },
...layers.slice(activeIndex + 1)];
activeIndex = activeIndex + 1;
@@ -626,8 +662,8 @@
if (cut) eraseSelectionFromActive(cap); // active is still the source layer here
newLayerFromCanvas(cap.canvas, cap.x, cap.y, cut ? "Layer via Cut" : "Layer via Copy", capSourceRect(cap));
}
- function copySelection(cut) {
- const cap = captureSelection();
+ function copySelection(cut, merged = false) {
+ const cap = captureSelection(merged);
if (!cap) return;
clipboard = { canvas: cap.canvas, x: cap.x, y: cap.y, sourceRect: capSourceRect(cap) };
if (cut) { eraseSelectionFromActive(cap); commitAction("Cut"); }
@@ -743,6 +779,7 @@
// Modifier state (for the selection-tool cursor feedback).
let mods = $state({ shift: false, alt: false, ctrl: false });
+ let spaceDown = $state(false); // hold Space → temporary Hand tool (drag to pan)
let isSelTool = $derived(tool === "select" || tool === "lasso" || tool === "objsel");
// Hidden active layer + a paint/move tool → PS not-allowed cursor on hover.
// Alt is the eyedropper (still allowed), so it keeps the normal cursor.
@@ -758,8 +795,11 @@
let selBadge = $derived(!isSelTool ? "" : (mods.shift && mods.alt) ? "∩" : mods.shift ? "+" : mods.alt ? "−" : "");
$effect(() => {
if (!open) return;
- const upd = (e) => { mods = { shift: e.shiftKey, alt: e.altKey, ctrl: e.ctrlKey || e.metaKey }; };
- const clear = () => { mods = { shift: false, alt: false, ctrl: false }; };
+ const upd = (e) => {
+ mods = { shift: e.shiftKey, alt: e.altKey, ctrl: e.ctrlKey || e.metaKey };
+ if (e.code === "Space") spaceDown = e.type === "keydown";
+ };
+ const clear = () => { mods = { shift: false, alt: false, ctrl: false }; spaceDown = false; };
window.addEventListener("keydown", upd, true);
window.addEventListener("keyup", upd, true);
window.addEventListener("blur", clear);
@@ -1142,8 +1182,8 @@
const uctx = under.getContext("2d");
for (let j = 0; j < i; j++) {
const Lj = layers[j];
- if (!Lj.visible) continue;
- uctx.globalAlpha = Lj.opacity ?? 1;
+ if (!layerShown(Lj)) continue;
+ uctx.globalAlpha = layerEffOpacity(Lj);
if (Lj.isBackground && !Lj.bitmap && baseImg) {
// Canvas-grow epoch: the new Background image element hasn't loaded
// yet at match time — the caller hands us the decoded ESRGAN render.
@@ -1321,11 +1361,11 @@
// source. Same clip + placement so the pair lands as one unit.
featherClip(underCanvas);
const uid = ++nextLayerNum;
- inserted.push({ id: uid, name: `UltraSharp ${uid}`, visible: true, opacity: 1,
+ inserted.push({ id: uid, name: `UltraSharp ${uid}`, visible: true, opacity: 1, groupId: layers[activeIndex]?.groupId,
bitmap: underCanvas.getContext("2d").getImageData(0, 0, width, height) });
}
const id = ++nextLayerNum;
- inserted.push({ id, name: `${label} ${id}`, visible: true, opacity: 1,
+ inserted.push({ id, name: `${label} ${id}`, visible: true, opacity: 1, groupId: layers[activeIndex]?.groupId,
bitmap: c.getContext("2d").getImageData(0, 0, width, height) });
layers = [...layers.slice(0, activeIndex + 1), ...inserted, ...layers.slice(activeIndex + 1)];
activeIndex = activeIndex + inserted.length;
@@ -1573,6 +1613,9 @@
{ id: "eyedrop", label: "Pick",
icon: ` ` },
];
+ // PS single-key tool shortcuts (no modifier) → tool id.
+ const TOOL_KEYS = { v: "move", m: "select", l: "lasso", b: "brush", e: "eraser",
+ g: "bucket", w: "objsel", j: "spot", s: "stamp", c: "crop", i: "eyedrop" };
const ACTION_ICONS = {
undo: ` `,
redo: ` `,
@@ -1704,6 +1747,10 @@
ctx.clearRect(0, 0, width, height);
ctx.drawImage(patch.restore, 0, 0);
}
+ // a Transform on a masked layer baked the mask into the pixels — restore the live mask
+ if (patch?.maskRestore) {
+ layers[activeIndex] = { ...layers[activeIndex], mask: patch.maskRestore.mask, maskUrl: patch.maskRestore.maskUrl };
+ }
patch = null;
}
@@ -1713,6 +1760,7 @@
// restores it from patch.restore.
function beginTransform() {
if (patch) return true;
+ if (maskEdit) exitMaskEdit(); // transform operates on the layer, not its mask
if (blockHiddenEdit("transform")) return false;
if (!paintCanvas || !layers[activeIndex]) return false;
flushActive();
@@ -1743,12 +1791,28 @@
const dw = Math.min(width, Math.ceil(bb.x + ox + bb.w)) - dx0;
const dh = Math.min(height, Math.ceil(bb.y + oy + bb.h)) - dy0;
if (dw < 2 || dh < 2) { errorMsg = "This layer's content is off-canvas — move it back to transform."; return false; }
+ // A masked layer: bake the mask into the pixels so it transforms WITH the
+ // content — otherwise the un-transformed mask would clip the scaled/rotated
+ // result. cancelPatch restores both the unmasked pixels (from `restore`) and the
+ // live mask. L.mask is content-space; paintCanvas is DOC-space (loadActiveBitmap
+ // draws at +ox/+oy), so the mask must ride the layer's ox/oy offset to align.
+ let maskRestore = null;
+ if (L.mask) {
+ const mc = document.createElement("canvas"); mc.width = width; mc.height = height;
+ mc.getContext("2d").putImageData(L.mask, 0, 0);
+ pctx.globalCompositeOperation = "destination-in";
+ pctx.drawImage(mc, ox, oy);
+ pctx.globalCompositeOperation = "source-over";
+ maskRestore = { mask: L.mask, maskUrl: L.maskUrl };
+ layers[activeIndex] = { ...L, mask: undefined, maskUrl: undefined };
+ }
const c = document.createElement("canvas");
c.width = dw; c.height = dh;
c.getContext("2d").drawImage(paintCanvas, dx0, dy0, dw, dh, 0, 0, dw, dh);
pctx.clearRect(dx0, dy0, dw, dh); // Background lifts to transparency too (normal layer)
patch = { canvas: c, raw: c, pts: null, pw: dw, ph: dh,
x: dx0, y: dy0, w: dw, h: dh, rot: 0, flipH: false, flipV: false, transform: true, restore,
+ maskRestore,
home: { x: dx0, y: dy0, w: dw, h: dh } };
return true;
}
@@ -1874,7 +1938,7 @@
src.width = bw; src.height = bh;
const sctx = src.getContext("2d");
try {
- drawBackdrop(sctx, bx, by, bw, bh, 0, 0, bw, bh);
+ // Active layer only — smoothing must not pull the backdrop into the layer.
sctx.drawImage(paintCanvas, bx, by, bw, bh, 0, 0, bw, bh);
} catch (e) {
console.warn("[PromptChain] smooth lift failed", e);
@@ -1942,7 +2006,8 @@
c.width = width; c.height = height;
const ctx = c.getContext("2d", { willReadFrequently: true });
try {
- drawBackdrop(ctx, 0, 0, width, height, 0, 0, width, height);
+ // Active layer only — warp the layer's own pixels, not the backdrop, so the
+ // layer keeps its transparency (same scoping as the Camera Raw fix).
ctx.drawImage(paintCanvas, 0, 0);
} catch (e) {
console.warn("[PromptChain] liquify session failed", e);
@@ -2169,7 +2234,7 @@
const dx = liqD[gi * 2], dy = liqD[gi * 2 + 1];
if (dx === 0 && dy === 0) {
const j = gi * 4;
- o[idx] = s[j]; o[idx + 1] = s[j + 1]; o[idx + 2] = s[j + 2];
+ o[idx] = s[j]; o[idx + 1] = s[j + 1]; o[idx + 2] = s[j + 2]; o[idx + 3] = s[j + 3];
} else {
const fx = Math.min(maxX, Math.max(0, X + dx));
const fy = Math.min(maxY, Math.max(0, Y + dy));
@@ -2182,11 +2247,13 @@
o[idx] = s[i00] * w00 + s[i10] * w10 + s[i01] * w01 + s[i11] * w11;
o[idx + 1] = s[i00 + 1] * w00 + s[i10 + 1] * w10 + s[i01 + 1] * w01 + s[i11 + 1] * w11;
o[idx + 2] = s[i00 + 2] * w00 + s[i10 + 2] * w10 + s[i01 + 2] * w01 + s[i11 + 2] * w11;
+ // carry the layer's own alpha through the warp (no opaque bake)
+ o[idx + 3] = s[i00 + 3] * w00 + s[i10 + 3] * w10 + s[i01 + 3] * w01 + s[i11 + 3] * w11;
}
- o[idx + 3] = 255;
}
}
paintCanvas.getContext("2d").putImageData(img, x0, y0);
+ if (hasCustomBlend) customTick++; // refresh the single-canvas custom-blend composite mid-stroke
}
@@ -2218,6 +2285,7 @@
function setTool(id) {
commitPatch();
+ if (maskEdit && (id === "adjust" || id === "crop" || id === "liquify")) exitMaskEdit(); // these don't operate on a mask
if (id === "adjust") { openAdjust(); return; } // modal, not a canvas tool
if (tool === "liquify" && id !== "liquify") dropLiqSession();
tool = id;
@@ -2232,12 +2300,16 @@
let adjustSrcCanvas = $state(null);
function openAdjust() {
- if (blockHiddenEdit("adjust")) return; // Camera Raw writes back to the active layer
+ if (blockHiddenEdit("adjust")) return; // Camera Raw reads + writes the active layer only
const c = document.createElement("canvas");
c.width = width; c.height = height;
const ctx = c.getContext("2d", { willReadFrequently: true });
try {
- drawBackdrop(ctx, 0, 0, width, height, 0, 0, width, height);
+ // Scope to the SELECTED layer: feed the modal only the active layer's pixels
+ // (paintCanvas), NOT the backdrop below it. Compositing the backdrop in baked
+ // the under-layers into the result and, because that composite is opaque, the
+ // returned ImageData overwrote the layer's transparency on apply (the layer then
+ // covered everything beneath it). The active layer's own alpha now rides through.
ctx.drawImage(paintCanvas, 0, 0);
} catch (e) {
console.warn("[PromptChain] adjust open failed", e);
@@ -2248,7 +2320,26 @@
}
function onAdjustApply(imageData) {
- paintCanvas.getContext("2d").putImageData(imageData, 0, 0);
+ const ctx = paintCanvas.getContext("2d");
+ // Honor an active selection: blend the adjusted pixels with the original by the
+ // selection's (feather-aware) coverage. Camera Raw preserves the layer's alpha,
+ // so a plain RGBA lerp is correct and avoids canvas-composite alpha inflation;
+ // outside the selection (t=0) the pixel stays exactly the original.
+ if (selActive && selMaskCanvas) {
+ const orig = ctx.getImageData(0, 0, width, height).data;
+ const sel = selMaskCanvas.getContext("2d").getImageData(0, 0, width, height).data;
+ const a = imageData.data;
+ for (let i = 0; i < a.length; i += 4) {
+ const t = sel[i + 3] / 255;
+ if (t >= 1) continue;
+ const it = 1 - t;
+ a[i] = a[i] * t + orig[i] * it;
+ a[i + 1] = a[i + 1] * t + orig[i + 1] * it;
+ a[i + 2] = a[i + 2] * t + orig[i + 2] * it;
+ a[i + 3] = a[i + 3] * t + orig[i + 3] * it;
+ }
+ }
+ ctx.putImageData(imageData, 0, 0);
commitAction("Camera Raw");
adjustOpen = false;
adjustSrcCanvas = null;
@@ -2348,6 +2439,7 @@
const ch = Math.min(height - cy, Math.round(cropBox.h));
if (cw < 4 || ch < 4) return;
flushActive(); // sync the live canvas into the active layer before cropping
+ if (selActive) deselect(); // selection mask is in old-document coordinates — invalid after crop
const sc = document.createElement("canvas");
sc.width = cw; sc.height = ch;
try {
@@ -2360,22 +2452,30 @@
// Crop every layer's bitmap to the new dimensions (uses the OLD width/height
// for the source, so it must run before width/height are reassigned).
const ow = width, oh = height;
- const cropPlane = (data) => {
+ const cropPlane = (data, lox, loy) => {
if (!data) return undefined;
const t = document.createElement("canvas");
t.width = ow; t.height = oh;
t.getContext("2d").putImageData(data, 0, 0);
+ // Bake the layer's non-destructive move offset into doc space so the crop
+ // reads the region the user actually SEES (bitmap + mask share the offset).
+ const doc = document.createElement("canvas");
+ doc.width = ow; doc.height = oh;
+ doc.getContext("2d").drawImage(t, lox, loy);
const cc = document.createElement("canvas");
cc.width = cw; cc.height = ch;
- cc.getContext("2d").drawImage(t, cx, cy, cw, ch, 0, 0, cw, ch);
+ cc.getContext("2d").drawImage(doc, cx, cy, cw, ch, 0, 0, cw, ch);
return cc;
};
const newLayers = layers.map((L) => {
const next = { ...L };
- const bc = cropPlane(L.bitmap);
+ const lox = L.ox || 0, loy = L.oy || 0;
+ const bc = cropPlane(L.bitmap, lox, loy);
if (bc) next.bitmap = bc.getContext("2d").getImageData(0, 0, cw, ch);
- const mc = cropPlane(L.mask);
+ const mc = cropPlane(L.mask, lox, loy);
if (mc) { next.mask = mc.getContext("2d").getImageData(0, 0, cw, ch); next.maskUrl = mc.toDataURL(); }
+ // crop establishes a new coordinate system — offsets are now baked into pixels
+ next.ox = 0; next.oy = 0;
return next;
});
sourceUrl = newUrl; width = cw; height = ch;
@@ -2479,6 +2579,9 @@
saving = false;
errorMsg = "";
tool = "brush";
+ maskEdit = false;
+ groups = [];
+ renamingGroupId = null;
sourceUrl = srcUrlProp;
width = widthProp;
height = heightProp;
@@ -2522,9 +2625,12 @@
e.preventDefault();
e.stopPropagation();
redo();
- } else if ((e.ctrlKey || e.metaKey) && (e.key === "j" || e.key === "J") && selActive) {
+ } else if ((e.ctrlKey || e.metaKey) && (e.key === "j" || e.key === "J")) {
e.preventDefault(); e.stopPropagation();
- layerViaCopy(e.shiftKey); // Ctrl+J copy, Ctrl+Shift+J cut → new layer
+ if (selActive) layerViaCopy(e.shiftKey); // Ctrl+J copy / Ctrl+Shift+J cut → new layer
+ else if (!e.shiftKey) duplicateLayer(activeIndex); // no selection → duplicate the whole layer (PS)
+ } else if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === "c" || e.key === "C") && selActive) {
+ e.preventDefault(); e.stopPropagation(); copySelection(false, true); // Copy Merged (visible composite)
} else if ((e.ctrlKey || e.metaKey) && (e.key === "c" || e.key === "C") && selActive) {
e.preventDefault(); e.stopPropagation(); copySelection(false);
} else if ((e.ctrlKey || e.metaKey) && (e.key === "x" || e.key === "X") && selActive) {
@@ -2534,12 +2640,18 @@
} else if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === "n" || e.key === "N")) {
e.preventDefault(); e.stopPropagation();
addLayer(); // new layer above the active one
+ } else if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === "g" || e.key === "G")) {
+ e.preventDefault(); e.stopPropagation();
+ const g = groupById(layers[activeIndex]?.groupId); if (g) ungroup(g.id);
+ } else if ((e.ctrlKey || e.metaKey) && (e.key === "g" || e.key === "G")) {
+ e.preventDefault(); e.stopPropagation();
+ if (selectedIds.length) groupSelected();
} else if ((e.ctrlKey || e.metaKey) && (e.key === "a" || e.key === "A")) {
e.preventDefault(); e.stopPropagation();
- selectAllRegion(); // select whole canvas
+ selectAllRegion(); commitAction("Select All"); // select whole canvas
} else if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === "i" || e.key === "I") && selActive) {
e.preventDefault(); e.stopPropagation();
- selectInverse(); // Photoshop's Ctrl+Shift+I
+ selectInverse(); commitAction("Select Inverse"); // Photoshop's Ctrl+Shift+I
} else if (e.key === "Backspace" && (e.altKey || e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.stopPropagation();
fillWith(e.altKey ? rgb : bgRgb); // Alt+Backspace = foreground, Ctrl+Backspace = background
@@ -2551,7 +2663,31 @@
e.preventDefault(); e.stopPropagation();
deleteSelectedLayers();
} else if ((e.ctrlKey || e.metaKey) && (e.key === "d" || e.key === "D") && selActive) {
- e.preventDefault(); e.stopPropagation(); deselect();
+ e.preventDefault(); e.stopPropagation(); deselect(); commitAction("Deselect");
+ } else if ((e.ctrlKey || e.metaKey) && e.key === "0") {
+ e.preventDefault(); e.stopPropagation(); resetView(); // fit to window
+ } else if ((e.ctrlKey || e.metaKey) && (e.key === "=" || e.key === "+")) {
+ e.preventDefault(); e.stopPropagation(); zoomBy(1.25);
+ } else if ((e.ctrlKey || e.metaKey) && (e.key === "-" || e.key === "_")) {
+ e.preventDefault(); e.stopPropagation(); zoomBy(1 / 1.25);
+ } else if (!e.ctrlKey && !e.metaKey && !e.altKey && (e.key === "[" || e.key === "]")) {
+ e.preventDefault(); e.stopPropagation();
+ const dir = e.key === "]" ? 1 : -1; // PS brush-size step grows with size
+ const step = brushSize < 10 ? 1 : brushSize < 50 ? 2 : brushSize < 100 ? 5 : 10;
+ brushSize = Math.max(1, Math.min(1024, brushSize + dir * step));
+ } else if (!e.ctrlKey && !e.metaKey && !e.altKey && !colorPopout && (e.key === "x" || e.key === "X")) {
+ e.preventDefault(); e.stopPropagation();
+ const fg = rgb; [hue, sat, val] = rgbToHsv(bgRgb[0], bgRgb[1], bgRgb[2]); bgRgb = fg; // swap foreground/background
+ } else if (!e.ctrlKey && !e.metaKey && !e.altKey && !colorPopout && (e.key === "d" || e.key === "D")) {
+ e.preventDefault(); e.stopPropagation();
+ hue = 0; sat = 0; val = 0; bgRgb = [255, 255, 255]; // default colors: black foreground / white background
+ } else if (!e.ctrlKey && !e.metaKey && !e.altKey && !patch && !colorPopout && TOOL_KEYS[(e.key || "").toLowerCase()]) {
+ e.preventDefault(); e.stopPropagation();
+ setTool(TOOL_KEYS[e.key.toLowerCase()]); // single-key tool switch (setTool commits any float first)
+ } else if (e.key === " ") {
+ // Hold Space to pan (Hand tool). spaceDown is tracked by the mods listener;
+ // swallow the default so the page can't scroll or click a focused button.
+ e.preventDefault(); e.stopPropagation();
} else if ((e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight")
&& !e.ctrlKey && !e.metaKey && !e.altKey && !patch && !colorPopout && !healDialog && tool !== "crop") {
// A selection nudges its OUTLINE 1px (10px with Shift), PS-style; with no
@@ -2584,7 +2720,7 @@
e.stopPropagation();
if (colorPopout) cancelColorPop(); // Esc = cancel, reverts the color
else if (patch) cancelPatch(); // first Esc drops the patch, second closes
- else if (selActive) deselect(); // first Esc drops the selection
+ else if (selActive) { deselect(); commitAction("Deselect"); } // first Esc drops the selection
else if (tool === "crop" && cropBox && !cropBoxIsFull()) resetCropBox();
else close();
}
@@ -2736,6 +2872,18 @@
panY = (rect.height - height * zoom) / 2;
}
+ // Keyboard zoom (Ctrl +/-): scale around the viewport center, mirroring onWheel.
+ function zoomBy(factor) {
+ if (!viewportEl) return;
+ const rect = viewportEl.getBoundingClientRect();
+ const cx = rect.width / 2, cy = rect.height / 2;
+ const newZoom = Math.max(0.05, Math.min(12, zoom * factor));
+ panX = cx - ((cx - panX) / zoom) * newZoom;
+ panY = cy - ((cy - panY) / zoom) * newZoom;
+ zoom = newZoom;
+ fitMode = false;
+ }
+
function toImageCoords(e) {
const rect = viewportEl.getBoundingClientRect();
return {
@@ -2758,9 +2906,9 @@
}
// ── brush engine ──
- // The stroke accumulates in a FLOAT coverage buffer, and painted pixels are
- // written as OPAQUE final colors: brush×cov + background×(1−cov) computed in
- // float against the source image, dithered and rounded exactly once. Two
+ // The stroke accumulates in a FLOAT coverage buffer; painted pixels are written
+ // once in the active layer's OWN straight-alpha space (Porter-Duff per tool) and
+ // dithered/rounded a single time (no opaque bake of the backdrop). Two
// quantization mechanisms ring otherwise: (1) compositing dabs straight onto
// the 8-bit canvas rounds after EVERY dab, so a held airbrush converges each
// pixel to a quantized equilibrium terrace; (2) a semi-transparent paint
@@ -2911,7 +3059,8 @@
for (let x = 0; x < bw; x++) {
const i = y * bw + x;
const gi = (y0 + y) * width + x0 + x;
- const cov = Math.min(1, strokeCov[gi]);
+ let cov = Math.min(1, strokeCov[gi]);
+ if (selStrokeData) cov *= selStrokeData[gi * 4 + 3] / 255; // clip heal to the selection shape
const o = i * 4, j = gi * 4;
if (cov <= 0) {
px[o] = pd[j]; px[o + 1] = pd[j + 1]; px[o + 2] = pd[j + 2]; px[o + 3] = pd[j + 3];
@@ -2923,7 +3072,7 @@
const target = cov * healed + (1 - cov) * dest[i * 3 + ch];
px[o + ch] = Math.max(0, Math.min(255, Math.round(target + d)));
}
- px[o + 3] = 255;
+ px[o + 3] = pd[j + 3]; // heal re-textures; keep the layer's own alpha (no opaque bake)
}
}
paintCanvas.getContext("2d").putImageData(img, x0, y0);
@@ -3026,9 +3175,10 @@
// Re-derive the batch region from pre-stroke state + cumulative coverage —
// identical inputs render identical pixels (stable dither), so re-rendering
- // an already-painted region is idempotent. Touched pixels store the FINAL
- // display color, opaque; cov hits 0 exactly where the color reaches the
- // pre-stroke composite, so the cutoff to untouched pixels is seamless.
+ // an already-painted region is idempotent. Pixels are written in the ACTIVE
+ // layer's OWN straight-alpha space (Porter-Duff per tool), so transparency is
+ // preserved; cov ≤ 0 restores pd verbatim, so the cutoff to untouched pixels
+ // is seamless.
function renderStrokeBatch() {
if (!batchBox) return;
const { x0, y0, x1, y1 } = batchBox;
@@ -3045,7 +3195,6 @@
const img = new ImageData(bw, bh);
const px = img.data;
const pd = preStroke.data;
- const sd = sourceData.data;
for (let y = 0; y < bh; y++) {
const row = (y0 + y) * width + x0;
for (let x = 0; x < bw; x++) {
@@ -3065,24 +3214,33 @@
+ Math.max(0, Math.min(width - 1, x0 + x + odx))) * 4
: 0;
const spa = stampMode ? pd[sj + 3] / 255 : 0;
- // pre-stroke composite (paint over source) in float, then blend:
- // brush pulls toward the color, eraser pulls back toward the source,
- // stamp pulls toward the source-offset composite
+ // Work in the ACTIVE layer's OWN straight-alpha pixels (pd), never the
+ // backdrop, so a layer keeps its transparency and lower layers are not
+ // baked in. Porter-Duff source-over per tool; alpha is set per-pixel, not
+ // forced to 255. On a flat Background (pa = 1) this reduces to the old
+ // opaque blend. (Stamp samples the layer's own offset pixels.)
+ const se = spa * cov; // stamp: effective source coverage
+ const na = healMode ? pa // heal preview re-textures; alpha unchanged
+ : erase ? pa * (1 - cov) // eraser = destination-out on own alpha
+ : stampMode ? se + pa * (1 - se) // clone source-over
+ : cov + pa * (1 - cov); // brush source-over
+ const naInv = na > 0 ? 1 / na : 0;
for (let ch = 0; ch < 3; ch++) {
- const comp = pd[j + ch] * pa + sd[j + ch] * (1 - pa);
+ const lc = pd[j + ch]; // the layer's own pre-stroke color
const target = healMode
- ? comp * (1 - cov * 0.4) + 200 * cov * 0.4
- : stampMode
- ? (pd[sj + ch] * spa + sd[sj + ch] * (1 - spa)) * cov + comp * (1 - cov)
- : erase
- ? comp * (1 - cov) + sd[j + ch] * cov
- : (ch === 0 ? cr : ch === 1 ? cg : cb) * cov + comp * (1 - cov);
- px[i + ch] = Math.max(0, Math.min(255, Math.round(target + d)));
+ ? lc * (1 - cov * 0.4) + 200 * cov * 0.4
+ : erase
+ ? lc // RGB stays; alpha carries the erase
+ : stampMode
+ ? (pd[sj + ch] * se + lc * pa * (1 - se)) * naInv
+ : ((ch === 0 ? cr : ch === 1 ? cg : cb) * cov + lc * pa * (1 - cov)) * naInv;
+ px[i + ch] = erase ? lc : Math.max(0, Math.min(255, Math.round(target + d)));
}
- px[i + 3] = 255;
+ px[i + 3] = Math.max(0, Math.min(255, Math.round(na * 255)));
}
}
paintCanvas.getContext("2d").putImageData(img, x0, y0);
+ if (hasCustomBlend) customTick++; // refresh the single-canvas custom-blend composite mid-stroke
}
// Stamp every ~15% of radius along the segment; the residual carries across
@@ -3293,6 +3451,61 @@
// ── layer helpers ──
const activeLayer = () => layers[activeIndex] || layers[0];
+ // Lightweight layer groups: a layer's groupId points at a groups[] entry; the
+ // group's visibility/opacity fold into each member's effective render. Members
+ // are kept contiguous in the flat array so the panel can show them under a header.
+ const groupById = (gid) => (gid ? groups.find((g) => g.id === gid) : null);
+ const layerShown = (L) => !!L?.visible && groupById(L?.groupId)?.visible !== false;
+ const layerEffOpacity = (L) => (L?.opacity ?? 1) * (groupById(L?.groupId)?.opacity ?? 1);
+ // Panel display rows, top-to-bottom: a group header before its members; a
+ // collapsed group hides its member rows. Ungrouped layers pass through.
+ let panelRows = $derived.by(() => {
+ const rows = [];
+ const seen = new Set();
+ for (let i = layers.length - 1; i >= 0; i--) {
+ const L = layers[i];
+ const g = groupById(L.groupId);
+ if (g && !seen.has(g.id)) { seen.add(g.id); rows.push({ type: "group", group: g }); }
+ if (g && g.collapsed) continue;
+ rows.push({ type: "layer", L, i, inGroup: !!g });
+ }
+ return rows;
+ });
+ function groupSelected() {
+ const ids = selectedIds.filter((id) => { const L = layers.find((x) => x.id === id); return L && !L.isBackground; });
+ if (!ids.length) return;
+ flushActive();
+ const gid = ++nextGroupNum;
+ const members = layers.filter((L) => ids.includes(L.id)).map((L) => ({ ...L, groupId: gid }));
+ const rest = layers.filter((L) => !ids.includes(L.id));
+ const topSelIdx = Math.max(...layers.map((L, i) => (ids.includes(L.id) ? i : -1)));
+ let insertAt = 0;
+ for (let i = 0; i < topSelIdx; i++) if (!ids.includes(layers[i].id)) insertAt++;
+ const activeId = layers[activeIndex]?.id;
+ groups = [...groups, { id: gid, name: `Group ${gid}`, collapsed: false, visible: true, opacity: 1 }];
+ layers = [...rest.slice(0, insertAt), ...members, ...rest.slice(insertAt)];
+ activeIndex = Math.max(0, layers.findIndex((L) => L.id === activeId));
+ selectedIds = members.map((L) => L.id);
+ loadActiveBitmap();
+ commitAction("Group Layers");
+ }
+ function ungroup(gid) {
+ if (!groups.some((g) => g.id === gid)) return;
+ flushActive();
+ layers = layers.map((L) => (L.groupId === gid ? { ...L, groupId: undefined } : L));
+ groups = groups.filter((g) => g.id !== gid);
+ commitAction("Ungroup");
+ }
+ function toggleGroupCollapsed(gid) { groups = groups.map((g) => (g.id === gid ? { ...g, collapsed: !g.collapsed } : g)); } // view-only, no history
+ function setGroupVisible(gid, v) { groups = groups.map((g) => (g.id === gid ? { ...g, visible: v } : g)); commitAction("Group Visibility"); }
+ function setGroupOpacity(gid, o) { groups = groups.map((g) => (g.id === gid ? { ...g, opacity: o } : g)); } // commit on release (slider onchange)
+ function startGroupRename(g) { renamingGroupId = g.id; renameText = g.name; }
+ function commitGroupRename() {
+ if (renamingGroupId == null) return;
+ const gid = renamingGroupId; renamingGroupId = null;
+ const name = renameText.trim();
+ if (name) { groups = groups.map((g) => (g.id === gid ? { ...g, name } : g)); commitAction("Rename Group"); }
+ }
// Photoshop refuses to edit a hidden layer — you'd be changing pixels you
// can't see (the active layer's canvas is display:none while hidden). On a
@@ -3328,6 +3541,10 @@
// view); the paintCanvas itself stays raw so editing under the mask works.
function activeMaskedCanvas() {
const L = activeLayer();
+ // In mask-edit, paintCanvas holds the grayscale MASK, not the layer's pixels.
+ // Composite/flatten/save/merge must render from the stored bitmap+mask instead,
+ // or the gray would bake into the output. Callers flushActive() first, so L is current.
+ if (maskEdit && L?.mask) return maskedRenderCanvas(L);
if (!L?.mask) return paintCanvas;
const t = document.createElement("canvas");
t.width = width; t.height = height;
@@ -3406,6 +3623,18 @@
if (!paintCanvas || !layers[activeIndex]) return;
const L = layers[activeIndex];
const ox = L.ox || 0, oy = L.oy || 0;
+ if (maskEdit) {
+ if (!L.mask) return;
+ // Content-space gray = existing mask, with the on-canvas window replaced by
+ // the freshly-painted paintCanvas (un-offset). Mirrors the bitmap offset path.
+ const gray = document.createElement("canvas"); gray.width = width; gray.height = height;
+ const gx = gray.getContext("2d");
+ gx.drawImage(maskToGrayCanvas(L.mask), 0, 0);
+ gx.clearRect(-ox, -oy, width, height);
+ gx.drawImage(paintCanvas, -ox, -oy);
+ writeLayerMask(activeIndex, grayToMaskImageData(gx.getImageData(0, 0, width, height)));
+ return;
+ }
if (!ox && !oy) {
layers[activeIndex] = { ...L, bitmap: paintCanvas.getContext("2d").getImageData(0, 0, width, height) };
return;
@@ -3426,20 +3655,43 @@
function loadActiveBitmap() {
const ctx = paintCanvas?.getContext("2d");
if (!ctx) return;
- ctx.clearRect(0, 0, width, height);
const L = layers[activeIndex];
- if (L) drawLayerRaw(ctx, L, 0, 0, width, height, 0, 0, width, height);
+ if (maskEdit && !L?.mask) maskEdit = false; // active layer has no mask → leave mask-edit (safety net)
+ ctx.clearRect(0, 0, width, height);
+ if (!L) return;
+ if (maskEdit && L.mask) { ctx.drawImage(maskToGrayCanvas(L.mask), L.ox || 0, L.oy || 0); return; }
+ drawLayerRaw(ctx, L, 0, 0, width, height, 0, 0, width, height);
}
function selectLayer(i) {
if (i < 0 || i >= layers.length || painting) return;
- if (i !== activeIndex) { flushActive(); activeIndex = i; loadActiveBitmap(); }
+ if (i === activeIndex) return;
+ if (maskEdit) exitMaskEdit(); // clicking another layer targets its pixels, not a mask
+ // Resolve a live Free-Transform/patch float onto its OWN (still-current) layer
+ // before switching, matching Photoshop's auto-apply — otherwise the float would
+ // bake into the wrong layer and the source layer commits erased (data loss).
+ commitPatch();
+ flushActive();
+ activeIndex = i;
+ loadActiveBitmap();
}
// Click a layer row, honoring Ctrl (toggle into the multi-selection) and
// Shift (range). Plain click = single. The clicked layer becomes active.
function clickLayer(i, e) {
const id = layers[i].id;
if ((e.ctrlKey || e.metaKey) && selectedIds.length) {
- selectedIds = selectedIds.includes(id) ? selectedIds : [...selectedIds, id];
+ if (selectedIds.includes(id)) {
+ // Ctrl-click an already-selected layer toggles it OFF (PS) — but never empty
+ // the selection; if it was the active layer, hand active to a survivor.
+ if (selectedIds.length > 1) {
+ selectedIds = selectedIds.filter((x) => x !== id);
+ if (id === layers[activeIndex]?.id) {
+ const s = layers.findIndex((L) => selectedIds.includes(L.id));
+ if (s >= 0) selectLayer(s);
+ }
+ }
+ return; // the ctrl-clicked (now-deselected) layer does not become active
+ }
+ selectedIds = [...selectedIds, id];
} else if (e.shiftKey && selectedIds.length) {
const lo = Math.min(activeIndex, i), hi = Math.max(activeIndex, i);
selectedIds = layers.slice(lo, hi + 1).map((L) => L.id);
@@ -3456,11 +3708,12 @@
flushActive();
const id = ++nextLayerNum;
layers = [...layers.slice(0, at + 1),
- { id, name: `Layer ${id}`, visible: true, opacity: 1, bitmap: null },
+ { id, name: `Layer ${id}`, visible: true, opacity: 1, bitmap: null, groupId: layers[at]?.groupId },
...layers.slice(at + 1)];
activeIndex = at + 1;
selectedIds = [id];
loadActiveBitmap(); // empty
+ commitAction("New Layer"); // discrete undoable step; keeps history in sync with the live layers
}
// True when the multi-selection has at least one deletable (non-Background) layer.
function canDeleteSelected() {
@@ -3478,6 +3731,7 @@
for (let k = 0; k < activeIndex; k++) if (!doomedIds.has(layers[k].id)) na++; // survivors below the old active
const survivors = layers.filter((L) => !doomedIds.has(L.id));
layers = survivors;
+ groups = groups.filter((g) => layers.some((L) => L.groupId === g.id));
activeIndex = Math.min(na, survivors.length - 1);
selectedIds = [layers[activeIndex]?.id].filter((x) => x != null);
loadActiveBitmap();
@@ -3495,7 +3749,7 @@
bitmap = c.getContext("2d").getImageData(0, 0, width, height);
}
layers = [...layers.slice(0, i + 1),
- { id, name: `${src.name} copy`, visible: true, opacity: src.opacity, blend: src.blend, ox: src.ox, oy: src.oy, mask: src.mask, maskUrl: src.maskUrl, sourceRect: src.sourceRect, bitmap },
+ { id, name: `${src.name} copy`, visible: true, opacity: src.opacity, blend: src.blend, ox: src.ox, oy: src.oy, mask: src.mask, maskUrl: src.maskUrl, sourceRect: src.sourceRect, groupId: src.groupId, bitmap },
...layers.slice(i + 1)];
activeIndex = i + 1;
selectedIds = [id];
@@ -3516,11 +3770,44 @@
["normal", "Normal"], ["multiply", "Multiply"], ["screen", "Screen"],
["overlay", "Overlay"], ["darken", "Darken"], ["lighten", "Lighten"],
["color-dodge", "Color Dodge"], ["color-burn", "Color Burn"],
+ ["linear-dodge", "Linear Dodge (Add)"], ["subtract", "Subtract"], ["divide", "Divide"],
["hard-light", "Hard Light"], ["soft-light", "Soft Light"],
+ ["vivid-light", "Vivid Light"], ["linear-light", "Linear Light"], ["pin-light", "Pin Light"], ["hard-mix", "Hard Mix"],
["difference", "Difference"], ["exclusion", "Exclusion"],
["hue", "Hue"], ["saturation", "Saturation"], ["color", "Color"], ["luminosity", "Luminosity"],
+ ["darker-color", "Darker Color"], ["lighter-color", "Lighter Color"],
];
- const layerBlendCss = (L) => (L?.blend && L.blend !== "normal" ? L.blend : null);
+ // Modes the browser supports in BOTH CSS mix-blend-mode and canvas
+ // globalCompositeOperation render via the fast DOM-stacked path. The rest
+ // (Photoshop modes the web APIs lack) need a hand-written per-pixel blend and
+ // are composited through a single canvas (see drawFlattened / the customEl effect).
+ const CUSTOM_BLENDS = new Set(["linear-dodge", "subtract", "divide", "vivid-light", "linear-light", "pin-light", "hard-mix", "darker-color", "lighter-color"]);
+ const isCustomBlend = (b) => CUSTOM_BLENDS.has(b);
+ // Custom path is active only when a VISIBLE layer actually uses a custom mode —
+ // native-mode editing keeps the fast DOM-stacked compositing untouched.
+ let hasCustomBlend = $derived(layers.some((L) => layerShown(L) && isCustomBlend(L.blend)));
+ let customTick = $state(0); // bumped after a paint dab so the single-canvas composite refreshes live
+ // Per-channel blend math (b = base, s = source, 0..1). darker/lighter-color are
+ // non-separable and handled in the pixel loop.
+ function blendChannel(mode, b, s) {
+ switch (mode) {
+ case "linear-dodge": return Math.min(1, b + s);
+ case "subtract": return Math.max(0, b - s);
+ case "divide": return s <= 0 ? 1 : Math.min(1, b / s);
+ case "linear-light": return Math.max(0, Math.min(1, b + 2 * s - 1));
+ case "pin-light": return s <= 0.5 ? Math.min(b, 2 * s) : Math.max(b, 2 * s - 1);
+ case "vivid-light":
+ if (s <= 0.5) { const d = 2 * s; return d <= 0 ? 0 : Math.max(0, 1 - (1 - b) / d); }
+ { const d = 2 * (1 - s); return d <= 0 ? 1 : Math.min(1, b / d); }
+ case "hard-mix": {
+ let v; if (s <= 0.5) { const d = 2 * s; v = d <= 0 ? 0 : Math.max(0, 1 - (1 - b) / d); }
+ else { const d = 2 * (1 - s); v = d <= 0 ? 1 : Math.min(1, b / d); }
+ return v < 0.5 ? 0 : 1;
+ }
+ default: return s;
+ }
+ }
+ const layerBlendCss = (L) => (L?.blend && L.blend !== "normal" && !isCustomBlend(L.blend) ? L.blend : null);
function setLayerBlend(i, blend) {
layers[i] = { ...layers[i], blend: blend === "normal" ? undefined : blend };
commitAction("Blend Mode");
@@ -3565,10 +3852,75 @@
}
function deleteLayerMask(i) {
if (!layers[i]?.mask) return;
+ if (maskEdit && i === activeIndex) maskEdit = false;
layers[i] = { ...layers[i], mask: undefined, maskUrl: undefined };
+ if (i === activeIndex) loadActiveBitmap();
commitAction("Delete Layer Mask");
}
+ // ── editing a layer mask by painting (#3) ──
+ // The mask is stored white-RGB / alpha=visibility. To reuse the whole brush
+ // engine, mask-edit mode shows the mask on paintCanvas as opaque GRAYSCALE
+ // (white = revealed) and routes flushActive/loadActiveBitmap to the mask; the
+ // brush/eraser/fill/smooth then "just work" (paint white to reveal, black to
+ // hide). Conversions are content-space; the doc-space offset rides ox/oy.
+ function maskToGrayCanvas(mask) {
+ const c = document.createElement("canvas"); c.width = width; c.height = height;
+ const ctx = c.getContext("2d");
+ if (mask) {
+ const g = new ImageData(width, height), d = g.data, m = mask.data;
+ for (let i = 0; i < d.length; i += 4) { const v = m[i + 3]; d[i] = v; d[i + 1] = v; d[i + 2] = v; d[i + 3] = 255; }
+ ctx.putImageData(g, 0, 0);
+ } else { ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, width, height); }
+ return c;
+ }
+ function grayToMaskImageData(gray) {
+ const s = gray.data, out = new ImageData(width, height), d = out.data;
+ for (let i = 0; i < s.length; i += 4) {
+ const lum = s[i] * 0.299 + s[i + 1] * 0.587 + s[i + 2] * 0.114;
+ d[i] = 255; d[i + 1] = 255; d[i + 2] = 255;
+ d[i + 3] = Math.round(lum * (s[i + 3] / 255)); // erased (alpha 0) → hidden; white → revealed
+ }
+ return out;
+ }
+ function writeLayerMask(i, maskId) {
+ const mc = document.createElement("canvas"); mc.width = width; mc.height = height;
+ mc.getContext("2d").putImageData(maskId, 0, 0);
+ layers[i] = { ...layers[i], mask: maskId, maskUrl: mc.toDataURL() };
+ }
+ function enterMaskEdit(i = activeIndex) {
+ if (i !== activeIndex) selectLayer(i);
+ if (!layers[activeIndex]?.mask || maskEdit || painting) return;
+ flushActive(); // maskEdit false here → saves the layer's pixels
+ maskEdit = true;
+ hue = 0; sat = 0; val = 1; // default to white = reveal (X/D still work)
+ loadActiveBitmap(); // maskEdit true → loads the mask as grayscale
+ }
+ function exitMaskEdit() {
+ if (!maskEdit) return;
+ flushActive(); // maskEdit true → saves the grayscale back as the mask
+ maskEdit = false;
+ loadActiveBitmap(); // loads the layer's pixels
+ }
+ function toggleMaskEdit(i) { if (maskEdit && i === activeIndex) exitMaskEdit(); else enterMaskEdit(i); }
+ function invertLayerMask(i) {
+ if (!layers[i]?.mask) return;
+ if (maskEdit && i === activeIndex) flushActive(); // pull live paint into the mask first
+ const m = layers[i].mask, inv = new ImageData(width, height), d = inv.data, s = m.data;
+ for (let k = 0; k < d.length; k += 4) { d[k] = 255; d[k + 1] = 255; d[k + 2] = 255; d[k + 3] = 255 - s[k + 3]; }
+ writeLayerMask(i, inv);
+ if (maskEdit && i === activeIndex) loadActiveBitmap();
+ commitAction("Invert Mask");
+ }
+ function fillLayerMask(i, vis) { // vis 255 = Reveal All, 0 = Hide All
+ if (!layers[i]?.mask) return;
+ const fill = new ImageData(width, height), d = fill.data;
+ for (let k = 0; k < d.length; k += 4) { d[k] = 255; d[k + 1] = 255; d[k + 2] = 255; d[k + 3] = vis; }
+ writeLayerMask(i, fill);
+ if (maskEdit && i === activeIndex) loadActiveBitmap();
+ commitAction(vis ? "Reveal All" : "Hide All");
+ }
+
function startRename(L) { renamingId = L.id; renameText = L.name; }
function commitRename() {
const i = layers.findIndex((L) => L.id === renamingId);
@@ -3592,13 +3944,58 @@
// topmost row lands at the very top.
let at = ti < 0 ? rest.length : ti + 1;
if (at < 1) at = 1; // never below Background
- rest.splice(at, 0, moved);
+ // Dropping onto a row inside a group joins that group; otherwise the layer
+ // leaves any group it was in. Then drop any group left with no members.
+ const targetGroupId = rest[ti]?.groupId;
+ rest.splice(at, 0, { ...moved, groupId: targetGroupId });
layers = rest;
+ groups = groups.filter((g) => layers.some((L) => L.groupId === g.id));
activeIndex = layers.findIndex((L) => L.id === activeId);
dragId = null;
commitAction("Reorder Layers");
}
+ // Hand-written per-pixel composite of srcCanvas onto ctx using a Photoshop
+ // blend the web APIs lack (CSS mix-blend-mode + canvas globalCompositeOperation
+ // both miss these). Shared by every full-frame composite path so live preview,
+ // merge, flatten and export all blend identically. Uses the W3C
+ // source-over-with-blend formula (Cs' = (1-αb)·Cs + αb·B(Cb,Cs)) so a custom
+ // layer over a transparent/partial backdrop shows the source, not black.
+ function blendLayerFull(ctx, srcCanvas, mode, eff) {
+ const baseImg = ctx.getImageData(0, 0, width, height);
+ const srcImg = srcCanvas.getContext("2d").getImageData(0, 0, width, height);
+ const bd = baseImg.data, sd = srcImg.data;
+ const sep = mode !== "darker-color" && mode !== "lighter-color";
+ for (let p = 0; p < bd.length; p += 4) {
+ const sa = (sd[p + 3] / 255) * eff;
+ if (sa <= 0) continue;
+ const br = bd[p] / 255, bg = bd[p + 1] / 255, bb = bd[p + 2] / 255;
+ const sr = sd[p] / 255, sg = sd[p + 1] / 255, sbb = sd[p + 2] / 255;
+ let rr, rg, rb;
+ if (sep) { rr = blendChannel(mode, br, sr); rg = blendChannel(mode, bg, sg); rb = blendChannel(mode, bb, sbb); }
+ else {
+ const lb = 0.299 * br + 0.587 * bg + 0.114 * bb, ls = 0.299 * sr + 0.587 * sg + 0.114 * sbb;
+ const keepBase = mode === "darker-color" ? lb <= ls : lb >= ls;
+ rr = keepBase ? br : sr; rg = keepBase ? bg : sg; rb = keepBase ? bb : sbb;
+ }
+ const ba = bd[p + 3] / 255, oa = sa + ba * (1 - sa), inv = oa > 0 ? 1 / oa : 0;
+ const cr = (1 - ba) * sr + ba * rr, cg = (1 - ba) * sg + ba * rg, cb2 = (1 - ba) * sbb + ba * rb;
+ bd[p] = Math.round((cr * sa + br * ba * (1 - sa)) * inv * 255);
+ bd[p + 1] = Math.round((cg * sa + bg * ba * (1 - sa)) * inv * 255);
+ bd[p + 2] = Math.round((cb2 * sa + bb * ba * (1 - sa)) * inv * 255);
+ bd[p + 3] = Math.round(oa * 255);
+ }
+ ctx.putImageData(baseImg, 0, 0);
+ }
+ // Render the active or an inactive layer's full-frame pixels onto a fresh
+ // canvas (active rides the live paintCanvas + mask). Source for blendLayerFull.
+ function layerFullCanvas(L, isActive) {
+ const tmp = document.createElement("canvas");
+ tmp.width = width; tmp.height = height;
+ if (isActive) tmp.getContext("2d").drawImage(activeMaskedCanvas(), 0, 0);
+ else drawLayerContent(tmp.getContext("2d"), L, 0, 0, width, height, 0, 0, width, height);
+ return tmp;
+ }
// Composite a set of layer indices (z-order) into one ImageData.
function compositeLayers(idxs) {
const c = document.createElement("canvas");
@@ -3606,8 +4003,13 @@
const ctx = c.getContext("2d");
for (const i of idxs) {
const L = layers[i];
- if (!L.visible) continue;
- ctx.globalAlpha = L.opacity;
+ if (!layerShown(L)) continue;
+ const eff = layerEffOpacity(L);
+ if (isCustomBlend(L.blend)) {
+ blendLayerFull(ctx, layerFullCanvas(L, i === activeIndex), L.blend, eff);
+ continue;
+ }
+ ctx.globalAlpha = eff;
ctx.globalCompositeOperation = layerBlendCss(L) || "source-over";
if (i === activeIndex) ctx.drawImage(activeMaskedCanvas(), 0, 0);
else drawLayerContent(ctx, L, 0, 0, width, height, 0, 0, width, height);
@@ -3626,12 +4028,15 @@
const lo = idxs[0];
const hasBg = idxs.some((i) => layers[i].isBackground);
const bitmap = compositeLayers(idxs);
+ // Keep the merged layer in its group when all merged layers shared one.
+ const grp = !hasBg && layers[lo].groupId && idxs.every((i) => layers[i].groupId === layers[lo].groupId) ? layers[lo].groupId : undefined;
const merged = { id: hasBg ? 0 : ++nextLayerNum, name: hasBg ? "Background" : layers[lo].name,
- visible: true, opacity: 1, bitmap, isBackground: hasBg || undefined };
+ visible: true, opacity: 1, bitmap, isBackground: hasBg || undefined, groupId: grp };
const keptBelow = layers.filter((L, i) => i < lo && !idxs.includes(i)).length;
const rest = layers.filter((_, i) => !idxs.includes(i));
rest.splice(keptBelow, 0, merged);
layers = rest;
+ groups = groups.filter((g) => layers.some((L) => L.groupId === g.id));
activeIndex = layers.findIndex((L) => L.id === merged.id);
selectedIds = [merged.id];
loadActiveBitmap();
@@ -3649,6 +4054,7 @@
c.width = width; c.height = height;
drawFlattened(c.getContext("2d"), 0, 0, width, height, 0, 0, width, height);
layers = [{ id: 0, name: "Background", visible: true, opacity: 1, bitmap: c.getContext("2d").getImageData(0, 0, width, height), isBackground: true }];
+ groups = [];
activeIndex = 0; selectedIds = [0];
loadActiveBitmap();
commitAction("Flatten Image");
@@ -3669,8 +4075,8 @@
function drawBackdrop(ctx, sx, sy, sw, sh, dx, dy, dw, dh) {
for (let k = 0; k < activeIndex; k++) {
const L = layers[k];
- if (!L.visible) continue;
- ctx.globalAlpha = L.opacity;
+ if (!layerShown(L)) continue;
+ ctx.globalAlpha = layerEffOpacity(L);
ctx.globalCompositeOperation = layerBlendCss(L) || "source-over";
drawLayerContent(ctx, L, sx, sy, sw, sh, dx, dy, dw, dh);
ctx.globalAlpha = 1;
@@ -3680,10 +4086,27 @@
// Draw the FULL flattened composite (Background + every visible layer, active
// from the live canvas) — used by the eyedropper so it samples what's shown.
function drawFlattened(ctx, sx, sy, sw, sh, dx, dy, dw, dh) {
+ const fullFrame = sx === 0 && sy === 0 && dx === 0 && dy === 0 && sw === width && sh === height && dw === width && dh === height;
+ // Custom blend modes only resolve in a whole-canvas composite (blendLayerFull works on the full
+ // pixel buffer). For sub-rect requests — crop/upscale/i2i/Copy-Merged/eyedropper handoffs — flatten
+ // the entire doc once (the fullFrame branch routes custom layers through blendLayerFull), then copy
+ // the requested window out, so exported/handed-off output matches the on-screen preview.
+ if (!fullFrame && layers.some((L) => layerShown(L) && isCustomBlend(L.blend))) {
+ const full = document.createElement("canvas");
+ full.width = width; full.height = height;
+ drawFlattened(full.getContext("2d"), 0, 0, width, height, 0, 0, width, height);
+ ctx.drawImage(full, sx, sy, sw, sh, dx, dy, dw, dh);
+ return;
+ }
for (let k = 0; k < layers.length; k++) {
const L = layers[k];
- if (!L.visible) continue;
- ctx.globalAlpha = L.opacity;
+ if (!layerShown(L)) continue;
+ const eff = layerEffOpacity(L);
+ if (fullFrame && isCustomBlend(L.blend)) {
+ blendLayerFull(ctx, layerFullCanvas(L, k === activeIndex), L.blend, eff);
+ continue;
+ }
+ ctx.globalAlpha = eff;
ctx.globalCompositeOperation = layerBlendCss(L) || "source-over";
if (k === activeIndex) ctx.drawImage(activeMaskedCanvas(), sx, sy, sw, sh, dx, dy, dw, dh);
else drawLayerContent(ctx, L, sx, sy, sw, sh, dx, dy, dw, dh);
@@ -3729,6 +4152,20 @@
}
});
+ // Single-canvas composite for custom (non-CSS) blend modes. Only renders when
+ // such a mode is in use (else the DOM-stacked path above shows the image). It
+ // depends on layers/groups (reactive) and customTick (bumped after each paint
+ // dab, since painting writes paintCanvas imperatively, not reactively).
+ $effect(() => {
+ customTick; imgLoadTick;
+ if (!hasCustomBlend || !customEl) return;
+ if (customEl.width !== width) customEl.width = width;
+ if (customEl.height !== height) customEl.height = height;
+ const ctx = customEl.getContext("2d");
+ ctx.clearRect(0, 0, width, height);
+ drawFlattened(ctx, 0, 0, width, height, 0, 0, width, height);
+ });
+
function clearAll() {
if (histIndex === 0 && history.length === 1) return;
patch = null;
@@ -3752,7 +4189,12 @@
}
try {
sampleCtx.clearRect(0, 0, s, s);
- drawFlattened(sampleCtx, x, y, s, s, 0, 0, s, s);
+ // Merged sample = the on-screen composite. With a custom blend live, customEl already
+ // holds the full-frame composite — read that window directly instead of recompositing
+ // the whole doc per pointer-move (drawFlattened's sub-rect path would do that).
+ if (sampleMerged && hasCustomBlend && customEl) sampleCtx.drawImage(customEl, x, y, s, s, 0, 0, s, s);
+ else if (sampleMerged) drawFlattened(sampleCtx, x, y, s, s, 0, 0, s, s);
+ else sampleCtx.drawImage(paintCanvas, x, y, s, s, 0, 0, s, s); // current layer only
const d = sampleCtx.getImageData(0, 0, s, s).data;
const sum = [0, 0, 0];
for (let i = 0; i < s * s; i++) {
@@ -3786,7 +4228,7 @@
// ── pointer handling ──
function onPointerDown(e) {
if (e.button > 2) return;
- if (e.button === 1 || e.button === 2 || (e.shiftKey && !isSelTool && !transforming)) { // Shift adds to a selection / snaps a transform rotation — doesn't pan
+ if (e.button === 1 || e.button === 2 || (spaceDown && e.button === 0) || (e.shiftKey && !isSelTool && !transforming)) { // middle/right or Space-drag pans; Shift adds to a selection / snaps a transform rotation
panning = true;
lastPt = { x: e.clientX, y: e.clientY };
e.preventDefault();
@@ -4025,11 +4467,14 @@
}
if (selecting) {
selecting = false;
+ const had = selActive;
// Compose the marquee into the selection mask (replace / add / subtract).
if (selRect && selRect.w >= 1 && selRect.h >= 1) {
applySelectionShape({ type: "rect", x: selRect.x, y: selRect.y, w: selRect.w, h: selRect.h }, selOp);
+ commitAction("Select");
} else if (selOp === "replace") {
deselect(); // a click with no drag clears the selection
+ if (had) commitAction("Deselect");
}
selRect = null;
return;
@@ -4037,8 +4482,9 @@
if (lassoing) {
lassoing = false;
if (tool === "lasso") {
- if (lassoPts && lassoPts.length >= 3) applySelectionShape({ type: "lasso", pts: lassoPts }, selOp);
- else if (selOp === "replace") deselect();
+ const had = selActive;
+ if (lassoPts && lassoPts.length >= 3) { applySelectionShape({ type: "lasso", pts: lassoPts }, selOp); commitAction("Select"); }
+ else if (selOp === "replace") { deselect(); if (had) commitAction("Deselect"); }
} else if (lassoPts && lassoPts.length >= 3) {
applySmooth(lassoPts); // smooth tool still applies immediately
}
@@ -4389,6 +4835,13 @@
5×5 average
+
+ From
+
+ All layers
+ Current layer
+
+
click the image to pick a color · hold to preview · Screen pick grabs from anywhere
{/if}
@@ -4458,7 +4911,7 @@
class:transform-mode={transforming}
class:no-edit={hiddenEditBlocked}
class:drag-over={dragActive}
- style:cursor={transforming && overRotateZone ? ROTATE_CURSOR : null}
+ style:cursor={spaceDown ? "grab" : (transforming && overRotateZone ? ROTATE_CURSOR : null)}
bind:this={viewportEl}
ondragover={onCanvasDragOver}
ondragleave={() => { dragActive = false; }}
@@ -4474,6 +4927,14 @@
ondragstart={(e) => e.preventDefault()}
onselectstart={(e) => e.preventDefault()}
>
+ {#if maskEdit}
+
+ e.stopPropagation()}>
+ ✎ Editing layer mask — paint white to reveal, black to hide
+ Done
+
+ {/if}
-
+
+ A layer mask applies as a CSS mask so the canvas pixels stay raw.
+ Hidden (but still painted on) when the custom-blend composite is shown. -->
{#each layers as L, i (L.id)}
{#if i > activeIndex}
{/if}
{/each}
+
+
{#if (tool === "heal" || tool === "stamp") && healSource && !healPaintingUi}
@@ -4704,48 +5168,83 @@
{/each}
- {#each [...layers].reverse() as L (L.id)}
- {@const i = layers.indexOf(L)}
-
-
-
clickLayer(i, e)}
- oncontextmenu={(e) => { e.preventDefault(); if (!selectedIds.includes(L.id)) clickLayer(i, e); layerMenu = { x: e.clientX, y: e.clientY, i }; }}
- ondragover={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; dragOverId = L.id; }}
- ondragleave={() => { if (dragOverId === L.id) dragOverId = null; }}
- ondrop={(e) => { e.preventDefault(); dropLayer(L.id); }}>
- { dragId = L.id; e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", String(L.id)); }}
- ondragend={() => { dragId = null; dragOverId = null; }}>⠿
- { e.stopPropagation(); setLayerVisible(i, !L.visible); }}>{L.visible ? "👁" : "—"}
- {#if renamingId === L.id}
-
- e.stopPropagation()}
- onkeydown={(e) => {
- e.stopPropagation();
- if (e.key === "Enter") commitRename();
- else if (e.key === "Escape") renamingId = null;
- }}
- onblur={commitRename} />
- {:else}
- { e.stopPropagation(); startRename(L); }}>{L.name}{#if L.mask}▦ {/if}{#if layerBlendCss(L)}{L.blend} {/if}
- {/if}
- { opacityDragStart = L.opacity; }}
- oninput={(e) => setLayerOpacity(i, +e.currentTarget.value)}
- onchange={(e) => { if (opacityDragStart === null || +e.currentTarget.value !== opacityDragStart) commitAction("Opacity"); opacityDragStart = null; }}
- onclick={(e) => e.stopPropagation()} title="Opacity" />
-
+ {#each panelRows as row (row.type === "group" ? "g" + row.group.id : row.L.id)}
+ {#if row.type === "group"}
+ {@const g = row.group}
+
+
+ toggleGroupCollapsed(g.id)}>{g.collapsed ? "▸" : "▾"}
+ setGroupVisible(g.id, !g.visible)}>{g.visible ? "👁" : "—"}
+ {#if renamingGroupId === g.id}
+
+ e.stopPropagation()}
+ onkeydown={(e) => { e.stopPropagation(); if (e.key === "Enter") commitGroupRename(); else if (e.key === "Escape") renamingGroupId = null; }}
+ onblur={commitGroupRename} />
+ {:else}
+ { e.stopPropagation(); startGroupRename(g); }}>📁 {g.name}
+ {/if}
+ { opacityDragStart = g.opacity; }}
+ oninput={(e) => setGroupOpacity(g.id, +e.currentTarget.value)}
+ onchange={(e) => { if (opacityDragStart === null || +e.currentTarget.value !== opacityDragStart) commitAction("Group Opacity"); opacityDragStart = null; }}
+ onclick={(e) => e.stopPropagation()} title="Group opacity" />
+ ungroup(g.id)}>⊟
+
+ {:else}
+ {@const L = row.L}
+ {@const i = row.i}
+
+
+
clickLayer(i, e)}
+ oncontextmenu={(e) => { e.preventDefault(); if (!selectedIds.includes(L.id)) clickLayer(i, e); layerMenu = { x: e.clientX, y: e.clientY, i, flip: e.clientX > window.innerWidth - 340 }; }}
+ ondragover={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; dragOverId = L.id; }}
+ ondragleave={() => { if (dragOverId === L.id) dragOverId = null; }}
+ ondrop={(e) => { e.preventDefault(); dropLayer(L.id); }}>
+
{ dragId = L.id; e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", String(L.id)); }}
+ ondragend={() => { dragId = null; dragOverId = null; }}>⠿
+
{ e.stopPropagation(); setLayerVisible(i, !L.visible); }}>{L.visible ? "👁" : "—"}
+ {#if L.mask}
+
+
{ e.stopPropagation(); toggleMaskEdit(i); }} />
+ {/if}
+ {#if renamingId === L.id}
+
+
e.stopPropagation()}
+ onkeydown={(e) => {
+ e.stopPropagation();
+ if (e.key === "Enter") commitRename();
+ else if (e.key === "Escape") renamingId = null;
+ }}
+ onblur={commitRename} />
+ {:else}
+
{ e.stopPropagation(); startRename(L); }}>{L.name}{#if L.blend && L.blend !== "normal"}{L.blend} {/if}
+ {/if}
+
{ opacityDragStart = L.opacity; }}
+ oninput={(e) => setLayerOpacity(i, +e.currentTarget.value)}
+ onchange={(e) => { if (opacityDragStart === null || +e.currentTarget.value !== opacityDragStart) commitAction("Opacity"); opacityDragStart = null; }}
+ onclick={(e) => e.stopPropagation()} title="Opacity" />
+
+ {/if}
{/each}
{:else}
@@ -4760,23 +5259,19 @@
{#if layerMenu}
+ {@const Lm = layers[layerMenu.i]}
+ {@const multi = selectedIds.length > 1}
+ {@const groupable = selectedIds.filter((id) => !layers.find((ly) => ly.id === id)?.isBackground).length}
{/if}
@@ -4795,11 +5326,11 @@